"""PRD §5.1 FRFA-CO019 — donor close-out report (PDF + CSV dispatch)."""
from __future__ import annotations

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

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

from apps.core.audit.models import AuditLog
from apps.core.audit.tasks import persist_audit_log
from apps.core.authentication.models import MFADevice
from apps.rims.finance.models import Budget, BudgetLine, FundingSource
from apps.rims.finance.services_analytics import donor_finance_packet
from apps.rims.finance.services_donor_closeout import generate_donor_closeout_pdf
from apps.rims.finance.tasks import dispatch_donor_closeout_report
from apps.rims.grants.models import (
    Application,
    Award,
    FundingRecord,
    GrantCall,
)
from apps.rims.grants.services import (
    award_application,
    shortlist_application,
    submit_application,
)
from apps.rims.operations.models import Partner


def _build_closed_award_with_donor(
    applicant_user,
    institution,
    *,
    funding_record: FundingRecord,
    donor: FundingSource,
    amount: Decimal = Decimal("1000"),
    disbursed_via_budget: bool = True,
) -> Award:
    call = GrantCall.objects.create(
        title=f"CO019 call {timezone.now().timestamp()}",
        slug=f"co019-{int(timezone.now().timestamp() * 1000) % 10_000_000}",
        call_type="grant",
        opens_at=timezone.now() - timedelta(days=400),
        closes_at=timezone.now() + timedelta(days=10),
        status=GrantCall.Status.PUBLISHED,
        funding_record=funding_record,
        budget_ceiling=Decimal("5000"),  # FRFA-AA001 — provides room for the award amount.
    )
    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=200), narrative="")
    award = Application.objects.get(pk=app.pk).award
    budget = Budget.objects.create(
        award=award,
        name=f"Closeout budget {award.pk}",
        funding_source=donor,
        currency=award.currency,
    )
    BudgetLine.objects.create(budget=budget, label="Line", amount=amount)
    award.status = Award.Status.CLOSED
    award.closed_at = timezone.now() - timedelta(days=5)
    award.save(update_fields=["status", "closed_at"])
    return award


@pytest.fixture(autouse=True)
def _sync_audit():
    """Make AuditLog writes synchronous in tests; on_commit doesn't fire in test txn."""
    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


@pytest.fixture
def donor_with_partner(db):
    partner = Partner.objects.create(
        name="Test Donor Partner",
        partner_type=Partner.PartnerType.FUNDER,
        country="Uganda",
        contact_email="donor@example.com",
    )
    donor = FundingSource.objects.create(
        name="Test Donor", code=f"td-{timezone.now().microsecond}", partner=partner
    )
    return donor


@pytest.fixture
def funding_record(db, grants_manager_user, finance_user):
    # PRD §5.1 FRFA-AA001 — when funding_record.status=APPROVED the award
    # preconditions are enforced. Provide grant_manager + finance_officer so
    # the preconditions pass for awards created under this record.
    return FundingRecord.objects.create(
        title="CO019 FR",
        amount=Decimal("10000"),
        start_date=timezone.now().date() - timedelta(days=400),
        end_date=timezone.now().date() + timedelta(days=400),
        status=FundingRecord.Status.APPROVED,
        grant_manager=grants_manager_user,
        finance_officer=finance_user,
    )


@pytest.mark.django_db
def test_pdf_renderer_produces_nonempty_bytes(
    applicant_user, institution, donor_with_partner, funding_record
):
    """PRD §5.1 FRFA-CO019 — generate_donor_closeout_pdf returns PDF bytes."""
    award = _build_closed_award_with_donor(
        applicant_user, institution, funding_record=funding_record, donor=donor_with_partner
    )
    pdf = generate_donor_closeout_pdf(donor_with_partner)
    assert pdf and isinstance(pdf, bytes)
    assert pdf.startswith(b"%PDF"), "Output is not a valid PDF document"


@pytest.mark.django_db
def test_csv_closeout_only_filter_and_columns(
    client, applicant_user, grants_manager_user, institution, donor_with_partner, funding_record
):
    """PRD §5.1 FRFA-CO019 — ?closeout_only=1 limits to closed + adds residual column."""
    award = _build_closed_award_with_donor(
        applicant_user, institution, funding_record=funding_record, donor=donor_with_partner
    )
    # Pre-populate residual on closeout snapshot to verify the column surfaces.
    from apps.rims.grants.closeout_helpers import get_or_create_closeout_record

    closeout = get_or_create_closeout_record(award)
    closeout.financial_snapshot = {"residual_returned": "250.00"}
    closeout.save(update_fields=["financial_snapshot", "updated_at"])

    MFADevice.objects.get_or_create(
        user=grants_manager_user,
        device_type=MFADevice.DeviceType.TOTP,
        defaults={"name": "Test TOTP", "secret": "JBSWY3DPEHPK3PXP", "is_verified": True},
    )
    client.force_login(grants_manager_user)
    resp = client.get(
        reverse("rims_finance:donor_report_export", kwargs={"pk": donor_with_partner.pk})
        + "?closeout_only=1"
    )
    assert resp.status_code == 200
    body = resp.content.decode()
    assert "Residual returned" in body
    assert "250.00" in body


@pytest.mark.django_db
def test_dispatch_emails_donor_with_pdf_attachment(
    applicant_user, institution, donor_with_partner, funding_record
):
    """PRD §5.1 FRFA-CO019 — dispatch_donor_closeout_report sends PDF to donor email."""
    award = _build_closed_award_with_donor(
        applicant_user, institution, funding_record=funding_record, donor=donor_with_partner
    )
    mail.outbox = []
    dispatched = dispatch_donor_closeout_report(funding_record.pk)
    assert dispatched == 1
    assert len(mail.outbox) == 1
    msg = mail.outbox[0]
    assert msg.to == ["donor@example.com"]
    assert any(att for att in msg.attachments if att[2] == "application/pdf")


@pytest.mark.django_db
def test_dispatch_is_idempotent_within_7_days(
    applicant_user, institution, donor_with_partner, funding_record
):
    """PRD §5.1 FRFA-CO019 — re-running within 7 days is a no-op (audit row gate)."""
    _build_closed_award_with_donor(
        applicant_user, institution, funding_record=funding_record, donor=donor_with_partner
    )
    mail.outbox = []
    first = dispatch_donor_closeout_report(funding_record.pk)
    second = dispatch_donor_closeout_report(funding_record.pk)
    assert first == 1
    assert second == 0  # audit row blocks re-dispatch
    assert len(mail.outbox) == 1


@pytest.mark.django_db
def test_forced_closure_award_surfaces_in_appendix(
    applicant_user, institution, donor_with_partner, funding_record
):
    """PRD §5.1 FRFA-CO019 — forced-closure justification surfaces in the close-out packet.

    Asserting on the packet (not raw PDF bytes) — WeasyPrint compresses content
    streams so binary substring search of PDF bytes is unreliable.
    """
    from apps.rims.finance.services_analytics import donor_finance_packet

    award = _build_closed_award_with_donor(
        applicant_user, institution, funding_record=funding_record, donor=donor_with_partner
    )
    from apps.rims.grants.closeout_helpers import get_or_create_closeout_record

    closeout = get_or_create_closeout_record(award)
    closeout.forced_closure_justification = "Donor pulled funding; close to reclaim residual."
    closeout.forced_at = timezone.now()
    closeout.save(update_fields=["forced_closure_justification", "forced_at", "updated_at"])

    packet = donor_finance_packet(donor_with_partner, closeout_only=True)
    forced = [r for r in packet.rows if r.get("forced_closure")]
    assert len(forced) == 1
    assert "Donor pulled funding" in forced[0]["forced_closure_justification"]
    # And the PDF still renders without error.
    pdf = generate_donor_closeout_pdf(donor_with_partner)
    assert pdf.startswith(b"%PDF")
