"""多轨道混音 + 字幕渲染引擎 + 视频拼接 单元测试.

测试：
1. 多轨道混音：配置解析、轨道预处理、多轨混音、降级
2. 字幕渲染引擎：样式配置、ASS生成、多源字幕合并、滤镜构建
3. 视频拼接：配置解析、stream copy、concat filter、降级
"""

import sys
import tempfile
from pathlib import Path

sys.path.insert(0, str(Path(__file__).resolve().parents[2] / "apps" / "worker"))
sys.path.insert(0, str(Path(__file__).resolve().parents[2] / "apps" / "api"))

import pytest

# ── Fixtures ──────────────────────────────────────────────────────────────────


@pytest.fixture
def work_dir(tmp_path):
    return tmp_path


@pytest.fixture
def main_audio_path(work_dir):
    """生成 10 秒测试主音频."""
    import subprocess

    path = work_dir / "main.aac"
    subprocess.run(
        [
            "ffmpeg",
            "-y",
            "-f",
            "lavfi",
            "-i",
            "sine=frequency=440:duration=10:sample_rate=44100",
            "-c:a",
            "aac",
            "-b:a",
            "128k",
            str(path),
        ],
        capture_output=True,
        check=True,
        timeout=30,
    )
    return path


@pytest.fixture
def sfx_audio_path(work_dir):
    """生成 3 秒效音频."""
    import subprocess

    path = work_dir / "sfx.aac"
    subprocess.run(
        [
            "ffmpeg",
            "-y",
            "-f",
            "lavfi",
            "-i",
            "sine=frequency=880:duration=3:sample_rate=44100",
            "-c:a",
            "aac",
            "-b:a",
            "128k",
            str(path),
        ],
        capture_output=True,
        check=True,
        timeout=30,
    )
    return path


@pytest.fixture
def voiceover_audio_path(work_dir):
    """生成 5 秒配音音频."""
    import subprocess

    path = work_dir / "voiceover.aac"
    subprocess.run(
        [
            "ffmpeg",
            "-y",
            "-f",
            "lavfi",
            "-i",
            "sine=frequency=220:duration=5:sample_rate=44100",
            "-c:a",
            "aac",
            "-b:a",
            "128k",
            str(path),
        ],
        capture_output=True,
        check=True,
        timeout=30,
    )
    return path


@pytest.fixture
def test_video_1(work_dir):
    """生成 5 秒测试视频1（1080x1920, 30fps）."""
    import subprocess

    path = work_dir / "video1.mp4"
    subprocess.run(
        [
            "ffmpeg",
            "-y",
            "-f",
            "lavfi",
            "-i",
            "color=c=red:s=1080x1920:d=5:r=30",
            "-f",
            "lavfi",
            "-i",
            "sine=frequency=440:duration=5:sample_rate=44100",
            "-c:v",
            "libx264",
            "-preset",
            "ultrafast",
            "-c:a",
            "aac",
            "-b:a",
            "128k",
            "-shortest",
            str(path),
        ],
        capture_output=True,
        check=True,
        timeout=60,
    )
    return path


@pytest.fixture
def test_video_2(work_dir):
    """生成 5 秒测试视频2（1080x1920, 30fps）."""
    import subprocess

    path = work_dir / "video2.mp4"
    subprocess.run(
        [
            "ffmpeg",
            "-y",
            "-f",
            "lavfi",
            "-i",
            "color=c=blue:s=1080x1920:d=5:r=30",
            "-f",
            "lavfi",
            "-i",
            "sine=frequency=660:duration=5:sample_rate=44100",
            "-c:v",
            "libx264",
            "-preset",
            "ultrafast",
            "-c:a",
            "aac",
            "-b:a",
            "128k",
            "-shortest",
            str(path),
        ],
        capture_output=True,
        check=True,
        timeout=60,
    )
    return path


# ============================================================================
# 一、多轨道混音测试
# ============================================================================


class TestAudioTrack:
    """AudioTrack 配置解析测试."""

    def test_default_values(self):
        from video_processing.multi_track_mixer import AudioTrack

        track = AudioTrack.from_dict({"track_id": "t1", "audio_path": "/tmp/test.aac"})
        assert track.track_id == "t1"
        assert track.audio_path == "/tmp/test.aac"
        assert track.volume == pytest.approx(0.7)  # sfx 默认音量
        assert track.track_type == "sfx"
        assert track.enabled is True
        assert track.fade_in == 0.0
        assert track.start_time == 0.0

    def test_volume_clamping(self):
        from video_processing.multi_track_mixer import AudioTrack

        track = AudioTrack.from_dict(
            {
                "track_id": "t1",
                "audio_path": "/tmp/test.aac",
                "volume": 5.0,
            }
        )
        assert track.volume == pytest.approx(2.0)  # 上限钳制

        track2 = AudioTrack.from_dict(
            {
                "track_id": "t2",
                "audio_path": "/tmp/test.aac",
                "volume": -1.0,
            }
        )
        assert track2.volume == pytest.approx(0.0)  # 下限钳制

    def test_track_type_default_volume(self):
        from video_processing.multi_track_mixer import DEFAULT_VOLUMES, AudioTrack

        for track_type, expected_vol in DEFAULT_VOLUMES.items():
            track = AudioTrack.from_dict(
                {
                    "track_id": "t1",
                    "track_type": track_type,
                    "audio_path": "/tmp/test.aac",
                }
            )
            assert track.volume == pytest.approx(expected_vol)

    def test_invalid_config_safe(self):
        from video_processing.multi_track_mixer import AudioTrack

        # 无效值应该安全降级到默认值
        track = AudioTrack.from_dict(
            {
                "track_id": "t1",
                "audio_path": "/tmp/test.aac",
                "volume": "invalid",
                "fade_in": "abc",
                "start_time": None,
            }
        )
        assert track.volume > 0  # 有默认值
        assert track.fade_in == 0.0
        assert track.start_time == 0.0

    def test_disabled_track(self):
        from video_processing.multi_track_mixer import AudioTrack

        track = AudioTrack.from_dict(
            {
                "track_id": "t1",
                "audio_path": "/tmp/test.aac",
                "enabled": False,
            }
        )
        assert track.enabled is False


class TestMultiTrackMixConfig:
    """MultiTrackMixConfig 配置解析测试."""

    def test_empty_config(self):
        from video_processing.multi_track_mixer import MultiTrackMixConfig

        config = MultiTrackMixConfig.from_config_dict(None)
        assert config.has_effect is False
        assert len(config.tracks) == 0

    def test_empty_dict(self):
        from video_processing.multi_track_mixer import MultiTrackMixConfig

        config = MultiTrackMixConfig.from_config_dict({})
        assert config.has_effect is False
        assert len(config.tracks) == 0

    def test_valid_tracks(self):
        from video_processing.multi_track_mixer import MultiTrackMixConfig

        config = MultiTrackMixConfig.from_config_dict(
            {
                "tracks": [
                    {"track_id": "sfx1", "track_type": "sfx", "audio_path": "/tmp/sfx1.aac", "volume": 0.5},
                    {"track_id": "vo1", "track_type": "voiceover", "audio_path": "/tmp/vo1.aac"},
                ],
                "master_volume": 0.8,
            }
        )
        assert config.has_effect is True
        assert len(config.tracks) == 2
        assert config.tracks[0].volume == pytest.approx(0.5)
        assert config.master_volume == pytest.approx(0.8)

    def test_skip_invalid_tracks(self):
        from video_processing.multi_track_mixer import MultiTrackMixConfig

        config = MultiTrackMixConfig.from_config_dict(
            {
                "tracks": [
                    {"track_id": "valid", "audio_path": "/tmp/valid.aac"},
                    {"track_id": "no_path"},  # 没有 audio_path，应该跳过
                    "not_a_dict",  # 不是字典，应该跳过
                    {"track_id": "disabled", "audio_path": "/tmp/dis.aac", "enabled": False},
                ],
            }
        )
        # 只有 valid 一个有效（disabled 的也跳过）
        assert len([t for t in config.tracks if t.enabled]) == 1


class TestMultiTrackMix:
    """多轨道混音集成测试."""

    def test_mix_two_tracks(self, work_dir, main_audio_path, sfx_audio_path):
        """主音频 + 音效轨混音."""
        from video_processing.multi_track_mixer import AudioTrack, MultiTrackMixConfig, mix_multi_track
        from video_processing.render_audio import RenderContext

        ctx = RenderContext(work_dir=work_dir, plan_id="test")

        config = MultiTrackMixConfig(
            tracks=[
                AudioTrack(
                    track_id="sfx1",
                    track_type="sfx",
                    audio_path=str(sfx_audio_path),
                    volume=0.5,
                    start_time=2.0,
                ),
            ],
            master_volume=1.0,
        )

        output = mix_multi_track(ctx, main_audio_path, config, target_duration=10.0)
        assert output.exists()
        assert output.stat().st_size > 0

    def test_mix_with_voiceover(self, work_dir, main_audio_path, voiceover_audio_path):
        """主音频 + 配音轨混音."""
        from video_processing.multi_track_mixer import AudioTrack, MultiTrackMixConfig, mix_multi_track
        from video_processing.render_audio import RenderContext

        ctx = RenderContext(work_dir=work_dir, plan_id="test2")

        config = MultiTrackMixConfig(
            tracks=[
                AudioTrack(
                    track_id="vo1",
                    track_type="voiceover",
                    audio_path=str(voiceover_audio_path),
                    volume=1.0,
                    start_time=1.0,
                    fade_in=0.5,
                    fade_out=0.5,
                ),
            ],
        )

        output = mix_multi_track(ctx, main_audio_path, config, target_duration=10.0)
        assert output.exists()
        assert output.stat().st_size > 0

    def test_no_tracks_returns_main(self, work_dir, main_audio_path):
        """没有附加轨道时返回主音频副本."""
        from video_processing.multi_track_mixer import MultiTrackMixConfig, mix_multi_track
        from video_processing.render_audio import RenderContext

        ctx = RenderContext(work_dir=work_dir, plan_id="test3")
        config = MultiTrackMixConfig(tracks=[])

        output = mix_multi_track(ctx, main_audio_path, config, target_duration=10.0)
        assert output.exists()
        assert output.stat().st_size > 0

    def test_mix_with_fade(self, work_dir, main_audio_path, sfx_audio_path):
        """带淡入淡出的混音."""
        from video_processing.multi_track_mixer import AudioTrack, MultiTrackMixConfig, mix_multi_track
        from video_processing.render_audio import RenderContext

        ctx = RenderContext(work_dir=work_dir, plan_id="test_fade")

        config = MultiTrackMixConfig(
            tracks=[
                AudioTrack(
                    track_id="sfx_fade",
                    track_type="sfx",
                    audio_path=str(sfx_audio_path),
                    fade_in=0.3,
                    fade_out=0.3,
                    start_time=1.0,
                ),
            ],
        )

        output = mix_multi_track(ctx, main_audio_path, config, target_duration=10.0)
        assert output.exists()
        assert output.stat().st_size > 0

    def test_mix_audio_tracks_from_config(self, work_dir, main_audio_path, sfx_audio_path):
        """从配置字典混音的便捷函数."""
        from video_processing.multi_track_mixer import mix_audio_tracks_from_config
        from video_processing.render_audio import RenderContext

        ctx = RenderContext(work_dir=work_dir, plan_id="test_config")

        config_dict = {
            "tracks": [
                {
                    "track_id": "sfx1",
                    "track_type": "sfx",
                    "audio_path": str(sfx_audio_path),
                    "volume": 0.6,
                    "start_time": 1.0,
                },
            ],
            "enabled": True,
        }

        output = mix_audio_tracks_from_config(ctx, main_audio_path, config_dict, 10.0)
        assert output.exists()
        assert output.stat().st_size > 0

    def test_disabled_config_returns_main(self, work_dir, main_audio_path):
        """配置未启用时返回主音频."""
        from video_processing.multi_track_mixer import mix_audio_tracks_from_config
        from video_processing.render_audio import RenderContext

        ctx = RenderContext(work_dir=work_dir, plan_id="test_disabled")

        output = mix_audio_tracks_from_config(ctx, main_audio_path, None, 10.0)
        assert output == main_audio_path  # 直接返回原文件


# ============================================================================
# 二、字幕渲染引擎测试
# ============================================================================


class TestSubtitleStyle:
    """SubtitleStyle 配置解析测试."""

    def test_default_style(self):
        from video_processing.subtitle_render_engine import SubtitleStyle

        style = SubtitleStyle.from_dict({})
        assert style.font_size == 24
        assert style.position == "bottom_center"
        assert style.stroke_enabled is True
        assert style.background_enabled is False
        assert style.alignment == 2  # bottom_center → ASS alignment 2

    def test_position_aliases(self):
        from video_processing.subtitle_render_engine import SubtitleStyle

        style = SubtitleStyle.from_dict({"position": "top"})
        assert style.position == "top_center"
        assert style.alignment == 8

        style2 = SubtitleStyle.from_dict({"position": "bottom"})
        assert style2.position == "bottom_center"
        assert style2.alignment == 2

        style3 = SubtitleStyle.from_dict({"position": "center"})
        assert style3.position == "center"
        assert style3.alignment == 5

    def test_9grid_positions(self):
        from video_processing.subtitle_render_engine import SubtitleStyle

        from packages.domain.subtitle_style import POSITION_ALIGNMENT

        for pos, align in POSITION_ALIGNMENT.items():
            style = SubtitleStyle.from_dict({"position": pos})
            assert style.position == pos
            assert style.alignment == align

    def test_invalid_position_fallback(self):
        from video_processing.subtitle_render_engine import SubtitleStyle

        style = SubtitleStyle.from_dict({"position": "invalid_position"})
        assert style.position == "bottom_center"  # 降级到默认

    def test_background_style(self):
        from video_processing.subtitle_render_engine import SubtitleStyle

        style = SubtitleStyle.from_dict(
            {
                "background_enabled": True,
                "background_color": "#000000",
                "background_opacity": 0.7,
            }
        )
        assert style.background_enabled is True
        assert style.background_opacity == pytest.approx(0.7)

    def test_color_conversion(self):
        from video_processing.subtitle_render_engine import SubtitleStyle

        style = SubtitleStyle.from_dict({"color": "#FF0000"})
        # #FF0000 → &H000000FF (ASS 格式: &HAABBGGRR)
        assert "FF" in style.ass_font_color
        assert "0000" in style.ass_font_color  # BB 和 GG 都是 00

    def test_safe_type_conversion(self):
        from video_processing.subtitle_render_engine import SubtitleStyle

        style = SubtitleStyle.from_dict(
            {
                "size": "invalid",
                "margin_v": None,
                "bold": "true",  # 字符串真值
            }
        )
        assert style.font_size == 24  # 降级到默认
        assert style.margin_v == 60
        # bool("true") = True，但这是 Python 行为，可以接受


class TestSubtitleRenderEngine:
    """字幕渲染引擎测试."""

    def test_empty_engine(self, work_dir):
        """空引擎生成空文件."""
        from video_processing.subtitle_render_engine import SubtitleRenderEngine

        engine = SubtitleRenderEngine(video_width=1080, video_height=1920, video_duration=10.0)
        assert engine.has_subtitles is False

        output = work_dir / "empty.ass"
        engine.generate_ass(output)
        assert output.exists()
        assert output.read_text(encoding="utf-8") == ""

    def test_add_title(self, work_dir):
        """添加标题字幕."""
        from video_processing.subtitle_render_engine import SubtitleRenderEngine, SubtitleStyle

        engine = SubtitleRenderEngine(video_width=1080, video_height=1920, video_duration=10.0)
        engine.add_title("测试标题")
        assert engine.has_subtitles is True

        output = work_dir / "title.ass"
        result = engine.generate_ass(output)
        assert result.exists()
        content = result.read_text(encoding="utf-8")
        assert "测试标题" in content
        assert "TitleStyle" in content

    def test_add_subtitle_text(self, work_dir):
        """添加整段字幕."""
        from video_processing.subtitle_render_engine import SubtitleRenderEngine, SubtitleStyle

        engine = SubtitleRenderEngine(video_width=1080, video_height=1920, video_duration=10.0)
        engine.add_subtitle_text("这是一段字幕")
        assert engine.has_subtitles is True

        output = work_dir / "subtitle.ass"
        engine.generate_ass(output)
        content = output.read_text(encoding="utf-8")
        assert "这是一段字幕" in content
        assert "SubtitleStyle" in content

    def test_add_timeline_segments(self, work_dir):
        """添加时间轴字幕片段."""
        from video_processing.subtitle_render_engine import SubtitleRenderEngine, SubtitleStyle

        engine = SubtitleRenderEngine(video_width=1080, video_height=1920, video_duration=10.0)
        segments = [
            {"start": 0.0, "end": 2.0, "text": "第一段字幕"},
            {"start": 2.0, "end": 5.0, "text": "第二段字幕"},
            {"start": 5.0, "end": 10.0, "text": "第三段字幕"},
        ]
        engine.add_timeline_segments(segments)
        assert engine.has_subtitles is True

        output = work_dir / "timeline.ass"
        engine.generate_ass(output)
        content = output.read_text(encoding="utf-8")
        assert "第一段字幕" in content
        assert "第二段字幕" in content
        assert "第三段字幕" in content
        # 检查有 3 条 Dialogue 事件
        assert content.count("Dialogue:") == 3

    def test_mixed_sources(self, work_dir):
        """标题 + 时间轴字幕 混合."""
        from video_processing.subtitle_render_engine import SubtitleRenderEngine, SubtitleStyle

        engine = SubtitleRenderEngine(video_width=1080, video_height=1920, video_duration=10.0)
        engine.add_title("视频标题")
        engine.add_timeline_segments(
            [
                {"start": 0.0, "end": 3.0, "text": "ASR 结果1"},
                {"start": 3.0, "end": 7.0, "text": "ASR 结果2"},
            ]
        )

        output = work_dir / "mixed.ass"
        engine.generate_ass(output)
        content = output.read_text(encoding="utf-8")
        assert "视频标题" in content
        assert "ASR 结果1" in content
        assert "ASR 结果2" in content
        assert content.count("Dialogue:") == 3

    def test_fade_animation(self, work_dir):
        """淡入淡出动画效果."""
        from video_processing.subtitle_render_engine import SubtitleRenderEngine, SubtitleStyle

        style = SubtitleStyle(fade_in=0.5, fade_out=0.5)
        engine = SubtitleRenderEngine(video_width=1080, video_height=1920, video_duration=10.0)
        engine.add_timeline_segments(
            [{"start": 0.0, "end": 5.0, "text": "淡入淡出测试"}],
            style=style,
        )

        output = work_dir / "fade.ass"
        engine.generate_ass(output)
        content = output.read_text(encoding="utf-8")
        assert "\\fad" in content  # ASS 淡入淡出标签

    def test_text_wrapping(self, work_dir):
        """长文本自动换行."""
        from video_processing.subtitle_render_engine import SubtitleRenderEngine, SubtitleStyle

        style = SubtitleStyle(max_chars_per_line=10)
        engine = SubtitleRenderEngine(video_width=1080, video_height=1920, video_duration=10.0)
        engine.add_subtitle_text("这是一段非常长的字幕文本，应该会自动换行显示", style=style)

        output = work_dir / "wrap.ass"
        engine.generate_ass(output)
        content = output.read_text(encoding="utf-8")
        assert "\\N" in content  # ASS 换行符

    def test_escape_special_chars(self, work_dir):
        """ASS 特殊字符转义."""
        from video_processing.subtitle_render_engine import SubtitleRenderEngine

        engine = SubtitleRenderEngine(video_width=1080, video_height=1920, video_duration=10.0)
        engine.add_subtitle_text("测试{大括号}换行\n第二行")

        output = work_dir / "escape.ass"
        engine.generate_ass(output)
        content = output.read_text(encoding="utf-8")
        # 大括号应该被转义
        assert "{" not in content.split("Dialogue:")[1].split("测试")[1][:10] or "(" in content
        assert "\\N" in content  # 换行转义


class TestBuildSubtitlesFromPlan:
    """从 plan.config 构建字幕测试."""

    def test_empty_config(self, work_dir):
        from video_processing.subtitle_render_engine import build_subtitles_from_plan

        output = work_dir / "empty_plan.ass"
        result = build_subtitles_from_plan(
            output,
            {},
            video_width=1080,
            video_height=1920,
            video_duration=10.0,
        )
        assert result is None

    def test_title_only(self, work_dir):
        from video_processing.subtitle_render_engine import build_subtitles_from_plan

        output = work_dir / "title_plan.ass"
        config = {
            "title_config": {
                "enabled": True,
                "text": "我的视频标题",
                "style": {"size": 48, "bold": True, "position": "top"},
            }
        }
        result = build_subtitles_from_plan(output, config, video_width=1080, video_height=1920, video_duration=10.0)
        assert result is not None
        assert result.exists()
        content = result.read_text(encoding="utf-8")
        assert "我的视频标题" in content

    def test_manual_subtitles(self, work_dir):
        from video_processing.subtitle_render_engine import build_subtitles_from_plan

        output = work_dir / "manual.ass"
        config = {
            "manual_subtitles": [
                {"start": 0.0, "end": 2.0, "text": "手动字幕1"},
                {"start": 2.5, "end": 5.0, "text": "手动字幕2"},
            ],
            "manual_subtitle_style": {"size": 28, "color": "#FFFF00"},
        }
        result = build_subtitles_from_plan(output, config, video_width=1080, video_height=1920, video_duration=10.0)
        assert result is not None
        content = result.read_text(encoding="utf-8")
        assert "手动字幕1" in content
        assert "手动字幕2" in content
        assert content.count("Dialogue:") == 2

    def test_disabled_title_skipped(self, work_dir):
        from video_processing.subtitle_render_engine import build_subtitles_from_plan

        output = work_dir / "disabled.ass"
        config = {
            "title_config": {
                "enabled": False,
                "text": "不显示的标题",
            }
        }
        result = build_subtitles_from_plan(output, config, video_width=1080, video_height=1920, video_duration=10.0)
        assert result is None


class TestSubtitleFilter:
    """字幕滤镜构建测试."""

    def test_build_subtitle_filter(self, work_dir):
        from video_processing.subtitle_render_engine import build_subtitle_filter

        ass_file = work_dir / "test.ass"
        ass_file.write_text("test", encoding="utf-8")

        result = build_subtitle_filter(
            str(ass_file),
            video_input_label="[v_in]",
            output_label="out",
            work_dir=work_dir,
        )
        assert "subtitles=" in result
        assert "[v_in]" in result
        assert "[out]" in result

    def test_default_labels(self, work_dir):
        from video_processing.subtitle_render_engine import build_subtitle_filter

        ass_file = work_dir / "sub.ass"
        ass_file.write_text("test", encoding="utf-8")

        result = build_subtitle_filter(str(ass_file), work_dir=work_dir)
        assert "0:v" in result
        assert "[subtitled]" in result


# ============================================================================
# 三、视频拼接引擎测试
# ============================================================================


class TestConcatSegment:
    """ConcatSegment 配置解析测试."""

    def test_default_values(self):
        from video_processing.concat_engine import ConcatSegment

        seg = ConcatSegment.from_dict({"video_path": "/tmp/test.mp4"})
        assert seg.video_path == "/tmp/test.mp4"
        assert seg.start_time == 0.0
        assert seg.duration == 0.0
        assert seg.has_audio is True

    def test_trimming_config(self):
        from video_processing.concat_engine import ConcatSegment

        seg = ConcatSegment.from_dict(
            {
                "video_path": "/tmp/test.mp4",
                "start_time": 5.0,
                "duration": 10.0,
            }
        )
        assert seg.start_time == 5.0
        assert seg.duration == 10.0

    def test_invalid_values_safe(self):
        from video_processing.concat_engine import ConcatSegment

        seg = ConcatSegment.from_dict(
            {
                "video_path": "/tmp/test.mp4",
                "start_time": "invalid",
                "duration": -5.0,
            }
        )
        assert seg.start_time == 0.0
        assert seg.duration == 0.0


class TestConcatConfig:
    """ConcatConfig 配置解析测试."""

    def test_empty_config(self):
        from video_processing.concat_engine import ConcatConfig

        config = ConcatConfig.from_config_dict(None)
        assert config.has_effect is False
        assert config.total_segments == 0

    def test_single_segment_no_effect(self):
        from video_processing.concat_engine import ConcatConfig

        config = ConcatConfig.from_config_dict(
            {
                "segments": [{"video_path": "/tmp/1.mp4"}],
            }
        )
        assert config.has_effect is False  # 只有一段不需要拼接

    def test_multiple_segments(self):
        from video_processing.concat_engine import ConcatConfig

        config = ConcatConfig.from_config_dict(
            {
                "segments": [
                    {"video_path": "/tmp/1.mp4"},
                    {"video_path": "/tmp/2.mp4"},
                    {"video_path": "/tmp/3.mp4"},
                ],
                "output_width": 1080,
                "output_height": 1920,
                "output_fps": 30.0,
            }
        )
        assert config.has_effect is True
        assert config.total_segments == 3
        assert config.output_width == 1080

    def test_skip_invalid_segments(self):
        from video_processing.concat_engine import ConcatConfig

        config = ConcatConfig.from_config_dict(
            {
                "segments": [
                    {"video_path": "/tmp/1.mp4"},
                    {},  # 没有 path
                    {"video_path": ""},  # 空 path
                    {"video_path": "/tmp/2.mp4"},
                ],
            }
        )
        assert config.total_segments == 2
        assert config.has_effect is True


class TestConcatEngine:
    """视频拼接引擎集成测试."""

    def test_concat_demuxer_stream_copy(self, work_dir, test_video_1, test_video_2):
        """concat demuxer 模式（stream copy）."""
        from video_processing.concat_engine import ConcatConfig, ConcatEngine, ConcatSegment

        engine = ConcatEngine(work_dir=work_dir)
        config = ConcatConfig(
            segments=[
                ConcatSegment(video_path=str(test_video_1)),
                ConcatSegment(video_path=str(test_video_2)),
            ],
        )

        output = work_dir / "concat_demuxer.mp4"
        result = engine.concat_videos(config, output)
        assert result.exists()
        assert result.stat().st_size > 0

        # 验证时长大约是两段之和（5+5=10秒）
        from video_processing.ffmpeg_utils import probe_duration

        dur = probe_duration(str(result))
        assert dur > 8.0  # 留一些误差余量
        assert dur < 12.0

    def test_concat_filter_reencode(self, work_dir, test_video_1, test_video_2):
        """concat filter 模式（强制重新编码）."""
        from video_processing.concat_engine import ConcatConfig, ConcatEngine, ConcatSegment

        engine = ConcatEngine(work_dir=work_dir)
        config = ConcatConfig(
            segments=[
                ConcatSegment(video_path=str(test_video_1)),
                ConcatSegment(video_path=str(test_video_2)),
            ],
            force_reencode=True,
        )

        output = work_dir / "concat_filter.mp4"
        result = engine.concat_videos(config, output)
        assert result.exists()
        assert result.stat().st_size > 0

        from video_processing.ffmpeg_utils import probe_duration

        dur = probe_duration(str(result))
        assert dur > 8.0
        assert dur < 12.0

    def test_concat_with_trimming(self, work_dir, test_video_1, test_video_2):
        """带裁剪的拼接（自动用 filter 模式）."""
        from video_processing.concat_engine import ConcatConfig, ConcatEngine, ConcatSegment

        engine = ConcatEngine(work_dir=work_dir)
        config = ConcatConfig(
            segments=[
                ConcatSegment(video_path=str(test_video_1), start_time=1.0, duration=2.0),
                ConcatSegment(video_path=str(test_video_2), start_time=0.0, duration=3.0),
            ],
        )

        output = work_dir / "concat_trimmed.mp4"
        result = engine.concat_videos(config, output)
        assert result.exists()
        assert result.stat().st_size > 0

        from video_processing.ffmpeg_utils import probe_duration

        dur = probe_duration(str(result))
        assert dur > 3.0  # 2+3=5秒
        assert dur < 7.0

    def test_single_segment_copy(self, work_dir, test_video_1):
        """单片段直接复制."""
        from video_processing.concat_engine import ConcatConfig, ConcatEngine, ConcatSegment

        engine = ConcatEngine(work_dir=work_dir)
        config = ConcatConfig(
            segments=[ConcatSegment(video_path=str(test_video_1))],
        )

        output = work_dir / "single.mp4"
        result = engine.concat_videos(config, output)
        assert result.exists()

    def test_concat_video_files_helper(self, work_dir, test_video_1, test_video_2):
        """便捷函数 concat_video_files."""
        from video_processing.concat_engine import concat_video_files

        output = work_dir / "concat_helper.mp4"
        result = concat_video_files(
            [str(test_video_1), str(test_video_2)],
            output,
            work_dir=work_dir,
        )
        assert result.exists()
        assert result.stat().st_size > 0

    def test_concat_from_config(self, work_dir, test_video_1, test_video_2):
        """从配置字典拼接的便捷函数."""
        from video_processing.concat_engine import concat_videos_from_config

        output = work_dir / "concat_config.mp4"
        config_dict = {
            "segments": [
                {"video_path": str(test_video_1)},
                {"video_path": str(test_video_2)},
            ],
        }
        result = concat_videos_from_config(config_dict, output, work_dir=work_dir)
        assert result is not None
        assert result.exists()

    def test_concat_empty_config_returns_none(self, work_dir):
        """空配置返回 None."""
        from video_processing.concat_engine import concat_videos_from_config

        output = work_dir / "empty.mp4"
        result = concat_videos_from_config(None, output, work_dir=work_dir)
        assert result is None

    def test_three_videos_concat(self, work_dir, test_video_1, test_video_2):
        """三段视频拼接."""
        from video_processing.concat_engine import ConcatConfig, ConcatEngine, ConcatSegment

        engine = ConcatEngine(work_dir=work_dir)
        config = ConcatConfig(
            segments=[
                ConcatSegment(video_path=str(test_video_1)),
                ConcatSegment(video_path=str(test_video_2)),
                ConcatSegment(video_path=str(test_video_1)),
            ],
            force_reencode=True,
        )

        output = work_dir / "three_videos.mp4"
        result = engine.concat_videos(config, output)
        assert result.exists()

        from video_processing.ffmpeg_utils import probe_duration

        dur = probe_duration(str(result))
        assert dur > 12.0  # 5+5+5=15秒
        assert dur < 18.0

    def test_output_resolution_override(self, work_dir, test_video_1, test_video_2):
        """指定输出分辨率."""
        from video_processing.concat_engine import ConcatConfig, ConcatEngine, ConcatSegment

        engine = ConcatEngine(work_dir=work_dir)
        config = ConcatConfig(
            segments=[
                ConcatSegment(video_path=str(test_video_1)),
                ConcatSegment(video_path=str(test_video_2)),
            ],
            output_width=720,
            output_height=1280,
            force_reencode=True,
        )

        output = work_dir / "concat_720p.mp4"
        result = engine.concat_videos(config, output)
        assert result.exists()

        from video_processing.ffmpeg_utils import probe_video_info

        info = probe_video_info(str(result))
        assert int(info.get("width", 0)) == 720
        assert int(info.get("height", 0)) == 1280
