"""M&E SRS Tables 63-64 — Indicator management with disaggregation.

Covers the disaggregation subsystem (categories → values → per-value baseline,
target, data source, collection method, frequency, responsible), plus the
adjacent indicator rules: mandatory definition, name-unique-within-output, and
the DRAFT → publish workflow.

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

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 apps.mel.indicators.forms import (
    DisaggregationValueForm,
    IndicatorForm,
    disaggregation_value_formset,
)
from apps.mel.indicators.models import (
    DisaggregationCategory,
    DisaggregationValue,
    Indicator,
    IndicatorFrequency,
    IndicatorState,
    LogFrame,
    LogFrameLevel,
    LogFrameRow,
)

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


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


def _output_row(slug="disagg-lf"):
    lf = LogFrame.objects.create(name=f"LF {slug}", slug=slug)
    impact = LogFrameRow.objects.create(logframe=lf, level=LogFrameLevel.IMPACT, title="I")
    outcome = LogFrameRow.objects.create(
        logframe=lf, level=LogFrameLevel.OUTCOME, title="O", parent=impact,
    )
    output = LogFrameRow.objects.create(
        logframe=lf, level=LogFrameLevel.OUTPUT, title="P", parent=outcome,
    )
    return output


def _indicator(row, *, code="disagg-ind", name="Farmers trained"):
    return Indicator.objects.create(
        logframe_row=row,
        code=code,
        name=name,
        definition="Count of farmers who completed training.",
        unit="farmers",
        calculation_method="Count.",
        data_source="manual",
        frequency=IndicatorFrequency.QUARTERLY,
    )


# ---------------------------------------------------------------------------
# Models — categories & values
# ---------------------------------------------------------------------------


def test_category_names_unique_within_indicator():
    ind = _indicator(_output_row())
    DisaggregationCategory.objects.create(indicator=ind, name="Gender")
    dup = DisaggregationCategory(indicator=ind, name="Gender")
    with pytest.raises(ValidationError):
        dup.full_clean()
        dup.validate_constraints()


def test_value_names_unique_within_category():
    ind = _indicator(_output_row())
    cat = DisaggregationCategory.objects.create(indicator=ind, name="Gender")
    DisaggregationValue.objects.create(category=cat, name="Male")
    dup = DisaggregationValue(category=cat, name="Male")
    with pytest.raises(ValidationError):
        dup.full_clean()
        dup.validate_constraints()


def test_value_completeness_flag(officer):
    ind = _indicator(_output_row())
    cat = DisaggregationCategory.objects.create(indicator=ind, name="Gender")
    incomplete = DisaggregationValue.objects.create(category=cat, name="Male")
    assert incomplete.is_complete is False
    complete = DisaggregationValue.objects.create(
        category=cat,
        name="Female",
        baseline_value=Decimal("0"),
        target_value=Decimal("100"),
        data_source="Survey",
        collection_method="Interview",
        frequency=IndicatorFrequency.QUARTERLY,
        responsible=officer,
    )
    assert complete.is_complete is True


# ---------------------------------------------------------------------------
# Forms — per-value mandatory fields
# ---------------------------------------------------------------------------


def test_value_form_requires_all_monitoring_fields():
    form = DisaggregationValueForm(
        data={"name": "Male", "order": 0}
    )
    assert not form.is_valid()
    for field in ("baseline_value", "target_value", "data_source", "collection_method", "frequency", "responsible"):
        assert field in form.errors


def test_value_form_valid_when_complete(officer):
    form = DisaggregationValueForm(
        data={
            "name": "Female",
            "baseline_value": "0",
            "target_value": "50",
            "data_source": "Survey",
            "collection_method": "Interview",
            "frequency": IndicatorFrequency.QUARTERLY,
            "responsible": officer.pk,
            "order": 0,
        }
    )
    assert form.is_valid(), form.errors


# ---------------------------------------------------------------------------
# Indicator rules — definition mandatory, name unique within output, draft
# ---------------------------------------------------------------------------


def test_indicator_definition_is_mandatory():
    row = _output_row()
    form = IndicatorForm(
        data={
            "results_framework": row.logframe.slug,
            "logframe_row": row.pk,
            "code": "no-def",
            "name": "No definition",
            "definition": "",
            "indicator_type": "quantitative",
            "unit": "x",
            "calculation_method": "count",
            "data_source": "manual",
            "frequency": IndicatorFrequency.MONTHLY,
        }
    )
    assert not form.is_valid()
    assert "definition" in form.errors


def test_indicator_name_unique_within_output():
    row = _output_row()
    _indicator(row, code="first", name="Farmers trained")
    form = IndicatorForm(
        data={
            "results_framework": row.logframe.slug,
            "logframe_row": row.pk,
            "code": "second",
            "name": "farmers trained",  # case-insensitive clash
            "definition": "Another one.",
            "indicator_type": "quantitative",
            "unit": "x",
            "calculation_method": "count",
            "data_source": "manual",
            "frequency": IndicatorFrequency.MONTHLY,
        }
    )
    assert not form.is_valid()
    assert "name" in form.errors


def test_same_name_allowed_under_different_outputs():
    row1 = _output_row(slug="disagg-lf-1")
    row2 = _output_row(slug="disagg-lf-2")
    _indicator(row1, code="a", name="Shared name")
    form = IndicatorForm(
        data={
            "results_framework": row2.logframe.slug,
            "logframe_row": row2.pk,
            "code": "b",
            "name": "Shared name",
            "definition": "OK on a different output.",
            "indicator_type": "quantitative",
            "unit": "x",
            "calculation_method": "count",
            "data_source": "manual",
            "frequency": IndicatorFrequency.MONTHLY,
        }
    )
    assert form.is_valid(), form.errors


# ---------------------------------------------------------------------------
# Views — create as draft, add category, publish
# ---------------------------------------------------------------------------


def test_create_view_makes_draft_and_redirects_to_disaggregation(client, officer):
    row = _output_row()
    client.force_login(officer)
    resp = client.post(
        reverse("mel_indicators:indicator_create"),
        {
            "results_framework": row.logframe.slug,
            "logframe_row": row.pk,
            "code": "draft-ind",
            "name": "Draft indicator",
            "definition": "A draft.",
            "indicator_type": "quantitative",
            "unit": "x",
            "calculation_method": "count",
            "data_source": "manual",
            "frequency": IndicatorFrequency.MONTHLY,
            "is_active": "on",
        },
    )
    assert resp.status_code == 302
    ind = Indicator.objects.get(code="draft-ind")
    assert ind.workflow_status == IndicatorState.DRAFT
    assert reverse(
        "mel_indicators:indicator_disaggregation", kwargs={"code": "draft-ind"}
    ) in resp.url


def test_add_category_view(client, officer):
    ind = _indicator(_output_row())
    client.force_login(officer)
    url = reverse("mel_indicators:indicator_disaggregation", kwargs={"code": ind.code})
    # Post only what the page's form actually renders (a single name input) —
    # posting extra fields here previously masked an unrendered-required-field
    # bug that made the button a silent no-op in the browser.
    resp = client.post(url, {"name": "Gender"})
    assert resp.status_code == 302
    assert ind.disaggregation_categories.filter(name="Gender").exists()


def _complete_disaggregation(ind, officer, *, category="Gender", values=("Male", "Female")):
    cat = DisaggregationCategory.objects.create(indicator=ind, name=category)
    for i, value in enumerate(values):
        DisaggregationValue.objects.create(
            category=cat,
            name=value,
            baseline_value=Decimal("0"),
            target_value=Decimal("100"),
            data_source="Survey",
            collection_method="Interview",
            frequency=IndicatorFrequency.QUARTERLY,
            responsible=officer,
            order=i,
        )
    return cat


def test_publish_view_activates_complete_indicator(client, officer):
    """SRS Table 64 — a validated indicator publishes and becomes ACTIVE."""
    ind = _indicator(_output_row())
    ind.workflow_status = IndicatorState.DRAFT
    ind.save(update_fields=["workflow_status"])
    _complete_disaggregation(ind, officer)
    client.force_login(officer)
    resp = client.post(
        reverse("mel_indicators:indicator_publish", kwargs={"code": ind.code})
    )
    assert resp.status_code == 302
    ind.refresh_from_db()
    assert ind.workflow_status == IndicatorState.ACTIVE


def test_publish_blocked_without_disaggregation(client, officer):
    """SRS Table 64 — publish is validation-gated; an empty draft stays DRAFT."""
    ind = _indicator(_output_row())
    ind.workflow_status = IndicatorState.DRAFT
    ind.save(update_fields=["workflow_status"])
    client.force_login(officer)
    resp = client.post(
        reverse("mel_indicators:indicator_publish", kwargs={"code": ind.code})
    )
    assert resp.status_code == 302  # bounced back to the disaggregation page
    ind.refresh_from_db()
    assert ind.workflow_status == IndicatorState.DRAFT


def test_publish_blocked_when_category_has_no_values(client, officer):
    """SRS Table 64 — 'Each disaggregation category shall contain at least one value.'"""
    ind = _indicator(_output_row())
    ind.workflow_status = IndicatorState.DRAFT
    ind.save(update_fields=["workflow_status"])
    DisaggregationCategory.objects.create(indicator=ind, name="Gender")  # no values
    client.force_login(officer)
    client.post(reverse("mel_indicators:indicator_publish", kwargs={"code": ind.code}))
    ind.refresh_from_db()
    assert ind.workflow_status == IndicatorState.DRAFT


def test_publish_blocked_when_value_incomplete(client, officer):
    """SRS Table 64 — every value needs its full performance information."""
    ind = _indicator(_output_row())
    ind.workflow_status = IndicatorState.DRAFT
    ind.save(update_fields=["workflow_status"])
    cat = DisaggregationCategory.objects.create(indicator=ind, name="Gender")
    DisaggregationValue.objects.create(category=cat, name="Male")  # incomplete
    client.force_login(officer)
    client.post(reverse("mel_indicators:indicator_publish", kwargs={"code": ind.code}))
    ind.refresh_from_db()
    assert ind.workflow_status == IndicatorState.DRAFT


def test_disaggregation_page_lists_publish_blockers(client, officer):
    ind = _indicator(_output_row())
    ind.workflow_status = IndicatorState.DRAFT
    ind.save(update_fields=["workflow_status"])
    client.force_login(officer)
    resp = client.get(
        reverse("mel_indicators:indicator_disaggregation", kwargs={"code": ind.code})
    )
    assert resp.status_code == 200
    assert resp.context["publish_blockers"]
    assert b"Before this indicator can be published" in resp.content


def test_draft_indicator_hidden_from_data_entry_and_scans(officer):
    """SRS Table 64 — DRAFT indicators are unavailable for monitoring/reporting."""
    from apps.mel.indicators.forms import DataPointForm
    from apps.mel.indicators.services import flag_off_track
    from apps.mel.reports.builders import build_indicator_rollup

    draft = _indicator(_output_row(slug="disagg-draft-hide"), code="draft-hidden")
    draft.workflow_status = IndicatorState.DRAFT
    draft.save(update_fields=["workflow_status"])

    form = DataPointForm()
    assert draft not in form.fields["indicator"].queryset

    assert all(
        row["indicator"].pk != draft.pk
        for row in flag_off_track(period_label="2026-07")
    )
    rollup = build_indicator_rollup(logframe=None, period_label="2026-07")
    assert all(r["code"] != draft.code for r in rollup["rows"])


def test_values_formset_saves(client, officer):
    ind = _indicator(_output_row())
    cat = DisaggregationCategory.objects.create(indicator=ind, name="Gender")
    client.force_login(officer)
    url = reverse(
        "mel_indicators:disaggregation_values", kwargs={"code": ind.code, "pk": cat.pk}
    )
    data = {
        "values-TOTAL_FORMS": "3",
        "values-INITIAL_FORMS": "0",
        "values-MIN_NUM_FORMS": "0",
        "values-MAX_NUM_FORMS": "1000",
        "values-0-name": "Male",
        "values-0-baseline_value": "0",
        "values-0-target_value": "40",
        "values-0-data_source": "Survey",
        "values-0-collection_method": "Interview",
        "values-0-frequency": IndicatorFrequency.QUARTERLY,
        "values-0-responsible": officer.pk,
        "values-1-name": "",
        "values-2-name": "",
    }
    resp = client.post(url, data)
    assert resp.status_code == 302, getattr(resp, "context", None)
    assert cat.values.filter(name="Male").count() == 1


# ---------------------------------------------------------------------------
# Formset catches a duplicate value gracefully (no IntegrityError / 500)
# ---------------------------------------------------------------------------


def test_value_formset_blocks_case_insensitive_duplicate(officer):
    """M&E SRS Table 64 — adding a duplicate value must surface a friendly form
    error, not a 500. The DB constraint is expression-based (``Lower(name)``), so
    an inline formset (which excludes the parent FK) can't rely on
    ``validate_unique`` — the formset-level clean has to catch it."""
    ind = _indicator(_output_row("disagg-fs"))
    cat = DisaggregationCategory.objects.create(indicator=ind, name="Gender")
    existing = DisaggregationValue.objects.create(
        category=cat, name="Male", baseline_value=Decimal("0"),
        target_value=Decimal("50"), data_source="survey",
        collection_method="interview", frequency=IndicatorFrequency.MONTHLY,
        responsible=officer,
    )
    data = {
        "values-TOTAL_FORMS": "2", "values-INITIAL_FORMS": "1",
        "values-MIN_NUM_FORMS": "0", "values-MAX_NUM_FORMS": "1000",
        "values-0-id": str(existing.pk), "values-0-name": "Male",
        "values-0-baseline_value": "0", "values-0-target_value": "50",
        "values-0-data_source": "survey", "values-0-collection_method": "interview",
        "values-0-frequency": "monthly", "values-0-responsible": str(officer.pk),
        # New row: case-variant duplicate of "Male".
        "values-1-id": "", "values-1-name": "male",
        "values-1-baseline_value": "0", "values-1-target_value": "10",
        "values-1-data_source": "survey", "values-1-collection_method": "interview",
        "values-1-frequency": "monthly", "values-1-responsible": str(officer.pk),
    }
    fs = disaggregation_value_formset(data=data, instance=cat)
    assert fs.is_valid() is False
    assert "name" in fs.forms[1].errors
    # And no duplicate was written.
    assert cat.values.count() == 1
