"""PRD §5.4 FRFM010 / FRFM011 / NFRFM003 — formal Budget revisions."""

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

from apps.rims.finance.models import Budget, BudgetLine, BudgetRevision
from apps.rims.finance.services import (
    approve_budget_revision,
    create_budget_revision,
    reject_budget_revision,
)
from apps.rims.grants.models import Application, Award, GrantCall


@pytest.fixture(autouse=True)
def _no_async_dispatch():
    with patch(
        "apps.core.notifications.services.dispatch_notification_channel.delay"
    ) as fake:
        yield fake


@pytest.fixture
def budget_with_lines(applicant_user, institution):
    call = GrantCall.objects.create(
        title="Rev call",
        slug="rev-call",
        opens_at=date.today() - timedelta(days=1),
        closes_at=date.today() + timedelta(days=30),
        status=GrantCall.Status.PUBLISHED,
    )
    app = Application.objects.create(call=call, applicant=applicant_user, institution=institution)
    award = Award.objects.create(
        application=app,
        amount=Decimal("1000"),
        currency="USD",
        project_end_date=date.today() + timedelta(days=180),
        status=Award.Status.ACTIVE,
    )
    budget = Budget.objects.create(award=award, name="Default budget", currency="USD", status=Budget.Status.APPROVED)
    BudgetLine.objects.create(budget=budget, label="Line A", amount=Decimal("400"))
    BudgetLine.objects.create(budget=budget, label="Line B", amount=Decimal("600"))
    return budget


@pytest.mark.django_db
def test_create_revision_records_old_amounts(budget_with_lines, grants_manager_user):
    line_a = budget_with_lines.lines.get(label="Line A")
    rev = create_budget_revision(
        budget_with_lines,
        change_summary="Reallocate 100 from Line A to Line B",
        line_changes=[{"line_id": line_a.pk, "new_amount": "300"}],
        actor=grants_manager_user,
    )
    assert rev.status == BudgetRevision.Status.REQUESTED
    assert rev.version == 1
    rline = rev.lines.get()
    assert rline.old_amount == Decimal("400")
    assert rline.new_amount == Decimal("300")


@pytest.mark.django_db
def test_create_revision_requires_change_summary(budget_with_lines):
    line_a = budget_with_lines.lines.get(label="Line A")
    with pytest.raises(ValidationError):
        create_budget_revision(
            budget_with_lines,
            change_summary="",
            line_changes=[{"line_id": line_a.pk, "new_amount": "300"}],
        )


@pytest.mark.django_db
def test_create_revision_requires_line_changes(budget_with_lines):
    with pytest.raises(ValidationError):
        create_budget_revision(
            budget_with_lines,
            change_summary="Empty payload",
            line_changes=[],
        )


@pytest.mark.django_db
def test_approve_applies_line_amounts(budget_with_lines, grants_manager_user):
    line_a = budget_with_lines.lines.get(label="Line A")
    rev = create_budget_revision(
        budget_with_lines,
        change_summary="Cut Line A",
        line_changes=[{"line_id": line_a.pk, "new_amount": "250"}],
        actor=grants_manager_user,
    )
    approve_budget_revision(rev, actor=grants_manager_user, comment="Approved per donor")
    line_a.refresh_from_db()
    assert line_a.amount == Decimal("250")
    rev.refresh_from_db()
    assert rev.status == BudgetRevision.Status.APPROVED
    assert rev.decided_comment == "Approved per donor"


@pytest.mark.django_db
def test_reject_keeps_line_amounts(budget_with_lines, grants_manager_user):
    line_a = budget_with_lines.lines.get(label="Line A")
    rev = create_budget_revision(
        budget_with_lines,
        change_summary="Cut Line A",
        line_changes=[{"line_id": line_a.pk, "new_amount": "250"}],
    )
    reject_budget_revision(rev, actor=grants_manager_user, comment="Out of policy")
    line_a.refresh_from_db()
    assert line_a.amount == Decimal("400")
    rev.refresh_from_db()
    assert rev.status == BudgetRevision.Status.REJECTED


@pytest.mark.django_db
def test_reject_requires_comment(budget_with_lines, grants_manager_user):
    line_a = budget_with_lines.lines.get(label="Line A")
    rev = create_budget_revision(
        budget_with_lines,
        change_summary="Cut Line A",
        line_changes=[{"line_id": line_a.pk, "new_amount": "250"}],
    )
    with pytest.raises(ValidationError):
        reject_budget_revision(rev, actor=grants_manager_user, comment="")


@pytest.mark.django_db
def test_subsequent_revisions_increment_version(budget_with_lines, grants_manager_user):
    line_a = budget_with_lines.lines.get(label="Line A")
    rev1 = create_budget_revision(
        budget_with_lines,
        change_summary="r1",
        line_changes=[{"line_id": line_a.pk, "new_amount": "350"}],
    )
    rev2 = create_budget_revision(
        budget_with_lines,
        change_summary="r2",
        line_changes=[{"line_id": line_a.pk, "new_amount": "300"}],
    )
    assert rev1.version == 1
    assert rev2.version == 2
