"""PRD §5.1 FRFA-CO012 (asset verification UI) + FRFA-CO015–017 (recovery
of an unspent balance).

Coverage:
- Grants Manager can record an asset verification outcome through the new
  view; an UNACCOUNTED outcome correctly flags the existing CO013 gate.
- Finance Officer can raise a recovery request and confirm receipt through
  the new views, satisfying the ``recovery_confirmed_or_waived`` gate.
- Programme Director can formally waive a recovery amount; a non-PD actor
  is rejected.
"""
from __future__ import annotations

from datetime import date, timedelta
from decimal import Decimal

import pytest
from django.contrib.auth import get_user_model
from django.urls import reverse
from django.utils import timezone

from apps.core.authentication.models import MFADevice
from apps.core.permissions.roles import UserRole
from apps.rims.grants.closeout_helpers import closeout_preconditions, get_or_create_closeout_record
from apps.rims.grants.models import (
    Application,
    AssetVerification,
    Award,
    GrantCall,
    RecoveryRequest,
    RecoveryWaiver,
)
from apps.rims.grants.services import award_application, shortlist_application, submit_application

User = get_user_model()


def _bypass_mfa(user):
    MFADevice.objects.get_or_create(
        user=user,
        device_type=MFADevice.DeviceType.TOTP,
        defaults={"name": "Test TOTP", "secret": "JBSWY3DPEHPK3PXP", "is_verified": True},
    )


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


def _build_award(applicant_user, institution, amount: Decimal = Decimal("1000")) -> Award:
    call = GrantCall.objects.create(
        title="CO012/015 call",
        slug=f"co012-015-{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, amount, date.today() + timedelta(days=180), narrative="")
    return Application.objects.get(pk=app.pk).award


@pytest.mark.django_db
def test_gm_records_asset_verification_via_view(client, applicant_user, grants_manager_user, institution):
    award = _build_award(applicant_user, institution)
    get_or_create_closeout_record(award)
    _bypass_mfa(grants_manager_user)
    client.force_login(grants_manager_user)
    resp = client.post(
        reverse("rims_grants:asset_verification_create", kwargs={"pk": award.pk}),
        {"asset_name": "Field laptop SN-42", "status": AssetVerification.Status.CONFIRMED, "notes": "Present at site visit."},
        follow=True,
    )
    assert resp.status_code == 200
    av = AssetVerification.objects.get(asset_name="Field laptop SN-42")
    assert av.status == AssetVerification.Status.CONFIRMED
    assert av.verified_by_id == grants_manager_user.pk


@pytest.mark.django_db
def test_unaccounted_asset_via_view_blocks_precondition_gate(client, applicant_user, grants_manager_user, institution):
    award = _build_award(applicant_user, institution)
    get_or_create_closeout_record(award)
    _bypass_mfa(grants_manager_user)
    client.force_login(grants_manager_user)
    client.post(
        reverse("rims_grants:asset_verification_create", kwargs={"pk": award.pk}),
        {"asset_name": "Missing projector", "status": AssetVerification.Status.UNACCOUNTED, "notes": ""},
    )
    gates = {g.key: g for g in closeout_preconditions(award)}
    assert gates["assets_resolved"].passed is False


@pytest.mark.django_db
def test_finance_officer_raises_and_confirms_recovery(client, applicant_user, finance_user, institution):
    award = _build_award(applicant_user, institution, amount=Decimal("1000"))
    closeout = get_or_create_closeout_record(award)
    _bypass_mfa(finance_user)
    client.force_login(finance_user)

    client.post(
        reverse("rims_grants:award_recovery_raise", kwargs={"pk": award.pk}),
        {"amount": "1000", "reason": "Unspent balance at close-out."},
    )
    recovery = RecoveryRequest.objects.get(closeout=closeout)
    assert recovery.received_on is None
    gates = {g.key: g for g in closeout_preconditions(award)}
    assert gates["recovery_confirmed_or_waived"].passed is False  # raised but not yet confirmed

    client.post(
        reverse("rims_grants:recovery_confirm", kwargs={"pk": recovery.pk}),
        {"payment_reference": "BANK-REF-1", "received_on": date.today().isoformat()},
    )
    recovery = RecoveryRequest.objects.get(pk=recovery.pk)
    assert recovery.received_on == date.today()
    assert recovery.payment_reference == "BANK-REF-1"
    gates = {g.key: g for g in closeout_preconditions(award)}
    assert gates["recovery_confirmed_or_waived"].passed is True


@pytest.mark.django_db
def test_program_director_can_waive_recovery(client, applicant_user, program_director, institution):
    award = _build_award(applicant_user, institution, amount=Decimal("1000"))
    get_or_create_closeout_record(award)
    _bypass_mfa(program_director)
    client.force_login(program_director)
    client.post(
        reverse("rims_grants:award_recovery_waive", kwargs={"pk": award.pk}),
        {"amount": "1000", "justification": "Award amount below the recovery pursuit threshold."},
    )
    waiver = RecoveryWaiver.objects.get(closeout__award=award)
    assert waiver.approved_by_id == program_director.pk
    gates = {g.key: g for g in closeout_preconditions(award)}
    assert gates["recovery_confirmed_or_waived"].passed is True


@pytest.mark.django_db
def test_non_pd_cannot_waive_recovery(client, applicant_user, grants_manager_user, institution):
    award = _build_award(applicant_user, institution, amount=Decimal("1000"))
    get_or_create_closeout_record(award)
    # Privileged roles need a verified MFA device or the middleware redirects
    # every POST to enrolment — bypass so the 403 we want to test surfaces.
    _bypass_mfa(grants_manager_user)
    client.force_login(grants_manager_user)
    resp = client.post(
        reverse("rims_grants:award_recovery_waive", kwargs={"pk": award.pk}),
        {"amount": "1000", "justification": "Trying to waive without authority."},
    )
    assert resp.status_code == 403
    assert not RecoveryWaiver.objects.filter(closeout__award=award).exists()
