"""PRD §5.4 NFRFM004 / FRFM026 / FRFM046 — variance scan + payment alerts."""

from __future__ import annotations

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

import pytest
from django.utils import timezone

from apps.core.notifications.models import Notification
from apps.rims.finance.models import (
    Budget,
    BudgetLine,
    DisbursementExecution,
    DisbursementRequest,
    PaymentSchedule,
)
from apps.rims.finance.services import save_execution_plan
from apps.rims.finance.tasks import remind_payment_due, scan_budget_variances
from apps.rims.grants.models import Application, Award, GrantCall
from apps.rims.operations.models import ReminderConfig, VarianceAlertConfig


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


def _award(applicant_user, institution, *, suffix: str, days_into_window: int = 60, total_window: int = 100) -> Award:
    call = GrantCall.objects.create(
        title=f"Var call {suffix}",
        slug=f"var-{suffix}",
        opens_at=timezone.now() - timedelta(days=days_into_window + 5),
        closes_at=timezone.now() + timedelta(days=14),
        status=GrantCall.Status.PUBLISHED,
    )
    app = Application.objects.create(call=call, applicant=applicant_user, institution=institution)
    activation = date.today() - timedelta(days=days_into_window)
    return Award.objects.create(
        application=app,
        amount=Decimal("1000"),
        currency="USD",
        activation_date=activation,
        project_end_date=activation + timedelta(days=total_window),
        status=Award.Status.ACTIVE,
    )


# ---------------------------------------------------------------------------
# scan_budget_variances
# ---------------------------------------------------------------------------
@pytest.mark.django_db
def test_variance_alert_disabled_when_config_off(applicant_user, institution, finance_user):
    award = _award(applicant_user, institution, suffix="off")
    budget = Budget.objects.create(award=award, name="B", currency="USD", status=Budget.Status.APPROVED)
    BudgetLine.objects.create(budget=budget, label="L", amount=Decimal("1000"))
    VarianceAlertConfig.objects.create(enabled=False, percent_threshold=Decimal("10"))
    assert scan_budget_variances() == 0


@pytest.mark.django_db
def test_variance_alert_below_threshold_no_notice(applicant_user, institution, finance_user):
    award = _award(applicant_user, institution, suffix="below")
    budget = Budget.objects.create(award=award, name="B", currency="USD", status=Budget.Status.APPROVED)
    BudgetLine.objects.create(budget=budget, label="L", amount=Decimal("1000"))
    # Spent 60% of budget at 60% elapsed → 0 gap.
    DisbursementRequest.objects.create(
        budget=budget,
        amount=Decimal("600"),
        status=DisbursementRequest.Status.PAID,
    )
    VarianceAlertConfig.objects.create(enabled=True, percent_threshold=Decimal("10"))
    assert scan_budget_variances() == 0


@pytest.mark.django_db
def test_variance_alert_above_threshold_fires_notice(
    applicant_user, institution, finance_user, grants_manager_user
):
    award = _award(applicant_user, institution, suffix="over")
    budget = Budget.objects.create(award=award, name="B", currency="USD", status=Budget.Status.APPROVED)
    BudgetLine.objects.create(budget=budget, label="L", amount=Decimal("1000"))
    # Spent 95% at 60% elapsed → 35% over, threshold 10%.
    DisbursementRequest.objects.create(
        budget=budget,
        amount=Decimal("950"),
        status=DisbursementRequest.Status.PAID,
    )
    VarianceAlertConfig.objects.create(enabled=True, percent_threshold=Decimal("10"))
    flagged = scan_budget_variances()
    assert flagged == 1
    assert Notification.objects.filter(
        recipient=finance_user, message__icontains="Variance alert"
    ).exists()


# ---------------------------------------------------------------------------
# remind_payment_due
# ---------------------------------------------------------------------------
@pytest.mark.django_db
def test_payment_reminder_targets_due_schedule(
    applicant_user, institution, finance_user, grants_manager_user
):
    award = _award(applicant_user, institution, suffix="pay")
    budget = Budget.objects.create(award=award, name="B", currency="USD", status=Budget.Status.APPROVED)
    PaymentSchedule.objects.create(
        budget=budget,
        due_on=date.today() + timedelta(days=3),
        amount=Decimal("100"),
        label="Tranche 1",
    )
    ReminderConfig.objects.create(closeout_warn_days_before=7)
    Notification.objects.all().delete()
    sent = remind_payment_due()
    assert sent == 1


@pytest.mark.django_db
def test_payment_reminder_skips_paid_schedule(applicant_user, institution):
    award = _award(applicant_user, institution, suffix="paid")
    budget = Budget.objects.create(award=award, name="B", currency="USD", status=Budget.Status.APPROVED)
    PaymentSchedule.objects.create(
        budget=budget,
        due_on=date.today() + timedelta(days=3),
        amount=Decimal("100"),
        label="Tranche 1",
        paid=True,
    )
    sent = remind_payment_due(warn_days_before=7)
    assert sent == 0


# ---------------------------------------------------------------------------
# FRFM046 — payee notification on EXECUTED transition
# ---------------------------------------------------------------------------
@pytest.mark.django_db
def test_executed_transition_notifies_requester(
    applicant_user, institution, finance_user, grants_manager_user
):
    award = _award(applicant_user, institution, suffix="exec")
    budget = Budget.objects.create(award=award, name="B", currency="USD", status=Budget.Status.APPROVED)
    BudgetLine.objects.create(budget=budget, label="L", amount=Decimal("1000"))
    req = DisbursementRequest.objects.create(
        budget=budget,
        amount=Decimal("100"),
        status=DisbursementRequest.Status.APPROVED,
        requested_by=grants_manager_user,
    )
    DisbursementExecution.objects.create(request=req)
    Notification.objects.all().delete()
    save_execution_plan(
        req,
        status=DisbursementExecution.Status.EXECUTED,
        payment_method=DisbursementRequest.PaymentMethod.BANK_TRANSFER,
        execution_reference="BANK-REF-XYZ",
    )
    assert Notification.objects.filter(
        recipient=grants_manager_user,
        message__icontains="executed",
    ).exists()
