"""template_clip_converter 模板片段转换器单元测试."""

from __future__ import annotations

from dataclasses import dataclass
from types import SimpleNamespace

import pytest

from packages.domain.template_clip_config import (
    ClipType,
    TemplateClipConfig,
    TransitionEffect,
)
from packages.domain.template_clip_converter import (
    _enum_value,
    clip_config_to_snapshot,
    clip_configs_to_snapshots,
    clip_to_template_clip_config,
    clips_to_template_clip_configs,
    filter_clip_config,
    filter_plan_config_to_template,
    safe_parse_clip_type,
    safe_parse_transition_effect,
    snapshot_to_template_clip_config,
    snapshots_to_template_clip_configs,
    validate_template_name,
)

# ── 安全枚举解析测试 ──────────────────────────────────────────────────────────


class TestSafeParseTransitionEffect:
    """safe_parse_transition_effect 测试."""

    def test_enum_passthrough(self):
        """传入枚举直接返回."""
        result = safe_parse_transition_effect(TransitionEffect.FADE)
        assert result == TransitionEffect.FADE
        assert isinstance(result, TransitionEffect)

    def test_valid_string(self):
        """有效字符串解析."""
        assert safe_parse_transition_effect("fade") == TransitionEffect.FADE
        assert safe_parse_transition_effect("cut") == TransitionEffect.CUT
        assert safe_parse_transition_effect("dissolve") == TransitionEffect.DISSOLVE

    def test_invalid_string_returns_default(self):
        """无效字符串返回默认值."""
        result = safe_parse_transition_effect("invalid_effect")
        assert result == TransitionEffect.CUT  # 默认CUT

    def test_none_returns_default(self):
        """None返回默认值."""
        result = safe_parse_transition_effect(None)
        assert result == TransitionEffect.CUT

    def test_custom_default(self):
        """自定义默认值."""
        result = safe_parse_transition_effect("bad", default=TransitionEffect.FADE)
        assert result == TransitionEffect.FADE

    def test_int_returns_default(self):
        """数字类型返回默认."""
        result = safe_parse_transition_effect(123)
        assert result == TransitionEffect.CUT

    def test_all_valid_strings(self):
        """所有有效字符串都能解析."""
        for te in TransitionEffect:
            result = safe_parse_transition_effect(te.value)
            assert result == te


class TestSafeParseClipType:
    """safe_parse_clip_type 测试."""

    def test_enum_passthrough(self):
        """传入枚举直接返回."""
        result = safe_parse_clip_type(ClipType.INTRO)
        assert result == ClipType.INTRO
        assert isinstance(result, ClipType)

    def test_valid_string(self):
        """有效字符串解析."""
        assert safe_parse_clip_type("intro") == ClipType.INTRO
        assert safe_parse_clip_type("main") == ClipType.MAIN
        assert safe_parse_clip_type("outro") == ClipType.OUTRO
        assert safe_parse_clip_type("title") == ClipType.TITLE

    def test_invalid_string_returns_default(self):
        """无效字符串返回默认值."""
        result = safe_parse_clip_type("invalid_type")
        assert result == ClipType.MAIN  # 默认MAIN

    def test_none_returns_default(self):
        """None返回默认值."""
        result = safe_parse_clip_type(None)
        assert result == ClipType.MAIN

    def test_custom_default(self):
        """自定义默认值."""
        result = safe_parse_clip_type("bad", default=ClipType.TRANSITION)
        assert result == ClipType.TRANSITION

    def test_all_valid_strings(self):
        """所有有效字符串都能解析."""
        for ct in ClipType:
            result = safe_parse_clip_type(ct.value)
            assert result == ct


# ── _enum_value 内部函数测试 ─────────────────────────────────────────────────


class TestEnumValue:
    """_enum_value 内部工具函数测试."""

    def test_enum_value(self):
        """枚举返回value."""
        assert _enum_value(TransitionEffect.FADE) == "fade"
        assert _enum_value(ClipType.MAIN) == "main"

    def test_string_passthrough(self):
        """字符串直接返回."""
        assert _enum_value("hello") == "hello"

    def test_int_passthrough(self):
        """数字直接返回."""
        assert _enum_value(42) == 42

    def test_none_passthrough(self):
        """None直接返回."""
        assert _enum_value(None) is None


# ── filter_clip_config 测试 ──────────────────────────────────────────────────


class TestFilterClipConfig:
    """filter_clip_config 测试."""

    def test_none_config_no_speed(self):
        """None config + 无playback_speed."""
        result = filter_clip_config(None)
        assert result == {}

    def test_none_config_with_speed(self):
        """None config + 有playback_speed."""
        result = filter_clip_config(None, playback_speed=1.5)
        assert result == {"playback_speed": 1.5}

    def test_speed_equals_one_not_included(self):
        """playback_speed=1.0不写入."""
        result = filter_clip_config({"key": "val"}, playback_speed=1.0)
        assert "playback_speed" not in result
        assert result["key"] == "val"

    def test_speed_not_one_included(self):
        """playback_speed≠1.0写入."""
        result = filter_clip_config({"key": "val"}, playback_speed=0.8)
        assert result["playback_speed"] == 0.8
        assert result["key"] == "val"

    def test_speed_none_not_included(self):
        """playback_speed=None不写入."""
        result = filter_clip_config({"key": "val"}, playback_speed=None)
        assert "playback_speed" not in result

    def test_removes_asset_info(self):
        """移除asset_info字段."""
        config = {"a": 1, "asset_info": {"name": "x"}, "b": 2}
        result = filter_clip_config(config)
        assert "asset_info" not in result
        assert result["a"] == 1
        assert result["b"] == 2

    def test_removes_source_asset_id(self):
        """移除source_asset_id字段."""
        config = {"source_asset_id": "abc", "other": "val"}
        result = filter_clip_config(config)
        assert "source_asset_id" not in result
        assert result["other"] == "val"

    def test_custom_skip_keys(self):
        """自定义跳过字段."""
        config = {"keep": "yes", "skip_me": "no", "also_skip": "no"}
        skip = frozenset({"skip_me", "also_skip"})
        result = filter_clip_config(config, skip_keys=skip)
        assert "keep" in result
        assert "skip_me" not in result
        assert "also_skip" not in result

    def test_empty_config(self):
        """空dict config."""
        result = filter_clip_config({})
        assert result == {}

    def test_does_not_mutate_original(self):
        """不修改原始config."""
        original = {"a": 1, "asset_info": "x"}
        original_copy = dict(original)
        filter_clip_config(original)
        assert original == original_copy

    def test_speed_overrides_config(self):
        """playback_speed 覆盖 clip_config 中的值."""
        result = filter_clip_config({"playback_speed": 2.0}, playback_speed=0.5)
        # clip_config是后面合并的，所以会覆盖前面的playback_speed
        assert result["playback_speed"] == 2.0


# ── filter_plan_config_to_template 测试 ──────────────────────────────────────


class TestFilterPlanConfigToTemplate:
    """filter_plan_config_to_template 测试."""

    def test_none_config(self):
        """None config返回空dict."""
        result = filter_plan_config_to_template(None)
        assert result == {}

    def test_empty_config(self):
        """空dict返回空."""
        result = filter_plan_config_to_template({})
        assert result == {}

    def test_removes_runtime_fields(self):
        """移除运行时字段."""
        config = {
            "keep": "yes",
            "is_template_draft": True,
            "asset_ids": ["a", "b"],
            "source_edit_plan_id": "plan_123",
            "generation_task_id": "task_456",
            "another_field": "also_keep",
        }
        result = filter_plan_config_to_template(config)
        assert "keep" in result
        assert "another_field" in result
        assert "is_template_draft" not in result
        assert "asset_ids" not in result
        assert "source_edit_plan_id" not in result
        assert "generation_task_id" not in result

    def test_custom_skip_keys(self):
        """自定义跳过字段."""
        config = {"keep": "yes", "skip_a": "no", "skip_b": "no"}
        skip = frozenset({"skip_a", "skip_b"})
        result = filter_plan_config_to_template(config, skip_keys=skip)
        assert "keep" in result
        assert "skip_a" not in result
        assert "skip_b" not in result

    def test_preserves_values(self):
        """保留字段的值不变."""
        config = {"title": "测试", "duration": 10.5, "items": [1, 2, 3]}
        result = filter_plan_config_to_template(config)
        assert result["title"] == "测试"
        assert result["duration"] == 10.5
        assert result["items"] == [1, 2, 3]


# ── clip_to_template_clip_config 测试 ────────────────────────────────────────


class TestClipToTemplateClipConfig:
    """clip_to_template_clip_config 测试."""

    def _make_clip(self, **kwargs):
        """创建模拟clip对象."""
        defaults = {
            "clip_type": "main",
            "order": 1,
            "duration": 5.0,
            "text_content": "默认文案",
            "transition_effect": "cut",
            "playback_speed": 1.0,
            "config": {},
        }
        defaults.update(kwargs)
        return SimpleNamespace(**defaults)

    def test_basic_conversion(self):
        """基础转换."""
        clip = self._make_clip(order=2, duration=3.5)
        result = clip_to_template_clip_config("tpl_001", clip)
        assert isinstance(result, TemplateClipConfig)
        assert result.template_id == "tpl_001"
        assert result.clip_type == ClipType.MAIN
        assert result.order == 2
        assert result.min_duration == 3.5
        assert result.max_duration == 3.5
        assert result.text_template == "默认文案"

    def test_intro_clip_type(self):
        """intro类型转换."""
        clip = self._make_clip(clip_type="intro", order=0)
        result = clip_to_template_clip_config("t1", clip)
        assert result.clip_type == ClipType.INTRO

    def test_transition_effect(self):
        """转场效果转换."""
        clip = self._make_clip(transition_effect="fade")
        result = clip_to_template_clip_config("t1", clip)
        assert result.transition_effect == TransitionEffect.FADE

    def test_fixed_duration(self):
        """固定时长：min=max=duration."""
        clip = self._make_clip(duration=7.2)
        result = clip_to_template_clip_config("t1", clip)
        assert result.min_duration == 7.2
        assert result.max_duration == 7.2
        assert result.min_duration == result.max_duration

    def test_zero_duration(self):
        """duration为0."""
        clip = self._make_clip(duration=0)
        result = clip_to_template_clip_config("t1", clip)
        assert result.min_duration == 0.0
        assert result.max_duration == 0.0

    def test_none_duration(self):
        """duration为None时默认0."""
        clip = self._make_clip(duration=None)
        result = clip_to_template_clip_config("t1", clip)
        assert result.min_duration == 0.0

    def test_text_content_mapped_to_text_template(self):
        """text_content → text_template."""
        clip = self._make_clip(text_content="你好世界")
        result = clip_to_template_clip_config("t1", clip)
        assert result.text_template == "你好世界"

    def test_none_text_content(self):
        """text_content为None时默认空字符串."""
        clip = self._make_clip(text_content=None)
        result = clip_to_template_clip_config("t1", clip)
        assert result.text_template == ""

    def test_playback_speed_in_config(self):
        """非1.0的playback_speed存入config."""
        clip = self._make_clip(playback_speed=1.5)
        result = clip_to_template_clip_config("t1", clip)
        assert result.config["playback_speed"] == 1.5

    def test_playback_speed_one_not_in_config(self):
        """playback_speed=1.0不存入config."""
        clip = self._make_clip(playback_speed=1.0)
        result = clip_to_template_clip_config("t1", clip)
        assert "playback_speed" not in result.config

    def test_config_passed_through(self):
        """clip.config传递到结果."""
        clip = self._make_clip(config={"font_size": 24, "color": "red"})
        result = clip_to_template_clip_config("t1", clip)
        assert result.config["font_size"] == 24
        assert result.config["color"] == "red"

    def test_config_filters_asset_fields(self):
        """config中过滤素材字段."""
        clip = self._make_clip(config={"asset_info": {"x": 1}, "keep": "yes"})
        result = clip_to_template_clip_config("t1", clip)
        assert "asset_info" not in result.config
        assert result.config["keep"] == "yes"

    def test_invalid_clip_type_defaults_to_main(self):
        """无效clip_type安全解析为main."""
        clip = self._make_clip(clip_type="unknown_type")
        result = clip_to_template_clip_config("t1", clip)
        assert result.clip_type == ClipType.MAIN

    def test_clip_without_attributes(self):
        """缺少属性的对象使用默认值."""

        @dataclass
        class MinimalClip:
            order: int = 0

        clip = MinimalClip(order=5)
        result = clip_to_template_clip_config("t1", clip)
        assert result.clip_type == ClipType.MAIN  # 默认
        assert result.order == 5
        assert result.text_template == ""
        assert result.transition_effect == TransitionEffect.CUT


class TestClipsToTemplateClipConfigs:
    """clips_to_template_clip_configs 批量转换测试."""

    def test_empty_list(self):
        """空列表返回空列表."""
        result = clips_to_template_clip_configs("t1", [])
        assert result == []

    def test_multiple_clips(self):
        """多个片段转换."""
        clips = [
            SimpleNamespace(
                clip_type="intro",
                order=0,
                duration=2.0,
                text_content="",
                transition_effect="fade",
                playback_speed=1.0,
                config={},
            ),
            SimpleNamespace(
                clip_type="main",
                order=1,
                duration=5.0,
                text_content="主体",
                transition_effect="cut",
                playback_speed=1.0,
                config={},
            ),
            SimpleNamespace(
                clip_type="outro",
                order=2,
                duration=3.0,
                text_content="结束",
                transition_effect="fade",
                playback_speed=1.0,
                config={},
            ),
        ]
        result = clips_to_template_clip_configs("tpl_multi", clips)
        assert len(result) == 3
        assert result[0].clip_type == ClipType.INTRO
        assert result[0].order == 0
        assert result[1].clip_type == ClipType.MAIN
        assert result[1].order == 1
        assert result[2].clip_type == ClipType.OUTRO
        assert result[2].order == 2
        assert all(r.template_id == "tpl_multi" for r in result)


# ── clip_config_to_snapshot 测试 ────────────────────────────────────────────


class TestClipConfigToSnapshot:
    """clip_config_to_snapshot 测试."""

    def test_basic_snapshot(self):
        """基础快照转换."""
        cfg = TemplateClipConfig.create(
            template_id="t1",
            clip_type="main",
            order=1,
            min_duration=3.0,
            max_duration=5.0,
            text_template="hello",
            transition_effect="fade",
            config={"key": "val"},
        )
        snap = clip_config_to_snapshot(cfg)
        assert snap["clip_type"] == "main"
        assert snap["order"] == 1
        assert snap["min_duration"] == 3.0
        assert snap["max_duration"] == 5.0
        assert snap["text_template"] == "hello"
        assert snap["transition_effect"] == "fade"
        assert snap["config"] == {"key": "val"}

    def test_enum_values_in_snapshot(self):
        """枚举转为字符串value."""
        cfg = TemplateClipConfig.create(
            template_id="t1",
            clip_type=ClipType.TITLE,
            order=0,
            transition_effect=TransitionEffect.DISSOLVE,
        )
        snap = clip_config_to_snapshot(cfg)
        assert snap["clip_type"] == "title"
        assert isinstance(snap["clip_type"], str)
        assert snap["transition_effect"] == "dissolve"
        assert isinstance(snap["transition_effect"], str)

    def test_empty_text_template(self):
        """空文案模板."""
        cfg = TemplateClipConfig.create(
            template_id="t1",
            clip_type="main",
            order=0,
            text_template="",
        )
        snap = clip_config_to_snapshot(cfg)
        assert snap["text_template"] == ""

    def test_none_text_template_becomes_empty(self):
        """text_template为None时返回空串."""
        # TemplateClipConfig的text_template默认是空串，模拟一个有None属性的对象
        obj = SimpleNamespace(
            clip_type=ClipType.MAIN,
            order=0,
            min_duration=1.0,
            max_duration=2.0,
            text_template=None,
            transition_effect=TransitionEffect.CUT,
            config={},
        )
        snap = clip_config_to_snapshot(obj)
        assert snap["text_template"] == ""

    def test_config_is_copy(self):
        """config是副本，不是原对象引用."""
        original_config = {"a": 1}
        cfg = TemplateClipConfig.create(
            template_id="t1",
            clip_type="main",
            order=0,
            config=original_config,
        )
        snap = clip_config_to_snapshot(cfg)
        snap["config"]["b"] = 2
        assert "b" not in cfg.config
        assert original_config.get("b") is None

    def test_none_config_becomes_empty(self):
        """config为None时返回空dict."""
        obj = SimpleNamespace(
            clip_type="main",
            order=0,
            min_duration=0,
            max_duration=0,
            text_template="",
            transition_effect="cut",
            config=None,
        )
        snap = clip_config_to_snapshot(obj)
        assert snap["config"] == {}

    def test_default_values_for_missing_attrs(self):
        """缺少属性时用默认值."""
        obj = SimpleNamespace()
        snap = clip_config_to_snapshot(obj)
        assert snap["clip_type"] is None  # _enum_value(None)=None
        assert snap["order"] == 0
        assert snap["min_duration"] == 0.0
        assert snap["max_duration"] == 0.0
        assert snap["text_template"] == ""
        assert snap["transition_effect"] is None
        assert snap["config"] == {}

    def test_snapshot_keys(self):
        """快照包含所有预期字段."""
        cfg = TemplateClipConfig.create(
            template_id="t1",
            clip_type="main",
            order=0,
        )
        snap = clip_config_to_snapshot(cfg)
        expected_keys = {
            "clip_type",
            "order",
            "min_duration",
            "max_duration",
            "text_template",
            "transition_effect",
            "config",
        }
        assert set(snap.keys()) == expected_keys


class TestClipConfigsToSnapshots:
    """clip_configs_to_snapshots 批量测试."""

    def test_empty_list(self):
        """空列表返回空列表."""
        assert clip_configs_to_snapshots([]) == []

    def test_multiple_configs(self):
        """多个配置转换."""
        configs = [
            TemplateClipConfig.create("t1", "intro", 0, min_duration=2.0, max_duration=2.0),
            TemplateClipConfig.create("t1", "main", 1, min_duration=3.0, max_duration=5.0),
            TemplateClipConfig.create("t1", "outro", 2, min_duration=1.0, max_duration=1.0),
        ]
        snaps = clip_configs_to_snapshots(configs)
        assert len(snaps) == 3
        assert snaps[0]["clip_type"] == "intro"
        assert snaps[1]["clip_type"] == "main"
        assert snaps[2]["clip_type"] == "outro"


# ── snapshot_to_template_clip_config 测试 ────────────────────────────────────


class TestSnapshotToTemplateClipConfig:
    """snapshot_to_template_clip_config 测试."""

    def test_basic_conversion(self):
        """基础快照→配置转换."""
        snap = {
            "clip_type": "main",
            "order": 2,
            "min_duration": 3.0,
            "max_duration": 5.0,
            "text_template": "hello",
            "transition_effect": "fade",
            "config": {"key": "val"},
        }
        result = snapshot_to_template_clip_config("tpl_new", snap)
        assert isinstance(result, TemplateClipConfig)
        assert result.template_id == "tpl_new"
        assert result.clip_type == ClipType.MAIN
        assert result.order == 2
        assert result.min_duration == 3.0
        assert result.max_duration == 5.0
        assert result.text_template == "hello"
        assert result.transition_effect == TransitionEffect.FADE
        assert result.config == {"key": "val"}

    def test_defaults_for_missing_keys(self):
        """缺失字段使用默认值."""
        snap: dict = {}
        result = snapshot_to_template_clip_config("t1", snap)
        assert result.clip_type == ClipType.MAIN  # 默认main
        assert result.order == 0
        assert result.min_duration == 0.0
        assert result.max_duration == 0.0
        assert result.text_template == ""
        assert result.transition_effect == TransitionEffect.CUT  # 默认cut
        assert result.config == {}

    def test_intro_type(self):
        """intro类型解析."""
        snap = {"clip_type": "intro", "order": 0}
        result = snapshot_to_template_clip_config("t1", snap)
        assert result.clip_type == ClipType.INTRO

    def test_invalid_clip_type_defaults(self):
        """无效clip_type安全回退到main."""
        snap = {"clip_type": "weird_type", "order": 0}
        result = snapshot_to_template_clip_config("t1", snap)
        assert result.clip_type == ClipType.MAIN

    def test_invalid_transition_defaults(self):
        """无效transition_effect安全回退到cut."""
        snap = {"transition_effect": "invalid_fx"}
        result = snapshot_to_template_clip_config("t1", snap)
        assert result.transition_effect == TransitionEffect.CUT

    def test_none_config_becomes_empty(self):
        """config为None时是空dict."""
        snap = {"config": None}
        result = snapshot_to_template_clip_config("t1", snap)
        assert result.config == {}

    def test_config_is_copy(self):
        """config是副本."""
        original_config = {"a": 1}
        snap = {"config": original_config}
        result = snapshot_to_template_clip_config("t1", snap)
        result.config["b"] = 2
        assert "b" not in original_config


class TestSnapshotsToTemplateClipConfigs:
    """snapshots_to_template_clip_configs 批量测试."""

    def test_empty_list(self):
        """空列表返回空列表."""
        result = snapshots_to_template_clip_configs("t1", [])
        assert result == []

    def test_multiple_snapshots(self):
        """多个快照转换."""
        snaps = [
            {"clip_type": "intro", "order": 0, "min_duration": 2.0, "max_duration": 2.0},
            {"clip_type": "main", "order": 1, "min_duration": 5.0, "max_duration": 5.0},
            {"clip_type": "outro", "order": 2, "min_duration": 3.0, "max_duration": 3.0},
        ]
        result = snapshots_to_template_clip_configs("tpl_multi", snaps)
        assert len(result) == 3
        assert result[0].clip_type == ClipType.INTRO
        assert result[1].clip_type == ClipType.MAIN
        assert result[2].clip_type == ClipType.OUTRO
        assert all(r.template_id == "tpl_multi" for r in result)


# ── validate_template_name 测试 ──────────────────────────────────────────────


class TestValidateTemplateName:
    """validate_template_name 测试."""

    def test_normal_name(self):
        """正常名称."""
        assert validate_template_name("我的模板") == "我的模板"

    def test_strips_whitespace(self):
        """去除首尾空格."""
        assert validate_template_name("  测试模板  ") == "测试模板"

    def test_strips_newlines(self):
        """去除换行."""
        assert validate_template_name("\n模板名\n") == "模板名"

    def test_empty_string_raises(self):
        """空字符串抛错."""
        with pytest.raises(ValueError, match="不能为空"):
            validate_template_name("")

    def test_none_raises(self):
        """None抛错."""
        with pytest.raises(ValueError, match="不能为空"):
            validate_template_name(None)

    def test_whitespace_only_raises(self):
        """纯空白抛错."""
        with pytest.raises(ValueError, match="不能为空"):
            validate_template_name("   ")

    def test_newline_only_raises(self):
        """纯换行抛错."""
        with pytest.raises(ValueError, match="不能为空"):
            validate_template_name("\n\t\r")

    def test_single_char_name(self):
        """单字符名称有效."""
        assert validate_template_name("A") == "A"

    def test_long_name_preserved(self):
        """长名称保留（长度限制由调用方控制）."""
        long_name = "A" * 200
        assert validate_template_name(long_name) == long_name


# ── 往返转换测试 ──────────────────────────────────────────────────────────────


class TestRoundTripConversions:
    """往返转换一致性测试."""

    def test_snapshot_round_trip(self):
        """snapshot → config → snapshot 数据一致."""
        original = {
            "clip_type": "main",
            "order": 5,
            "min_duration": 2.5,
            "max_duration": 7.5,
            "text_template": "测试文案",
            "transition_effect": "dissolve",
            "config": {"speed": 1.2, "theme": "dark"},
        }
        cfg = snapshot_to_template_clip_config("t1", original)
        snap = clip_config_to_snapshot(cfg)
        assert snap["clip_type"] == original["clip_type"]
        assert snap["order"] == original["order"]
        assert snap["min_duration"] == original["min_duration"]
        assert snap["max_duration"] == original["max_duration"]
        assert snap["text_template"] == original["text_template"]
        assert snap["transition_effect"] == original["transition_effect"]
        assert snap["config"] == original["config"]

    def test_config_snapshot_round_trip(self):
        """config → snapshot → config 数据一致."""
        cfg = TemplateClipConfig.create(
            template_id="t1",
            clip_type="title",
            order=0,
            min_duration=3.0,
            max_duration=3.0,
            text_template="标题",
            transition_effect="wipe",
            config={"font_size": 32},
        )
        snap = clip_config_to_snapshot(cfg)
        restored = snapshot_to_template_clip_config("t1", snap)
        assert restored.clip_type == cfg.clip_type
        assert restored.order == cfg.order
        assert restored.min_duration == cfg.min_duration
        assert restored.max_duration == cfg.max_duration
        assert restored.text_template == cfg.text_template
        assert restored.transition_effect == cfg.transition_effect
        assert restored.config == cfg.config
