"""PRD §5.1 FRFA-AM034 / FRFA-AM035 — Program Officer final decision gates.

FRFA-AM034: recording a final decision (Approved OR Rejected) requires
mandatory written decision comments before the decision can be saved.

FRFA-AM035: the system must validate that the total number of approved
applications does not exceed the call's configured max_awards before
confirming an approval, blocking with an explanatory message when it would
be exceeded.

Covers both the service-layer functions (reject_application /
approve_application) and the views that front them (single-application
reject modal, bulk reject, bulk "mark as selected" / approve).
"""
from __future__ import annotations

from datetime import timedelta
from decimal import Decimal

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

from apps.rims.grants.models import Application, GrantCall
from apps.rims.grants.services import (
    approve_application,
    reject_application,
    submit_application,
)


def _call(**overrides):
    base = dict(
        title="Decision-gate call",
        slug=f"decision-gate-{int(timezone.now().timestamp() * 1_000_000) % 10_000_000}",
        opens_at=timezone.now() - timedelta(days=1),
        closes_at=timezone.now() + timedelta(days=14),
        status=GrantCall.Status.PUBLISHED,
    )
    base.update(overrides)
    return GrantCall.objects.create(**base)


def _shortlisted_app(call, applicant, institution):
    """Build an application that has reached SHORTLISTED.

    ``submit_application`` already auto-screens eligibility and advances the
    application to UNDER_REVIEW when ``call.programme_officer_screening_enabled``
    is False (the default used by ``_call`` here), so only the final
    ``shortlist()`` transition is needed on top of that.
    """
    app = Application.objects.create(call=call, applicant=applicant, institution=institution)
    submit_application(app)
    app = Application.objects.get(pk=app.pk)
    assert app.status == Application.Status.UNDER_REVIEW
    app.shortlist()
    app.save(update_fields=["status", "updated_at"])
    return Application.objects.get(pk=app.pk)


def _second_applicant(institution, email="second.applicant@example.com"):
    from django.contrib.auth import get_user_model

    from apps.core.permissions.roles import UserRole

    User = get_user_model()
    u = User.objects.create_user(email=email, password="pw-decision-gate")
    u.institution = institution
    u.role = UserRole.SCHOLAR
    u.save()
    return u


# ── Service-layer: mandatory comment ────────────────────────────────────


@pytest.mark.django_db
def test_reject_application_without_comment_raises(applicant_user, institution):
    call = _call()
    app = _shortlisted_app(call, applicant_user, institution)
    with pytest.raises(ValidationError):
        reject_application(app, comment="")
    assert Application.objects.get(pk=app.pk).status == Application.Status.SHORTLISTED


@pytest.mark.django_db
def test_reject_application_with_comment_succeeds(applicant_user, institution):
    call = _call()
    app = _shortlisted_app(call, applicant_user, institution)
    reject_application(app, comment="Does not meet the technical merit bar.")
    refreshed = Application.objects.get(pk=app.pk)
    assert refreshed.status == Application.Status.REJECTED
    assert refreshed.final_decision_comment == "Does not meet the technical merit bar."


@pytest.mark.django_db
def test_approve_application_without_comment_raises(applicant_user, institution):
    call = _call(max_awards=5)
    app = _shortlisted_app(call, applicant_user, institution)
    with pytest.raises(ValidationError):
        approve_application(app, comment="   ")
    assert Application.objects.get(pk=app.pk).status == Application.Status.SHORTLISTED


# ── Service-layer: max_awards cap enforcement ───────────────────────────


@pytest.mark.django_db
def test_approve_application_under_cap_succeeds(applicant_user, institution):
    call = _call(max_awards=2)
    app = _shortlisted_app(call, applicant_user, institution)
    approve_application(app, comment="Strong proposal, approved.")
    refreshed = Application.objects.get(pk=app.pk)
    assert refreshed.status == Application.Status.APPROVED
    assert refreshed.final_decision_comment == "Strong proposal, approved."


@pytest.mark.django_db
def test_approve_application_at_cap_is_blocked(applicant_user, institution):
    call = _call(max_awards=1)
    already_approved_applicant = _second_applicant(institution, "already-approved@example.com")
    approved_app = _shortlisted_app(call, already_approved_applicant, institution)
    approve_application(approved_app, comment="First awardee approved.")
    assert Application.objects.get(pk=approved_app.pk).status == Application.Status.APPROVED

    # Cap is 1 and already reached — the next approval must be blocked.
    candidate_app = _shortlisted_app(call, applicant_user, institution)
    with pytest.raises(ValidationError, match="maximum of 1"):
        approve_application(candidate_app, comment="Second awardee attempt.")
    unchanged = Application.objects.get(pk=candidate_app.pk)
    assert unchanged.status == Application.Status.SHORTLISTED
    assert unchanged.final_decision_comment == ""


@pytest.mark.django_db
def test_approve_application_no_cap_configured_is_unbounded(applicant_user, institution):
    """max_awards is null/0 — the PRD-documented 'no cap configured' case."""
    call = _call(max_awards=None)
    already_approved_applicant = _second_applicant(institution, "no-cap-first@example.com")
    approved_app = _shortlisted_app(call, already_approved_applicant, institution)
    approve_application(approved_app, comment="Approved under no-cap call.")

    candidate_app = _shortlisted_app(call, applicant_user, institution)
    approve_application(candidate_app, comment="Also approved — no cap configured.")
    assert Application.objects.get(pk=candidate_app.pk).status == Application.Status.APPROVED


# ── View-layer: single reject (manager_application_detail modal) ───────


@pytest.mark.django_db
def test_reject_view_without_comment_blocked(client, grants_manager_user, applicant_user, institution):
    call = _call()
    app = _shortlisted_app(call, applicant_user, institution)
    client.force_login(grants_manager_user)
    response = client.post(
        reverse("rims_grants:application_reject", kwargs={"pk": app.pk}),
        data={"notes": ""},
        follow=True,
    )
    assert response.status_code == 200
    messages = [str(m) for m in response.context["messages"]]
    assert any("comment" in m.lower() for m in messages)
    assert Application.objects.get(pk=app.pk).status == Application.Status.SHORTLISTED


@pytest.mark.django_db
def test_reject_view_with_comment_succeeds(client, grants_manager_user, applicant_user, institution):
    call = _call()
    app = _shortlisted_app(call, applicant_user, institution)
    client.force_login(grants_manager_user)
    response = client.post(
        reverse("rims_grants:application_reject", kwargs={"pk": app.pk}),
        data={"notes": "Ineligible institution for this cycle."},
        follow=True,
    )
    assert response.status_code == 200
    refreshed = Application.objects.get(pk=app.pk)
    assert refreshed.status == Application.Status.REJECTED
    assert refreshed.final_decision_comment == "Ineligible institution for this cycle."


# ── View-layer: bulk "mark as selected" (approve) with max_awards cap ───


@pytest.mark.django_db
def test_bulk_mark_selected_without_comment_blocked(client, grants_manager_user, applicant_user, institution):
    call = _call(max_awards=5)
    app = _shortlisted_app(call, applicant_user, institution)
    client.force_login(grants_manager_user)
    response = client.post(
        reverse("rims_grants:pipeline_bulk_mark_selected", kwargs={"slug": call.slug}),
        data={"selected": [str(app.pk)], "notes": ""},
        follow=True,
    )
    assert response.status_code == 200
    messages = [str(m) for m in response.context["messages"]]
    assert any("comment" in m.lower() for m in messages)
    assert Application.objects.get(pk=app.pk).status == Application.Status.SHORTLISTED


@pytest.mark.django_db
def test_bulk_mark_selected_at_cap_blocked_with_clear_message(
    client, grants_manager_user, applicant_user, institution
):
    call = _call(max_awards=1)
    already_approved_applicant = _second_applicant(institution, "cap-view-first@example.com")
    approved_app = _shortlisted_app(call, already_approved_applicant, institution)
    approve_application(approved_app, comment="Already approved before this test's bulk action.")

    candidate_app = _shortlisted_app(call, applicant_user, institution)
    client.force_login(grants_manager_user)
    response = client.post(
        reverse("rims_grants:pipeline_bulk_mark_selected", kwargs={"slug": call.slug}),
        data={"selected": [str(candidate_app.pk)], "notes": "Trying to exceed the cap."},
        follow=True,
    )
    assert response.status_code == 200
    messages = [str(m) for m in response.context["messages"]]
    assert any("maximum of 1" in m for m in messages)
    unchanged = Application.objects.get(pk=candidate_app.pk)
    assert unchanged.status == Application.Status.SHORTLISTED
    assert unchanged.final_decision_comment == ""


@pytest.mark.django_db
def test_bulk_mark_selected_under_cap_succeeds(client, grants_manager_user, applicant_user, institution):
    call = _call(max_awards=3)
    app = _shortlisted_app(call, applicant_user, institution)
    client.force_login(grants_manager_user)
    response = client.post(
        reverse("rims_grants:pipeline_bulk_mark_selected", kwargs={"slug": call.slug}),
        data={"selected": [str(app.pk)], "notes": "Selected for this cohort."},
        follow=True,
    )
    assert response.status_code == 200
    refreshed = Application.objects.get(pk=app.pk)
    assert refreshed.status == Application.Status.APPROVED
    assert refreshed.final_decision_comment == "Selected for this cohort."
