"""/loudness, /spectrum, /naming (#41) — engines + endpoints + naming drift guard.

Torch-free, real DSP on synth clips. The naming engine is a vendored copy of the
Foundry namer; test_naming_vendored_copy_has_not_drifted cross-checks it.

    cd armada/api && ~/.virtualenvs/fourier/bin/python -m pytest tests/test_extra.py -q
"""
import importlib.util
import io
from pathlib import Path

import numpy as np
import pytest
import soundfile as sf
from fastapi.testclient import TestClient

import app as APP
import config
from engines import feats, loudness, signal
from engines import naming as vendored_naming

client = TestClient(APP.app)
SR = 22050


def _wav_bytes(freqs=(220, 440), secs=2.0, sr=SR, amp=0.4):
    t = np.linspace(0, secs, int(sr * secs), endpoint=False)
    y = (amp * sum(np.sin(2 * np.pi * f * t) for f in freqs) / len(freqs)).astype("float32")
    buf = io.BytesIO()
    sf.write(buf, y, sr, format="WAV")
    return buf.getvalue()


def _tmp(freqs=(220, 440), secs=2.0, amp=0.4):
    p = Path(__import__("tempfile").mktemp(suffix=".wav"))
    p.write_bytes(_wav_bytes(freqs, secs, amp=amp))
    return str(p)


GW = lambda scope="*": {"X-Tenant": "hexa", "X-Scopes": "api:audio:*"}


# ── loudness ──────────────────────────────────────────────────────────────────
def test_loudness_engine_measures_lufs():
    r = loudness.loudness(_tmp(amp=0.3))
    assert r["lufs_integrated"] is not None and r["lufs_integrated"] < 0
    assert r["peak_dbfs"] <= 0.5 and r["true_peak_dbfs"] >= r["peak_dbfs"] - 1.0
    assert set(r["gain_to_target_db"]) == {"streaming", "club"}


def test_loudness_endpoint():
    r = client.post("/loudness", files={"file": ("c.wav", _wav_bytes(), "audio/wav")},
                    headers=GW("loudness"))
    assert r.status_code == 200 and "lufs_integrated" in r.json()


# ── spectrum (FFT-as-a-service) ─────────────────────────────────────────────────
def test_spectrum_shape_and_range():
    r = signal.spectrum(_tmp(), bands=32, frames=50, mel=True)
    assert r["bands"] == 32 and r["frames"] == 50
    assert len(r["spec"]) == 32 and len(r["spec"][0]) == 50
    flat = [v for row in r["spec"] for v in row]
    assert min(flat) >= 0.0 and max(flat) <= 1.0          # normalized 0..1
    assert len(r["band_hz"]) == 32


def test_spectrum_single_frame_is_avg_spectrum():
    r = signal.spectrum(_tmp(), bands=16, frames=1, mel=False)
    assert r["frames"] == 1 and len(r["spec"]) == 16 and len(r["spec"][0]) == 1


def test_spectrum_endpoint():
    r = client.post("/spectrum?bands=24&frames=40", files={"file": ("c.wav", _wav_bytes(), "audio/wav")},
                    headers=GW("spectrum"))
    assert r.status_code == 200
    body = r.json()
    assert body["bands"] == 24 and len(body["spec"]) == 24


# ── naming ──────────────────────────────────────────────────────────────────
def test_naming_endpoint_is_convention_clean():
    # a 60 Hz sine → role bass; name must lint clean (NN_role_character)
    r = client.post("/naming?index=3", files={"file": ("c.wav", _wav_bytes(freqs=(60,)), "audio/wav")},
                    headers=GW("naming"))
    assert r.status_code == 200
    body = r.json()
    assert body["role"] == "bass" and body["suggested_name"].startswith("03_")
    assert body["lint"] == []          # convention-compliant


def test_character_of_is_measured():
    bright = feats.character_of({"spectral_centroid": 5000, "spectral_flatness": 0.1, "zcr": 0.05})
    deep = feats.character_of({"spectral_centroid": 100, "spectral_flatness": 0.1, "zcr": 0.05})
    assert bright == "bright" and deep == "deep"


def _load_foundry_naming():
    p = Path(config.FOUNDRY) / "engine" / "naming.py"
    if not p.exists():
        return None
    spec = importlib.util.spec_from_file_location("_foundry_naming", p)
    mod = importlib.util.module_from_spec(spec)
    spec.loader.exec_module(mod)
    return mod


def test_naming_vendored_copy_has_not_drifted():
    canonical = _load_foundry_naming()
    if canonical is None:
        pytest.skip("Foundry not on disk — drift guard runs in-repo only")
    for idx, role, char in [(1, "bass", "deep"), (12, "melodic", "bright"), (3, "drums", None)]:
        assert (vendored_naming.suggest_name(idx, role, character=char)
                == canonical.suggest_name(idx, role, character=char))
    assert vendored_naming.lint("03_bass_deep") == canonical.lint("03_bass_deep")
    assert vendored_naming.ROLE_SYNONYMS == canonical.ROLE_SYNONYMS
