"""PRD §5.4 FRFM045 + FRFM047/048 — payment receipt capture tests.

Covers:
- FRFM041 — execution requires a transaction reference (bank / mobile money).
- FRFM045 — proof_of_payment file persists with channel + uploaded_by metadata.
- FRFM047 — bank or mobile money statement excerpt persists on reconciliation.
- FRFM048 — discrepancy status requires a signed amount.
- Batch execution propagates per-item proof files to the underlying executions.
"""
from __future__ import annotations

from datetime import date, timedelta
from decimal import Decimal

import pytest
from django.core.exceptions import ValidationError
from django.core.files.uploadedfile import SimpleUploadedFile
from django.utils import timezone

from apps.rims.finance.models import (
    Budget,
    BudgetLine,
    DisbursementExecution,
    DisbursementReconciliation,
    DisbursementRequest,
)
from apps.rims.finance.services import (
    create_payment_batch,
    execute_payment_batch,
    lock_payment_batch,
    reconcile_disbursement,
    save_execution_plan,
)
from apps.rims.grants.models import Application, GrantCall
from apps.rims.grants.services import award_application, shortlist_application, submit_application


def _make_paid_request(applicant_user, institution, amount: str = "100"):
    call = GrantCall.objects.create(
        title="Receipts call",
        slug=f"receipts-{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"), date.today() + timedelta(days=180), narrative="")
    award = Application.objects.get(pk=app.pk).award
    budget = Budget.objects.create(award=award, name="Receipts budget")
    BudgetLine.objects.create(budget=budget, label="L", amount=Decimal("1000"))
    req = DisbursementRequest.objects.create(
        budget=budget,
        amount=Decimal(amount),
        status=DisbursementRequest.Status.APPROVED,
        approved_at=timezone.now(),
    )
    return req


def _pdf(name: str = "receipt.pdf", body: bytes = b"%PDF-1.4 fake"):
    return SimpleUploadedFile(name, body, content_type="application/pdf")


@pytest.mark.django_db
def test_execute_blocks_when_reference_missing(applicant_user, institution):
    req = _make_paid_request(applicant_user, institution)
    with pytest.raises(ValidationError) as exc:
        save_execution_plan(
            req,
            status=DisbursementExecution.Status.EXECUTED,
            payment_method="bank_transfer",
        )
    assert "transaction" in str(exc.value).lower() or "reference" in str(exc.value).lower()


@pytest.mark.django_db
def test_execute_with_proof_persists_metadata(applicant_user, institution, grants_manager_user):
    req = _make_paid_request(applicant_user, institution)
    save_execution_plan(
        req,
        status=DisbursementExecution.Status.EXECUTED,
        payment_method="mobile_money",
        execution_reference="MTN-9X2-771",
        proof_of_payment=_pdf("momo.pdf"),
        proof_channel=DisbursementExecution.ProofChannel.MOBILE_MONEY,
        actor=grants_manager_user,
    )
    req.refresh_from_db()
    assert req.status == DisbursementRequest.Status.PAID
    execution = req.execution
    assert execution.proof_of_payment.name.endswith(".pdf")
    assert execution.proof_channel == "mobile_money"
    assert execution.proof_received_at is not None
    assert execution.proof_uploaded_by == grants_manager_user
    assert execution.execution_reference == "MTN-9X2-771"


@pytest.mark.django_db
def test_execute_without_proof_still_records_reference(applicant_user, institution):
    """Proof file is recommended but not blocking — only the reference is required."""
    req = _make_paid_request(applicant_user, institution)
    save_execution_plan(
        req,
        status=DisbursementExecution.Status.EXECUTED,
        payment_method="bank_transfer",
        execution_reference="ABSA-2026-04-02-0042",
    )
    req.refresh_from_db()
    assert req.status == DisbursementRequest.Status.PAID
    assert req.execution.execution_reference == "ABSA-2026-04-02-0042"
    assert not req.execution.proof_of_payment


@pytest.mark.django_db
def test_reconciliation_attaches_statement_excerpt(applicant_user, institution, finance_user):
    req = _make_paid_request(applicant_user, institution)
    save_execution_plan(
        req,
        status=DisbursementExecution.Status.EXECUTED,
        payment_method="bank_transfer",
        execution_reference="TRX-1",
    )
    reconcile_disbursement(
        req,
        status=DisbursementReconciliation.Status.RECONCILED,
        notes="Matched Apr statement",
        statement_excerpt=_pdf("statement.pdf"),
        statement_source=DisbursementReconciliation.StatementSource.BANK,
        statement_period_start=date.today() - timedelta(days=14),
        statement_period_end=date.today(),
        actor=finance_user,
    )
    rec = DisbursementReconciliation.objects.get(request=req)
    assert rec.statement_excerpt.name.endswith(".pdf")
    assert rec.statement_source == "bank"
    assert rec.statement_period_start is not None
    assert rec.reconciled_by == finance_user


@pytest.mark.django_db
def test_reconciliation_discrepancy_requires_amount(applicant_user, institution):
    req = _make_paid_request(applicant_user, institution)
    save_execution_plan(
        req,
        status=DisbursementExecution.Status.EXECUTED,
        payment_method="bank_transfer",
        execution_reference="TRX-2",
    )
    with pytest.raises(ValidationError):
        reconcile_disbursement(
            req,
            status=DisbursementReconciliation.Status.DISCREPANCY,
            notes="Bank charges ate into transfer",
        )
    # With the amount it persists fine.
    reconcile_disbursement(
        req,
        status=DisbursementReconciliation.Status.DISCREPANCY,
        discrepancy_amount=Decimal("-2.50"),
        notes="Bank fee",
    )
    rec = DisbursementReconciliation.objects.get(request=req)
    assert rec.discrepancy_amount == Decimal("-2.50")


@pytest.mark.django_db
def test_batch_execute_attaches_per_item_proofs(applicant_user, institution):
    r1 = _make_paid_request(applicant_user, institution, amount="100")
    r2 = _make_paid_request(applicant_user, institution, amount="250")
    batch = create_payment_batch([r1, r2], batch_reference="BATCH-RECEIPTS")
    lock_payment_batch(batch)
    item_ids = list(batch.items.values_list("pk", flat=True))
    proofs = {
        item_ids[0]: (_pdf("a.pdf"), DisbursementExecution.ProofChannel.BANK),
        item_ids[1]: (_pdf("b.pdf"), DisbursementExecution.ProofChannel.MOBILE_MONEY),
    }
    refs = {item_ids[0]: "BANK-1", item_ids[1]: "MOMO-2"}
    execute_payment_batch(batch, item_references=refs, item_proofs=proofs)
    for req in (r1, r2):
        req.refresh_from_db()
        assert req.status == DisbursementRequest.Status.PAID
        assert req.execution.proof_of_payment.name.endswith(".pdf")
    assert r1.execution.proof_channel == "bank"
    assert r2.execution.proof_channel == "mobile_money"


@pytest.mark.django_db
def test_batch_execute_synthesises_reference_when_missing(applicant_user, institution):
    """FRFM041 fallback: batches still execute even when the operator skipped per-item refs."""
    req = _make_paid_request(applicant_user, institution)
    batch = create_payment_batch([req], batch_reference="BATCH-AUTO-REF")
    lock_payment_batch(batch)
    execute_payment_batch(batch)
    req.refresh_from_db()
    assert req.status == DisbursementRequest.Status.PAID
    assert req.execution.execution_reference.startswith("BATCH-AUTO-REF-")
