"""PRD §5.1 FRFA-AM010 — extended Application status model.

Covers the four new states:
- WITHDRAWN (applicant self-withdrawal)
- WITHDRAWN_NO_RESPONSE (system auto-withdraw)
- REJECTED_INELIGIBLE (eligibility-driven rejection, distinct from merit reject)
- EXPIRED_DRAFT (draft locked out when call auto-closes)

Plus integration with the P0.2 auto-close sweep: draft applications on a
call that passes its deadline are transitioned to EXPIRED_DRAFT so they
cannot be submitted after the fact.
"""
from datetime import timedelta

import pytest
from django.utils import timezone
from django_fsm import TransitionNotAllowed

from apps.rims.grants.models import Application, GrantCall
from apps.rims.grants.services import submit_application
from apps.rims.grants.tasks import close_expired_calls


def _make_call(*, closes_in_days=10):
    return GrantCall.objects.create(
        title="Status flows",
        slug=f"ext-{int(timezone.now().timestamp())}-{closes_in_days}",
        opens_at=timezone.now() - timedelta(days=1),
        closes_at=timezone.now() + timedelta(days=closes_in_days),
        status=GrantCall.Status.PUBLISHED,
    )


@pytest.mark.django_db
def test_withdraw_from_submitted(applicant_user, institution):
    call = _make_call()
    app = Application.objects.create(call=call, applicant=applicant_user, institution=institution)
    submit_application(app)
    app = Application.objects.get(pk=app.pk)
    assert app.status == Application.Status.UNDER_REVIEW  # auto_screen passes
    app.withdraw()
    app.save(update_fields=["status", "updated_at"])
    assert Application.objects.get(pk=app.pk).status == Application.Status.WITHDRAWN


@pytest.mark.django_db
def test_withdraw_from_draft(applicant_user, institution):
    call = _make_call()
    app = Application.objects.create(call=call, applicant=applicant_user, institution=institution)
    assert app.status == Application.Status.DRAFT
    app.withdraw()
    app.save(update_fields=["status", "updated_at"])
    assert Application.objects.get(pk=app.pk).status == Application.Status.WITHDRAWN


@pytest.mark.django_db
def test_cannot_withdraw_after_awarded(applicant_user, institution):
    """Terminal states lock out withdrawal — reporting and audit integrity."""
    from datetime import date
    from decimal import Decimal
    from apps.rims.grants.services import award_application, shortlist_application

    call = _make_call()
    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, Decimal("1000"), date.today() + timedelta(days=30), narrative="")
    app = Application.objects.get(pk=app.pk)
    with pytest.raises(TransitionNotAllowed):
        app.withdraw()


@pytest.mark.django_db
def test_mark_no_response_withdrawal(applicant_user, institution):
    call = _make_call()
    app = Application.objects.create(call=call, applicant=applicant_user, institution=institution)
    submit_application(app)
    app = Application.objects.get(pk=app.pk)
    # Fast-track: call the direct FSM transition that the revision-deadline
    # sweep will ultimately invoke.
    app.mark_no_response_withdrawal()
    app.save(update_fields=["status", "updated_at"])
    assert Application.objects.get(pk=app.pk).status == Application.Status.WITHDRAWN_NO_RESPONSE


@pytest.mark.django_db
def test_reject_ineligible_is_distinct_from_reject(applicant_user, institution):
    call = _make_call()
    app = Application.objects.create(
        call=call, applicant=applicant_user, institution=institution, status=Application.Status.DRAFT
    )
    app.submit()
    app.save(update_fields=["status", "updated_at"])
    app = Application.objects.get(pk=app.pk)
    app.reject_ineligible()
    app.save(update_fields=["status", "updated_at"])
    fresh = Application.objects.get(pk=app.pk)
    assert fresh.status == Application.Status.REJECTED_INELIGIBLE
    assert fresh.status != Application.Status.REJECTED


@pytest.mark.django_db
def test_expire_draft_via_auto_close_sweep(applicant_user, institution):
    """End-to-end: a DRAFT application on a call whose closes_at is now past
    is transitioned to EXPIRED_DRAFT by the `close_expired_calls` beat task."""
    call = GrantCall.objects.create(
        title="Will expire",
        slug=f"will-expire-{int(timezone.now().timestamp())}",
        opens_at=timezone.now() - timedelta(days=10),
        closes_at=timezone.now() - timedelta(minutes=1),
        status=GrantCall.Status.PUBLISHED,
    )
    draft = Application.objects.create(call=call, applicant=applicant_user, institution=institution)
    assert draft.status == Application.Status.DRAFT

    close_expired_calls()
    draft = Application.objects.get(pk=draft.pk)
    call = GrantCall.objects.get(pk=call.pk)
    assert call.status == GrantCall.Status.CLOSED
    assert draft.status == Application.Status.EXPIRED_DRAFT


@pytest.mark.django_db
def test_expire_draft_idempotent(applicant_user, institution):
    """Re-running the sweep must not re-transition an already-expired draft,
    and must not accidentally expire drafts on still-open calls."""
    open_call = _make_call(closes_in_days=10)
    open_draft = Application.objects.create(
        call=open_call, applicant=applicant_user, institution=institution
    )
    # Tick 1: no expired calls exist yet.
    close_expired_calls()
    assert Application.objects.get(pk=open_draft.pk).status == Application.Status.DRAFT
