"""PRD §5.1 FRFA-AA026 — auto-flag "Ready for Close-Out".

Coverage:
- ``evaluate_award_ready_for_closeout`` / ``check_and_flag_ready_for_closeout``
  require all three conditions: all milestones met, all tranches disbursed,
  disbursed total == award amount.
- The flag fires live via both the AwardTranche-disbursed signal and the
  Milestone-accepted signal (rims_projects app).
- Downstream verification (FRFA-CO001): once READY_FOR_CLOSEOUT, opening the
  close-out workbench still auto-generates the close-out record and
  dispatches the initiation notifications.
"""
from __future__ import annotations

from datetime import date, timedelta
from decimal import Decimal

import pytest

from django.utils import timezone

from apps.core.notifications.models import Notification
from apps.rims.grants.closeout_helpers import (
    check_and_flag_ready_for_closeout,
    evaluate_award_ready_for_closeout,
    get_or_create_closeout_record,
)
from apps.rims.grants.models import (
    Application,
    Award,
    AwardTranche,
    FundingRecord,
    GrantCall,
)
from apps.rims.grants.services import award_application, shortlist_application, submit_application
from apps.rims.projects.models import Milestone, Project


def _funded_award(applicant_user, institution, grants_manager_user, *, suffix, amount=Decimal("1000")):
    fr = FundingRecord.objects.create(
        title=f"FR AA026 {suffix}",
        amount=Decimal("10000"),
        start_date=date.today() - timedelta(days=10),
        end_date=date.today() + timedelta(days=365),
        grant_manager=grants_manager_user,
    )
    call = GrantCall.objects.create(
        title=f"Call AA026 {suffix}",
        slug=f"aa026-{suffix}",
        opens_at=timezone.now() - timedelta(days=1),
        closes_at=timezone.now() + timedelta(days=14),
        status=GrantCall.Status.PUBLISHED,
        funding_record=fr,
    )
    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, amount, date.today() + timedelta(days=180))
    return Application.objects.get(pk=app.pk).award


@pytest.mark.django_db
def test_no_tranches_never_flags_ready(applicant_user, grants_manager_user, institution):
    award = _funded_award(applicant_user, institution, grants_manager_user, suffix="notranche")
    assert evaluate_award_ready_for_closeout(award) is False
    assert check_and_flag_ready_for_closeout(award) is False
    award = Award.objects.get(pk=award.pk)
    assert award.status == Award.Status.ACTIVE


@pytest.mark.django_db
def test_partial_disbursement_does_not_flag_ready(applicant_user, grants_manager_user, institution):
    award = _funded_award(applicant_user, institution, grants_manager_user, suffix="partial", amount=Decimal("1000"))
    AwardTranche.objects.create(award=award, label="T1", amount=Decimal("400"), status=AwardTranche.Status.DISBURSED)
    AwardTranche.objects.create(award=award, label="T2", amount=Decimal("600"), status=AwardTranche.Status.PLANNED)
    assert check_and_flag_ready_for_closeout(award) is False
    award = Award.objects.get(pk=award.pk)
    assert award.status == Award.Status.ACTIVE


@pytest.mark.django_db
def test_pending_milestone_blocks_ready_flag(applicant_user, grants_manager_user, institution):
    award = _funded_award(applicant_user, institution, grants_manager_user, suffix="milestone", amount=Decimal("1000"))
    project = Project.objects.create(
        award=award,
        title="AA026 project",
        summary="",
        start_date=date.today(),
        end_date=date.today() + timedelta(days=120),
        status=Project.Status.ACTIVE,
    )
    Milestone.objects.create(
        project=project,
        name="M1",
        due_date=date.today() + timedelta(days=10),
        report_due=True,
        status=Milestone.Status.SUBMITTED,
    )
    AwardTranche.objects.create(award=award, label="T1", amount=Decimal("1000"), status=AwardTranche.Status.DISBURSED)
    assert check_and_flag_ready_for_closeout(award) is False


@pytest.mark.django_db
def test_all_conditions_met_flags_ready_and_notifies_gm(applicant_user, grants_manager_user, institution):
    award = _funded_award(applicant_user, institution, grants_manager_user, suffix="full", amount=Decimal("1000"))
    t1 = AwardTranche.objects.create(award=award, label="T1", amount=Decimal("400"), status=AwardTranche.Status.PLANNED)
    t2 = AwardTranche.objects.create(award=award, label="T2", amount=Decimal("600"), status=AwardTranche.Status.PLANNED)
    Notification.objects.all().delete()

    # Bypass the live post_save signal so this test exercises the direct
    # function call in isolation — .update() does not fire post_save, unlike
    # .save()/.create() with status=DISBURSED (which the signal test below
    # covers, and which would otherwise flip the award here too, since
    # AwardTranche.award=award caches the *same* Python object passed in).
    AwardTranche.objects.filter(pk__in=[t1.pk, t2.pk]).update(status=AwardTranche.Status.DISBURSED)

    assert check_and_flag_ready_for_closeout(award) is True
    award = Award.objects.get(pk=award.pk)
    assert award.status == Award.Status.READY_FOR_CLOSEOUT
    assert award.closeout_ready_at is not None
    assert Notification.objects.filter(
        recipient=grants_manager_user, message__icontains="initiate grant close-out"
    ).exists()

    # Idempotent — re-running the check on an already-flagged award is a no-op.
    assert check_and_flag_ready_for_closeout(award) is False


@pytest.mark.django_db
def test_ready_flag_fires_via_tranche_disbursed_signal(applicant_user, grants_manager_user, institution):
    """The AwardTranche post_save signal (not a direct call) triggers the flag."""
    award = _funded_award(applicant_user, institution, grants_manager_user, suffix="tranchesignal", amount=Decimal("500"))
    tranche = AwardTranche.objects.create(award=award, label="T1", amount=Decimal("500"), status=AwardTranche.Status.PLANNED)
    tranche.status = AwardTranche.Status.DISBURSED
    tranche.save(update_fields=["status"])
    award = Award.objects.get(pk=award.pk)
    assert award.status == Award.Status.READY_FOR_CLOSEOUT


@pytest.mark.django_db
def test_ready_flag_fires_via_milestone_accepted_signal(applicant_user, grants_manager_user, institution):
    """The Milestone post_save signal (rims_projects app) triggers the flag
    once it is the last unmet condition."""
    award = _funded_award(applicant_user, institution, grants_manager_user, suffix="milestonesignal", amount=Decimal("1000"))
    project = Project.objects.create(
        award=award,
        title="AA026 project 2",
        summary="",
        start_date=date.today(),
        end_date=date.today() + timedelta(days=120),
        status=Project.Status.ACTIVE,
    )
    milestone = Milestone.objects.create(
        project=project,
        name="M1",
        due_date=date.today() + timedelta(days=10),
        report_due=True,
        status=Milestone.Status.SUBMITTED,
    )
    AwardTranche.objects.create(award=award, label="T1", amount=Decimal("1000"), status=AwardTranche.Status.DISBURSED)
    award = Award.objects.get(pk=award.pk)
    assert award.status == Award.Status.ACTIVE  # tranche alone is insufficient

    milestone.status = Milestone.Status.ACCEPTED
    milestone.save(update_fields=["status"])
    award = Award.objects.get(pk=award.pk)
    assert award.status == Award.Status.READY_FOR_CLOSEOUT


@pytest.mark.django_db
def test_ready_for_closeout_downstream_still_initiates_closeout(applicant_user, grants_manager_user, institution):
    """PRD §5.1 FRFA-CO001 — once AA026 flags READY_FOR_CLOSEOUT, the
    existing close-out-record auto-generation + notification path (confirmed
    correct by the prior audit) still fires once triggered."""
    award = _funded_award(applicant_user, institution, grants_manager_user, suffix="downstream", amount=Decimal("1000"))
    AwardTranche.objects.create(award=award, label="T1", amount=Decimal("1000"), status=AwardTranche.Status.DISBURSED)
    award = Award.objects.get(pk=award.pk)
    assert award.status == Award.Status.READY_FOR_CLOSEOUT

    Notification.objects.all().delete()
    rec = get_or_create_closeout_record(award)
    assert rec.checklist
    assert Notification.objects.filter(
        recipient=grants_manager_user, message__icontains="close-out has been initiated"
    ).exists()
    assert Notification.objects.filter(
        recipient=applicant_user, message__icontains="close-out has been initiated"
    ).exists()
