"""TTSJob 领域模型单元测试 — Phase 3 CosyVoice 集成."""

from __future__ import annotations

import pytest

from packages.domain.tts_job import TTSJob, TTSJobStatus


class TestTTSJobCreate:
    """测试 TTSJob.create() 工厂方法。"""

    def test_create_success(self) -> None:
        """正常创建 TTS 任务。"""
        job = TTSJob.create(
            user_id="user_001",
            input_text="这是一段测试文本",
            voice_id="longxiaochun",
            voice_model="cosyvoice-v1",
            project_id="project_001",
            voice_clone_profile_id="profile_001",
            sample_rate=22050,
            format="mp3",
        )

        assert job.id
        assert job.user_id == "user_001"
        assert job.input_text == "这是一段测试文本"
        assert job.voice_id == "longxiaochun"
        assert job.voice_model == "cosyvoice-v1"
        assert job.project_id == "project_001"
        assert job.voice_clone_profile_id == "profile_001"
        assert job.status == TTSJobStatus.PENDING
        assert job.sample_rate == 22050
        assert job.format == "mp3"
        assert job.retry_count == 0
        assert job.max_retries == 3

    def test_create_minimal(self) -> None:
        """使用最小参数创建。"""
        job = TTSJob.create(user_id="user_001", input_text="测试文本")

        assert job.user_id == "user_001"
        assert job.input_text == "测试文本"
        assert job.status == TTSJobStatus.PENDING
        assert job.voice_id == ""
        assert job.project_id == ""
        assert job.voice_clone_profile_id == ""
        assert job.format == "mp3"

    def test_create_empty_user_id_raises(self) -> None:
        """空 user_id 应抛出 ValueError。"""
        with pytest.raises(ValueError, match="user_id 不能为空"):
            TTSJob.create(user_id="", input_text="测试")

    def test_create_empty_input_text_raises(self) -> None:
        """空 input_text 应抛出 ValueError。"""
        with pytest.raises(ValueError, match="input_text 不能为空"):
            TTSJob.create(user_id="user_001", input_text="")

    def test_create_whitespace_input_text_raises(self) -> None:
        """空白 input_text 应抛出 ValueError。"""
        with pytest.raises(ValueError, match="input_text 不能为空"):
            TTSJob.create(user_id="user_001", input_text="   ")

    def test_create_input_text_too_long_raises(self) -> None:
        """input_text 超过 10000 字符应抛出 ValueError。"""
        with pytest.raises(ValueError, match="input_text 长度不能超过 10000 字符"):
            TTSJob.create(user_id="user_001", input_text="a" * 10001)

    def test_create_invalid_format_raises(self) -> None:
        """不支持的输出格式应抛出 ValueError。"""
        with pytest.raises(ValueError, match="不支持的输出格式"):
            TTSJob.create(user_id="user_001", input_text="测试", format="aac")

    def test_create_valid_formats(self) -> None:
        """所有支持的格式都应正常创建。"""
        for fmt in ("mp3", "wav", "pcm"):
            job = TTSJob.create(user_id="user_001", input_text="测试", format=fmt)
            assert job.format == fmt

    def test_create_strips_whitespace(self) -> None:
        """应去除首尾空白。"""
        job = TTSJob.create(
            user_id="  user_001  ",
            input_text="  测试文本  ",
            voice_id="  voice_001  ",
        )

        assert job.user_id == "user_001"
        assert job.input_text == "测试文本"
        assert job.voice_id == "voice_001"


class TestTTSJobStatus:
    """测试状态相关属性和方法。"""

    def test_initial_status_is_pending(self) -> None:
        """初始状态应为 PENDING。"""
        job = TTSJob.create(user_id="user_001", input_text="测试")
        assert job.status == TTSJobStatus.PENDING

    def test_is_terminal_pending(self) -> None:
        """PENDING 不是终态。"""
        job = TTSJob.create(user_id="user_001", input_text="测试")
        assert not job.is_terminal

    def test_is_terminal_completed(self) -> None:
        """COMPLETED 是终态。"""
        job = TTSJob.create(user_id="user_001", input_text="测试")
        job.mark_processing()
        job.mark_completed(output_audio_url="https://example.com/audio.mp3")
        assert job.is_terminal

    def test_is_terminal_failed(self) -> None:
        """FAILED 是终态。"""
        job = TTSJob.create(user_id="user_001", input_text="测试")
        job.mark_processing()
        job.mark_failed("合成失败")
        assert job.is_terminal

    def test_is_terminal_cancelled(self) -> None:
        """CANCELLED 是终态。"""
        job = TTSJob.create(user_id="user_001", input_text="测试")
        job.mark_cancelled()
        assert job.is_terminal

    def test_is_retryable_not_failed(self) -> None:
        """非 FAILED 状态不可重试。"""
        job = TTSJob.create(user_id="user_001", input_text="测试")
        assert not job.is_retryable

    def test_is_retryable_failed_under_limit(self) -> None:
        """FAILED 且未超过重试上限时可重试。"""
        job = TTSJob.create(user_id="user_001", input_text="测试")
        job.mark_processing()
        job.mark_failed("合成失败")
        assert job.is_retryable

    def test_is_retryable_failed_over_limit(self) -> None:
        """超过重试上限时不可重试。"""
        job = TTSJob.create(user_id="user_001", input_text="测试", max_retries=1)
        job.mark_processing()
        job.mark_failed("第一次失败")
        job.prepare_retry()
        job.mark_processing()
        job.mark_failed("第二次失败")
        assert not job.is_retryable

    def test_is_completed_with_url(self) -> None:
        """COMPLETED 且有 output_audio_url 时应返回 True。"""
        job = TTSJob.create(user_id="user_001", input_text="测试")
        job.mark_processing()
        job.mark_completed(output_audio_url="https://example.com/audio.mp3")
        assert job.is_completed

    def test_is_completed_without_url(self) -> None:
        """COMPLETED 但无 output_audio_url 时应返回 False。"""
        job = TTSJob.create(user_id="user_001", input_text="测试")
        job.mark_processing()
        job.status = TTSJobStatus.COMPLETED
        job.output_audio_url = ""
        assert not job.is_completed


class TestTTSJobTransitions:
    """测试状态转换。"""

    def test_mark_processing(self) -> None:
        """PENDING → PROCESSING 转换。"""
        job = TTSJob.create(user_id="user_001", input_text="测试")
        job.mark_processing()
        assert job.status == TTSJobStatus.PROCESSING
        assert job.error_message == ""
        assert job.started_at is not None

    def test_mark_completed(self) -> None:
        """PROCESSING → COMPLETED 转换。"""
        job = TTSJob.create(user_id="user_001", input_text="测试")
        job.mark_processing()
        job.mark_completed(
            output_audio_url="https://example.com/audio.mp3",
            output_audio_key="tts/audio.mp3",
            duration=5.5,
            file_size=102400,
        )
        assert job.status == TTSJobStatus.COMPLETED
        assert job.output_audio_url == "https://example.com/audio.mp3"
        assert job.output_audio_key == "tts/audio.mp3"
        assert job.duration == 5.5
        assert job.file_size == 102400
        assert job.completed_at is not None
        assert job.error_message == ""

    def test_mark_completed_empty_url_raises(self) -> None:
        """mark_completed 空 output_audio_url 应抛出 ValueError。"""
        job = TTSJob.create(user_id="user_001", input_text="测试")
        job.mark_processing()
        with pytest.raises(ValueError, match="output_audio_url 不能为空"):
            job.mark_completed(output_audio_url="")

    def test_mark_completed_minimal(self) -> None:
        """mark_completed 最小参数。"""
        job = TTSJob.create(user_id="user_001", input_text="测试")
        job.mark_processing()
        job.mark_completed(output_audio_url="https://example.com/audio.mp3")
        assert job.status == TTSJobStatus.COMPLETED
        assert job.duration == 0.0
        assert job.file_size == 0

    def test_mark_failed(self) -> None:
        """PROCESSING → FAILED 转换。"""
        job = TTSJob.create(user_id="user_001", input_text="测试")
        job.mark_processing()
        job.mark_failed("API 调用失败")
        assert job.status == TTSJobStatus.FAILED
        assert job.error_message == "API 调用失败"

    def test_mark_cancelled_from_pending(self) -> None:
        """PENDING → CANCELLED 转换。"""
        job = TTSJob.create(user_id="user_001", input_text="测试")
        job.mark_cancelled()
        assert job.status == TTSJobStatus.CANCELLED

    def test_mark_cancelled_from_processing(self) -> None:
        """PROCESSING → CANCELLED 转换。"""
        job = TTSJob.create(user_id="user_001", input_text="测试")
        job.mark_processing()
        job.mark_cancelled()
        assert job.status == TTSJobStatus.CANCELLED

    def test_invalid_transition_raises(self) -> None:
        """非法状态转换应抛出 ValueError。"""
        job = TTSJob.create(user_id="user_001", input_text="测试")
        with pytest.raises(ValueError, match="非法状态转换"):
            job.mark_completed(output_audio_url="https://example.com/audio.mp3")

    def test_invalid_status_string_raises(self) -> None:
        """无效状态字符串应抛出 ValueError。"""
        job = TTSJob.create(user_id="user_001", input_text="测试")
        with pytest.raises(ValueError, match="无效状态"):
            job.transition_to("invalid_status")

    def test_transition_to_with_string(self) -> None:
        """支持字符串形式的状态转换。"""
        job = TTSJob.create(user_id="user_001", input_text="测试")
        job.transition_to("processing")
        assert job.status == TTSJobStatus.PROCESSING


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

    def test_prepare_retry_success(self) -> None:
        """成功重试。"""
        job = TTSJob.create(user_id="user_001", input_text="测试")
        job.mark_processing()
        job.mark_failed("失败")
        job.prepare_retry()

        assert job.status == TTSJobStatus.PENDING
        assert job.retry_count == 1
        assert job.error_message == ""
        assert job.started_at is None
        assert job.completed_at is None

    def test_prepare_retry_not_failed_raises(self) -> None:
        """非 FAILED 状态重试应抛出 ValueError。"""
        job = TTSJob.create(user_id="user_001", input_text="测试")
        with pytest.raises(ValueError, match="不可重试"):
            job.prepare_retry()

    def test_prepare_retry_over_limit_raises(self) -> None:
        """超过重试上限重试应抛出 ValueError。"""
        job = TTSJob.create(user_id="user_001", input_text="测试", max_retries=1)
        job.mark_processing()
        job.mark_failed("第一次失败")
        job.prepare_retry()
        job.mark_processing()
        job.mark_failed("第二次失败")

        with pytest.raises(ValueError, match="不可重试"):
            job.prepare_retry()


class TestTTSJobToDict:
    """测试序列化。"""

    def test_to_dict_contains_all_fields(self) -> None:
        """to_dict 应包含所有字段。"""
        job = TTSJob.create(
            user_id="user_001",
            input_text="测试文本",
            voice_id="longxiaochun",
            voice_model="cosyvoice-v1",
            project_id="project_001",
            voice_clone_profile_id="profile_001",
            sample_rate=22050,
            format="mp3",
            max_retries=5,
            metadata={"key": "value"},
        )

        result = job.to_dict()

        assert result["id"] == job.id
        assert result["user_id"] == "user_001"
        assert result["input_text"] == "测试文本"
        assert result["voice_id"] == "longxiaochun"
        assert result["voice_model"] == "cosyvoice-v1"
        assert result["project_id"] == "project_001"
        assert result["voice_clone_profile_id"] == "profile_001"
        assert result["status"] == "pending"
        assert result["sample_rate"] == 22050
        assert result["format"] == "mp3"
        assert result["retry_count"] == 0
        assert result["max_retries"] == 5
        assert result["is_retryable"] is False
        assert result["is_completed"] is False
        assert result["metadata"] == {"key": "value"}
        assert result["started_at"] is None
        assert result["completed_at"] is None
        assert result["created_at"] is not None
        assert result["updated_at"] is not None

    def test_to_dict_after_completion(self) -> None:
        """任务完成后 to_dict 应反映最新状态。"""
        job = TTSJob.create(user_id="user_001", input_text="测试")
        job.mark_processing()
        job.mark_completed(
            output_audio_url="https://example.com/audio.mp3",
            duration=10.5,
            file_size=204800,
        )

        result = job.to_dict()

        assert result["status"] == "completed"
        assert result["output_audio_url"] == "https://example.com/audio.mp3"
        assert result["duration"] == 10.5
        assert result["file_size"] == 204800
        assert result["is_completed"] is True
        assert result["started_at"] is not None
        assert result["completed_at"] is not None
