"""trim_config 裁剪配置领域模型单元测试."""

from __future__ import annotations

import pytest

from packages.domain.trim_config import (
    MIN_TRIM_DURATION,
    TrimConfig,
    TrimSegment,
    build_audio_trim_filter,
    build_video_trim_filter,
    extract_trim_from_clip_config,
    parse_segments_from_config,
    resolve_segments,
)

# ── TrimConfig.from_dict 测试 ────────────────────────────────────────────────


class TestTrimConfigFromDict:
    """TrimConfig.from_dict 测试."""

    def test_none_returns_none(self):
        """None返回None."""
        assert TrimConfig.from_dict(None) is None

    def test_empty_dict_returns_none(self):
        """空dict返回None."""
        assert TrimConfig.from_dict({}) is None

    def test_all_zero_returns_none(self):
        """全0返回None（不裁剪）."""
        assert TrimConfig.from_dict({"start_time": 0, "end_time": 0, "duration": 0}) is None

    def test_start_and_end(self):
        """start + end."""
        result = TrimConfig.from_dict({"start_time": 5, "end_time": 10})
        assert result is not None
        assert result.start_time == 5.0
        assert result.end_time == 10.0

    def test_start_and_duration(self):
        """start + duration."""
        result = TrimConfig.from_dict({"start_time": 2, "duration": 5})
        assert result is not None
        assert result.start_time == 2.0
        assert result.duration == 5.0

    def test_end_and_duration(self):
        """end + duration."""
        result = TrimConfig.from_dict({"end_time": 10, "duration": 3})
        assert result is not None
        assert result.end_time == 10.0
        assert result.duration == 3.0

    def test_only_start_returns_config(self):
        """只有start_time也返回有效配置（从start取到末尾语义）."""
        result = TrimConfig.from_dict({"start_time": 3})
        assert result is not None
        assert result.start_time == 3.0

    def test_only_duration_returns_config(self):
        """只有duration也返回（从开头取duration）."""
        result = TrimConfig.from_dict({"duration": 5})
        assert result is not None
        assert result.duration == 5.0

    def test_only_end_returns_config(self):
        """只有end_time也返回."""
        result = TrimConfig.from_dict({"end_time": 8})
        assert result is not None
        assert result.end_time == 8.0

    def test_string_values(self):
        """字符串值能正确解析."""
        result = TrimConfig.from_dict({"start_time": "2.5", "duration": "3"})
        assert result is not None
        assert result.start_time == 2.5
        assert result.duration == 3.0

    def test_none_values_treated_as_zero(self):
        """None值当作0处理."""
        result = TrimConfig.from_dict({"start_time": None, "duration": 5})
        assert result is not None
        assert result.start_time == 0.0
        assert result.duration == 5.0

    def test_false_values_treated_as_zero(self):
        """0/false值当作0处理."""
        result = TrimConfig.from_dict({"start_time": 0, "duration": 0})
        assert result is None

    def test_all_three_params(self):
        """三个参数都给了."""
        result = TrimConfig.from_dict({"start_time": 1, "end_time": 6, "duration": 5})
        assert result is not None
        assert result.start_time == 1.0
        assert result.end_time == 6.0
        assert result.duration == 5.0


# ── TrimConfig.validate_and_resolve 测试 ────────────────────────────────────


class TestTrimConfigValidateAndResolve:
    """validate_and_resolve 三选二推导 + 边界钳制测试."""

    # 基础三选二推导

    def test_start_and_end(self):
        """start + end → 推导duration."""
        cfg = TrimConfig(start_time=5, end_time=15)
        result = cfg.validate_and_resolve(asset_duration=30)
        assert result.start_time == 5.0
        assert result.end_time == 15.0
        assert result.duration == 10.0

    def test_start_and_duration(self):
        """start + duration → 推导end."""
        cfg = TrimConfig(start_time=3, duration=7)
        result = cfg.validate_and_resolve(asset_duration=30)
        assert result.start_time == 3.0
        assert result.duration == 7.0
        assert result.end_time == 10.0

    def test_end_and_duration(self):
        """end + duration → 推导start."""
        cfg = TrimConfig(end_time=20, duration=5)
        result = cfg.validate_and_resolve(asset_duration=30)
        assert result.end_time == 20.0
        assert result.duration == 5.0
        assert result.start_time == 15.0

    def test_end_minus_duration_negative(self):
        """end + duration 但算出start<0 → 钳制到0重新计算."""
        cfg = TrimConfig(end_time=3, duration=10)
        result = cfg.validate_and_resolve(asset_duration=30)
        assert result.start_time == 0.0
        assert result.end_time == 3.0
        assert result.duration == 3.0

    def test_only_start_takes_to_end(self):
        """只有start → 取到素材末尾."""
        cfg = TrimConfig(start_time=5)
        result = cfg.validate_and_resolve(asset_duration=30)
        assert result.start_time == 5.0
        assert result.end_time == 30.0
        assert result.duration == 25.0

    def test_only_end_takes_from_start(self):
        """只有end → 从开头取到end."""
        cfg = TrimConfig(end_time=10)
        result = cfg.validate_and_resolve(asset_duration=30)
        assert result.start_time == 0.0
        assert result.end_time == 10.0
        assert result.duration == 10.0

    def test_only_duration(self):
        """只有duration → 从开头取duration."""
        cfg = TrimConfig(duration=8)
        result = cfg.validate_and_resolve(asset_duration=30)
        assert result.start_time == 0.0
        assert result.end_time == 8.0
        assert result.duration == 8.0

    def test_all_zero_noop(self):
        """全0 → noop不裁剪."""
        cfg = TrimConfig()
        result = cfg.validate_and_resolve(asset_duration=30)
        assert result.start_time == 0.0
        assert result.duration == 0.0
        assert result.is_noop

    # 边界钳制

    def test_start_negative_clamped(self):
        """start为负 → 钳制到0."""
        cfg = TrimConfig(start_time=-5, duration=10)
        result = cfg.validate_and_resolve(asset_duration=30)
        assert result.start_time == 0.0
        assert result.duration == 10.0
        assert result.end_time == 10.0

    def test_end_exceeds_asset_duration(self):
        """end超过素材时长 → 钳制."""
        cfg = TrimConfig(start_time=5, end_time=50)
        result = cfg.validate_and_resolve(asset_duration=30)
        assert result.end_time == 30.0
        assert result.duration == 25.0

    def test_start_exceeds_asset_duration(self):
        """start超过素材时长 → 移到末尾取最小片段."""
        cfg = TrimConfig(start_time=40, duration=5)
        result = cfg.validate_and_resolve(asset_duration=30)
        assert result.end_time == 30.0
        assert result.start_time >= 0
        assert result.duration >= 0

    def test_start_equals_end_invalid(self):
        """start >= end → 无效（duration=0）."""
        cfg = TrimConfig(start_time=10, end_time=10)
        result = cfg.validate_and_resolve(asset_duration=30)
        assert result.duration == 0.0
        assert result.is_valid is False

    def test_start_greater_than_end(self):
        """start > end → 无效."""
        cfg = TrimConfig(start_time=15, end_time=10)
        result = cfg.validate_and_resolve(asset_duration=30)
        assert result.duration == 0.0
        assert result.is_valid is False

    def test_zero_asset_duration(self):
        """素材时长为0 → 返回noop."""
        cfg = TrimConfig(start_time=5, duration=10)
        result = cfg.validate_and_resolve(asset_duration=0)
        assert result.start_time == 0.0
        assert result.duration == 0.0
        assert result.is_noop

    def test_negative_asset_duration(self):
        """素材时长为负 → 返回noop."""
        cfg = TrimConfig(start_time=1, duration=2)
        result = cfg.validate_and_resolve(asset_duration=-5)
        assert result.is_noop

    # duration 边界

    def test_duration_preserved_exactly(self):
        """精确时长保持."""
        cfg = TrimConfig(start_time=1.234, duration=2.567)
        result = cfg.validate_and_resolve(asset_duration=10)
        assert abs(result.duration - 2.567) < 0.001
        assert abs(result.start_time - 1.234) < 0.001

    def test_duration_never_negative(self):
        """duration永远不为负."""
        cfg = TrimConfig(start_time=10, end_time=5)
        result = cfg.validate_and_resolve(asset_duration=30)
        assert result.duration >= 0


# ── TrimConfig 属性测试 ─────────────────────────────────────────────────────


class TestTrimConfigProperties:
    """TrimConfig 属性测试."""

    def test_is_valid_valid_trim(self):
        """有效裁剪."""
        cfg = TrimConfig(start_time=0, end_time=0, duration=5)
        assert cfg.is_valid is True

    def test_is_valid_zero_duration(self):
        """duration=0无效."""
        cfg = TrimConfig(duration=0)
        assert cfg.is_valid is False

    def test_is_valid_min_threshold(self):
        """刚好等于最小阈值也算有效."""
        cfg = TrimConfig(duration=MIN_TRIM_DURATION)
        assert cfg.is_valid is True

    def test_is_valid_below_min(self):
        """低于最小阈值无效."""
        cfg = TrimConfig(duration=MIN_TRIM_DURATION / 2)
        assert cfg.is_valid is False

    def test_is_noop_true(self):
        """从0开始且duration=0是noop."""
        cfg = TrimConfig(start_time=0, duration=0)
        assert cfg.is_noop is True

    def test_is_noop_false_has_start(self):
        """有start不是noop."""
        cfg = TrimConfig(start_time=5, duration=0)
        assert cfg.is_noop is False

    def test_is_noop_false_has_duration(self):
        """有duration不是noop."""
        cfg = TrimConfig(start_time=0, duration=1)
        assert cfg.is_noop is False

    def test_trim_from_start_true(self):
        """start=0是从开头裁剪."""
        cfg = TrimConfig(start_time=0, duration=5)
        assert cfg.trim_from_start is True

    def test_trim_from_start_false(self):
        """start>0不是从开头裁剪."""
        cfg = TrimConfig(start_time=2, duration=5)
        assert cfg.trim_from_start is False

    def test_trim_from_start_negative_treated_as_zero(self):
        """start<0也认为从开头."""
        cfg = TrimConfig(start_time=-1, duration=5)
        assert cfg.trim_from_start is True


# ── TrimSegment 测试 ─────────────────────────────────────────────────────────


class TestTrimSegment:
    """TrimSegment 测试."""

    def test_from_dict_basic(self):
        """基础构造."""
        data = {"segment_id": "seg1", "start_time": 1, "end_time": 5, "order": 2}
        seg = TrimSegment.from_dict(data)
        assert seg.segment_id == "seg1"
        assert seg.order == 2
        assert seg.trim.start_time == 1.0
        assert seg.trim.end_time == 5.0

    def test_from_dict_default_order(self):
        """缺order使用默认值."""
        data = {"segment_id": "s1", "start_time": 0, "duration": 3}
        seg = TrimSegment.from_dict(data, default_order=5)
        assert seg.order == 5

    def test_from_dict_missing_segment_id(self):
        """缺segment_id用默认名."""
        data = {"start_time": 0, "duration": 2}
        seg = TrimSegment.from_dict(data, default_order=3)
        assert seg.segment_id == "seg_3"

    def test_from_dict_duration(self):
        """duration正确传递."""
        data = {"segment_id": "s1", "duration": 10}
        seg = TrimSegment.from_dict(data)
        assert seg.trim.duration == 10.0


# ── build_video_trim_filter 测试 ────────────────────────────────────────────


class TestBuildVideoTrimFilter:
    """build_video_trim_filter 视频滤镜构建测试."""

    def test_noop_filter(self):
        """noop时只有setpts."""
        cfg = TrimConfig()
        result = build_video_trim_filter("[0:v]", cfg, "[vout]")
        assert "trim=" not in result
        assert "setpts=PTS-STARTPTS" in result
        assert result.startswith("[0:v]")
        assert result.endswith("[vout]")

    def test_start_and_duration(self):
        """start + duration 完整滤镜."""
        cfg = TrimConfig(start_time=10, end_time=15, duration=5)
        result = build_video_trim_filter("[0:v]", cfg, "[v0]")
        assert "trim=start=10.000:duration=5.000" in result
        assert "setpts=PTS-STARTPTS" in result
        assert result.startswith("[0:v]")
        assert result.endswith("[v0]")

    def test_only_start(self):
        """只有start（取到末尾的情况resolve后也有duration）."""
        cfg = TrimConfig(start_time=5, end_time=30, duration=25)
        result = build_video_trim_filter("[1:v]", cfg, "[v1]")
        assert "start=5.000" in result
        assert "duration=25.000" in result

    def test_only_duration_from_start(self):
        """从开头裁剪duration."""
        cfg = TrimConfig(start_time=0, end_time=3, duration=3)
        result = build_video_trim_filter("[0:v]", cfg, "[out]")
        assert "trim=duration=3.000" in result or "trim=start=0" in result
        # start=0 不写，只有duration
        assert "start=0" not in result

    def test_preserves_input_output_labels(self):
        """保持输入输出标签."""
        cfg = TrimConfig(start_time=1, duration=2)
        result = build_video_trim_filter("[in_label]", cfg, "[out_label]")
        assert result.startswith("[in_label]")
        assert result.endswith("[out_label]")

    def test_three_decimal_precision(self):
        """三位小数精度."""
        cfg = TrimConfig(start_time=1.234, duration=2.678)
        result = build_video_trim_filter("[0:v]", cfg, "[v]")
        assert "start=1.234" in result
        assert "duration=2.678" in result


# ── build_audio_trim_filter 测试 ────────────────────────────────────────────


class TestBuildAudioTrimFilter:
    """build_audio_trim_filter 音频滤镜构建测试."""

    def test_noop_filter(self):
        """noop时只有asetpts."""
        cfg = TrimConfig()
        result = build_audio_trim_filter("[0:a]", cfg, "[aout]")
        assert "atrim=" not in result
        assert "asetpts=PTS-STARTPTS" in result
        assert result.startswith("[0:a]")
        assert result.endswith("[aout]")

    def test_start_and_duration(self):
        """start + duration 完整滤镜."""
        cfg = TrimConfig(start_time=5, duration=3)
        result = build_audio_trim_filter("[0:a]", cfg, "[a0]")
        assert "atrim=start=5.000:duration=3.000" in result
        assert "asetpts=PTS-STARTPTS" in result

    def test_only_duration(self):
        """只有duration（start=0时不写start参数）."""
        cfg = TrimConfig(start_time=0, duration=4)
        result = build_audio_trim_filter("[0:a]", cfg, "[a0]")
        assert "atrim=duration=4.000" in result

    def test_uses_atrim_not_trim(self):
        """用atrim不是trim."""
        cfg = TrimConfig(start_time=1, duration=2)
        result = build_audio_trim_filter("[0:a]", cfg, "[a]")
        assert "atrim=" in result
        assert ",trim=" not in result


# ── resolve_segments 测试 ────────────────────────────────────────────────────


class TestResolveSegments:
    """resolve_segments 多段裁剪解析测试."""

    def test_empty_list(self):
        """空列表返回空."""
        result = resolve_segments([], asset_duration=30)
        assert result == []

    def test_single_segment(self):
        """单段解析."""
        seg = TrimSegment(
            segment_id="s1",
            trim=TrimConfig(start_time=0, duration=5),
            order=0,
        )
        result = resolve_segments([seg], asset_duration=30)
        assert len(result) == 1
        assert result[0].segment_id == "s1"
        assert result[0].trim.duration == 5.0

    def test_multiple_segments_sorted(self):
        """多段按order排序."""
        segs = [
            TrimSegment(segment_id="s1", trim=TrimConfig(duration=2), order=2),
            TrimSegment(segment_id="s2", trim=TrimConfig(duration=3), order=0),
            TrimSegment(segment_id="s3", trim=TrimConfig(duration=1), order=1),
        ]
        result = resolve_segments(segs, asset_duration=30)
        assert len(result) == 3
        assert result[0].segment_id == "s2"
        assert result[1].segment_id == "s3"
        assert result[2].segment_id == "s1"

    def test_filter_invalid_segments(self):
        """过滤无效段."""
        segs = [
            TrimSegment(segment_id="valid", trim=TrimConfig(duration=5), order=0),
            TrimSegment(segment_id="invalid", trim=TrimConfig(duration=0), order=1),
        ]
        result = resolve_segments(segs, asset_duration=30)
        assert len(result) == 1
        assert result[0].segment_id == "valid"

    def test_negative_order_uses_index(self):
        """order为负时使用索引."""
        segs = [
            TrimSegment(segment_id="s1", trim=TrimConfig(duration=2), order=-1),
            TrimSegment(segment_id="s2", trim=TrimConfig(duration=3), order=-1),
        ]
        result = resolve_segments(segs, asset_duration=30)
        assert len(result) == 2
        # order用各自的index值(0, 1)

    def test_resolves_with_asset_duration(self):
        """用素材时长做边界钳制."""
        seg = TrimSegment(
            segment_id="s1",
            trim=TrimConfig(start_time=0, duration=50),  # 超过素材时长
            order=0,
        )
        result = resolve_segments([seg], asset_duration=30)
        assert len(result) == 1
        assert result[0].trim.end_time == 30.0
        assert result[0].trim.duration == 30.0


# ── parse_segments_from_config 测试 ─────────────────────────────────────────


class TestParseSegmentsFromConfig:
    """parse_segments_from_config 测试."""

    def test_none_config(self):
        """None返回空."""
        assert parse_segments_from_config(None) == []

    def test_empty_config(self):
        """空dict返回空."""
        assert parse_segments_from_config({}) == []

    def test_trim_segments_list(self):
        """多段配置解析."""
        config = {
            "trim_segments": [
                {"segment_id": "s1", "start_time": 0, "duration": 3, "order": 0},
                {"segment_id": "s2", "start_time": 5, "duration": 4, "order": 1},
            ]
        }
        result = parse_segments_from_config(config)
        assert len(result) == 2
        assert result[0].segment_id == "s1"
        assert result[0].trim.duration == 3.0
        assert result[1].segment_id == "s2"
        assert result[1].trim.start_time == 5.0

    def test_trim_segments_empty_list(self):
        """空segments列表 + 无单段 → 空."""
        config = {"trim_segments": []}
        assert parse_segments_from_config(config) == []

    def test_trim_segments_not_list(self):
        """segments不是list → 回退到单段（如果有）."""
        config = {"trim_segments": "not_a_list"}
        assert parse_segments_from_config(config) == []

    def test_single_trim_start(self):
        """单段：trim_start."""
        config = {"trim_start": 2, "trim_duration": 5}
        result = parse_segments_from_config(config)
        assert len(result) == 1
        assert result[0].segment_id == "main"
        assert result[0].trim.start_time == 2.0
        assert result[0].trim.duration == 5.0

    def test_single_trim_end(self):
        """单段：trim_end."""
        config = {"trim_end": 10}
        result = parse_segments_from_config(config)
        assert len(result) == 1
        assert result[0].trim.end_time == 10.0

    def test_segments_take_priority_over_single(self):
        """多段配置优先于单段."""
        config = {
            "trim_segments": [{"segment_id": "s1", "start_time": 0, "duration": 2}],
            "trim_start": 5,
            "trim_duration": 3,
        }
        result = parse_segments_from_config(config)
        assert len(result) == 1
        assert result[0].segment_id == "s1"  # 多段优先

    def test_segments_filter_non_dict(self):
        """过滤非dict元素."""
        config = {
            "trim_segments": [
                {"segment_id": "s1", "duration": 2},
                "not_a_dict",
                None,
                123,
            ]
        }
        result = parse_segments_from_config(config)
        assert len(result) == 1
        assert result[0].segment_id == "s1"


# ── extract_trim_from_clip_config 测试 ──────────────────────────────────────


class TestExtractTrimFromClipConfig:
    """extract_trim_from_clip_config 测试."""

    def test_none_config(self):
        """None返回None."""
        assert extract_trim_from_clip_config(None) is None

    def test_empty_config(self):
        """空dict返回None."""
        assert extract_trim_from_clip_config({}) is None

    def test_trim_subdict(self):
        """trim子字典提取."""
        config = {"trim": {"start_time": 2, "duration": 5}}
        result = extract_trim_from_clip_config(config)
        assert result is not None
        assert result.start_time == 2.0
        assert result.duration == 5.0

    def test_trim_subdict_empty(self):
        """trim子字典为空 → None."""
        config = {"trim": {}}
        assert extract_trim_from_clip_config(config) is None

    def test_flat_trim_fields(self):
        """扁平trim_字段."""
        config = {"trim_start": 1, "trim_end": 6}
        result = extract_trim_from_clip_config(config)
        assert result is not None
        assert result.start_time == 1.0
        assert result.end_time == 6.0

    def test_flat_trim_duration(self):
        """扁平trim_duration."""
        config = {"trim_duration": 10}
        result = extract_trim_from_clip_config(config)
        assert result is not None
        assert result.duration == 10.0

    def test_trim_subdict_priority(self):
        """trim子字典优先于扁平字段."""
        config = {
            "trim": {"start_time": 1, "duration": 2},
            "trim_start": 10,
            "trim_duration": 20,
        }
        result = extract_trim_from_clip_config(config)
        assert result is not None
        assert result.start_time == 1.0
        assert result.duration == 2.0

    def test_trim_not_dict_ignored(self):
        """trim不是dict时忽略（回退到扁平字段）."""
        config = {"trim": "not_a_dict", "trim_duration": 5}
        result = extract_trim_from_clip_config(config)
        assert result is not None
        assert result.duration == 5.0

    def test_no_trim_fields(self):
        """无裁剪字段返回None."""
        config = {"other_field": "value", "font_size": 12}
        assert extract_trim_from_clip_config(config) is None
