"""TTS 相关领域模块单测 — text_splitter + tts_config + tts_job."""

from __future__ import annotations

import pytest

from packages.application.tts_job.text_splitter import split_text
from packages.domain.tts_config import TtsConfig
from packages.domain.tts_job import TERMINAL_STATUSES, TTSJob, TTSJobStatus

# ═══════════════════════════════════════════════════════════════════════════════
# text_splitter 文本分段
# ═══════════════════════════════════════════════════════════════════════════════


class TestSplitTextBasic:
    """基础分段功能."""

    def test_empty_text_returns_empty_list(self):
        assert split_text("", max_chars=500) == []

    def test_whitespace_only_returns_empty_list(self):
        assert split_text("   \n\n  ", max_chars=500) == []

    def test_short_text_returns_single_segment(self):
        text = "你好世界"
        result = split_text(text, max_chars=500)
        assert len(result) == 1
        assert result[0] == text

    def test_text_equal_to_max_chars_single_segment(self):
        text = "a" * 500
        result = split_text(text, max_chars=500)
        assert len(result) == 1
        assert len(result[0]) == 500

    def test_none_max_chars_uses_default(self):
        """默认 max_chars=500."""
        text = "你好"
        result = split_text(text)  # 使用默认值
        assert len(result) == 1


class TestSplitTextSentenceBoundary:
    """按句子边界分段."""

    def test_splits_at_period(self):
        text = "第一句。第二句。第三句。"
        result = split_text(text, max_chars=10)
        # 每句都比较短，会在句子边界处合并
        assert len(result) >= 2
        assert "".join(result) == text.strip()

    def test_splits_at_question_mark(self):
        text = "你是谁？我是AI。你好吗？很好。"
        result = split_text(text, max_chars=15)
        assert len(result) >= 2
        assert "".join(result) == text.strip()

    def test_splits_at_exclamation_mark(self):
        text = "太棒了！真厉害！好厉害！"
        result = split_text(text, max_chars=10)
        assert len(result) >= 2
        assert "".join(result) == text.strip()

    def test_splits_at_newline(self):
        text = "第一段\n第二段\n第三段"
        result = split_text(text, max_chars=10)
        assert len(result) >= 2

    def test_splits_at_semicolon(self):
        text = "第一部分；第二部分；第三部分。"
        result = split_text(text, max_chars=15)
        assert len(result) >= 1
        assert "".join(result) == text.strip()


class TestSplitTextLongSentence:
    """长句子（超过 max_chars）强制切段."""

    def test_very_long_sentence_hard_cut(self):
        """单个超长句子会被强制切段."""
        text = "我" * 600  # 没有标点
        result = split_text(text, max_chars=500)
        assert len(result) >= 2
        total = sum(len(seg) for seg in result)
        assert total == len(text)

    def test_each_segment_leq_max_chars(self):
        """每个分段都不超过 max_chars."""
        text = "测试句子。" * 100
        result = split_text(text, max_chars=100)
        for seg in result:
            assert len(seg) <= 100

    def test_no_empty_segments(self):
        """不产生空分段."""
        text = "测试。" * 50
        result = split_text(text, max_chars=50)
        for seg in result:
            assert len(seg) > 0


class TestSplitTextMergeShortSegments:
    """合并过短的分段."""

    def test_short_segments_get_merged(self):
        """< 50 字符的段会被合并（如果不超限）."""
        # 多个短句子，应该会被合并
        text = "你好。我是。他是。她是。它是。"
        result = split_text(text, max_chars=50)
        # 每段6字符左右，应该被合并成一段
        assert len(result) < 5

    def test_last_short_segment_merged_to_previous(self):
        """最后一段如果很短，会合并到前一段."""
        text = "a" * 48 + "。" + "b" * 48 + "。" + "cc"
        result = split_text(text, max_chars=100)
        # 最后的 "cc" 很短，应该被合并
        assert result[-1] != "cc"


class TestSplitTextEdgeCases:
    """边界情况."""

    def test_single_character(self):
        result = split_text("一", max_chars=500)
        assert result == ["一"]

    def test_only_punctuation(self):
        text = "。。。"
        result = split_text(text, max_chars=500)
        assert len(result) == 1

    def test_mixed_chinese_english(self):
        text = "Hello世界。Hello世界。" * 20
        result = split_text(text, max_chars=50)
        assert len(result) >= 2
        assert "".join(result) == text.strip()

    def test_max_chars_one(self):
        """极端情况：max_chars=1."""
        text = "abc"
        result = split_text(text, max_chars=1)
        assert len(result) == 3
        assert result == ["a", "b", "c"]


# ═══════════════════════════════════════════════════════════════════════════════
# TtsConfig 配置解析 + 边界钳制
# ═══════════════════════════════════════════════════════════════════════════════


class TestTtsConfigDefaults:
    """默认值."""

    def test_default_config_disabled(self):
        config = TtsConfig()
        assert config.enabled is False
        assert config.voice_id == ""
        assert config.speed == 1.0
        assert config.pitch == 0.0
        assert config.volume == 0.8
        assert config.text == ""
        assert config.align_mode == "full"
        assert config.overlap_mode == "replace"

    def test_parse_none_returns_default(self):
        config = TtsConfig.parse(None)
        assert config.enabled is False

    def test_parse_empty_dict_returns_default(self):
        config = TtsConfig.parse({})
        assert config.enabled is False

    def test_parse_non_dict_returns_default(self):
        config = TtsConfig.parse("not a dict")
        assert config.enabled is False


class TestTtsConfigParseEnabled:
    """enabled 字段解析."""

    def test_parse_enabled_true(self):
        config = TtsConfig.parse({"enabled": True})
        assert config.enabled is True

    def test_parse_enabled_false(self):
        config = TtsConfig.parse({"enabled": False})
        assert config.enabled is False

    def test_parse_enabled_invalid_type(self):
        """enabled 不是 bool 时回退到 False."""
        config = TtsConfig.parse({"enabled": "true"})
        assert config.enabled is False

    def test_disabled_ignores_other_fields(self):
        """enabled=False 时其他字段都用默认值."""
        config = TtsConfig.parse(
            {
                "enabled": False,
                "voice_id": "test",
                "speed": 2.0,
            }
        )
        assert config.enabled is False
        assert config.voice_id == ""
        assert config.speed == 1.0


class TestTtsConfigParseFields:
    """各字段解析."""

    def test_parse_voice_id(self):
        config = TtsConfig.parse({"enabled": True, "voice_id": "voice_001"})
        assert config.voice_id == "voice_001"

    def test_parse_voice_id_invalid_type(self):
        config = TtsConfig.parse({"enabled": True, "voice_id": 123})
        assert config.voice_id == ""

    def test_parse_speed(self):
        config = TtsConfig.parse({"enabled": True, "speed": 1.5})
        assert config.speed == 1.5

    def test_parse_speed_int(self):
        config = TtsConfig.parse({"enabled": True, "speed": 2})
        assert config.speed == 2.0

    def test_parse_speed_invalid_type(self):
        config = TtsConfig.parse({"enabled": True, "speed": "fast"})
        assert config.speed == 1.0

    def test_parse_pitch(self):
        config = TtsConfig.parse({"enabled": True, "pitch": 5})
        assert config.pitch == 5.0

    def test_parse_pitch_invalid_type(self):
        config = TtsConfig.parse({"enabled": True, "pitch": "high"})
        assert config.pitch == 0.0

    def test_parse_volume(self):
        config = TtsConfig.parse({"enabled": True, "volume": 0.5})
        assert config.volume == 0.5

    def test_parse_volume_invalid_type(self):
        config = TtsConfig.parse({"enabled": True, "volume": "loud"})
        assert config.volume == 0.8

    def test_parse_text(self):
        config = TtsConfig.parse({"enabled": True, "text": "你好世界"})
        assert config.text == "你好世界"

    def test_parse_text_invalid_type(self):
        config = TtsConfig.parse({"enabled": True, "text": 12345})
        assert config.text == ""

    def test_parse_align_mode_valid(self):
        config = TtsConfig.parse({"enabled": True, "align_mode": "subtitle"})
        assert config.align_mode == "subtitle"

    def test_parse_align_mode_invalid(self):
        config = TtsConfig.parse({"enabled": True, "align_mode": "invalid"})
        assert config.align_mode == "full"

    def test_parse_overlap_mode_mix(self):
        config = TtsConfig.parse({"enabled": True, "overlap_mode": "mix"})
        assert config.overlap_mode == "mix"

    def test_parse_overlap_mode_invalid(self):
        config = TtsConfig.parse({"enabled": True, "overlap_mode": "invalid"})
        assert config.overlap_mode == "replace"


class TestTtsConfigClamp:
    """边界钳制."""

    def test_speed_below_minimum_clamped(self):
        config = TtsConfig.parse({"enabled": True, "speed": 0.1})
        assert config.speed == 0.5

    def test_speed_above_maximum_clamped(self):
        config = TtsConfig.parse({"enabled": True, "speed": 3.0})
        assert config.speed == 2.0

    def test_speed_at_minimum_ok(self):
        config = TtsConfig.parse({"enabled": True, "speed": 0.5})
        assert config.speed == 0.5

    def test_speed_at_maximum_ok(self):
        config = TtsConfig.parse({"enabled": True, "speed": 2.0})
        assert config.speed == 2.0

    def test_pitch_below_minimum_clamped(self):
        config = TtsConfig.parse({"enabled": True, "pitch": -20})
        assert config.pitch == -12

    def test_pitch_above_maximum_clamped(self):
        config = TtsConfig.parse({"enabled": True, "pitch": 20})
        assert config.pitch == 12

    def test_pitch_at_minimum_ok(self):
        config = TtsConfig.parse({"enabled": True, "pitch": -12})
        assert config.pitch == -12

    def test_pitch_at_maximum_ok(self):
        config = TtsConfig.parse({"enabled": True, "pitch": 12})
        assert config.pitch == 12

    def test_volume_below_minimum_clamped(self):
        config = TtsConfig.parse({"enabled": True, "volume": -0.5})
        assert config.volume == 0.0

    def test_volume_above_maximum_clamped(self):
        config = TtsConfig.parse({"enabled": True, "volume": 2.0})
        assert config.volume == 1.0

    def test_volume_at_minimum_ok(self):
        config = TtsConfig.parse({"enabled": True, "volume": 0.0})
        assert config.volume == 0.0

    def test_volume_at_maximum_ok(self):
        config = TtsConfig.parse({"enabled": True, "volume": 1.0})
        assert config.volume == 1.0


# ═══════════════════════════════════════════════════════════════════════════════
# TTSJob 领域模型 — 状态机
# ═══════════════════════════════════════════════════════════════════════════════


class TestTTSJobCreate:
    """创建任务."""

    def test_create_basic(self):
        job = TTSJob.create(user_id="user1", input_text="你好")
        assert job.id is not None
        assert len(job.id) > 0
        assert job.user_id == "user1"
        assert job.input_text == "你好"
        assert job.status == TTSJobStatus.PENDING
        assert job.retry_count == 0
        assert job.max_retries == 3
        assert job.started_at is None
        assert job.completed_at is None

    def test_create_with_voice_id(self):
        job = TTSJob.create(user_id="user1", input_text="你好", voice_id="voice_001")
        assert job.voice_id == "voice_001"

    def test_create_with_project_id(self):
        job = TTSJob.create(user_id="user1", input_text="你好", project_id="proj_001")
        assert job.project_id == "proj_001"

    def test_create_with_voice_clone_profile_id(self):
        job = TTSJob.create(
            user_id="user1",
            input_text="你好",
            voice_clone_profile_id="clone_001",
        )
        assert job.voice_clone_profile_id == "clone_001"

    def test_create_with_custom_max_retries(self):
        job = TTSJob.create(user_id="user1", input_text="你好", max_retries=5)
        assert job.max_retries == 5

    def test_create_with_format(self):
        job = TTSJob.create(user_id="user1", input_text="你好", format="wav")
        assert job.format == "wav"

    def test_create_with_sample_rate(self):
        job = TTSJob.create(user_id="user1", input_text="你好", sample_rate=44100)
        assert job.sample_rate == 44100


class TestTTSJobStatusProperties:
    """状态查询属性."""

    def test_pending_not_terminal(self):
        job = TTSJob.create(user_id="u1", input_text="hi")
        assert job.is_terminal is False

    def test_processing_not_terminal(self):
        job = TTSJob.create(user_id="u1", input_text="hi")
        job.mark_processing()
        assert job.is_terminal is False

    def test_completed_is_terminal(self):
        job = TTSJob.create(user_id="u1", input_text="hi")
        job.mark_processing()
        job.mark_completed(output_audio_url="url")
        assert job.is_terminal is True
        assert job.is_completed is True

    def test_failed_is_terminal(self):
        job = TTSJob.create(user_id="u1", input_text="hi")
        job.mark_processing()
        job.mark_failed("error")
        assert job.is_terminal is True
        assert job.status == TTSJobStatus.FAILED

    def test_cancelled_is_terminal(self):
        job = TTSJob.create(user_id="u1", input_text="hi")
        job.mark_cancelled()
        assert job.is_terminal is True

    def test_terminal_statuses_contains_all_three(self):
        assert TTSJobStatus.COMPLETED in TERMINAL_STATUSES
        assert TTSJobStatus.FAILED in TERMINAL_STATUSES
        assert TTSJobStatus.CANCELLED in TERMINAL_STATUSES


class TestTTSJobTransitions:
    """状态转换."""

    def test_pending_to_processing(self):
        job = TTSJob.create(user_id="u1", input_text="hi")
        job.mark_processing()
        assert job.status == TTSJobStatus.PROCESSING
        assert job.started_at is not None
        assert job.error_message == ""

    def test_pending_to_failed(self):
        job = TTSJob.create(user_id="u1", input_text="hi")
        job.mark_failed("网络错误")
        assert job.status == TTSJobStatus.FAILED
        assert job.error_message == "网络错误"

    def test_pending_to_cancelled(self):
        job = TTSJob.create(user_id="u1", input_text="hi")
        job.mark_cancelled()
        assert job.status == TTSJobStatus.CANCELLED

    def test_processing_to_completed(self):
        job = TTSJob.create(user_id="u1", input_text="hi")
        job.mark_processing()
        job.mark_completed(output_audio_url="https://example.com/audio.mp3")
        assert job.status == TTSJobStatus.COMPLETED
        assert job.output_audio_url == "https://example.com/audio.mp3"
        assert job.completed_at is not None

    def test_processing_to_failed(self):
        job = TTSJob.create(user_id="u1", input_text="hi")
        job.mark_processing()
        job.mark_failed("API超时")
        assert job.status == TTSJobStatus.FAILED
        assert job.error_message == "API超时"

    def test_processing_to_cancelled(self):
        job = TTSJob.create(user_id="u1", input_text="hi")
        job.mark_processing()
        job.mark_cancelled()
        assert job.status == TTSJobStatus.CANCELLED

    def test_failed_to_pending_retry(self):
        job = TTSJob.create(user_id="u1", input_text="hi")
        job.mark_failed("error")
        job.prepare_retry()
        assert job.status == TTSJobStatus.PENDING
        assert job.retry_count == 1
        assert job.error_message == ""
        assert job.started_at is None

    def test_completed_cannot_transition_back(self):
        job = TTSJob.create(user_id="u1", input_text="hi")
        job.mark_processing()
        job.mark_completed(output_audio_url="url")
        with pytest.raises(ValueError, match="非法状态转换"):
            job.mark_failed("test")

    def test_cancelled_cannot_retry(self):
        job = TTSJob.create(user_id="u1", input_text="hi")
        job.mark_cancelled()
        with pytest.raises(ValueError, match="不可重试"):
            job.prepare_retry()

    def test_mark_failed_sets_error_message(self):
        job = TTSJob.create(user_id="u1", input_text="hi")
        job.mark_failed("服务端错误")
        assert job.error_message == "服务端错误"

    def test_failed_status_after_mark_failed(self):
        job = TTSJob.create(user_id="u1", input_text="hi")
        job.mark_processing()
        job.mark_failed("API超时")
        assert job.status == TTSJobStatus.FAILED
        assert job.error_message == "API超时"


class TestTTSJobRetry:
    """重试逻辑."""

    def test_retry_increments_retry_count(self):
        job = TTSJob.create(user_id="u1", input_text="hi")
        job.mark_failed("error1")
        job.prepare_retry()
        assert job.retry_count == 1

        job.mark_processing()
        job.mark_failed("error2")
        job.prepare_retry()
        assert job.retry_count == 2

    def test_retry_clears_error_message(self):
        job = TTSJob.create(user_id="u1", input_text="hi")
        job.mark_failed("error")
        job.prepare_retry()
        assert job.error_message == ""

    def test_retry_resets_timestamps(self):
        job = TTSJob.create(user_id="u1", input_text="hi")
        job.mark_processing()
        job.mark_failed("error")
        job.prepare_retry()
        assert job.started_at is None

    def test_can_retry_while_below_max_retries(self):
        job = TTSJob.create(user_id="u1", input_text="hi", max_retries=2)
        # 第一次失败+重试
        job.mark_failed("e1")
        job.prepare_retry()
        assert job.retry_count == 1
        # 第二次失败+重试
        job.mark_processing()
        job.mark_failed("e2")
        job.prepare_retry()
        assert job.retry_count == 2

    def test_cannot_retry_when_exceeded_max_retries(self):
        job = TTSJob.create(user_id="u1", input_text="hi", max_retries=1)
        job.mark_failed("e1")
        job.prepare_retry()
        assert job.retry_count == 1
        # 再次失败就不能重试了（已经用完1次重试）
        job.mark_processing()
        job.mark_failed("e2")
        with pytest.raises(ValueError, match="不可重试"):
            job.prepare_retry()

    def test_is_retryable_true_when_failed_and_under_limit(self):
        job = TTSJob.create(user_id="u1", input_text="hi", max_retries=3)
        job.mark_failed("error")
        assert job.is_retryable is True

    def test_is_retryable_false_when_not_failed(self):
        job = TTSJob.create(user_id="u1", input_text="hi")
        assert job.is_retryable is False

    def test_prepare_retry_fails_when_not_failed(self):
        job = TTSJob.create(user_id="u1", input_text="hi")
        with pytest.raises(ValueError, match="不可重试"):
            job.prepare_retry()


class TestTTSJobCompleted:
    """完成时的字段."""

    def test_mark_completed_sets_output_url(self):
        job = TTSJob.create(user_id="u1", input_text="hi")
        job.mark_processing()
        job.mark_completed(
            output_audio_url="https://cdn.example.com/audio.mp3",
            output_audio_key="audio/xxx.mp3",
            duration=10.5,
            file_size=102400,
        )
        assert job.output_audio_url == "https://cdn.example.com/audio.mp3"
        assert job.output_audio_key == "audio/xxx.mp3"
        assert job.duration == 10.5
        assert job.file_size == 102400
        assert job.completed_at is not None

    def test_mark_completed_default_values(self):
        job = TTSJob.create(user_id="u1", input_text="hi")
        job.mark_processing()
        job.mark_completed(output_audio_url="url")
        assert job.duration == 0.0
        assert job.file_size == 0


class TestTTSJobMetadata:
    """元数据."""

    def test_create_with_metadata(self):
        meta = {"source": "api", "priority": "high"}
        job = TTSJob.create(user_id="u1", input_text="hi", metadata=meta)
        assert job.metadata["source"] == "api"
        assert job.metadata["priority"] == "high"

    def test_default_metadata_empty_dict(self):
        job = TTSJob.create(user_id="u1", input_text="hi")
        assert job.metadata == {}


class TestTTSJobCreateValidation:
    """创建时的参数校验."""

    def test_empty_user_id_raises(self):
        with pytest.raises(ValueError, match="user_id"):
            TTSJob.create(user_id="", input_text="hi")

    def test_whitespace_user_id_raises(self):
        with pytest.raises(ValueError, match="user_id"):
            TTSJob.create(user_id="   ", input_text="hi")

    def test_empty_input_text_raises(self):
        with pytest.raises(ValueError, match="input_text"):
            TTSJob.create(user_id="u1", input_text="")

    def test_input_text_too_long_raises(self):
        long_text = "a" * 10001
        with pytest.raises(ValueError, match="10000"):
            TTSJob.create(user_id="u1", input_text=long_text)

    def test_invalid_format_raises(self):
        with pytest.raises(ValueError, match="不支持的输出格式"):
            TTSJob.create(user_id="u1", input_text="hi", format="flac")

    def test_valid_format_wav(self):
        job = TTSJob.create(user_id="u1", input_text="hi", format="wav")
        assert job.format == "wav"

    def test_valid_format_pcm(self):
        job = TTSJob.create(user_id="u1", input_text="hi", format="pcm")
        assert job.format == "pcm"

    def test_input_text_stripped(self):
        job = TTSJob.create(user_id="u1", input_text="  你好  ")
        assert job.input_text == "你好"


class TestTTSJobToDict:
    """序列化."""

    def test_to_dict_contains_key_fields(self):
        job = TTSJob.create(user_id="u1", input_text="hi", voice_id="v1")
        d = job.to_dict()
        assert d["id"] == job.id
        assert d["user_id"] == "u1"
        assert d["input_text"] == "hi"
        assert d["voice_id"] == "v1"
        assert d["status"] == "pending"
        assert d["retry_count"] == 0
        assert d["is_retryable"] is False

    def test_to_dict_completed_status(self):
        job = TTSJob.create(user_id="u1", input_text="hi")
        job.mark_processing()
        job.mark_completed(output_audio_url="https://example.com/a.mp3", duration=10.5, file_size=1024)
        d = job.to_dict()
        assert d["status"] == "completed"
        assert d["output_audio_url"] == "https://example.com/a.mp3"
        assert d["duration"] == 10.5
        assert d["file_size"] == 1024
        assert d["is_completed"] is True
        assert d["started_at"] is not None
        assert d["completed_at"] is not None

    def test_to_dict_failed_status(self):
        job = TTSJob.create(user_id="u1", input_text="hi")
        job.mark_failed("error msg")
        d = job.to_dict()
        assert d["status"] == "failed"
        assert d["error_message"] == "error msg"
        assert d["is_retryable"] is True


class TestTTSJobCompletedValidation:
    """完成时的校验."""

    def test_mark_completed_empty_url_raises(self):
        job = TTSJob.create(user_id="u1", input_text="hi")
        job.mark_processing()
        with pytest.raises(ValueError, match="output_audio_url"):
            job.mark_completed(output_audio_url="")

    def test_is_completed_requires_url(self):
        """is_completed 属性需要 output_audio_url."""
        job = TTSJob.create(user_id="u1", input_text="hi")
        job.mark_processing()
        # 直接设置状态为 completed 但不给 URL（模拟异常情况）
        # 正常流程 mark_completed 会校验 URL，所以这里不会出现
        # 但确认属性逻辑：没有 URL 时 is_completed 为 False
        job.output_audio_url = ""
        # 直接绕过状态机
        from packages.domain.tts_job import _VALID_TRANSITIONS  # noqa

        job.status = TTSJobStatus.COMPLETED
        assert job.is_completed is False
