"""PRD §5.4 FRFM001 / FRFM004 — multi-year budget setup with per-year allocations."""
from __future__ import annotations

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

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

from apps.core.audit.models import AuditLog
from apps.core.audit.tasks import persist_audit_log
from apps.rims.finance.models import Budget, BudgetLine, BudgetYearAllocation
from apps.rims.finance.services import split_budget_to_years
from apps.rims.grants.models import Award, Application, GrantCall


@pytest.fixture(autouse=True)
def _sync_audit():
    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


def _make_award(applicant_user, institution) -> Award:
    call = GrantCall.objects.create(
        title=f"6B test call {timezone.now().timestamp()}",
        slug=f"6b-{int(timezone.now().timestamp() * 1000) % 10_000_000}",
        call_type="grant",
        opens_at=timezone.now() - timedelta(days=200),
        closes_at=timezone.now() + timedelta(days=10),
        status=GrantCall.Status.PUBLISHED,
    )
    app = Application.objects.create(call=call, applicant=applicant_user, institution=institution)
    award = Award.objects.create(
        application=app,
        amount=Decimal("30000"),
        currency="UGX",
        awarded_at=timezone.now(),
        project_end_date=timezone.now().date() + timedelta(days=900),
    )
    return award


@pytest.mark.django_db
def test_budget_supports_multi_year_period(applicant_user, institution):
    award = _make_award(applicant_user, institution)
    today = timezone.now().date()
    budget = Budget.objects.create(
        award=award,
        name="Multi-year ops",
        currency="UGX",
        is_multi_year=True,
        period_start=today,
        period_end=today + timedelta(days=900),
    )
    assert budget.is_multi_year is True
    assert budget.period_end > budget.period_start


@pytest.mark.django_db
def test_clean_rejects_inverted_period(applicant_user, institution):
    award = _make_award(applicant_user, institution)
    today = timezone.now().date()
    budget = Budget(
        award=award,
        name="Inverted period",
        currency="UGX",
        is_multi_year=True,
        period_start=today,
        period_end=today - timedelta(days=10),
    )
    with pytest.raises(DjangoValidationError) as excinfo:
        budget.full_clean()
    assert "period_end" in excinfo.value.message_dict


@pytest.mark.django_db
def test_split_budget_to_years_writes_allocations_and_audit(
    applicant_user, institution, finance_user
):
    award = _make_award(applicant_user, institution)
    budget = Budget.objects.create(
        award=award,
        name="Multi-year ops",
        currency="UGX",
        is_multi_year=True,
    )
    BudgetLine.objects.create(budget=budget, label="Personnel", amount=Decimal("10000"))
    BudgetLine.objects.create(budget=budget, label="Travel", amount=Decimal("20000"))

    rows = split_budget_to_years(
        budget,
        [
            {"fiscal_year": 2026, "amount": Decimal("10000")},
            {"fiscal_year": 2027, "amount": Decimal("12000")},
            {"fiscal_year": 2028, "amount": Decimal("8000")},
        ],
        actor=finance_user,
    )
    assert len(rows) == 3
    assert BudgetYearAllocation.objects.filter(budget=budget).count() == 3
    assert AuditLog.objects.filter(action="BUDGET_YEAR_ALLOCATION_CHANGED").exists()


@pytest.mark.django_db
def test_split_budget_to_years_rejects_sum_mismatch(
    applicant_user, institution, finance_user
):
    award = _make_award(applicant_user, institution)
    budget = Budget.objects.create(award=award, name="Sum mismatch", currency="UGX")
    BudgetLine.objects.create(budget=budget, label="Line A", amount=Decimal("5000"))

    with pytest.raises(DjangoValidationError):
        split_budget_to_years(
            budget,
            [{"fiscal_year": 2026, "amount": Decimal("9999")}],
            actor=finance_user,
        )


@pytest.mark.django_db
def test_split_budget_to_years_idempotent_replace(
    applicant_user, institution, finance_user
):
    award = _make_award(applicant_user, institution)
    budget = Budget.objects.create(award=award, name="Idempotent", currency="UGX")
    BudgetLine.objects.create(budget=budget, label="Line A", amount=Decimal("1000"))

    split_budget_to_years(
        budget, [{"fiscal_year": 2026, "amount": Decimal("1000")}], actor=finance_user
    )
    assert BudgetYearAllocation.objects.filter(budget=budget).count() == 1
    # Replace with two years summing to the same total.
    split_budget_to_years(
        budget,
        [
            {"fiscal_year": 2026, "amount": Decimal("600")},
            {"fiscal_year": 2027, "amount": Decimal("400")},
        ],
        actor=finance_user,
    )
    assert BudgetYearAllocation.objects.filter(budget=budget).count() == 2
