"""CosyVoiceService 单元测试。"""

from __future__ import annotations

import json
from unittest.mock import MagicMock

import httpx
import pytest

from packages.application.cosyvoice_service import (
    CloneResult,
    CosyVoiceAuthError,
    CosyVoiceError,
    CosyVoiceService,
    CosyVoiceTimeoutError,
    SynthesizeResult,
)
from packages.domain.preset_voices import PresetVoice


def _make_service(
    *,
    api_key: str = "test-api-key",
    base_url: str = "https://test.cosyvoice.api",
    model: str = "cosyvoice-v1",
    http_client: httpx.Client | None = None,
) -> CosyVoiceService:
    """创建测试用 CosyVoiceService。"""
    return CosyVoiceService(
        api_key=api_key,
        base_url=base_url,
        model=model,
        http_client=http_client,
    )


def _mock_response(
    status_code: int = 200,
    json_data: dict | None = None,
    text: str = "",
) -> httpx.Response:
    """创建 mock HTTP 响应。"""
    # httpx.Response 需要 content 参数才能正确调用 .json()
    content = b""
    if json_data is not None:
        content = json.dumps(json_data).encode("utf-8")
    elif text:
        content = text.encode("utf-8")

    return httpx.Response(
        status_code=status_code,
        content=content,
        request=httpx.Request("POST", "https://test.cosyvoice.api"),
    )


# ── list_preset_voices ───────────────────────────────────


class TestListPresetVoices:
    """测试预置音色列表。"""

    def test_returns_all_preset_voices(self) -> None:
        """返回所有预置音色。"""
        service = _make_service()
        voices = service.list_preset_voices()

        assert len(voices) == 8
        assert all(isinstance(v, PresetVoice) for v in voices)

    def test_preset_voice_ids(self) -> None:
        """预置音色 ID 正确。"""
        service = _make_service()
        voices = service.list_preset_voices()
        voice_ids = [v.voice_id for v in voices]

        assert "longxiaochun" in voice_ids
        assert "longxiaoxia" in voice_ids
        assert "longxiaochen" in voice_ids
        assert "longyue" in voice_ids
        assert "longshu" in voice_ids
        assert "longjing" in voice_ids
        assert "longbo" in voice_ids
        assert "longtian" in voice_ids

    def test_preset_voice_has_required_fields(self) -> None:
        """预置音色包含必要字段。"""
        service = _make_service()
        voices = service.list_preset_voices()

        for voice in voices:
            assert voice.voice_id
            assert voice.name
            assert voice.gender in ("male", "female")
            assert voice.language == "zh-CN"


# ── clone_voice ──────────────────────────────────────────


class TestCloneVoice:
    """测试音色克隆。"""

    def test_clone_sync_success(self) -> None:
        """同步克隆成功（直接返回 voice_id）。"""
        mock_client = MagicMock(spec=httpx.Client)
        mock_client.request.return_value = _mock_response(
            json_data={
                "request_id": "req-001",
                "output": {"voice_id": "clone-voice-001"},
            }
        )

        service = _make_service(http_client=mock_client)
        result = service.clone_voice(
            audio_url="https://example.com/audio.mp3",
            voice_name="我的音色",
        )

        assert isinstance(result, CloneResult)
        assert result.voice_id == "clone-voice-001"
        assert result.request_id == "req-001"
        mock_client.request.assert_called_once()

    def test_clone_async_with_polling(self) -> None:
        """异步克隆（返回 task_id，轮询后成功）。"""
        mock_client = MagicMock(spec=httpx.Client)

        # 第一次调用：提交任务，返回 task_id
        submit_response = _mock_response(
            json_data={
                "request_id": "req-001",
                "output": {"task_id": "task-abc123", "task_status": "PENDING"},
            }
        )

        # 第二次调用：查询状态 → RUNNING
        running_response = _mock_response(
            json_data={
                "request_id": "req-002",
                "output": {"task_status": "RUNNING"},
            }
        )

        # 第三次调用：查询状态 → SUCCEEDED
        success_response = _mock_response(
            json_data={
                "request_id": "req-003",
                "output": {
                    "task_status": "SUCCEEDED",
                    "voice_id": "clone-voice-async-001",
                },
            }
        )

        mock_client.request.side_effect = [
            submit_response,
            running_response,
            success_response,
        ]

        service = _make_service(http_client=mock_client)
        service.POLL_INTERVAL = 0  # 测试中不等待

        result = service.clone_voice(
            audio_url="https://example.com/audio.mp3",
        )

        assert result.voice_id == "clone-voice-async-001"
        assert mock_client.request.call_count == 3

    def test_clone_async_task_failed(self) -> None:
        """异步克隆任务失败。"""
        mock_client = MagicMock(spec=httpx.Client)

        submit_response = _mock_response(
            json_data={
                "output": {"task_id": "task-fail"},
            }
        )
        failed_response = _mock_response(
            json_data={
                "output": {
                    "task_status": "FAILED",
                    "message": "音频质量不达标",
                },
            }
        )

        mock_client.request.side_effect = [submit_response, failed_response]

        service = _make_service(http_client=mock_client)
        service.POLL_INTERVAL = 0

        with pytest.raises(CosyVoiceError, match="音频质量不达标"):
            service.clone_voice(audio_url="https://example.com/bad.mp3")

    def test_clone_empty_audio_url_raises(self) -> None:
        """空 audio_url 抛出 ValueError。"""
        service = _make_service()

        with pytest.raises(ValueError, match="audio_url 不能为空"):
            service.clone_voice(audio_url="")

    def test_clone_no_api_key_raises_auth_error(self) -> None:
        """未配置 API Key 抛出 CosyVoiceAuthError。"""
        service = _make_service(api_key="")

        with pytest.raises(CosyVoiceAuthError, match="API Key 未配置"):
            service.clone_voice(audio_url="https://example.com/audio.mp3")

    def test_clone_auth_failure(self) -> None:
        """API 认证失败（401）。"""
        mock_client = MagicMock(spec=httpx.Client)
        mock_client.request.return_value = _mock_response(status_code=401)

        service = _make_service(http_client=mock_client)

        with pytest.raises(CosyVoiceAuthError, match="认证失败"):
            service.clone_voice(audio_url="https://example.com/audio.mp3")

    def test_clone_client_error_no_retry(self) -> None:
        """客户端错误（400）不重试。"""
        mock_client = MagicMock(spec=httpx.Client)
        mock_client.request.return_value = _mock_response(status_code=400, text="Bad Request")

        service = _make_service(http_client=mock_client)

        with pytest.raises(CosyVoiceError, match="HTTP 400"):
            service.clone_voice(audio_url="https://example.com/audio.mp3")

        # 客户端错误不重试，只调用一次
        assert mock_client.request.call_count == 1

    def test_clone_server_error_retries(self) -> None:
        """服务端错误（500）重试。"""
        mock_client = MagicMock(spec=httpx.Client)
        mock_client.request.return_value = _mock_response(status_code=500)

        service = _make_service(http_client=mock_client)
        service.RETRY_BACKOFF = 0  # 测试中不等待

        with pytest.raises(CosyVoiceError, match="服务端错误"):
            service.clone_voice(audio_url="https://example.com/audio.mp3")

        # 服务端错误重试 MAX_RETRIES 次
        assert mock_client.request.call_count == service.MAX_RETRIES

    def test_clone_timeout_retries(self) -> None:
        """超时重试。"""
        mock_client = MagicMock(spec=httpx.Client)
        mock_client.request.side_effect = httpx.TimeoutException("timeout")

        service = _make_service(http_client=mock_client)
        service.RETRY_BACKOFF = 0

        with pytest.raises(CosyVoiceTimeoutError):
            service.clone_voice(audio_url="https://example.com/audio.mp3")

        assert mock_client.request.call_count == service.MAX_RETRIES

    def test_clone_with_voice_name(self) -> None:
        """带 voice_name 参数。"""
        mock_client = MagicMock(spec=httpx.Client)
        mock_client.request.return_value = _mock_response(json_data={"output": {"voice_id": "v-001"}})

        service = _make_service(http_client=mock_client)
        service.clone_voice(
            audio_url="https://example.com/audio.mp3",
            voice_name="测试音色",
        )

        call_args = mock_client.request.call_args
        payload = call_args.kwargs.get("json") or call_args[1].get("json")
        assert payload["parameters"]["voice_name"] == "测试音色"

    def test_clone_no_task_id_or_voice_id_raises(self) -> None:
        """API 返回无效响应（无 task_id 也无 voice_id）。"""
        mock_client = MagicMock(spec=httpx.Client)
        mock_client.request.return_value = _mock_response(json_data={"output": {}})

        service = _make_service(http_client=mock_client)

        with pytest.raises(CosyVoiceError, match="未返回 task_id 或 voice_id"):
            service.clone_voice(audio_url="https://example.com/audio.mp3")


# ── submit_clone_task ────────────────────────────────────


class TestSubmitCloneTask:
    """测试 submit_clone_task（非阻塞提交）。"""

    def test_submit_async_returns_task_id(self) -> None:
        """异步模式：返回 task_id。"""
        mock_client = MagicMock(spec=httpx.Client)
        mock_client.request.return_value = _mock_response(
            json_data={
                "request_id": "req-001",
                "output": {"task_id": "task-abc", "task_status": "PENDING"},
            }
        )

        service = _make_service(http_client=mock_client)
        result = service.submit_clone_task(
            audio_url="https://example.com/audio.mp3",
            voice_name="测试音色",
        )

        assert result["task_id"] == "task-abc"
        assert result["voice_id"] == ""
        assert result["request_id"] == "req-001"
        mock_client.request.assert_called_once()

    def test_submit_sync_returns_voice_id(self) -> None:
        """同步模式：直接返回 voice_id。"""
        mock_client = MagicMock(spec=httpx.Client)
        mock_client.request.return_value = _mock_response(
            json_data={
                "request_id": "req-002",
                "output": {"voice_id": "voice-sync-001"},
            }
        )

        service = _make_service(http_client=mock_client)
        result = service.submit_clone_task(
            audio_url="https://example.com/audio.mp3",
        )

        assert result["task_id"] == ""
        assert result["voice_id"] == "voice-sync-001"
        assert result["request_id"] == "req-002"

    def test_submit_empty_audio_url_raises(self) -> None:
        """空 audio_url 抛出 ValueError。"""
        service = _make_service()

        with pytest.raises(ValueError, match="audio_url 不能为空"):
            service.submit_clone_task(audio_url="")

    def test_submit_no_api_key_raises(self) -> None:
        """未配置 API Key 抛出 CosyVoiceAuthError。"""
        service = _make_service(api_key="")

        with pytest.raises(CosyVoiceAuthError, match="API Key 未配置"):
            service.submit_clone_task(audio_url="https://example.com/audio.mp3")

    def test_submit_no_task_id_or_voice_id_raises(self) -> None:
        """API 返回无效响应时抛出 CosyVoiceError。"""
        mock_client = MagicMock(spec=httpx.Client)
        mock_client.request.return_value = _mock_response(json_data={"output": {}})

        service = _make_service(http_client=mock_client)

        with pytest.raises(CosyVoiceError, match="未返回 task_id 或 voice_id"):
            service.submit_clone_task(audio_url="https://example.com/audio.mp3")

    def test_submit_with_voice_name_in_payload(self) -> None:
        """voice_name 参数包含在请求体中。"""
        mock_client = MagicMock(spec=httpx.Client)
        mock_client.request.return_value = _mock_response(json_data={"output": {"task_id": "task-001"}})

        service = _make_service(http_client=mock_client)
        service.submit_clone_task(
            audio_url="https://example.com/audio.mp3",
            voice_name="我的音色",
            language="en-US",
        )

        call_args = mock_client.request.call_args
        payload = call_args.kwargs.get("json") or call_args[1].get("json")
        assert payload["parameters"]["voice_name"] == "我的音色"
        assert payload["parameters"]["language"] == "en-US"


# ── check_task_status ────────────────────────────────────


class TestCheckTaskStatus:
    """测试 check_task_status（单次状态查询）。"""

    def test_check_succeeded(self) -> None:
        """查询成功状态。"""
        mock_client = MagicMock(spec=httpx.Client)
        mock_client.request.return_value = _mock_response(
            json_data={
                "output": {
                    "task_status": "SUCCEEDED",
                    "voice_id": "voice-done-001",
                },
            }
        )

        service = _make_service(http_client=mock_client)
        result = service.check_task_status("task-abc")

        assert result["status"] == "SUCCEEDED"
        assert result["voice_id"] == "voice-done-001"
        assert result["message"] == ""

    def test_check_running(self) -> None:
        """查询运行中状态。"""
        mock_client = MagicMock(spec=httpx.Client)
        mock_client.request.return_value = _mock_response(
            json_data={
                "output": {"task_status": "RUNNING"},
            }
        )

        service = _make_service(http_client=mock_client)
        result = service.check_task_status("task-abc")

        assert result["status"] == "RUNNING"
        assert result["voice_id"] == ""

    def test_check_failed_with_message(self) -> None:
        """查询失败状态，包含错误消息。"""
        mock_client = MagicMock(spec=httpx.Client)
        mock_client.request.return_value = _mock_response(
            json_data={
                "output": {
                    "task_status": "FAILED",
                    "message": "音频质量不达标",
                },
            }
        )

        service = _make_service(http_client=mock_client)
        result = service.check_task_status("task-fail")

        assert result["status"] == "FAILED"
        assert result["message"] == "音频质量不达标"

    def test_check_no_api_key_raises(self) -> None:
        """未配置 API Key 抛出 CosyVoiceAuthError。"""
        service = _make_service(api_key="")

        with pytest.raises(CosyVoiceAuthError, match="API Key 未配置"):
            service.check_task_status("task-abc")

    def test_check_uses_correct_path(self) -> None:
        """请求路径包含 task_id。"""
        mock_client = MagicMock(spec=httpx.Client)
        mock_client.request.return_value = _mock_response(json_data={"output": {"task_status": "PENDING"}})

        service = _make_service(http_client=mock_client)
        service.check_task_status("task-xyz-123")

        call_args = mock_client.request.call_args
        url = call_args.kwargs.get("url") or call_args[1].get("url") or call_args[0][0]
        assert "/tasks/task-xyz-123" in url


# ── synthesize_speech ────────────────────────────────────


class TestSynthesizeSpeech:
    """测试语音合成。"""

    def test_synthesize_sync_success(self) -> None:
        """同步合成成功（直接返回 audio_url）。"""
        mock_client = MagicMock(spec=httpx.Client)
        mock_client.request.return_value = _mock_response(
            json_data={
                "request_id": "req-tts-001",
                "output": {
                    "audio_url": "https://cdn.example.com/audio.mp3",
                    "duration": 5.2,
                    "file_size": 83200,
                },
            }
        )

        service = _make_service(http_client=mock_client)
        result = service.synthesize_speech(
            text="你好世界",
            voice_id="longxiaochun",
        )

        assert isinstance(result, SynthesizeResult)
        assert result.audio_url == "https://cdn.example.com/audio.mp3"
        assert result.duration == 5.2
        assert result.file_size == 83200
        assert result.request_id == "req-tts-001"

    def test_synthesize_async_with_polling(self) -> None:
        """异步合成（返回 task_id，轮询后成功）。"""
        mock_client = MagicMock(spec=httpx.Client)

        submit_response = _mock_response(
            json_data={
                "output": {"task_id": "task-tts-001", "task_status": "PENDING"},
            }
        )
        success_response = _mock_response(
            json_data={
                "output": {
                    "task_status": "SUCCEEDED",
                    "audio_url": "https://cdn.example.com/async.mp3",
                    "duration": 3.0,
                },
            }
        )

        mock_client.request.side_effect = [submit_response, success_response]

        service = _make_service(http_client=mock_client)
        service.POLL_INTERVAL = 0

        result = service.synthesize_speech(
            text="异步合成测试",
            voice_id="longxiaoxia",
        )

        assert result.audio_url == "https://cdn.example.com/async.mp3"
        assert result.duration == 3.0
        assert mock_client.request.call_count == 2

    def test_synthesize_async_task_failed(self) -> None:
        """异步合成任务失败。"""
        mock_client = MagicMock(spec=httpx.Client)

        submit_response = _mock_response(json_data={"output": {"task_id": "task-tts-fail"}})
        failed_response = _mock_response(
            json_data={
                "output": {
                    "task_status": "FAILED",
                    "message": "文本过长",
                },
            }
        )

        mock_client.request.side_effect = [submit_response, failed_response]

        service = _make_service(http_client=mock_client)
        service.POLL_INTERVAL = 0

        with pytest.raises(CosyVoiceError, match="文本过长"):
            service.synthesize_speech(
                text="超长文本" * 10000,
                voice_id="longxiaochun",
            )

    def test_synthesize_empty_text_raises(self) -> None:
        """空 text 抛出 ValueError。"""
        service = _make_service()

        with pytest.raises(ValueError, match="text 不能为空"):
            service.synthesize_speech(text="", voice_id="longxiaochun")

    def test_synthesize_empty_voice_id_raises(self) -> None:
        """空 voice_id 抛出 ValueError。"""
        service = _make_service()

        with pytest.raises(ValueError, match="voice_id 不能为空"):
            service.synthesize_speech(text="测试", voice_id="")

    def test_synthesize_no_api_key_raises(self) -> None:
        """未配置 API Key 抛出 CosyVoiceAuthError。"""
        service = _make_service(api_key="")

        with pytest.raises(CosyVoiceAuthError, match="API Key 未配置"):
            service.synthesize_speech(text="测试", voice_id="longxiaochun")

    def test_synthesize_with_parameters(self) -> None:
        """带采样率、格式、语速参数。"""
        mock_client = MagicMock(spec=httpx.Client)
        mock_client.request.return_value = _mock_response(
            json_data={"output": {"audio_url": "https://cdn.example.com/out.wav"}}
        )

        service = _make_service(http_client=mock_client)
        service.synthesize_speech(
            text="参数测试",
            voice_id="longxiaochun",
            sample_rate=44100,
            format="wav",
            speed=1.5,
        )

        call_args = mock_client.request.call_args
        payload = call_args.kwargs.get("json") or call_args[1].get("json")
        params = payload["parameters"]
        assert params["sample_rate"] == 44100
        assert params["format"] == "wav"
        assert params["rate"] == 1.5

    def test_synthesize_no_url_or_task_id_raises(self) -> None:
        """API 返回无效响应（无 audio_url 也无 task_id）。"""
        mock_client = MagicMock(spec=httpx.Client)
        mock_client.request.return_value = _mock_response(json_data={"output": {}})

        service = _make_service(http_client=mock_client)

        with pytest.raises(CosyVoiceError, match="未返回 audio_url 或 task_id"):
            service.synthesize_speech(text="测试", voice_id="longxiaochun")

    def test_synthesize_server_error_retries(self) -> None:
        """服务端错误重试。"""
        mock_client = MagicMock(spec=httpx.Client)
        mock_client.request.return_value = _mock_response(status_code=502)

        service = _make_service(http_client=mock_client)
        service.RETRY_BACKOFF = 0

        with pytest.raises(CosyVoiceError, match="服务端错误"):
            service.synthesize_speech(text="测试", voice_id="longxiaochun")

        assert mock_client.request.call_count == service.MAX_RETRIES

    def test_synthesize_timeout_retries(self) -> None:
        """超时重试。"""
        mock_client = MagicMock(spec=httpx.Client)
        mock_client.request.side_effect = httpx.TimeoutException("timeout")

        service = _make_service(http_client=mock_client)
        service.RETRY_BACKOFF = 0

        with pytest.raises(CosyVoiceTimeoutError):
            service.synthesize_speech(text="测试", voice_id="longxiaochun")

        assert mock_client.request.call_count == service.MAX_RETRIES


# ── 重试逻辑 ─────────────────────────────────────────────


class TestRetryLogic:
    """测试重试逻辑。"""

    def test_retry_then_success(self) -> None:
        """第一次失败，第二次成功。"""
        mock_client = MagicMock(spec=httpx.Client)

        # 第一次：服务端错误
        error_response = _mock_response(status_code=500)
        # 第二次：成功
        success_response = _mock_response(json_data={"output": {"voice_id": "v-retry-ok"}})

        mock_client.request.side_effect = [error_response, success_response]

        service = _make_service(http_client=mock_client)
        service.RETRY_BACKOFF = 0

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

        assert result.voice_id == "v-retry-ok"
        assert mock_client.request.call_count == 2

    def test_max_retries_exhausted(self) -> None:
        """达到最大重试次数后抛出异常。"""
        mock_client = MagicMock(spec=httpx.Client)
        mock_client.request.return_value = _mock_response(status_code=503)

        service = _make_service(http_client=mock_client)
        service.RETRY_BACKOFF = 0

        with pytest.raises(CosyVoiceError):
            service.clone_voice(audio_url="https://example.com/audio.mp3")

        assert mock_client.request.call_count == service.MAX_RETRIES
