"""Fleet color language — the generated tokens must be valid, distinguishable, and
greyscale-safe (parsers-over-copy: ontology in models → tokens, tested mechanically).
gen_tokens lives in tools/, so add it to the path like the ts-types generator."""
import re
import sys
from pathlib import Path

import models as M

ROOT = Path(__file__).resolve().parent.parent.parent.parent      # …/Sound/Tidal
sys.path.insert(0, str(ROOT / "tools"))
import gen_tokens as G  # noqa: E402

HEX = re.compile(r"^#[0-9a-f]{6}$")


def _rellum(hexs):
    """WCAG relative luminance, for greyscale-distinctness checks."""
    def lin(c):
        c /= 255
        return c / 12.92 if c <= 0.03928 else ((c + 0.055) / 1.055) ** 2.4
    r, g, b = (int(hexs[i:i + 2], 16) for i in (1, 3, 5))
    return 0.2126 * lin(r) + 0.7152 * lin(g) + 0.0722 * lin(b)


# ── UT: the OKLCH→hex math + shade ladder ────────────────────────────────────
def test_oklch_hex_is_valid_and_clamped():
    for L in (0.0, 0.5, 1.0):
        for H in (0, 90, 200, 359):
            assert HEX.match(G.oklch_hex(L, 0.15, H))


def test_shades_are_valid_and_monotonically_darker():
    for fam in M.SAMPLE_FAMILIES:
        base, sh = G.shades(fam)
        assert HEX.match(base)
        assert len(sh) == M.SHADE_COUNT
        assert all(HEX.match(s) for s in sh)
        lums = [_rellum(s) for s in sh]
        assert lums == sorted(lums, reverse=True), f"{fam.key} ladder not light→dark"
        # light and dark ends are clearly separated (greyscale-safe)
        assert lums[0] - lums[-1] > 0.15, f"{fam.key} ladder too flat in greyscale"


# ── ontology integrity ───────────────────────────────────────────────────────
def test_every_ontology_term_has_glyph_and_hex():
    for axis in (M.ROLE_FAMILIES, M.LIFECYCLE_TERMS, M.AGREE_TERMS):
        for t in axis:
            assert t.glyph, t.key
            assert HEX.match(t.color), (t.key, t.color)


def test_agree_terms_cover_the_enum():
    keys = {t.key for t in M.AGREE_TERMS}
    assert keys == {lv.value for lv in M.AgreeLevel}


def test_unparsed_is_not_painted_like_conflict():
    """Cardinal rule: a parser miss must never look like a data conflict (red)."""
    by = {t.key: t.color for t in M.AGREE_TERMS}
    assert by["unparsed"] != by["conflict"]
    assert by["empty"] != by["conflict"]


def test_sample_families_have_distinct_hues_and_match_cues():
    hues = [f.hue for f in M.SAMPLE_FAMILIES]
    assert len(set(hues)) == len(hues)          # no two families share a hue
    for f in M.SAMPLE_FAMILIES:
        assert f.match, f"{f.key} has no lexical cues"
        assert f.glyph


# ── DRY contract: emitted tokens validate against the model ──────────────────
def test_tokens_validate_against_model():
    from models import DesignTokens
    tok = G.build()
    dt = DesignTokens.model_validate(tok)
    assert len(dt.sample) == len(M.SAMPLE_FAMILIES)
    # every sample slot is a valid hex
    for f in tok["sample"]:
        assert HEX.match(f["base"]) and all(HEX.match(s) for s in f["shades"])


def test_sample_classifier_examples():
    """The canonical SSOT classifier lands obvious cases AND the token-aware ones."""
    c = M.classify_sample_family
    assert c("kick") == "kick"
    assert c("808bd") == "kick"
    assert c("hats") == "hat"
    assert c("meth_bass") == "bass"
    assert c("piano") == "keys"
    # token-aware / drum-machine / embedded (the coverage-improving cases)
    assert c("h2ogmhh") == "hat"          # DM suffix, gated by 'gm'
    assert c("vec1_snare") == "snare"     # embedded instrument token
    assert c("nujazz_bass125") == "bass"  # token wins over 'nujazz'
    assert c("nujazz_keys120") == "keys"
    assert c("fbass") == "bass"
    assert c("weird_dialogs") == "vox"
    assert c("ho") == "hat"


def test_classifier_does_not_overreach():
    """Honest unknowns stay unknown — the 'shil-OH' false-positive must not return."""
    assert M.classify_sample_family("shiloh") != "hat"
    for unknown in ("armora", "90s_matrix"):
        assert M.classify_sample_family(unknown) is None


def test_classifier_short_cue_no_prefix_match():
    """Cases we learned the hard way (validation): a 2-char cue must NOT prefix-match.
    `cp` (clap) must not swallow `cpluck`; `808` (bass) must not swallow `808hc`/`808mc`
    (which are 808 drum-machine perc, not bass) — those defer to audio (None) instead."""
    c = M.classify_sample_family
    assert c("cp") == "snare"          # the exact cue still resolves
    assert c("cpluck") != "snare"      # not 'cp'+luck; its real identity is keys/string
    assert c("cpluck") == "keys"
    assert c("808bd") == "kick"        # 808 + bd token still a kick
    assert c("808hc") is None          # was wrongly 'bass'; HC is ambiguous → CLAP decides
    assert c("808mc") is None


def test_classifier_refuses_kit_and_source_names():
    """A folder name that doesn't encode an instrument must NOT be force-classified.
    Kits (jazz:0=kick, :1=snare…), drum machines, and folders named after a SOURCE
    song carry no reliable family signal — only per-sample analysis can label them."""
    c = M.classify_sample_family
    for kit in ("jazz", "gretsch", "drum", "drumtraks", "techno"):   # multisample kits
        assert c(kit) is None, kit
    for src in ("wap", "take5", "the_revolution", "xplosive", "rampleS13"):  # sampled
        assert c(src) is None, src
    # but a name that DOES encode the instrument still resolves
    assert c("jungle_breaks") == "break"
    assert c("jungle_pads") == "pad"      # was wrongly 'break' via the genre token


def test_style_normalization():
    assert M.norm_style("nu-jazz") == M.norm_style("nujazz") == "nujazz"
    assert M.norm_style("breakbeat") == M.norm_style("breaks") == "breaks"
    assert M.norm_style("techno") == "techno"          # unknown passes through
