"""PRD §5.1 FRFA-GPS006/007/008/009 + NFRFA011 — operations admin UIs.

Smoke coverage for the staff-facing CRUD over Donor and StrategicObjective,
the Audit log viewer, and the ReviewerProfile integration into the
auto_assign_reviewers service.
"""

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.urls import reverse
from django.utils import timezone

from apps.core.audit.mixins import log_audit
from apps.core.permissions.roles import UserRole
from apps.rims.grants.models import (
    Application,
    GrantCall,
    StrategicObjective,
)
from apps.rims.grants.services import submit_application
from apps.rims.operations.models import Donor, ReviewerProfile

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 admin_user(db, institution):
    """Operations staff user — UserRole.ADMIN matches RimsAccessMixin
    allowed_roles, no per-perm grant needed."""
    u = User.objects.create_user(email="ops.admin@example.com", password="pw-ops")
    u.role = UserRole.ADMIN
    u.is_staff = True
    u.save()
    return u


# ---------------------------------------------------------------------------
# Donor staff CRUD
# ---------------------------------------------------------------------------
@pytest.mark.django_db
def test_donor_list_view_requires_login(client):
    resp = client.get(reverse("rims_operations:donor_list"))
    assert resp.status_code in (302, 403)


@pytest.mark.django_db
def test_donor_list_view_renders_for_admin(client, admin_user):
    Donor.objects.create(name="Test Donor", country="Uganda")
    client.force_login(admin_user)
    resp = client.get(reverse("rims_operations:donor_list"))
    assert resp.status_code == 200
    assert b"Test Donor" in resp.content


@pytest.mark.django_db
def test_donor_create_warns_on_duplicate_then_accepts_with_confirm(client, admin_user):
    """find_duplicate_donor matches case-insensitively, so even differing case
    triggers the warning. The DB unique constraint is case-sensitive, so a
    different casing + confirm_duplicate=1 is the intended bypass route."""
    Donor.objects.create(name="Existing Donor", country="Uganda")
    client.force_login(admin_user)

    # First attempt — same casing, name+country match: duplicate flagged.
    resp = client.post(
        reverse("rims_operations:donor_create"),
        {"name": "EXISTING DONOR", "country": "Uganda"},
    )
    assert resp.status_code == 200
    assert Donor.objects.filter(name__iexact="existing donor").count() == 1

    # Second attempt — different casing on the new row + confirm_duplicate=1.
    resp = client.post(
        reverse("rims_operations:donor_create"),
        {"name": "EXISTING DONOR", "country": "Uganda", "confirm_duplicate": "1"},
    )
    assert resp.status_code == 302
    assert Donor.objects.filter(name__iexact="existing donor").count() == 2


@pytest.mark.django_db
def test_donor_create_succeeds_for_unique_name_country(client, admin_user):
    client.force_login(admin_user)
    resp = client.post(
        reverse("rims_operations:donor_create"),
        {"name": "Brand New Donor", "country": "Kenya"},
    )
    assert resp.status_code == 302
    assert Donor.objects.filter(name="Brand New Donor", country="Kenya").exists()


# ---------------------------------------------------------------------------
# Strategic Objective staff CRUD
# ---------------------------------------------------------------------------
@pytest.mark.django_db
def test_strategic_objective_create_and_list(client, admin_user):
    client.force_login(admin_user)
    resp = client.post(
        reverse("rims_operations:strategic_objective_create"),
        {"name": "Inclusive Agriculture", "slug": "inclusive-ag", "active": "on"},
    )
    assert resp.status_code == 302
    obj = StrategicObjective.objects.get(slug="inclusive-ag")
    assert obj.name == "Inclusive Agriculture"

    resp = client.get(reverse("rims_operations:strategic_objective_list"))
    assert resp.status_code == 200
    assert b"Inclusive Agriculture" in resp.content


@pytest.mark.django_db
def test_strategic_objective_edit(client, admin_user):
    obj = StrategicObjective.objects.create(name="Initial", slug="initial", active=True)
    client.force_login(admin_user)
    resp = client.post(
        reverse("rims_operations:strategic_objective_edit", kwargs={"pk": obj.pk}),
        {"name": "Renamed", "slug": "renamed", "active": "on"},
    )
    assert resp.status_code == 302
    obj.refresh_from_db()
    assert obj.name == "Renamed"
    assert obj.slug == "renamed"


# ---------------------------------------------------------------------------
# Audit log viewer
# ---------------------------------------------------------------------------
@pytest.mark.django_db(transaction=True)
def test_audit_log_view_filters_by_action(client, admin_user):
    log_audit(
        actor=admin_user,
        action="CUSTOM_TEST_ACTION",
        target_app="rims_grants",
        target_model="Award",
        object_id="42",
    )
    log_audit(
        actor=admin_user,
        action="OTHER_ACTION",
        target_app="rims_grants",
        target_model="Award",
        object_id="43",
    )
    client.force_login(admin_user)
    resp = client.get(reverse("rims_operations:audit_log") + "?action=CUSTOM_TEST")
    assert resp.status_code == 200
    assert b"CUSTOM_TEST_ACTION" in resp.content
    assert b"OTHER_ACTION" not in resp.content


# ---------------------------------------------------------------------------
# ReviewerProfile integration with auto_assign_reviewers
# ---------------------------------------------------------------------------
@pytest.mark.django_db
def test_auto_assign_reviewers_skips_unavailable_profile(
    applicant_user, institution, reviewer_user
):
    # Build a call with reviewer pool so the call yields the reviewer_user candidate.
    call = GrantCall.objects.create(
        title="Pool call",
        slug="pool-call",
        opens_at=timezone.now() - timedelta(days=1),
        closes_at=timezone.now() + timedelta(days=14),
        status=GrantCall.Status.PUBLISHED,
        reviewers_per_application=1,
    )
    call.reviewer_pool.add(reviewer_user)
    app = Application.objects.create(call=call, applicant=applicant_user, institution=institution)
    submit_application(app)
    app = Application.objects.get(pk=app.pk)

    # Mark reviewer unavailable.
    ReviewerProfile.objects.create(user=reviewer_user, available=False, max_concurrent_reviews=5)

    from django.core.exceptions import ValidationError as _VE

    from apps.rims.grants.services import auto_assign_reviewers

    with pytest.raises(_VE):
        auto_assign_reviewers(app)


@pytest.mark.django_db
def test_auto_assign_reviewers_respects_max_concurrent_cap(
    applicant_user, institution, reviewer_user
):
    from apps.rims.grants.models import Review

    other_applicant = User.objects.create_user(email="other.app@example.com", password="x")
    other_applicant.institution = institution
    other_applicant.role = UserRole.SCHOLAR
    other_applicant.save()

    call = GrantCall.objects.create(
        title="Cap call",
        slug="cap-call",
        opens_at=timezone.now() - timedelta(days=1),
        closes_at=timezone.now() + timedelta(days=14),
        status=GrantCall.Status.PUBLISHED,
        reviewers_per_application=1,
    )
    call.reviewer_pool.add(reviewer_user)
    app = Application.objects.create(call=call, applicant=applicant_user, institution=institution)
    submit_application(app)
    app = Application.objects.get(pk=app.pk)

    # Reviewer already at the cap via a parallel application from a different
    # applicant on the same call.
    other_app = Application.objects.create(call=call, applicant=other_applicant, institution=institution)
    Review.objects.create(application=other_app, reviewer=reviewer_user, due_at=timezone.now() + timedelta(days=7))
    ReviewerProfile.objects.create(user=reviewer_user, available=True, max_concurrent_reviews=1)

    from django.core.exceptions import ValidationError as _VE

    from apps.rims.grants.services import auto_assign_reviewers

    with pytest.raises(_VE):
        auto_assign_reviewers(app)


@pytest.mark.django_db
def test_auto_assign_reviewers_proceeds_when_no_profile_exists(
    applicant_user, institution, reviewer_user
):
    """Legacy reviewers without a ReviewerProfile row are still considered."""
    from apps.rims.grants.services import auto_assign_reviewers

    call = GrantCall.objects.create(
        title="Legacy call",
        slug="legacy-call",
        opens_at=timezone.now() - timedelta(days=1),
        closes_at=timezone.now() + timedelta(days=14),
        status=GrantCall.Status.PUBLISHED,
        reviewers_per_application=1,
    )
    call.reviewer_pool.add(reviewer_user)
    app = Application.objects.create(call=call, applicant=applicant_user, institution=institution)
    submit_application(app)
    app = Application.objects.get(pk=app.pk)

    reviews = auto_assign_reviewers(app)
    assert len(reviews) == 1
    assert reviews[0].reviewer_id == reviewer_user.pk
