"""PRD §5.4 FRFM040 — payment batching service tests.

Coverage:
- Build a batch from approved requests with a snapshot amount.
- Reject mixed currencies.
- Reject already-batched, draft, rejected, or paid requests.
- Lock then execute → every item's DisbursementExecution flips to EXECUTED
  and its DisbursementRequest flips to PAID. Per-payee notification fires.
- Cancel detaches items so they can be re-batched.
"""
from __future__ import annotations

from datetime import date, timedelta
from decimal import Decimal

import pytest
from django.core.exceptions import ValidationError
from django.utils import timezone

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


def _award(applicant_user, institution, *, currency="USD"):
    call = GrantCall.objects.create(
        title=f"FRFM040 {currency}",
        slug=f"frfm040-{currency}-{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
    if currency != award.currency:
        award.currency = currency
        award.save(update_fields=["currency"])
    return award


def _budget(award, *, currency=None):
    return Budget.objects.create(
        award=award,
        name="Batch test",
        currency=currency or award.currency,
    )


def _approved_request(budget, amount, *, payee="payee@example.com"):
    line = BudgetLine.objects.create(budget=budget, label="L", amount=Decimal("9999"))
    req = DisbursementRequest.objects.create(
        budget=budget,
        amount=Decimal(amount),
        status=DisbursementRequest.Status.APPROVED,
        approved_at=timezone.now(),
        payee_email=payee,
    )
    return req


@pytest.mark.django_db
def test_create_batch_snapshots_amounts(applicant_user, institution):
    award = _award(applicant_user, institution)
    budget = _budget(award)
    r1 = _approved_request(budget, "100")
    r2 = _approved_request(budget, "250")

    batch = create_payment_batch(
        [r1, r2],
        batch_reference="BATCH-2026-05-01-A",
        scheduled_for=date.today() + timedelta(days=3),
    )
    assert batch.status == PaymentBatch.Status.DRAFT
    items = list(batch.items.order_by("sequence"))
    assert {it.amount for it in items} == {Decimal("100"), Decimal("250")}
    assert all(it.amount == it.request.amount for it in items)


@pytest.mark.django_db
def test_create_batch_rejects_mixed_currencies(applicant_user, institution):
    a1 = _award(applicant_user, institution, currency="USD")
    a2 = _award(applicant_user, institution, currency="UGX")
    b1 = _budget(a1)
    b2 = _budget(a2)
    r1 = _approved_request(b1, "100")
    r2 = _approved_request(b2, "250")
    with pytest.raises(ValidationError) as exc:
        create_payment_batch([r1, r2], batch_reference="BATCH-CURRENCY-MIX")
    assert "mixed currencies" in str(exc.value).lower()


@pytest.mark.django_db
def test_create_batch_rejects_double_enrolment(applicant_user, institution):
    award = _award(applicant_user, institution)
    budget = _budget(award)
    r1 = _approved_request(budget, "100")
    create_payment_batch([r1], batch_reference="BATCH-FIRST")
    with pytest.raises(ValidationError) as exc:
        create_payment_batch([r1], batch_reference="BATCH-SECOND")
    assert "already in batch" in str(exc.value).lower()


@pytest.mark.django_db
def test_create_batch_rejects_non_approved_request(applicant_user, institution):
    award = _award(applicant_user, institution)
    budget = _budget(award)
    r1 = _approved_request(budget, "100")
    r1.status = DisbursementRequest.Status.SUBMITTED
    r1.save(update_fields=["status"])
    with pytest.raises(ValidationError):
        create_payment_batch([r1], batch_reference="BATCH-BAD")


@pytest.mark.django_db
def test_lock_then_execute_marks_each_item_paid(applicant_user, institution):
    award = _award(applicant_user, institution)
    budget = _budget(award)
    r1 = _approved_request(budget, "100", payee="a@example.com")
    r2 = _approved_request(budget, "250", payee="b@example.com")
    batch = create_payment_batch([r1, r2], batch_reference="BATCH-EXEC")
    lock_payment_batch(batch)
    execute_payment_batch(batch, item_references={item.pk: f"REF-{item.pk}" for item in batch.items.all()})

    batch.refresh_from_db()
    assert batch.status == PaymentBatch.Status.EXECUTED
    assert batch.executed_at is not None
    for req in (r1, r2):
        req.refresh_from_db()
        assert req.status == DisbursementRequest.Status.PAID
        execution = DisbursementExecution.objects.get(request=req)
        assert execution.status == DisbursementExecution.Status.EXECUTED
        assert execution.execution_reference.startswith("REF-")


@pytest.mark.django_db
def test_execute_blocks_when_request_demoted_after_lock(applicant_user, institution):
    award = _award(applicant_user, institution)
    budget = _budget(award)
    r1 = _approved_request(budget, "100")
    batch = create_payment_batch([r1], batch_reference="BATCH-DEMOTED")
    lock_payment_batch(batch)
    # Simulate someone reverting the request between lock and execute.
    r1.status = DisbursementRequest.Status.REJECTED
    r1.save(update_fields=["status"])
    with pytest.raises(ValidationError):
        execute_payment_batch(batch)


@pytest.mark.django_db
def test_lock_empty_batch_rejected(applicant_user, institution):
    award = _award(applicant_user, institution)
    budget = _budget(award)
    r1 = _approved_request(budget, "100")
    batch = create_payment_batch([r1], batch_reference="BATCH-EMPTY")
    batch.items.all().delete()
    with pytest.raises(ValidationError):
        lock_payment_batch(batch)


@pytest.mark.django_db
def test_cancel_batch_detaches_items(applicant_user, institution):
    award = _award(applicant_user, institution)
    budget = _budget(award)
    r1 = _approved_request(budget, "100")
    batch = create_payment_batch([r1], batch_reference="BATCH-CANCEL")
    cancel_payment_batch(batch)
    batch.refresh_from_db()
    assert batch.status == PaymentBatch.Status.CANCELLED
    assert not PaymentBatchItem.objects.filter(request=r1).exists()
    # Free to be re-batched
    create_payment_batch([r1], batch_reference="BATCH-REUSE")


@pytest.mark.django_db
def test_executed_batch_cannot_be_cancelled(applicant_user, institution):
    award = _award(applicant_user, institution)
    budget = _budget(award)
    r1 = _approved_request(budget, "100")
    batch = create_payment_batch([r1], batch_reference="BATCH-EXECUTED-LOCKED")
    lock_payment_batch(batch)
    execute_payment_batch(batch)
    with pytest.raises(ValidationError):
        cancel_payment_batch(batch)
