"""Tests for OutcomeAssessment and ImpactEvaluation (FRMFL020–027)."""
from __future__ import annotations

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

from apps.core.permissions.roles import UserRole
from apps.mel.indicators.models import (
    ImpactEvaluation,
    LogFrame,
    LogFrameLevel,
    LogFrameRow,
    OutcomeAssessment,
)

pytestmark = pytest.mark.django_db

User = get_user_model()


def _officer(email: str = "officer-eval@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 test_outcome_assessment_rejects_non_outcome_row():
    lf = LogFrame.objects.create(name="OA LF", slug="oa-lf")
    impact = LogFrameRow.objects.create(logframe=lf, level=LogFrameLevel.IMPACT, title="I")
    oa = OutcomeAssessment(logframe_row=impact, period_label="2026-Q2")
    with pytest.raises(ValidationError):
        oa.full_clean()


def test_outcome_assessment_unique_per_period():
    lf = LogFrame.objects.create(name="OA2 LF", slug="oa2-lf")
    impact = LogFrameRow.objects.create(logframe=lf, level=LogFrameLevel.IMPACT, title="I")
    outcome = LogFrameRow.objects.create(
        logframe=lf, level=LogFrameLevel.OUTCOME, title="O", parent=impact,
    )
    OutcomeAssessment.objects.create(
        logframe_row=outcome,
        period_label="2026-Q2",
        contribution_analysis="Outputs translated well into target behaviour.",
    )
    from django.db import IntegrityError, transaction

    with transaction.atomic():
        with pytest.raises(IntegrityError):
            OutcomeAssessment.objects.create(
                logframe_row=outcome, period_label="2026-Q2",
            )


def test_impact_evaluation_persists_with_history():
    lf = LogFrame.objects.create(name="Impact LF", slug="impact-lf")
    ev = ImpactEvaluation.objects.create(
        logframe=lf,
        title="End-of-programme impact",
        objectives="Measure long-term adoption.",
        counterfactual="Comparison with control cohort.",
        findings="Adoption rate doubled.",
    )
    assert ev.pk is not None
    # simple-history wired — one history row exists.
    assert ev.history.count() == 1


# ---------------------------------------------------------------------------
# View-layer CRUD coverage (FRMFL020–027 UI)
# ---------------------------------------------------------------------------


def _seed_outcome_row(slug: str = "ui-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="Adoption", parent=impact,
    )
    return lf, outcome


def test_outcome_create_view_persists_and_redirects():
    lf, outcome = _seed_outcome_row(slug="oa-create")
    client = Client()
    client.force_login(_officer("oa-create@example.com"))

    response = client.post(
        reverse("mel_indicators:outcome_create"),
        {
            "logframe_row": outcome.pk,
            "period_label": "2026-Q2",
            "metrics": "{}",
            "contribution_analysis": "Outputs translated into measurable adoption.",
            "recommendations": "Sustain delivery cadence.",
            "assessed_at": "2026-04-15",
        },
    )
    assert response.status_code == 302
    oa = OutcomeAssessment.objects.get(logframe_row=outcome, period_label="2026-Q2")
    assert "Outputs translated" in oa.contribution_analysis


def test_outcome_create_rejects_non_outcome_row_choices():
    lf, outcome = _seed_outcome_row(slug="oa-reject")
    impact = LogFrameRow.objects.get(logframe=lf, level=LogFrameLevel.IMPACT)
    client = Client()
    client.force_login(_officer("oa-reject@example.com"))

    response = client.post(
        reverse("mel_indicators:outcome_create"),
        {
            "logframe_row": impact.pk,  # not an outcome row
            "period_label": "2026-Q2",
            "metrics": "{}",
            "contribution_analysis": "Should fail.",
            "assessed_at": "2026-04-15",
        },
    )
    # Form rejects the non-outcome row at the queryset level → 200 with errors.
    assert response.status_code == 200
    assert not OutcomeAssessment.objects.filter(logframe_row=impact).exists()


def test_outcome_detail_lists_indicator_progress():
    from datetime import date
    from decimal import Decimal

    from apps.mel.indicators.models import (
        Indicator,
        IndicatorFrequency,
        IndicatorTarget,
    )
    from apps.mel.indicators.services import record_data_point

    lf, outcome = _seed_outcome_row(slug="oa-detail")
    ind = Indicator.objects.create(
        logframe_row=outcome,
        code="oa-ind",
        name="Adoption rate",
        unit="%",
        calculation_method="latest",
        data_source="survey",
        frequency=IndicatorFrequency.QUARTERLY,
    )
    IndicatorTarget.objects.create(
        indicator=ind,
        period_label="2026-Q2",
        period_start=date(2026, 4, 1),
        period_end=date(2026, 6, 30),
        baseline_value=Decimal("0"),
        target_value=Decimal("100"),
    )
    record_data_point(
        indicator=ind,
        value=85,
        period_label="2026-Q2",
        auto_verify=True,
        source_module="survey",
        source_event="rollup",
        source_object_id="1",
    )
    oa = OutcomeAssessment.objects.create(
        logframe_row=outcome,
        period_label="2026-Q2",
        contribution_analysis="Outputs landed; adoption climbing.",
    )
    client = Client()
    client.force_login(_officer("oa-detail@example.com"))
    response = client.get(reverse("mel_indicators:outcome_detail", kwargs={"pk": oa.pk}))
    assert response.status_code == 200
    body = response.content.decode()
    assert "Adoption rate" in body
    assert "Outputs landed" in body


def test_outcome_create_requires_role():
    lf, outcome = _seed_outcome_row(slug="oa-anon")
    user = User.objects.create_user(email="oa-anon@example.com", password="x")
    client = Client()
    client.force_login(user)
    response = client.get(reverse("mel_indicators:outcome_create"))
    assert response.status_code in (302, 403)


def test_impact_create_view_persists_and_redirects():
    lf = LogFrame.objects.create(name="LF imp-create", slug="imp-create")
    client = Client()
    client.force_login(_officer("imp-create@example.com"))

    response = client.post(
        reverse("mel_indicators:impact_create"),
        {
            "logframe": lf.pk,
            "title": "Cohort 5 longitudinal study",
            "objectives": "Measure 3-year sustained adoption.",
            "counterfactual": "Compare to cohort 3 baseline.",
            "comparison_group": "Non-participating universities.",
            "findings": "",
            "recommendations": "",
            "started_on": "2026-01-15",
            "completed_on": "",
            "peer_review_status": "pending",
        },
    )
    assert response.status_code == 302
    ev = ImpactEvaluation.objects.get(logframe=lf, title="Cohort 5 longitudinal study")
    assert ev.peer_review_status == "pending"


def test_impact_list_filters_by_logframe():
    lf_a = LogFrame.objects.create(name="LF A", slug="imp-a")
    lf_b = LogFrame.objects.create(name="LF B", slug="imp-b")
    ImpactEvaluation.objects.create(logframe=lf_a, title="A eval")
    ImpactEvaluation.objects.create(logframe=lf_b, title="B eval")

    client = Client()
    client.force_login(_officer("imp-list@example.com"))
    response = client.get(reverse("mel_indicators:impact_list") + "?logframe=imp-a")
    assert response.status_code == 200
    body = response.content.decode()
    assert "A eval" in body
    assert "B eval" not in body


def test_outcome_detail_writes_audit_log_on_access():
    from apps.core.audit.models import AuditLog

    lf, outcome = _seed_outcome_row(slug="oa-audit")
    oa = OutcomeAssessment.objects.create(
        logframe_row=outcome,
        period_label="2026-Q2",
        contribution_analysis="X",
    )
    user = _officer("oa-audit@example.com")
    client = Client()
    client.force_login(user)
    client.get(reverse("mel_indicators:outcome_detail", kwargs={"pk": oa.pk}))

    log = AuditLog.objects.filter(
        target_model="OutcomeAssessment", object_id=str(oa.pk),
    ).latest("timestamp")
    assert log.action == AuditLog.Action.ACCESS
    assert log.actor_id == user.pk


def test_impact_detail_writes_audit_log_on_access():
    from apps.core.audit.models import AuditLog

    lf = LogFrame.objects.create(name="LF imp-audit", slug="imp-audit")
    ev = ImpactEvaluation.objects.create(logframe=lf, title="Eval")
    user = _officer("imp-audit@example.com")
    client = Client()
    client.force_login(user)
    client.get(reverse("mel_indicators:impact_detail", kwargs={"pk": ev.pk}))

    log = AuditLog.objects.filter(
        target_model="ImpactEvaluation", object_id=str(ev.pk),
    ).latest("timestamp")
    assert log.action == AuditLog.Action.ACCESS
    assert log.actor_id == user.pk
    assert log.object_repr == "Eval"


def test_outcome_history_view_lists_revisions():
    lf, outcome = _seed_outcome_row(slug="oa-hist")
    oa = OutcomeAssessment.objects.create(
        logframe_row=outcome,
        period_label="2026-Q2",
        contribution_analysis="v1",
    )
    oa.contribution_analysis = "v2"
    oa.save()

    client = Client()
    client.force_login(_officer("oa-hist@example.com"))
    response = client.get(reverse("mel_indicators:outcome_history", kwargs={"pk": oa.pk}))
    assert response.status_code == 200
    body = response.content.decode()
    # Two revisions expected (create + update).
    assert "2 revision" in body or "2 revisions" in body
    assert "Created" in body
    assert "Updated" in body


def test_outcome_form_rejects_non_object_metrics():
    from apps.mel.indicators.forms import OutcomeAssessmentForm

    lf, outcome = _seed_outcome_row(slug="oa-metrics")
    form = OutcomeAssessmentForm(
        data={
            "logframe_row": outcome.pk,
            "period_label": "2026-Q2",
            "metrics": '["array", "not", "object"]',
            "contribution_analysis": "x",
            "assessed_at": "2026-04-15",
        },
    )
    assert not form.is_valid()
    assert "metrics" in form.errors
