"""M&E SRS Table 65 — Progress Reporting / Monitoring.

Covers the clarification loop (request → resubmit → revalidate) and the
hierarchical Output→Outcome→Impact aggregation of validated data.

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

from datetime import date
from decimal import Decimal

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

from apps.mel.indicators.models import (
    DataPoint,
    Indicator,
    IndicatorFrequency,
    IndicatorTarget,
    LogFrame,
    LogFrameLevel,
    LogFrameRow,
)
from apps.mel.indicators.services import (
    aggregate_results_by_level,
    record_data_point,
    request_datapoint_clarification,
    resubmit_datapoint,
    verify_datapoint,
)

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


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


@pytest.fixture
def reporter():
    return User.objects.create_user(
        email="pr-reporter@example.com", password="x", role="program_manager",
    )


def _chain(slug="pr-lf"):
    lf = LogFrame.objects.create(name=f"LF {slug}", slug=slug)
    impact = LogFrameRow.objects.create(logframe=lf, level=LogFrameLevel.IMPACT, title="Impact")
    outcome = LogFrameRow.objects.create(
        logframe=lf, level=LogFrameLevel.OUTCOME, title="Outcome", parent=impact,
    )
    output = LogFrameRow.objects.create(
        logframe=lf, level=LogFrameLevel.OUTPUT, title="Output", parent=outcome,
    )
    return lf, impact, outcome, output


def _indicator(row, *, code):
    return Indicator.objects.create(
        logframe_row=row,
        code=code,
        name=f"Indicator {code}",
        definition="d",
        unit="x",
        calculation_method="count",
        data_source="manual",
        frequency=IndicatorFrequency.QUARTERLY,
    )


# ---------------------------------------------------------------------------
# Clarification loop
# ---------------------------------------------------------------------------


def test_request_clarification_moves_to_needs_clarification(officer, reporter):
    _, _, _, output = _chain()
    ind = _indicator(output, code="clar-1")
    dp = DataPoint.objects.create(
        indicator=ind, period_label="2026-Q1", value=Decimal("5"), reported_by=reporter,
    )
    request_datapoint_clarification(dp.pk, requested_by=officer, note="Recheck the count")
    dp.refresh_from_db()
    assert dp.status == DataPoint.Status.NEEDS_CLARIFICATION
    assert dp.clarification_note == "Recheck the count"


def test_clarified_point_not_counted_until_resubmitted_and_verified(officer, reporter):
    _, _, _, output = _chain()
    ind = _indicator(output, code="clar-2")
    dp = DataPoint.objects.create(
        indicator=ind, period_label="2026-Q1", value=Decimal("5"), reported_by=reporter,
    )
    request_datapoint_clarification(dp.pk, requested_by=officer, note="fix")
    # A clarification-flagged point is not verified, so it contributes nothing.
    from apps.mel.indicators.services import sum_period_value
    assert sum_period_value(ind, "2026-Q1") == Decimal("0")

    resubmit_datapoint(dp.pk, resubmitted_by=reporter, value=Decimal("8"))
    dp.refresh_from_db()
    assert dp.status == DataPoint.Status.PENDING
    assert dp.value == Decimal("8")
    assert dp.clarification_note == ""

    verify_datapoint(dp.pk, verified_by=officer)
    assert sum_period_value(ind, "2026-Q1") == Decimal("8")


# ---------------------------------------------------------------------------
# Validation state guards (M&E SRS Table 65, req 5/6)
# ---------------------------------------------------------------------------


def test_rejected_point_cannot_be_verified_without_resubmission(officer, reporter):
    from django.core.exceptions import ValidationError

    from apps.mel.indicators.services import reject_datapoint

    _, _, _, output = _chain("pr-guard-1")
    ind = _indicator(output, code="guard-1")
    dp = DataPoint.objects.create(
        indicator=ind, period_label="2026-Q1", value=Decimal("5"), reported_by=reporter,
    )
    reject_datapoint(dp.pk, rejected_by=officer, reason="bad")
    with pytest.raises(ValidationError):
        verify_datapoint(dp.pk, verified_by=officer)


def test_verified_point_cannot_be_rejected(officer, reporter):
    from django.core.exceptions import ValidationError

    from apps.mel.indicators.services import reject_datapoint

    _, _, _, output = _chain("pr-guard-2")
    ind = _indicator(output, code="guard-2")
    dp = DataPoint.objects.create(
        indicator=ind, period_label="2026-Q1", value=Decimal("5"), reported_by=reporter,
    )
    verify_datapoint(dp.pk, verified_by=officer)
    with pytest.raises(ValidationError):
        reject_datapoint(dp.pk, rejected_by=officer, reason="too late")


# ---------------------------------------------------------------------------
# Automated data path excludes DRAFT indicators (M&E SRS Table 64, req 13)
# ---------------------------------------------------------------------------


def test_record_automated_point_skips_draft_indicator(officer):
    from apps.mel.indicators.models import IndicatorState
    from apps.mel.indicators.services import record_automated_point

    _, _, _, output = _chain("pr-auto")
    ind = _indicator(output, code="auto-draft")
    ind.source_module = "rims"
    ind.workflow_status = IndicatorState.DRAFT
    ind.save(update_fields=["source_module", "workflow_status"])

    result = record_automated_point(
        indicator_code="auto-draft",
        source_module="rims",
        source_event="grant.awarded",
        source_object_id="42",
    )
    assert result is None
    assert ind.data_points.count() == 0

    # Once published, the same signal records a point.
    ind.workflow_status = IndicatorState.ACTIVE
    ind.save(update_fields=["workflow_status"])
    record_automated_point(
        indicator_code="auto-draft",
        source_module="rims",
        source_event="grant.awarded",
        source_object_id="42",
    )
    assert ind.data_points.count() == 1


def test_clarify_view_requires_note(client, officer, reporter):
    _, _, _, output = _chain()
    ind = _indicator(output, code="clar-3")
    dp = DataPoint.objects.create(
        indicator=ind, period_label="2026-Q1", value=Decimal("5"), reported_by=reporter,
    )
    client.force_login(officer)
    resp = client.post(
        reverse("mel_indicators:datapoint_clarify", kwargs={"pk": dp.pk}), {"note": ""}
    )
    assert resp.status_code == 302
    dp.refresh_from_db()
    assert dp.status == DataPoint.Status.PENDING  # unchanged — note was empty


def test_clarify_then_resubmit_views(client, officer, reporter):
    _, _, _, output = _chain()
    ind = _indicator(output, code="clar-4")
    dp = DataPoint.objects.create(
        indicator=ind, period_label="2026-Q1", value=Decimal("5"), reported_by=reporter,
    )
    client.force_login(officer)
    client.post(
        reverse("mel_indicators:datapoint_clarify", kwargs={"pk": dp.pk}),
        {"note": "please recount"},
    )
    dp.refresh_from_db()
    assert dp.status == DataPoint.Status.NEEDS_CLARIFICATION

    client.force_login(reporter)
    resp = client.post(
        reverse("mel_indicators:datapoint_resubmit", kwargs={"pk": dp.pk}),
        {"value": "9"},
    )
    assert resp.status_code == 302
    dp.refresh_from_db()
    assert dp.status == DataPoint.Status.PENDING
    assert dp.value == Decimal("9")


# ---------------------------------------------------------------------------
# Hierarchical aggregation
# ---------------------------------------------------------------------------


def test_aggregate_rolls_output_into_outcome_and_impact(officer):
    lf, impact, outcome, output = _chain(slug="pr-agg")
    out_ind = _indicator(output, code="agg-out")
    oc_ind = _indicator(outcome, code="agg-oc")
    period = "2026-Q1"
    IndicatorTarget.objects.create(
        indicator=out_ind, period_label=period,
        period_start=date(2026, 1, 1), period_end=date(2026, 3, 31),
        baseline_value=Decimal("0"), target_value=Decimal("100"),
    )
    # Verified output data point of 30, outcome data point of 12.
    dp1 = DataPoint.objects.create(
        indicator=out_ind, period_label=period, value=Decimal("30"),
        status=DataPoint.Status.VERIFIED,
    )
    dp2 = DataPoint.objects.create(
        indicator=oc_ind, period_label=period, value=Decimal("12"),
        status=DataPoint.Status.VERIFIED,
    )
    agg = aggregate_results_by_level(lf, period)
    # Output level = only the output indicator (30).
    assert agg[LogFrameLevel.OUTPUT]["actual"] == Decimal("30")
    # Outcome rolls up the output beneath it plus its own indicator (30 + 12).
    assert agg[LogFrameLevel.OUTCOME]["actual"] == Decimal("42")
    # Impact rolls up everything (30 + 12).
    assert agg[LogFrameLevel.IMPACT]["actual"] == Decimal("42")


def test_aggregate_ignores_unverified(officer):
    lf, impact, outcome, output = _chain(slug="pr-agg2")
    out_ind = _indicator(output, code="agg2-out")
    period = "2026-Q1"
    DataPoint.objects.create(
        indicator=out_ind, period_label=period, value=Decimal("50"),
        status=DataPoint.Status.PENDING,
    )
    agg = aggregate_results_by_level(lf, period)
    assert agg[LogFrameLevel.OUTPUT]["actual"] == Decimal("0")


# ---------------------------------------------------------------------------
# 2026-07-14 verification sweep — validation queue, implementer loop,
# evidence gate, variance enforcement
# ---------------------------------------------------------------------------


@pytest.fixture
def implementer():
    return User.objects.create_user(
        email="pr-implementer@example.com", password="x", role="programme_officer",
    )


def test_validation_queue_lists_pending_and_clarification(client, officer, reporter):
    _, _, _, output = _chain(slug="pr-q1")
    ind = _indicator(output, code="q1-ind")
    pending = DataPoint.objects.create(
        indicator=ind, period_label="2026-Q1", value=Decimal("5"), reported_by=reporter,
    )
    clarif = DataPoint.objects.create(
        indicator=ind, period_label="2026-Q2", value=Decimal("6"), reported_by=reporter,
        status=DataPoint.Status.NEEDS_CLARIFICATION,
    )
    verified = DataPoint.objects.create(
        indicator=ind, period_label="2026-Q3", value=Decimal("7"),
        status=DataPoint.Status.VERIFIED,
    )
    client.force_login(officer)
    resp = client.get(reverse("mel_indicators:validation_queue"))
    assert resp.status_code == 200
    pks = {dp.pk for dp in resp.context["data_points"]}
    assert pending.pk in pks and clarif.pk in pks
    assert verified.pk not in pks
    body = resp.content.decode()
    assert "Verify" in body and "Resubmit" in body


def test_validation_queue_scopes_non_reviewers_to_own_submissions(
    client, officer, reporter, implementer
):
    _, _, _, output = _chain(slug="pr-q2")
    ind = _indicator(output, code="q2-ind")
    mine = DataPoint.objects.create(
        indicator=ind, period_label="2026-Q1", value=Decimal("5"),
        reported_by=implementer,
    )
    other = DataPoint.objects.create(
        indicator=ind, period_label="2026-Q1", value=Decimal("9"), reported_by=reporter,
    )
    client.force_login(implementer)
    resp = client.get(reverse("mel_indicators:validation_queue"))
    assert resp.status_code == 200
    pks = {dp.pk for dp in resp.context["data_points"]}
    assert mine.pk in pks
    assert other.pk not in pks


def test_implementer_can_resubmit_own_clarified_point(client, officer, implementer):
    """The clarification loop is completable by the original reporter."""
    _, _, _, output = _chain(slug="pr-q3")
    ind = _indicator(output, code="q3-ind")
    dp = DataPoint.objects.create(
        indicator=ind, period_label="2026-Q1", value=Decimal("5"),
        reported_by=implementer, status=DataPoint.Status.NEEDS_CLARIFICATION,
        clarification_note="recount",
    )
    client.force_login(implementer)
    resp = client.post(
        reverse("mel_indicators:datapoint_resubmit", kwargs={"pk": dp.pk}),
        {"value": "11", "note": "recounted"},
    )
    assert resp.status_code == 302
    dp.refresh_from_db()
    assert dp.status == DataPoint.Status.PENDING
    assert dp.value == Decimal("11")


def test_verify_honours_safe_next_redirect(client, officer, reporter):
    _, _, _, output = _chain(slug="pr-q4")
    ind = _indicator(output, code="q4-ind")
    dp = DataPoint.objects.create(
        indicator=ind, period_label="2026-Q1", value=Decimal("5"), reported_by=reporter,
    )
    client.force_login(officer)
    queue = reverse("mel_indicators:validation_queue") + "?status=pending"
    resp = client.post(
        reverse("mel_indicators:datapoint_verify", kwargs={"pk": dp.pk}),
        {"next": queue},
    )
    assert resp.status_code == 302
    assert resp.url == queue
    # An off-site next is ignored.
    dp2 = DataPoint.objects.create(
        indicator=ind, period_label="2026-Q2", value=Decimal("5"), reported_by=reporter,
    )
    resp = client.post(
        reverse("mel_indicators:datapoint_verify", kwargs={"pk": dp2.pk}),
        {"next": "https://evil.example.com/"},
    )
    assert resp.url.startswith("/")


def test_implementer_data_entry_scoped_to_responsible_indicators(
    client, officer, implementer
):
    """SRS Table 65 — implementers submit only for indicators assigned to them."""
    _, _, _, output = _chain(slug="pr-q5")
    mine = _indicator(output, code="q5-mine")
    mine.responsible = implementer
    mine.save(update_fields=["responsible"])
    other = _indicator(output, code="q5-other")

    client.force_login(implementer)
    resp = client.get(reverse("mel_indicators:data_entry"))
    assert resp.status_code == 200
    qs = resp.context["form"].fields["indicator"].queryset
    assert mine in qs and other not in qs

    resp = client.post(
        reverse("mel_indicators:data_entry"),
        {
            "indicator": mine.pk,
            "period_label": "2026-Q1",
            "reported_at": "2026-07-01T10:00",
            "value": "4",
            "qualitative_note": "",
        },
    )
    assert resp.status_code == 302, getattr(resp.context.get("form"), "errors", None)
    dp = DataPoint.objects.get(indicator=mine, period_label="2026-Q1")
    assert dp.status == DataPoint.Status.PENDING
    assert dp.reported_by == implementer

    # Submitting for an unassigned indicator fails validation.
    resp = client.post(
        reverse("mel_indicators:data_entry"),
        {
            "indicator": other.pk,
            "period_label": "2026-Q1",
            "reported_at": "2026-07-01T10:00",
            "value": "4",
        },
    )
    assert resp.status_code == 200  # re-rendered with errors
    assert not DataPoint.objects.filter(indicator=other).exists()


def test_data_entry_403_for_user_with_no_assigned_indicators(client, implementer):
    client.force_login(implementer)
    resp = client.get(reverse("mel_indicators:data_entry"))
    assert resp.status_code == 403


def test_logframe_dashboard_renders_level_rollup(client, officer):
    lf, impact, outcome, output = _chain(slug="pr-q6")
    ind = _indicator(output, code="q6-ind")
    DataPoint.objects.create(
        indicator=ind, period_label="2026-07", value=Decimal("20"),
        status=DataPoint.Status.VERIFIED,
    )
    client.force_login(officer)
    resp = client.get(
        reverse("mel_indicators:logframe_dashboard", kwargs={"slug": lf.slug})
        + "?period=2026-07"
    )
    assert resp.status_code == 200
    rollup = resp.context["level_rollup"]
    assert rollup["output"]["actual"] == Decimal("20")
    assert rollup["impact"]["actual"] == Decimal("20")
    assert b"Results chain rollup" in resp.content


def test_evidence_gap_blocks_report_finalize_and_notifies(officer):
    """SRS Table 65 (A2) — no finalize while verified points lack evidence."""
    from apps.core.notifications.models import Notification
    from apps.mel.indicators.services import evidence_gaps, notify_evidence_required
    from apps.mel.reports.models import ReportTemplate
    from apps.mel.reports.services import generate_report, report_publish_blockers

    lf, impact, outcome, output = _chain(slug="pr-q7")
    ind = _indicator(output, code="q7-ind")
    ind.responsible = officer
    ind.save(update_fields=["responsible"])
    DataPoint.objects.create(
        indicator=ind, period_label="2026-04", value=Decimal("3"),
        status=DataPoint.Status.VERIFIED,
    )
    tpl = ReportTemplate.objects.create(
        name="T q7", slug="t-q7", logframe=lf, sections=["indicator_rollup"],
    )
    report = generate_report(
        template=tpl,
        period_label="2026-04",
        period_start=date(2026, 4, 1),
        period_end=date(2026, 4, 30),
        user=officer,
        narrative="All good.",
    )
    blockers = report_publish_blockers(report)
    assert any("evidence" in b.lower() for b in blockers)
    # The service tier (require_narrative=False) does not enforce evidence.
    assert report_publish_blockers(report, require_narrative=False) == []

    gaps = evidence_gaps(lf, "2026-04")
    assert [dp.indicator.code for dp in gaps] == ["q7-ind"]
    sent = notify_evidence_required(gaps)
    assert sent == 1
    assert Notification.objects.filter(
        recipient=officer, verb=Notification.Verb.MEL_EVIDENCE_REQUIRED
    ).exists()


def test_variance_refresh_requires_explanation_when_below_target(client, officer):
    """SRS Table 65 (A3) — below-target refresh demands cause + corrective action."""
    from apps.mel.indicators.models import IndicatorVariance, VarianceRating

    _, _, _, output = _chain(slug="pr-q8")
    ind = _indicator(output, code="q8-ind")
    IndicatorTarget.objects.create(
        indicator=ind, period_label="2026-Q1",
        period_start=date(2026, 1, 1), period_end=date(2026, 3, 31),
        baseline_value=Decimal("0"), target_value=Decimal("100"),
    )
    DataPoint.objects.create(
        indicator=ind, period_label="2026-Q1", value=Decimal("10"),
        status=DataPoint.Status.VERIFIED,
    )
    client.force_login(officer)
    url = reverse("mel_indicators:indicator_variance_refresh", kwargs={"code": ind.code})
    # -90% → CRITICAL; blank explanation is refused.
    client.post(url, {"period": "2026-Q1", "cause_analysis": "", "adjustment": ""})
    assert not IndicatorVariance.objects.filter(indicator=ind).exists()
    # With both fields present it saves and carries the SRS rating.
    client.post(
        url,
        {
            "period": "2026-Q1",
            "cause_analysis": "Late onboarding of partner universities.",
            "adjustment": "Fast-track remaining cohorts next quarter.",
        },
    )
    row = IndicatorVariance.objects.get(indicator=ind, period_label="2026-Q1")
    assert row.rating == VarianceRating.CRITICAL
    assert row.variance_abs == Decimal("-90")
