"""PRD §5.4 — Expenditure UI smoke + service-level checks.

Covers role gating on the list view, create-flow happy path, overspend rejection
via record_expenditure, HTMX delete returning a 200 (with empty body for OOB
swap), and a sanity check on the cashflow JSON shape.
"""

from __future__ import annotations

import json
import uuid
from datetime import date, timedelta
from decimal import Decimal

import pytest
from django.contrib.auth import get_user_model
from django.core.exceptions import ValidationError
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, Expenditure
from apps.rims.finance.services import record_expenditure, total_expended
from apps.rims.grants.models import Application, GrantCall
from apps.rims.grants.services import (
    award_application,
    shortlist_application,
    submit_application,
)

User = get_user_model()


@pytest.fixture
def budget(db, applicant_user, institution):
    call = GrantCall.objects.create(
        title="Expenditure UI call",
        slug=f"exp-{uuid.uuid4().hex[:10]}",
        opens_at=timezone.now() - timedelta(days=1),
        closes_at=timezone.now() + timedelta(days=30),
        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("10000"), date.today() + timedelta(days=180), narrative="")
    award = Application.objects.get(pk=app.pk).award
    b = Budget.objects.create(award=award, name="Expenditure budget")
    BudgetLine.objects.create(budget=b, label="Personnel", amount=Decimal("5000"))
    BudgetLine.objects.create(budget=b, label="Travel", amount=Decimal("2000"))
    return b


# --- Service ----------------------------------------------------------------

@pytest.mark.django_db
def test_record_expenditure_happy_path(budget, finance_user):
    line = budget.lines.first()
    exp = record_expenditure(
        line,
        amount=Decimal("250"),
        incurred_on=date.today(),
        description="Workshop catering",
        actor=finance_user,
    )
    assert exp.pk is not None
    assert total_expended(budget) == Decimal("250")


@pytest.mark.django_db
def test_record_expenditure_rejects_overspend(budget, finance_user):
    line = budget.lines.first()
    record_expenditure(line, amount=Decimal("6000"), incurred_on=date.today(), actor=finance_user)
    record_expenditure(line, amount=Decimal("1000"), incurred_on=date.today(), actor=finance_user)
    # ceiling = 5000 + 2000 = 7000; already at 7000 — one more should fail.
    with pytest.raises(ValidationError):
        record_expenditure(line, amount=Decimal("1"), incurred_on=date.today(), actor=finance_user)


@pytest.mark.django_db
def test_record_expenditure_rejects_zero_amount(budget, finance_user):
    line = budget.lines.first()
    with pytest.raises(ValidationError):
        record_expenditure(line, amount=Decimal("0"), incurred_on=date.today(), actor=finance_user)


# --- List view --------------------------------------------------------------

@pytest.mark.django_db
def test_expenditure_list_role_gating(client, budget, finance_user):
    line = budget.lines.first()
    Expenditure.objects.create(
        budget_line=line, amount=Decimal("100"), incurred_on=date.today(), description="x"
    )
    client.force_login(finance_user)
    resp = client.get(reverse("rims_finance:expenditure_list"))
    assert resp.status_code == 200
    assert b"Expenditures" in resp.content


@pytest.mark.django_db
def test_expenditure_list_denies_unauthorised_role(client, budget):
    """A user with a non-finance role (SCHOLAR) is denied."""
    u = User.objects.create_user(email=f"sc-{uuid.uuid4().hex[:6]}@x.com", password="x")
    u.role = UserRole.SCHOLAR
    u.save(update_fields=["role"])
    client.force_login(u)
    resp = client.get(reverse("rims_finance:expenditure_list"))
    assert resp.status_code == 403


@pytest.mark.django_db
def test_expenditure_list_htmx_returns_partial(client, budget, finance_user):
    line = budget.lines.first()
    Expenditure.objects.create(
        budget_line=line, amount=Decimal("100"), incurred_on=date.today(), description="rows"
    )
    client.force_login(finance_user)
    resp = client.get(reverse("rims_finance:expenditure_list"), HTTP_HX_REQUEST="true")
    assert resp.status_code == 200
    body = resp.content.decode()
    assert "<html" not in body
    assert "rows" in body  # the description we wrote


# --- Create view ------------------------------------------------------------

@pytest.mark.django_db
def test_expenditure_create_happy_path(client, budget, finance_user):
    client.force_login(finance_user)
    line = budget.lines.first()
    resp = client.post(
        reverse("rims_finance:expenditure_create", kwargs={"budget_pk": budget.pk}),
        {
            "budget_line": line.pk,
            "amount": "300",
            "incurred_on": date.today().isoformat(),
            "description": "Bus hire",
        },
    )
    assert resp.status_code == 302
    assert Expenditure.objects.filter(budget_line=line, amount=Decimal("300")).exists()


@pytest.mark.django_db
def test_expenditure_create_overspend_rejected_with_message(client, budget, finance_user):
    """Overspend should fail validation and re-render the form, not create."""
    client.force_login(finance_user)
    line = budget.lines.first()
    Expenditure.objects.create(
        budget_line=line, amount=Decimal("7000"), incurred_on=date.today()
    )
    resp = client.post(
        reverse("rims_finance:expenditure_create", kwargs={"budget_pk": budget.pk}),
        {
            "budget_line": line.pk,
            "amount": "1",
            "incurred_on": date.today().isoformat(),
        },
    )
    assert resp.status_code == 200  # form re-rendered
    # Only the pre-existing row is present.
    assert Expenditure.objects.filter(budget_line=line).count() == 1


# --- Delete view ------------------------------------------------------------

@pytest.mark.django_db
def test_expenditure_delete_htmx_returns_empty_200(client, budget, finance_user):
    line = budget.lines.first()
    exp = Expenditure.objects.create(
        budget_line=line, amount=Decimal("100"), incurred_on=date.today()
    )
    client.force_login(finance_user)
    resp = client.post(
        reverse("rims_finance:expenditure_delete", kwargs={"pk": exp.pk}),
        HTTP_HX_REQUEST="true",
    )
    assert resp.status_code == 200
    assert resp.content == b""
    assert not Expenditure.objects.filter(pk=exp.pk).exists()


@pytest.mark.django_db
def test_expenditure_delete_non_htmx_redirects(client, budget, finance_user):
    line = budget.lines.first()
    exp = Expenditure.objects.create(
        budget_line=line, amount=Decimal("100"), incurred_on=date.today()
    )
    client.force_login(finance_user)
    resp = client.post(reverse("rims_finance:expenditure_delete", kwargs={"pk": exp.pk}))
    assert resp.status_code == 302


# --- Cashflow JSON ----------------------------------------------------------

@pytest.mark.django_db
def test_cashflow_data_view_returns_expected_shape(client, budget, finance_user):
    line = budget.lines.first()
    Expenditure.objects.create(
        budget_line=line, amount=Decimal("250"), incurred_on=date.today(), description="x"
    )
    DisbursementRequest.objects.create(
        budget=budget,
        amount=Decimal("500"),
        status=DisbursementRequest.Status.PAID,
        approved_at=timezone.now(),
    )
    client.force_login(finance_user)
    resp = client.get(reverse("rims_finance:cashflow_data", kwargs={"pk": budget.pk}))
    assert resp.status_code == 200
    data = json.loads(resp.content.decode())
    assert "scope" in data and "buckets" in data
    assert data["scope"] == budget.name
    assert isinstance(data["buckets"], list)
    if data["buckets"]:
        sample = data["buckets"][0]
        assert {"label", "expended", "disbursed"}.issubset(sample.keys())
