"""P2 WebSocket 流式合成单元测试。

覆盖：
- TTSStreamingService 流式合成逻辑
- 短文本流式合成
- 长文本分段流式合成
- 错误处理
"""

from __future__ import annotations

from unittest.mock import AsyncMock, MagicMock, patch

import pytest

from packages.application.cosyvoice_service import CosyVoiceError, CosyVoiceService
from packages.application.tts_job.streaming_service import (
    TTSStreamingService,
)


class MockWebSocket:
    """Mock WebSocket for testing."""

    def __init__(self):
        self.sent_json = []
        self.sent_bytes = []
        self.accepted = False

    async def accept(self):
        self.accepted = True

    async def send_json(self, data):
        self.sent_json.append(data)

    async def send_bytes(self, data):
        self.sent_bytes.append(data)

    async def receive_json(self):
        return {}


class TestTTSStreamingService:
    """测试 TTS 流式合成服务。"""

    @pytest.mark.asyncio
    async def test_empty_text_returns_error(self):
        """空文本返回错误。"""
        cosyvoice = MagicMock(spec=CosyVoiceService)
        service = TTSStreamingService(cosyvoice)

        ws = MockWebSocket()
        params = {"text": "", "voice_id": "test_voice"}

        await service.synthesize_and_stream(ws, params)

        assert len(ws.sent_json) == 1
        assert ws.sent_json[0]["type"] == "error"
        assert "文本不能为空" in ws.sent_json[0]["message"]

    @pytest.mark.asyncio
    async def test_text_too_long_returns_error(self):
        """超长文本返回错误。"""
        cosyvoice = MagicMock(spec=CosyVoiceService)
        service = TTSStreamingService(cosyvoice)

        ws = MockWebSocket()
        params = {"text": "x" * 10001, "voice_id": "test_voice"}

        await service.synthesize_and_stream(ws, params)

        assert len(ws.sent_json) == 1
        assert ws.sent_json[0]["type"] == "error"
        assert "文本过长" in ws.sent_json[0]["message"]

    @pytest.mark.asyncio
    async def test_short_text_stream_success(self):
        """短文本流式合成成功。"""
        cosyvoice = MagicMock(spec=CosyVoiceService)
        cosyvoice.submit_synthesize_task.return_value = {
            "task_id": "task_1",
            "audio_url": "https://temp.com/audio.mp3",
            "duration": 5.0,
            "file_size": 10000,
        }

        service = TTSStreamingService(cosyvoice)

        ws = MockWebSocket()
        params = {"text": "测试文本", "voice_id": "test_voice", "format": "mp3"}

        with patch.object(service, "_download_audio", return_value=b"fake audio data"):
            await service.synthesize_and_stream(ws, params)

        # 验证发送了 started 帧
        assert ws.sent_json[0]["type"] == "started"
        assert ws.sent_json[0]["segment_count"] == 1

        # 验证发送了二进制音频数据
        assert len(ws.sent_bytes) > 0

        # 验证发送了 done 帧
        assert ws.sent_json[-1]["type"] == "done"
        assert ws.sent_json[-1]["format"] == "mp3"

    @pytest.mark.asyncio
    async def test_short_text_cosyvoice_error(self):
        """短文本合成时 CosyVoice 报错。"""
        cosyvoice = MagicMock(spec=CosyVoiceService)
        cosyvoice.submit_synthesize_task.side_effect = CosyVoiceError("API error")

        service = TTSStreamingService(cosyvoice)

        ws = MockWebSocket()
        params = {"text": "测试文本", "voice_id": "test_voice"}

        await service.synthesize_and_stream(ws, params)

        # 验证发送了 started 帧和 error 帧
        assert len(ws.sent_json) == 2
        assert ws.sent_json[0]["type"] == "started"
        assert ws.sent_json[1]["type"] == "error"
        assert "API error" in ws.sent_json[1]["message"]

    @pytest.mark.asyncio
    async def test_short_text_no_audio_url(self):
        """短文本合成未返回 audio_url。"""
        cosyvoice = MagicMock(spec=CosyVoiceService)
        cosyvoice.submit_synthesize_task.return_value = {
            "task_id": "task_1",
            "audio_url": "",
        }

        service = TTSStreamingService(cosyvoice)

        ws = MockWebSocket()
        params = {"text": "测试文本", "voice_id": "test_voice"}

        await service.synthesize_and_stream(ws, params)

        # 验证发送了 started 帧和 error 帧
        assert len(ws.sent_json) == 2
        assert ws.sent_json[0]["type"] == "started"
        assert ws.sent_json[1]["type"] == "error"
        assert "未返回音频 URL" in ws.sent_json[1]["message"]

    @pytest.mark.asyncio
    async def test_long_text_stream_success(self):
        """长文本分段流式合成成功。"""
        cosyvoice = MagicMock(spec=CosyVoiceService)
        # 每个分段都返回 audio_url
        cosyvoice.submit_synthesize_task.side_effect = [
            {"task_id": "", "audio_url": "https://temp.com/seg1.mp3", "duration": 2.0},
            {"task_id": "", "audio_url": "https://temp.com/seg2.mp3", "duration": 3.0},
        ]

        service = TTSStreamingService(cosyvoice)

        ws = MockWebSocket()
        # 超过 500 字的文本
        params = {"text": "x" * 600, "voice_id": "test_voice", "format": "mp3"}

        with patch.object(service, "_download_audio", return_value=b"segment audio"):
            await service.synthesize_and_stream(ws, params)

        # 验证发送了 started 帧（多段）
        assert ws.sent_json[0]["type"] == "started"
        assert ws.sent_json[0]["segment_count"] >= 2

        # 验证发送了 segment_done 帧
        segment_done_count = sum(1 for msg in ws.sent_json if msg["type"] == "segment_done")
        assert segment_done_count >= 2

        # 验证发送了 done 帧
        assert ws.sent_json[-1]["type"] == "done"

    @pytest.mark.asyncio
    async def test_long_text_segment_failure(self):
        """长文本分段合成失败。"""
        cosyvoice = MagicMock(spec=CosyVoiceService)
        cosyvoice.submit_synthesize_task.side_effect = CosyVoiceError("Segment error")

        service = TTSStreamingService(cosyvoice)

        ws = MockWebSocket()
        params = {"text": "x" * 600, "voice_id": "test_voice"}

        await service.synthesize_and_stream(ws, params)

        # 验证发送了错误帧
        error_msgs = [msg for msg in ws.sent_json if msg["type"] == "error"]
        assert len(error_msgs) > 0
        assert "合成失败" in error_msgs[0]["message"]

    @pytest.mark.asyncio
    async def test_stream_audio_chunks(self):
        """音频分块推送。"""
        cosyvoice = MagicMock(spec=CosyVoiceService)
        service = TTSStreamingService(cosyvoice)

        ws = MockWebSocket()
        audio_data = b"x" * 10000  # 10KB

        total = await service._stream_audio_chunks(ws, audio_data)

        assert total == 10000
        # 验证分块发送（4KB per chunk）
        assert len(ws.sent_bytes) == 3  # 4096 + 4096 + 1808

    @pytest.mark.asyncio
    async def test_send_json_suppresses_exceptions(self):
        """_send_json 抑制异常。"""
        cosyvoice = MagicMock(spec=CosyVoiceService)
        service = TTSStreamingService(cosyvoice)

        ws = MockWebSocket()
        ws.send_json = AsyncMock(side_effect=Exception("Send failed"))

        # 不应该抛出异常
        await service._send_json(ws, {"type": "test"})

    @pytest.mark.asyncio
    async def test_download_audio(self):
        """下载音频数据（SSRF防护走safe_download_bytes，mock掉安全层）。"""
        cosyvoice = MagicMock(spec=CosyVoiceService)
        service = TTSStreamingService(cosyvoice)

        with patch("packages.application.tts_job.streaming_service.safe_download_bytes") as mock_download:
            mock_download.return_value = b"audio data"

            result = service._download_audio("https://example.com/audio.mp3")

            assert result == b"audio data"
            mock_download.assert_called_once()


class TestTTSStreamingEdgeCases:
    """流式合成边界测试."""

    @pytest.mark.asyncio
    async def test_exactly_500_chars_uses_short_text(self):
        """刚好500字走短文本路径."""
        cosyvoice = MagicMock(spec=CosyVoiceService)
        cosyvoice.submit_synthesize_task.return_value = {
            "audio_url": "https://temp.com/audio.mp3",
            "duration": 5.0,
        }
        service = TTSStreamingService(cosyvoice)
        ws = MockWebSocket()
        params = {"text": "x" * 500, "voice_id": "test_voice"}

        with patch.object(service, "_download_audio", return_value=b"audio"):
            await service.synthesize_and_stream(ws, params)

        # 短文本只有1个segment
        assert ws.sent_json[0]["type"] == "started"
        assert ws.sent_json[0]["segment_count"] == 1

    @pytest.mark.asyncio
    async def test_501_chars_uses_long_text(self):
        """501字走长文本分段路径."""
        cosyvoice = MagicMock(spec=CosyVoiceService)
        cosyvoice.submit_synthesize_task.return_value = {
            "audio_url": "https://temp.com/audio.mp3",
            "duration": 2.0,
        }
        service = TTSStreamingService(cosyvoice)
        ws = MockWebSocket()
        params = {"text": "x" * 501, "voice_id": "test_voice"}

        with patch.object(service, "_download_audio", return_value=b"audio"):
            await service.synthesize_and_stream(ws, params)

        # 长文本segment_count > 1
        assert ws.sent_json[0]["type"] == "started"
        assert ws.sent_json[0]["segment_count"] >= 2

    @pytest.mark.asyncio
    async def test_short_text_speed_param_passed(self):
        """短文本合成时速度参数正确传递."""
        cosyvoice = MagicMock(spec=CosyVoiceService)
        cosyvoice.submit_synthesize_task.return_value = {
            "audio_url": "https://temp.com/audio.mp3",
            "duration": 3.0,
        }
        service = TTSStreamingService(cosyvoice)
        ws = MockWebSocket()
        params = {"text": "测试", "voice_id": "v1", "speed": 1.5, "format": "wav"}

        with patch.object(service, "_download_audio", return_value=b"audio"):
            await service.synthesize_and_stream(ws, params)

        cosyvoice.submit_synthesize_task.assert_called_once()
        call_kwargs = cosyvoice.submit_synthesize_task.call_args.kwargs
        assert call_kwargs["speed"] == 1.5
        assert call_kwargs["format"] == "wav"
        assert call_kwargs["voice_id"] == "v1"

    @pytest.mark.asyncio
    async def test_short_text_sample_rate_param(self):
        """短文本合成时采样率参数传递."""
        cosyvoice = MagicMock(spec=CosyVoiceService)
        cosyvoice.submit_synthesize_task.return_value = {
            "audio_url": "https://temp.com/audio.mp3",
            "duration": 1.0,
        }
        service = TTSStreamingService(cosyvoice)
        ws = MockWebSocket()
        params = {"text": "测试", "voice_id": "v1", "sample_rate": 44100}

        with patch.object(service, "_download_audio", return_value=b"audio"):
            await service.synthesize_and_stream(ws, params)

        call_kwargs = cosyvoice.submit_synthesize_task.call_args.kwargs
        assert call_kwargs["sample_rate"] == 44100

    @pytest.mark.asyncio
    async def test_single_chunk_audio(self):
        """小于4KB的音频只发1块."""
        cosyvoice = MagicMock(spec=CosyVoiceService)
        service = TTSStreamingService(cosyvoice)
        ws = MockWebSocket()
        audio_data = b"x" * 1000  # 1KB < 4KB

        total = await service._stream_audio_chunks(ws, audio_data)
        assert total == 1000
        assert len(ws.sent_bytes) == 1
        assert ws.sent_bytes[0] == audio_data

    @pytest.mark.asyncio
    async def test_exact_chunk_size_audio(self):
        """刚好4KB的音频只发1块."""
        cosyvoice = MagicMock(spec=CosyVoiceService)
        service = TTSStreamingService(cosyvoice)
        ws = MockWebSocket()
        audio_data = b"x" * 4096

        total = await service._stream_audio_chunks(ws, audio_data)
        assert total == 4096
        assert len(ws.sent_bytes) == 1

    @pytest.mark.asyncio
    async def test_empty_audio_chunks(self):
        """空音频数据不发送任何块."""
        cosyvoice = MagicMock(spec=CosyVoiceService)
        service = TTSStreamingService(cosyvoice)
        ws = MockWebSocket()

        total = await service._stream_audio_chunks(ws, b"")
        assert total == 0
        assert len(ws.sent_bytes) == 0

    @pytest.mark.asyncio
    async def test_short_text_unexpected_exception(self):
        """短文本合成时非预期异常捕获."""
        cosyvoice = MagicMock(spec=CosyVoiceService)
        cosyvoice.submit_synthesize_task.side_effect = RuntimeError("Unexpected error")

        service = TTSStreamingService(cosyvoice)
        ws = MockWebSocket()
        params = {"text": "测试", "voice_id": "v1"}

        await service.synthesize_and_stream(ws, params)

        assert ws.sent_json[-1]["type"] == "error"
        assert "合成失败" in ws.sent_json[-1]["message"]

    @pytest.mark.asyncio
    async def test_short_text_download_failure(self):
        """短文本音频下载失败."""
        cosyvoice = MagicMock(spec=CosyVoiceService)
        cosyvoice.submit_synthesize_task.return_value = {
            "audio_url": "https://temp.com/audio.mp3",
            "duration": 1.0,
        }
        service = TTSStreamingService(cosyvoice)
        ws = MockWebSocket()
        params = {"text": "测试", "voice_id": "v1"}

        with patch.object(service, "_download_audio", side_effect=Exception("Download failed")):
            await service.synthesize_and_stream(ws, params)

        assert ws.sent_json[-1]["type"] == "error"
        assert "音频推送失败" in ws.sent_json[-1]["message"]

    @pytest.mark.asyncio
    async def test_long_text_segment_count_matches_split(self):
        """长文本分段数量与split_text结果一致."""
        from packages.application.tts_job.text_splitter import split_text

        text = "x" * 1200
        segments = split_text(text, max_chars=500)
        expected_count = len(segments)

        cosyvoice = MagicMock(spec=CosyVoiceService)
        cosyvoice.submit_synthesize_task.return_value = {
            "audio_url": "https://temp.com/a.mp3",
            "duration": 1.0,
        }
        service = TTSStreamingService(cosyvoice)
        ws = MockWebSocket()
        params = {"text": text, "voice_id": "v1"}

        with patch.object(service, "_download_audio", return_value=b"audio"):
            await service.synthesize_and_stream(ws, params)

        assert ws.sent_json[0]["segment_count"] == expected_count
        segment_done = sum(1 for m in ws.sent_json if m["type"] == "segment_done")
        assert segment_done == expected_count

    @pytest.mark.asyncio
    async def test_long_text_total_bytes_accumulated(self):
        """长文本总字节数正确累加."""
        cosyvoice = MagicMock(spec=CosyVoiceService)
        cosyvoice.submit_synthesize_task.return_value = {
            "audio_url": "https://temp.com/a.mp3",
            "duration": 1.0,
        }
        service = TTSStreamingService(cosyvoice)
        ws = MockWebSocket()
        params = {"text": "x" * 600, "voice_id": "v1"}

        audio_chunk = b"x" * 5000
        with patch.object(service, "_download_audio", return_value=audio_chunk):
            await service.synthesize_and_stream(ws, params)

        # done帧中file_size应为分段数 * 每段大小
        done_msg = ws.sent_json[-1]
        assert done_msg["type"] == "done"
        segment_count = ws.sent_json[0]["segment_count"]
        assert done_msg["file_size"] == segment_count * 5000

    def test_download_audio_passes_purpose_and_mime(self):
        """_download_audio正确传递参数给safe_download_bytes."""
        cosyvoice = MagicMock(spec=CosyVoiceService)
        service = TTSStreamingService(cosyvoice)

        with patch("packages.application.tts_job.streaming_service.safe_download_bytes") as mock:
            mock.return_value = b"data"
            service._download_audio("https://example.com/a.wav")

            mock.assert_called_once()
            kwargs = mock.call_args.kwargs
            assert kwargs["purpose"] == "tts_streaming_download"
            assert kwargs["timeout"] == 60.0
            assert "allowed_mime_types" in kwargs
