"""PRD §5.1 FRFA-AA023 / FRFA-AA024 — cohort addendum 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,
    AwardCohort,
    AwardLetterVersion,
    CohortAddendum,
    FundingRecord,
    GrantCall,
)
from apps.rims.grants.services import (
    approve_cohort_addendum,
    award_application,
    reject_cohort_addendum,
    request_cohort_addendum,
    shortlist_application,
    submit_application,
    submit_cohort_addendum_for_review,
    withdraw_cohort_addendum,
)

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.cohort@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"Cohort 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"Cohort call {suffix}",
        slug=f"cohort-{suffix}",
        opens_at=timezone.now() - timedelta(days=1),
        closes_at=timezone.now() + timedelta(days=14),
        status=GrantCall.Status.PUBLISHED,
        call_type=GrantCall.CallType.FELLOWSHIP,
        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


@pytest.mark.django_db
def test_request_cohort_addendum_creates_requested_row(
    applicant_user, institution, grants_manager_user, finance_user
):
    """FRFA-AA023 — addendum starts REQUESTED with snapshot of award state."""
    _, _, award = _setup_award(
        applicant_user, institution, grants_manager_user, finance_user, suffix="req"
    )
    addendum = request_cohort_addendum(
        award,
        reason="Adding 2027 cohort due to expanded scope",
        new_cohort_terms={
            "label": "2027 fellows",
            "target_participants": 10,
            "budget_allocation": "1500",
        },
        actor=grants_manager_user,
    )
    assert addendum.status == CohortAddendum.Status.REQUESTED
    # Snapshot captures award amount as-is from the in-memory Decimal.
    assert Decimal(addendum.previous_terms["amount"]) == Decimal("4000")
    assert addendum.new_terms["label"] == "2027 fellows"


@pytest.mark.django_db
def test_request_cohort_addendum_requires_reason_and_label(
    applicant_user, institution, grants_manager_user, finance_user
):
    """FRFA-AA023 — reason and label are mandatory."""
    _, _, award = _setup_award(
        applicant_user, institution, grants_manager_user, finance_user, suffix="val"
    )
    with pytest.raises(ValidationError):
        request_cohort_addendum(
            award,
            reason="",
            new_cohort_terms={"label": "x"},
            actor=grants_manager_user,
        )
    with pytest.raises(ValidationError):
        request_cohort_addendum(
            award,
            reason="ok",
            new_cohort_terms={"label": ""},
            actor=grants_manager_user,
        )


@pytest.mark.django_db
def test_submit_addendum_notifies_program_director(
    applicant_user, institution, grants_manager_user, finance_user, program_director_user
):
    """FRFA-AA023 — submit transitions and emits a notification to directors."""
    _, _, award = _setup_award(
        applicant_user, institution, grants_manager_user, finance_user, suffix="ntf"
    )
    addendum = request_cohort_addendum(
        award,
        reason="Late cohort",
        new_cohort_terms={"label": "x", "target_participants": 5, "budget_allocation": "100"},
        actor=grants_manager_user,
    )
    submit_cohort_addendum_for_review(addendum, actor=grants_manager_user)
    addendum.refresh_from_db()
    assert addendum.status == CohortAddendum.Status.DIRECTOR_REVIEW
    assert Notification.objects.filter(
        recipient=program_director_user,
        verb=Notification.Verb.COHORT_ADDENDUM_REQUESTED,
    ).exists()


@pytest.mark.django_db
def test_approve_addendum_creates_cohort_and_regenerates_letter(
    applicant_user, institution, grants_manager_user, finance_user, program_director_user
):
    """FRFA-AA024 — approval creates an AwardCohort row and stamps a letter version."""
    _, _, award = _setup_award(
        applicant_user, institution, grants_manager_user, finance_user, suffix="ok"
    )
    addendum = request_cohort_addendum(
        award,
        reason="Need a 2027 cohort to support the scaled programme",
        new_cohort_terms={
            "label": "2027 cohort",
            "target_participants": 12,
            "budget_allocation": "1500",
        },
        actor=grants_manager_user,
    )
    submit_cohort_addendum_for_review(addendum, actor=grants_manager_user)

    versions_before = AwardLetterVersion.objects.filter(award=award).count()
    approve_cohort_addendum(
        addendum, decision_notes="Approved", actor=program_director_user
    )
    addendum.refresh_from_db()

    assert addendum.status == CohortAddendum.Status.APPROVED
    assert addendum.cohort_id is not None
    cohort = addendum.cohort
    assert cohort.cohort_number == 1
    assert cohort.label == "2027 cohort"
    assert cohort.target_participants == 12
    assert cohort.budget_allocation == Decimal("1500.00")
    # Letter regenerated.
    assert (
        AwardLetterVersion.objects.filter(award=award).count() == versions_before + 1
    )


@pytest.mark.django_db
def test_approve_addendum_increments_cohort_number(
    applicant_user, institution, grants_manager_user, finance_user, program_director_user
):
    """FRFA-AA024 — second addendum gets cohort_number=2, not 1."""
    _, _, award = _setup_award(
        applicant_user, institution, grants_manager_user, finance_user, suffix="inc"
    )
    # Seed cohort #1 manually.
    AwardCohort.objects.create(
        award=award,
        cohort_number=1,
        label="2025 cohort",
        target_participants=10,
        budget_allocation=Decimal("1000"),
        status=AwardCohort.Status.ACTIVE,
    )
    addendum = request_cohort_addendum(
        award,
        reason="Second cohort",
        new_cohort_terms={
            "label": "2027 cohort",
            "target_participants": 8,
            "budget_allocation": "500",
        },
        actor=grants_manager_user,
    )
    submit_cohort_addendum_for_review(addendum, actor=grants_manager_user)
    approve_cohort_addendum(addendum, actor=program_director_user)
    assert addendum.cohort.cohort_number == 2


@pytest.mark.django_db
def test_reject_addendum_requires_notes_and_does_not_create_cohort(
    applicant_user, institution, grants_manager_user, finance_user, program_director_user
):
    """FRFA-AA024 — rejection requires notes and leaves the award untouched."""
    _, _, award = _setup_award(
        applicant_user, institution, grants_manager_user, finance_user, suffix="rej"
    )
    addendum = request_cohort_addendum(
        award,
        reason="Late cohort",
        new_cohort_terms={"label": "x", "target_participants": 3, "budget_allocation": "100"},
        actor=grants_manager_user,
    )
    submit_cohort_addendum_for_review(addendum, actor=grants_manager_user)

    with pytest.raises(ValidationError):
        reject_cohort_addendum(addendum, decision_notes="", actor=program_director_user)

    reject_cohort_addendum(
        addendum, decision_notes="Budget envelope exhausted", actor=program_director_user
    )
    addendum.refresh_from_db()
    assert addendum.status == CohortAddendum.Status.REJECTED
    assert AwardCohort.objects.filter(award=award).count() == 0


@pytest.mark.django_db
def test_withdraw_addendum_allowed_only_from_requested(
    applicant_user, institution, grants_manager_user, finance_user
):
    """FRFA-AA023 — withdraw is allowed only from REQUESTED."""
    _, _, award = _setup_award(
        applicant_user, institution, grants_manager_user, finance_user, suffix="wd"
    )
    addendum = request_cohort_addendum(
        award,
        reason="Will reconsider",
        new_cohort_terms={"label": "y", "target_participants": 1, "budget_allocation": "50"},
        actor=grants_manager_user,
    )
    withdraw_cohort_addendum(addendum, actor=grants_manager_user)
    addendum.refresh_from_db()
    assert addendum.status == CohortAddendum.Status.WITHDRAWN

    # Trying to withdraw again should fail.
    with pytest.raises(ValidationError):
        withdraw_cohort_addendum(addendum, actor=grants_manager_user)


@pytest.mark.django_db
def test_approved_addendum_notifies_applicant_at_applicant_url(
    applicant_user, institution, grants_manager_user, finance_user, program_director_user
):
    """FRFA-AA024 — applicant notification uses application_detail URL."""
    _, _, award = _setup_award(
        applicant_user, institution, grants_manager_user, finance_user, suffix="appurl"
    )
    addendum = request_cohort_addendum(
        award,
        reason="Add cohort",
        new_cohort_terms={"label": "x", "target_participants": 1, "budget_allocation": "100"},
        actor=grants_manager_user,
    )
    submit_cohort_addendum_for_review(addendum, actor=grants_manager_user)
    approve_cohort_addendum(addendum, actor=program_director_user)

    note = Notification.objects.get(
        recipient=applicant_user,
        verb=Notification.Verb.COHORT_ADDENDUM_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
