"""plan_generator_utils 单元测试 - wave166

覆盖：
- distribute_assets 素材分配（4种模式 + 边界）
- map_clip_types_for_mode clip类型映射（4种模式）
- generate_default_clips 默认片段生成（4种模式 + 边界）
- create_clips_from_configs 从模板配置创建
"""

import pytest

from packages.domain.edit_plan_clip import EditPlanClip
from packages.domain.editing_mode import EditingMode
from packages.domain.plan_generator_utils import (
    DEFAULT_CLIP_DURATION,
    create_clips_from_configs,
    distribute_assets,
    generate_default_clips,
    map_clip_types_for_mode,
)
from packages.domain.template_clip_config import ClipType, TemplateClipConfig

# ============================================================
# 辅助函数
# ============================================================


def _make_main_clip(plan_id: str, order: int = 0) -> EditPlanClip:
    return EditPlanClip.create(
        plan_id=plan_id,
        clip_type=ClipType.MAIN.value,
        order=order,
        duration=5.0,
    )


def _make_clips(plan_id: str, count: int, clip_type: str = "main") -> list[EditPlanClip]:
    return [
        EditPlanClip.create(
            plan_id=plan_id,
            clip_type=clip_type,
            order=i,
            duration=5.0,
        )
        for i in range(count)
    ]


# ============================================================
# distribute_assets - ONE_TAKE
# ============================================================


class TestDistributeOneTake:
    def test_equal_count(self):
        clips = _make_clips("p1", 3)
        assets = ["a1", "a2", "a3"]
        distribute_assets(clips, assets, EditingMode.ONE_TAKE.value)
        assert clips[0].asset_id == "a1"
        assert clips[1].asset_id == "a2"
        assert clips[2].asset_id == "a3"

    def test_more_clips_than_assets(self):
        clips = _make_clips("p1", 5)
        assets = ["a1", "a2"]
        distribute_assets(clips, assets, EditingMode.ONE_TAKE.value)
        assert clips[0].asset_id == "a1"
        assert clips[1].asset_id == "a2"
        assert clips[2].asset_id == ""  # 没分配到

    def test_more_assets_than_clips(self):
        clips = _make_clips("p1", 2)
        assets = ["a1", "a2", "a3"]
        distribute_assets(clips, assets, EditingMode.ONE_TAKE.value)
        assert clips[0].asset_id == "a1"
        assert clips[1].asset_id == "a2"

    def test_empty_assets(self):
        clips = _make_clips("p1", 3)
        distribute_assets(clips, [], EditingMode.ONE_TAKE.value)
        for c in clips:
            assert c.asset_id == ""

    def test_empty_clips(self):
        # 不报错即可
        distribute_assets([], ["a1", "a2"], EditingMode.ONE_TAKE.value)

    def test_only_main_clips_get_assigned(self):
        # intro/outro 不应该被分配
        clips = []
        clips.append(EditPlanClip.create("p1", clip_type="intro", order=0, duration=3.0))
        clips.append(_make_main_clip("p1", order=1))
        clips.append(EditPlanClip.create("p1", clip_type="outro", order=2, duration=3.0))
        assets = ["a1"]
        distribute_assets(clips, assets, EditingMode.ONE_TAKE.value)
        assert clips[0].asset_id == ""  # intro 无
        assert clips[1].asset_id == "a1"  # main 有
        assert clips[2].asset_id == ""  # outro 无


# ============================================================
# distribute_assets - PIP
# ============================================================


class TestDistributePip:
    def test_first_asset_to_main(self):
        clips = _make_clips("p1", 3)
        # 第一个main是背景，其余改为overlay
        map_clip_types_for_mode(clips, EditingMode.PIP.value)
        assets = ["a1", "a2", "a3"]
        distribute_assets(clips, assets, EditingMode.PIP.value)
        assert clips[0].asset_id == "a1"  # main → 背景
        assert clips[1].asset_id == "a2"  # overlay
        assert clips[2].asset_id == "a3"  # overlay

    def test_single_asset(self):
        clips = _make_clips("p1", 1)
        map_clip_types_for_mode(clips, EditingMode.PIP.value)
        assets = ["a1"]
        distribute_assets(clips, assets, EditingMode.PIP.value)
        assert clips[0].asset_id == "a1"

    def test_only_main_clip_with_no_overlays(self):
        clips = _make_clips("p1", 1)
        map_clip_types_for_mode(clips, EditingMode.PIP.value)
        assets = ["a1", "a2", "a3"]  # 多余素材
        distribute_assets(clips, assets, EditingMode.PIP.value)
        assert clips[0].asset_id == "a1"


# ============================================================
# distribute_assets - VOICE_OVER
# ============================================================


class TestDistributeVoiceOver:
    def test_assets_to_main_clips(self):
        clips = _make_clips("p1", 3)
        assets = ["a1", "a2", "a3"]
        distribute_assets(clips, assets, EditingMode.VOICE_OVER.value)
        assert clips[0].asset_id == "a1"
        assert clips[1].asset_id == "a2"
        assert clips[2].asset_id == "a3"

    def test_more_clips_than_assets(self):
        clips = _make_clips("p1", 5)
        assets = ["a1", "a2"]
        distribute_assets(clips, assets, EditingMode.VOICE_OVER.value)
        assert clips[0].asset_id == "a1"
        assert clips[1].asset_id == "a2"
        assert clips[2].asset_id == ""


# ============================================================
# distribute_assets - VOICE_PIP
# ============================================================


class TestDistributeVoicePip:
    def test_three_assets_three_roles(self):
        clips = _make_clips("p1", 3)
        map_clip_types_for_mode(clips, EditingMode.VOICE_PIP.value)
        assets = ["a1", "a2", "a3"]
        distribute_assets(clips, assets, EditingMode.VOICE_PIP.value)
        assert clips[0].clip_type == "background"
        assert clips[0].asset_id == "a1"
        assert clips[1].clip_type == "corner_voice"
        assert clips[1].asset_id == "a2"
        assert clips[2].clip_type == "b_roll"
        assert clips[2].asset_id == "a3"

    def test_single_asset(self):
        clips = _make_clips("p1", 1)
        map_clip_types_for_mode(clips, EditingMode.VOICE_PIP.value)
        assets = ["a1"]
        distribute_assets(clips, assets, EditingMode.VOICE_PIP.value)
        assert clips[0].clip_type == "background"
        assert clips[0].asset_id == "a1"

    def test_two_assets(self):
        clips = _make_clips("p1", 2)
        map_clip_types_for_mode(clips, EditingMode.VOICE_PIP.value)
        assets = ["a1", "a2"]
        distribute_assets(clips, assets, EditingMode.VOICE_PIP.value)
        assert clips[0].asset_id == "a1"
        assert clips[1].asset_id == "a2"


# ============================================================
# distribute_assets - 边界情况
# ============================================================


class TestDistributeEdgeCases:
    def test_unknown_mode_falls_back_to_one_take(self):
        clips = _make_clips("p1", 2)
        assets = ["a1", "a2"]
        distribute_assets(clips, assets, "unknown_mode")
        assert clips[0].asset_id == "a1"
        assert clips[1].asset_id == "a2"

    def test_none_clips_no_crash(self):
        # 空列表
        distribute_assets([], ["a1"], EditingMode.ONE_TAKE.value)

    def test_none_assets_no_crash(self):
        clips = _make_clips("p1", 2)
        distribute_assets(clips, [], EditingMode.ONE_TAKE.value)
        for c in clips:
            assert c.asset_id == ""


# ============================================================
# map_clip_types_for_mode
# ============================================================


class TestMapClipTypesForMode:
    def test_one_take_unchanged(self):
        clips = _make_clips("p1", 3)
        original_types = [c.clip_type for c in clips]
        map_clip_types_for_mode(clips, EditingMode.ONE_TAKE.value)
        assert [c.clip_type for c in clips] == original_types

    def test_voice_over_unchanged(self):
        clips = _make_clips("p1", 3)
        map_clip_types_for_mode(clips, EditingMode.VOICE_OVER.value)
        for c in clips:
            assert c.clip_type == ClipType.MAIN.value

    def test_pip_first_stays_main_rest_overlay(self):
        clips = _make_clips("p1", 4)
        map_clip_types_for_mode(clips, EditingMode.PIP.value)
        assert clips[0].clip_type == ClipType.MAIN.value
        assert clips[1].clip_type == "overlay"
        assert clips[2].clip_type == "overlay"
        assert clips[3].clip_type == "overlay"

    def test_pip_single_clip_stays_main(self):
        clips = _make_clips("p1", 1)
        map_clip_types_for_mode(clips, EditingMode.PIP.value)
        assert clips[0].clip_type == ClipType.MAIN.value

    def test_voice_pip_mapping(self):
        clips = _make_clips("p1", 5)
        map_clip_types_for_mode(clips, EditingMode.VOICE_PIP.value)
        assert clips[0].clip_type == "background"
        assert clips[1].clip_type == "corner_voice"
        assert clips[2].clip_type == "b_roll"
        assert clips[3].clip_type == "b_roll"
        assert clips[4].clip_type == "b_roll"

    def test_voice_pip_two_clips(self):
        clips = _make_clips("p1", 2)
        map_clip_types_for_mode(clips, EditingMode.VOICE_PIP.value)
        assert clips[0].clip_type == "background"
        assert clips[1].clip_type == "corner_voice"

    def test_voice_pip_single_clip(self):
        clips = _make_clips("p1", 1)
        map_clip_types_for_mode(clips, EditingMode.VOICE_PIP.value)
        assert clips[0].clip_type == "background"

    def test_non_main_clips_unchanged(self):
        clips = [
            EditPlanClip.create("p1", clip_type="intro", order=0, duration=3.0),
            _make_main_clip("p1", order=1),
            EditPlanClip.create("p1", clip_type="outro", order=2, duration=3.0),
        ]
        map_clip_types_for_mode(clips, EditingMode.VOICE_PIP.value)
        assert clips[0].clip_type == "intro"
        assert clips[1].clip_type == "background"  # main 被改了
        assert clips[2].clip_type == "outro"

    def test_empty_clips_no_error(self):
        map_clip_types_for_mode([], EditingMode.PIP.value)

    def test_no_main_clips_no_error(self):
        clips = [
            EditPlanClip.create("p1", clip_type="intro", order=0, duration=3.0),
        ]
        map_clip_types_for_mode(clips, EditingMode.PIP.value)
        assert clips[0].clip_type == "intro"


# ============================================================
# generate_default_clips
# ============================================================


class TestGenerateDefaultClipsOneTake:
    def test_basic(self):
        clips = generate_default_clips("p1", EditingMode.ONE_TAKE.value, 3)
        assert len(clips) == 3
        for c in clips:
            assert c.clip_type == ClipType.MAIN.value
            assert c.plan_id == "p1"

    def test_order_sequential(self):
        clips = generate_default_clips("p1", EditingMode.ONE_TAKE.value, 5)
        for i, c in enumerate(clips):
            assert c.order == i

    def test_zero_assets_at_least_one(self):
        clips = generate_default_clips("p1", EditingMode.ONE_TAKE.value, 0)
        assert len(clips) == 1

    def test_negative_assets_at_least_one(self):
        clips = generate_default_clips("p1", EditingMode.ONE_TAKE.value, -5)
        assert len(clips) == 1

    def test_default_duration(self):
        clips = generate_default_clips("p1", EditingMode.ONE_TAKE.value, 1)
        assert clips[0].duration == DEFAULT_CLIP_DURATION


class TestGenerateDefaultClipsPip:
    def test_one_asset(self):
        clips = generate_default_clips("p1", EditingMode.PIP.value, 1)
        assert len(clips) == 1
        assert clips[0].clip_type == ClipType.MAIN.value

    def test_three_assets(self):
        clips = generate_default_clips("p1", EditingMode.PIP.value, 3)
        assert len(clips) == 3
        assert clips[0].clip_type == ClipType.MAIN.value
        assert clips[1].clip_type == "overlay"
        assert clips[2].clip_type == "overlay"

    def test_zero_assets(self):
        clips = generate_default_clips("p1", EditingMode.PIP.value, 0)
        assert len(clips) >= 1
        assert clips[0].clip_type == ClipType.MAIN.value


class TestGenerateDefaultClipsVoiceOver:
    def test_basic(self):
        clips = generate_default_clips("p1", EditingMode.VOICE_OVER.value, 3)
        assert len(clips) == 3
        for c in clips:
            assert c.clip_type == ClipType.MAIN.value

    def test_has_b_roll_config(self):
        clips = generate_default_clips("p1", EditingMode.VOICE_OVER.value, 2)
        # VOICE_OVER 标记 role=b_roll
        assert clips[0].config.get("role") == "b_roll"


class TestGenerateDefaultClipsVoicePip:
    def test_one_asset(self):
        clips = generate_default_clips("p1", EditingMode.VOICE_PIP.value, 1)
        assert len(clips) == 1
        assert clips[0].clip_type == "background"

    def test_two_assets(self):
        clips = generate_default_clips("p1", EditingMode.VOICE_PIP.value, 2)
        assert len(clips) == 2
        assert clips[0].clip_type == "background"
        assert clips[1].clip_type == "corner_voice"

    def test_five_assets(self):
        clips = generate_default_clips("p1", EditingMode.VOICE_PIP.value, 5)
        assert len(clips) == 5
        assert clips[0].clip_type == "background"
        assert clips[1].clip_type == "corner_voice"
        assert clips[2].clip_type == "b_roll"
        assert clips[3].clip_type == "b_roll"
        assert clips[4].clip_type == "b_roll"

    def test_zero_assets(self):
        clips = generate_default_clips("p1", EditingMode.VOICE_PIP.value, 0)
        assert len(clips) >= 1
        assert clips[0].clip_type == "background"


class TestGenerateDefaultClipsUnknownMode:
    def test_falls_back_to_one_take(self):
        clips = generate_default_clips("p1", "unknown_mode", 3)
        assert len(clips) == 3
        for c in clips:
            assert c.clip_type == ClipType.MAIN.value


# ============================================================
# create_clips_from_configs
# ============================================================


def _make_template_config(
    cfg_id: str,
    order: int,
    clip_type: ClipType = ClipType.MAIN,
    min_dur: float = 0,
    max_dur: float = 0,
) -> TemplateClipConfig:
    return TemplateClipConfig(
        id=cfg_id,
        template_id="t1",
        clip_type=clip_type,
        order=order,
        min_duration=min_dur,
        max_duration=max_dur,
        transition_effect="cut",
        config={},
    )


class TestCreateClipsFromConfigs:
    def test_empty_configs(self):
        result = create_clips_from_configs("p1", [])
        assert result == []

    def test_single_config(self):
        configs = [_make_template_config("c1", 0, min_dur=3.0, max_dur=7.0)]
        result = create_clips_from_configs("p1", configs)
        assert len(result) == 1
        assert result[0].plan_id == "p1"
        assert result[0].template_clip_config_id == "c1"
        # 平均时长 = (3+7)/2 = 5.0
        assert result[0].duration == pytest.approx(5.0)

    def test_duration_min_only(self):
        configs = [_make_template_config("c1", 0, min_dur=4.0)]
        result = create_clips_from_configs("p1", configs)
        assert result[0].duration == 4.0

    def test_duration_max_only(self):
        configs = [_make_template_config("c1", 0, max_dur=6.0)]
        result = create_clips_from_configs("p1", configs)
        assert result[0].duration == 6.0

    def test_duration_default_when_no_bounds(self):
        configs = [_make_template_config("c1", 0)]
        result = create_clips_from_configs("p1", configs)
        assert result[0].duration == DEFAULT_CLIP_DURATION

    def test_sorted_by_order(self):
        configs = [
            _make_template_config("c_third", 2),
            _make_template_config("c_first", 0),
            _make_template_config("c_second", 1),
        ]
        result = create_clips_from_configs("p1", configs)
        assert len(result) == 3
        assert result[0].template_clip_config_id == "c_first"
        assert result[1].template_clip_config_id == "c_second"
        assert result[2].template_clip_config_id == "c_third"
        assert result[0].order == 0
        assert result[1].order == 1
        assert result[2].order == 2

    def test_clip_type_preserved(self):
        configs = [
            TemplateClipConfig(
                id="c_intro",
                template_id="t1",
                clip_type=ClipType.INTRO,
                order=0,
                min_duration=3.0,
                max_duration=3.0,
                transition_effect="cut",
                config={},
            ),
            TemplateClipConfig(
                id="c_main",
                template_id="t1",
                clip_type=ClipType.MAIN,
                order=1,
                min_duration=5.0,
                max_duration=5.0,
                transition_effect="cut",
                config={},
            ),
        ]
        result = create_clips_from_configs("p1", configs)
        assert result[0].clip_type == ClipType.INTRO.value
        assert result[1].clip_type == ClipType.MAIN.value

    def test_playback_speed_from_config(self):
        configs = [
            TemplateClipConfig(
                id="c1",
                template_id="t1",
                clip_type=ClipType.MAIN,
                order=0,
                min_duration=5.0,
                max_duration=5.0,
                transition_effect="cut",
                config={"playback_speed": 1.5},
            )
        ]
        result = create_clips_from_configs("p1", configs)
        assert result[0].playback_speed == pytest.approx(1.5)

    def test_speed_ratio_fallback(self):
        # 兼容 speed_ratio 字段名
        configs = [
            TemplateClipConfig(
                id="c1",
                template_id="t1",
                clip_type=ClipType.MAIN,
                order=0,
                min_duration=5.0,
                max_duration=5.0,
                transition_effect="cut",
                config={"speed_ratio": 0.8},
            )
        ]
        result = create_clips_from_configs("p1", configs)
        assert result[0].playback_speed == pytest.approx(0.8)

    def test_default_playback_speed(self):
        configs = [_make_template_config("c1", 0, min_dur=5.0, max_dur=5.0)]
        result = create_clips_from_configs("p1", configs)
        assert result[0].playback_speed == pytest.approx(1.0)

    def test_transition_effect_preserved(self):
        configs = [
            TemplateClipConfig(
                id="c1",
                template_id="t1",
                clip_type=ClipType.MAIN,
                order=0,
                min_duration=5.0,
                max_duration=5.0,
                transition_effect="fade",
                config={},
            )
        ]
        result = create_clips_from_configs("p1", configs)
        assert result[0].transition_effect == "fade"

    def test_returns_edit_plan_clip_objects(self):
        configs = [_make_template_config("c1", 0, min_dur=3.0, max_dur=5.0)]
        result = create_clips_from_configs("p1", configs)
        assert isinstance(result[0], EditPlanClip)
