"""P0 (RIMS assessment §2) — RBAC re-gating of four critical views.

Verifies the four roles introduced for P0 hit the right surfaces:
- AUDITOR → AuditLogView (GET 200, POST blocked as read-only).
- FINANCE_MANAGER → ApproveDisbursementView.
- PROGRAM_COORDINATOR → BudgetUpdateView.
- PROJECT_MANAGER → DisbursementCreateView.

And the old gate (FinanceStaffMixin / GrantsStaffMixin) is correctly
restricted away from these surfaces where it used to leak.
"""
from __future__ import annotations

from datetime import timedelta
from decimal import Decimal

import pytest
from django.contrib.auth import get_user_model
from django.urls import reverse
from django.utils import timezone

from apps.core.permissions.roles import UserRole
from apps.rims.finance.models import Budget, BudgetLine, DisbursementRequest
from apps.rims.grants.models import Application, GrantCall
from apps.rims.grants.services import award_application, shortlist_application, submit_application

User = get_user_model()


def _user(role, email):
    user = User.objects.create_user(email=email, password="pw-p0")
    user.role = role
    user.save()
    return user


@pytest.fixture
def auditor_user(db):
    return _user(UserRole.AUDITOR, "auditor.p0@example.com")


@pytest.fixture
def finance_manager_user(db):
    return _user(UserRole.FINANCE_MANAGER, "fm.p0@example.com")


@pytest.fixture
def program_coordinator_user(db):
    return _user(UserRole.PROGRAM_COORDINATOR, "pc.p0@example.com")


@pytest.fixture
def project_manager_user(db):
    return _user(UserRole.PROJECT_MANAGER, "pm.p0@example.com")


@pytest.fixture
def some_applicant(db):
    return _user(UserRole.APPLICANT, "outsider.p0@example.com")


@pytest.fixture
def budget_with_request(applicant_user, institution):
    call = GrantCall.objects.create(
        title="P0 RBAC",
        slug=f"p0-rbac-{int(timezone.now().timestamp() * 1000) % 10_000_000}",
        opens_at=timezone.now() - timedelta(days=1),
        closes_at=timezone.now() + timedelta(days=10),
        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)
    shortlist_application(app)
    award_application(
        app,
        Decimal("1000"),
        timezone.now().date() + timedelta(days=180),
        narrative="",
    )
    award = Application.objects.get(pk=app.pk).award
    budget = Budget.objects.create(award=award, name="P0 RBAC budget", currency=award.currency)
    BudgetLine.objects.create(budget=budget, label="L", amount=Decimal("9999"))
    req = DisbursementRequest.objects.create(
        budget=budget,
        amount=Decimal("100"),
        status=DisbursementRequest.Status.SUBMITTED,
    )
    return budget, req


# ---------------------------------------------------------------------------
# AuditLogView — auditor only, read-only.
# ---------------------------------------------------------------------------

@pytest.mark.django_db
def test_auditor_can_view_audit_log(client, auditor_user):
    client.force_login(auditor_user)
    resp = client.get(reverse("rims_operations:audit_log"))
    assert resp.status_code == 200


@pytest.mark.django_db
def test_non_auditor_blocked_from_audit_log(client, some_applicant):
    client.force_login(some_applicant)
    resp = client.get(reverse("rims_operations:audit_log"))
    assert resp.status_code == 403


# ---------------------------------------------------------------------------
# ApproveDisbursementView — finance manager only.
# ---------------------------------------------------------------------------

@pytest.mark.django_db
def test_finance_manager_can_reach_disbursement_approve(client, finance_manager_user, budget_with_request):
    _, req = budget_with_request
    client.force_login(finance_manager_user)
    resp = client.post(reverse("rims_finance:disbursement_approve", kwargs={"pk": req.pk}))
    # 302 redirect on success regardless of validation outcome; permission gate passed.
    assert resp.status_code in (200, 302)


@pytest.mark.django_db
def test_finance_officer_blocked_from_disbursement_approve(client, finance_user, budget_with_request):
    _, req = budget_with_request
    client.force_login(finance_user)
    resp = client.post(reverse("rims_finance:disbursement_approve", kwargs={"pk": req.pk}))
    assert resp.status_code == 403


# ---------------------------------------------------------------------------
# BudgetUpdateView — program coordinator only.
# ---------------------------------------------------------------------------

@pytest.mark.django_db
def test_program_coordinator_can_reach_budget_edit(client, program_coordinator_user, budget_with_request):
    budget, _ = budget_with_request
    client.force_login(program_coordinator_user)
    resp = client.get(reverse("rims_finance:budget_edit", kwargs={"pk": budget.pk}))
    assert resp.status_code == 200


@pytest.mark.django_db
def test_finance_officer_blocked_from_budget_edit(client, finance_user, budget_with_request):
    budget, _ = budget_with_request
    client.force_login(finance_user)
    resp = client.get(reverse("rims_finance:budget_edit", kwargs={"pk": budget.pk}))
    assert resp.status_code == 403


# ---------------------------------------------------------------------------
# DisbursementCreateView — project manager (and finance/grants staff).
# ---------------------------------------------------------------------------

@pytest.mark.django_db
def test_project_manager_can_reach_disbursement_create(client, project_manager_user, budget_with_request):
    budget, _ = budget_with_request
    client.force_login(project_manager_user)
    resp = client.get(reverse("rims_finance:disbursement_create", kwargs={"budget_pk": budget.pk}))
    assert resp.status_code == 200


@pytest.mark.django_db
def test_outsider_blocked_from_disbursement_create(client, some_applicant, budget_with_request):
    budget, _ = budget_with_request
    client.force_login(some_applicant)
    resp = client.get(reverse("rims_finance:disbursement_create", kwargs={"budget_pk": budget.pk}))
    assert resp.status_code == 403


# ---------------------------------------------------------------------------
# Phase 1 — Service-level budget revision approval contract.
# ProgramCoordinatorMixin guards the editor view; the approver semantics
# live in services.approve_budget_revision (no web form yet — Phase 6).
# ---------------------------------------------------------------------------

@pytest.mark.django_db
def test_program_coordinator_can_approve_budget_revision(program_coordinator_user, budget_with_request):
    """SRS §5.4 FRFM011/029 — PROGRAM_COORDINATOR is the canonical approver
    of BudgetRevision rows. Locks in the actor semantics even before a web
    form exists.
    """
    from apps.rims.finance.models import BudgetLine, BudgetRevision, BudgetRevisionLine
    from apps.rims.finance.services import approve_budget_revision

    budget, _ = budget_with_request
    line = BudgetLine.objects.filter(budget=budget).first()
    assert line is not None, "budget_with_request fixture should seed at least one line"
    original = line.amount
    revision = BudgetRevision.objects.create(
        budget=budget,
        version=1,
        status=BudgetRevision.Status.REQUESTED,
        change_summary="Phase 1 service contract — PC approves revision.",
    )
    BudgetRevisionLine.objects.create(
        revision=revision,
        line=line,
        old_amount=original,
        new_amount=original + Decimal("100"),
        justification="Bump for service-level test.",
    )

    approve_budget_revision(revision, actor=program_coordinator_user, comment="ok")

    revision.refresh_from_db()
    line.refresh_from_db()
    assert revision.status == BudgetRevision.Status.APPROVED
    assert revision.decided_by_id == program_coordinator_user.id
    assert line.amount == original + Decimal("100")
