"""PRD 6.1 Phase 1 RIMS + tracker smoke — HTTP, eligibility, discovery, API surface."""

import json
import uuid
from datetime import date, 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.core.authentication.models import UserProfile
from apps.core.permissions.roles import UserRole
from apps.rims.grants.models import Application, EligibilityRule, GrantCall, RequiredDocument, Review
from apps.rims.grants.services import assign_reviewer, award_application, submit_application, submit_review


@pytest.mark.django_db
def test_public_open_calls_list_anonymous_200(client):
    GrantCall.objects.create(
        title="Listed",
        slug=f"listed-{uuid.uuid4().hex[:8]}",
        opens_at=timezone.now() - timedelta(days=1),
        closes_at=timezone.now() + timedelta(days=20),
        status=GrantCall.Status.PUBLISHED,
    )
    url = reverse("rims_grants:call_list")
    resp = client.get(url)
    assert resp.status_code == 200


@pytest.mark.django_db
def test_open_calls_filter_querystring(client):
    GrantCall.objects.create(
        title="Alpha unique string xyz",
        slug=f"a-{uuid.uuid4().hex[:8]}",
        opens_at=timezone.now() - timedelta(days=1),
        closes_at=timezone.now() + timedelta(days=20),
        status=GrantCall.Status.PUBLISHED,
    )
    GrantCall.objects.create(
        title="Beta other",
        slug=f"b-{uuid.uuid4().hex[:8]}",
        opens_at=timezone.now() - timedelta(days=1),
        closes_at=timezone.now() + timedelta(days=20),
        status=GrantCall.Status.PUBLISHED,
    )
    resp = client.get(reverse("rims_grants:call_list"), {"q": "unique string xyz"})
    assert resp.status_code == 200
    assert b"Alpha unique string xyz" in resp.content
    assert b"Beta other" not in resp.content


@pytest.mark.django_db
def test_apply_blocked_after_deadline(client, applicant_user, institution):
    call = GrantCall.objects.create(
        title="Closed",
        slug=f"closed-{uuid.uuid4().hex[:8]}",
        opens_at=timezone.now() - timedelta(days=30),
        closes_at=timezone.now() - timedelta(days=1),
        status=GrantCall.Status.PUBLISHED,
    )
    client.force_login(applicant_user)
    url = reverse("rims_grants:call_apply", kwargs={"slug": call.slug})
    resp = client.post(url, {"institution": institution.pk})
    assert resp.status_code in (302, 400)
    assert Application.objects.filter(call=call, applicant=applicant_user).count() == 0


@pytest.mark.django_db
def test_eligibility_nationality_flags_but_does_not_reject(applicant_user, institution):
    """Eligibility is advisory: a failing nationality rule flags the application
    (eligibility_passed=False) but it still enters the review queue."""
    call = GrantCall.objects.create(
        title="Rule call",
        slug=f"rule-{uuid.uuid4().hex[:8]}",
        opens_at=timezone.now() - timedelta(days=1),
        closes_at=timezone.now() + timedelta(days=20),
        status=GrantCall.Status.PUBLISHED,
    )
    EligibilityRule.objects.create(
        call=call,
        rule_type=EligibilityRule.RuleType.NATIONALITY,
        params={"countries": ["Uganda"]},
        is_blocking=True,
    )
    UserProfile.objects.get_or_create(user=applicant_user, defaults={"country": "Kenya"})
    applicant_user.profile.country = "Kenya"
    applicant_user.profile.save()
    app = Application.objects.create(call=call, applicant=applicant_user, institution=institution)
    submit_application(app)
    fresh = Application.objects.get(pk=app.pk)
    assert fresh.status == Application.Status.UNDER_REVIEW
    assert fresh.eligibility_passed is False
    assert "Flagged" in (fresh.eligibility_notes or "")


@pytest.mark.django_db
def test_manager_application_list_requires_role(client, applicant_user, grants_manager_user, institution):
    call = GrantCall.objects.create(
        title="M",
        slug=f"m-{uuid.uuid4().hex[:8]}",
        opens_at=timezone.now(),
        closes_at=timezone.now() + timedelta(days=5),
        status=GrantCall.Status.PUBLISHED,
    )
    url = reverse("rims_grants:manager_application_list", kwargs={"slug": call.slug})
    client.force_login(applicant_user)
    assert client.get(url).status_code == 403
    client.force_login(grants_manager_user)
    resp = client.get(url)
    assert resp.status_code == 200
    content = resp.content.decode()
    assert "Applications" in content
    assert call.title in content


@pytest.mark.django_db
def test_manager_all_applications_list_requires_role(client, applicant_user, grants_manager_user, institution):
    url = reverse("rims_grants:manager_all_applications")
    client.force_login(applicant_user)
    assert client.get(url).status_code == 403
    client.force_login(grants_manager_user)
    resp = client.get(url)
    assert resp.status_code == 200
    assert b"All applications" in resp.content


@pytest.mark.django_db
def test_manager_call_manage_shows_checklist_and_application_preview(
    client, grants_manager_user, applicant_user, institution
):
    call = GrantCall.objects.create(
        title="Manage surface",
        slug=f"ms-{uuid.uuid4().hex[:8]}",
        opens_at=timezone.now() - timedelta(days=1),
        closes_at=timezone.now() + timedelta(days=5),
        status=GrantCall.Status.PUBLISHED,
    )
    RequiredDocument.objects.create(
        call=call, label="CV / Résumé", description="For PRD checklist UI.", is_required=True, sort_order=0
    )
    app = Application.objects.create(call=call, applicant=applicant_user, institution=institution)
    submit_application(app)
    client.force_login(grants_manager_user)
    url = reverse("rims_grants:call_detail_manage", kwargs={"slug": call.slug})
    resp = client.get(url)
    assert resp.status_code == 200
    body = resp.content.decode()
    assert "CV / Résumé" in body
    # Table cell renders {{ app.applicant.get_full_name|default:app.applicant.email }} —
    # applicant_user has a full name set (profile-completeness fixture, see
    # apps/rims/conftest.py), so the name takes precedence over the email fallback.
    assert applicant_user.get_full_name() in body
    assert "submission" in body


@pytest.mark.django_db
def test_reviewer_queue_requires_login(client, reviewer_user):
    url = reverse("rims_grants:review_queue")
    assert client.get(url).status_code == 302
    client.force_login(reviewer_user)
    assert client.get(url).status_code == 200


@pytest.mark.django_db
def test_aggregate_average_score_two_reviewers(applicant_user, institution, reviewer_user):
    from django.contrib.auth import get_user_model

    User = get_user_model()
    r2 = User.objects.create_user(email="r2@p1.test", password="pw")
    r2.role = UserRole.REVIEWER
    r2.save()

    call = GrantCall.objects.create(
        title="Avg",
        slug=f"avg-{uuid.uuid4().hex[:8]}",
        opens_at=timezone.now() - timedelta(days=1),
        closes_at=timezone.now() + timedelta(days=20),
        status=GrantCall.Status.PUBLISHED,
    )
    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
    assign_reviewer(app, reviewer_user, declare_conflict=False)
    assign_reviewer(app, r2, declare_conflict=False)
    rv1 = Review.objects.get(application=app, reviewer=reviewer_user)
    rv2 = Review.objects.get(application=app, reviewer=r2)
    submit_review(rv1, {"Overall": Decimal("20")}, "")
    submit_review(rv2, {"Overall": Decimal("10")}, "")

    from apps.rims.grants.services import aggregate_average_score

    assert aggregate_average_score(Application.objects.get(pk=app.pk)) == Decimal("15.00")


@pytest.mark.django_db
def test_flagged_application_still_reaches_reviewers(applicant_user, institution, reviewer_user):
    """A flagged (eligibility-failing) application now enters the review queue,
    so a reviewer CAN be assigned — a human screens the evidence rather than the
    system auto-rejecting it."""
    call = GrantCall.objects.create(
        title="Inelig",
        slug=f"inelig-{uuid.uuid4().hex[:8]}",
        opens_at=timezone.now() - timedelta(days=1),
        closes_at=timezone.now() + timedelta(days=20),
        status=GrantCall.Status.PUBLISHED,
    )
    EligibilityRule.objects.create(
        call=call,
        rule_type=EligibilityRule.RuleType.NATIONALITY,
        params={"countries": ["Uganda"]},
        is_blocking=True,
    )
    UserProfile.objects.get_or_create(user=applicant_user, defaults={"country": "Kenya"})
    applicant_user.profile.country = "Kenya"
    applicant_user.profile.save()
    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
    assert app.eligibility_passed is False
    # No exception — the flagged application is assignable for human review.
    assign_reviewer(app, reviewer_user, declare_conflict=False)
    assert Review.objects.filter(application=app, reviewer=reviewer_user).exists()


@pytest.mark.django_db
def test_award_blocked_when_interview_required_not_done(applicant_user, institution):
    call = GrantCall.objects.create(
        title="Interview call",
        slug=f"int-{uuid.uuid4().hex[:8]}",
        opens_at=timezone.now() - timedelta(days=1),
        closes_at=timezone.now() + timedelta(days=20),
        status=GrantCall.Status.PUBLISHED,
        interview_required=True,
    )
    app = Application.objects.create(call=call, applicant=applicant_user, institution=institution)
    submit_application(app)
    app = Application.objects.get(pk=app.pk)
    app.shortlist()
    app.save(update_fields=["status", "updated_at"])
    with pytest.raises(ValidationError, match="interview"):
        award_application(app, Decimal("1000"), date.today().replace(year=date.today().year + 1))


@pytest.mark.django_db
def test_award_allowed_when_interview_marked_done(applicant_user, institution):
    call = GrantCall.objects.create(
        title="Interview ok",
        slug=f"intok-{uuid.uuid4().hex[:8]}",
        opens_at=timezone.now() - timedelta(days=1),
        closes_at=timezone.now() + timedelta(days=20),
        status=GrantCall.Status.PUBLISHED,
        interview_required=True,
    )
    app = Application.objects.create(call=call, applicant=applicant_user, institution=institution)
    submit_application(app)
    app = Application.objects.get(pk=app.pk)
    app.interview_completed = True
    app.save(update_fields=["interview_completed", "updated_at"])
    app.shortlist()
    app.save(update_fields=["status", "updated_at"])
    award_application(app, Decimal("500"), date.today().replace(year=date.today().year + 1))
    assert Application.objects.get(pk=app.pk).status == Application.Status.AWARDED


@pytest.mark.django_db
def test_eligibility_degree_flags_but_does_not_reject(applicant_user, institution):
    """A failing degree rule flags the application but does not auto-reject it —
    the degree is evidenced by an uploaded document a human reviews."""
    call = GrantCall.objects.create(
        title="Degree call",
        slug=f"deg-{uuid.uuid4().hex[:8]}",
        opens_at=timezone.now() - timedelta(days=1),
        closes_at=timezone.now() + timedelta(days=20),
        status=GrantCall.Status.PUBLISHED,
    )
    EligibilityRule.objects.create(
        call=call,
        rule_type=EligibilityRule.RuleType.DEGREE,
        params={"levels": ["phd"]},
        is_blocking=True,
    )
    profile, _ = UserProfile.objects.get_or_create(user=applicant_user)
    profile.degree_level = UserProfile.DegreeLevel.MASTERS
    profile.save()
    app = Application.objects.create(call=call, applicant=applicant_user, institution=institution)
    submit_application(app)
    fresh = Application.objects.get(pk=app.pk)
    assert fresh.status == Application.Status.UNDER_REVIEW
    assert fresh.eligibility_passed is False


@pytest.mark.django_db
def test_apply_get_not_blocked_by_failing_degree_rule(client, applicant_user, institution):
    """Eligibility is advisory: the apply form renders even when a blocking
    degree rule fails (degree level isn't collected on the form — it's evidenced
    by an uploaded document), and the failure surfaces as a warning."""
    call = GrantCall.objects.create(
        title="Degree gate call",
        slug=f"deggate-{uuid.uuid4().hex[:8]}",
        opens_at=timezone.now() - timedelta(days=1),
        closes_at=timezone.now() + timedelta(days=20),
        status=GrantCall.Status.PUBLISHED,
    )
    EligibilityRule.objects.create(
        call=call,
        rule_type=EligibilityRule.RuleType.DEGREE,
        params={"levels": ["phd"]},
        is_blocking=True,
    )
    # Applicant profile has no degree_level set — the rule will fail.
    UserProfile.objects.get_or_create(user=applicant_user)
    client.force_login(applicant_user)
    url = reverse("rims_grants:call_apply", kwargs={"slug": call.slug})
    resp = client.get(url)
    # Renders the form (200) rather than redirecting away (302).
    assert resp.status_code == 200
    body = resp.content.decode()
    assert "Degree level required" in body
    assert "You can still submit" in body


@pytest.mark.django_db
def test_apply_post_submits_despite_failing_degree_rule(client, applicant_user, institution):
    """Submission proceeds whether or not the applicant qualifies; the
    eligibility failure is recorded as an advisory flag and the application
    enters the review queue (a human screens it), rather than being auto-rejected."""
    call = GrantCall.objects.create(
        title="Degree gate post call",
        slug=f"deggatepost-{uuid.uuid4().hex[:8]}",
        opens_at=timezone.now() - timedelta(days=1),
        closes_at=timezone.now() + timedelta(days=20),
        status=GrantCall.Status.PUBLISHED,
    )
    EligibilityRule.objects.create(
        call=call,
        rule_type=EligibilityRule.RuleType.DEGREE,
        params={"levels": ["phd"]},
        is_blocking=True,
    )
    UserProfile.objects.get_or_create(user=applicant_user)
    client.force_login(applicant_user)
    url = reverse("rims_grants:call_apply", kwargs={"slug": call.slug})
    resp = client.post(
        url,
        {
            "institution": institution.pk,
            "proposal": "x" * 250,  # clears the FRFA-AM005 minimum-length check
        },
    )
    app = Application.objects.filter(call=call, applicant=applicant_user).first()
    assert app is not None  # submission was NOT blocked
    assert resp.status_code == 302
    assert resp.url == reverse("rims_grants:application_detail", kwargs={"pk": app.pk})
    # The eligibility failure is recorded as a flag, not used to reject upfront.
    assert app.status == Application.Status.UNDER_REVIEW
    assert app.eligibility_passed is False
    assert "Flagged" in (app.eligibility_notes or "")


@pytest.mark.django_db
def test_manager_call_list_403_staff_without_grant_role(client, django_user_model):
    u = django_user_model.objects.create_user(email="staff-only@p1.test", password="pw-phase1")
    u.is_staff = True
    u.role = UserRole.LEARNER
    u.save()
    client.force_login(u)
    assert client.get(reverse("rims_grants:manager_call_list")).status_code == 403


@pytest.mark.django_db
def test_openapi_schema_lists_rims_routes(client):
    url = reverse("schema")
    resp = client.get(url, HTTP_ACCEPT="application/json")
    assert resp.status_code == 200
    data = json.loads(resp.content)
    paths = data.get("paths") or {}
    assert any("grant-calls" in p for p in paths)
    assert any("applications" in p for p in paths)


@pytest.mark.django_db
def test_projects_list_403_for_applicant_200_for_grants_manager(client, applicant_user, grants_manager_user):
    url = reverse("rims_projects:project_list")
    client.force_login(applicant_user)
    assert client.get(url).status_code == 403
    client.force_login(grants_manager_user)
    assert client.get(url).status_code == 200


@pytest.mark.django_db
def test_scholars_list_403_for_applicant_200_for_grants_manager(client, applicant_user, grants_manager_user):
    url = reverse("rims_scholarships:scholar_list")
    client.force_login(applicant_user)
    assert client.get(url).status_code == 403
    client.force_login(grants_manager_user)
    assert client.get(url).status_code == 200


@pytest.mark.django_db
def test_operations_partners_403_for_applicant(client, applicant_user):
    url = reverse("rims_operations:partner_list")
    client.force_login(applicant_user)
    assert client.get(url).status_code == 403


@pytest.mark.django_db
def test_registration_stores_institution_fk(client, institution):
    email = f"reg-{uuid.uuid4().hex[:8]}@p1.test"
    resp = client.post(
        reverse("accounts:register"),
        {
            "email": email,
            "first_name": "A",
            "last_name": "B",
            "phone": "",
            "institution": str(institution.pk),
            "password1": "complex-pass-123!",
            "password2": "complex-pass-123!",
        },
    )
    assert resp.status_code == 302
    from django.contrib.auth import get_user_model

    u = get_user_model().objects.get(email=email)
    assert u.institution_id == institution.pk


@pytest.mark.django_db
def test_assign_reviewer_http_post_blocked_when_ineligible(
    client, grants_manager_user, applicant_user, institution, reviewer_user
):
    call = GrantCall.objects.create(
        title="Inelig",
        slug=f"inel-{uuid.uuid4().hex[:8]}",
        opens_at=timezone.now() - timedelta(days=1),
        closes_at=timezone.now() + timedelta(days=20),
        status=GrantCall.Status.PUBLISHED,
    )
    app = Application.objects.create(
        call=call,
        applicant=applicant_user,
        institution=institution,
        status=Application.Status.INELIGIBLE,
    )
    client.force_login(grants_manager_user)
    url = reverse("rims_grants:application_assign_reviewer", kwargs={"pk": app.pk})
    resp = client.post(url, {"reviewer": str(reviewer_user.pk)})
    assert resp.status_code == 302
    assert not Review.objects.filter(application=app).exists()


@pytest.mark.django_db
def test_applicant_application_detail_ineligible_copy(client, applicant_user, institution):
    call = GrantCall.objects.create(
        title="E",
        slug=f"e-{uuid.uuid4().hex[:8]}",
        opens_at=timezone.now() - timedelta(days=1),
        closes_at=timezone.now() + timedelta(days=20),
        status=GrantCall.Status.PUBLISHED,
    )
    app = Application.objects.create(
        call=call,
        applicant=applicant_user,
        institution=institution,
        status=Application.Status.INELIGIBLE,
        eligibility_notes="Failed rules: Degree level",
    )
    client.force_login(applicant_user)
    url = reverse("rims_grants:application_detail", kwargs={"pk": app.pk})
    resp = client.get(url)
    assert resp.status_code == 200
    assert b"Not eligible" in resp.content
    assert b"reviewer queue" in resp.content.lower()
