"""PRD §5.1 FRFA-CO012/013 — tranche residual reconciliation at close-out."""
from __future__ import annotations

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

import pytest
from django.utils import timezone

from apps.core.audit.models import AuditLog
from apps.core.audit.tasks import persist_audit_log
from apps.rims.grants.closeout_helpers import (
    get_or_create_closeout_record,
    record_closeout_residual,
)
from apps.rims.grants.models import (
    Application,
    Award,
    GrantCall,
)
from apps.rims.grants.services import (
    award_application,
    shortlist_application,
    submit_application,
)


@pytest.fixture(autouse=True)
def _sync_audit():
    """PRD §5.1 — audit writes go through write_audit_log.delay + transaction.on_commit.
    In test transactions on_commit never fires; mock both to write synchronously so
    we can assert on AuditLog rows directly per [[project_rep_p2_2026_05_16]]."""
    with patch("apps.core.audit.tasks.write_audit_log.delay", side_effect=lambda **kw: persist_audit_log(**kw)):
        with patch(
            "apps.core.audit.mixins.transaction.on_commit",
            side_effect=lambda fn: fn(),
        ):
            yield


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


@pytest.mark.django_db
def test_under_disbursed_award_records_residual(applicant_user, grants_manager_user, institution):
    """PRD §5.1 FRFA-CO012/013 — residual = award_amount - total_disbursed when > 0."""
    award = _build_award(applicant_user, institution, amount=Decimal("1000"))
    get_or_create_closeout_record(award)
    # No Budget rows → total_disbursed = 0 from award_financial_snapshot.
    # award_amount = 1000 → residual should be 1000.
    residual = record_closeout_residual(award, actor=grants_manager_user)
    assert residual == Decimal("1000.00")
    award.refresh_from_db()
    snapshot = award.closeout_record.financial_snapshot or {}
    # DecimalField(decimal_places=2) → str(Decimal("1000.00")) == "1000.00".
    assert snapshot.get("residual_returned") == "1000.00"


@pytest.mark.django_db
def test_under_disbursed_award_writes_audit_row(applicant_user, grants_manager_user, institution):
    """PRD §5.1 FRFA-CO012/013 — CLOSEOUT_RESIDUAL_RECONCILED audit row."""
    award = _build_award(applicant_user, institution, amount=Decimal("750"))
    get_or_create_closeout_record(award)
    record_closeout_residual(award, actor=grants_manager_user)
    audit = AuditLog.objects.filter(
        action="CLOSEOUT_RESIDUAL_RECONCILED",
        target_app="rims_grants",
        target_model="Award",
        object_id=str(award.pk),
    ).first()
    assert audit is not None, "expected CLOSEOUT_RESIDUAL_RECONCILED audit row"
    assert audit.changes["residual"] == "750.00"
    assert audit.changes["currency"] == award.currency


@pytest.mark.django_db
def test_fully_disbursed_award_records_no_residual(applicant_user, grants_manager_user, institution):
    """PRD §5.1 FRFA-CO012/013 — when residual == 0, no audit row and snapshot key absent."""
    award = _build_award(applicant_user, institution, amount=Decimal("0"))
    get_or_create_closeout_record(award)
    residual = record_closeout_residual(award, actor=grants_manager_user)
    assert residual == Decimal("0")
    award.refresh_from_db()
    snapshot = award.closeout_record.financial_snapshot or {}
    assert "residual_returned" not in snapshot
    # And no audit row was written for this award.
    assert not AuditLog.objects.filter(
        action="CLOSEOUT_RESIDUAL_RECONCILED",
        object_id=str(award.pk),
    ).exists()
