"""Tests for multi-algorithm consensus engine."""

from __future__ import annotations

import random
from unittest.mock import MagicMock, patch

from orxaq.intelligence.consensus import ConsensusEngine
from orxaq.intelligence.ges_algorithm import GESAlgorithm, GESResult
from orxaq.intelligence.pc_algorithm import PCAlgorithm


def _make_chain_data(n: int = 500, seed: int = 42) -> list[dict]:
    """X → Z → Y chain with strong effects."""
    rng = random.Random(seed)
    data = []
    for _ in range(n):
        x = rng.gauss(0, 1)
        z = 3.0 * x + rng.gauss(0, 0.3)
        y = 3.0 * z + rng.gauss(0, 0.3)
        data.append({"X": x, "Z": z, "Y": y})
    return data


def _make_independent_data(n: int = 500, seed: int = 42) -> list[dict]:
    """Three independent variables."""
    rng = random.Random(seed)
    return [{"X": rng.gauss(0, 1), "Y": rng.gauss(0, 1), "Z": rng.gauss(0, 1)} for _ in range(n)]


class TestConsensusDiscovery:
    def test_discovers_true_edges(self):
        """Consensus should find edges in a chain structure."""
        data = _make_chain_data(n=800)
        engine = ConsensusEngine(alpha=0.05, threshold=0.5)
        result = engine.discover(data, ["X", "Z", "Y"])

        assert len(result.edges) > 0
        # At least one edge should involve Z
        z_edges = [e for e in result.edges if e.source == "Z" or e.target == "Z"]
        assert len(z_edges) > 0

    def test_edges_have_support_count(self):
        """Each consensus edge should have a positive support count."""
        data = _make_chain_data(n=800)
        engine = ConsensusEngine(alpha=0.05, threshold=0.5)
        result = engine.discover(data, ["X", "Z", "Y"])

        for edge in result.edges:
            assert edge.support_count >= 1
            assert edge.support_fraction > 0.0

    def test_edges_have_supporters(self):
        """Each edge should list which algorithms support it."""
        data = _make_chain_data(n=800)
        engine = ConsensusEngine(alpha=0.05, threshold=0.5)
        result = engine.discover(data, ["X", "Z", "Y"])

        for edge in result.edges:
            assert len(edge.supporters) >= 1
            for s in edge.supporters:
                assert s in ("pc", "ges")

    def test_confidence_levels(self):
        """Edges with all algorithms agreeing should be 'high' confidence."""
        data = _make_chain_data(n=800)
        engine = ConsensusEngine(alpha=0.05, threshold=0.5)
        result = engine.discover(data, ["X", "Z", "Y"])

        for edge in result.edges:
            assert edge.confidence in ("high", "medium", "low")
            if edge.support_count == 2:
                assert edge.confidence == "high"

    def test_threshold_filtering(self):
        """High threshold should exclude weakly supported edges."""
        data = _make_chain_data(n=800)

        # Low threshold — more edges
        engine_low = ConsensusEngine(alpha=0.05, threshold=0.3)
        result_low = engine_low.discover(data, ["X", "Z", "Y"])

        # High threshold — fewer or equal edges
        engine_high = ConsensusEngine(alpha=0.05, threshold=1.0)
        result_high = engine_high.discover(data, ["X", "Z", "Y"])

        assert len(result_high.edges) <= len(result_low.edges)


class TestConsensusMetadata:
    def test_algorithms_used(self):
        """Should report both algorithms."""
        data = _make_chain_data(n=300)
        engine = ConsensusEngine()
        result = engine.discover(data, ["X", "Z", "Y"])

        assert "pc" in result.algorithms_used
        assert "ges" in result.algorithms_used

    def test_agreement_threshold_recorded(self):
        """Threshold should be recorded in result."""
        engine = ConsensusEngine(threshold=0.75)
        data = _make_chain_data(n=300)
        result = engine.discover(data, ["X", "Z", "Y"])
        assert result.agreement_threshold == 0.75


class TestConsensusBootstrap:
    def test_soliton_scores_when_bootstrapped(self):
        """Soliton scores should be set when bootstrap_samples > 0."""
        data = _make_chain_data(n=300)
        engine = ConsensusEngine(alpha=0.05, threshold=0.5, bootstrap_samples=10, seed=42)
        result = engine.discover(data, ["X", "Z", "Y"])

        for edge in result.edges:
            assert edge.soliton_score is not None
            assert 0.0 <= edge.soliton_score <= 1.0

    def test_no_soliton_without_bootstrap(self):
        """Soliton scores should be None without bootstrap."""
        data = _make_chain_data(n=300)
        engine = ConsensusEngine(alpha=0.05, threshold=0.5, bootstrap_samples=0)
        result = engine.discover(data, ["X", "Z", "Y"])

        for edge in result.edges:
            assert edge.soliton_score is None


class TestConsensusIndependent:
    def test_few_edges_for_independent_data(self):
        """Independent data should produce few consensus edges."""
        data = _make_independent_data(n=500)
        engine = ConsensusEngine(alpha=0.05, threshold=1.0)
        result = engine.discover(data, ["X", "Y", "Z"])

        # With full agreement required, independent data should have 0-1 edges
        assert len(result.edges) <= 1


# ── Targeted coverage tests ──────────────────────────────────


def _make_collider_data(n: int = 800, seed: int = 42) -> list[dict]:
    """A → C ← B collider structure — PC orients edges."""
    rng = random.Random(seed)
    data = []
    for _ in range(n):
        a = rng.gauss(0, 1)
        b = rng.gauss(0, 1)
        c = 3.0 * a + 3.0 * b + rng.gauss(0, 0.3)
        data.append({"A": a, "B": b, "C": c})
    return data


class TestConsensusDirectedEdges:
    """Cover lines 96-98 (PC directed edges in consensus)."""

    def test_collider_produces_directed_edges(self):
        """Collider structure should produce directed consensus edges."""
        data = _make_collider_data(n=800)
        engine = ConsensusEngine(alpha=0.05, threshold=0.5)
        result = engine.discover(data, ["A", "B", "C"])
        assert len(result.edges) > 0
        directed = [e for e in result.edges if e.directed]
        assert len(directed) > 0


class TestConsensusMediumConfidence:
    """Cover lines 140-141 (medium confidence assignment)."""

    def test_single_algorithm_edge_is_medium(self):
        """Edge found by one algorithm + threshold=0.5 → 'medium'."""
        data = _make_chain_data(n=300)
        engine = ConsensusEngine(alpha=0.05, threshold=0.5)
        mock_ges_result = GESResult(
            edges=frozenset(),
            bic_score=0.0,
            additions=0,
            removals=0,
        )
        with patch.object(GESAlgorithm, "fit", return_value=mock_ges_result):
            result = engine.discover(data, ["X", "Z", "Y"])

        assert len(result.edges) > 0
        for edge in result.edges:
            assert edge.confidence == "medium"
            assert edge.support_count == 1


class TestConsensusBootstrapOriented:
    """Cover lines 200-204 (bootstrap finds edge in oriented, not skeleton)."""

    def test_edge_found_via_oriented(self):
        """Bootstrap checks oriented edges when not found in skeleton."""
        data = _make_chain_data(n=200)
        engine = ConsensusEngine(
            alpha=0.05,
            threshold=0.5,
            bootstrap_samples=3,
            seed=42,
        )

        mock_result = MagicMock()
        mock_result.skeleton = frozenset()
        mock_result.oriented = frozenset({("X", "Z")})

        with patch.object(PCAlgorithm, "fit", return_value=mock_result):
            score = engine._bootstrap_stability(
                data,
                ["X", "Z", "Y"],
                ("X", "Z"),
                set(),
                set(),
            )

        assert score > 0.0
