"""TTS 配音引擎单元测试."""

from pathlib import Path

import pytest

from apps.worker.video_processing.tts_engine import TtsEngine, VoiceoverResult, VoiceoverSegment
from packages.adapters.tts.mock_tts_service import MockTtsService
from packages.domain.tts_config import TtsConfig
from packages.domain.voice_presets import (
    VoiceGender,
    VoicePreset,
    VoiceStyle,
    get_default_voice,
    get_voice,
    list_voices,
)
from packages.ports.tts_service import TtsError, TtsService

# ─── TtsConfig 配置解析 ────────────────────────────────────


class TestTtsConfig:
    def test_default_values(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(self):
        config = TtsConfig.parse(None)
        assert config.enabled is False

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

    def test_parse_enabled(self):
        config = TtsConfig.parse({"enabled": True, "voice_id": "female_warm", "text": "你好"})
        assert config.enabled is True
        assert config.voice_id == "female_warm"
        assert config.text == "你好"

    def test_parse_not_enabled_ignores_other_fields(self):
        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

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

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

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

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

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

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

    def test_parse_invalid_types(self):
        config = TtsConfig.parse(
            {
                "enabled": True,
                "speed": "fast",
                "pitch": "high",
                "volume": "loud",
                "voice_id": 123,
                "text": 456,
                "align_mode": "invalid",
                "overlap_mode": "invalid",
            }
        )
        assert config.speed == 1.0
        assert config.pitch == 0.0
        assert config.volume == 0.8
        assert config.voice_id == ""
        assert config.text == ""
        assert config.align_mode == "full"
        assert config.overlap_mode == "replace"


# ─── 预设音色库 ───────────────────────────────────────────


class TestPresetVoices:
    def test_list_voices_all(self):
        voices = list_voices()
        assert len(voices) >= 6

    def test_get_voice_existing(self):
        voice = get_voice("female_warm")
        assert voice is not None
        assert voice.voice_id == "female_warm"
        assert voice.name == "温暖女声"
        assert voice.gender == VoiceGender.FEMALE

    def test_get_voice_not_found(self):
        assert get_voice("nonexistent") is None

    def test_get_default_voice(self):
        voice = get_default_voice()
        assert voice is not None
        assert voice.provider == "mock"

    def test_filter_by_gender(self):
        female = list_voices(gender="female")
        assert len(female) >= 2
        for v in female:
            assert v.gender == VoiceGender.FEMALE

        male = list_voices(gender="male")
        assert len(male) >= 2
        for v in male:
            assert v.gender == VoiceGender.MALE

    def test_filter_by_style(self):
        stable = list_voices(style="stable")
        for v in stable:
            assert v.style == VoiceStyle.STABLE

    def test_filter_by_keyword(self):
        result = list_voices(keyword="女声")
        assert len(result) >= 1
        for v in result:
            assert "女" in v.name

    def test_voice_fields(self):
        voice = get_voice("male_stable")
        assert voice is not None
        assert voice.name
        assert voice.voice_id
        assert voice.description
        assert voice.sample_rate > 0


# ─── Mock TTS 服务 ────────────────────────────────────────


class TestMockTtsService:
    def setup_method(self):
        self.service = MockTtsService()

    def test_provider_name(self):
        assert self.service.provider_name == "mock"

    def test_available_voices(self):
        voices = self.service.available_voices()
        assert len(voices) >= 6

    def test_synthesize_success(self, tmp_path):
        output = tmp_path / "test.wav"
        result = self.service.synthesize(
            "测试文本一二三四五",
            voice_id="female_warm",
            speed=1.0,
            output_path=output,
        )
        assert result == output
        assert result.exists()
        assert result.stat().st_size > 0

    def test_synthesize_different_voices(self, tmp_path):
        voices = ["female_warm", "male_stable", "child_cute"]
        for vid in voices:
            output = tmp_path / f"{vid}.wav"
            result = self.service.synthesize("测试", voice_id=vid, output_path=output)
            assert result.exists()

    def test_synthesize_speed_faster(self, tmp_path):
        """语速快应该时长短."""
        out_slow = tmp_path / "slow.wav"
        out_fast = tmp_path / "fast.wav"
        text = "一二三四五六七八九十"

        self.service.synthesize(text, speed=0.5, output_path=out_slow)
        self.service.synthesize(text, speed=2.0, output_path=out_fast)

        # 快速应该文件更小（时长短）
        size_slow = out_slow.stat().st_size
        size_fast = out_fast.stat().st_size
        assert size_fast < size_slow

    def test_synthesize_pitch_changes(self, tmp_path):
        output = tmp_path / "high_pitch.wav"
        result = self.service.synthesize("测试", voice_id="female_warm", pitch=6, output_path=output)
        assert result.exists()

    def test_synthesize_empty_text_raises(self):
        with pytest.raises(TtsError):
            self.service.synthesize("")

    def test_synthesize_whitespace_text_raises(self):
        with pytest.raises(TtsError):
            self.service.synthesize("   ")

    def test_estimate_duration(self):
        dur = self.service.estimate_duration("一二三四五")
        assert dur > 0
        assert dur < 10  # 5个字应该少于10秒

    def test_estimate_duration_speed(self):
        text = "一二三四五六七八九十"
        dur_normal = self.service.estimate_duration(text, speed=1.0)
        dur_fast = self.service.estimate_duration(text, speed=2.0)
        dur_slow = self.service.estimate_duration(text, speed=0.5)

        assert dur_fast < dur_normal
        assert dur_slow > dur_normal

    def test_synthesize_unknown_voice_fallback(self, tmp_path):
        output = tmp_path / "fallback.wav"
        # 未知音色应该 fallback 到默认音色，不报错
        result = self.service.synthesize("测试", voice_id="unknown_voice", output_path=output)
        assert result.exists()


# ─── TtsEngine 配音引擎 ──────────────────────────────────


class TestTtsEngine:
    def _make_engine(self, tmp_path):
        service = MockTtsService()
        work_dir = tmp_path / "tts_engine"
        return TtsEngine(service, work_dir)

    def test_generate_full_voiceover_disabled(self, tmp_path):
        engine = self._make_engine(tmp_path)
        config = TtsConfig(enabled=False)
        result = engine.generate_full_voiceover(config)
        assert result.success is False

    def test_generate_full_voiceover_empty_text(self, tmp_path):
        engine = self._make_engine(tmp_path)
        config = TtsConfig(enabled=True, text="")
        result = engine.generate_full_voiceover(config)
        assert result.success is False

    def test_generate_full_voiceover_success(self, tmp_path):
        engine = self._make_engine(tmp_path)
        config = TtsConfig(
            enabled=True,
            voice_id="female_warm",
            text="这是一段测试配音文本",
        )
        result = engine.generate_full_voiceover(config)
        assert result.success is True
        assert len(result.segments) == 1
        assert result.total_duration > 0
        assert result.segments[0].audio_path is not None
        assert result.segments[0].audio_path.exists()
        assert result.segments[0].duration > 0

    def test_generate_full_voiceover_with_speed(self, tmp_path):
        engine = self._make_engine(tmp_path)
        config_slow = TtsConfig(
            enabled=True,
            voice_id="female_warm",
            text="测试文本一二三四五六七八九十",
            speed=0.5,
        )
        config_fast = TtsConfig(
            enabled=True,
            voice_id="female_warm",
            text="测试文本一二三四五六七八九十",
            speed=2.0,
        )
        result_slow = engine.generate_full_voiceover(config_slow)
        result_fast = engine.generate_full_voiceover(config_fast)

        assert result_slow.success
        assert result_fast.success
        # 慢速时长 > 快速时长
        assert result_slow.total_duration > result_fast.total_duration

    def test_generate_subtitle_voiceover_empty_subtitles(self, tmp_path):
        engine = self._make_engine(tmp_path)
        config = TtsConfig(enabled=True, voice_id="female_warm")
        result = engine.generate_subtitle_voiceover(config, [])
        assert result.success is False

    def test_generate_subtitle_voiceover_success(self, tmp_path):
        engine = self._make_engine(tmp_path)
        config = TtsConfig(enabled=True, voice_id="female_warm", align_mode="subtitle")
        subtitles = [
            {"text": "大家好", "start_time": 0, "end_time": 2},
            {"text": "欢迎观看", "start_time": 2, "end_time": 4},
            {"text": "今天的视频", "start_time": 4, "end_time": 6},
        ]
        result = engine.generate_subtitle_voiceover(config, subtitles)
        assert result.success is True
        assert len(result.segments) == 3

        # 每个片段的 start_time 应该对应字幕的开始时间
        assert result.segments[0].start_time == 0
        assert result.segments[1].start_time == 2
        assert result.segments[2].start_time == 4

        for seg in result.segments:
            assert seg.audio_path is not None
            assert seg.audio_path.exists()
            assert seg.duration > 0

    def test_generate_subtitle_voiceover_skips_empty(self, tmp_path):
        engine = self._make_engine(tmp_path)
        config = TtsConfig(enabled=True, voice_id="female_warm")
        subtitles = [
            {"text": "有文本", "start_time": 0, "end_time": 1},
            {"text": "", "start_time": 1, "end_time": 2},
            {"text": "也有文本", "start_time": 2, "end_time": 3},
        ]
        result = engine.generate_subtitle_voiceover(config, subtitles)
        assert result.success is True
        assert len(result.segments) == 2  # 跳过了空文本

    def test_generate_full_voiceover_failure_graceful(self, tmp_path, monkeypatch):
        """失败时优雅降级，不抛出异常."""
        engine = self._make_engine(tmp_path)

        def failing_synth(*args, **kwargs):
            raise TtsError("模拟失败")

        monkeypatch.setattr(engine._tts, "synthesize", failing_synth)

        config = TtsConfig(enabled=True, voice_id="test", text="测试")
        result = engine.generate_full_voiceover(config)
        assert result.success is False
        assert result.error_message
        assert "模拟失败" in result.error_message

    def test_generate_subtitle_voiceover_partial_failure(self, tmp_path, monkeypatch):
        """部分片段失败时跳过，其他正常生成."""
        engine = self._make_engine(tmp_path)
        original_synth = engine._tts.synthesize
        call_count = [0]

        def sometimes_fail(*args, **kwargs):
            call_count[0] += 1
            if call_count[0] == 2:  # 第2个片段失败
                raise TtsError("模拟失败")
            return original_synth(*args, **kwargs)

        monkeypatch.setattr(engine._tts, "synthesize", sometimes_fail)

        config = TtsConfig(enabled=True, voice_id="female_warm")
        subtitles = [
            {"text": "第一段", "start_time": 0, "end_time": 2},
            {"text": "第二段失败", "start_time": 2, "end_time": 4},
            {"text": "第三段", "start_time": 4, "end_time": 6},
        ]
        result = engine.generate_subtitle_voiceover(config, subtitles)
        # 有部分成功就算成功
        assert result.success is True
        assert len(result.segments) == 2  # 跳过了失败的第2段

    def test_build_audio_mix_filter_empty(self, tmp_path):
        engine = self._make_engine(tmp_path)
        result = VoiceoverResult(success=False)
        filter_str, files = engine.build_audio_mix_filter(result, video_duration=10)
        assert filter_str == ""
        assert files == []

    def test_build_audio_mix_filter_single(self, tmp_path):
        engine = self._make_engine(tmp_path)
        audio_file = tmp_path / "seg.wav"
        audio_file.write_bytes(b"fake")

        segment = VoiceoverSegment(
            text="test",
            start_time=1.0,
            end_time=3.0,
            audio_path=audio_file,
            duration=2.0,
        )
        result = VoiceoverResult(success=True, segments=[segment], total_duration=3.0)

        filter_str, files = engine.build_audio_mix_filter(result, video_duration=10)
        assert len(files) == 1
        assert "adelay" in filter_str
        assert "1000" in filter_str  # 1秒 = 1000ms


# ─── VoicePreset 数据类 ──────────────────────────────────


class TestVoicePreset:
    def test_create_preset(self):
        preset = VoicePreset(
            voice_id="test_voice",
            name="测试音色",
            gender=VoiceGender.MALE,
            style=VoiceStyle.NARRATION,
        )
        assert preset.voice_id == "test_voice"
        assert preset.name == "测试音色"
        assert preset.gender == VoiceGender.MALE
        assert preset.style == VoiceStyle.NARRATION
        assert preset.sample_rate == 22050
