"""PRD §5.1 FRFA-AA021/AA022 — amendment approval must be Programme-Director-only.

Before this fix, ``AwardAmendmentDecisionView`` used ``GrantsManagerMixin`` for
both proposing *and* deciding an amendment — the same Grants Manager who
raised a change to an award's terms could also approve it themselves, a
segregation-of-duties gap. This suite confirms: (1) a Grants Manager cannot
reach the approve/reject endpoints (mirrors the ``ProgramDirectorMixin``
already used for award cancellations), and (2) a Programme Director can
approve (terms updated, new amendment version, GM + awardee notified) or
reject (mandatory reason, terms unchanged, GM notified).
"""
from __future__ import annotations

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

import pytest
from django.contrib.auth import get_user_model
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, AwardAmendment, FundingRecord, GrantCall
from apps.rims.grants.services import (
    award_application,
    create_award_amendment,
    shortlist_application,
    submit_application,
)

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.amendment@example.com", password="pw-amend")
    u.role = UserRole.PROGRAM_DIRECTOR
    u.save()
    return u


def _awarded(applicant_user, institution, grants_manager_user, finance_user, *, suffix, amount=Decimal("1000")) -> Award:
    fr = FundingRecord.objects.create(
        title=f"Amendment PD FR {suffix}",
        amount=Decimal("100000"),
        start_date=timezone.now().date() - timedelta(days=10),
        end_date=timezone.now().date() + timedelta(days=365),
        grant_manager=grants_manager_user,
        finance_officer=finance_user,
        status=FundingRecord.Status.APPROVED,
    )
    call = GrantCall.objects.create(
        title=f"Amendment PD call {suffix}",
        slug=f"amend-pd-{suffix}-{int(timezone.now().timestamp() * 1000) % 10_000_000}",
        opens_at=timezone.now() - timedelta(days=1),
        closes_at=timezone.now() + timedelta(days=10),
        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_application(app, amount, timezone.now().date() + timedelta(days=180), narrative="")
    return Application.objects.get(pk=app.pk).award


def _pending_amendment(award, grants_manager_user, *, new_amount="1500.00") -> AwardAmendment:
    return create_award_amendment(
        award,
        change_summary="Increase award amount",
        justification="Additional co-funding secured.",
        new_terms={"amount": new_amount},
        actor=grants_manager_user,
    )


@pytest.mark.django_db
def test_grants_manager_cannot_approve_own_amendment(client, applicant_user, institution, grants_manager_user, finance_user):
    award = _awarded(applicant_user, institution, grants_manager_user, finance_user, suffix="gm-approve")
    amendment = _pending_amendment(award, grants_manager_user)
    client.force_login(grants_manager_user)

    resp = client.post(reverse("rims_grants:award_amendment_approve", args=[amendment.pk]))

    assert resp.status_code == 403
    # AwardAmendment.status is a protected FSMField — refresh_from_db() raises
    # AttributeError on it, so re-fetch via .get() instead.
    amendment = AwardAmendment.objects.get(pk=amendment.pk)
    assert amendment.status == AwardAmendment.Status.REQUESTED


@pytest.mark.django_db
def test_grants_manager_cannot_reject_own_amendment(client, applicant_user, institution, grants_manager_user, finance_user):
    award = _awarded(applicant_user, institution, grants_manager_user, finance_user, suffix="gm-reject")
    amendment = _pending_amendment(award, grants_manager_user)
    client.force_login(grants_manager_user)

    resp = client.post(
        reverse("rims_grants:award_amendment_reject", args=[amendment.pk]),
        {"reason": "Changed my mind"},
    )

    assert resp.status_code == 403
    amendment = AwardAmendment.objects.get(pk=amendment.pk)
    assert amendment.status == AwardAmendment.Status.REQUESTED


@pytest.mark.django_db
def test_program_director_can_approve_amendment_and_terms_update(
    client, applicant_user, institution, grants_manager_user, program_director_user, finance_user
):
    award = _awarded(
        applicant_user, institution, grants_manager_user, finance_user, suffix="pd-approve", amount=Decimal("1000")
    )
    amendment = _pending_amendment(award, grants_manager_user, new_amount="1500.00")
    client.force_login(program_director_user)

    resp = client.post(reverse("rims_grants:award_amendment_approve", args=[amendment.pk]))

    assert resp.status_code == 302
    amendment = AwardAmendment.objects.get(pk=amendment.pk)
    assert amendment.status == AwardAmendment.Status.APPROVED
    assert amendment.approver_id == program_director_user.pk
    award.refresh_from_db()
    assert award.amount == Decimal("1500.00")

    # GM + awardee notified.
    notified_recipient_ids = set(Notification.objects.values_list("recipient_id", flat=True))
    assert grants_manager_user.pk in notified_recipient_ids
    assert applicant_user.pk in notified_recipient_ids


@pytest.mark.django_db
def test_program_director_can_reject_amendment_with_mandatory_reason(
    client, applicant_user, institution, grants_manager_user, program_director_user, finance_user
):
    award = _awarded(
        applicant_user, institution, grants_manager_user, finance_user, suffix="pd-reject", amount=Decimal("1000")
    )
    amendment = _pending_amendment(award, grants_manager_user, new_amount="1500.00")
    client.force_login(program_director_user)

    # Missing reason is rejected by the service (ValidationError -> message, terms unchanged).
    resp = client.post(reverse("rims_grants:award_amendment_reject", args=[amendment.pk]), {"reason": ""})
    assert resp.status_code == 302
    amendment = AwardAmendment.objects.get(pk=amendment.pk)
    assert amendment.status == AwardAmendment.Status.REQUESTED

    resp = client.post(
        reverse("rims_grants:award_amendment_reject", args=[amendment.pk]),
        {"reason": "Insufficient justification for the increase."},
    )
    assert resp.status_code == 302
    amendment = AwardAmendment.objects.get(pk=amendment.pk)
    assert amendment.status == AwardAmendment.Status.REJECTED
    assert amendment.rejection_reason == "Insufficient justification for the increase."
    award.refresh_from_db()
    assert award.amount == Decimal("1000.00")


@pytest.mark.django_db
def test_amendment_detail_page_reachable_by_program_director_not_by_scholar(
    client, applicant_user, institution, grants_manager_user, program_director_user, finance_user
):
    award = _awarded(applicant_user, institution, grants_manager_user, finance_user, suffix="pd-detail")
    amendment = _pending_amendment(award, grants_manager_user)

    client.force_login(program_director_user)
    resp = client.get(reverse("rims_grants:award_amendment_detail", args=[amendment.pk]))
    assert resp.status_code == 200
    assert resp.context["can_decide"] is True

    client.force_login(applicant_user)
    resp = client.get(reverse("rims_grants:award_amendment_detail", args=[amendment.pk]))
    assert resp.status_code == 403
