"""PRD §5.1 FRFA-AA016 / FRFA-AA017 — cancellation cascade regression suite."""

from __future__ import annotations

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

import pytest
from django.contrib.auth import get_user_model
from django.core.exceptions import ValidationError
from django.urls import reverse
from django.utils import timezone

from apps.core.notifications.models import Notification
from apps.core.permissions.roles import UserRole
from apps.rims.grants.models import (
    Application,
    Award,
    AwardCancellationRequest,
    AwardTranche,
    FundingRecord,
    GrantCall,
    RecoveryRequest,
)
from apps.rims.grants.services import (
    approve_cancellation_and_cascade,
    award_application,
    create_award_cancellation,
    reject_cancellation,
    shortlist_application,
    submit_application,
    submit_cancellation_for_review,
    withdraw_cancellation,
)

User = get_user_model()


@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 program_director_user(db):
    u = User.objects.create_user(
        email="director.phase4@example.com", password="pw-phase4"
    )
    u.role = UserRole.PROGRAM_DIRECTOR
    u.save()
    return u


def _setup_award(applicant_user, institution, grants_manager_user, finance_user, *, suffix):
    fr = FundingRecord.objects.create(
        title=f"Cancel FR {suffix}",
        amount=Decimal("100000"),
        start_date=date.today() - timedelta(days=10),
        end_date=date.today() + timedelta(days=365),
        grant_manager=grants_manager_user,
        finance_officer=finance_user,
        status=FundingRecord.Status.APPROVED,
    )
    call = GrantCall.objects.create(
        title=f"Cancel call {suffix}",
        slug=f"cancel-{suffix}",
        opens_at=timezone.now() - timedelta(days=1),
        closes_at=timezone.now() + timedelta(days=14),
        status=GrantCall.Status.PUBLISHED,
        funding_record=fr,
        budget_ceiling=Decimal("10000"),
    )
    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 = award_application(app, Decimal("4000"), date.today() + timedelta(days=180))
    return fr, call, award


def _seed_tranches(award):
    AwardTranche.objects.create(
        award=award,
        label="T1 (disbursed)",
        amount=Decimal("1000"),
        status=AwardTranche.Status.DISBURSED,
        disbursed_at=timezone.now() - timedelta(days=15),
    )
    AwardTranche.objects.create(
        award=award,
        label="T2 (planned)",
        amount=Decimal("1500"),
        status=AwardTranche.Status.PLANNED,
    )
    AwardTranche.objects.create(
        award=award,
        label="T3 (approved)",
        amount=Decimal("1500"),
        status=AwardTranche.Status.APPROVED,
    )


@pytest.mark.django_db
def test_create_cancellation_starts_in_requested_state(
    applicant_user, institution, grants_manager_user, finance_user
):
    """FRFA-AA016 — initial cancellation row is REQUESTED."""
    _, _, award = _setup_award(
        applicant_user, institution, grants_manager_user, finance_user, suffix="init"
    )
    cancellation = create_award_cancellation(
        award, reason="Awardee withdrew", actor=grants_manager_user
    )
    assert cancellation.status == AwardCancellationRequest.Status.REQUESTED
    assert cancellation.reason == "Awardee withdrew"


@pytest.mark.django_db
def test_submit_for_review_transitions_to_director_review(
    applicant_user, institution, grants_manager_user, finance_user, program_director_user
):
    """FRFA-AA016 — REQUESTED → DIRECTOR_REVIEW notifies Programme Director."""
    _, _, award = _setup_award(
        applicant_user, institution, grants_manager_user, finance_user, suffix="submit"
    )
    cancellation = create_award_cancellation(
        award, reason="Programme termination", actor=grants_manager_user
    )
    submit_cancellation_for_review(cancellation, actor=grants_manager_user)
    cancellation.refresh_from_db()
    assert cancellation.status == AwardCancellationRequest.Status.DIRECTOR_REVIEW
    assert Notification.objects.filter(
        recipient=program_director_user,
        verb=Notification.Verb.AWARD_CANCELLATION_REQUESTED,
    ).exists()


@pytest.mark.django_db
def test_withdraw_cancellation_blocks_after_director_review(
    applicant_user, institution, grants_manager_user, finance_user
):
    """FRFA-AA016 — withdraw is only allowed from REQUESTED."""
    _, _, award = _setup_award(
        applicant_user, institution, grants_manager_user, finance_user, suffix="wd"
    )
    cancellation = create_award_cancellation(
        award, reason="Reconsidered", actor=grants_manager_user
    )
    submit_cancellation_for_review(cancellation, actor=grants_manager_user)
    with pytest.raises(ValidationError):
        withdraw_cancellation(cancellation, actor=grants_manager_user)


@pytest.mark.django_db
def test_approve_cascade_cancels_undisbursed_tranches(
    applicant_user, institution, grants_manager_user, finance_user, program_director_user
):
    """FRFA-AA017 — approval cancels all non-disbursed tranches with cancelled_at stamped."""
    _, _, award = _setup_award(
        applicant_user, institution, grants_manager_user, finance_user, suffix="t"
    )
    _seed_tranches(award)
    cancellation = create_award_cancellation(
        award, reason="Withdrawal", actor=grants_manager_user
    )
    submit_cancellation_for_review(cancellation, actor=grants_manager_user)
    approve_cancellation_and_cascade(
        cancellation, decision_note="Approved", actor=program_director_user
    )

    cancelled = AwardTranche.objects.filter(
        award=award, status=AwardTranche.Status.CANCELLED
    )
    assert cancelled.count() == 2
    for tranche in cancelled:
        assert tranche.cancelled_at is not None
    # Disbursed tranche left untouched.
    assert AwardTranche.objects.filter(
        award=award, status=AwardTranche.Status.DISBURSED
    ).count() == 1


@pytest.mark.django_db
def test_approve_cascade_auto_creates_recovery_requests(
    applicant_user, institution, grants_manager_user, finance_user, program_director_user
):
    """FRFA-AA017 — disbursed tranches generate auto-recovery rows linked to the cancellation."""
    _, _, award = _setup_award(
        applicant_user, institution, grants_manager_user, finance_user, suffix="rec"
    )
    _seed_tranches(award)
    cancellation = create_award_cancellation(
        award, reason="Recovery test", actor=grants_manager_user
    )
    submit_cancellation_for_review(cancellation, actor=grants_manager_user)
    approve_cancellation_and_cascade(cancellation, actor=program_director_user)

    rec_qs = RecoveryRequest.objects.filter(cancellation=cancellation)
    assert rec_qs.count() == 1
    rec = rec_qs.first()
    assert rec.amount == Decimal("1000")
    assert rec.award_id == award.pk
    assert rec.tranche is not None


@pytest.mark.django_db
def test_approve_cascade_transitions_award_to_cancelled_with_stamp(
    applicant_user, institution, grants_manager_user, finance_user, program_director_user
):
    """FRFA-AA017 — Award.status becomes CANCELLED with cancelled_at stamped."""
    _, _, award = _setup_award(
        applicant_user, institution, grants_manager_user, finance_user, suffix="stat"
    )
    _seed_tranches(award)
    cancellation = create_award_cancellation(
        award, reason="Status test", actor=grants_manager_user
    )
    submit_cancellation_for_review(cancellation, actor=grants_manager_user)
    approve_cancellation_and_cascade(cancellation, actor=program_director_user)

    award.refresh_from_db()
    assert award.status == Award.Status.CANCELLED
    assert award.cancelled_at is not None
    cancellation.refresh_from_db()
    assert cancellation.cascade_summary["cancelled_tranches"] == 2
    assert cancellation.cascade_summary["recovery_count"] == 1


@pytest.mark.django_db
def test_approve_cascade_notifies_applicant_at_applicant_url(
    applicant_user, institution, grants_manager_user, finance_user, program_director_user
):
    """FRFA-AA017 — applicant notification routes via application_detail
    (never the manager-only URL)."""
    _, _, award = _setup_award(
        applicant_user, institution, grants_manager_user, finance_user, suffix="ntf"
    )
    cancellation = create_award_cancellation(
        award, reason="Notification test", actor=grants_manager_user
    )
    submit_cancellation_for_review(cancellation, actor=grants_manager_user)
    approve_cancellation_and_cascade(cancellation, actor=program_director_user)

    note = Notification.objects.get(
        recipient=applicant_user,
        verb=Notification.Verb.AWARD_CANCELLATION_APPROVED,
    )
    expected_path = reverse(
        "rims_grants:application_detail", kwargs={"pk": award.application_id}
    )
    assert note.action_url == expected_path
    assert "manage" not in note.action_url


@pytest.mark.django_db
def test_reject_cancellation_requires_note_and_leaves_award_active(
    applicant_user, institution, grants_manager_user, finance_user, program_director_user
):
    """FRFA-AA016 — rejection requires a note and does not run the cascade."""
    _, _, award = _setup_award(
        applicant_user, institution, grants_manager_user, finance_user, suffix="rej"
    )
    _seed_tranches(award)
    cancellation = create_award_cancellation(
        award, reason="Reject test", actor=grants_manager_user
    )
    submit_cancellation_for_review(cancellation, actor=grants_manager_user)

    with pytest.raises(ValidationError):
        reject_cancellation(cancellation, decision_note="", actor=program_director_user)

    reject_cancellation(
        cancellation, decision_note="Programme requires continuation", actor=program_director_user
    )
    cancellation.refresh_from_db()
    award.refresh_from_db()
    assert cancellation.status == AwardCancellationRequest.Status.REJECTED
    assert award.status == Award.Status.ACTIVE
    # No tranches were cancelled.
    assert AwardTranche.objects.filter(
        award=award, status=AwardTranche.Status.CANCELLED
    ).count() == 0
