"""Tests for model regression testing module."""

from __future__ import annotations

from orxaq.kernel.checkpoint import create_checkpoint
from orxaq.kernel.regression import (
    MetricDiff,
    PerformanceTracker,
    RegressionReport,
    StructuralDiff,
    compare_checkpoints,
    compare_structure,
    dag_metrics,
    run_regression_test,
)
from orxaq.kernel.types import CausalEdge, CausalRole, Entity, VariableType
from orxaq.kernel.world_dag import WorldDAG


def _make_entity(eid: str, role: CausalRole = CausalRole.EXOGENOUS) -> Entity:
    return Entity(
        entity_id=eid,
        name=eid,
        causal_role=role,
        variable_type=VariableType.STATIC,
    )


def _make_dag(n_entities: int = 3, n_edges: int = 2) -> WorldDAG:
    """Build a simple DAG for testing."""
    dag = WorldDAG()
    ids = [f"e{i}" for i in range(n_entities)]
    for eid in ids:
        dag.add_entity(_make_entity(eid))
    for i in range(min(n_edges, n_entities - 1)):
        dag.add_edge(
            CausalEdge(
                source=ids[i],
                target=ids[i + 1],
                mechanism="test",
                confidence=0.8,
                soliton_score=0.7,
            )
        )
    return dag


# ── StructuralDiff ──────────────────────────────────────


class TestStructuralDiff:
    def test_no_changes(self):
        diff = StructuralDiff()
        assert not diff.has_changes
        assert diff.change_count == 0

    def test_with_changes(self):
        diff = StructuralDiff(
            added_entities=["e3"],
            removed_edges=[("e0", "e1")],
        )
        assert diff.has_changes
        assert diff.change_count == 2

    def test_to_dict(self):
        diff = StructuralDiff(
            added_entities=["x"],
            added_edges=[("a", "b")],
        )
        d = diff.to_dict()
        assert d["has_changes"] is True
        assert d["change_count"] == 2
        assert d["added_edges"] == [{"source": "a", "target": "b"}]


# ── MetricDiff ──────────────────────────────────────────


class TestMetricDiff:
    def test_improvement(self):
        md = MetricDiff(
            name="accuracy",
            baseline_value=0.90,
            candidate_value=0.95,
            direction="higher_is_better",
        )
        assert md.delta > 0
        assert not md.is_regression

    def test_regression(self):
        md = MetricDiff(
            name="accuracy",
            baseline_value=0.90,
            candidate_value=0.80,
            threshold=0.05,
            direction="higher_is_better",
        )
        assert md.is_regression

    def test_within_threshold(self):
        md = MetricDiff(
            name="accuracy",
            baseline_value=0.90,
            candidate_value=0.87,
            threshold=0.05,
            direction="higher_is_better",
        )
        assert not md.is_regression

    def test_lower_is_better(self):
        md = MetricDiff(
            name="error_rate",
            baseline_value=0.10,
            candidate_value=0.15,
            threshold=0.02,
            direction="lower_is_better",
        )
        assert md.is_regression

    def test_pct_change(self):
        md = MetricDiff(
            name="x",
            baseline_value=100.0,
            candidate_value=110.0,
        )
        assert md.pct_change == 10.0

    def test_pct_change_zero_baseline(self):
        md = MetricDiff(
            name="x",
            baseline_value=0.0,
            candidate_value=5.0,
        )
        assert md.pct_change == 100.0

    def test_pct_change_both_zero(self):
        md = MetricDiff(
            name="x",
            baseline_value=0.0,
            candidate_value=0.0,
        )
        assert md.pct_change == 0.0

    def test_to_dict(self):
        md = MetricDiff(
            name="acc",
            baseline_value=0.9,
            candidate_value=0.95,
        )
        d = md.to_dict()
        assert d["name"] == "acc"
        assert d["baseline"] == 0.9
        assert d["candidate"] == 0.95
        assert "delta" in d
        assert "pct_change" in d


# ── compare_structure ───────────────────────────────────


class TestCompareStructure:
    def test_identical_dags(self):
        dag = _make_dag()
        diff = compare_structure(dag, dag)
        assert not diff.has_changes

    def test_added_entity(self):
        base = _make_dag(3, 2)
        cand = _make_dag(3, 2)
        cand.add_entity(_make_entity("e_new"))
        diff = compare_structure(base, cand)
        assert "e_new" in diff.added_entities
        assert not diff.removed_entities

    def test_removed_entity(self):
        base = _make_dag(3, 2)
        cand = _make_dag(2, 1)
        diff = compare_structure(base, cand)
        assert "e2" in diff.removed_entities

    def test_added_edge(self):
        base = _make_dag(3, 1)
        cand = _make_dag(3, 2)
        diff = compare_structure(base, cand)
        assert len(diff.added_edges) == 1

    def test_modified_edge(self):
        base = _make_dag(3, 2)
        cand = WorldDAG()
        for eid in ["e0", "e1", "e2"]:
            cand.add_entity(_make_entity(eid))
        cand.add_edge(
            CausalEdge(
                source="e0",
                target="e1",
                mechanism="test",
                confidence=0.5,
                soliton_score=0.7,
            )
        )
        cand.add_edge(
            CausalEdge(
                source="e1",
                target="e2",
                mechanism="test",
                confidence=0.8,
                soliton_score=0.7,
            )
        )
        diff = compare_structure(base, cand)
        assert len(diff.modified_edges) == 1
        assert diff.modified_edges[0]["changes"]["confidence"]["from"] == 0.8
        assert diff.modified_edges[0]["changes"]["confidence"]["to"] == 0.5


# ── compare_checkpoints ────────────────────────────────


class TestCompareCheckpoints:
    def test_compare(self):
        base_dag = _make_dag(3, 2)
        cand_dag = _make_dag(4, 3)
        cp_base = create_checkpoint(base_dag, "model", "1.0")
        cp_cand = create_checkpoint(cand_dag, "model", "2.0")
        diff = compare_checkpoints(cp_base, cp_cand)
        assert diff.has_changes
        assert "e3" in diff.added_entities


# ── dag_metrics ─────────────────────────────────────────


class TestDagMetrics:
    def test_basic_metrics(self):
        dag = _make_dag(3, 2)
        m = dag_metrics(dag)
        assert m["entity_count"] == 3.0
        assert m["edge_count"] == 2.0
        assert m["density"] > 0
        assert 0 <= m["avg_confidence"] <= 1
        assert 0 <= m["avg_soliton_score"] <= 1

    def test_empty_dag(self):
        dag = WorldDAG()
        m = dag_metrics(dag)
        assert m["entity_count"] == 0.0
        assert m["edge_count"] == 0.0
        assert m["density"] == 0.0
        assert m["avg_confidence"] == 0.0

    def test_single_entity(self):
        dag = WorldDAG()
        dag.add_entity(_make_entity("solo"))
        m = dag_metrics(dag)
        assert m["entity_count"] == 1.0
        assert m["density"] == 0.0


# ── run_regression_test ─────────────────────────────────


class TestRunRegressionTest:
    def test_passing(self):
        dag = _make_dag(3, 2)
        cp1 = create_checkpoint(dag, "model", "1.0")
        cp2 = create_checkpoint(dag, "model", "2.0")
        report = run_regression_test(cp1, cp2)
        assert report.passed
        assert report.regression_count == 0

    def test_failing_regression(self):
        base_dag = _make_dag(5, 4)
        cand_dag = _make_dag(3, 1)
        cp1 = create_checkpoint(base_dag, "model", "1.0")
        cp2 = create_checkpoint(cand_dag, "model", "2.0")
        report = run_regression_test(cp1, cp2)
        assert not report.passed
        assert report.regression_count > 0
        assert len(report.notes) > 0

    def test_custom_thresholds(self):
        base_dag = _make_dag(3, 2)
        cand_dag = _make_dag(3, 2)
        cp1 = create_checkpoint(base_dag, "model", "1.0")
        cp2 = create_checkpoint(cand_dag, "model", "2.0")
        report = run_regression_test(
            cp1,
            cp2,
            metric_thresholds={"avg_confidence": 0.01},
        )
        assert report.passed

    def test_report_to_dict(self):
        dag = _make_dag(3, 2)
        cp = create_checkpoint(dag, "m", "1.0")
        report = run_regression_test(cp, cp)
        d = report.to_dict()
        assert "baseline" in d
        assert "candidate" in d
        assert "passed" in d
        assert "metrics" in d

    def test_structural_notes(self):
        base_dag = _make_dag(3, 2)
        cand_dag = _make_dag(4, 3)
        cp1 = create_checkpoint(base_dag, "m", "1.0")
        cp2 = create_checkpoint(cand_dag, "m", "2.0")
        report = run_regression_test(cp1, cp2)
        assert any("Structural" in n for n in report.notes)


# ── PerformanceTracker ──────────────────────────────────


class TestPerformanceTracker:
    def test_record_and_history(self):
        tracker = PerformanceTracker()
        tracker.record("v1", {"acc": 0.90})
        tracker.record("v2", {"acc": 0.92})
        assert len(tracker.history()) == 2

    def test_latest(self):
        tracker = PerformanceTracker()
        tracker.record("v1", {"acc": 0.90})
        tracker.record("v2", {"acc": 0.92})
        latest = tracker.latest()
        assert latest["version"] == "v2"

    def test_latest_empty(self):
        tracker = PerformanceTracker()
        assert tracker.latest() is None

    def test_metric_series(self):
        tracker = PerformanceTracker()
        tracker.record("v1", {"acc": 0.90})
        tracker.record("v2", {"acc": 0.92})
        tracker.record("v3", {"acc": 0.91})
        series = tracker.metric_series("acc")
        assert series == [0.90, 0.92, 0.91]

    def test_detect_drift_improving(self):
        tracker = PerformanceTracker()
        tracker.record("v1", {"acc": 0.80})
        tracker.record("v2", {"acc": 0.85})
        tracker.record("v3", {"acc": 0.90})
        drift = tracker.detect_drift("acc", window=3)
        assert drift["trend"] == "improving"
        assert not drift["alert"]

    def test_detect_drift_degrading(self):
        tracker = PerformanceTracker()
        tracker.record("v1", {"acc": 0.90})
        tracker.record("v2", {"acc": 0.85})
        tracker.record("v3", {"acc": 0.80})
        drift = tracker.detect_drift("acc", window=3)
        assert drift["trend"] == "degrading"
        assert drift["alert"]

    def test_detect_drift_stable(self):
        tracker = PerformanceTracker()
        tracker.record("v1", {"acc": 0.90})
        tracker.record("v2", {"acc": 0.89})
        drift = tracker.detect_drift("acc", window=2, threshold=0.05)
        assert drift["trend"] == "stable"

    def test_detect_drift_insufficient(self):
        tracker = PerformanceTracker()
        tracker.record("v1", {"acc": 0.90})
        drift = tracker.detect_drift("acc")
        assert drift["trend"] == "insufficient_data"

    def test_summary(self):
        tracker = PerformanceTracker()
        tracker.record("v1", {"acc": 0.9, "cov": 0.8})
        s = tracker.summary()
        assert s["versions"] == 1
        assert "acc" in s["metrics"]

    def test_summary_empty(self):
        tracker = PerformanceTracker()
        s = tracker.summary()
        assert s["versions"] == 0

    def test_detect_drift_zero_baseline(self):
        """Cover line 331: first==0 branch in detect_drift."""
        tracker = PerformanceTracker()
        tracker.record("v1", {"acc": 0.0})
        tracker.record("v2", {"acc": 0.0})
        drift = tracker.detect_drift("acc", window=2)
        assert drift["trend"] == "stable"
        assert drift["magnitude"] == 0.0


# ── Targeted coverage (lines 157, 162) ──────────────────────


class TestEdgeDiffDetails:
    """Cover lines 157, 162: soliton_score and mechanism changes in edge diff."""

    def test_soliton_score_change(self):
        """Cover line 157: soliton_score differs between checkpoints."""
        dag1 = WorldDAG()
        dag2 = WorldDAG()
        for dag in (dag1, dag2):
            dag.add_entity(_make_entity("A"))
            dag.add_entity(_make_entity("B"))
        dag1.add_edge(CausalEdge(source="A", target="B", soliton_score=0.3))
        dag2.add_edge(CausalEdge(source="A", target="B", soliton_score=0.9))
        diff = compare_structure(dag1, dag2)
        assert len(diff.modified_edges) == 1
        assert "soliton_score" in diff.modified_edges[0]["changes"]

    def test_mechanism_change(self):
        """Cover line 162: mechanism differs between checkpoints."""
        dag1 = WorldDAG()
        dag2 = WorldDAG()
        for dag in (dag1, dag2):
            dag.add_entity(_make_entity("A"))
            dag.add_entity(_make_entity("B"))
        dag1.add_edge(CausalEdge(source="A", target="B", mechanism="old_mech"))
        dag2.add_edge(CausalEdge(source="A", target="B", mechanism="new_mech"))
        diff = compare_structure(dag1, dag2)
        assert len(diff.modified_edges) == 1
        assert "mechanism" in diff.modified_edges[0]["changes"]


# ── RegressionReport ────────────────────────────────────


class TestRegressionReport:
    def test_regression_count(self):
        report = RegressionReport(
            baseline_name="m@1.0",
            candidate_name="m@2.0",
            structural=StructuralDiff(),
            metrics=[
                MetricDiff(
                    name="a",
                    baseline_value=0.9,
                    candidate_value=0.7,
                    threshold=0.05,
                ),
                MetricDiff(
                    name="b",
                    baseline_value=0.9,
                    candidate_value=0.95,
                    threshold=0.05,
                ),
            ],
            passed=False,
        )
        assert report.regression_count == 1
