"""Tests for feedpak contributor credits on the highway.

Covers the `_sanitize_authors` helper (unit) and the `song_info` WebSocket
frame carrying the manifest `authors` list end-to-end (integration). The
frontend uses a non-empty `authors` list to gate a credits overlay shown when
a song loads, so loose/archive/synthetic plays must surface `[]`.
"""

from __future__ import annotations

import importlib
import json
import sys

import pytest
import yaml
from fastapi.testclient import TestClient


# ── _sanitize_authors unit tests ────────────────────────────────────────────


@pytest.fixture()
def server_mod(monkeypatch, tmp_path):
    monkeypatch.setenv("CONFIG_DIR", str(tmp_path / "config"))
    monkeypatch.setenv("DLC_DIR", str(tmp_path / "dlc"))
    (tmp_path / "dlc").mkdir()
    sys.modules.pop("server", None)
    mod = importlib.import_module("server")
    yield mod
    conn = getattr(getattr(mod, "meta_db", None), "conn", None)
    if conn is not None:
        conn.close()


def test_sanitize_authors_valid(server_mod):
    out = server_mod._sanitize_authors(
        {
            "authors": [
                {"name": "Azure", "role": "charter", "email": "a@b.c", "url": "x"},
                {"name": "Bob Lee", "role": "editor"},
                {"name": "Solo"},
            ]
        }
    )
    # name + role only; email/url dropped; missing role → None.
    assert out == [
        {"name": "Azure", "role": "charter"},
        {"name": "Bob Lee", "role": "editor"},
        {"name": "Solo", "role": None},
    ]


def test_sanitize_authors_skips_malformed(server_mod):
    out = server_mod._sanitize_authors(
        {
            "authors": [
                {"name": ""},          # blank name → skipped
                {"name": "   "},       # whitespace name → skipped
                {"role": "mixer"},     # no name → skipped
                "not-a-dict",          # non-dict → skipped
                {"name": "  Kept  ", "role": "  arranger  "},  # trimmed
            ]
        }
    )
    assert out == [{"name": "Kept", "role": "arranger"}]


@pytest.mark.parametrize("manifest", [None, {}, {"authors": None}, {"authors": "x"}, "nope"])
def test_sanitize_authors_absent_or_nonlist(server_mod, manifest):
    assert server_mod._sanitize_authors(manifest) == []


# ── song_info WS integration ────────────────────────────────────────────────


def _write_sloppak(dlc_root, *, authors):
    pak = dlc_root / "authortest.sloppak"
    pak.mkdir()
    (pak / "arrangements").mkdir()
    (pak / "arrangements" / "lead.json").write_text(
        json.dumps(
            {
                "notes": [],
                "chords": [],
                "anchors": [],
                "handshapes": [],
                "templates": [],
                "beats": [{"time": 0.0, "measure": 1}],
                "sections": [{"name": "intro", "number": 1, "time": 0.0}],
            }
        )
    )
    manifest = {
        "title": "Author Test",
        "artist": "Tester",
        "album": "",
        "year": 2026,
        "duration": 10.0,
        "arrangements": [{"id": "lead", "name": "Lead", "file": "arrangements/lead.json"}],
        "stems": [],
    }
    if authors is not None:
        manifest["authors"] = authors
    (pak / "manifest.yaml").write_text(yaml.safe_dump(manifest, sort_keys=False))
    return pak


@pytest.fixture()
def make_client(tmp_path, monkeypatch):
    def _make():
        monkeypatch.setenv("CONFIG_DIR", str(tmp_path / "config"))
        monkeypatch.setenv("DLC_DIR", str(tmp_path / "dlc"))
        monkeypatch.setenv("FEEDBACK_SYNC_STARTUP", "1")
        sys.modules.pop("server", None)
        server = importlib.import_module("server")
        monkeypatch.setattr(server, "load_plugins", lambda *a, **kw: None)
        monkeypatch.setattr(server, "startup_scan", lambda: None)
        monkeypatch.setattr(server, "SLOPPAK_CACHE_DIR", tmp_path / "cache")
        return server

    (tmp_path / "dlc").mkdir()
    yield _make
    server = sys.modules.get("server")
    conn = getattr(getattr(server, "meta_db", None), "conn", None)
    if conn is not None:
        conn.close()


def _song_info(client, path):
    with client.websocket_connect(path) as ws:
        for _ in range(200):
            msg = ws.receive_json()
            if msg.get("error"):
                raise AssertionError(f"WS error frame: {msg}")
            if msg.get("type") == "song_info":
                return msg
            if msg.get("type") == "ready":
                break
    raise AssertionError("no song_info frame received")


def test_song_info_carries_authors(make_client):
    server = make_client()
    _write_sloppak(
        server._get_dlc_dir(),
        authors=[{"name": "Azure", "role": "charter", "email": "a@b.c"}],
    )
    with TestClient(server.app) as client:
        info = _song_info(client, "/ws/highway/authortest.sloppak?arrangement=0")
    assert info["authors"] == [{"name": "Azure", "role": "charter"}]


def test_song_info_authors_empty_when_absent(make_client):
    server = make_client()
    _write_sloppak(server._get_dlc_dir(), authors=None)
    with TestClient(server.app) as client:
        info = _song_info(client, "/ws/highway/authortest.sloppak?arrangement=0")
    assert info["authors"] == []
