"""Walking-skeleton + API smoke tests — no CLAP/torch required.

Cover the seams that must hold under the PLATFORM contract (SRE update): routes
are served at root (the gateway owns the /audio/v1 prefix), and auth is the
gateway-injected X-Tenant / X-Scopes identity (with a local-dev bearer fallback).
The emotion inference itself is monkeypatched — here we assert routing, auth
gating, the cache, and the deploy seams (/separate 503, /metrics). Env/DB/path
setup is in conftest.py.

    cd armada/api && ~/.virtualenvs/douanier/bin/python -m pytest tests/ -q
"""
from fastapi.testclient import TestClient

import app as APP
from engines import ears

client = TestClient(APP.app)

# the local-dev fallback: a bearer matching DOUANIER_DEV_TOKEN (conftest) → wildcard.
DEV = {"Authorization": "Bearer test-secret"}
# how the platform gateway actually calls us: injected identity headers. A broad
# api:audio:* grant authorizes every audio route (scopes are path-derived now).
GW = {"X-Tenant": "hexa", "X-Scopes": "api:audio:*"}


def test_healthz_ok():
    r = client.get("/healthz")
    assert r.status_code == 200
    body = r.json()
    assert body["service"] == "douanier" and body["api"] == "audio"
    assert "emotion" in body["engines"] and "separate" in body["engines"]


def test_me_via_gateway_headers():
    r = client.get("/me", headers={"X-Tenant": "hexa", "X-Scopes": "api:audio:* admin:*"})
    assert r.status_code == 200
    body = r.json()
    assert body["account"] == "hexa"
    assert set(body["scopes"]) == {"api:audio:*", "admin:*"}


def test_me_via_dev_bearer_fallback():
    r = client.get("/me", headers=DEV)
    assert r.status_code == 200 and r.json()["account"] == "dev"


def test_me_requires_identity():
    assert client.get("/me").status_code == 401


def test_emotion_requires_identity():
    r = client.post("/analyze/emotion", files={"file": ("c.wav", b"RIFF....", "audio/wav")})
    assert r.status_code == 401


def test_emotion_rejects_bad_bearer():
    r = client.post("/analyze/emotion",
                    files={"file": ("c.wav", b"RIFF....", "audio/wav")},
                    headers={"Authorization": "Bearer nope"})
    assert r.status_code == 401


def test_emotion_scope_enforced():
    # an authenticated tenant whose grant doesn't cover this route is 403, not 401.
    # api:audio:features authorizes /features but NOT /analyze/emotion.
    r = client.post("/analyze/emotion",
                    files={"file": ("c.wav", b"RIFF....", "audio/wav")},
                    headers={"X-Tenant": "hexa", "X-Scopes": "api:audio:features"})
    assert r.status_code == 403


def test_emotion_happy_path_and_cache(monkeypatch):
    # Stub the heavy engine call — we're testing the route/contract + cache, not the model.
    fake = {"valence": 0.42, "arousal": -0.10, "top": [["warm", 0.5]], "confidence": 0.2}
    calls = {"n": 0}

    def _fake(_p):
        calls["n"] += 1
        return fake
    monkeypatch.setattr(ears, "emotion_of", _fake)

    payload = b"UNIQUE-cache-probe-bytes-9f3a"   # undecodable → raw_ content id
    files = {"file": ("c.wav", payload, "audio/wav")}

    r1 = client.post("/analyze/emotion", files=files, headers=GW)
    assert r1.status_code == 200
    assert r1.headers["X-Douanier-Cache"] == "miss"
    assert r1.json()["emotion"]["valence"] == 0.42
    assert r1.json()["content_id"].startswith(("cid_", "raw_"))

    r2 = client.post("/analyze/emotion", files=files, headers=GW)
    assert r2.headers["X-Douanier-Cache"] == "hit"
    assert calls["n"] == 1            # engine ran ONCE; 2nd served from cache


def test_emotion_too_large(monkeypatch):
    monkeypatch.setattr(APP.config, "MAX_UPLOAD_MB", 0)  # everything is too large
    r = client.post("/analyze/emotion",
                    files={"file": ("c.wav", b"x" * 1024, "audio/wav")}, headers=GW)
    assert r.status_code == 413


def _wav_bytes(freqs=(220, 277, 330), secs=1.5):
    import io
    import numpy as np
    import soundfile as sf
    t = np.linspace(0, secs, int(22050 * secs), endpoint=False)
    y = (0.3 * sum(np.sin(2 * np.pi * f * t) for f in freqs) / len(freqs)).astype("float32")
    buf = io.BytesIO()
    sf.write(buf, y, 22050, format="WAV")
    return buf.getvalue()


def test_features_endpoint_and_cache():
    files = {"file": ("c.wav", _wav_bytes(), "audio/wav")}
    r1 = client.post("/features", files=files, headers=GW)
    assert r1.status_code == 200
    assert r1.headers["X-Douanier-Cache"] == "miss"
    body = r1.json()
    assert "features" in body and body["engine"] == "light"
    assert "spectral_centroid" in body["features"]
    r2 = client.post("/features", files={"file": ("c.wav", _wav_bytes(), "audio/wav")},
                     headers=GW)
    assert r2.headers["X-Douanier-Cache"] == "hit"


def test_features_scope_enforced():
    # api:audio:analyze:emotion authorizes /analyze/emotion but NOT /features.
    r = client.post("/features", files={"file": ("c.wav", b"RIFF....", "audio/wav")},
                    headers={"X-Tenant": "hexa", "X-Scopes": "api:audio:analyze:emotion"})
    assert r.status_code == 403


def test_samples_endpoint_measures_role():
    files = {"file": ("c.wav", _wav_bytes(freqs=(60,)), "audio/wav")}
    r = client.post("/analyze/samples?name=lead", files=files, headers=GW)
    assert r.status_code == 200
    body = r.json()
    # 60 Hz sine → bass by measurement, despite the "lead" name hint
    assert body["role"] == "bass" and body["engine"] == "light"


def test_separate_is_503_until_gpu():
    r = client.post("/separate", headers=GW)
    assert r.status_code == 503
    assert r.json()["detail"]["error"] == "compute_unavailable"


def test_metrics_exposes_prometheus_text():
    r = client.get("/metrics")
    assert r.status_code == 200
    assert "douanier_up 1" in r.text
    assert "douanier_requests_total" in r.text


def test_openapi_advertises_public_root_path():
    # root_path=/audio/v1 → clients see the real public paths in the spec.
    spec = client.get("/openapi.json").json()
    assert spec.get("servers") == [{"url": "/audio/v1"}]
