"""M&E SRS Table 66 — Corrective action lifecycle + compliance wiring.

Covers the overdue-action escalation sweep, the follow-up (VERIFIED) state, and
the report missing-data finalization block.

Do NOT run under a parallel pytest — the test_iilmp DB collides.
"""
from __future__ import annotations

from datetime import date, timedelta

import pytest
from django.contrib.auth import get_user_model
from django.contrib.contenttypes.models import ContentType
from django.core.exceptions import ValidationError
from django.utils import timezone

from apps.mel.indicators.models import (
    Indicator,
    IndicatorFrequency,
    LogFrame,
    LogFrameLevel,
    LogFrameRow,
)
from apps.mel.tracking.models import CorrectiveAction, CorrectiveActionStatus
from apps.mel.tracking.services import (
    resolve_corrective_action,
    scan_overdue_corrective_actions,
)

User = get_user_model()
pytestmark = pytest.mark.django_db


@pytest.fixture
def officer():
    return User.objects.create_user(
        email="ca-officer@example.com", password="x", role="mel_officer",
    )


_seq = iter(range(1, 10_000))


def _indicator():
    n = next(_seq)
    lf = LogFrame.objects.create(name=f"CA LF {n}", slug=f"ca-lf-{n}")
    impact = LogFrameRow.objects.create(logframe=lf, level=LogFrameLevel.IMPACT, title="I")
    return Indicator.objects.create(
        logframe_row=impact, code=f"ca-ind-{n}", name="X", definition="d",
        unit="x", calculation_method="c", data_source="m",
        frequency=IndicatorFrequency.MONTHLY,
    )


def _action(owner, *, due, status=CorrectiveActionStatus.OPEN):
    ind = _indicator()
    return CorrectiveAction.objects.create(
        subject_type=ContentType.objects.get_for_model(Indicator),
        subject_id=ind.pk,
        title="Fix the thing",
        root_cause="cause",
        action_plan="plan",
        owner=owner,
        due_date=due,
        status=status,
    )


# ---------------------------------------------------------------------------
# Overdue escalation sweep
# ---------------------------------------------------------------------------


def test_overdue_sweep_escalates_and_stamps(officer):
    action = _action(officer, due=date(2026, 1, 1))  # long past
    escalated = scan_overdue_corrective_actions(today=date(2026, 7, 13))
    assert escalated == 1
    action.refresh_from_db()
    assert action.escalated_at is not None


def test_overdue_sweep_skips_future_and_closed(officer):
    _action(officer, due=timezone.now().date() + timedelta(days=30))  # future
    _action(officer, due=date(2026, 1, 1), status=CorrectiveActionStatus.VERIFIED)  # closed
    escalated = scan_overdue_corrective_actions(today=date(2026, 7, 13))
    assert escalated == 0


def test_overdue_sweep_does_not_double_escalate(officer):
    _action(officer, due=date(2026, 1, 1))
    first = scan_overdue_corrective_actions(today=date(2026, 7, 13))
    second = scan_overdue_corrective_actions(today=date(2026, 7, 13))
    assert first == 1
    assert second == 0  # already escalated within the day


def test_is_overdue_property(officer):
    action = _action(officer, due=date(2026, 1, 1))
    assert action.is_overdue is True


# ---------------------------------------------------------------------------
# Follow-up / verified state
# ---------------------------------------------------------------------------


def test_resolve_to_verified_records_followup(officer):
    action = _action(officer, due=date(2026, 1, 1))
    resolve_corrective_action(
        action.pk, actor=officer, new_status=CorrectiveActionStatus.VERIFIED,
        note="Re-measured — performance restored.",
    )
    action.refresh_from_db()
    assert action.status == CorrectiveActionStatus.VERIFIED
    assert action.verified_by_id == officer.pk
    assert action.verified_at is not None
    assert action.closed_at is not None
    assert action.effectiveness_note == "Re-measured — performance restored."


def test_resolve_service_requires_followup_note_for_verified(officer):
    # M&E SRS Table 66 — the follow-up assessment is mandatory at the service
    # layer, not just the view, so non-UI callers cannot close without evidence.
    action = _action(officer, due=date(2026, 1, 1))
    with pytest.raises(ValidationError):
        resolve_corrective_action(
            action.pk, actor=officer, new_status=CorrectiveActionStatus.VERIFIED, note="",
        )
    action.refresh_from_db()
    assert action.status == CorrectiveActionStatus.OPEN


def test_verified_action_cannot_be_reopened(officer):
    # M&E SRS Table 66 — a closed (verified) action does not silently revert.
    action = _action(officer, due=date(2026, 1, 1))
    resolve_corrective_action(
        action.pk, actor=officer, new_status=CorrectiveActionStatus.VERIFIED,
        note="Confirmed effective.",
    )
    with pytest.raises(ValidationError):
        resolve_corrective_action(
            action.pk, actor=officer, new_status=CorrectiveActionStatus.IN_PROGRESS,
        )


def test_resolve_view_requires_note_for_verified(client, officer):
    action = _action(officer, due=date(2026, 1, 1))
    client.force_login(officer)
    from django.urls import reverse

    resp = client.post(
        reverse("mel_tracking:corrective_action_resolve", kwargs={"pk": action.pk}),
        {"status": "verified", "note": ""},
    )
    assert resp.status_code == 302
    action.refresh_from_db()
    assert action.status == CorrectiveActionStatus.OPEN  # unchanged — note missing


# ---------------------------------------------------------------------------
# Report missing-data finalization block
# ---------------------------------------------------------------------------


def test_report_publish_blocked_without_snapshot():
    from apps.mel.reports.services import report_publish_blockers, transition_report
    from apps.mel.reports.models import Report, ReportStatus, ReportTemplate

    tpl = ReportTemplate.objects.create(name="T", slug="t-ca")
    report = Report.objects.create(
        template=tpl, period_label="2026-Q1",
        period_start=date(2026, 1, 1), period_end=date(2026, 3, 31),
        status=ReportStatus.IN_REVIEW, consolidated_data={},
    )
    assert "snapshot" in " ".join(report_publish_blockers(report)).lower()
    with pytest.raises(ValidationError):
        transition_report(report, ReportStatus.APPROVED)


# ---------------------------------------------------------------------------
# 2026-07-14 verification sweep — register, severity rule, compliance review
# ---------------------------------------------------------------------------


def test_corrective_register_lists_and_filters(client, officer):
    from django.urls import reverse

    open_action = _action(officer, due=date(2026, 1, 1))  # overdue
    done_action = _action(officer, due=date(2026, 8, 1), status=CorrectiveActionStatus.DONE)
    verified = _action(officer, due=date(2026, 8, 1), status=CorrectiveActionStatus.VERIFIED)

    client.force_login(officer)
    url = reverse("mel_tracking:corrective_action_list")
    resp = client.get(url)  # default scope=open
    assert resp.status_code == 200
    pks = {a.pk for a in resp.context["actions"]}
    assert open_action.pk in pks
    assert done_action.pk not in pks and verified.pk not in pks

    resp = client.get(url + "?scope=awaiting")
    assert {a.pk for a in resp.context["actions"]} == {done_action.pk}
    assert b"Awaiting verification" in resp.content

    resp = client.get(url + "?scope=open&overdue=1")
    assert {a.pk for a in resp.context["actions"]} == {open_action.pk}


def test_high_severity_requires_owner_and_due_date():
    """SRS Table 66 — high/critical issues need a responsible officer + due date."""
    from apps.mel.tracking.forms import CorrectiveActionCreateForm

    form = CorrectiveActionCreateForm(
        data={
            "title": "Critical gap",
            "severity": "critical",
            "root_cause": "x",
            "action_plan": "y",
        }
    )
    assert not form.is_valid()
    assert "owner" in form.errors and "due_date" in form.errors

    form = CorrectiveActionCreateForm(
        data={
            "title": "Low gap",
            "severity": "low",
            "root_cause": "x",
            "action_plan": "y",
        }
    )
    assert form.is_valid(), form.errors


def test_manual_compliance_alert_notifies_and_critical_escalates(client, officer):
    """SRS Table 66 rows 7/13/21 — manual review + notification + escalation."""
    from django.urls import reverse

    from apps.core.notifications.models import Notification
    from apps.mel.reports.models import ComplianceAlert

    manager = User.objects.create_user(
        email="ca-manager@example.com", password="x", role="program_manager",
    )
    client.force_login(officer)
    resp = client.post(
        reverse("mel_reports:compliance_alert_create"),
        {
            "severity": "critical",
            "requirement_type": "donor_agreement",
            "message": "Procurement breached the donor co-financing clause.",
        },
    )
    assert resp.status_code == 302
    alert = ComplianceAlert.objects.get(code="manual.donor_agreement")
    assert alert.raised_by == officer
    assert alert.severity == "critical"
    # Officer notified with the alert verb; management escalated.
    assert Notification.objects.filter(
        recipient=officer, verb=Notification.Verb.MEL_COMPLIANCE_ALERT
    ).exists()
    assert Notification.objects.filter(
        recipient=manager, verb=Notification.Verb.MEL_COMPLIANCE_ESCALATION
    ).exists()


def test_alert_resolution_keeps_message_immutable(client, officer):
    from django.urls import reverse

    from apps.mel.reports.models import ComplianceAlert

    alert = ComplianceAlert.objects.create(
        code="indicator.red.x", message="Original finding.", severity="high",
    )
    client.force_login(officer)
    resp = client.post(
        reverse("mel_reports:compliance_alert_resolve", kwargs={"pk": alert.pk}),
        {"reason": "Target revised after donor approval."},
    )
    assert resp.status_code == 302
    alert.refresh_from_db()
    assert alert.resolved is True
    assert alert.message == "Original finding."  # untouched
    assert alert.resolution_note == "Target revised after donor approval."
    assert alert.history.count() >= 1  # simple_history snapshot exists


def test_budget_overrun_creates_compliance_alert(officer):
    """SRS Table 66 — significant budget variance raises an alert."""
    from apps.mel.reports.models import (
        ComplianceAlert,
        Report,
        ReportStatus,
        ReportTemplate,
    )
    from apps.mel.reports.services import run_compliance_checks

    tpl = ReportTemplate.objects.create(name="BV", slug="bv-tpl", sections=[])
    report = Report.objects.create(
        template=tpl,
        period_label="2026-Q2",
        period_start=date(2026, 4, 1),
        period_end=date(2026, 6, 30),
        status=ReportStatus.DRAFT,
        consolidated_data={
            "budget_variance": {
                "installed": True,
                "budget_id": 77,
                "budget_name": "Cohort budget",
                "rag_threshold_pct": 15,
                "variance": {"direction": "over", "gap_pct": 40.0},
            }
        },
    )
    created = run_compliance_checks(report)
    codes = {a.code for a in created}
    assert "budget.overrun.77" in codes
    alert = ComplianceAlert.objects.get(code="budget.overrun.77")
    assert alert.severity == "critical"  # 40 ≥ 2×15
