"""Tests for the per-voice directory layout produced by clone_add."""

import json
import wave
from pathlib import Path
from unittest.mock import patch

import pytest
import yaml

from voice_mode.tools.impressions.profiles import (
    NORMALISED_FORMAT_DESC,
    clone_add,
)


def _write_wav(path: Path, seconds: float = 5.0, rate: int = 16000) -> Path:
    with wave.open(str(path), "w") as wf:
        wf.setnchannels(1)
        wf.setsampwidth(2)
        wf.setframerate(rate)
        wf.writeframes(b"\x00\x00" * int(rate * seconds))
    return path


@pytest.fixture
def tmp_voicemode(tmp_path, monkeypatch):
    voices_json = tmp_path / "voices.json"
    voices_dir = tmp_path / "voices"
    voices_dir.mkdir()
    monkeypatch.setattr(
        "voice_mode.tools.impressions.profiles.VOICES_JSON", voices_json
    )
    monkeypatch.setattr(
        "voice_mode.tools.impressions.profiles.VOICES_DIR", voices_dir
    )
    return {"voices_json": voices_json, "voices_dir": voices_dir}


@pytest.fixture
def stub_wav(tmp_path):
    return _write_wav(tmp_path / "input.wav", seconds=5.0)


def _patch_normalise(voices_dir: Path):
    """Patch _normalise_audio to write a valid 5s WAV at dest (no real ffmpeg).

    Also patch _probe_duration_seconds so the post-normalise log returns 5.0
    deterministically regardless of the stub WAV's actual size.
    """

    def fake_normalise(src, dest):
        _write_wav(Path(dest), seconds=5.0, rate=24000)

    return fake_normalise


@pytest.mark.asyncio
async def test_clone_add_creates_per_voice_directory(tmp_voicemode, stub_wav):
    voices_dir = tmp_voicemode["voices_dir"]
    with patch(
        "voice_mode.tools.impressions.profiles._normalise_audio",
        side_effect=_patch_normalise(voices_dir),
    ), patch(
        "voice_mode.tools.impressions.profiles._transcribe_audio",
        return_value="hello world",
    ):
        result = await clone_add("testfoo", str(stub_wav))

    assert result["success"] is True
    voice_dir = voices_dir / "testfoo"
    assert voice_dir.is_dir()
    assert (voice_dir / "default.wav").exists()
    assert (voice_dir / "voice.md").exists()


@pytest.mark.asyncio
async def test_voice_md_has_required_keys(tmp_voicemode, stub_wav):
    voices_dir = tmp_voicemode["voices_dir"]
    with patch(
        "voice_mode.tools.impressions.profiles._normalise_audio",
        side_effect=_patch_normalise(voices_dir),
    ), patch(
        "voice_mode.tools.impressions.profiles._transcribe_audio",
        return_value="this is the transcript",
    ):
        await clone_add("testfoo", str(stub_wav))

    voice_md = (voices_dir / "testfoo" / "voice.md").read_text()
    assert voice_md.startswith("---\n")
    front_matter, _, body = voice_md.partition("\n---\n")
    front_matter = front_matter.lstrip("-").lstrip("\n")
    parsed = yaml.safe_load(front_matter)

    for key in ("name", "source", "duration_seconds", "format", "transcript"):
        assert key in parsed, f"voice.md missing required key: {key}"

    assert parsed["name"] == "testfoo"
    assert parsed["transcript"] == "this is the transcript"
    assert parsed["format"] == NORMALISED_FORMAT_DESC
    assert isinstance(parsed["duration_seconds"], float)
    assert "Auto-generated by voicemode clone add on" in body


@pytest.mark.asyncio
async def test_clone_add_writes_default_txt_sidecar(tmp_voicemode, stub_wav):
    """clone_add writes a default.txt sidecar holding the transcript, so the
    loader resolves a non-empty ref_text from its primary lookup (VM-1439)."""
    voices_dir = tmp_voicemode["voices_dir"]
    with patch(
        "voice_mode.tools.impressions.profiles._normalise_audio",
        side_effect=_patch_normalise(voices_dir),
    ), patch(
        "voice_mode.tools.impressions.profiles._transcribe_audio",
        return_value="this is the transcript",
    ):
        result = await clone_add("testfoo", str(stub_wav))

    assert result["success"] is True
    sidecar = voices_dir / "testfoo" / "default.txt"
    assert sidecar.exists()
    assert sidecar.read_text().strip() == "this is the transcript"


@pytest.mark.asyncio
async def test_voices_json_points_at_per_voice_default_wav(tmp_voicemode, stub_wav):
    voices_dir = tmp_voicemode["voices_dir"]
    with patch(
        "voice_mode.tools.impressions.profiles._normalise_audio",
        side_effect=_patch_normalise(voices_dir),
    ), patch(
        "voice_mode.tools.impressions.profiles._transcribe_audio",
        return_value="hello world",
    ):
        await clone_add("testfoo", str(stub_wav))

    data = json.loads(tmp_voicemode["voices_json"].read_text())
    assert "testfoo" in data["voices"]
    assert data["voices"]["testfoo"]["ref_audio"].endswith("testfoo/default.wav")


@pytest.mark.asyncio
async def test_clone_add_refuses_when_voice_dir_already_populated(
    tmp_voicemode, stub_wav
):
    voices_dir = tmp_voicemode["voices_dir"]
    existing = voices_dir / "testfoo"
    existing.mkdir()
    (existing / "default.wav").write_bytes(b"existing")

    with patch(
        "voice_mode.tools.impressions.profiles._normalise_audio",
        side_effect=_patch_normalise(voices_dir),
    ), patch(
        "voice_mode.tools.impressions.profiles._transcribe_audio",
        return_value="hello world",
    ):
        result = await clone_add("testfoo", str(stub_wav))

    assert result["success"] is False
    assert "already exists" in result["error"]


@pytest.mark.asyncio
async def test_clone_add_does_not_remove_legacy_flat_wav(tmp_voicemode, stub_wav):
    """Out-of-scope migration: a legacy flat <name>.wav at voices_root must remain."""
    voices_dir = tmp_voicemode["voices_dir"]
    legacy = voices_dir / "testfoo.wav"
    legacy.write_bytes(b"legacy")

    with patch(
        "voice_mode.tools.impressions.profiles._normalise_audio",
        side_effect=_patch_normalise(voices_dir),
    ), patch(
        "voice_mode.tools.impressions.profiles._transcribe_audio",
        return_value="hello world",
    ):
        result = await clone_add("testfoo", str(stub_wav))

    assert result["success"] is True
    assert legacy.exists()
    assert (voices_dir / "testfoo" / "default.wav").exists()


@pytest.mark.asyncio
async def test_clone_add_preserves_other_voices_json_entries(
    tmp_voicemode, stub_wav
):
    voices_dir = tmp_voicemode["voices_dir"]
    pre_existing = {
        "voices": {
            "alice": {
                "ref_audio": "alice.wav",
                "ref_text": "hi",
                "description": "",
            }
        }
    }
    tmp_voicemode["voices_json"].write_text(json.dumps(pre_existing))

    with patch(
        "voice_mode.tools.impressions.profiles._normalise_audio",
        side_effect=_patch_normalise(voices_dir),
    ), patch(
        "voice_mode.tools.impressions.profiles._transcribe_audio",
        return_value="hello world",
    ):
        await clone_add("testfoo", str(stub_wav))

    data = json.loads(tmp_voicemode["voices_json"].read_text())
    assert "alice" in data["voices"]
    assert data["voices"]["alice"]["ref_audio"] == "alice.wav"
    assert "testfoo" in data["voices"]


@pytest.mark.asyncio
async def test_success_message_includes_layout_paths(tmp_voicemode, stub_wav):
    voices_dir = tmp_voicemode["voices_dir"]
    with patch(
        "voice_mode.tools.impressions.profiles._normalise_audio",
        side_effect=_patch_normalise(voices_dir),
    ), patch(
        "voice_mode.tools.impressions.profiles._transcribe_audio",
        return_value="hello world transcript here",
    ):
        result = await clone_add("testfoo", str(stub_wav))

    msg = result["message"]
    assert f"Path: {voices_dir / 'testfoo'}/" in msg
    assert "default.wav" in msg
    assert "mono 24kHz 16-bit PCM" in msg
    assert 'Text: "hello world transcript here' in msg
