"""Tests for pv-at --track, the per-track control acceptance runner (#74).

PLN's ask: "all controls have impact on sound... even the crushes should change
noticeably". The runner answers that per control, and the answers it gives are only
worth anything if the JUDGEMENT is right — so this file tests the judgement (three
outcomes, drift gating, restore-to-rest, the kill-switch guard) without needing
audio. The audio half can only be validated with monitors up.
"""
from __future__ import annotations

import importlib.util
import sys
from pathlib import Path

import pytest

TOOLS = Path(__file__).resolve().parents[2]
sys.path.insert(0, str(TOOLS))


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


from at import lens  # noqa: E402
at_main = _load("at_main", TOOLS / "at" / "__main__.py")


# --------------------------------------------------------------------------- #
# the three outcomes
# --------------------------------------------------------------------------- #

def _v(outcome: str, **kw):
    return at_main.ControlVerdict(cc=kw.pop("cc", 49), kind=kw.pop("kind", "filter"),
                                  lens=kw.pop("lens", "centroid"),
                                  orbits=kw.pop("orbits", [1]),
                                  outcome=outcome, **kw)


def test_moved_passes():
    assert _v("MOVED").passed


def test_no_impact_fails():
    """A control that does nothing is the whole point of the test."""
    assert not _v("NO_IMPACT").passed


def test_inconclusive_is_NOT_a_pass():
    """Two outcomes would force a lie. A swing that does not beat the pattern's own
    drift is genuinely unknown, and calling it a pass is how a green suite comes to
    mean nothing."""
    assert not _v("INCONCLUSIVE").passed


def test_no_signal_fails_and_is_distinct_from_no_impact():
    """"nothing was playing" and "the control is dead" have different fixes, and
    reporting them the same way sent a whole evening chasing SuperDirt once."""
    assert not _v("NO_SIGNAL").passed
    assert _v("NO_SIGNAL").outcome != _v("NO_IMPACT").outcome


def test_unmeasurable_passes_because_it_is_not_a_defect():
    """pan and orbit cannot be seen by a mono level/spectrum probe. Reporting them as
    broken every run would train PLN to ignore the report."""
    assert _v("UNMEASURABLE", kind="pan", lens="").passed


# --------------------------------------------------------------------------- #
# the drift gate — a swing only counts if it beats the signal's own variation
# --------------------------------------------------------------------------- #

def test_every_lens_has_a_floor():
    """2 * max(drift, 0) is zero, so a lens whose drift happens to be ~0 on a quiet
    capture would make ANY tiny swing look significant."""
    for _kw, _kind, name in lens.EFFECT_LENS:
        if name:
            assert at_main._lens_floor(name) > 0, name


def test_the_floor_is_scaled_to_the_lens_unit():
    """A 1 dB rms change and a 1 Hz centroid change are not comparable quantities;
    one floor for both would be meaningless in at least one of them."""
    assert at_main._lens_floor("centroid") > at_main._lens_floor("rms")
    assert at_main._lens_floor("low_ratio") < 1.0


def test_an_unknown_lens_gets_no_floor_rather_than_a_guessed_one():
    assert at_main._lens_floor("something-new") == 0.0


# --------------------------------------------------------------------------- #
# safety: what may and may not be sent
# --------------------------------------------------------------------------- #

def test_the_panic_gate_is_never_swept():
    """CC93 arms panic and silences every gPanic'd stream. Probing it would mute the
    rehearsal it is meant to validate."""
    assert 93 in lens.NEVER_PROBE
    ctl = lens.Control(cc=93, orbits=[1], kind="level", lens="rms")
    v = at_main._probe_control(ctl, seconds=1.0, port="0:0")
    assert v.outcome == "UNMEASURABLE"
    assert "kill switch" in v.detail


def test_an_unmeasurable_control_is_reported_without_sending_any_midi():
    """No port is touched, so this is safe to assert on with no rig at all."""
    ctl = lens.Control(cc=8, orbits=[1], kind="pan", lens="")
    v = at_main._probe_control(ctl, seconds=1.0, port="0:0")
    assert v.outcome == "UNMEASURABLE"


def test_a_control_with_no_orbit_is_inconclusive_not_probed():
    ctl = lens.Control(cc=60, orbits=[], kind="crush", lens="centroid")
    v = at_main._probe_control(ctl, seconds=1.0, port="0:0")
    assert v.outcome == "INCONCLUSIVE"


def test_the_ardour_fader_bank_can_never_reach_the_probe():
    """Excluded by construction in lens, and asserted again at the probe. CC77 down
    is total silence, so this is the one place belt AND braces is proportionate."""
    for cc in range(77, 85):
        assert cc in lens.FORBIDDEN_CC
        ctl = lens.Control(cc=cc, orbits=[1], kind="level", lens="rms")
        with pytest.raises(AssertionError):
            at_main._probe_control(ctl, seconds=1.0, port="0:0")


def test_lens_never_returns_a_forbidden_cc_from_a_real_track():
    from pvlint.core import load
    t = load(str(TOOLS.parent / "live/midi/nova/acid/gimme_acid.tidal"))
    assert not ({c.cc for c in lens.controls(t)} & lens.FORBIDDEN_CC)


# --------------------------------------------------------------------------- #
# rest values — the #48 footgun
# --------------------------------------------------------------------------- #

@pytest.mark.parametrize("cc", sorted(lens.DJ_FILTERS))
def test_a_dj_filter_rests_at_centre_and_probes_downward(cc):
    """djf 0.05 is a ~26 Hz low-pass, i.e. silence. Parking a DJF at 0 to establish a
    "baseline" measures silence and then calls the track broken."""
    c = lens.Control(cc=cc, orbits=[1], kind="filter", lens="centroid")
    assert c.bipolar
    assert c.rest == 64
    assert c.probes[0] == 10 and 118 in c.probes


def test_a_unipolar_control_rests_at_zero():
    c = lens.Control(cc=53, orbits=[1], kind="crush", lens="centroid")
    assert not c.bipolar
    assert c.rest == 0
    assert c.probes[0] == 127


# --------------------------------------------------------------------------- #
# the dry run — the half that IS checkable without monitors
# --------------------------------------------------------------------------- #

OPAL = TOOLS.parent / "armada" / "setlist_opal2026.txt"


def _set_tracks():
    return [ln.split("#")[0].strip() for ln in OPAL.read_text().splitlines()
            if ln.split("#")[0].strip()]


@pytest.mark.parametrize("rel", _set_tracks())
def test_the_probe_plan_builds_for_every_track_in_the_set(rel, capsys):
    """"Built but never run" is how the LED work shipped a colour ramp that was never
    called. The plan is falsifiable with no audio, so it gets asserted."""
    assert at_main.cmd_track_plan(TOOLS.parent / rel) == 0
    out = capsys.readouterr().out
    assert "no Ardour-owned CC in the plan" in out
    assert "bipolar (rest at CENTRE 64" in out


@pytest.mark.parametrize("rel", _set_tracks())
def test_every_track_in_the_set_finds_its_dj_filters_as_bipolar(rel):
    from pvlint.core import load
    controls = lens.controls(load(str(TOOLS.parent / rel)))
    bip = [c for c in controls if c.bipolar]
    assert bip, f"{rel}: no bipolar control found — gF1/2/3 went missing"
    assert all(c.rest == 64 for c in bip)


@pytest.mark.parametrize("rel", _set_tracks())
def test_every_track_binds_a_workable_number_of_controls(rel):
    """A track reporting 0 controls means the parse failed, not that the track is
    simple. Measured range across the set on 2026-07-29: 15-29."""
    from pvlint.core import load
    n = len(lens.controls(load(str(TOOLS.parent / rel))))
    assert 5 <= n <= 60, f"{rel}: {n} controls — the parse probably broke"


def test_the_plan_json_is_machine_readable(capsys):
    import json
    assert at_main.cmd_track_plan(
        TOOLS.parent / "live/midi/nova/acid/gimme_acid.tidal", as_json=True) == 0
    payload = json.loads(capsys.readouterr().out)
    assert payload["status"] == "plan"
    ccs = {c["cc"] for c in payload["controls"]}
    assert {49, 50, 51} <= ccs
    for c in payload["controls"]:
        assert c["cc"] not in lens.FORBIDDEN_CC
        assert c["rest"] in (0, 64)
