"""Unit tests for lens inference — which measurement can SEE which control.

Every positive case below is a REAL line from live/ (694 tracks, 5046 control bindings),
because a classifier tuned on invented examples classifies invented code. The cases that
matter most are the ones asserting a control is NOT measured by rms: rms is blind to a
filter and to a bitcrusher, so an rms-based "did anything change?" reports a confident NO
on an effect that is working perfectly. That single mistake would make the whole
per-track acceptance test worthless while looking green.
"""

from __future__ import annotations

import sys
from pathlib import Path

import pytest

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

from at import lens  # noqa: E402
from pvlint.core import Track  # noqa: E402


def ctl(src: str, cc: int):
    """The Control for `cc` in a one-orbit source snippet."""
    found = {c.cc: c for c in lens.controls(Track(path="<test>", text=src))}
    return found.get(cc)


# --------------------------------------------------------- the right lens

@pytest.mark.parametrize("src,cc,lens_name", [
    # spectrum — rms CANNOT see any of these
    ('d1 $ s "bd" # crushbus 44 (range 16 1.6 "^53")', 53, "centroid"),
    ('d1 $ s "bd" # djfbus 3 (range 0.05 0.95 "^51")', 51, "centroid"),
    ('d1 $ s "bd" # lpf (range 200 8000 "^13")', 13, "centroid"),
    ('d1 $ s "bd" # squizbus 7 (range 1 4 "^30")', 30, "centroid"),
    # the sub-octave family ADDS weight underneath; brightness is the wrong question
    ('d1 $ s "bd" # octersubbus 21 (range 0 1.2 "^32")', 32, "low_ratio"),
    ('d1 $ s "bd" # octerbus 6 (range 0 1 "^31")', 31, "high_ratio"),
    # level
    ('d1 $ gMute3 $ s "bd"\n  $ midiOn "^75" (const silence)', 75, "rms"),
    ('d1 $ s "bd" # gain (range 0 1.2 "^29")', 29, "rms"),
    # density — a ply that fires changes the onset rate and almost nothing else
    ('d1 $ midiOn "^59" (ply 4) $ s "bd*4"', 59, "onsets"),
    ('d1 $ midiOn "^89" (slice 4 "0 1 2 3") $ s "break"', 89, "onsets"),
    ('d1 $ s "bd" # chop (range 1 16 "^44")', 44, "onsets"),
    # time axis — a reverb extends ACTIVITY, which an aggregate cannot show
    ('d1 $ s "bd" # roombus 9 (range 0 0.6 "^33")', 33, "duty"),
    ('d1 $ s "bd" # lratebus 43 (range 0.6 6.7 "^33")', 33, "duty"),
    ('d1 $ s "bd" # legato (range 0.2 2 "^34")', 34, "duty"),
])
def test_the_lens_matches_the_effect_kind(src, cc, lens_name):
    c = ctl(src, cc)
    assert c is not None, f"CC{cc} not detected at all"
    assert c.lens == lens_name, f"CC{cc} classified {c.kind}/{c.lens}"


@pytest.mark.parametrize("src,cc", [
    ('d1 $ s "bd" # crushbus 44 (range 16 1.6 "^53")', 53),
    ('d1 $ s "bd" # djfbus 3 (range 0.05 0.95 "^51")', 51),
    ('d1 $ s "bd" # coarse (range 1 24 "^54")', 54),
])
def test_spectral_effects_are_NEVER_measured_by_level(src, cc):
    """The single most expensive possible mistake in this module.

    A crusher or a filter rearranges the spectrum and leaves the level alone. Measure it
    with rms and the suite reports "no impact" on a working effect — which is how a
    green test suite comes to mean nothing.
    """
    assert ctl(src, cc).lens != "rms"


# ------------------------------------------------------ multi-line reality

def test_a_wrapped_expression_is_still_classified():
    """PLN wraps his lines, and a line-by-line scan left a THIRD of the corpus
    unclassified. An unclassified control is one the AT silently skips — so the
    controls most likely to be broken would be the ones excluded from the test."""
    src = ('d1 $ midiOn "^57" (\n'
           '    (# crushbus 41 (range 16 4 "^54"))\n'
           '  ) $ s "bd*4"\n')
    assert ctl(src, 57).lens == "centroid"


def test_the_keyword_NEAREST_the_cc_wins_not_the_first_in_the_table():
    """`midiOn "^59" (ply 4) # crushbus ...` is about the ply; the crush has its own CC.
    Resolving by table order rather than position handed such lines to whichever effect
    happened to sit higher in the list."""
    src = 'd1 $ midiOn "^59" (ply 4) $ s "bd" # crushbus 1 (range 16 4 "^60")'
    assert ctl(src, 59).lens == "onsets"


def test_a_gated_pattern_swap_is_measured_on_onsets():
    """`midiOn "^41" (<| "k k k <k k*2>")` names no effect at all — it replaces the
    pattern. What changes is WHEN events happen."""
    src = 'd1 $ midiOn "^41" (<| "k k k <k [<~ k> k]>") $ s "bd"'
    c = ctl(src, 41)
    assert c.kind == "pattern" and c.lens == "onsets"


def test_a_bare_single_value_string_is_not_mistaken_for_a_rhythm():
    src = 'd1 $ s "bd" # n (range 0 8 "^15")'
    assert ctl(src, 15).kind != "pattern"


# ------------------------------------------------- helper -> orbit attribution

def test_a_let_helper_attributes_its_cc_to_every_orbit_that_applies_it():
    """you_my_sunshine's real shape: ONE gF3 helper owned FOUR orbits, which is why one
    knob left d5/d7/d9/d11 all inaudible. Attributing the CC only to the `let` line
    would have reported "CC51 affects no orbits" — exactly backwards."""
    src = ('let gF3 = (# djfbus 3 (range 0.05 0.95 "^51"))\n'
           'd5 $ gF3 $ s "voice"\n'
           'd7 $ gF3 $ s "hh"\n'
           'd9 $ gF3 $ s "pad"\n'
           'd11 $ s "chop"\n')
    c = ctl(src, 51)
    assert c.orbits == [5, 7, 9]
    assert 11 not in c.orbits


def test_a_helper_no_orbit_applies_is_still_reported_as_dead_wiring():
    src = ('let gF9 = (# djfbus 9 (range 0.05 0.95 "^55"))\n'
           'd1 $ s "bd"\n')
    c = ctl(src, 55)
    assert c is not None and c.orbits == []
    assert c not in lens.measurable(Track(path="t", text=src))


# ------------------------------------------------------------------ safety

@pytest.mark.parametrize("cc", list(range(77, 85)))
def test_the_ardour_fader_bank_can_never_be_returned(cc):
    """CC 77-84 are MIDI-learned to Ardour's track gains and CC77 is the GLOBAL gain --
    down is total silence. Excluded by construction, not by convention, so no future
    caller can opt back in by accident."""
    src = f'd1 $ s "bd" # gain "^{cc}"'
    assert ctl(src, cc) is None
    assert cc in lens.FORBIDDEN_CC


def test_no_measurable_control_is_ever_in_the_forbidden_bank():
    src = 'd1 $ s "bd" # gain "^77" # crushbus 1 (range 16 4 "^54")'
    ccs = {c.cc for c in lens.measurable(Track(path="t", text=src))}
    assert ccs == {54}


# ----------------------------------------------- rest values and probes

@pytest.mark.parametrize("cc", [49, 50, 51])
def test_a_dj_filter_rests_at_CENTRE_not_at_zero(cc):
    """djf 0.5 is bypass; djf 0.05 is a ~26 Hz low-pass, i.e. SILENCE. Parking a filter
    at 0 to establish a baseline would measure silence and call the track broken. This
    exact confusion cost an evening (#48) and left four orbits dark on stage."""
    c = ctl(f'd1 $ s "bd" # djfbus 1 (range 0.05 0.95 "^{cc}")', cc)
    assert c.rest == 64
    assert c.bipolar


def test_a_dj_filter_probes_BOTH_directions():
    """It is bipolar — one side closes a low-pass, the other opens a high-pass. Probing
    one side only is how you get a confident wrong answer about a working filter."""
    c = ctl('d1 $ s "bd" # djfbus 1 (range 0.05 0.95 "^49")', 49)
    assert min(c.probes) < 64 < max(c.probes)


def test_an_ordinary_control_rests_at_zero():
    c = ctl('d1 $ s "bd" # crushbus 1 (range 16 4 "^54")', 54)
    assert c.rest == 0 and not c.bipolar


# ---------------------------------------------------------- unmeasurable

def test_pan_is_reported_UNMEASURABLE_not_broken():
    """The capture sums to mono, so no available lens can see pan. Reporting it as "no
    impact" would be a false failure on a working control — and a suite that cries wolf
    is a suite you learn to ignore."""
    c = ctl('d1 $ s "bd" # pan (range 0 1 "^35")', 35)
    assert c.kind == "pan" and not c.measurable


def test_lens_value_reads_every_declared_lens():
    """Guards against a lens name in the table with no reader behind it — which would
    raise KeyError mid-run, on the rig, during a soundcheck."""
    from at.harness import Shape
    sh = Shape(orbit=1, peak_db=-6.0, rms_db=-20.0, centroid_hz=800.0,
               low_ratio=0.5, high_ratio=0.2, onsets_per_s=4.0, duty=0.7)
    for _kw, _kind, name in lens.EFFECT_LENS:
        if name:
            assert isinstance(lens.lens_value(sh, name), float)
            assert name in lens.LENS_UNITS


# --------------------------------------------------------------- BootTidal helpers
# These exist because the first --track plan on gimme_acid reported "0 bipolar, no
# CC 49/50/51 at all" — on a track that applies gF1/gF2/gF3 to SIX orbits. The
# helpers live in BootTidal.hs, so scanning only the .tidal file was blind to the
# three DJ filters and to every mute in the rig.

def test_boot_helpers_finds_the_three_dj_filters():
    h = lens.boot_helpers()
    assert h["gF1"] == {49: ("filter", "centroid", h["gF1"][49][2])}
    assert set(h["gF2"]) == {50}
    assert set(h["gF3"]) == {51}


def test_the_dj_filter_lens_is_borrowed_from_the_parameterised_helper():
    """`gF1 = gDJF "^49"` carries no effect keyword of its own — "gDJF" is not in any
    table. The lens has to come from gDJF's body (lpf + hpf), which sits on two
    separate lines."""
    assert lens.boot_helpers()["gF1"][49][1] == "centroid"


def test_boot_helpers_resolves_composition():
    """`gM<N> = gMute<N>` — one CC each, since gMask was retired 2026-07-29.

    This test used to assert {41, 73} because gM1 was `gMask . gMute1`. Keeping the
    composition-resolving assertion matters even though the composition is now
    trivial: the parser's job is to follow `.` chains, and a one-link chain still
    exercises that path. What changed is the ANSWER, not the requirement.
    """
    assert set(lens.boot_helpers()["gM1"]) == {73}
    assert set(lens.boot_helpers()["gM2"]) == {74}
    assert set(lens.boot_helpers()["gM3"]) == {75}


def test_the_mutes_are_measured_by_LEVEL():
    """gMute is `mask "f*16"` — it silences the stream, so the lens is rms.

    The density half of this test is gone with gMask (see the next test). The
    distinction it protected is still worth stating: `mask` appears in both
    helpers, so the KEYWORD cannot pick the lens — only what the mask pattern
    actually does can. [[feedback_right_lens_per_control]].
    """
    h = lens.boot_helpers()
    for n, cc in ((1, 73), (2, 74), (3, 75)):
        assert h[f"gMute{n}"][cc][0] == "mute"
        assert h[f"gMute{n}"][cc][1] == "rms"


def test_gMask_is_retired_and_claims_no_cc():
    """gMask = id since 2026-07-29 — PLN: "kill gMask! its unprevisible anyway".

    It is kept as a name (230 .tidal files call it) but owns nothing. The reason
    this needs a TEST and not just a comment: ^41 has been handed to d1's gate in
    the column-aligned button map, so a gMask that quietly reclaimed CC 41 would
    put two different jobs on one button — and that is the failure this whole
    remap exists to remove. Assert the slot is free.
    """
    h = lens.boot_helpers()
    assert not h.get("gMask"), f"gMask must own no CC, got {h.get('gMask')}"
    for name, ccs in h.items():
        assert 41 not in ccs, f"^41 must be free for d1's gate, but {name} claims it"


def test_all_three_mutes_are_classified_IDENTICALLY():
    """The bug this caught: gMute1/2/3 are three textually identical helpers and they
    came back mute, density, density. rms would have been the wrong lens on two of
    PLN's three mutes, so two working mutes would have reported NO_IMPACT."""
    h = lens.boot_helpers()
    kinds = {(h[f"gMute{n}"][cc][0], h[f"gMute{n}"][cc][1])
             for n, cc in ((1, 73), (2, 74), (3, 75))}
    assert len(kinds) == 1


@pytest.mark.parametrize("body,is_mute", [
    ("f*16", True),      # the regression: "16" contains a 1, read as a true value
    ("f*8", True),
    ("f", True),
    ("f f f f", True),
    ("f(4,32)", True),   # all-false euclid still masks everything out
    ("0*16", True),
    ("t!7 f", False),    # a density gate: seven pass, one drops
    ("t f", False),
    ("1 0 1 0", False),
    ("t*16", False),
])
def test_an_all_false_mask_is_a_mute_and_anything_else_is_density(body, is_mute):
    assert lens._is_mute_mask(f'mask "{body}"') is is_mute


def test_a_continuation_line_cannot_leak_into_the_wrong_helper():
    """`gM3` is followed by the Launchpad block, and appending every indented line
    to whatever was defined last made gM3 report CC 7 and CC 9 as mutes. A
    continuation has to LOOK like one (open with an operator)."""
    assert set(lens.boot_helpers()["gM3"]) == {75}
    assert 7 not in lens.boot_helpers()["gM3"]


def test_gpanic_is_found_but_marked_never_to_probe():
    """CC93 arms panic — it silences every gPanic'd stream. Reported, never swept:
    "probe every control" plus "one control is a kill switch" is how a self-test
    mutes a rehearsal and gets blamed on the rig."""
    assert 93 in lens.boot_helpers()["gPanic"]
    assert 93 in lens.NEVER_PROBE


def test_boot_helpers_never_returns_an_ardour_owned_cc():
    for name, ccs in lens.boot_helpers().items():
        assert not (set(ccs) & lens.FORBIDDEN_CC), name


def test_boot_helpers_degrades_to_empty_when_boottidal_is_missing(tmp_path):
    """Analysis must fall back to what the .tidal file itself says, not explode."""
    assert lens.boot_helpers(tmp_path / "nope.hs") == {}


def test_a_track_using_gF1_gets_cc49_attributed_to_its_orbits():
    """The integration that the unit tests above cannot prove: the boot helper has to
    reach `controls()` and land on the ORBITS that apply it."""
    from pvlint.core import load
    t = load(str(TOOLS.parent / "live/midi/nova/acid/gimme_acid.tidal"))
    by_cc = {c.cc: c for c in lens.controls(t)}
    assert 49 in by_cc and by_cc[49].orbits, "gF1's CC49 reached no orbit"
    assert by_cc[49].kind == "filter"
    assert by_cc[49].bipolar and by_cc[49].rest == 64
    assert by_cc[49].probes[0] == 10
    # and the mutes land on their orbits too
    assert by_cc[75].orbits and by_cc[75].lens == "rms"


def test_a_local_let_shadows_the_boot_helper_of_the_same_name(tmp_path):
    """Tidal semantics: a track's own `let gMute = ...` wins. Several tracks redefine
    these, and reporting the boot version would describe code that is not running."""
    from pvlint.core import Track as T
    t = T(path="x.tidal", text='let gMute1 = (midiOn "^60" (ply 4))\n\n'
                               'd1 $ gMute1 $ s "bd"\n')
    by_cc = {c.cc: c for c in lens.controls(t)}
    assert 60 in by_cc
    assert by_cc[60].orbits == [1]


def test_every_opal_track_resolves_all_three_dj_filters():
    """The guard that would have caught the original miss. Every track in the set
    applies gF1/gF2/gF3 somewhere, so all three must appear with orbits attached."""
    import re as _re
    from pvlint.core import load
    setlist = TOOLS.parent / "armada" / "setlist_opal2026.txt"
    rows = [ln.split("#")[0].strip() for ln in setlist.read_text().splitlines()]
    tracks = [r for r in rows if r]
    assert len(tracks) == 13
    for rel in tracks:
        t = load(str(TOOLS.parent / rel))
        by_cc = {c.cc: c for c in lens.controls(t)}
        text = (TOOLS.parent / rel).read_text()
        for cc, helper in ((49, "gF1"), (50, "gF2"), (51, "gF3")):
            if _re.search(rf"\b{helper}\b", text):
                assert cc in by_cc, f"{rel}: {helper} applied but CC{cc} not found"
                assert by_cc[cc].orbits, f"{rel}: CC{cc} attributed to no orbit"


# ------------------------------------------------------- corpus coverage

def test_the_classifier_covers_the_real_corpus():
    """A coverage GUARD, not a vanity metric: it fails if a future edit regresses the
    multi-line scan that took unknown from 33% to 3.3%. Numbers measured 2026-07-29
    over 694 tracks / 5046 bindings."""
    import glob

    from pvlint.core import load
    total = unknown = 0
    for f in sorted(glob.glob(str(TOOLS.parent / "live/**/*.tidal"), recursive=True)):
        for c in lens.controls(load(f)):
            total += 1
            unknown += c.kind == "unknown"
    assert total > 4000, f"corpus shrank unexpectedly ({total} bindings)"
    assert unknown / total < 0.08, f"{unknown}/{total} unclassified — the scan regressed"
