"""Tests for the FRFA-AM040 archived-applications register.

Verifies:
- Authorised staff (grants manager / admin) can read the archive list.
- Only REJECTED / INELIGIBLE / WITHDRAWN / EXPIRED_DRAFT statuses appear.
- Active statuses (DRAFT / SUBMITTED / UNDER_REVIEW / APPROVED) are excluded.
- The CSV export streams the same filtered set.
- Status, year, and free-text query filters narrow the list correctly.

The Application model has a (call, applicant) unique constraint, so each row
in a fixture call needs a distinct applicant.
"""
from __future__ import annotations

from datetime import timedelta

import pytest
from django.urls import reverse
from django.utils import timezone

from apps.core.permissions.roles import UserRole
from apps.rims.grants.models import Application, GrantCall


def _user(model, email, role):
    role_value = role.value if hasattr(role, "value") else role
    u = model.objects.create_user(email=email, password="pw-archive-1234")
    u.role = role_value
    u.save(update_fields=["role"])
    # Privileged roles (admin, grants_manager, …) get bounced to MFA enrolment
    # by MFAEnforcementMiddleware on every request. Pre-seed a verified TOTP
    # device so the test client can reach RIMS pages directly.
    from apps.core.authentication.mfa_policy import PRIVILEGED_ROLES
    from apps.core.authentication.models import MFADevice

    if role_value in PRIVILEGED_ROLES:
        MFADevice.objects.create(
            user=u,
            device_type=MFADevice.DeviceType.TOTP,
            name="Test TOTP",
            secret="JBSWY3DPEHPK3PXP",
            is_verified=True,
        )
    return u


def _make_applicant(model, n: int):
    return _user(model, f"applicant.archive{n}@example.com", UserRole.APPLICANT)


@pytest.fixture
def grants_manager(db, django_user_model):
    return _user(django_user_model, "gm.archive@example.com", UserRole.GRANTS_MANAGER)


@pytest.fixture
def call(db):
    return GrantCall.objects.create(
        title="Archive Test Call",
        slug="archive-test-call",
        opens_at=timezone.now() - timedelta(days=10),
        closes_at=timezone.now() + timedelta(days=10),
        status=GrantCall.Status.PUBLISHED,
    )


def _archive_url():
    return reverse("rims_grants:application_archive_list")


@pytest.mark.django_db
def test_archive_lists_only_terminal_states(client, grants_manager, django_user_model, call):
    a1 = _make_applicant(django_user_model, 1)
    a2 = _make_applicant(django_user_model, 2)
    a3 = _make_applicant(django_user_model, 3)
    a4 = _make_applicant(django_user_model, 4)
    rejected = Application.objects.create(call=call, applicant=a1, status=Application.Status.REJECTED)
    withdrawn = Application.objects.create(call=call, applicant=a2, status=Application.Status.WITHDRAWN)
    # Active rows must NOT appear:
    Application.objects.create(call=call, applicant=a3, status=Application.Status.DRAFT)
    Application.objects.create(call=call, applicant=a4, status=Application.Status.UNDER_REVIEW)

    client.force_login(grants_manager)
    resp = client.get(_archive_url())
    assert resp.status_code == 200
    visible_ids = {app.pk for app in resp.context["applications"]}
    assert visible_ids == {rejected.pk, withdrawn.pk}


@pytest.mark.django_db
def test_archive_status_filter_narrows(client, grants_manager, django_user_model, call):
    a1 = _make_applicant(django_user_model, 1)
    a2 = _make_applicant(django_user_model, 2)
    rejected = Application.objects.create(call=call, applicant=a1, status=Application.Status.REJECTED)
    Application.objects.create(call=call, applicant=a2, status=Application.Status.WITHDRAWN)

    client.force_login(grants_manager)
    resp = client.get(_archive_url(), {"status": "rejected"})
    assert resp.status_code == 200
    assert {app.pk for app in resp.context["applications"]} == {rejected.pk}


@pytest.mark.django_db
def test_archive_query_filters_by_call_title(client, grants_manager, django_user_model, call):
    a1 = _make_applicant(django_user_model, 1)
    a2 = _make_applicant(django_user_model, 2)
    Application.objects.create(call=call, applicant=a1, status=Application.Status.REJECTED)
    other_call = GrantCall.objects.create(
        title="Other Call",
        slug="archive-other",
        opens_at=timezone.now() - timedelta(days=10),
        closes_at=timezone.now() + timedelta(days=10),
        status=GrantCall.Status.PUBLISHED,
    )
    Application.objects.create(call=other_call, applicant=a2, status=Application.Status.WITHDRAWN)

    client.force_login(grants_manager)
    resp = client.get(_archive_url(), {"q": "Archive Test"})
    assert resp.status_code == 200
    apps = resp.context["applications"]
    assert all(app.call.slug == call.slug for app in apps)


@pytest.mark.django_db
def test_archive_csv_export(client, grants_manager, django_user_model, call):
    a1 = _make_applicant(django_user_model, 1)
    a2 = _make_applicant(django_user_model, 2)
    Application.objects.create(call=call, applicant=a1, status=Application.Status.REJECTED)
    Application.objects.create(call=call, applicant=a2, status=Application.Status.WITHDRAWN)

    client.force_login(grants_manager)
    resp = client.get(_archive_url(), {"export": "csv"})
    assert resp.status_code == 200
    assert resp["Content-Type"].startswith("text/csv")
    body = resp.content.decode("utf-8").splitlines()
    # Header row + 2 data rows
    assert len(body) == 3
    assert body[0].startswith("Application ID")
    text = resp.content.decode("utf-8")
    assert "applicant.archive1@example.com" in text
    assert "applicant.archive2@example.com" in text


@pytest.mark.django_db
def test_archive_blocks_unprivileged_user(client, django_user_model, call):
    a1 = _make_applicant(django_user_model, 1)
    Application.objects.create(call=call, applicant=a1, status=Application.Status.REJECTED)
    client.force_login(a1)
    resp = client.get(_archive_url())
    # GrantsManagerMixin / RimsAccessMixin redirects (302) or denies (403)
    # for non-staff. Either is acceptable; what's not acceptable is a 200.
    assert resp.status_code in (302, 403)
