"""Regression tests for the rims_status_badge template tag.

Two layers of protection:
  1. Per-family snapshot of (variant, label) so a color drift PR is caught.
  2. FSM-coverage check — every Status.choices value on every model with a
     badge family must resolve to a non-fallback entry in _TABLES. Catches the
     case where a new status is added to a model but the badge map is not.
"""
import pytest
from django.template import Context, Template

from apps.rims.grants.templatetags.rims_tags import (
    AGREEMENT_BADGES,
    AMENDMENT_BADGES,
    APPLICATION_BADGES,
    AWARD_BADGES,
    BUDGET_BADGES,
    BUDGET_REVISION_BADGES,
    CALL_BADGES,
    DISBURSEMENT_BADGES,
    FUNDING_BADGES,
    INTERVIEW_BADGES,
    MILESTONE_BADGES,
    MOU_BADGES,
    PAYMENT_BATCH_BADGES,
    PAYMENT_TX_BADGES,
    PROJECT_BADGES,
    RECONCILIATION_BADGES,
    REPORT_BADGES,
    SCHOLAR_BADGES,
    STIPEND_BADGES,
    TRANCHE_BADGES,
    VERIFICATION_BADGES,
    _TABLES,
)


class _Obj:
    """Minimal stand-in for a model instance with a status attribute."""

    def __init__(self, status, is_overdue=False, accepted=False):
        self.status = status
        self.is_overdue = is_overdue
        self.accepted = accepted

    def get_status_display(self):
        return str(self.status).replace("_", " ").title()


def _render(obj, kind):
    tpl = Template("{% load rims_tags %}{% rims_status_badge obj kind %}")
    return tpl.render(Context({"obj": obj, "kind": kind}))


# ── Layer 1: snapshot every (family, status) → variant + label ──────────────

@pytest.mark.parametrize(
    "kind,status,variant,label",
    [
        ("application", "draft", "neutral", "Draft"),
        ("application", "submitted", "primary", "Submitted"),
        ("application", "under_review", "primary", "Under review"),
        ("application", "shortlisted", "amber", "Shortlisted"),
        ("application", "awarded", "green", "Awarded"),
        ("application", "rejected", "red", "Not selected"),
        ("application", "ineligible", "red", "Not eligible"),
        ("application", "withdrawn", "neutral", "Withdrawn"),
        ("application", "withdrawn_no_response", "neutral", "Withdrawn (no response)"),
        ("application", "expired_draft", "neutral", "Draft expired"),
        ("award", "agreement_pending", "amber", "Agreement pending"),
        ("award", "active", "green", "Active"),
        ("award", "ready_for_closeout", "amber", "Ready for close-out"),
        ("award", "closeout_in_progress", "primary", "Close-out in progress"),
        ("award", "closed", "neutral", "Closed"),
        ("award", "cancelled", "red", "Cancelled"),
        ("call", "draft", "neutral", "Draft"),
        ("call", "published", "primary", "Published"),
        ("call", "completed", "neutral", "Completed"),
        ("call", "withdrawn", "red", "Withdrawn"),
        ("project", "active", "green", "Active"),
        ("project", "at_risk", "amber", "At risk"),
        ("project", "completed", "neutral", "Completed"),
        ("project", "cancelled", "red", "Cancelled"),
        ("mou", "active", "green", "Active"),
        ("mou", "expired", "neutral", "Expired"),
        ("mou", "terminated", "red", "Terminated"),
        ("disbursement", "approved", "primary", "Approved"),
        ("disbursement", "paid", "green", "Paid"),
        ("disbursement", "rejected", "red", "Rejected"),
        ("milestone", "under_review", "primary", "Under review"),
        ("milestone", "accepted", "green", "Accepted"),
        ("milestone", "overdue", "amber", "Overdue"),
        ("milestone", "revision_required", "red", "Revision required"),
        ("payment_batch", "draft", "neutral", "Draft"),
        ("payment_batch", "executed", "green", "Executed"),
        ("payment_batch", "reversed", "red", "Reversed"),
        ("budget", "draft", "neutral", "Draft"),
        ("budget", "approved", "primary", "Approved"),
        ("budget", "finalized", "green", "Finalized"),
        ("scholar", "enrolled", "primary", "Enrolled"),
        ("scholar", "active", "green", "Active"),
        ("scholar", "on_leave", "amber", "On leave"),
        ("scholar", "withdrawn", "neutral", "Withdrawn"),
        ("interview", "scheduled", "amber", "Scheduled"),
        ("interview", "completed", "green", "Completed"),
        ("verification", "verified", "green", "Verified"),
        ("verification", "flagged", "amber", "Flagged"),
    ],
)
def test_status_badge_snapshot(kind, status, variant, label):
    """A change here is a design decision — update both this test and the map together."""
    rendered = _render(_Obj(status), kind)
    assert f"badge-{variant}" in rendered, rendered
    assert label in rendered, rendered


def test_award_overdue_overrides_status():
    """is_overdue=True forces an amber Overdue pill regardless of status."""
    rendered = _render(_Obj("active", is_overdue=True), "award")
    assert "badge-amber" in rendered
    assert "Overdue" in rendered


def test_agreement_accepted_true_renders_green():
    rendered = _render(_Obj("", accepted=True), "agreement")
    assert "badge-green" in rendered
    assert "Accepted" in rendered


def test_agreement_accepted_false_renders_amber():
    rendered = _render(_Obj("", accepted=False), "agreement")
    assert "badge-amber" in rendered
    assert "Pending" in rendered


def test_unknown_status_falls_back_to_get_status_display():
    """Defensive: unknown status uses the model's display method, not a hardcoded label."""
    rendered = _render(_Obj("never_seen_before"), "application")
    assert "badge-neutral" in rendered
    assert "Never Seen Before" in rendered


# ── Layer 2: FSM coverage — every Status.choices must be in its table ───────

def _choices_for(model_cls):
    """Return the set of status values declared on a model's Status enum."""
    status_field = model_cls._meta.get_field("status")
    return {value for value, _label in status_field.choices}


@pytest.mark.django_db
def test_application_status_choices_all_mapped():
    from apps.rims.grants.models import Application
    missing = _choices_for(Application) - set(APPLICATION_BADGES)
    assert not missing, f"Application statuses missing from APPLICATION_BADGES: {missing}"


@pytest.mark.django_db
def test_award_status_choices_all_mapped():
    from apps.rims.grants.models import Award
    missing = _choices_for(Award) - set(AWARD_BADGES)
    assert not missing, f"Award statuses missing from AWARD_BADGES: {missing}"


@pytest.mark.django_db
def test_call_status_choices_all_mapped():
    from apps.rims.grants.models import GrantCall
    missing = _choices_for(GrantCall) - set(CALL_BADGES)
    assert not missing, f"GrantCall statuses missing from CALL_BADGES: {missing}"


@pytest.mark.django_db
def test_amendment_status_choices_all_mapped():
    from apps.rims.grants.models import AwardAmendment
    missing = _choices_for(AwardAmendment) - set(AMENDMENT_BADGES)
    assert not missing, f"AwardAmendment statuses missing from AMENDMENT_BADGES: {missing}"


@pytest.mark.django_db
def test_tranche_status_choices_all_mapped():
    from apps.rims.grants.models import AwardTranche
    missing = _choices_for(AwardTranche) - set(TRANCHE_BADGES)
    assert not missing, f"AwardTranche statuses missing from TRANCHE_BADGES: {missing}"


@pytest.mark.django_db
def test_funding_status_choices_all_mapped():
    from apps.rims.grants.models import FundingRecord
    missing = _choices_for(FundingRecord) - set(FUNDING_BADGES)
    assert not missing, f"FundingRecord statuses missing from FUNDING_BADGES: {missing}"


@pytest.mark.django_db
def test_disbursement_status_choices_all_mapped():
    from apps.rims.finance.models import DisbursementRequest
    missing = _choices_for(DisbursementRequest) - set(DISBURSEMENT_BADGES)
    assert not missing, f"DisbursementRequest statuses missing from DISBURSEMENT_BADGES: {missing}"


@pytest.mark.django_db
def test_payment_batch_status_choices_all_mapped():
    from apps.rims.finance.models import PaymentBatch
    missing = _choices_for(PaymentBatch) - set(PAYMENT_BATCH_BADGES)
    assert not missing, f"PaymentBatch statuses missing from PAYMENT_BATCH_BADGES: {missing}"


@pytest.mark.django_db
def test_budget_status_choices_all_mapped():
    from apps.rims.finance.models import Budget
    missing = _choices_for(Budget) - set(BUDGET_BADGES)
    assert not missing, f"Budget statuses missing from BUDGET_BADGES: {missing}"


@pytest.mark.django_db
def test_payment_tx_status_choices_all_mapped():
    from apps.rims.finance.models import DisbursementExecution
    missing = _choices_for(DisbursementExecution) - set(PAYMENT_TX_BADGES)
    assert not missing, f"DisbursementExecution statuses missing from PAYMENT_TX_BADGES: {missing}"


@pytest.mark.django_db
def test_reconciliation_status_choices_all_mapped():
    from apps.rims.finance.models import DisbursementReconciliation
    missing = _choices_for(DisbursementReconciliation) - set(RECONCILIATION_BADGES)
    assert not missing, f"DisbursementReconciliation statuses missing from RECONCILIATION_BADGES: {missing}"


@pytest.mark.django_db
def test_budget_revision_status_choices_all_mapped():
    from apps.rims.finance.models import BudgetRevision
    missing = _choices_for(BudgetRevision) - set(BUDGET_REVISION_BADGES)
    assert not missing, f"BudgetRevision statuses missing from BUDGET_REVISION_BADGES: {missing}"


@pytest.mark.django_db
def test_project_status_choices_all_mapped():
    from apps.rims.projects.models import Project
    missing = _choices_for(Project) - set(PROJECT_BADGES)
    assert not missing, f"Project statuses missing from PROJECT_BADGES: {missing}"


@pytest.mark.django_db
def test_milestone_status_choices_all_mapped():
    from apps.rims.projects.models import Milestone
    missing = _choices_for(Milestone) - set(MILESTONE_BADGES)
    assert not missing, f"Milestone statuses missing from MILESTONE_BADGES: {missing}"


@pytest.mark.django_db
def test_mou_status_choices_all_mapped():
    from apps.rims.operations.models import MOU
    missing = _choices_for(MOU) - set(MOU_BADGES)
    assert not missing, f"MOU statuses missing from MOU_BADGES: {missing}"


# ── Layer 3: §4 taste-assessment color discipline ─────────────────────────

def test_red_never_used_for_late_or_in_flight():
    """§4: red is for blocking or terminal-negative only. Not for late, not for in-flight."""
    late_or_in_flight = {"submitted", "under_review", "shortlisted", "overdue",
                         "agreement_pending", "ready_for_closeout", "closeout_in_progress",
                         "on_leave", "queries_raised"}
    offenders = []
    for kind, table in _TABLES.items():
        if not isinstance(table, dict):
            continue
        for status, (variant, _label) in table.items():
            if status in late_or_in_flight and variant == "red":
                offenders.append(f"{kind}/{status}")
    assert not offenders, f"§4 violation — red used for non-terminal states: {offenders}"


def test_green_reserved_for_celebratory_outcomes():
    """§4: green is celebratory only — awarded, paid, accepted, executed, finalized, completed-as-success."""
    celebratory = {"awarded", "active", "approved", "paid", "accepted", "executed",
                   "finalized", "reconciled", "completed", "verified", "renewed"}
    offenders = []
    for kind, table in _TABLES.items():
        if not isinstance(table, dict):
            continue
        for status, (variant, _label) in table.items():
            if variant == "green" and status not in celebratory:
                offenders.append(f"{kind}/{status}")
    assert not offenders, f"§4 violation — green used for non-celebratory states: {offenders}"
