"""最后一公里：TTS 合成结果保存到配音库 单元测试。

覆盖：
- 正常保存已完成 TTS job 到配音库
- 自动携带元信息（音色名、时长、语速等）
- 自定义名称
- TTS job 不存在 → 404
- TTS job 未完成 → 400
- 配音库配额已满 → 429
"""

from __future__ import annotations

from datetime import datetime, timezone
from unittest.mock import MagicMock

import pytest

from packages.domain.tts_job import TTSJob, TTSJobStatus
from packages.domain.voice_library import VoiceLibraryItem


def _make_completed_job(**kwargs) -> TTSJob:
    """构造一个已完成的 TTSJob。"""
    defaults = {
        "id": "tts_job_001",
        "user_id": "user_001",
        "input_text": "你好世界",
        "voice_id": "voice_001",
        "voice_model": "CosyVoice-v1",
        "project_id": "proj_001",
        "voice_clone_profile_id": "",
        "status": TTSJobStatus.COMPLETED,
        "output_audio_url": "https://oss.example.com/audio.mp3",
        "output_audio_key": "tts-outputs/user_001/tts_job_001.mp3",
        "duration": 5.5,
        "file_size": 88000,
        "sample_rate": 22050,
        "format": "mp3",
        "error_message": "",
        "retry_count": 0,
        "max_retries": 3,
        "metadata": {"speed": 1.0, "language": "zh-CN"},
        "started_at": datetime(2026, 7, 7, 10, 0, 0, tzinfo=timezone.utc),
        "completed_at": datetime(2026, 7, 7, 10, 0, 5, tzinfo=timezone.utc),
        "created_at": datetime(2026, 7, 7, 10, 0, 0, tzinfo=timezone.utc),
        "updated_at": datetime(2026, 7, 7, 10, 0, 5, tzinfo=timezone.utc),
    }
    defaults.update(kwargs)
    return TTSJob(**defaults)


def _make_voice_library_item(**kwargs) -> VoiceLibraryItem:
    """构造一个配音库条目。"""
    defaults = {
        "id": "voice_lib_001",
        "user_id": "user_001",
        "name": "TTS-tts_job_",
        "text": "你好世界",
        "voice_provider": "cosyvoice",
        "voice_id": "voice_001",
        "voice_name": "CosyVoice-v1",
        "audio_url": "https://oss.example.com/audio.mp3",
        "duration": 5.5,
        "file_size": 88000,
        "status": "completed",
        "project_id": "proj_001",
        "tags": [],
        "metadata_": {
            "source": "tts_job",
            "tts_job_id": "tts_job_001",
            "format": "mp3",
            "sample_rate": 22050,
            "speed": 1.0,
            "language": "zh-CN",
        },
        "created_at": datetime(2026, 7, 7, 10, 1, 0, tzinfo=timezone.utc),
        "updated_at": datetime(2026, 7, 7, 10, 1, 0, tzinfo=timezone.utc),
    }
    defaults.update(kwargs)
    return VoiceLibraryItem(**defaults)


class TestSaveToLibraryMapping:
    """测试 TTSJob → VoiceLibraryItem 字段映射。"""

    def test_completed_job_maps_correctly(self) -> None:
        """已完成的 TTS job 字段正确映射到配音库条目。"""
        job = _make_completed_job()

        # 验证 is_completed 属性
        assert job.is_completed is True

        # 验证关键字段映射
        assert job.output_audio_url == "https://oss.example.com/audio.mp3"
        assert job.duration == 5.5
        assert job.file_size == 88000
        assert job.voice_id == "voice_001"
        assert job.voice_model == "CosyVoice-v1"
        assert job.input_text == "你好世界"
        assert job.format == "mp3"
        assert job.sample_rate == 22050

    def test_metadata_carries_speed_and_format(self) -> None:
        """元信息携带语速、格式等。"""
        job = _make_completed_job()

        metadata = {
            "source": "tts_job",
            "tts_job_id": job.id,
            "format": job.format,
            "sample_rate": job.sample_rate,
        }
        if job.metadata:
            for key in ("speed", "language"):
                if key in job.metadata:
                    metadata[key] = job.metadata[key]

        assert metadata["format"] == "mp3"
        assert metadata["sample_rate"] == 22050
        assert metadata["speed"] == 1.0
        assert metadata["language"] == "zh-CN"
        assert metadata["source"] == "tts_job"

    def test_name_auto_generated_when_empty(self) -> None:
        """未提供名称时自动生成。"""
        job = _make_completed_job()
        name = None  # 模拟未提供名称
        generated_name = name or f"TTS-{job.id[:8]}"
        assert generated_name == "TTS-tts_job_"

    def test_name_uses_custom_when_provided(self) -> None:
        """提供自定义名称时使用自定义名称。"""
        custom_name = "我的配音"
        generated_name = custom_name or "TTS-fallback"
        assert generated_name == "我的配音"


class TestSaveToLibraryNotCompleted:
    """测试未完成 job 不能保存。"""

    def test_pending_job_not_completed(self) -> None:
        """pending 状态的 job 不能保存。"""
        job = _make_completed_job(status=TTSJobStatus.PENDING)
        assert job.is_completed is False

    def test_processing_job_not_completed(self) -> None:
        """processing 状态的 job 不能保存。"""
        job = _make_completed_job(status=TTSJobStatus.PROCESSING)
        assert job.is_completed is False

    def test_failed_job_not_completed(self) -> None:
        """failed 状态的 job 不能保存。"""
        job = _make_completed_job(status=TTSJobStatus.FAILED)
        assert job.is_completed is False

    def test_completed_without_url_not_completed(self) -> None:
        """status=completed 但没有 audio_url 的 job 不算完成。"""
        job = _make_completed_job(
            status=TTSJobStatus.COMPLETED,
            output_audio_url="",
        )
        assert job.is_completed is False


class TestSaveToLibraryQuota:
    """测试配额检查。"""

    def test_quota_exceeded_raises(self) -> None:
        """配音库配额已满时抛出 QuotaExceededError。"""
        from packages.application.voice_library.use_cases import QuotaExceededError

        error = QuotaExceededError(dimension="max_voiceovers", limit=10, used=10)
        assert "10/10" in str(error)

    def test_quota_under_limit_passes(self) -> None:
        """配额未满时不报错。"""
        from packages.domain.quota import QuotaDimension, quota_checker

        result = quota_checker.check("free", QuotaDimension.MAX_VOICEOVERS.value, 5)
        assert result.allowed is True


class TestSaveToLibraryCreateCommand:
    """测试 CreateVoiceLibraryCommand 构建。"""

    def test_command_fields_from_tts_job(self) -> None:
        """从 TTSJob 构建的 Command 字段正确。"""
        from packages.application.voice_library.commands import CreateVoiceLibraryCommand

        job = _make_completed_job()
        metadata_ = {
            "source": "tts_job",
            "tts_job_id": job.id,
            "format": job.format,
            "sample_rate": job.sample_rate,
            "speed": 1.0,
            "language": "zh-CN",
        }

        command = CreateVoiceLibraryCommand(
            user_id=job.user_id,
            name=f"TTS-{job.id[:8]}",
            text=job.input_text,
            voice_provider="cosyvoice",
            voice_id=job.voice_id,
            voice_name=job.voice_model,
            audio_url=job.output_audio_url,
            duration=job.duration,
            file_size=job.file_size,
            status="completed",
            project_id=job.project_id,
            tags=[],
            metadata_=metadata_,
        )

        assert command.user_id == "user_001"
        assert command.name == "TTS-tts_job_"
        assert command.text == "你好世界"
        assert command.voice_provider == "cosyvoice"
        assert command.voice_id == "voice_001"
        assert command.voice_name == "CosyVoice-v1"
        assert command.audio_url == "https://oss.example.com/audio.mp3"
        assert command.duration == 5.5
        assert command.file_size == 88000
        assert command.status == "completed"
        assert command.project_id == "proj_001"
        assert command.metadata_["source"] == "tts_job"

    def test_command_with_empty_project_id(self) -> None:
        """project_id 为空时传空字符串。"""
        from packages.application.voice_library.commands import CreateVoiceLibraryCommand

        job = _make_completed_job(project_id="")

        command = CreateVoiceLibraryCommand(
            user_id=job.user_id,
            name="test",
            text=job.input_text,
            voice_provider="cosyvoice",
            voice_id=job.voice_id,
            voice_name="",
            audio_url=job.output_audio_url,
            duration=job.duration,
            file_size=job.file_size,
            status="completed",
            project_id=job.project_id or "",
            tags=[],
            metadata_={},
        )

        assert command.project_id == ""

    def test_command_voice_name_fallback(self) -> None:
        """voice_model 为空时 voice_name 回退为空字符串。"""
        from packages.application.voice_library.commands import CreateVoiceLibraryCommand

        job = _make_completed_job(voice_model="")

        command = CreateVoiceLibraryCommand(
            user_id=job.user_id,
            name="test",
            text=job.input_text,
            voice_provider="cosyvoice",
            voice_id=job.voice_id,
            voice_name=job.voice_model or "",
            audio_url=job.output_audio_url,
            duration=job.duration,
            file_size=job.file_size,
            status="completed",
            project_id="",
            tags=[],
            metadata_={},
        )

        assert command.voice_name == ""


class TestSaveToLibraryUseCase:
    """测试 CreateVoiceLibraryUseCase 调用。"""

    def test_use_case_creates_item(self) -> None:
        """UseCase 正确创建配音库条目。"""
        from packages.application.voice_library.commands import CreateVoiceLibraryCommand
        from packages.application.voice_library.use_cases import CreateVoiceLibraryUseCase

        repo = MagicMock()
        repo.count_by_user.return_value = 0  # 配额未满

        repo.create.side_effect = lambda item: item

        use_case = CreateVoiceLibraryUseCase(repo)
        command = CreateVoiceLibraryCommand(
            user_id="user_001",
            name="test",
            text="你好",
            voice_provider="cosyvoice",
            voice_id="v1",
            voice_name="Voice1",
            audio_url="https://example.com/audio.mp3",
            duration=3.0,
            file_size=5000,
            status="completed",
            project_id="",
            tags=[],
            metadata_={},
        )

        item = use_case.execute(command, plan_name="free")

        repo.create.assert_called_once()
        assert item is not None

    def test_use_case_quota_exceeded(self) -> None:
        """UseCase 配额已满时抛出 QuotaExceededError。"""
        from packages.application.voice_library.commands import CreateVoiceLibraryCommand
        from packages.application.voice_library.use_cases import (
            CreateVoiceLibraryUseCase,
            QuotaExceededError,
        )

        repo = MagicMock()
        repo.count_by_user.return_value = 100  # 超过 premium 配额

        use_case = CreateVoiceLibraryUseCase(repo)
        command = CreateVoiceLibraryCommand(
            user_id="user_001",
            name="test",
            text="你好",
            voice_provider="cosyvoice",
            voice_id="v1",
            voice_name="Voice1",
            audio_url="https://example.com/audio.mp3",
            duration=3.0,
            file_size=5000,
            status="completed",
            project_id="",
            tags=[],
            metadata_={},
        )

        with pytest.raises(QuotaExceededError):
            use_case.execute(command, plan_name="premium")
