"""Regression tests for the LCXL colour convention.

These exist because the LED work was previously validated only against a FAKE
aseqdump stream — written, wired into boot, and never once seen to light an LED.
A colour rule is a pure function, so it can and must be tested.
"""
import importlib.util, sys
from pathlib import Path

TOOLS = Path(__file__).resolve().parents[2]

def _load(name, path):
    spec = importlib.util.spec_from_file_location(name, path)
    m = importlib.util.module_from_spec(spec); sys.modules[name] = m
    spec.loader.exec_module(m); return m

leds = _load("lcxl_leds", TOOLS / "lcxl-leds.py")
mock = _load("mock_lcxl", TOOLS / "mock-lcxl.py")


def test_colour_byte_decodes_to_two_bits_each():
    assert mock.decode_colour(13) == (1, 0)    # dim red
    assert mock.decode_colour(15) == (3, 0)    # bright red
    assert mock.decode_colour(60) == (0, 3)    # bright green
    assert mock.decode_colour(63) == (3, 3)    # bright amber


def test_value_ramp_is_pln_six_steps_in_order():
    """dim red -> bright red -> dim amber -> bright amber -> dim green -> bright green."""
    seq = []
    for v in range(128):
        c = leds.value_ramp(v)
        if not seq or seq[-1] != c:
            seq.append(c)
    assert seq == [13, 15, 29, 31, 28, 60]


def test_value_ramp_is_monotonic_in_perceived_progress():
    """Never goes backwards: each step is >= the previous in (green, red) order."""
    def rank(c):
        r, g = mock.decode_colour(c)
        return (g, r)
    vals = [leds.value_ramp(v) for v in range(128)]
    ranks = [rank(c) for c in vals]
    assert ranks == sorted(ranks) or len(set(ranks)) == 6


def test_value_ramp_bottom_is_dim_not_off():
    """A mapped control at zero must still be visible, or you forget it exists."""
    assert leds.value_ramp(0) != leds.OFF


def test_dj_filter_is_bipolar_and_brightest_at_centre():
    """Centre 64 is bypass. Both ends are extremes — a monotonic ramp would lie."""
    assert leds.filter_colour(64) == 63
    assert leds.filter_colour(0) != leds.filter_colour(127)
    assert leds.filter_colour(64) not in (leds.filter_colour(0), leds.filter_colour(127))


def test_dj_filter_is_never_dark():
    """A filter you cannot see is a filter you forget is closed."""
    assert all(leds.filter_colour(v) != leds.OFF for v in range(128))


def test_sysex_roundtrip_through_the_mock_surface():
    frame = leds.sysex_pairs([(0x10, 13), (0x11, 60), (0x20, 15)])
    state = mock.parse_frames(frame)
    assert state == {0x10: 13, 0x11: 60, 0x20: 15}


def test_mock_never_reports_a_forbidden_fader_led():
    """CC77-84 are faders: no LEDs at all. They must not appear in a frame."""
    assert "D" not in leds.ROW_BASE
