"""PRD §5.1 FRFA-CO002 / CO003 / CO004 / CO023 — close-out enforcement.

Coverage:
  - CO002 disbursement gate audits when an actor tries to push past the
    CLOSEOUT_IN_PROGRESS / CLOSED / CANCELLED block.
  - CO004 default_closeout_checklist returns funding-type-specific items per
    PRD Table 14.
  - CO023 force_award_closeout requires PROGRAMME_DIRECTOR + non-empty
    justification, flips status, writes the audit row, and dispatches the
    GM notice.

Earlier WS1.3 tests already cover CO003 (expire_awards auto-trigger).
"""

from __future__ import annotations

from datetime import date, timedelta
from decimal import Decimal
from unittest.mock import patch

import pytest
from django.contrib.auth import get_user_model
from django.core.exceptions import ValidationError
from django.utils import timezone

from apps.core.audit.models import AuditLog
from apps.core.notifications.models import Notification
from apps.core.permissions.roles import UserRole
from apps.rims.finance.models import Budget, BudgetLine, DisbursementRequest
from apps.rims.finance.services import (
    assert_award_allows_claims,
    submit_disbursement_request,
)
from apps.rims.grants.closeout_helpers import (
    default_closeout_checklist,
    force_award_closeout,
    get_or_create_closeout_record,
)
from apps.rims.grants.models import (
    Application,
    Award,
    AwardCloseoutRecord,
    FundingRecord,
    GrantCall,
)
from apps.rims.grants.services import (
    award_application,
    shortlist_application,
    submit_application,
)

User = get_user_model()


@pytest.fixture(autouse=True)
def _no_async_dispatch():
    with patch(
        "apps.core.notifications.services.dispatch_notification_channel.delay"
    ) as fake:
        yield fake


@pytest.fixture
def program_director(db, institution):
    u = User.objects.create_user(email="pd.phase1@example.com", password="pw-phase1")
    u.role = UserRole.PROGRAM_DIRECTOR
    u.save()
    return u


def _funded_award(applicant_user, institution, grants_manager_user, *, suffix: str, call_type: str = "grant") -> Award:
    fr = FundingRecord.objects.create(
        title=f"FR {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 {suffix}",
        slug=f"co-{suffix}",
        opens_at=timezone.now() - timedelta(days=1),
        closes_at=timezone.now() + timedelta(days=14),
        status=GrantCall.Status.PUBLISHED,
        funding_record=fr,
        call_type=call_type,
    )
    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))
    return Application.objects.get(pk=app.pk).award


def _budget_for(award: Award) -> Budget:
    b = Budget.objects.create(
        award=award,
        name="Default budget",
        status=Budget.Status.APPROVED,
    )
    BudgetLine.objects.create(budget=b, label="Line A", amount=Decimal("500"))
    return b


# ---------------------------------------------------------------------------
# FRFA-CO002 — disbursement block + audit on close-out
# ---------------------------------------------------------------------------
@pytest.mark.django_db(transaction=True)
def test_close_out_block_writes_audit_row(applicant_user, institution, grants_manager_user):
    award = _funded_award(applicant_user, institution, grants_manager_user, suffix="co002")
    budget = _budget_for(award)
    Award.objects.filter(pk=award.pk).update(status=Award.Status.CLOSEOUT_IN_PROGRESS)
    # Re-fetch so the cached FK on `budget` reflects the updated award status.
    budget = Budget.objects.get(pk=budget.pk)
    with pytest.raises(ValidationError):
        assert_award_allows_claims(budget, actor=grants_manager_user)
    assert AuditLog.objects.filter(
        action="DISBURSEMENT_BLOCKED_BY_CLOSEOUT",
        actor=grants_manager_user,
    ).exists()


@pytest.mark.django_db(transaction=True)
def test_close_out_block_via_submit_disbursement_request(
    applicant_user, institution, grants_manager_user
):
    award = _funded_award(applicant_user, institution, grants_manager_user, suffix="co002b")
    budget = _budget_for(award)
    Award.objects.filter(pk=award.pk).update(status=Award.Status.CLOSEOUT_IN_PROGRESS)
    # The DisbursementRequest is freshly created, so the FK chain to award
    # walks through the DB and picks up the new status — no manual refresh
    # needed for the request itself.
    req = DisbursementRequest.objects.create(
        budget=Budget.objects.get(pk=budget.pk),
        amount=Decimal("100"),
        justification="x",
        status=DisbursementRequest.Status.DRAFT,
        requested_by=grants_manager_user,
    )
    with pytest.raises(ValidationError):
        submit_disbursement_request(req, actor=grants_manager_user)
    # Status should remain DRAFT — the request did not advance.
    req.refresh_from_db()
    assert req.status == DisbursementRequest.Status.DRAFT


# ---------------------------------------------------------------------------
# FRFA-CO004 — funding-type-specific checklist
# ---------------------------------------------------------------------------
@pytest.mark.django_db
def test_default_checklist_grant_includes_research_outputs():
    items = default_closeout_checklist(funding_type="grant")
    keys = {it["key"] for it in items}
    assert "final_narrative" in keys
    assert "financial_reconciliation" in keys
    assert "research_outputs" in keys


@pytest.mark.django_db
def test_default_checklist_scholarship_includes_academic_completion():
    items = default_closeout_checklist(funding_type="scholarship")
    keys = {it["key"] for it in items}
    assert "academic_completion_certificate" in keys
    assert "institutional_confirmation" in keys
    # Grant-only items don't appear:
    assert "research_outputs" not in keys


@pytest.mark.django_db
def test_default_checklist_fellowship_and_challenge():
    fell = {it["key"] for it in default_closeout_checklist(funding_type="fellowship")}
    chal = {it["key"] for it in default_closeout_checklist(funding_type="challenge")}
    assert "fellowship_completion_report" in fell
    assert "host_institution_confirmation" in fell
    assert "innovation_implementation_report" in chal
    assert "impact_evidence" in chal
    # Cross-type items are correctly partitioned:
    assert "innovation_implementation_report" not in fell
    assert "fellowship_completion_report" not in chal


@pytest.mark.django_db
def test_get_or_create_closeout_record_seeds_type_specific_checklist(
    applicant_user, institution, grants_manager_user
):
    award = _funded_award(
        applicant_user, institution, grants_manager_user, suffix="co004", call_type="scholarship"
    )
    rec = get_or_create_closeout_record(award)
    keys = {it["key"] for it in rec.checklist}
    assert "academic_completion_certificate" in keys


# ---------------------------------------------------------------------------
# FRFA-CO023 — forced close-out
# ---------------------------------------------------------------------------
@pytest.mark.django_db
def test_force_closeout_requires_justification(
    applicant_user, institution, grants_manager_user, program_director
):
    award = _funded_award(applicant_user, institution, grants_manager_user, suffix="co023j")
    with pytest.raises(ValidationError):
        force_award_closeout(award, justification="   ", approver=program_director)


@pytest.mark.django_db
def test_force_closeout_rejects_non_pd_actor(
    applicant_user, institution, grants_manager_user
):
    award = _funded_award(applicant_user, institution, grants_manager_user, suffix="co023r")
    with pytest.raises(ValidationError):
        force_award_closeout(
            award, justification="Awardee unresponsive.", approver=grants_manager_user
        )


@pytest.mark.django_db(transaction=True)
def test_force_closeout_flips_award_and_writes_audit(
    applicant_user, institution, grants_manager_user, program_director
):
    award = _funded_award(applicant_user, institution, grants_manager_user, suffix="co023ok")
    Notification.objects.all().delete()
    rec = force_award_closeout(
        award,
        justification="Awardee unreachable for 90 days.",
        approver=program_director,
    )
    award = Award.objects.get(pk=award.pk)
    assert award.status == Award.Status.CLOSED
    assert rec.forced_closure_justification == "Awardee unreachable for 90 days."
    assert rec.forced_at is not None
    assert rec.forced_by_id == program_director.pk
    assert AuditLog.objects.filter(
        action="FORCED_CLOSEOUT_APPROVED",
        actor=program_director,
    ).exists()
    # GM gets a forced-closure-specific notification, applicant gets the
    # standard FRFA-CO021 closure email via the post_save signal.
    assert Notification.objects.filter(
        recipient=grants_manager_user, message__icontains="Forced close-out"
    ).exists()
    assert Notification.objects.filter(
        recipient=applicant_user, message__icontains="formally closed"
    ).exists()
