"""video_concat 视频拼接领域模型单元测试."""

from __future__ import annotations

import pytest

from packages.domain.video_concat import (
    ALLOWED_VIDEO_EXTENSIONS,
    CONCAT_DEMUXER_REQUIRED_PARAMS,
    MAX_CONCAT_SEGMENTS,
    ConcatConfig,
    ConcatSegment,
)

# ── 常量测试 ──────────────────────────────────────────────────────────────────


class TestConstants:
    """常量测试."""

    def test_max_concat_segments(self):
        """最大拼接段数."""
        assert MAX_CONCAT_SEGMENTS == 50

    def test_allowed_extensions_not_empty(self):
        """支持的视频格式不为空."""
        assert len(ALLOWED_VIDEO_EXTENSIONS) > 0

    def test_common_formats_supported(self):
        """常见格式都支持."""
        assert ".mp4" in ALLOWED_VIDEO_EXTENSIONS
        assert ".mov" in ALLOWED_VIDEO_EXTENSIONS
        assert ".avi" in ALLOWED_VIDEO_EXTENSIONS
        assert ".mkv" in ALLOWED_VIDEO_EXTENSIONS
        assert ".webm" in ALLOWED_VIDEO_EXTENSIONS

    def test_demuxer_params_not_empty(self):
        """demuxer必需参数不为空."""
        assert len(CONCAT_DEMUXER_REQUIRED_PARAMS) > 0

    def test_demuxer_params_include_codec(self):
        """包含编解码相关参数."""
        assert "codec_name" in CONCAT_DEMUXER_REQUIRED_PARAMS
        assert "width" in CONCAT_DEMUXER_REQUIRED_PARAMS
        assert "height" in CONCAT_DEMUXER_REQUIRED_PARAMS
        assert "r_frame_rate" in CONCAT_DEMUXER_REQUIRED_PARAMS


# ── ConcatSegment 测试 ───────────────────────────────────────────────────────


class TestConcatSegment:
    """ConcatSegment 测试."""

    def test_basic_creation(self):
        """基础创建."""
        seg = ConcatSegment(video_path="/tmp/video.mp4")
        assert seg.video_path == "/tmp/video.mp4"
        assert seg.start_time == 0.0
        assert seg.duration == 0.0
        assert seg.has_audio is True

    def test_full_creation(self):
        """完整字段创建."""
        seg = ConcatSegment(
            video_path="/tmp/v.mov",
            start_time=5.5,
            duration=10.0,
            has_audio=False,
        )
        assert seg.video_path == "/tmp/v.mov"
        assert seg.start_time == 5.5
        assert seg.duration == 10.0
        assert seg.has_audio is False

    # is_valid 属性

    def test_is_valid_with_path(self):
        """有视频路径是有效的."""
        seg = ConcatSegment(video_path="/tmp/v.mp4")
        assert seg.is_valid is True

    def test_is_valid_empty_path(self):
        """空路径无效."""
        seg = ConcatSegment(video_path="")
        assert seg.is_valid is False

    def test_effective_duration_positive(self):
        """正的effective_duration."""
        seg = ConcatSegment(video_path="v.mp4", duration=10.5)
        assert seg.effective_duration == 10.5

    def test_effective_duration_zero(self):
        """duration为0时effective_duration为0."""
        seg = ConcatSegment(video_path="v.mp4", duration=0)
        assert seg.effective_duration == 0.0

    def test_effective_duration_negative(self):
        """负的duration被钳制到0."""
        seg = ConcatSegment(video_path="v.mp4", duration=-5)
        assert seg.effective_duration == 0.0


# ── ConcatSegment.from_dict 测试 ─────────────────────────────────────────────


class TestConcatSegmentFromDict:
    """ConcatSegment.from_dict 工厂方法测试."""

    def test_none_input(self):
        """None输入返回默认片段（空路径）."""
        seg = ConcatSegment.from_dict(None)
        assert seg.video_path == ""
        assert seg.is_valid is False

    def test_empty_dict(self):
        """空dict返回默认."""
        seg = ConcatSegment.from_dict({})
        assert seg.video_path == ""

    def test_not_dict_input(self):
        """非dict输入安全处理."""
        seg = ConcatSegment.from_dict("not_a_dict")
        assert seg.video_path == ""

    def test_list_input(self):
        """list输入安全处理."""
        seg = ConcatSegment.from_dict([1, 2, 3])
        assert seg.video_path == ""

    def test_with_video_path(self):
        """带视频路径."""
        seg = ConcatSegment.from_dict({"video_path": "/tmp/v.mp4"})
        assert seg.video_path == "/tmp/v.mp4"
        assert seg.is_valid is True

    def test_with_start_time(self):
        """带start_time."""
        seg = ConcatSegment.from_dict({"video_path": "v.mp4", "start_time": 3.5})
        assert seg.start_time == 3.5

    def test_start_time_negative_clamped(self):
        """负的start_time钳制到0."""
        seg = ConcatSegment.from_dict({"video_path": "v.mp4", "start_time": -5})
        assert seg.start_time == 0.0

    def test_start_time_invalid_string(self):
        """无效start_time字符串回退到0."""
        seg = ConcatSegment.from_dict({"video_path": "v.mp4", "start_time": "abc"})
        assert seg.start_time == 0.0

    def test_with_duration(self):
        """带duration."""
        seg = ConcatSegment.from_dict({"video_path": "v.mp4", "duration": 8.5})
        assert seg.duration == 8.5

    def test_duration_negative_clamped(self):
        """负的duration钳制到0."""
        seg = ConcatSegment.from_dict({"video_path": "v.mp4", "duration": -10})
        assert seg.duration == 0.0

    def test_duration_invalid_string(self):
        """无效duration回退到0."""
        seg = ConcatSegment.from_dict({"video_path": "v.mp4", "duration": "xyz"})
        assert seg.duration == 0.0

    def test_has_audio_true(self):
        """has_audio为True."""
        seg = ConcatSegment.from_dict({"video_path": "v.mp4", "has_audio": True})
        assert seg.has_audio is True

    def test_has_audio_false(self):
        """has_audio为False."""
        seg = ConcatSegment.from_dict({"video_path": "v.mp4", "has_audio": False})
        assert seg.has_audio is False

    def test_has_audio_default_true(self):
        """has_audio默认True."""
        seg = ConcatSegment.from_dict({"video_path": "v.mp4"})
        assert seg.has_audio is True

    def test_string_start_time(self):
        """字符串形式的start_time."""
        seg = ConcatSegment.from_dict({"video_path": "v.mp4", "start_time": "2.5"})
        assert seg.start_time == 2.5

    def test_int_duration(self):
        """整数duration."""
        seg = ConcatSegment.from_dict({"video_path": "v.mp4", "duration": 10})
        assert seg.duration == 10.0


# ── ConcatConfig 基础测试 ────────────────────────────────────────────────────


class TestConcatConfig:
    """ConcatConfig 基础测试."""

    def test_default_creation(self):
        """默认创建."""
        cfg = ConcatConfig()
        assert cfg.segments == []
        assert cfg.output_width == 0
        assert cfg.output_height == 0
        assert cfg.output_fps == 0.0
        assert cfg.force_reencode is False
        assert cfg.transition == "none"
        assert cfg.transition_duration == 0.3

    def test_with_segments(self):
        """带片段创建."""
        segs = [ConcatSegment(video_path="v1.mp4"), ConcatSegment(video_path="v2.mp4")]
        cfg = ConcatConfig(segments=segs, output_width=1920, output_height=1080)
        assert len(cfg.segments) == 2
        assert cfg.output_width == 1920
        assert cfg.output_height == 1080

    # 属性测试

    def test_has_effect_with_two_segments(self):
        """2个以上有效片段has_effect为True."""
        cfg = ConcatConfig(
            segments=[
                ConcatSegment(video_path="v1.mp4"),
                ConcatSegment(video_path="v2.mp4"),
            ]
        )
        assert cfg.has_effect is True

    def test_has_effect_with_one_segment(self):
        """只有1个有效片段has_effect为False."""
        cfg = ConcatConfig(segments=[ConcatSegment(video_path="v1.mp4")])
        assert cfg.has_effect is False

    def test_has_effect_with_no_segments(self):
        """空片段has_effect为False."""
        cfg = ConcatConfig()
        assert cfg.has_effect is False

    def test_valid_segment_count(self):
        """有效片段计数."""
        cfg = ConcatConfig(
            segments=[
                ConcatSegment(video_path="v1.mp4"),
                ConcatSegment(video_path=""),  # 无效
                ConcatSegment(video_path="v2.mp4"),
            ]
        )
        assert cfg.valid_segment_count == 2

    def test_total_segments_alias(self):
        """total_segments是valid_segment_count的别名."""
        cfg = ConcatConfig(
            segments=[
                ConcatSegment(video_path="v1.mp4"),
                ConcatSegment(video_path="v2.mp4"),
            ]
        )
        assert cfg.total_segments == cfg.valid_segment_count
        assert cfg.total_segments == 2

    def test_first_valid_segment(self):
        """第一个有效片段."""
        cfg = ConcatConfig(
            segments=[
                ConcatSegment(video_path=""),  # 无效
                ConcatSegment(video_path="first_valid.mp4"),
                ConcatSegment(video_path="second.mp4"),
            ]
        )
        first = cfg.first_valid_segment
        assert first is not None
        assert first.video_path == "first_valid.mp4"

    def test_first_valid_segment_none(self):
        """无有效片段时first_valid_segment为None."""
        cfg = ConcatConfig(segments=[ConcatSegment(video_path="")])
        assert cfg.first_valid_segment is None

    def test_first_valid_segment_empty(self):
        """空列表时为None."""
        cfg = ConcatConfig()
        assert cfg.first_valid_segment is None

    def test_estimated_total_duration(self):
        """估算总时长."""
        cfg = ConcatConfig(
            segments=[
                ConcatSegment(video_path="v1.mp4", duration=10.0),
                ConcatSegment(video_path="v2.mp4", duration=5.5),
                ConcatSegment(video_path="v3.mp4", duration=0),  # 不计入
            ]
        )
        assert cfg.estimated_total_duration == pytest.approx(15.5)

    def test_estimated_total_duration_empty(self):
        """空片段时长为0."""
        cfg = ConcatConfig()
        assert cfg.estimated_total_duration == 0.0

    def test_estimated_total_duration_skips_invalid(self):
        """跳过无效片段."""
        cfg = ConcatConfig(
            segments=[
                ConcatSegment(video_path="", duration=100),  # 无效，跳过
                ConcatSegment(video_path="v1.mp4", duration=5.0),
            ]
        )
        assert cfg.estimated_total_duration == 5.0


# ── ConcatConfig.from_config_dict 测试 ───────────────────────────────────────


class TestConcatConfigFromConfigDict:
    """ConcatConfig.from_config_dict 工厂方法测试."""

    def test_none_config(self):
        """None返回默认配置."""
        cfg = ConcatConfig.from_config_dict(None)
        assert cfg.segments == []
        assert cfg.output_width == 0

    def test_empty_config(self):
        """空dict返回默认."""
        cfg = ConcatConfig.from_config_dict({})
        assert cfg.segments == []

    def test_not_dict(self):
        """非dict安全处理."""
        cfg = ConcatConfig.from_config_dict("not_dict")
        assert cfg.segments == []

    def test_with_segments(self):
        """带片段列表."""
        cfg = ConcatConfig.from_config_dict(
            {
                "segments": [
                    {"video_path": "v1.mp4", "duration": 10},
                    {"video_path": "v2.mp4", "start_time": 2},
                ]
            }
        )
        assert len(cfg.segments) == 2
        assert cfg.segments[0].video_path == "v1.mp4"
        assert cfg.segments[0].duration == 10.0
        assert cfg.segments[1].start_time == 2.0

    def test_segments_not_list(self):
        """segments不是list时忽略."""
        cfg = ConcatConfig.from_config_dict({"segments": "not_a_list"})
        assert cfg.segments == []

    def test_skips_segments_without_path(self):
        """跳过没有video_path的片段."""
        cfg = ConcatConfig.from_config_dict(
            {
                "segments": [
                    {"video_path": "v1.mp4"},
                    {"duration": 5},  # 没有video_path
                    {"video_path": ""},  # 空path
                ]
            }
        )
        assert len(cfg.segments) == 1
        assert cfg.segments[0].video_path == "v1.mp4"

    def test_skips_non_dict_segments(self):
        """跳过非dict片段."""
        cfg = ConcatConfig.from_config_dict(
            {
                "segments": [
                    {"video_path": "v1.mp4"},
                    "not_a_dict",
                    None,
                    123,
                ]
            }
        )
        assert len(cfg.segments) == 1

    def test_output_dimensions(self):
        """输出尺寸."""
        cfg = ConcatConfig.from_config_dict(
            {
                "output_width": 1920,
                "output_height": 1080,
            }
        )
        assert cfg.output_width == 1920
        assert cfg.output_height == 1080

    def test_output_dimensions_negative_clamped(self):
        """负的尺寸钳制到0."""
        cfg = ConcatConfig.from_config_dict(
            {
                "output_width": -100,
                "output_height": -50,
            }
        )
        assert cfg.output_width == 0
        assert cfg.output_height == 0

    def test_output_dimensions_invalid(self):
        """无效尺寸回退到0."""
        cfg = ConcatConfig.from_config_dict(
            {
                "output_width": "abc",
                "output_height": "xyz",
            }
        )
        assert cfg.output_width == 0
        assert cfg.output_height == 0

    def test_output_fps(self):
        """输出帧率."""
        cfg = ConcatConfig.from_config_dict({"output_fps": 30.0})
        assert cfg.output_fps == 30.0

    def test_output_fps_negative_clamped(self):
        """负帧率钳制到0."""
        cfg = ConcatConfig.from_config_dict({"output_fps": -5})
        assert cfg.output_fps == 0.0

    def test_force_reencode(self):
        """强制重新编码."""
        cfg = ConcatConfig.from_config_dict({"force_reencode": True})
        assert cfg.force_reencode is True

    def test_force_reencode_default_false(self):
        """默认不强制重编码."""
        cfg = ConcatConfig.from_config_dict({})
        assert cfg.force_reencode is False

    def test_transition(self):
        """转场效果."""
        cfg = ConcatConfig.from_config_dict({"transition": "crossfade"})
        assert cfg.transition == "crossfade"

    def test_transition_default_none(self):
        """默认转场none."""
        cfg = ConcatConfig.from_config_dict({})
        assert cfg.transition == "none"

    def test_transition_duration(self):
        """转场时长."""
        cfg = ConcatConfig.from_config_dict({"transition_duration": 1.0})
        assert cfg.transition_duration == 1.0

    def test_transition_duration_min(self):
        """转场时长最小值0.1."""
        cfg = ConcatConfig.from_config_dict({"transition_duration": 0.01})
        assert cfg.transition_duration == 0.1

    def test_transition_duration_invalid(self):
        """无效转场时长回退到默认."""
        cfg = ConcatConfig.from_config_dict({"transition_duration": "invalid"})
        assert cfg.transition_duration == 0.3


# ── clamp_segments 测试 ──────────────────────────────────────────────────────


class TestClampSegments:
    """clamp_segments 截断测试."""

    def test_under_limit_no_change(self):
        """低于上限时不截断."""
        segs = [ConcatSegment(video_path=f"v{i}.mp4") for i in range(10)]
        cfg = ConcatConfig(segments=segs)
        cfg.clamp_segments(max_segments=50)
        assert len(cfg.segments) == 10

    def test_over_limit_truncated(self):
        """超过上限时截断."""
        segs = [ConcatSegment(video_path=f"v{i}.mp4") for i in range(100)]
        cfg = ConcatConfig(segments=segs)
        cfg.clamp_segments(max_segments=30)
        assert len(cfg.segments) == 30
        assert cfg.segments[0].video_path == "v0.mp4"
        assert cfg.segments[-1].video_path == "v29.mp4"

    def test_default_max_uses_constant(self):
        """默认max_segments使用常量."""
        segs = [ConcatSegment(video_path=f"v{i}.mp4") for i in range(100)]
        cfg = ConcatConfig(segments=segs)
        cfg.clamp_segments()  # 默认MAX_CONCAT_SEGMENTS
        assert len(cfg.segments) == MAX_CONCAT_SEGMENTS

    def test_empty_segments(self):
        """空列表不报错."""
        cfg = ConcatConfig()
        cfg.clamp_segments()
        assert cfg.segments == []

    def test_at_limit_stays(self):
        """刚好在上限时不变."""
        segs = [ConcatSegment(video_path=f"v{i}.mp4") for i in range(50)]
        cfg = ConcatConfig(segments=segs)
        cfg.clamp_segments(max_segments=50)
        assert len(cfg.segments) == 50
