"""PRD §5.4 FRFM014/018-025 + FRFM031-039 — finance analytics tests."""
from __future__ import annotations

from datetime import date, timedelta
from decimal import Decimal

import pytest
from django.utils import timezone

from apps.rims.finance.models import (
    Budget,
    BudgetLine,
    DisbursementRequest,
    Expenditure,
    FundingSource,
)
from apps.rims.finance.services_analytics import (
    aggregate_variance_snapshot,
    compute_variance,
    cost_breakdown_by_category,
    donor_finance_packet,
    donor_finance_summary,
    spend_curve,
)
from apps.rims.grants.models import Application, Award, GrantCall
from apps.rims.grants.services import award_application, shortlist_application, submit_application


def _award_with_window(applicant_user, institution, *, days_in: int, total_days: int = 180):
    """Create an Award whose project window started ``days_in`` days ago and runs
    ``total_days`` days total.
    """
    call = GrantCall.objects.create(
        title="Variance call",
        slug=f"var-{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
    activation = date.today() - timedelta(days=days_in)
    award.activation_date = activation
    award.project_end_date = activation + timedelta(days=total_days)
    award.save(update_fields=["activation_date", "project_end_date"])
    return award


@pytest.mark.django_db
def test_compute_variance_returns_none_when_window_unknown(applicant_user, institution):
    award = _award_with_window(applicant_user, institution, days_in=10)
    award.activation_date = None
    award.save(update_fields=["activation_date"])
    budget = Budget.objects.create(award=award, name="No window")
    BudgetLine.objects.create(budget=budget, label="L", amount=Decimal("1000"))
    assert compute_variance(budget) is None


@pytest.mark.django_db
def test_compute_variance_flags_overspend(applicant_user, institution):
    award = _award_with_window(applicant_user, institution, days_in=18, total_days=180)
    budget = Budget.objects.create(award=award, name="Overpace")
    BudgetLine.objects.create(budget=budget, label="L", amount=Decimal("1000"))
    # ~10% time elapsed; disburse 50% of ceiling → overrun by ~40%
    DisbursementRequest.objects.create(
        budget=budget,
        amount=Decimal("500"),
        status=DisbursementRequest.Status.PAID,
    )
    v = compute_variance(budget)
    assert v is not None
    assert v.direction == "over"
    assert v.gap > 0.30


@pytest.mark.django_db
def test_aggregate_variance_snapshot_threshold(applicant_user, institution):
    award_over = _award_with_window(applicant_user, institution, days_in=18, total_days=180)
    b_over = Budget.objects.create(award=award_over, name="Over")
    BudgetLine.objects.create(budget=b_over, label="L", amount=Decimal("1000"))
    DisbursementRequest.objects.create(
        budget=b_over, amount=Decimal("500"), status=DisbursementRequest.Status.PAID
    )

    award_ok = _award_with_window(applicant_user, institution, days_in=90, total_days=180)
    b_ok = Budget.objects.create(award=award_ok, name="OnTrack")
    BudgetLine.objects.create(budget=b_ok, label="L", amount=Decimal("1000"))
    DisbursementRequest.objects.create(
        budget=b_ok, amount=Decimal("500"), status=DisbursementRequest.Status.PAID
    )

    flagged = aggregate_variance_snapshot(threshold_ratio=0.10)
    flagged_ids = {v.budget_id for v in flagged}
    assert b_over.pk in flagged_ids
    assert b_ok.pk not in flagged_ids


@pytest.mark.django_db
def test_cost_breakdown_aggregates_lines_and_expenditures(applicant_user, institution):
    award = _award_with_window(applicant_user, institution, days_in=10)
    budget = Budget.objects.create(award=award, name="Costs")
    line_p = BudgetLine.objects.create(
        budget=budget, label="Salaries", amount=Decimal("400"), category=BudgetLine.Category.PERSONNEL
    )
    BudgetLine.objects.create(
        budget=budget, label="Trip", amount=Decimal("100"), category=BudgetLine.Category.TRAVEL
    )
    Expenditure.objects.create(
        budget_line=line_p, amount=Decimal("250"), incurred_on=date.today()
    )
    slices = cost_breakdown_by_category(budget)
    by_cat = {s.category: s for s in slices}
    assert by_cat["personnel"].budgeted == Decimal("400")
    assert by_cat["personnel"].expended == Decimal("250")
    assert by_cat["travel"].budgeted == Decimal("100")
    assert by_cat["travel"].expended == Decimal("0")


@pytest.mark.django_db
def test_donor_packet_resolves_header_and_line_attribution(applicant_user, institution):
    donor = FundingSource.objects.create(name="USAID", code="usaid")
    award_a = _award_with_window(applicant_user, institution, days_in=10)
    award_b = _award_with_window(applicant_user, institution, days_in=10)
    # Budget A has the donor at the header level
    b_a = Budget.objects.create(award=award_a, name="Header-attributed", funding_source=donor)
    BudgetLine.objects.create(budget=b_a, label="L", amount=Decimal("500"))
    DisbursementRequest.objects.create(
        budget=b_a,
        amount=Decimal("200"),
        status=DisbursementRequest.Status.APPROVED,
        approved_at=timezone.now(),
    )
    # Budget B has a single line attributed to the donor
    b_b = Budget.objects.create(award=award_b, name="Line-attributed")
    line_b = BudgetLine.objects.create(
        budget=b_b, label="Donor line", amount=Decimal("300"), funding_source=donor
    )
    Expenditure.objects.create(
        budget_line=line_b, amount=Decimal("75"), incurred_on=date.today()
    )

    packet = donor_finance_packet(donor)
    assert packet.budget_count == 2
    assert {row["budget_name"] for row in packet.rows} == {"Header-attributed", "Line-attributed"}
    assert packet.disbursed_total == Decimal("200")
    assert packet.expended_total == Decimal("75")


@pytest.mark.django_db
def test_donor_packet_period_filter(applicant_user, institution):
    donor = FundingSource.objects.create(name="DFID", code="dfid")
    award = _award_with_window(applicant_user, institution, days_in=400, total_days=730)
    budget = Budget.objects.create(award=award, name="Periodic", funding_source=donor)
    line = BudgetLine.objects.create(budget=budget, label="L", amount=Decimal("1000"))
    Expenditure.objects.create(budget_line=line, amount=Decimal("100"), incurred_on=date.today() - timedelta(days=300))
    Expenditure.objects.create(budget_line=line, amount=Decimal("200"), incurred_on=date.today() - timedelta(days=10))
    in_window = donor_finance_packet(donor, period_start=date.today() - timedelta(days=30))
    assert in_window.expended_total == Decimal("200")
    out_of_window = donor_finance_packet(donor, period_end=date.today() - timedelta(days=200))
    assert out_of_window.expended_total == Decimal("100")


@pytest.mark.django_db
def test_spend_curve_buckets_by_month(applicant_user, institution):
    award = _award_with_window(applicant_user, institution, days_in=60)
    budget = Budget.objects.create(award=award, name="Curve")
    line = BudgetLine.objects.create(budget=budget, label="L", amount=Decimal("1000"))
    Expenditure.objects.create(
        budget_line=line, amount=Decimal("100"), incurred_on=date.today().replace(day=1)
    )
    Expenditure.objects.create(
        budget_line=line, amount=Decimal("50"), incurred_on=date.today().replace(day=1)
    )
    series = spend_curve(budget, bucket="month")
    assert series
    today_label = date.today().strftime("%Y-%m")
    matching = [r for r in series if r["label"] == today_label]
    assert matching
    assert matching[0]["expended"] == 150.0


@pytest.mark.django_db
def test_donor_finance_summary_returns_one_row_per_donor():
    FundingSource.objects.create(name="A", code="a")
    FundingSource.objects.create(name="B", code="b")
    summary = donor_finance_summary()
    assert {p.funding_source_name for p in summary} == {"A", "B"}
