"""Tests for snake/serpentine ring extension of configure_sfr_intercube_ring.

Verifies the multi-row snake ring SFR wiring used for ADR-0060 §5.5
prefill Ring KV at C=8 on a 2×4 sub-mesh of the 4×4 CUBE mesh.

The snake path for ``submesh_shape=(2, 4), origin=(0, 0)`` is::

    (0,0)→(0,1)→(0,2)→(0,3)→(1,3)→(1,2)→(1,1)→(1,0)→wrap to (0,0)

In cube indices on a mesh_w=4 grid: ``[0, 1, 2, 3, 7, 6, 5, 4]``.

Every consecutive pair (including the wrap) is a 1-hop CUBE NOC neighbour.
The kernel sees a 1D logical E/W ring; the snake is invisible to it.
"""
from __future__ import annotations

from pathlib import Path

import pytest

from kernbench.ccl.install import load_ccl_config, resolve_algorithm_config
from kernbench.ccl.sfr_config import configure_sfr_intercube_ring
from kernbench.sim_engine.engine import GraphEngine
from kernbench.topology.builder import resolve_topology

TOPOLOGY_PATH = Path(__file__).parent.parent / "topology.yaml"

N_CUBES = 16          # 4×4 SIP CUBE mesh
PES_PER_CUBE = 8
MESH_W = 4

# Canonical snake path for (2,4) sub-mesh at origin (0,0).
SNAKE_2X4_O00 = [0, 1, 2, 3, 7, 6, 5, 4]

# Canonical snake path for (2,4) sub-mesh at origin (1,0) (rows 1-2).
SNAKE_2X4_O10 = [4, 5, 6, 7, 11, 10, 9, 8]


def _engine_and_spec():
    topo = resolve_topology(str(TOPOLOGY_PATH))
    engine = GraphEngine(topo.topology_obj, enable_data=True)
    return engine, topo.topology_obj.spec


def _merged_cfg():
    cfg = load_ccl_config()
    return resolve_algorithm_config(cfg, name="lrab_hierarchical_allreduce")


def _qp(engine, sip: int, cube: int, pe: int):
    return engine._components[f"sip{sip}.cube{cube}.pe{pe}.pe_ipcq"].queue_pairs


def _row_col(cube: int) -> tuple[int, int]:
    return cube // MESH_W, cube % MESH_W


def _l1(c1: int, c2: int) -> int:
    r1, col1 = _row_col(c1)
    r2, col2 = _row_col(c2)
    return abs(r1 - r2) + abs(col1 - col2)


class TestSnakeRing2x4Origin00:
    """Snake ring through the top 2×4 sub-mesh of the 4×4 SIP."""

    def test_snake_ring_2x4_world_size(self):
        """All PEs on all CUBEs of all SIPs still enumerated (snake only
        restricts which CUBEs have ring links, not the world)."""
        engine, spec = _engine_and_spec()
        cfg = _merged_cfg()
        plan = configure_sfr_intercube_ring(
            engine, spec, cfg, submesh_shape=(2, 4),
        )
        n_sips = int(spec["system"]["sips"]["count"])
        assert plan["world_size"] == n_sips * N_CUBES * PES_PER_CUBE
        assert len(plan["rank_to_pe"]) == plan["world_size"]

    def test_snake_ring_2x4_path_corners_E(self):
        """The four 'interesting' E hops on the snake path:

          cube0.E → cube1   (start of row 0)
          cube3.E → cube7   (row-bridge: top-right corner down)
          cube7.E → cube6   (row 1, going leftward)
          cube4.E → cube0   (wrap: bottom-left to top-left)
        """
        engine, spec = _engine_and_spec()
        cfg = _merged_cfg()
        configure_sfr_intercube_ring(
            engine, spec, cfg, submesh_shape=(2, 4),
        )
        assert _qp(engine, 0, 0, 0)["E"]["peer"].cube == 1
        assert _qp(engine, 0, 3, 0)["E"]["peer"].cube == 7
        assert _qp(engine, 0, 7, 0)["E"]["peer"].cube == 6
        assert _qp(engine, 0, 4, 0)["E"]["peer"].cube == 0

    def test_snake_ring_2x4_path_corners_W(self):
        """Symmetric W hops (W is reverse of E along the snake):

          cube0.W → cube4   (wrap reverse)
          cube7.W → cube3   (row-bridge reverse: up)
          cube6.W → cube7   (row 1 reverse: rightward)
          cube4.W → cube5   (row-1 leftmost, W goes right within row 1)

        Note: under the snake path [0,1,2,3,7,6,5,4], W(cube_i) is the
        predecessor on that path. So W(4)=5 (previous on path).
        """
        engine, spec = _engine_and_spec()
        cfg = _merged_cfg()
        configure_sfr_intercube_ring(
            engine, spec, cfg, submesh_shape=(2, 4),
        )
        assert _qp(engine, 0, 0, 0)["W"]["peer"].cube == 4
        assert _qp(engine, 0, 7, 0)["W"]["peer"].cube == 3
        assert _qp(engine, 0, 6, 0)["W"]["peer"].cube == 7
        assert _qp(engine, 0, 4, 0)["W"]["peer"].cube == 5

    def test_snake_ring_2x4_interior(self):
        """Interior hops along each row (not at row-bridge or wrap)."""
        engine, spec = _engine_and_spec()
        cfg = _merged_cfg()
        configure_sfr_intercube_ring(
            engine, spec, cfg, submesh_shape=(2, 4),
        )
        # Row 0 interior: cube1 between cube0 and cube2.
        assert _qp(engine, 0, 1, 0)["E"]["peer"].cube == 2
        assert _qp(engine, 0, 1, 0)["W"]["peer"].cube == 0
        # Row 1 interior: cube5 between cube6 and cube4 along the snake.
        assert _qp(engine, 0, 5, 0)["E"]["peer"].cube == 4
        assert _qp(engine, 0, 5, 0)["W"]["peer"].cube == 6

    def test_snake_ring_2x4_all_hops_are_1_hop(self):
        """Every E hop on the snake is L1-distance 1 in the cube mesh.

        Defensive — holds by construction (snake = Hamiltonian cycle
        on the 2×4 grid graph), but explicit guards against future
        path-builder regressions.
        """
        engine, spec = _engine_and_spec()
        cfg = _merged_cfg()
        configure_sfr_intercube_ring(
            engine, spec, cfg, submesh_shape=(2, 4),
        )
        for cube in SNAKE_2X4_O00:
            qp = _qp(engine, 0, cube, 0)
            e_peer = qp["E"]["peer"].cube
            w_peer = qp["W"]["peer"].cube
            assert _l1(cube, e_peer) == 1, (
                f"snake E hop cube{cube}→cube{e_peer} is not 1-hop"
            )
            assert _l1(cube, w_peer) == 1, (
                f"snake W hop cube{cube}→cube{w_peer} is not 1-hop"
            )

    def test_snake_ring_2x4_off_path_cubes_have_no_ring_links(self):
        """CUBEs not on the snake path (rows 2-3, i.e. cubes 8..15)
        get no E/W ring entries — consistent with the current
        ``if cube < ring_size`` behaviour."""
        engine, spec = _engine_and_spec()
        cfg = _merged_cfg()
        configure_sfr_intercube_ring(
            engine, spec, cfg, submesh_shape=(2, 4),
        )
        for cube in range(8, 16):
            qp = _qp(engine, 0, cube, 0)
            assert "E" not in qp, (
                f"sip0.cube{cube}.pe0 has E but is off the snake path"
            )
            assert "W" not in qp

    def test_snake_ring_2x4_intra_namespace_unchanged(self):
        """Snake doesn't disturb the intra-cube 2×4 PE grid wiring
        (intra_N/S/E/W) — it only changes cube-level E/W."""
        from kernbench.ccl.sfr_config import _intra_cube_neighbors

        engine, spec = _engine_and_spec()
        cfg = _merged_cfg()
        configure_sfr_intercube_ring(
            engine, spec, cfg, submesh_shape=(2, 4),
        )
        # Spot-check a few cubes (including off-path) and all 8 PEs.
        for cube in (0, 3, 4, 7, 10, 15):
            for pe in range(PES_PER_CUBE):
                qp = _qp(engine, 0, cube, pe)
                expected = _intra_cube_neighbors(pe)
                for d, expected_pe in expected.items():
                    assert d in qp, f"cube{cube}.pe{pe} missing {d}"
                    assert qp[d]["peer"].pe == expected_pe
                    assert qp[d]["peer"].cube == cube


class TestSnakeRing2x4OriginShift:
    """Snake ring through rows 1-2 (origin=(1, 0)) — proves the
    origin parameter shifts the sub-mesh."""

    def test_snake_ring_origin_shift_path(self):
        """For origin=(1, 0), snake path is [4,5,6,7,11,10,9,8]:

          row 1 left→right:  cube4 → cube5 → cube6 → cube7
                                                       ↓
          row 2 right→left:  cube11 → cube10 → cube9 → cube8
                                                          ↺ wraps to cube4
        """
        engine, spec = _engine_and_spec()
        cfg = _merged_cfg()
        configure_sfr_intercube_ring(
            engine, spec, cfg, submesh_shape=(2, 4), submesh_origin=(1, 0),
        )
        # Path corners.
        assert _qp(engine, 0, 4, 0)["E"]["peer"].cube == 5
        assert _qp(engine, 0, 7, 0)["E"]["peer"].cube == 11
        assert _qp(engine, 0, 11, 0)["E"]["peer"].cube == 10
        assert _qp(engine, 0, 8, 0)["E"]["peer"].cube == 4  # wrap

    def test_snake_ring_origin_shift_off_path_no_links(self):
        """For origin=(1, 0), cubes 0..3 (row 0) and 12..15 (row 3)
        get no ring links."""
        engine, spec = _engine_and_spec()
        cfg = _merged_cfg()
        configure_sfr_intercube_ring(
            engine, spec, cfg, submesh_shape=(2, 4), submesh_origin=(1, 0),
        )
        for cube in list(range(0, 4)) + list(range(12, 16)):
            qp = _qp(engine, 0, cube, 0)
            assert "E" not in qp
            assert "W" not in qp


class TestSnakeRingBackwardCompat:
    """The existing 1D-row API (no submesh_shape) must behave identically
    to before the snake extension lands."""

    def test_snake_ring_backward_compat_1d_default(self):
        """ring_size=4 (single-row) without submesh_shape — identical
        behaviour to today's 1D-row ring: cube0..3 with E/W wrap;
        cubes 4..15 have no ring links."""
        engine, spec = _engine_and_spec()
        cfg = _merged_cfg()
        configure_sfr_intercube_ring(
            engine, spec, cfg, ring_size=4,
        )
        # Row-0 1D ring with wrap.
        assert _qp(engine, 0, 0, 0)["E"]["peer"].cube == 1
        assert _qp(engine, 0, 0, 0)["W"]["peer"].cube == 3  # wrap
        assert _qp(engine, 0, 3, 0)["E"]["peer"].cube == 0  # wrap
        # Off-row cubes get no ring links.
        for cube in range(4, 16):
            qp = _qp(engine, 0, cube, 0)
            assert "E" not in qp
            assert "W" not in qp


class TestSnakeRingValidation:
    """Input validation of the submesh_shape / ring_size combinations."""

    def test_snake_ring_invalid_submesh_overflow(self):
        """submesh_shape that doesn't fit the cube mesh is rejected."""
        engine, spec = _engine_and_spec()
        cfg = _merged_cfg()
        with pytest.raises(ValueError, match=r"sub.?mesh"):
            configure_sfr_intercube_ring(
                engine, spec, cfg, submesh_shape=(2, 5),  # col overflow
            )

    def test_snake_ring_ring_size_mismatch(self):
        """submesh_shape and ring_size disagree → ValueError."""
        engine, spec = _engine_and_spec()
        cfg = _merged_cfg()
        with pytest.raises(ValueError, match=r"ring_size"):
            configure_sfr_intercube_ring(
                engine, spec, cfg,
                submesh_shape=(2, 4), ring_size=4,
            )
