"""Browser-QA sweep fixes (2026-07-11) — WS1 indicators.

Covers defects 1, 3, 4, 5, 6, 7, 17, 35, 37 from the MEL defect register.
Do NOT run under a parallel pytest — the test_iilmp DB collides.
"""
from __future__ import annotations

from datetime import date, datetime
from decimal import Decimal

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

from apps.core.audit.models import AuditLog
from apps.mel.indicators.forms import DataPointForm, LogFrameRowForm
from apps.mel.indicators.models import (
    DataPoint,
    Indicator,
    IndicatorFrequency,
    IndicatorTarget,
    LogFrame,
    LogFrameLevel,
    LogFrameRow,
    ValidationRule,
)
from apps.mel.indicators.services import (
    calculate_progress,
    import_data_points_csv,
    record_data_point,
)
from apps.mel.indicators.views import ConsolidatedAuditLogView

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


# ---------------------------------------------------------------------------
# fixtures / helpers
# ---------------------------------------------------------------------------


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


def _mk_indicator(*, code="ws1-ind", lf_slug="ws1-lf"):
    lf = LogFrame.objects.create(name=f"LF {code}", slug=lf_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,
    )
    ind = Indicator.objects.create(
        logframe_row=output,
        code=code,
        name="WS1 indicator",
        unit="farmers",
        calculation_method="Count.",
        data_source="manual",
        frequency=IndicatorFrequency.MONTHLY,
    )
    return lf, ind


def _target(ind, *, period="2026-04", baseline="0", target="10"):
    return IndicatorTarget.objects.create(
        indicator=ind,
        period_label=period,
        period_start=date(2026, 4, 1),
        period_end=date(2026, 4, 30),
        baseline_value=Decimal(baseline),
        target_value=Decimal(target),
    )


# ---------------------------------------------------------------------------
# Defect 1 — validation-rule rejection re-renders instead of 500
# ---------------------------------------------------------------------------


def test_data_entry_validation_rule_rejection_renders_form_not_500(client, officer):
    _, ind = _mk_indicator()
    ValidationRule.objects.create(
        indicator=ind,
        rule_type=ValidationRule.RuleType.RANGE,
        params={"min": 0, "max": 1000},
        message="Value must be between 0 and 1000 farmers.",
        is_active=True,
    )
    client.force_login(officer)
    resp = client.post(
        reverse("mel_indicators:data_entry"),
        {
            "indicator": ind.pk,
            "period_label": "2026-04",
            "reported_at": "2026-04-15T10:30",
            "value": "5000",
            "qualitative_note": "",
        },
    )
    # Form re-render (200), NOT a 500 and NOT a redirect.
    assert resp.status_code == 200
    assert b"Value must be between 0 and 1000 farmers." in resp.content
    # Nothing was written.
    assert DataPoint.objects.filter(indicator=ind).count() == 0


# ---------------------------------------------------------------------------
# Defect 4 — exactly one DataPoint per manual submit
# ---------------------------------------------------------------------------


def test_manual_entry_creates_exactly_one_datapoint(client, officer):
    _, ind = _mk_indicator(code="ws1-dup", lf_slug="ws1-dup-lf")
    client.force_login(officer)
    resp = client.post(
        reverse("mel_indicators:data_entry"),
        {
            "indicator": ind.pk,
            "period_label": "2026-04",
            "reported_at": "2026-04-15T10:30",
            "value": "42",
            "qualitative_note": "",
        },
    )
    assert resp.status_code == 302  # redirect to detail on success
    assert DataPoint.objects.filter(indicator=ind).count() == 1
    dp = DataPoint.objects.get(indicator=ind)
    assert dp.reported_by_id == officer.pk


# ---------------------------------------------------------------------------
# Defect 3 / BUG-1 — reported_at renders without seconds
# ---------------------------------------------------------------------------


def test_reported_at_widget_has_no_seconds():
    form = DataPointForm(initial={"reported_at": datetime(2026, 7, 11, 5, 52, 42)})
    html = str(form["reported_at"])
    assert "2026-07-11T05:52" in html
    assert "05:52:42" not in html  # no seconds → native minute validation works


def test_reported_at_accepts_minute_precision_input():
    _, ind = _mk_indicator(code="ws1-dt", lf_slug="ws1-dt-lf")
    form = DataPointForm(
        {
            "indicator": ind.pk,
            "period_label": "2026-04",
            "reported_at": "2026-04-15T10:30",
            "value": "5",
        }
    )
    assert form.is_valid(), form.errors


# ---------------------------------------------------------------------------
# Defect 5 — MEI010 target revision writes audit + revised_by/at
# ---------------------------------------------------------------------------


def test_target_revision_records_audit_and_reviser(client, officer, django_capture_on_commit_callbacks):
    from config.celery import app as celery_app

    _, ind = _mk_indicator(code="ws1-rev", lf_slug="ws1-rev-lf")
    target = _target(ind, target="500")
    assert target.original_target_value == Decimal("500.0000")

    celery_app.conf.task_always_eager = True
    client.force_login(officer)
    with django_capture_on_commit_callbacks(execute=True):
        resp = client.post(
            reverse("mel_indicators:target_edit", kwargs={"pk": target.pk}),
            {
                "baseline_value": "0",
                "target_value": "450",
                "notes": "",
                "revision_reason": "Drought cut the achievable target.",
            },
        )
    assert resp.status_code == 302

    target.refresh_from_db()
    assert target.target_value == Decimal("450.0000")
    assert target.original_target_value == Decimal("500.0000")  # frozen
    assert target.revised_by_id == officer.pk
    assert target.revised_at is not None
    assert target.revision_reason == "Drought cut the achievable target."

    entry = AuditLog.objects.filter(
        target_model="IndicatorTarget",
        object_id=str(target.pk),
        action=AuditLog.Action.UPDATE,
    ).first()
    assert entry is not None
    assert entry.changes.get("new") == "450"
    assert entry.changes.get("previous") == "500.0000"


# ---------------------------------------------------------------------------
# Defect 6 — cross-logframe parent rejected + parent queryset scoped
# ---------------------------------------------------------------------------


def test_cross_logframe_parent_rejected_at_clean():
    lf1, _ = _mk_indicator(code="ws1-x1", lf_slug="ws1-x1-lf")
    lf1_outcome = LogFrameRow.objects.filter(
        logframe=lf1, level=LogFrameLevel.OUTCOME,
    ).first()
    lf2 = LogFrame.objects.create(name="LF two", slug="ws1-x2-lf")
    LogFrameRow.objects.create(logframe=lf2, level=LogFrameLevel.IMPACT, title="I2")

    row = LogFrameRow(
        logframe=lf2,
        parent=lf1_outcome,  # parent lives in a DIFFERENT log frame
        level=LogFrameLevel.OUTPUT,
        title="Bad row",
    )
    from django.core.exceptions import ValidationError

    with pytest.raises(ValidationError) as exc:
        row.full_clean()
    assert "same Strategic Objective" in str(exc.value)


def test_row_form_parent_queryset_scoped_to_logframe():
    lf1, _ = _mk_indicator(code="ws1-s1", lf_slug="ws1-s1-lf")
    lf2 = LogFrame.objects.create(name="Scope two", slug="ws1-s2-lf")
    LogFrameRow.objects.create(logframe=lf2, level=LogFrameLevel.IMPACT, title="Only me")

    form = LogFrameRowForm(logframe_scope=lf2)
    parent_ids = set(form.fields["parent"].queryset.values_list("logframe_id", flat=True))
    assert parent_ids <= {lf2.pk}
    assert lf1.pk not in parent_ids


# ---------------------------------------------------------------------------
# Defect 7 — officer verifies a pending point; it then counts toward actual
# ---------------------------------------------------------------------------


def test_officer_verify_flips_status_and_counts_toward_actual(client, officer):
    _, ind = _mk_indicator(code="ws1-vfy", lf_slug="ws1-vfy-lf")
    _target(ind, target="10")
    result = record_data_point(
        indicator=ind, value=4, period_label="2026-04", auto_verify=False,
    )
    dp = result.data_point
    assert dp.status == DataPoint.Status.PENDING
    # Pending values are NOT counted.
    assert calculate_progress(ind, "2026-04")["actual"] == Decimal("0")

    client.force_login(officer)
    resp = client.post(reverse("mel_indicators:datapoint_verify", kwargs={"pk": dp.pk}))
    assert resp.status_code == 302

    dp.refresh_from_db()
    assert dp.status == DataPoint.Status.VERIFIED
    assert calculate_progress(ind, "2026-04")["actual"] == Decimal("4")


def test_indicator_detail_shows_pending_review_section(client, officer):
    _, ind = _mk_indicator(code="ws1-pend", lf_slug="ws1-pend-lf")
    record_data_point(indicator=ind, value=3, period_label="2026-04", auto_verify=False)
    client.force_login(officer)
    resp = client.get(reverse("mel_indicators:indicator_detail", kwargs={"code": ind.code}))
    assert resp.status_code == 200
    assert b"Pending data points" in resp.content
    assert b"Verify" in resp.content


# ---------------------------------------------------------------------------
# Defect 17 — audit log surfaces field-level deltas on UPDATE rows
# ---------------------------------------------------------------------------


def test_audit_log_surfaces_field_delta_on_update():
    admin = User.objects.create_user(email="ws1-admin@example.com", password="x")
    admin.is_superuser = True
    admin.role = "system_admin"
    admin.save()

    _, ind = _mk_indicator(code="ws1-audit", lf_slug="ws1-audit-lf")
    ind.name = "Renamed indicator"
    ind.save()  # creates an UPDATE history record with a name delta

    rf = RequestFactory()
    req = rf.get(reverse("mel_indicators:audit_log") + "?model=Indicator&action=update")
    req.user = admin
    resp = ConsolidatedAuditLogView.as_view()(req)
    assert resp.status_code == 200
    html = resp.content.decode()
    # The diff old-value only lives in the delta cell, never in object_repr.
    assert "WS1 indicator" in html  # old value surfaced
    assert "field" in html and "changed" in html


def test_audit_log_object_label_has_no_timestamp_suffix():
    admin = User.objects.create_user(email="ws1-admin2@example.com", password="x")
    admin.is_superuser = True
    admin.role = "system_admin"
    admin.save()
    _mk_indicator(code="ws1-lbl", lf_slug="ws1-lbl-lf")

    rf = RequestFactory()
    req = rf.get(reverse("mel_indicators:audit_log"))
    req.user = admin
    resp = ConsolidatedAuditLogView.as_view()(req)
    html = resp.content.decode()
    assert " as of " not in html  # microsecond suffix stripped


# ---------------------------------------------------------------------------
# Defect 35 — CSV import friendly number error
# ---------------------------------------------------------------------------


def test_csv_import_non_numeric_value_friendly_error():
    _, ind = _mk_indicator(code="ws1-csv", lf_slug="ws1-csv-lf")
    csv_text = "indicator_code,period_label,value\n%s,2026-04,notanumber\n" % ind.code
    result = import_data_points_csv(
        csv_text=csv_text, csv_bytes=csv_text.encode(), user=None, dry_run=True,
    )
    assert result["errors"], result
    assert result["errors"][0]["error"] == "value must be a number"
    assert "ConversionSyntax" not in result["errors"][0]["error"]


# ---------------------------------------------------------------------------
# Defect 37 — indicator search matches code as well as name
# ---------------------------------------------------------------------------


def test_indicator_search_matches_code(client, officer):
    _, ind = _mk_indicator(code="ws1-searchcode", lf_slug="ws1-search-lf")
    client.force_login(officer)
    resp = client.get(reverse("mel_indicators:indicator_list") + "?q=searchcode")
    assert resp.status_code == 200
    assert b"ws1-searchcode" in resp.content
