"""SRS §4.1.7 "Create Implementation Plan" + "Generate Results Framework".

Covers:
- ImplementationPlan Draft -> Submitted -> Approved -> RevisionRequired cycle
- generate_results_framework() derives a mel_indicators.LogFrame with the
  correct Impact -> Outcome -> Output -> Activity hierarchy and one
  Indicator + IndicatorTarget per ImplementationPlanIndicator
- approve_implementation_plan() triggers generation as one atomic step
- generation is idempotent (calling it twice doesn't create a second LogFrame)
"""
from datetime import date, timedelta
from decimal import Decimal

import pytest
from django.core.exceptions import ValidationError
from django.utils import timezone
from django_fsm import TransitionNotAllowed

from apps.mel.indicators.models import LogFrameLevel
from apps.rims.grants.models import (
    Application,
    GrantCall,
    ImplementationPlan,
    ImplementationPlanActivity,
    ImplementationPlanIndicator,
    ImplementationPlanOutcome,
    ImplementationPlanOutput,
)
from apps.rims.grants.services import (
    approve_implementation_plan,
    award_application,
    generate_results_framework,
    shortlist_application,
    submit_application,
)


def _award_for(applicant_user, institution, *, suffix="ip"):
    call = GrantCall.objects.create(
        title="Implementation plan call",
        slug=f"ip-{suffix}-{int(timezone.now().timestamp())}",
        opens_at=timezone.now() - timedelta(days=1),
        closes_at=timezone.now() + timedelta(days=10),
        status=GrantCall.Status.PUBLISHED,
    )
    app = Application.objects.create(call=call, applicant=applicant_user, institution=institution)
    submit_application(app)
    app = Application.objects.get(pk=app.pk)
    shortlist_application(app)
    award_application(app, Decimal("1000"), date.today() + timedelta(days=180), narrative="")
    return Application.objects.get(pk=app.pk).award


def _plan_with_full_hierarchy(award):
    plan = ImplementationPlan.objects.create(award=award, objective="Improve smallholder yields")
    outcome = ImplementationPlanOutcome.objects.create(plan=plan, title="Farmers adopt improved practices")
    ImplementationPlanIndicator.objects.create(
        outcome=outcome, name="% farmers adopting practices",
        baseline_value=Decimal("10"), target_value=Decimal("60"),
        means_of_verification=ImplementationPlanIndicator.MeansOfVerification.MONITORING_REPORT,
        reporting_frequency=ImplementationPlanIndicator.ReportingFrequency.QUARTERLY,
    )
    output = ImplementationPlanOutput.objects.create(outcome=outcome, title="Farmers trained")
    ImplementationPlanIndicator.objects.create(
        output=output, name="Number of farmers trained",
        baseline_value=Decimal("0"), target_value=Decimal("500"),
        means_of_verification=ImplementationPlanIndicator.MeansOfVerification.ATTENDANCE_REGISTER,
        reporting_frequency=ImplementationPlanIndicator.ReportingFrequency.MONTHLY,
    )
    ImplementationPlanActivity.objects.create(output=output, title="Conduct training sessions")
    ImplementationPlanActivity.objects.create(output=output, title="Distribute training materials")
    return plan


@pytest.mark.django_db
def test_plan_fsm_cycle(applicant_user, institution):
    award = _award_for(applicant_user, institution, suffix="fsm")
    plan = ImplementationPlan.objects.create(award=award, objective="Objective")
    assert plan.status == ImplementationPlan.Status.DRAFT

    plan.submit()
    plan.save()
    assert plan.status == ImplementationPlan.Status.SUBMITTED

    plan.request_revision()
    plan.save()
    assert plan.status == ImplementationPlan.Status.REVISION_REQUIRED

    plan.resubmit()
    plan.save()
    assert plan.status == ImplementationPlan.Status.SUBMITTED

    with pytest.raises(TransitionNotAllowed):
        plan.submit()


@pytest.mark.django_db
def test_indicator_requires_exactly_one_parent(applicant_user, institution):
    award = _award_for(applicant_user, institution, suffix="indicator")
    plan = ImplementationPlan.objects.create(award=award, objective="Objective")
    outcome = ImplementationPlanOutcome.objects.create(plan=plan, title="Outcome")
    output = ImplementationPlanOutput.objects.create(outcome=outcome, title="Output")

    neither = ImplementationPlanIndicator(
        name="Bad", means_of_verification=ImplementationPlanIndicator.MeansOfVerification.REPORT,
        reporting_frequency=ImplementationPlanIndicator.ReportingFrequency.ANNUAL,
    )
    with pytest.raises(ValidationError):
        neither.full_clean()

    both = ImplementationPlanIndicator(
        outcome=outcome, output=output, name="Bad2",
        means_of_verification=ImplementationPlanIndicator.MeansOfVerification.REPORT,
        reporting_frequency=ImplementationPlanIndicator.ReportingFrequency.ANNUAL,
    )
    with pytest.raises(ValidationError):
        both.full_clean()


@pytest.mark.django_db
def test_generate_results_framework_builds_hierarchy(applicant_user, institution):
    award = _award_for(applicant_user, institution, suffix="rf")
    plan = _plan_with_full_hierarchy(award)
    plan.submit()
    plan.save()
    plan.approve()
    plan.approved_at = timezone.now()
    plan.save()

    logframe = generate_results_framework(plan)
    # ImplementationPlan.status is a protected FSMField — refresh_from_db()
    # can't touch it, so re-fetch instead.
    plan = ImplementationPlan.objects.get(pk=plan.pk)
    assert plan.results_framework_id == logframe.pk

    rows = list(logframe.rows.all())
    levels = sorted(r.level for r in rows)
    assert levels.count(LogFrameLevel.IMPACT) == 1
    assert levels.count(LogFrameLevel.OUTCOME) == 1
    assert levels.count(LogFrameLevel.OUTPUT) == 1
    assert levels.count(LogFrameLevel.ACTIVITY) == 2

    outcome = plan.outcomes.first()
    output = outcome.outputs.first()
    for plan_indicator in list(outcome.indicators.all()) + list(output.indicators.all()):
        assert plan_indicator.mel_indicator_id is not None
        target = plan_indicator.mel_indicator.targets.first()
        assert target.baseline_value == plan_indicator.baseline_value
        assert target.target_value == plan_indicator.target_value

    for activity in output.activities.all():
        assert activity.mel_logframe_row_id is not None
        assert activity.mel_logframe_row.level == LogFrameLevel.ACTIVITY


@pytest.mark.django_db
def test_generate_results_framework_is_idempotent(applicant_user, institution):
    award = _award_for(applicant_user, institution, suffix="idem")
    plan = _plan_with_full_hierarchy(award)
    plan.submit()
    plan.save()
    plan.approve()
    plan.approved_at = timezone.now()
    plan.save()

    first = generate_results_framework(plan)
    second = generate_results_framework(plan)
    assert first.pk == second.pk


@pytest.mark.django_db
def test_generate_results_framework_requires_approved_status(applicant_user, institution):
    award = _award_for(applicant_user, institution, suffix="notapproved")
    plan = _plan_with_full_hierarchy(award)
    with pytest.raises(ValidationError):
        generate_results_framework(plan)


@pytest.mark.django_db
def test_approve_implementation_plan_generates_framework(applicant_user, institution, grants_manager_user):
    award = _award_for(applicant_user, institution, suffix="approve-helper")
    plan = _plan_with_full_hierarchy(award)
    plan.submit()
    plan.save()

    approve_implementation_plan(plan, actor=grants_manager_user)
    # ImplementationPlan.status is a protected FSMField — refresh_from_db()
    # can't touch it, so re-fetch instead.
    plan = ImplementationPlan.objects.get(pk=plan.pk)
    assert plan.status == ImplementationPlan.Status.APPROVED
    assert plan.approved_by_id == grants_manager_user.pk
    assert plan.results_framework_id is not None
