"""
config_schemas 配置结构定义单元测试

覆盖：
- 4个枚举类型
- 10个Pydantic模型
- 2个normalize工具函数
- 2个默认值常量
"""

import copy

import pytest
from pydantic import ValidationError

from packages.domain.config_schemas import (
    DEFAULT_EDIT_PLAN_CONFIG,
    DEFAULT_EDIT_TEMPLATE_CONFIG,
    BGMConfig,
    BGMSource,
    CoverConfig,
    CoverType,
    EditPlanConfigSchema,
    EditTemplateConfigSchema,
    ExportConfig,
    FilterConfig,
    ShadowConfig,
    StrokeConfig,
    SubtitleConfig,
    TextAnimation,
    TextPosition,
    TitleConfig,
    normalize_plan_config,
    normalize_template_config,
)

# ── 枚举测试 ──────────────────────────────────────────────────────────────────


class TestCoverType:
    """CoverType 枚举测试"""

    def test_all_types_exist(self):
        assert CoverType.AI_FRAME == "ai_frame"
        assert CoverType.MANUAL == "manual"
        assert CoverType.UPLOAD == "upload"
        assert CoverType.AI_REGENERATE == "ai_regenerate"

    def test_total_count(self):
        assert len(CoverType) == 4

    def test_is_string_enum(self):
        for t in CoverType:
            assert isinstance(t.value, str)
            assert isinstance(t, str)

    def test_string_comparison(self):
        assert CoverType.AI_FRAME == "ai_frame"
        assert CoverType.UPLOAD != "manual"


class TestTextPosition:
    """TextPosition 枚举测试"""

    def test_all_positions_exist(self):
        assert TextPosition.TOP == "top"
        assert TextPosition.CENTER == "center"
        assert TextPosition.BOTTOM == "bottom"

    def test_total_count(self):
        assert len(TextPosition) == 3

    def test_is_string_enum(self):
        for p in TextPosition:
            assert isinstance(p.value, str)
            assert isinstance(p, str)


class TestTextAnimation:
    """TextAnimation 枚举测试"""

    def test_all_animations_exist(self):
        assert TextAnimation.NONE == "none"
        assert TextAnimation.FADE_IN == "fade_in"
        assert TextAnimation.SLIDE_UP == "slide_up"
        assert TextAnimation.SLIDE_DOWN == "slide_down"
        assert TextAnimation.SCALE == "scale"

    def test_total_count(self):
        assert len(TextAnimation) == 5

    def test_is_string_enum(self):
        for a in TextAnimation:
            assert isinstance(a.value, str)
            assert isinstance(a, str)


class TestBGMSource:
    """BGMSource 枚举测试"""

    def test_all_sources_exist(self):
        assert BGMSource.LIBRARY == "library"
        assert BGMSource.UPLOAD == "upload"
        assert BGMSource.AI_RECOMMEND == "ai_recommend"

    def test_total_count(self):
        assert len(BGMSource) == 3

    def test_is_string_enum(self):
        for s in BGMSource:
            assert isinstance(s.value, str)
            assert isinstance(s, str)


# ── 子结构模型测试 ────────────────────────────────────────────────────────────


class TestStrokeConfig:
    """StrokeConfig 描边配置测试"""

    def test_default_values(self):
        config = StrokeConfig()
        assert config.enabled is False
        assert config.color == "#000000"
        assert config.width == 1

    def test_custom_values(self):
        config = StrokeConfig(enabled=True, color="#ff0000", width=5)
        assert config.enabled is True
        assert config.color == "#ff0000"
        assert config.width == 5

    def test_width_min_boundary(self):
        config = StrokeConfig(width=1)
        assert config.width == 1

    def test_width_max_boundary(self):
        config = StrokeConfig(width=10)
        assert config.width == 10

    def test_width_below_min_raises(self):
        with pytest.raises(ValidationError):
            StrokeConfig(width=0)

    def test_width_above_max_raises(self):
        with pytest.raises(ValidationError):
            StrokeConfig(width=11)


class TestShadowConfig:
    """ShadowConfig 阴影配置测试"""

    def test_default_values(self):
        config = ShadowConfig()
        assert config.enabled is False
        assert config.blur == 4
        assert config.offset_x == 2
        assert config.offset_y == 2

    def test_custom_values(self):
        config = ShadowConfig(enabled=True, blur=10, offset_x=5, offset_y=3)
        assert config.enabled is True
        assert config.blur == 10
        assert config.offset_x == 5
        assert config.offset_y == 3

    def test_blur_min_boundary(self):
        config = ShadowConfig(blur=0)
        assert config.blur == 0

    def test_blur_max_boundary(self):
        config = ShadowConfig(blur=20)
        assert config.blur == 20

    def test_blur_above_max_raises(self):
        with pytest.raises(ValidationError):
            ShadowConfig(blur=21)

    def test_negative_offset(self):
        config = ShadowConfig(offset_x=-3, offset_y=-2)
        assert config.offset_x == -3
        assert config.offset_y == -2


class TestCoverConfig:
    """CoverConfig 封面配置测试"""

    def test_default_values(self):
        config = CoverConfig()
        assert config.type == CoverType.AI_FRAME
        assert config.image_url == ""
        assert config.frame_time is None

    def test_custom_values(self):
        config = CoverConfig(type=CoverType.MANUAL, image_url="http://img/1.jpg", frame_time=5.5)
        assert config.type == CoverType.MANUAL
        assert config.image_url == "http://img/1.jpg"
        assert config.frame_time == 5.5

    def test_frame_time_min_boundary(self):
        config = CoverConfig(frame_time=0.0)
        assert config.frame_time == 0.0

    def test_frame_time_negative_raises(self):
        with pytest.raises(ValidationError):
            CoverConfig(frame_time=-1.0)

    def test_string_type_conversion(self):
        """字符串枚举值自动转换"""
        config = CoverConfig(type="upload")
        assert config.type == CoverType.UPLOAD


class TestTitleConfig:
    """TitleConfig 标题配置测试"""

    def test_default_values(self):
        config = TitleConfig()
        assert config.enabled is True
        assert config.ai_auto is True
        assert config.text == ""
        assert config.position == TextPosition.TOP
        assert config.font == "思源黑体"
        assert config.color == "#ffffff"
        assert config.size == 48
        assert config.bold is True
        assert config.italic is False
        assert isinstance(config.stroke, StrokeConfig)
        assert isinstance(config.shadow, ShadowConfig)

    def test_custom_values(self):
        config = TitleConfig(
            enabled=False,
            ai_auto=False,
            text="测试标题",
            position=TextPosition.BOTTOM,
            font="微软雅黑",
            color="#000000",
            size=72,
            bold=False,
            italic=True,
        )
        assert config.enabled is False
        assert config.ai_auto is False
        assert config.text == "测试标题"
        assert config.position == TextPosition.BOTTOM
        assert config.size == 72

    def test_size_min_boundary(self):
        config = TitleConfig(size=12)
        assert config.size == 12

    def test_size_max_boundary(self):
        config = TitleConfig(size=120)
        assert config.size == 120

    def test_size_below_min_raises(self):
        with pytest.raises(ValidationError):
            TitleConfig(size=11)

    def test_size_above_max_raises(self):
        with pytest.raises(ValidationError):
            TitleConfig(size=121)

    def test_nested_stroke_config(self):
        config = TitleConfig(stroke={"enabled": True, "width": 3})
        assert config.stroke.enabled is True
        assert config.stroke.width == 3

    def test_nested_shadow_config(self):
        config = TitleConfig(shadow={"enabled": True, "blur": 8})
        assert config.shadow.enabled is True
        assert config.shadow.blur == 8


class TestSubtitleConfig:
    """SubtitleConfig 字幕配置测试"""

    def test_default_values(self):
        config = SubtitleConfig()
        assert config.enabled is True
        assert config.position == TextPosition.BOTTOM
        assert config.font == "思源黑体"
        assert config.color == "#ffffff"
        assert config.size == 24
        assert config.animation == TextAnimation.FADE_IN
        assert config.auto_generated is False
        assert config.language == ""
        assert config.max_chars_per_line == 20
        assert config.min_chars_per_segment == 8

    def test_custom_values(self):
        config = SubtitleConfig(
            enabled=False,
            position=TextPosition.TOP,
            size=36,
            animation=TextAnimation.SLIDE_UP,
            auto_generated=True,
            language="zh",
            max_chars_per_line=30,
            min_chars_per_segment=10,
        )
        assert config.enabled is False
        assert config.position == TextPosition.TOP
        assert config.animation == TextAnimation.SLIDE_UP
        assert config.auto_generated is True
        assert config.language == "zh"

    def test_size_min_boundary(self):
        config = SubtitleConfig(size=12)
        assert config.size == 12

    def test_size_max_boundary(self):
        config = SubtitleConfig(size=60)
        assert config.size == 60

    def test_size_below_min_raises(self):
        with pytest.raises(ValidationError):
            SubtitleConfig(size=11)

    def test_max_chars_min_boundary(self):
        config = SubtitleConfig(max_chars_per_line=8)
        assert config.max_chars_per_line == 8

    def test_max_chars_max_boundary(self):
        config = SubtitleConfig(max_chars_per_line=40)
        assert config.max_chars_per_line == 40

    def test_min_chars_min_boundary(self):
        config = SubtitleConfig(min_chars_per_segment=2)
        assert config.min_chars_per_segment == 2

    def test_min_chars_max_boundary(self):
        config = SubtitleConfig(min_chars_per_segment=20)
        assert config.min_chars_per_segment == 20


class TestBGMConfig:
    """BGMConfig 背景音乐配置测试"""

    def test_default_values(self):
        config = BGMConfig()
        assert config.enabled is False
        assert config.source == BGMSource.LIBRARY
        assert config.asset_id == ""
        assert config.preset_id == ""
        assert config.audio_url == ""
        assert config.volume == 0.3
        assert config.fade_in == 0.0
        assert config.fade_out == 0.0
        assert config.loop_enabled is True
        assert config.sidechain_enabled is False
        assert config.sidechain_ratio == 0.3
        assert config.sidechain_attack == 0.02
        assert config.sidechain_release == 0.5
        assert config.sidechain_threshold == -25.0

    def test_custom_values(self):
        config = BGMConfig(
            enabled=True,
            source=BGMSource.UPLOAD,
            asset_id="bgm_001",
            volume=0.5,
            fade_in=2.0,
            fade_out=3.0,
            loop_enabled=False,
            sidechain_enabled=True,
            sidechain_ratio=0.5,
            sidechain_attack=0.05,
            sidechain_release=1.0,
            sidechain_threshold=-20.0,
        )
        assert config.enabled is True
        assert config.source == BGMSource.UPLOAD
        assert config.asset_id == "bgm_001"
        assert config.volume == 0.5
        assert config.loop_enabled is False
        assert config.sidechain_enabled is True
        assert config.sidechain_ratio == 0.5

    def test_volume_min_boundary(self):
        config = BGMConfig(volume=0.0)
        assert config.volume == 0.0

    def test_volume_max_boundary(self):
        config = BGMConfig(volume=1.0)
        assert config.volume == 1.0

    def test_volume_above_max_raises(self):
        with pytest.raises(ValidationError):
            BGMConfig(volume=1.1)

    def test_fade_in_max_boundary(self):
        config = BGMConfig(fade_in=30.0)
        assert config.fade_in == 30.0

    def test_fade_in_above_max_raises(self):
        with pytest.raises(ValidationError):
            BGMConfig(fade_in=31.0)

    def test_sidechain_ratio_range(self):
        config = BGMConfig(sidechain_ratio=0.0)
        assert config.sidechain_ratio == 0.0
        config = BGMConfig(sidechain_ratio=1.0)
        assert config.sidechain_ratio == 1.0

    def test_sidechain_attack_min(self):
        config = BGMConfig(sidechain_attack=0.001)
        assert config.sidechain_attack == 0.001

    def test_sidechain_attack_below_min_raises(self):
        with pytest.raises(ValidationError):
            BGMConfig(sidechain_attack=0.0001)

    def test_sidechain_threshold_range(self):
        config = BGMConfig(sidechain_threshold=-60.0)
        assert config.sidechain_threshold == -60.0
        config = BGMConfig(sidechain_threshold=0.0)
        assert config.sidechain_threshold == 0.0

    def test_sidechain_threshold_out_of_range_raises(self):
        with pytest.raises(ValidationError):
            BGMConfig(sidechain_threshold=-61.0)
        with pytest.raises(ValidationError):
            BGMConfig(sidechain_threshold=1.0)


class TestExportConfig:
    """ExportConfig 导出配置测试"""

    def test_default_values(self):
        config = ExportConfig()
        assert config.resolution == "1080x1920"
        assert config.fps == 30
        assert config.video_bitrate == 8000
        assert config.audio_bitrate == 128
        assert config.format == "mp4"
        assert config.quality_preset == "balanced"
        assert config.watermark_enabled is False
        assert config.watermark_text == ""

    def test_custom_values(self):
        config = ExportConfig(
            resolution="2160x3840",
            fps=60,
            video_bitrate=20000,
            audio_bitrate=320,
            format="mov",
            quality_preset="best",
            watermark_enabled=True,
            watermark_text="测试水印",
        )
        assert config.resolution == "2160x3840"
        assert config.fps == 60
        assert config.video_bitrate == 20000
        assert config.format == "mov"

    def test_fps_min_boundary(self):
        config = ExportConfig(fps=15)
        assert config.fps == 15

    def test_fps_max_boundary(self):
        config = ExportConfig(fps=60)
        assert config.fps == 60

    def test_fps_below_min_raises(self):
        with pytest.raises(ValidationError):
            ExportConfig(fps=14)

    def test_fps_above_max_raises(self):
        with pytest.raises(ValidationError):
            ExportConfig(fps=61)

    def test_video_bitrate_range(self):
        config = ExportConfig(video_bitrate=1000)
        assert config.video_bitrate == 1000
        config = ExportConfig(video_bitrate=20000)
        assert config.video_bitrate == 20000

    def test_audio_bitrate_range(self):
        config = ExportConfig(audio_bitrate=64)
        assert config.audio_bitrate == 64
        config = ExportConfig(audio_bitrate=320)
        assert config.audio_bitrate == 320


class TestFilterConfig:
    """FilterConfig 滤镜配置测试"""

    def test_default_values(self):
        config = FilterConfig()
        assert config.enabled is False
        assert config.preset_id == "filter_none"
        assert config.intensity == 100
        assert config.brightness == 0.0
        assert config.contrast == 1.0
        assert config.saturation == 1.0
        assert config.warmth == 0.0

    def test_custom_values(self):
        config = FilterConfig(
            enabled=True,
            preset_id="filter_vintage",
            intensity=50,
            brightness=0.5,
            contrast=1.5,
            saturation=2.0,
            warmth=0.3,
        )
        assert config.enabled is True
        assert config.preset_id == "filter_vintage"
        assert config.intensity == 50
        assert config.brightness == 0.5

    def test_intensity_min_boundary(self):
        config = FilterConfig(intensity=0)
        assert config.intensity == 0

    def test_intensity_max_boundary(self):
        config = FilterConfig(intensity=100)
        assert config.intensity == 100

    def test_intensity_out_of_range_raises(self):
        with pytest.raises(ValidationError):
            FilterConfig(intensity=-1)
        with pytest.raises(ValidationError):
            FilterConfig(intensity=101)

    def test_brightness_range(self):
        config = FilterConfig(brightness=-1.0)
        assert config.brightness == -1.0
        config = FilterConfig(brightness=1.0)
        assert config.brightness == 1.0

    def test_contrast_range(self):
        config = FilterConfig(contrast=0.0)
        assert config.contrast == 0.0
        config = FilterConfig(contrast=2.0)
        assert config.contrast == 2.0

    def test_saturation_range(self):
        config = FilterConfig(saturation=0.0)
        assert config.saturation == 0.0
        config = FilterConfig(saturation=3.0)
        assert config.saturation == 3.0

    def test_warmth_range(self):
        config = FilterConfig(warmth=-1.0)
        assert config.warmth == -1.0
        config = FilterConfig(warmth=1.0)
        assert config.warmth == 1.0

    def test_brightness_out_of_range_raises(self):
        with pytest.raises(ValidationError):
            FilterConfig(brightness=-1.1)
        with pytest.raises(ValidationError):
            FilterConfig(brightness=1.1)


# ── 完整 config 模型测试 ─────────────────────────────────────────────────────


class TestEditPlanConfigSchema:
    """EditPlanConfigSchema 完整计划配置测试"""

    def test_default_values(self):
        config = EditPlanConfigSchema()
        assert isinstance(config.cover, CoverConfig)
        assert isinstance(config.title, TitleConfig)
        assert isinstance(config.subtitle, SubtitleConfig)
        assert isinstance(config.bgm, BGMConfig)
        assert isinstance(config.export, ExportConfig)
        assert isinstance(config.filter, FilterConfig)
        assert config.editing_mode == "one_take"

    def test_partial_update(self):
        """只传部分字段，其余保持默认"""
        config = EditPlanConfigSchema(
            title={"enabled": False},
            bgm={"enabled": True, "volume": 0.5},
            editing_mode="pip",
        )
        assert config.title.enabled is False
        assert config.bgm.enabled is True
        assert config.bgm.volume == 0.5
        assert config.editing_mode == "pip"
        # 其他字段保持默认
        assert config.cover.type == CoverType.AI_FRAME
        assert config.subtitle.enabled is True

    def test_nested_model_preservation(self):
        """嵌套模型完整可用"""
        config = EditPlanConfigSchema()
        assert config.title.stroke.enabled is False
        assert config.title.shadow.blur == 4
        assert config.bgm.sidechain_ratio == 0.3


class TestEditTemplateConfigSchema:
    """EditTemplateConfigSchema 模板配置测试"""

    def test_default_values(self):
        config = EditTemplateConfigSchema()
        assert isinstance(config.cover, CoverConfig)
        assert isinstance(config.title, TitleConfig)
        assert isinstance(config.subtitle, SubtitleConfig)
        assert isinstance(config.bgm, BGMConfig)
        assert isinstance(config.export, ExportConfig)
        assert isinstance(config.filter, FilterConfig)
        assert config.editing_mode == "one_take"
        assert config.transition_enabled is True

    def test_transition_enabled_false(self):
        config = EditTemplateConfigSchema(transition_enabled=False)
        assert config.transition_enabled is False

    def test_partial_update(self):
        config = EditTemplateConfigSchema(
            filter={"enabled": True, "preset_id": "filter_cinematic"},
            transition_enabled=False,
            editing_mode="voice_over",
        )
        assert config.filter.enabled is True
        assert config.filter.preset_id == "filter_cinematic"
        assert config.transition_enabled is False
        assert config.editing_mode == "voice_over"


# ── 默认值常量测试 ────────────────────────────────────────────────────────────


class TestDefaultConfigs:
    """默认值常量测试"""

    def test_plan_config_structure(self):
        """DEFAULT_EDIT_PLAN_CONFIG 结构完整"""
        assert "cover" in DEFAULT_EDIT_PLAN_CONFIG
        assert "title" in DEFAULT_EDIT_PLAN_CONFIG
        assert "subtitle" in DEFAULT_EDIT_PLAN_CONFIG
        assert "bgm" in DEFAULT_EDIT_PLAN_CONFIG
        assert "export" in DEFAULT_EDIT_PLAN_CONFIG
        assert "filter" in DEFAULT_EDIT_PLAN_CONFIG
        assert "editing_mode" in DEFAULT_EDIT_PLAN_CONFIG

    def test_template_config_has_transition(self):
        """模板配置比计划配置多一个 transition_enabled"""
        assert "transition_enabled" in DEFAULT_EDIT_TEMPLATE_CONFIG
        assert DEFAULT_EDIT_TEMPLATE_CONFIG["transition_enabled"] is True

    def test_template_extends_plan(self):
        """模板配置是计划配置的超集"""
        for key in DEFAULT_EDIT_PLAN_CONFIG:
            assert key in DEFAULT_EDIT_TEMPLATE_CONFIG

    def test_defaults_are_deep_copy_safe(self):
        """修改默认值不会影响常量本身"""
        original = copy.deepcopy(DEFAULT_EDIT_PLAN_CONFIG)
        config = EditPlanConfigSchema()
        config.title.text = "modified"
        assert DEFAULT_EDIT_PLAN_CONFIG == original


# ── normalize 函数测试 ────────────────────────────────────────────────────────


class TestNormalizePlanConfig:
    """normalize_plan_config 函数测试"""

    def test_none_returns_default(self):
        result = normalize_plan_config(None)
        assert result == DEFAULT_EDIT_PLAN_CONFIG
        # 确保是深拷贝
        result["cover"]["type"] = "manual"
        assert DEFAULT_EDIT_PLAN_CONFIG["cover"]["type"] == "ai_frame"

    def test_empty_dict_returns_default(self):
        result = normalize_plan_config({})
        assert result == DEFAULT_EDIT_PLAN_CONFIG

    def test_partial_cover_update(self):
        raw = {"cover": {"type": "manual", "frame_time": 5.0}}
        result = normalize_plan_config(raw)
        assert result["cover"]["type"] == "manual"
        assert result["cover"]["frame_time"] == 5.0
        # 其他字段保留默认
        assert result["cover"]["image_url"] == ""

    def test_partial_title_update(self):
        raw = {"title": {"enabled": False, "text": "自定义标题"}}
        result = normalize_plan_config(raw)
        assert result["title"]["enabled"] is False
        assert result["title"]["text"] == "自定义标题"
        assert result["title"]["size"] == 48  # 默认值保留

    def test_partial_subtitle_update(self):
        raw = {"subtitle": {"enabled": False, "size": 32}}
        result = normalize_plan_config(raw)
        assert result["subtitle"]["enabled"] is False
        assert result["subtitle"]["size"] == 32

    def test_partial_bgm_update(self):
        raw = {"bgm": {"enabled": True, "volume": 0.8}}
        result = normalize_plan_config(raw)
        assert result["bgm"]["enabled"] is True
        assert result["bgm"]["volume"] == 0.8

    def test_editing_mode_update(self):
        raw = {"editing_mode": "pip"}
        result = normalize_plan_config(raw)
        assert result["editing_mode"] == "pip"

    def test_non_standard_fields_preserved(self):
        """非标准字段会被保留（透传）"""
        raw = {"generation_task_id": "task_123", "custom_field": "value"}
        result = normalize_plan_config(raw)
        assert result["generation_task_id"] == "task_123"
        assert result["custom_field"] == "value"

    def test_full_update(self):
        """多个字段同时更新"""
        raw = {
            "cover": {"type": "upload", "image_url": "http://img.jpg"},
            "title": {"enabled": False},
            "bgm": {"enabled": True, "volume": 0.5},
            "editing_mode": "voice_pip",
            "extra_key": "extra_val",
        }
        result = normalize_plan_config(raw)
        assert result["cover"]["type"] == "upload"
        assert result["title"]["enabled"] is False
        assert result["bgm"]["enabled"] is True
        assert result["bgm"]["volume"] == 0.5
        assert result["editing_mode"] == "voice_pip"
        assert result["extra_key"] == "extra_val"

    def test_non_dict_section_ignored(self):
        """section不是dict时忽略"""
        raw = {"cover": "not_a_dict"}
        result = normalize_plan_config(raw)
        # cover 应保持默认值
        assert result["cover"]["type"] == "ai_frame"
        # 但 cover 字段本身作为非标准字段保留
        # 不对，看实现：只有当 isinstance(raw[section_key], dict) 时才 update
        # 非dict的section会作为非标准字段保留吗？看实现：
        # section_key in raw and isinstance -> update
        # 然后遍历所有key，不在标准列表中的才会保留
        # cover 在标准列表中，所以不会被保留为非标准字段
        # 所以结果应该是默认的cover配置
        assert isinstance(result["cover"], dict)
        assert result["cover"]["type"] == "ai_frame"

    def test_editing_mode_non_string_ignored(self):
        """editing_mode不是字符串时忽略"""
        raw = {"editing_mode": 123}
        result = normalize_plan_config(raw)
        # 作为非标准字段保留？不，看实现：
        # 如果 "editing_mode" in raw and isinstance(str) 才更新
        # 然后遍历所有key，不在标准列表中的保留
        # editing_mode 在标准列表中，所以不会保留为非标准
        # 所以 editing_mode 保持默认
        assert result["editing_mode"] == "one_take"


class TestNormalizeTemplateConfig:
    """normalize_template_config 函数测试"""

    def test_none_returns_default(self):
        result = normalize_template_config(None)
        assert result == DEFAULT_EDIT_TEMPLATE_CONFIG
        # 确保是深拷贝
        result["transition_enabled"] = False
        assert DEFAULT_EDIT_TEMPLATE_CONFIG["transition_enabled"] is True

    def test_empty_dict_returns_default(self):
        result = normalize_template_config({})
        assert result == DEFAULT_EDIT_TEMPLATE_CONFIG

    def test_transition_enabled_update(self):
        raw = {"transition_enabled": False}
        result = normalize_template_config(raw)
        assert result["transition_enabled"] is False

    def test_transition_enabled_non_bool_ignored(self):
        raw = {"transition_enabled": "yes"}
        result = normalize_template_config(raw)
        # transition_enabled 在标准列表中，非bool则不更新
        # 也不会作为非标准字段保留
        assert result["transition_enabled"] is True

    def test_partial_sections_update(self):
        raw = {
            "cover": {"type": "ai_regenerate"},
            "filter": {"enabled": True, "intensity": 75},
            "transition_enabled": False,
            "editing_mode": "pip",
        }
        result = normalize_template_config(raw)
        assert result["cover"]["type"] == "ai_regenerate"
        assert result["filter"]["enabled"] is True
        assert result["filter"]["intensity"] == 75
        assert result["transition_enabled"] is False
        assert result["editing_mode"] == "pip"

    def test_non_standard_fields_preserved(self):
        raw = {"template_version": 3, "author": "test"}
        result = normalize_template_config(raw)
        assert result["template_version"] == 3
        assert result["author"] == "test"

    def test_template_has_extra_field_over_plan(self):
        """模板normalize结果比计划多transition_enabled"""
        plan_result = normalize_plan_config({"bgm": {"enabled": True}})
        template_result = normalize_template_config({"bgm": {"enabled": True}})
        assert "transition_enabled" in template_result
        assert "transition_enabled" not in plan_result
