"""The grid is authored once — these tests are what make that true (#97).

Two jobs:

  1. The AUTHORED table is internally coherent (48 controls, every row 1-8,
     every orbit housed, no Tidal slot on an Ardour-owned control).
  2. Every DERIVED consumer still agrees with it, and the GENERATED artifacts are
     up to date.

Job 2 is the one that earns its keep. Before this, the grid was hardcoded in five
places, and on 2026-07-29 the Python copies moved while the HUD's did not — the
topbar drew d9 on A8 while the LED board and the .tidal files said A1. The
display contradicted the hardware under PLN's hands and nothing failed. Now a
stale copy fails here instead.
"""

from __future__ import annotations

import importlib.util
import json
import os
import sys

import pytest

TOOLS = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
sys.path.insert(0, TOOLS)

import lcxl_grid as grid  # noqa: E402


def _load(name: str, filename: str):
    """Import a hyphenated tool module (not a legal package name)."""
    spec = importlib.util.spec_from_file_location(name, os.path.join(TOOLS, filename))
    mod = importlib.util.module_from_spec(spec)
    spec.loader.exec_module(mod)
    return mod


# ------------------------------------------------------- 1. the authored table

def test_grid_is_self_consistent():
    """The invariants of the SURFACE. A failure means the table is wrong."""
    assert grid.problems() == []


def test_48_controls_no_cc_reused():
    assert len(grid.CC_TO_CELL) == 48
    assert len(set(grid.CC_TO_CELL)) == 48
    assert len(grid.CELL_TO_CC) == 48


def test_button_rows_are_one_row_of_eight_not_two():
    """E and F are non-contiguous on the hardware (41-44 then 57-60).

    Treating each half as its own row is precisely what put d6's second button in
    column 5 in the old map.
    """
    assert grid.ROW_CCS["E"] == [41, 42, 43, 44, 57, 58, 59, 60]
    assert grid.ROW_CCS["F"] == [73, 74, 75, 76, 89, 90, 91, 92]
    assert grid.CC_TO_CELL[58] == ("E", 6)      # d6's gate, column 6
    assert grid.CC_TO_CELL[90] == ("F", 6)      # d6's gate2, column 6


def test_column_n_is_orbit_n_for_d1_to_d8():
    """The whole convention in one assertion."""
    for orbit in range(1, 9):
        for role in ("level", "fx", "gate"):
            cc = grid.orbit_home(orbit)[role]
            assert grid.CC_TO_CELL[cc][1] == orbit, f"d{orbit} {role} is off-column"


def test_d1_to_d3_own_one_knob_and_one_button():
    """C1-3 and F1-3 are the FAMILY controls, so d1-d3 are one-slot orbits.

    This is the structural reason bombe_dj's two-button kick gesture is over
    budget by design, and why #94 had to comment it out rather than relocate it.
    """
    for orbit in (1, 2, 3):
        assert len(grid.slots(orbit, "knob")) == 1
        assert len(grid.slots(orbit, "button")) == 1
    for orbit in range(4, 9):
        assert len(grid.slots(orbit, "knob")) == 2
        assert len(grid.slots(orbit, "button")) == 2


def test_no_tidal_slot_lands_on_an_ardour_control():
    """Writing CC 77-84 or 13-16 from Tidal is the goes-to-silence footgun."""
    for orbit in range(1, 13):
        for kind in ("knob", "button"):
            for cc in grid.slots(orbit, kind):
                assert cc not in grid.ARDOUR_CCS, f"d{orbit} {kind} slot {cc}"


def test_family_controls_are_the_measured_ones():
    """gF1-3 on C1-3, gMute1-3 on F1-3, plus gPanic. Measured, not assumed."""
    assert {49, 50, 51} <= grid.FAMILY_CCS
    assert {73, 74, 75} <= grid.FAMILY_CCS
    assert grid.PANIC_CC in grid.FAMILY_CCS
    # ...and nothing per-orbit leaked in
    assert 41 not in grid.FAMILY_CCS      # d1's gate, since gMask retired
    assert 76 not in grid.FAMILY_CCS      # d4's gate2


# ------------------------------------------------- 2. consumers must not drift

def test_surface_columns_grid_matches():
    sc = _load("surface_columns", "surface-columns.py")
    label = {"A": "A", "B": "B", "C": "C", "D": "fader", "E": "btn1", "F": "btn2"}
    expect = {cc: (col, label[row]) for cc, (row, col) in grid.CC_TO_CELL.items()}
    assert sc.GRID == expect


def test_migrate_columns_sets_and_slots_match():
    mc = _load("migrate_columns", "migrate-columns.py")
    assert mc.KNOBS == grid.KNOB_CCS
    assert mc.BUTTONS == grid.BUTTON_CCS
    assert mc.ARDOUR == grid.ARDOUR_CCS
    for orbit in range(1, 9):
        assert mc.knob_slots(orbit) == grid.slots(orbit, "knob")
        assert mc.button_slots(orbit) == grid.slots(orbit, "button")


def test_migrate_columns_labels_every_control():
    """No control may print as a bare `ccNN` — that reads as "unknown" to PLN."""
    mc = _load("migrate_columns", "migrate-columns.py")
    for cc in grid.CC_TO_CELL:
        assert not mc.label(cc).startswith("cc"), cc
    assert mc.label(45) == "cc45"          # genuinely off-grid stays honest


def test_pvlint_pv008_uses_the_grid():
    from pvlint import rules
    assert rules.BUTTON_CCS == grid.BUTTON_CCS
    assert rules.FAMILY_CCS == grid.FAMILY_CCS


# ------------------------------------------------- 3. generated artifacts fresh

def test_generated_json_is_up_to_date():
    """Regenerating must be a no-op. If this fails, run --generate and commit."""
    assert grid.JSON_OUT.exists(), "run: python3 tools/lcxl_grid.py --generate"
    on_disk = json.loads(grid.JSON_OUT.read_text())
    assert on_disk == grid.as_dict(), (
        "tools/lcxl_grid.json is stale — run `python3 tools/lcxl_grid.py --generate`")


@pytest.mark.skipif(not grid.HUD_OUT.parent.is_dir(),
                    reason="HUD package not checked out next to this repo")
def test_generated_hud_js_is_up_to_date():
    """The copy that went stale last time. Now it is generated and checked."""
    text = grid.HUD_OUT.read_text()
    assert "DO NOT EDIT" in text
    # raw_decode stops at the end of the first JSON value and ignores the JS that
    # follows it, so this does not depend on brace-counting the whole file.
    payload, _end = json.JSONDecoder().raw_decode(
        text.split("export const GRID = ", 1)[1])
    assert payload == grid.as_dict(), (
        "the HUD's generated grid is stale — run "
        "`python3 tools/lcxl_grid.py --generate` and commit both repos")


@pytest.mark.skipif(not grid.HUD_OUT.parent.is_dir(), reason="HUD not present")
def test_hud_no_longer_hardcodes_the_convention():
    """render.js must IMPORT the map, not restate it — the actual #97 fix."""
    render = (grid.HUD_OUT.parent / "render.js").read_text()
    assert "lcxl-grid.generated" in render
    assert "const ORBIT_CONVENTION = {" not in render, (
        "render.js has re-grown its own hardcoded ORBIT_CONVENTION")
