"""渲染图层工具函数单测.

纯函数模块，覆盖：图层角色映射、z_index、
clip时长计算、总时长估算、直通判断。
"""

from __future__ import annotations

from dataclasses import dataclass, field

from packages.domain.render_layer_utils import (
    LAYER_Z_INDEX,
    MAIN_LAYER_ROLES,
    PIP_DEFAULT_SCALE,
    can_pass_through,
    clip_adjusted_duration,
    clip_effective_duration,
    clip_playback_speed,
    estimate_total_duration,
    get_layer_z_index,
    resolve_layer_role,
)


class TestConstants:
    def test_layer_z_index_keys(self):
        assert "background" in LAYER_Z_INDEX
        assert "broll" in LAYER_Z_INDEX
        assert "main" in LAYER_Z_INDEX
        assert "overlay" in LAYER_Z_INDEX
        assert "corner_voice" in LAYER_Z_INDEX
        assert "audio" in LAYER_Z_INDEX

    def test_layer_z_index_values(self):
        assert LAYER_Z_INDEX["background"] == -1
        assert LAYER_Z_INDEX["broll"] == 0
        assert LAYER_Z_INDEX["main"] == 0
        assert LAYER_Z_INDEX["overlay"] == 1
        assert LAYER_Z_INDEX["corner_voice"] == 1
        assert LAYER_Z_INDEX["audio"] == 2

    def test_pip_default_scale(self):
        assert PIP_DEFAULT_SCALE == 0.25

    def test_main_layer_roles(self):
        assert "main" in MAIN_LAYER_ROLES
        assert "broll" in MAIN_LAYER_ROLES
        assert "background" in MAIN_LAYER_ROLES
        assert len(MAIN_LAYER_ROLES) == 3


class TestResolveLayerRole:
    def test_main_default(self):
        assert resolve_layer_role("main") == "main"

    def test_intro_maps_to_main(self):
        assert resolve_layer_role("intro") == "main"

    def test_outro_maps_to_main(self):
        assert resolve_layer_role("outro") == "main"

    def test_overlay(self):
        assert resolve_layer_role("overlay") == "overlay"

    def test_corner_voice(self):
        assert resolve_layer_role("corner_voice") == "corner_voice"

    def test_background(self):
        assert resolve_layer_role("background") == "background"

    def test_b_roll(self):
        assert resolve_layer_role("b_roll") == "broll"

    def test_main_with_b_roll_role(self):
        assert resolve_layer_role("main", {"role": "b_roll"}) == "broll"

    def test_main_with_audio_role(self):
        assert resolve_layer_role("main", {"role": "audio"}) == "audio"

    def test_main_with_unknown_role(self):
        assert resolve_layer_role("main", {"role": "something"}) == "main"

    def test_overlay_ignores_config_role(self):
        """overlay类型不受config.role影响."""
        assert resolve_layer_role("overlay", {"role": "b_roll"}) == "overlay"

    def test_intro_ignores_config_role(self):
        """intro类型不受config.role影响."""
        assert resolve_layer_role("intro", {"role": "audio"}) == "main"

    def test_none_config(self):
        assert resolve_layer_role("main", None) == "main"

    def test_empty_config(self):
        assert resolve_layer_role("main", {}) == "main"

    def test_unknown_clip_type_defaults_to_main(self):
        """未知clip_type走default分支返回main."""
        assert resolve_layer_role("unknown_type") == "main"


class TestGetLayerZIndex:
    def test_background(self):
        assert get_layer_z_index("background") == -1

    def test_main(self):
        assert get_layer_z_index("main") == 0

    def test_broll(self):
        assert get_layer_z_index("broll") == 0

    def test_overlay(self):
        assert get_layer_z_index("overlay") == 1

    def test_corner_voice(self):
        assert get_layer_z_index("corner_voice") == 1

    def test_audio(self):
        assert get_layer_z_index("audio") == 2

    def test_unknown_returns_zero(self):
        assert get_layer_z_index("unknown_role") == 0

    def test_empty_string_returns_zero(self):
        assert get_layer_z_index("") == 0


class TestClipEffectiveDuration:
    def test_duration_only(self):
        """只有duration，没有actual，用duration."""
        assert clip_effective_duration(5.0) == 5.0

    def test_duration_less_than_actual(self):
        """duration < actual，取duration."""
        assert clip_effective_duration(3.0, 5.0) == 3.0

    def test_duration_greater_than_actual(self):
        """#1749：duration > actual 仍取目标段长 duration（素材短走末帧冻结，不钳制）."""
        assert clip_effective_duration(10.0, 5.0) == 10.0

    def test_duration_equal_to_actual(self):
        assert clip_effective_duration(5.0, 5.0) == 5.0

    def test_zero_duration_with_actual(self):
        """duration=0表示使用完整素材，取actual."""
        assert clip_effective_duration(0.0, 8.0) == 8.0

    def test_negative_duration_with_actual(self):
        """duration<0也取actual."""
        assert clip_effective_duration(-1.0, 8.0) == 8.0

    def test_zero_duration_zero_actual(self):
        assert clip_effective_duration(0.0, 0.0) == 0.0

    def test_zero_actual_uses_duration(self):
        """actual=0时，duration>0就用duration."""
        assert clip_effective_duration(5.0, 0.0) == 5.0

    def test_both_zero(self):
        assert clip_effective_duration(0.0) == 0.0


class TestClipPlaybackSpeed:
    def test_normal_speed(self):
        assert clip_playback_speed(1.0) == 1.0

    def test_fast_speed(self):
        assert clip_playback_speed(2.0) == 2.0

    def test_slow_speed(self):
        assert clip_playback_speed(0.5) == 0.5

    def test_zero_speed_fallback(self):
        assert clip_playback_speed(0) == 1.0

    def test_negative_speed_fallback(self):
        assert clip_playback_speed(-1.0) == 1.0

    def test_string_fallback(self):
        assert clip_playback_speed("fast") == 1.0

    def test_none_fallback(self):
        assert clip_playback_speed(None) == 1.0

    def test_int_speed(self):
        assert clip_playback_speed(2) == 2.0

    def test_list_fallback(self):
        assert clip_playback_speed([1, 2]) == 1.0

    def test_dict_fallback(self):
        assert clip_playback_speed({"speed": 2}) == 1.0


class TestClipAdjustedDuration:
    def test_normal_speed_no_change(self):
        assert clip_adjusted_duration(5.0, 10.0, 1.0) == 5.0

    def test_double_speed_half_duration(self):
        assert clip_adjusted_duration(4.0, 10.0, 2.0) == 2.0

    def test_half_speed_double_duration(self):
        assert clip_adjusted_duration(4.0, 10.0, 0.5) == 8.0

    def test_uses_effective_duration(self):
        """#1749：effective=目标段长 10.0（不再钳 actual），2 倍速 → 10/2 = 5.0."""
        assert clip_adjusted_duration(10.0, 4.0, 2.0) == 5.0

    def test_default_speed(self):
        assert clip_adjusted_duration(5.0, 10.0) == 5.0

    def test_invalid_speed_fallback(self):
        assert clip_adjusted_duration(5.0, 10.0, "bad") == 5.0

    def test_zero_speed_fallback(self):
        assert clip_adjusted_duration(5.0, 10.0, 0) == 5.0

    def test_very_close_to_one_speed(self):
        """速度接近1.0时直接返回base，不做除法."""
        result = clip_adjusted_duration(5.0, 10.0, 1.0000001)
        assert result == 5.0

    def test_zero_duration_zero_actual(self):
        assert clip_adjusted_duration(0.0, 0.0, 1.0) == 0.0


@dataclass
class FakeClip:
    duration: float = 0.0
    actual_duration: float = 0.0
    playback_speed: float = 1.0
    # #1749：逐处转场口径——cut 零重叠，非 cut 才扣 transition_duration
    transition_effect: str = "cut"
    transition_duration: float = 0.0


@dataclass
class FakeLayer:
    role: str = "main"
    clips: list[FakeClip] = field(default_factory=list)


class TestEstimateTotalDuration:
    def test_single_main_layer_single_clip(self):
        layers = [
            FakeLayer(role="main", clips=[FakeClip(duration=5.0, actual_duration=10.0)]),
        ]
        assert estimate_total_duration(layers) == 5.0

    def test_single_main_layer_multiple_clips(self):
        layers = [
            FakeLayer(
                role="main",
                clips=[
                    FakeClip(duration=3.0, actual_duration=5.0),
                    FakeClip(duration=2.0, actual_duration=4.0),
                ],
            ),
        ]
        assert estimate_total_duration(layers) == 5.0

    def test_with_transition_duration(self):
        layers = [
            FakeLayer(
                role="main",
                clips=[
                    FakeClip(duration=5.0),
                    FakeClip(duration=5.0, transition_effect="fade", transition_duration=0.5),
                ],
            ),
        ]
        # #1749：总10s - 1个非cut转场 * 0.5s = 9.5s（cut 零重叠）
        assert estimate_total_duration(layers, transition_duration=0.5) == 9.5

    def test_cut_clips_have_zero_overlap(self):
        layers = [
            FakeLayer(
                role="main",
                clips=[
                    FakeClip(duration=5.0),
                    FakeClip(duration=5.0),
                ],
            ),
        ]
        # #1749：cut 转场零重叠 → 总时长 10s，即使传了全局 transition_duration
        assert estimate_total_duration(layers, transition_duration=0.5) == 10.0

    def test_transition_with_many_clips(self):
        layers = [
            FakeLayer(
                role="main",
                clips=[FakeClip(duration=3.0) for _ in range(5)],
            ),
        ]
        # #1749：全 cut 零重叠 → 5个3s = 15s
        assert estimate_total_duration(layers, transition_duration=0.5) == 15.0

    def test_non_cut_transitions_with_many_clips(self):
        layers = [
            FakeLayer(
                role="main",
                clips=[FakeClip(duration=3.0, transition_effect="fade", transition_duration=0.5) for _ in range(5)],
            ),
        ]
        # 5个3s = 15s，4个非cut转场 * 0.5s = 2s，总13s（#1749 逐处重叠）
        assert estimate_total_duration(layers, transition_duration=0.5) == 13.0

    def test_prefers_main_over_broll(self):
        """main图层优先级高于broll."""
        layers = [
            FakeLayer(role="broll", clips=[FakeClip(duration=10.0)]),
            FakeLayer(role="main", clips=[FakeClip(duration=5.0)]),
        ]
        assert estimate_total_duration(layers) == 5.0

    def test_prefers_broll_over_background(self):
        layers = [
            FakeLayer(role="background", clips=[FakeClip(duration=20.0)]),
            FakeLayer(role="broll", clips=[FakeClip(duration=10.0)]),
        ]
        assert estimate_total_duration(layers) == 10.0

    def test_no_main_layer(self):
        layers = [
            FakeLayer(role="overlay", clips=[FakeClip(duration=5.0)]),
        ]
        # 没有主图层，返回0
        assert estimate_total_duration(layers) == 0.0

    def test_empty_layers(self):
        assert estimate_total_duration([]) == 0.0

    def test_main_layer_no_clips(self):
        layers = [FakeLayer(role="main", clips=[])]
        assert estimate_total_duration(layers) == 0.0

    def test_minimum_total_duration(self):
        """总时长最小为0.1s."""
        layers = [
            FakeLayer(role="main", clips=[FakeClip(duration=0.01, actual_duration=0.01)]),
        ]
        # 转场把总时长减到接近0时，会被钳制到0.1
        result = estimate_total_duration(layers, transition_duration=10.0)
        assert result == 0.1

    def test_with_playback_speed(self):
        layers = [
            FakeLayer(
                role="main",
                clips=[FakeClip(duration=4.0, playback_speed=2.0)],
            ),
        ]
        # 4s / 2x = 2s
        assert estimate_total_duration(layers) == 2.0

    def test_multiple_layers_picks_first_main(self):
        layers = [
            FakeLayer(role="overlay", clips=[FakeClip(duration=1.0)]),
            FakeLayer(role="main", clips=[FakeClip(duration=5.0)]),
            FakeLayer(role="main", clips=[FakeClip(duration=10.0)]),  # 不看这个
        ]
        assert estimate_total_duration(layers) == 5.0


class TestCanPassThrough:
    def test_single_main_layer_single_clip(self):
        layers = [
            FakeLayer(role="main", clips=[FakeClip(duration=5.0)]),
        ]
        assert can_pass_through(layers) is True

    def test_single_broll_layer_single_clip(self):
        layers = [
            FakeLayer(role="broll", clips=[FakeClip(duration=5.0)]),
        ]
        assert can_pass_through(layers) is True

    def test_single_background_layer_single_clip(self):
        layers = [
            FakeLayer(role="background", clips=[FakeClip(duration=5.0)]),
        ]
        assert can_pass_through(layers) is True

    def test_multiple_layers_false(self):
        layers = [
            FakeLayer(role="main", clips=[FakeClip(duration=5.0)]),
            FakeLayer(role="overlay", clips=[FakeClip(duration=3.0)]),
        ]
        assert can_pass_through(layers) is False

    def test_single_layer_multiple_clips_false(self):
        layers = [
            FakeLayer(
                role="main",
                clips=[FakeClip(duration=3.0), FakeClip(duration=2.0)],
            ),
        ]
        assert can_pass_through(layers) is False

    def test_overlay_layer_false(self):
        """overlay不是主图层角色."""
        layers = [
            FakeLayer(role="overlay", clips=[FakeClip(duration=5.0)]),
        ]
        assert can_pass_through(layers) is False

    def test_has_stickers_false(self):
        layers = [
            FakeLayer(role="main", clips=[FakeClip(duration=5.0)]),
        ]
        assert can_pass_through(layers, has_stickers=True) is False

    def test_has_watermark_false(self):
        layers = [
            FakeLayer(role="main", clips=[FakeClip(duration=5.0)]),
        ]
        assert can_pass_through(layers, has_watermark=True) is False

    def test_both_stickers_and_watermark_false(self):
        layers = [
            FakeLayer(role="main", clips=[FakeClip(duration=5.0)]),
        ]
        assert can_pass_through(layers, has_stickers=True, has_watermark=True) is False

    def test_empty_layers_false(self):
        assert can_pass_through([]) is False

    def test_corner_voice_layer_false(self):
        layers = [
            FakeLayer(role="corner_voice", clips=[FakeClip(duration=5.0)]),
        ]
        assert can_pass_through(layers) is False
