"""
Tests for experiment-driven code changes:
  - EXP-004: Sign-aware hold logic in FeedbackLoop
  - EXP-002: KernelTopology bulk protection weights
  - EXP-002: Topology-aware TrajectoryBus integration
  - EXP-002: Topology-aware PeerQueryRouter routing
"""

import numpy as np

from qig_core.consciousness.feedback_loop import FeedbackLoop
from qig_core.consciousness.peer_query import PeerQuery, PeerQueryRouter
from qig_core.consciousness.topology import KernelTopology
from qig_core.consciousness.trajectory_bus import TrajectoryBus, TrajectoryMessage
from qig_core.coordizer.geometry import BASIN_DIM, random_basin, slerp, to_simplex


# ── EXP-004: Sign-aware hold logic ──────────────────────────────


class TestFeedbackSignAwareHold:
    """EXP-004: κ changes sign near h ≈ 1.4. When correction direction
    flips across consecutive cycles, hold steady to avoid oscillation."""

    def test_stable_corrections_anneal_normally(self):
        """When correction direction is stable, annealing proceeds."""
        loop = FeedbackLoop(threshold=0.1)
        intended = [to_simplex(np.ones(BASIN_DIM))]

        # Two measurements with similar correction directions
        expressed_1 = random_basin(BASIN_DIM)
        loop.measure(intended, expressed_1)

        # Second measurement with same intended — correction direction similar
        expressed_2 = slerp(expressed_1, to_simplex(np.ones(BASIN_DIM)), 0.1)
        loop.measure(intended, expressed_2)

        # Should NOT be in regime hold — corrections are aligned
        assert not loop.regime_hold_active

    def _make_flip_pair(self):
        """Create (intended_a, intended_b, expressed) that produce flipped corrections.

        The key: expressed is near intended_b so correction_1 = slerp(expressed≈B, A, 0.5)
        pulls strongly toward A. Then expressed is near intended_a so correction_2 =
        slerp(expressed≈A, B, 0.5) pulls strongly toward B. These corrections are
        in opposite directions on Δ⁶³.
        """
        intended_a = to_simplex(np.eye(BASIN_DIM)[0] + 0.001)
        intended_b = to_simplex(np.eye(BASIN_DIM)[BASIN_DIM // 2] + 0.001)
        return intended_a, intended_b

    def test_flipped_correction_triggers_hold(self):
        """When correction flips direction, regime hold activates."""
        loop = FeedbackLoop(threshold=0.01)
        intended_a, intended_b = self._make_flip_pair()

        # Cycle 1: intended=A, expressed near B → correction pulled toward A
        loop.measure([intended_a], intended_b)

        # Cycle 2: intended=B, expressed near A → correction pulled toward B
        loop.measure([intended_b], intended_a)

        # Should be in regime hold — correction direction flipped
        assert loop.regime_hold_active

    def test_hold_suppresses_anneal(self):
        """When regime hold is active, anneal returns zero changes."""
        loop = FeedbackLoop(threshold=0.01)
        intended_a, intended_b = self._make_flip_pair()

        loop.measure([intended_a], intended_b)
        m2 = loop.measure([intended_b], intended_a)

        # Even though should_anneal is True, hold should suppress
        bank = {i: random_basin(BASIN_DIM) for i in range(10)}
        updated, n_annealed = loop.anneal(bank, m2)
        assert n_annealed == 0

    def test_get_state_includes_regime_hold(self):
        """State dict includes regime_hold flag."""
        loop = FeedbackLoop()
        state = loop.get_state()
        assert "regime_hold" in state
        assert state["regime_hold"] is False

    def test_regime_hold_property(self):
        """regime_hold_active property accessible."""
        loop = FeedbackLoop()
        assert loop.regime_hold_active is False


# ── EXP-002: KernelTopology ─────────────────────────────────────


class TestKernelTopology:
    """EXP-002: Interior nodes have 66.9× better geometric integrity."""

    def test_add_kernel_and_connect(self):
        topo = KernelTopology()
        topo.add_kernel("a")
        topo.add_kernel("b")
        topo.connect("a", "b")
        assert "b" in topo._nodes["a"].neighbors
        assert "a" in topo._nodes["b"].neighbors

    def test_interior_detection(self):
        """Kernel with 3+ connections is interior."""
        topo = KernelTopology()
        # Hub connected to 3 others
        topo.connect("hub", "a")
        topo.connect("hub", "b")
        topo.connect("hub", "c")
        assert topo.is_interior("hub")
        assert not topo.is_interior("a")  # only 1 connection

    def test_bulk_protection_weight(self):
        """Interior kernels get weight boost."""
        topo = KernelTopology()
        topo.connect("hub", "a")
        topo.connect("hub", "b")
        topo.connect("hub", "c")
        assert topo.bulk_protection_weight("hub") > 1.0
        assert topo.bulk_protection_weight("a") == 1.0
        assert topo.bulk_protection_weight("unknown") == 1.0

    def test_e8_default_topology(self):
        """Default E8 topology creates interior nodes."""
        topo = KernelTopology.e8_default()
        state = topo.get_state()

        assert state["n_kernels"] == 9  # genesis + 8 core
        assert state["n_interior"] > 0  # at least genesis is interior
        assert topo.is_interior("genesis")  # hub connected to all 8

    def test_e8_genesis_degree(self):
        """Genesis has degree 8 (connected to all core kernels)."""
        topo = KernelTopology.e8_default()
        assert topo._nodes["genesis"].degree == 8

    def test_e8_core_kernels_have_connections(self):
        """Core kernels should have 2+ connections (genesis + E8 neighbors)."""
        topo = KernelTopology.e8_default()
        for kid in [
            "perception",
            "memory",
            "action",
            "strategy",
            "ethics",
            "meta",
            "heart",
            "ocean",
        ]:
            assert topo._nodes[kid].degree >= 2, f"{kid} has too few connections"

    def test_get_state_serializable(self):
        topo = KernelTopology.e8_default()
        state = topo.get_state()
        assert isinstance(state, dict)
        assert "nodes" in state
        assert "genesis" in state["nodes"]


# ── EXP-002: Topology-aware TrajectoryBus ────────────────────────


class TestTopologyAwareBus:
    """TrajectoryBus.integrate() with topology weights."""

    def test_integrate_without_topology_unchanged(self):
        """Without topology, integration works as before."""
        own = [random_basin(BASIN_DIM) for _ in range(3)]
        msg = TrajectoryMessage(
            source_kernel_id="peer",
            trajectory=[random_basin(BASIN_DIM) for _ in range(3)],
            confidence=0.5,
        )
        result = TrajectoryBus.integrate(own, [msg])
        assert result.n_contributors == 2

    def test_integrate_with_topology_weights(self):
        """Interior kernel trajectories get higher weight."""
        topo = KernelTopology.e8_default()

        own = [to_simplex(np.ones(BASIN_DIM)) for _ in range(3)]
        target = [random_basin(BASIN_DIM) for _ in range(3)]

        # Message from interior kernel (genesis)
        msg_interior = TrajectoryMessage(
            source_kernel_id="genesis",
            trajectory=target,
            confidence=0.5,
        )
        result_interior = TrajectoryBus.integrate(
            own, [msg_interior], topology=topo, own_kernel_id="memory"
        )

        # Message from boundary kernel (same trajectory, same confidence)
        msg_boundary = TrajectoryMessage(
            source_kernel_id="ethics",  # may be boundary depending on topology
            trajectory=target,
            confidence=0.5,
        )
        result_boundary = TrajectoryBus.integrate(
            own, [msg_boundary], topology=topo, own_kernel_id="memory"
        )

        # Both should produce results
        assert result_interior.n_contributors == 2
        assert result_boundary.n_contributors == 2


# ── EXP-002: Topology-aware PeerQueryRouter ──────────────────────


class TestTopologyAwareRouter:
    """PeerQueryRouter.route() with topology weights."""

    def test_route_without_topology_unchanged(self):
        """Without topology, routing works as before."""
        router = PeerQueryRouter()
        query = PeerQuery(
            query_basin=random_basin(BASIN_DIM),
            requesting_kernel_id="perception",
        )
        basins = {
            "memory": random_basin(BASIN_DIM),
            "action": random_basin(BASIN_DIM),
        }
        result = router.route(query, basins)
        assert len(result.responses) > 0

    def test_route_with_topology_boosts_interior(self):
        """Interior kernels should get confidence boost."""
        topo = KernelTopology.e8_default()
        router = PeerQueryRouter(max_peers=8)

        query = PeerQuery(
            query_basin=random_basin(BASIN_DIM),
            requesting_kernel_id="perception",
        )
        # All kernels at same distance from query
        uniform = to_simplex(np.ones(BASIN_DIM))
        basins = {
            kid: uniform.copy()
            for kid in [
                "genesis",
                "memory",
                "action",
                "strategy",
                "ethics",
                "meta",
                "heart",
                "ocean",
            ]
        }

        result_no_topo = router.route(query, basins)
        result_with_topo = router.route(query, basins, topology=topo)

        # Genesis (interior) should have higher confidence with topology
        genesis_no = next(
            (
                r
                for r in result_no_topo.responses
                if r.responding_kernel_id == "genesis"
            ),
            None,
        )
        genesis_with = next(
            (
                r
                for r in result_with_topo.responses
                if r.responding_kernel_id == "genesis"
            ),
            None,
        )
        if genesis_no and genesis_with:
            assert genesis_with.confidence >= genesis_no.confidence
