"""trim_config 单元测试 - wave165

覆盖：
- TrimConfig.from_dict 构造
- TrimConfig.validate_and_resolve 三选二推导 + 边界钳制
- TrimConfig.is_valid / is_noop / trim_from_start 属性
- TrimSegment.from_dict 构造
- build_video_trim_filter 视频裁剪滤镜
- build_audio_trim_filter 音频裁剪滤镜
- resolve_segments 多段解析
- parse_segments_from_config 配置解析
- extract_trim_from_clip_config 提取工具
"""

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:
    def test_none_returns_none(self):
        assert TrimConfig.from_dict(None) is None

    def test_empty_dict_returns_none(self):
        assert TrimConfig.from_dict({}) is None

    def test_all_zero_returns_none(self):
        assert TrimConfig.from_dict({"start_time": 0, "end_time": 0, "duration": 0}) is None

    def test_start_only_returns_config(self):
        result = TrimConfig.from_dict({"start_time": 5.0})
        assert result is not None
        assert result.start_time == 5.0
        assert result.end_time == 0
        assert result.duration == 0

    def test_duration_only_returns_config(self):
        result = TrimConfig.from_dict({"duration": 10.0})
        assert result is not None
        assert result.duration == 10.0
        assert result.start_time == 0

    def test_start_and_end(self):
        result = TrimConfig.from_dict({"start_time": 5.0, "end_time": 15.0})
        assert result is not None
        assert result.start_time == 5.0
        assert result.end_time == 15.0

    def test_start_and_duration(self):
        result = TrimConfig.from_dict({"start_time": 2.0, "duration": 8.0})
        assert result is not None
        assert result.start_time == 2.0
        assert result.duration == 8.0

    def test_end_and_duration(self):
        result = TrimConfig.from_dict({"end_time": 20.0, "duration": 5.0})
        assert result is not None
        assert result.end_time == 20.0
        assert result.duration == 5.0

    def test_all_three(self):
        result = TrimConfig.from_dict({"start_time": 1, "end_time": 5, "duration": 4})
        assert result is not None
        assert result.start_time == 1.0

    def test_string_values(self):
        result = TrimConfig.from_dict({"start_time": "3.5", "duration": "2.0"})
        assert result is not None
        assert result.start_time == 3.5
        assert result.duration == 2.0

    def test_falsy_values_treated_as_zero(self):
        result = TrimConfig.from_dict({"start_time": None, "duration": None})
        assert result is None  # 两个都是None等价于0

    def test_zero_start_with_duration(self):
        result = TrimConfig.from_dict({"start_time": 0, "duration": 5.0})
        assert result is not None
        assert result.duration == 5.0


# ============================================================
# validate_and_resolve - 三选二推导
# ============================================================


class TestValidateAndResolveDerive:
    ASSET_DUR = 60.0  # 素材时长60秒

    def test_start_plus_end(self):
        config = TrimConfig(start_time=10.0, end_time=25.0)
        result = config.validate_and_resolve(self.ASSET_DUR)
        assert result.start_time == 10.0
        assert result.end_time == 25.0
        assert result.duration == 15.0

    def test_start_plus_duration(self):
        config = TrimConfig(start_time=5.0, duration=10.0)
        result = config.validate_and_resolve(self.ASSET_DUR)
        assert result.start_time == 5.0
        assert result.end_time == 15.0
        assert result.duration == 10.0

    def test_end_plus_duration(self):
        config = TrimConfig(end_time=30.0, duration=10.0)
        result = config.validate_and_resolve(self.ASSET_DUR)
        assert result.start_time == 20.0
        assert result.end_time == 30.0
        assert result.duration == 10.0

    def test_end_plus_duration_start_negative_clamped(self):
        # end=5, duration=10 → start=-5 → 钳制到0，duration=5
        config = TrimConfig(end_time=5.0, duration=10.0)
        result = config.validate_and_resolve(self.ASSET_DUR)
        assert result.start_time == 0.0
        assert result.end_time == 5.0
        assert result.duration == 5.0

    def test_start_only_takes_to_end(self):
        config = TrimConfig(start_time=50.0)
        result = config.validate_and_resolve(self.ASSET_DUR)
        assert result.start_time == 50.0
        assert result.end_time == 60.0
        assert result.duration == 10.0

    def test_end_only_starts_from_zero(self):
        config = TrimConfig(end_time=30.0)
        result = config.validate_and_resolve(self.ASSET_DUR)
        assert result.start_time == 0.0
        assert result.end_time == 30.0
        assert result.duration == 30.0

    def test_duration_only_starts_from_zero(self):
        config = TrimConfig(duration=20.0)
        result = config.validate_and_resolve(self.ASSET_DUR)
        assert result.start_time == 0.0
        assert result.end_time == 20.0
        assert result.duration == 20.0

    def test_all_three_uses_start_end(self):
        # start=5, end=20, duration=10 → 优先用 start+end 推导 → duration=15
        config = TrimConfig(start_time=5.0, end_time=20.0, duration=10.0)
        result = config.validate_and_resolve(self.ASSET_DUR)
        assert result.start_time == 5.0
        assert result.end_time == 20.0
        assert result.duration == 15.0  # start+end 优先

    def test_start_equals_end_invalid(self):
        config = TrimConfig(start_time=10.0, end_time=10.0)
        result = config.validate_and_resolve(self.ASSET_DUR)
        assert result.duration == 0.0
        assert result.is_valid is False


# ============================================================
# validate_and_resolve - 边界钳制
# ============================================================


class TestValidateAndResolveClamp:
    ASSET_DUR = 60.0

    def test_negative_start_clamped(self):
        config = TrimConfig(start_time=-5.0, duration=10.0)
        result = config.validate_and_resolve(self.ASSET_DUR)
        assert result.start_time == 0.0
        assert result.duration == 10.0

    def test_end_exceeds_asset_duration(self):
        config = TrimConfig(start_time=50.0, duration=20.0)
        result = config.validate_and_resolve(self.ASSET_DUR)
        assert result.end_time == 60.0
        assert result.duration == 10.0

    def test_start_exceeds_asset_duration(self):
        config = TrimConfig(start_time=70.0, duration=5.0)
        result = config.validate_and_resolve(self.ASSET_DUR)
        assert result.start_time < self.ASSET_DUR
        assert result.end_time == self.ASSET_DUR

    def test_zero_asset_duration_returns_noop(self):
        config = TrimConfig(start_time=5.0, duration=10.0)
        result = config.validate_and_resolve(0.0)
        assert result.is_noop is True

    def test_negative_asset_duration_returns_noop(self):
        config = TrimConfig(start_time=5.0, duration=10.0)
        result = config.validate_and_resolve(-1.0)
        assert result.is_noop is True

    def test_no_params_returns_noop(self):
        config = TrimConfig()
        result = config.validate_and_resolve(self.ASSET_DUR)
        assert result.is_noop is True


# ============================================================
# 属性 is_valid / is_noop / trim_from_start
# ============================================================


class TestTrimConfigProperties:
    def test_is_valid_true(self):
        config = TrimConfig(start_time=0, end_time=0, duration=5.0)
        assert config.is_valid is True

    def test_is_valid_false_zero(self):
        config = TrimConfig(duration=0.0)
        assert config.is_valid is False

    def test_is_valid_false_negative(self):
        config = TrimConfig(duration=-1.0)
        assert config.is_valid is False

    def test_is_valid_exactly_minimum(self):
        config = TrimConfig(duration=MIN_TRIM_DURATION)
        assert config.is_valid is True

    def test_is_valid_below_minimum(self):
        config = TrimConfig(duration=MIN_TRIM_DURATION / 2)
        assert config.is_valid is False

    def test_is_noop_true(self):
        config = TrimConfig()
        assert config.is_noop is True

    def test_is_noop_false_with_start(self):
        config = TrimConfig(start_time=5.0)
        assert config.is_noop is False

    def test_is_noop_false_with_duration(self):
        config = TrimConfig(duration=10.0)
        assert config.is_noop is False

    def test_trim_from_start_true(self):
        config = TrimConfig(start_time=0.0, duration=10.0)
        assert config.trim_from_start is True

    def test_trim_from_start_false(self):
        config = TrimConfig(start_time=5.0, duration=10.0)
        assert config.trim_from_start is False

    def test_trim_from_start_negative_treated_as_start(self):
        # trim_from_start 检查 start_time <= 0，负值也算从开头
        config = TrimConfig(start_time=-1.0)
        assert config.trim_from_start is True


# ============================================================
# TrimSegment
# ============================================================


class TestTrimSegment:
    def test_from_dict_minimal(self):
        seg = TrimSegment.from_dict({"start_time": 5.0, "duration": 10.0})
        assert seg.segment_id  # 有默认值
        assert seg.trim.start_time == 5.0
        assert seg.trim.duration == 10.0
        assert seg.order == 0

    def test_from_dict_with_segment_id(self):
        seg = TrimSegment.from_dict({"segment_id": "seg_abc", "start_time": 1.0, "duration": 2.0})
        assert seg.segment_id == "seg_abc"

    def test_from_dict_with_order(self):
        seg = TrimSegment.from_dict({"order": 3, "start_time": 1.0, "duration": 2.0})
        assert seg.order == 3

    def test_from_dict_default_order(self):
        seg = TrimSegment.from_dict({"start_time": 1.0}, default_order=5)
        assert seg.order == 5

    def test_segment_dataclass(self):
        seg = TrimSegment(
            segment_id="s1",
            trim=TrimConfig(start_time=0, duration=5),
            order=1,
        )
        assert seg.segment_id == "s1"
        assert seg.order == 1
        assert seg.trim.duration == 5.0


# ============================================================
# build_video_trim_filter
# ============================================================


class TestBuildVideoTrimFilter:
    def test_noop_passthrough(self):
        trim = TrimConfig()
        result = build_video_trim_filter("[0:v]", trim, "[out]")
        assert result == "[0:v]setpts=PTS-STARTPTS[out]"

    def test_with_start_and_duration(self):
        trim = TrimConfig(start_time=5.5, duration=10.0).validate_and_resolve(60.0)
        result = build_video_trim_filter("[0:v]", trim, "[v0]")
        assert "trim=" in result
        assert "start=5.500" in result
        assert "duration=10.000" in result
        assert "setpts=PTS-STARTPTS" in result
        assert result.startswith("[0:v]")
        assert result.endswith("[v0]")

    def test_start_at_zero(self):
        trim = TrimConfig(duration=5.0).validate_and_resolve(60.0)
        result = build_video_trim_filter("[in]", trim, "[out]")
        assert "trim=" in result
        assert "start=" not in result  # start=0 不写
        assert "duration=5.000" in result

    def test_trim_only_start(self):
        # 只有 start，duration 由素材推导
        trim = TrimConfig(start_time=10.0).validate_and_resolve(30.0)
        result = build_video_trim_filter("[0:v]", trim, "[out]")
        assert "start=10.000" in result
        assert "duration=20.000" in result

    def test_comma_separated_filters(self):
        trim = TrimConfig(start_time=1.0, duration=2.0).validate_and_resolve(10.0)
        result = build_video_trim_filter("[in]", trim, "[out]")
        assert "trim=" in result
        # trim 和 setpts 用逗号分隔
        assert "setpts=PTS-STARTPTS" in result


# ============================================================
# build_audio_trim_filter
# ============================================================


class TestBuildAudioTrimFilter:
    def test_noop_passthrough(self):
        trim = TrimConfig()
        result = build_audio_trim_filter("[0:a]", trim, "[out]")
        assert result == "[0:a]asetpts=PTS-STARTPTS[out]"

    def test_with_start_and_duration(self):
        trim = TrimConfig(start_time=3.0, duration=7.0).validate_and_resolve(60.0)
        result = build_audio_trim_filter("[0:a]", trim, "[a0]")
        assert "atrim=" in result
        assert "start=3.000" in result
        assert "duration=7.000" in result
        assert "asetpts=PTS-STARTPTS" in result
        assert result.startswith("[0:a]")
        assert result.endswith("[a0]")

    def test_start_at_zero(self):
        trim = TrimConfig(duration=5.0).validate_and_resolve(60.0)
        result = build_audio_trim_filter("[in]", trim, "[out]")
        assert "atrim=" in result
        assert "start=" not in result
        assert "duration=5.000" in result

    def test_uses_atrim_not_trim(self):
        trim = TrimConfig(start_time=1.0, duration=2.0).validate_and_resolve(10.0)
        result = build_audio_trim_filter("[in]", trim, "[out]")
        assert "atrim=" in result
        # 不应该有单独的 trim=（即视频的 trim）
        # 注意：atrim= 包含 "trim=" 子串，所以检查完整的


# ============================================================
# resolve_segments
# ============================================================


class TestResolveSegments:
    def test_empty_list(self):
        result = resolve_segments([], 60.0)
        assert result == []

    def test_single_segment(self):
        segs = [
            TrimSegment(
                segment_id="s1",
                trim=TrimConfig(start_time=0, duration=10.0),
                order=0,
            )
        ]
        result = resolve_segments(segs, 60.0)
        assert len(result) == 1
        assert result[0].segment_id == "s1"
        assert result[0].trim.duration == 10.0

    def test_invalid_segment_skipped(self):
        segs = [
            TrimSegment(
                segment_id="good",
                trim=TrimConfig(start_time=0, duration=10.0),
                order=0,
            ),
            TrimSegment(
                segment_id="bad",
                trim=TrimConfig(start_time=10, end_time=10),  # 0时长
                order=1,
            ),
        ]
        result = resolve_segments(segs, 60.0)
        assert len(result) == 1
        assert result[0].segment_id == "good"

    def test_sorted_by_order(self):
        segs = [
            TrimSegment(
                segment_id="third",
                trim=TrimConfig(duration=1.0),
                order=2,
            ),
            TrimSegment(
                segment_id="first",
                trim=TrimConfig(duration=1.0),
                order=0,
            ),
            TrimSegment(
                segment_id="second",
                trim=TrimConfig(duration=1.0),
                order=1,
            ),
        ]
        result = resolve_segments(segs, 60.0)
        assert len(result) == 3
        assert [s.segment_id for s in result] == ["first", "second", "third"]

    def test_negative_order_uses_index(self):
        segs = [
            TrimSegment(
                segment_id="s0",
                trim=TrimConfig(duration=1.0),
                order=-1,
            ),
            TrimSegment(
                segment_id="s1",
                trim=TrimConfig(duration=1.0),
                order=-1,
            ),
        ]
        result = resolve_segments(segs, 60.0)
        assert len(result) == 2
        # order 为负时使用索引 i，所以 s0 order=0, s1 order=1
        assert result[0].segment_id == "s0"
        assert result[1].segment_id == "s1"

    def test_respects_asset_duration(self):
        segs = [
            TrimSegment(
                segment_id="s1",
                trim=TrimConfig(start_time=50, duration=20.0),  # 超出60s素材
                order=0,
            )
        ]
        result = resolve_segments(segs, 60.0)
        assert len(result) == 1
        assert result[0].trim.end_time == 60.0
        assert result[0].trim.duration == 10.0


# ============================================================
# parse_segments_from_config
# ============================================================


class TestParseSegmentsFromConfig:
    def test_none_returns_empty(self):
        assert parse_segments_from_config(None) == []

    def test_empty_dict_returns_empty(self):
        assert parse_segments_from_config({}) == []

    def test_trim_segments_list(self):
        config = {
            "trim_segments": [
                {"segment_id": "s1", "start_time": 0, "duration": 5, "order": 0},
                {"segment_id": "s2", "start_time": 10, "duration": 5, "order": 1},
            ]
        }
        result = parse_segments_from_config(config)
        assert len(result) == 2
        assert result[0].segment_id == "s1"
        assert result[1].segment_id == "s2"

    def test_trim_segments_empty_list(self):
        config = {"trim_segments": []}
        result = parse_segments_from_config(config)
        assert result == []

    def test_trim_segments_not_list_ignored(self):
        config = {"trim_segments": "not_a_list"}
        result = parse_segments_from_config(config)
        assert result == []

    def test_single_trim_start(self):
        config = {"trim_start": 5.0}
        result = parse_segments_from_config(config)
        assert len(result) == 1
        assert result[0].segment_id == "main"
        assert result[0].trim.start_time == 5.0

    def test_single_trim_end(self):
        config = {"trim_end": 30.0}
        result = parse_segments_from_config(config)
        assert len(result) == 1
        assert result[0].trim.end_time == 30.0

    def test_single_trim_duration(self):
        config = {"trim_duration": 10.0}
        result = parse_segments_from_config(config)
        assert len(result) == 1
        assert result[0].trim.duration == 10.0

    def test_single_full(self):
        config = {"trim_start": 2.0, "trim_end": 8.0, "trim_duration": 6.0}
        result = parse_segments_from_config(config)
        assert len(result) == 1
        assert result[0].trim.start_time == 2.0

    def test_segments_priority_over_single(self):
        # 同时有 trim_segments 和单段字段，优先多段
        config = {
            "trim_segments": [
                {"segment_id": "s1", "start_time": 0, "duration": 5},
            ],
            "trim_start": 10.0,
        }
        result = parse_segments_from_config(config)
        assert len(result) == 1
        assert result[0].segment_id == "s1"

    def test_segment_dict_with_non_dict_entries_ignored(self):
        config = {
            "trim_segments": [
                {"segment_id": "s1", "start_time": 0, "duration": 5},
                "not_a_dict",
                None,
            ]
        }
        result = parse_segments_from_config(config)
        assert len(result) == 1


# ============================================================
# extract_trim_from_clip_config
# ============================================================


class TestExtractTrimFromClipConfig:
    def test_none_returns_none(self):
        assert extract_trim_from_clip_config(None) is None

    def test_empty_dict_returns_none(self):
        assert extract_trim_from_clip_config({}) is None

    def test_trim_subdict(self):
        config = {"trim": {"start_time": 5.0, "duration": 10.0}}
        result = extract_trim_from_clip_config(config)
        assert result is not None
        assert result.start_time == 5.0
        assert result.duration == 10.0

    def test_trim_subdict_empty(self):
        config = {"trim": {}}
        result = extract_trim_from_clip_config(config)
        assert result is None

    def test_flat_trim_start(self):
        config = {"trim_start": 5.0, "trim_duration": 10.0}
        result = extract_trim_from_clip_config(config)
        assert result is not None
        assert result.start_time == 5.0
        assert result.duration == 10.0

    def test_flat_trim_end_only(self):
        config = {"trim_end": 20.0}
        result = extract_trim_from_clip_config(config)
        assert result is not None
        assert result.end_time == 20.0

    def test_trim_subdict_priority_over_flat(self):
        # trim子字典优先
        config = {
            "trim": {"start_time": 1.0, "duration": 2.0},
            "trim_start": 10.0,
            "trim_duration": 20.0,
        }
        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_no_trim_fields(self):
        config = {"other_field": "value"}
        assert extract_trim_from_clip_config(config) is None

    def test_trim_not_dict_ignored(self):
        config = {"trim": "not_a_dict"}
        # trim 不是 dict，继续看扁平字段 → 没有 → None
        result = extract_trim_from_clip_config(config)
        assert result is None
