"""
TTS 相关单元测试（第二十二波）

覆盖：
- AudioMerger (空列表/单文件/多文件合并/格式/异常)
"""

import os
import subprocess
import tempfile

import pytest

from packages.application.tts_job.audio_merger import (
    AudioMergeError,
    AudioMerger,
)


def _make_silence(duration: float = 0.5, sample_rate: int = 22050, fmt: str = "mp3") -> str:
    """生成一段静音音频文件，返回路径。"""
    tmp = tempfile.NamedTemporaryFile(suffix=f".{fmt}", delete=False)
    tmp.close()
    cmd = [
        "ffmpeg",
        "-y",
        "-f",
        "lavfi",
        "-i",
        f"anullsrc=r={sample_rate}:cl=mono",
        "-t",
        str(duration),
        "-q:a",
        "9",
        tmp.name,
    ]
    subprocess.run(cmd, capture_output=True, check=True)
    return tmp.name


class TestAudioMerger:
    """AudioMerger 音频合并器"""

    def test_empty_list_raises(self):
        """空列表抛错"""
        merger = AudioMerger()
        with pytest.raises(AudioMergeError, match="没有可合并"):
            merger.merge([])

    def test_single_file_returns_content(self):
        """单个文件直接返回内容"""
        path = _make_silence(duration=0.3)
        try:
            merger = AudioMerger()
            result = merger.merge([path])
            assert isinstance(result, bytes)
            assert len(result) > 100  # 应该有有效数据
            # 应该和文件本身一致
            with open(path, "rb") as f:
                original = f.read()
            assert result == original
        finally:
            os.unlink(path)

    def test_two_files_merged(self):
        """两个文件合并"""
        p1 = _make_silence(duration=0.3)
        p2 = _make_silence(duration=0.4)
        try:
            merger = AudioMerger()
            result = merger.merge([p1, p2])
            assert isinstance(result, bytes)
            assert len(result) > 200  # 合并后应该有数据
            # 写出来用 ffprobe 验证时长
            tmp = tempfile.NamedTemporaryFile(suffix=".mp3", delete=False)
            tmp.write(result)
            tmp.close()
            try:
                probe = subprocess.run(
                    [
                        "ffprobe",
                        "-v",
                        "error",
                        "-show_entries",
                        "format=duration",
                        "-of",
                        "default=noprint_wrappers=1:nokey=1",
                        tmp.name,
                    ],
                    capture_output=True,
                    text=True,
                    check=True,
                )
                duration = float(probe.stdout.strip())
                # 0.3 + 0.4 = 0.7 秒左右，允许一定误差
                assert 0.5 < duration < 1.0
            finally:
                os.unlink(tmp.name)
        finally:
            os.unlink(p1)
            os.unlink(p2)

    def test_three_files_merged(self):
        """三个文件合并"""
        paths = [_make_silence(duration=0.2) for _ in range(3)]
        try:
            merger = AudioMerger()
            result = merger.merge(paths)
            assert isinstance(result, bytes)
            assert len(result) > 200
        finally:
            for p in paths:
                os.unlink(p)

    def test_wav_format(self):
        """wav 格式合并"""
        p1 = _make_silence(duration=0.2, fmt="wav")
        p2 = _make_silence(duration=0.2, fmt="wav")
        try:
            merger = AudioMerger()
            result = merger.merge([p1, p2], output_format="wav")
            assert isinstance(result, bytes)
            # WAV 头部以 RIFF 开头
            assert result[:4] == b"RIFF"
        finally:
            os.unlink(p1)
            os.unlink(p2)

    def test_nonexistent_file_raises(self):
        """不存在的文件会抛错"""
        merger = AudioMerger()
        with pytest.raises(AudioMergeError):
            merger.merge(["/nonexistent/path/a.mp3", "/nonexistent/path/b.mp3"])

    def test_cleanup_temp_dir(self):
        """临时目录会被清理"""
        p1 = _make_silence(duration=0.2)
        p2 = _make_silence(duration=0.2)
        try:
            import tempfile as _tf

            before = set(os.listdir(_tf.gettempdir()))
            merger = AudioMerger()
            merger.merge([p1, p2])
            after = set(os.listdir(_tf.gettempdir()))
            # 不应该残留 tts_merge_ 前缀的目录
            new_items = after - before
            tts_items = [i for i in new_items if i.startswith("tts_merge_")]
            assert len(tts_items) == 0, f"残留临时目录: {tts_items}"
        finally:
            os.unlink(p1)
            os.unlink(p2)


# ============================================================
# TTSStreamingService - 入口路由与边界
# ============================================================


class TestTTSStreamingServiceRouting:
    """TTSStreamingService 入口路由与边界条件"""

    @pytest.mark.asyncio
    async def test_empty_text_returns_error(self):
        """空文本返回错误"""
        from unittest.mock import AsyncMock, MagicMock

        from packages.application.tts_job.streaming_service import TTSStreamingService

        mock_cosy = MagicMock()
        svc = TTSStreamingService(cosyvoice_service=mock_cosy)
        ws = AsyncMock()

        await svc.synthesize_and_stream(ws, {"text": ""})

        ws.send_json.assert_called_once()
        call_args = ws.send_json.call_args[0][0]
        assert call_args["type"] == "error"
        assert "不能为空" in call_args["message"]
        # 不应该调用 cosyvoice
        mock_cosy.submit_synthesize_task.assert_not_called()

    @pytest.mark.asyncio
    async def test_text_too_long_returns_error(self):
        """文本过长返回错误"""
        from unittest.mock import AsyncMock, MagicMock

        from packages.application.tts_job.streaming_service import (
            _MAX_TEXT_LENGTH,
            TTSStreamingService,
        )

        mock_cosy = MagicMock()
        svc = TTSStreamingService(cosyvoice_service=mock_cosy)
        ws = AsyncMock()

        long_text = "a" * (_MAX_TEXT_LENGTH + 1)
        await svc.synthesize_and_stream(ws, {"text": long_text})

        ws.send_json.assert_called_once()
        call_args = ws.send_json.call_args[0][0]
        assert call_args["type"] == "error"
        assert "过长" in call_args["message"]
        mock_cosy.submit_synthesize_task.assert_not_called()

    @pytest.mark.asyncio
    async def test_short_text_routes_to_short_path(self):
        """短文本走短文本路径（单段合成）"""
        from unittest.mock import AsyncMock, MagicMock, patch

        from packages.application.tts_job.streaming_service import TTSStreamingService

        mock_cosy = MagicMock()
        mock_cosy.submit_synthesize_task.return_value = {
            "audio_url": "https://example.com/audio.mp3",
            "duration": 3.5,
        }
        svc = TTSStreamingService(cosyvoice_service=mock_cosy)
        ws = AsyncMock()

        # mock 掉音频下载
        fake_audio = b"fake_audio_data" * 100
        with patch.object(svc, "_download_audio", return_value=fake_audio):
            await svc.synthesize_and_stream(ws, {"text": "你好世界", "voice_id": "v1"})

        # 应该调用了 cosy
        mock_cosy.submit_synthesize_task.assert_called_once()
        # 应该有 started 和 done 消息
        msg_types = [c[0][0]["type"] for c in ws.send_json.call_args_list]
        assert "started" in msg_types
        assert "done" in msg_types
        # 应该有音频分块发送
        assert ws.send_bytes.call_count > 0

    @pytest.mark.asyncio
    async def test_short_text_cosy_error(self):
        """短文本合成失败返回错误"""
        from unittest.mock import AsyncMock, MagicMock

        from packages.application.cosyvoice_service import CosyVoiceError
        from packages.application.tts_job.streaming_service import TTSStreamingService

        mock_cosy = MagicMock()
        mock_cosy.submit_synthesize_task.side_effect = CosyVoiceError("音色不存在")
        svc = TTSStreamingService(cosyvoice_service=mock_cosy)
        ws = AsyncMock()

        await svc.synthesize_and_stream(ws, {"text": "你好", "voice_id": "v-bad"})

        # 最后一条消息应该是 error
        last_msg = ws.send_json.call_args_list[-1][0][0]
        assert last_msg["type"] == "error"
        assert "音色不存在" in last_msg["message"]

    @pytest.mark.asyncio
    async def test_short_text_no_audio_url(self):
        """合成结果没有 audio_url 返回错误"""
        from unittest.mock import AsyncMock, MagicMock

        from packages.application.tts_job.streaming_service import TTSStreamingService

        mock_cosy = MagicMock()
        mock_cosy.submit_synthesize_task.return_value = {"duration": 1.0}  # 没有 audio_url
        svc = TTSStreamingService(cosyvoice_service=mock_cosy)
        ws = AsyncMock()

        await svc.synthesize_and_stream(ws, {"text": "你好"})

        last_msg = ws.send_json.call_args_list[-1][0][0]
        assert last_msg["type"] == "error"
        assert "音频 URL" in last_msg["message"]

    @pytest.mark.asyncio
    async def test_stream_audio_chunks_returns_total(self):
        """_stream_audio_chunks 返回正确字节数，分块正确"""
        from unittest.mock import AsyncMock, MagicMock

        from packages.application.tts_job.streaming_service import (
            _AUDIO_CHUNK_SIZE,
            TTSStreamingService,
        )

        mock_cosy = MagicMock()
        svc = TTSStreamingService(cosyvoice_service=mock_cosy)
        ws = AsyncMock()

        # 生成 10000 字节的假音频
        audio_data = b"x" * 10000
        total = await svc._stream_audio_chunks(ws, audio_data)

        assert total == 10000
        # 应该分 ceil(10000/4096) = 3 块
        expected_chunks = (10000 + _AUDIO_CHUNK_SIZE - 1) // _AUDIO_CHUNK_SIZE
        assert ws.send_bytes.call_count == expected_chunks
        # 验证所有块拼接起来等于原数据
        all_bytes = b"".join(c[0][0] for c in ws.send_bytes.call_args_list)
        assert all_bytes == audio_data

    @pytest.mark.asyncio
    async def test_send_json_handles_error(self):
        """_send_json 发送失败不抛出异常"""
        from unittest.mock import AsyncMock, MagicMock

        from packages.application.tts_job.streaming_service import TTSStreamingService

        mock_cosy = MagicMock()
        svc = TTSStreamingService(cosyvoice_service=mock_cosy)
        ws = AsyncMock()
        ws.send_json.side_effect = Exception("连接已断开")

        # 不应该抛异常
        await svc._send_json(ws, {"type": "done"})
        ws.send_json.assert_called_once()
