"""Tests for ImpactDatapoint + Cohen's d analytics (FRMFL026)."""
from __future__ import annotations

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

from apps.core.permissions.roles import UserRole
from apps.mel.indicators.models import (
    ImpactDatapoint,
    ImpactEvaluation,
    LogFrame,
)
from apps.mel.indicators.services import calculate_cohens_d
from apps.mel.reports.builders import build_impact_section

pytestmark = pytest.mark.django_db

User = get_user_model()


def _officer(email: str = "imp-dp@example.com"):
    user = User.objects.create_user(email=email, password="x")
    user.role = UserRole.MEL_OFFICER.value
    user.save(update_fields=["role"])
    return user


def _eval(slug: str = "dp-lf") -> ImpactEvaluation:
    lf = LogFrame.objects.create(name=f"LF {slug}", slug=slug)
    return ImpactEvaluation.objects.create(logframe=lf, title=f"{slug} eval")


def _seed(ev: ImpactEvaluation, treatment: list[float], control: list[float]) -> None:
    for v in treatment:
        ImpactDatapoint.objects.create(
            evaluation=ev, group=ImpactDatapoint.Group.TREATMENT, value=v,
        )
    for v in control:
        ImpactDatapoint.objects.create(
            evaluation=ev, group=ImpactDatapoint.Group.CONTROL, value=v,
        )


# ---------------------------------------------------------------------------
# Service-level tests
# ---------------------------------------------------------------------------


def test_calculate_cohens_d_positive_effect():
    ev = _eval("d-pos")
    _seed(ev, treatment=[5, 6, 7], control=[3, 4, 5])
    result = calculate_cohens_d(ev)
    assert result is not None
    # Means differ by 2.0; pooled SD = 1.0 → Cohen's d = 2.0.
    assert result.mean_treatment == pytest.approx(6.0)
    assert result.mean_control == pytest.approx(4.0)
    assert result.pooled_std == pytest.approx(1.0)
    assert result.cohens_d == pytest.approx(2.0)
    assert result.n_treatment == 3
    assert result.n_control == 3
    assert result.magnitude == "large"


def test_calculate_cohens_d_negative_effect():
    ev = _eval("d-neg")
    _seed(ev, treatment=[3, 4, 5], control=[5, 6, 7])
    result = calculate_cohens_d(ev)
    assert result is not None
    assert result.cohens_d == pytest.approx(-2.0)
    assert result.magnitude == "large"  # magnitude is on absolute value


def test_calculate_cohens_d_negligible_effect():
    ev = _eval("d-tiny")
    _seed(ev, treatment=[5.0, 5.1, 5.0, 4.9], control=[5.0, 5.05, 5.0, 4.95])
    result = calculate_cohens_d(ev)
    assert result is not None
    assert abs(result.cohens_d) < 0.2
    assert result.magnitude == "negligible"


def test_calculate_cohens_d_returns_none_when_treatment_empty():
    ev = _eval("d-no-t")
    _seed(ev, treatment=[], control=[3, 4, 5])
    assert calculate_cohens_d(ev) is None


def test_calculate_cohens_d_returns_none_when_control_empty():
    ev = _eval("d-no-c")
    _seed(ev, treatment=[5, 6, 7], control=[])
    assert calculate_cohens_d(ev) is None


def test_calculate_cohens_d_returns_none_when_below_three_points():
    ev = _eval("d-too-few")
    _seed(ev, treatment=[5], control=[3])
    assert calculate_cohens_d(ev) is None


def test_calculate_cohens_d_returns_none_when_pooled_std_is_zero():
    ev = _eval("d-zero-sd")
    _seed(ev, treatment=[5, 5, 5], control=[5, 5, 5])
    assert calculate_cohens_d(ev) is None


def test_impact_datapoint_history_records_revisions():
    ev = _eval("dp-hist")
    dp = ImpactDatapoint.objects.create(
        evaluation=ev, group=ImpactDatapoint.Group.TREATMENT, value=5,
    )
    dp.value = 6
    dp.save()
    assert dp.history.count() == 2


# ---------------------------------------------------------------------------
# Builder integration
# ---------------------------------------------------------------------------


def test_build_impact_section_includes_cohens_d_when_enough_datapoints():
    ev = _eval("build-d")
    _seed(ev, treatment=[5, 6, 7], control=[3, 4, 5])
    section = build_impact_section(logframe=ev.logframe)
    assert section["rows"]
    row = section["rows"][0]
    assert row["cohens_d"] is not None
    assert row["cohens_d"]["value"] == pytest.approx(2.0)
    assert row["cohens_d"]["magnitude"] == "large"
    assert row["cohens_d"]["n_treatment"] == 3
    assert row["cohens_d"]["n_control"] == 3


def test_build_impact_section_returns_null_cohens_d_when_no_datapoints():
    ev = _eval("build-empty")
    section = build_impact_section(logframe=ev.logframe)
    assert section["rows"][0]["cohens_d"] is None


# ---------------------------------------------------------------------------
# View-layer tests
# ---------------------------------------------------------------------------


def test_impact_detail_renders_cohens_d_block_when_data_exists():
    ev = _eval("view-d")
    _seed(ev, treatment=[5, 6, 7], control=[3, 4, 5])
    user = _officer("view-d@example.com")
    client = Client()
    client.force_login(user)

    resp = client.get(reverse("mel_indicators:impact_detail", kwargs={"pk": ev.pk}))
    assert resp.status_code == 200
    body = resp.content.decode()
    assert "Cohen" in body
    # The pre-formatted value should appear (2.000 with 3-dp formatting).
    assert "2.000" in body
    assert "large effect" in body.lower()


def test_impact_detail_shows_empty_state_when_no_datapoints():
    ev = _eval("view-empty")
    user = _officer("view-empty@example.com")
    client = Client()
    client.force_login(user)

    resp = client.get(reverse("mel_indicators:impact_detail", kwargs={"pk": ev.pk}))
    assert resp.status_code == 200
    body = resp.content.decode()
    assert "Cohen" in body
    assert "treatment" in body.lower()


def test_impact_datapoint_create_view_persists_and_redirects():
    ev = _eval("create-dp")
    user = _officer("create-dp@example.com")
    client = Client()
    client.force_login(user)

    resp = client.post(
        reverse("mel_indicators:impact_datapoint_create", kwargs={"evaluation_pk": ev.pk}),
        {
            "group": ImpactDatapoint.Group.TREATMENT,
            "value": "7.5",
            "recorded_at": "2026-05-04T10:00",
            "notes": "First measurement",
        },
    )
    assert resp.status_code == 302
    assert ev.datapoints.count() == 1
    dp = ev.datapoints.get()
    assert dp.recorded_by_id == user.pk
    assert float(dp.value) == 7.5


def test_impact_datapoint_delete_view_removes_row():
    ev = _eval("del-dp")
    dp = ImpactDatapoint.objects.create(
        evaluation=ev, group=ImpactDatapoint.Group.CONTROL, value=4,
    )
    user = _officer("del-dp@example.com")
    client = Client()
    client.force_login(user)

    resp = client.post(
        reverse(
            "mel_indicators:impact_datapoint_delete",
            kwargs={"evaluation_pk": ev.pk, "pk": dp.pk},
        ),
    )
    assert resp.status_code == 302
    assert not ImpactDatapoint.objects.filter(pk=dp.pk).exists()


def test_impact_datapoint_create_requires_officer_role():
    ev = _eval("role-dp")
    learner = User.objects.create_user(email="learner-dp@example.com", password="x")
    client = Client()
    client.force_login(learner)
    resp = client.post(
        reverse("mel_indicators:impact_datapoint_create", kwargs={"evaluation_pk": ev.pk}),
        {
            "group": ImpactDatapoint.Group.TREATMENT,
            "value": "1",
            "recorded_at": "2026-05-04T10:00",
            "notes": "",
        },
    )
    assert resp.status_code in (302, 403)
    assert ev.datapoints.count() == 0
