"""Unit tests for the voice library catalog shaper + ID validation.

DB- and network-touching paths (fetch_catalog, install_voice) need a
real environment, so we test only the pure transformations here:

- Voice ID regex accepts valid piper IDs and rejects path-traversal attempts.
- shape_catalog_for_ui projects an HF-shaped catalog dict into the UI list.
- _resolve_file_urls picks the right .onnx and .onnx.json paths.
"""
from __future__ import annotations

import pytest

from fabledassistant.services import voice_library as lib


def test_voice_id_accepts_real_piper_ids():
    for vid in [
        "en_US-amy-medium",
        "en_US-ryan-medium",
        "en_GB-alan-low",
        "fr_FR-siwis-medium",
        "de_DE-thorsten-high",
        "es_AR-daniela-high",
        "zh_CN-huayan-medium",
        "ar_JO-kareem-low",
        "ca_ES-upc_ona-x_low",
    ]:
        assert lib._is_valid_voice_id(vid), f"should accept {vid!r}"


def test_voice_id_rejects_path_traversal_and_garbage():
    # Regex purpose is path-traversal + structurally-malformed prevention,
    # NOT enforcement of the HF catalog's lowercase-language convention.
    # Uppercase variants like `EN_US-amy-medium` pass the regex but would
    # 404 at install time against HF — harmless, no need to over-tighten.
    for vid in [
        "../../etc/passwd",
        "en_US-amy-medium/../something",
        "../en_US-amy-medium",
        "",
        "en_US-amy",  # missing quality
        "amy-medium",  # missing language
        "en_US--medium",  # empty dataset
        "en_US-amy-large",  # invalid quality
        ".onnx",
        "en_US-amy-medium.onnx",  # has extension; we want the bare id
    ]:
        assert not lib._is_valid_voice_id(vid), f"should reject {vid!r}"


def test_shape_catalog_projects_expected_fields(monkeypatch, tmp_path):
    """Catalog dict → list of UI cards, with install state from disk scan."""
    # Redirect the install-scan paths to a tmp dir; mark en_US-amy-medium
    # as installed under /data so we can verify the field flips.
    user_dir = tmp_path / "data_voices"
    bundled_dir = tmp_path / "opt_voices"
    user_dir.mkdir()
    bundled_dir.mkdir()
    (user_dir / "en_US-amy-medium.onnx").write_text("model")
    (user_dir / "en_US-amy-medium.onnx.json").write_text("{}")

    monkeypatch.setattr(lib, "_USER_VOICES_DIR", user_dir)
    monkeypatch.setattr(lib, "_BUNDLED_VOICES_DIR", bundled_dir)

    catalog = {
        "en_US-amy-medium": {
            "name": "amy",
            "language": {
                "code": "en_US",
                "name_native": "English",
                "country_english": "United States",
            },
            "quality": "medium",
            "num_speakers": 1,
            "files": {
                "en/en_US/amy/medium/en_US-amy-medium.onnx": {
                    "size_bytes": 63_500_000,
                },
                "en/en_US/amy/medium/en_US-amy-medium.onnx.json": {
                    "size_bytes": 4_300,
                },
            },
        },
        "fr_FR-siwis-medium": {
            "name": "siwis",
            "language": {
                "code": "fr_FR",
                "name_native": "Français",
                "country_english": "France",
            },
            "quality": "medium",
            "num_speakers": 1,
            "files": {
                "fr/fr_FR/siwis/medium/fr_FR-siwis-medium.onnx": {
                    "size_bytes": 60_000_000,
                },
                "fr/fr_FR/siwis/medium/fr_FR-siwis-medium.onnx.json": {
                    "size_bytes": 3_900,
                },
            },
        },
    }
    out = lib.shape_catalog_for_ui(catalog)
    assert len(out) == 2
    by_id = {v["id"]: v for v in out}

    amy = by_id["en_US-amy-medium"]
    assert amy["name"] == "amy"
    assert amy["language_code"] == "en_US"
    assert amy["quality"] == "medium"
    assert amy["size_bytes"] == 63_500_000 + 4_300
    assert amy["installed"] is True
    assert amy["installed_source"] == "user"

    siwis = by_id["fr_FR-siwis-medium"]
    assert siwis["language_code"] == "fr_FR"
    assert siwis["installed"] is False
    assert siwis["installed_source"] is None

    # Sorted by language_code, then name — en_US before fr_FR.
    assert out[0]["id"] == "en_US-amy-medium"
    assert out[1]["id"] == "fr_FR-siwis-medium"


def test_resolve_file_urls_picks_onnx_and_sidecar():
    catalog = {
        "en_US-amy-medium": {
            "files": {
                "en/en_US/amy/medium/en_US-amy-medium.onnx": {"size_bytes": 1},
                "en/en_US/amy/medium/en_US-amy-medium.onnx.json": {"size_bytes": 2},
            },
        }
    }
    urls = lib._resolve_file_urls(catalog, "en_US-amy-medium")
    assert urls["onnx"].endswith("en_US-amy-medium.onnx")
    assert urls["onnx.json"].endswith("en_US-amy-medium.onnx.json")
    assert urls["onnx"].startswith("https://huggingface.co/rhasspy/piper-voices/resolve/main/")


def test_resolve_file_urls_raises_when_voice_missing():
    with pytest.raises(KeyError):
        lib._resolve_file_urls({}, "en_US-amy-medium")


def test_resolve_file_urls_raises_when_files_incomplete():
    catalog = {
        "en_US-amy-medium": {
            "files": {
                # Only the .onnx, missing the sidecar — that's malformed.
                "en/en_US/amy/medium/en_US-amy-medium.onnx": {"size_bytes": 1},
            }
        }
    }
    with pytest.raises(ValueError, match="missing"):
        lib._resolve_file_urls(catalog, "en_US-amy-medium")
