"""PRD §5.1 FRFA-AA011 — tranche schedule must sum exactly to the award amount.

Before this fix, ``AwardTrancheCreateView`` assigned ``tranche.award`` *after*
``form.is_valid()`` had already run ``full_clean()`` on the unsaved instance,
so ``AwardTranche.clean()``'s award-aware sum check never fired (it early-
returns when ``award_id is None``) — the "must equal the award amount" rule
was dead code with no inline error surfaced anywhere. The fix moves tranche
configuration onto an ``AwardTrancheFormSet`` (inline formset, same pattern
as ``GrantBudgetLineFormSet``) so: (1) Django sets each row's FK to the
parent award before per-row validation runs, fixing the ordering bug, and
(2) the *whole* submitted schedule is validated together so the sum-must-
equal-amount rule is actually enforceable (a single row can't know what its
siblings add up to).
"""
from __future__ import annotations

from datetime import timedelta
from decimal import Decimal

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

from apps.rims.grants.models import Application, Award, AwardTranche, GrantCall
from apps.rims.grants.services import award_application, shortlist_application, submit_application


def _awarded(applicant_user, institution, *, suffix, amount=Decimal("1000")) -> Award:
    call = GrantCall.objects.create(
        title=f"Tranche schedule call {suffix}",
        slug=f"tranche-sched-{suffix}-{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


def _management_form(*, total, initial=0):
    return {
        "tranches-TOTAL_FORMS": str(total),
        "tranches-INITIAL_FORMS": str(initial),
        "tranches-MIN_NUM_FORMS": "0",
        "tranches-MAX_NUM_FORMS": "1000",
    }


def _row(idx, *, label, amount, status="planned", sort_order=None, pk=None, delete=False):
    prefix = f"tranches-{idx}"
    data = {
        f"{prefix}-label": label,
        f"{prefix}-amount": str(amount),
        f"{prefix}-due_on": "",
        f"{prefix}-milestone": "",
        f"{prefix}-status": status,
        f"{prefix}-payment_reference": "",
        f"{prefix}-sort_order": str(sort_order if sort_order is not None else idx),
    }
    if pk is not None:
        data[f"{prefix}-id"] = str(pk)
    if delete:
        data[f"{prefix}-DELETE"] = "on"
    return data


@pytest.mark.django_db
def test_tranche_schedule_summing_exactly_saves(client, grants_manager_user, applicant_user, institution):
    award = _awarded(applicant_user, institution, suffix="exact", amount=Decimal("1000"))
    client.force_login(grants_manager_user)

    data = _management_form(total=2)
    data.update(_row(0, label="Inception", amount="600"))
    data.update(_row(1, label="Completion", amount="400"))

    resp = client.post(reverse("rims_grants:award_tranche_create", args=[award.pk]), data)

    assert resp.status_code == 302
    tranches = list(AwardTranche.objects.filter(award=award).order_by("sort_order"))
    assert len(tranches) == 2
    assert sum((t.amount for t in tranches), Decimal("0")) == Decimal("1000")


@pytest.mark.django_db
def test_tranche_schedule_under_award_amount_is_blocked(client, grants_manager_user, applicant_user, institution):
    award = _awarded(applicant_user, institution, suffix="under", amount=Decimal("1000"))
    client.force_login(grants_manager_user)

    data = _management_form(total=1)
    data.update(_row(0, label="Partial", amount="700"))

    resp = client.post(reverse("rims_grants:award_tranche_create", args=[award.pk]), data)

    assert resp.status_code == 400
    assert AwardTranche.objects.filter(award=award).count() == 0
    assert b"700" in resp.content  # non-form error should mention the configured total
    assert b"1000" in resp.content  # ...and the award amount it must reconcile to


@pytest.mark.django_db
def test_tranche_schedule_over_award_amount_is_blocked(client, grants_manager_user, applicant_user, institution):
    award = _awarded(applicant_user, institution, suffix="over", amount=Decimal("1000"))
    client.force_login(grants_manager_user)

    data = _management_form(total=2)
    data.update(_row(0, label="A", amount="700"))
    data.update(_row(1, label="B", amount="500"))

    resp = client.post(reverse("rims_grants:award_tranche_create", args=[award.pk]), data)

    assert resp.status_code == 400
    assert AwardTranche.objects.filter(award=award).count() == 0


@pytest.mark.django_db
def test_cancelled_tranches_excluded_from_required_total(client, grants_manager_user, applicant_user, institution):
    """A cancelled tranche's amount should not count toward the reconciliation
    total — the remaining active tranches must sum to the award amount on
    their own."""
    award = _awarded(applicant_user, institution, suffix="cancelled", amount=Decimal("1000"))
    existing = AwardTranche.objects.create(award=award, label="Stale", amount=Decimal("300"))
    client.force_login(grants_manager_user)

    data = _management_form(total=2, initial=1)
    data.update(_row(0, label="Stale", amount="300", status="cancelled", pk=existing.pk))
    data.update(_row(1, label="Full replacement", amount="1000"))

    resp = client.post(reverse("rims_grants:award_tranche_create", args=[award.pk]), data)

    assert resp.status_code == 302
    existing.refresh_from_db()
    assert existing.status == AwardTranche.Status.CANCELLED
    active_total = sum(
        (
            t.amount
            for t in AwardTranche.objects.filter(award=award).exclude(status=AwardTranche.Status.CANCELLED)
        ),
        Decimal("0"),
    )
    assert active_total == Decimal("1000")


@pytest.mark.django_db
def test_tranche_create_view_requires_grants_manager(client, applicant_user, institution):
    award = _awarded(applicant_user, institution, suffix="rbac", amount=Decimal("500"))
    client.force_login(applicant_user)

    resp = client.get(reverse("rims_grants:award_tranche_create", args=[award.pk]))

    assert resp.status_code == 403
