"""P0 (RIMS assessment §3 #1) — Award close-out guard.

Coverage:
- Happy path: all reports created ACCEPTED, no UNACCOUNTED assets → award
  walks ACTIVE → CLOSEOUT_IN_PROGRESS → CLOSED in one POST.
- An UNACCOUNTED AssetVerification blocks closure and rolls back: the award
  stays in its previous status; no FinalNarrativeReport / FinalFinancialReport
  rows persist from the aborted attempt.
- Per the new guard, a pre-existing non-ACCEPTED FinalFinancialReport (e.g.
  QUERIES_RAISED) blocks closure for the same reason.
"""
from __future__ import annotations

from datetime import timedelta
from decimal import Decimal

import pytest
from django.urls import reverse
from django.utils import timezone

from apps.core.authentication.models import MFADevice
from apps.rims.grants.closeout_helpers import get_or_create_closeout_record
from apps.rims.grants.models import (
    Application,
    AssetVerification,
    Award,
    FinalFinancialReport,
    FinalNarrativeReport,
    GrantCall,
)
from apps.rims.grants.services import (
    award_application,
    shortlist_application,
    submit_application,
)


def _bypass_mfa(user):
    """Mirror the existing test_archive_view pattern — privileged roles need
    a verified MFA device or the middleware redirects every POST to enrolment."""
    MFADevice.objects.get_or_create(
        user=user,
        device_type=MFADevice.DeviceType.TOTP,
        defaults={"name": "Test TOTP", "secret": "JBSWY3DPEHPK3PXP", "is_verified": True},
    )


def _full_checklist_post_payload(closeout, narrative: str = "Final narrative.") -> dict:
    payload = {"narrative": narrative}
    for row in closeout.checklist or []:
        if row.get("required"):
            payload[f"co_done__{row['key']}"] = "on"
            payload[f"co_note__{row['key']}"] = "evidence captured"
    return payload


def _award_factory(applicant_user, institution):
    call = GrantCall.objects.create(
        title="P0 closeout",
        slug=f"p0-co-{int(timezone.now().timestamp() * 1000) % 10_000_000}",
        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"),
        timezone.now().date() + timedelta(days=180),
        narrative="",
    )
    return Application.objects.get(pk=app.pk).award


@pytest.mark.django_db
def test_closeout_happy_path_closes_award(client, applicant_user, grants_manager_user, institution):
    award = _award_factory(applicant_user, institution)
    closeout = get_or_create_closeout_record(award)

    _bypass_mfa(grants_manager_user)
    client.force_login(grants_manager_user)
    resp = client.post(
        reverse("rims_grants:award_closeout", kwargs={"pk": award.pk}),
        _full_checklist_post_payload(closeout),
        follow=True,
    )
    award.refresh_from_db()
    msgs = [(m.level_tag, m.message) for m in resp.context.get("messages", [])] if resp.context else []
    assert award.status == Award.Status.CLOSED, (
        f"status={award.status!r}, http={resp.status_code}, msgs={msgs}"
    )
    assert award.closed_at is not None
    assert award.closeout_ready_at is not None
    assert closeout.narrative_reports.count() == 1
    assert closeout.financial_reports.count() == 1


@pytest.mark.django_db
def test_unaccounted_asset_blocks_closeout_atomically(
    client, applicant_user, grants_manager_user, institution
):
    award = _award_factory(applicant_user, institution)
    closeout = get_or_create_closeout_record(award)
    AssetVerification.objects.create(
        closeout=closeout,
        asset_name="Laptop SN-001",
        status=AssetVerification.Status.UNACCOUNTED,
    )

    _bypass_mfa(grants_manager_user)
    client.force_login(grants_manager_user)
    client.post(
        reverse("rims_grants:award_closeout", kwargs={"pk": award.pk}),
        _full_checklist_post_payload(closeout),
        follow=True,
    )
    award.refresh_from_db()
    # Award did NOT close — the guard rolled the txn back.
    assert award.status != Award.Status.CLOSED
    # And the reports created inside the atomic block did not persist.
    assert FinalNarrativeReport.objects.filter(closeout=closeout).count() == 0
    assert FinalFinancialReport.objects.filter(closeout=closeout).count() == 0


@pytest.mark.django_db
def test_queries_raised_financial_report_blocks_closeout(
    client, applicant_user, grants_manager_user, institution
):
    award = _award_factory(applicant_user, institution)
    closeout = get_or_create_closeout_record(award)
    # Pre-existing financial report still in QUERIES_RAISED — must block.
    # FSMField.status is protected — drive it through the FSM transition.
    pre_existing = FinalFinancialReport.objects.create(
        closeout=closeout,
        notes="Pending finance review",
    )
    pre_existing.raise_queries()
    pre_existing.save()

    _bypass_mfa(grants_manager_user)
    client.force_login(grants_manager_user)
    client.post(
        reverse("rims_grants:award_closeout", kwargs={"pk": award.pk}),
        _full_checklist_post_payload(closeout),
        follow=True,
    )
    award.refresh_from_db()
    # FSMField(protected=True) refuses refresh_from_db — re-fetch instead.
    pre_existing = FinalFinancialReport.objects.get(pk=pre_existing.pk)
    assert award.status != Award.Status.CLOSED
    # The pre-existing report stays in QUERIES_RAISED (guard rolled back the
    # ACCEPTED row that would have been added by the closeout flow).
    assert pre_existing.status == FinalFinancialReport.Status.QUERIES_RAISED
    # Only the pre-existing row remains; no extra ACCEPTED row was committed.
    assert FinalFinancialReport.objects.filter(closeout=closeout).count() == 1
