"""config_schemas 领域层单元测试 - 配置 schema / 枚举 / 标准化函数"""

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 TestEnums:
    """枚举类型测试"""

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

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

    def test_text_animation_values(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_bgm_source_values(self):
        assert BGMSource.LIBRARY == "library"
        assert BGMSource.UPLOAD == "upload"
        assert BGMSource.AI_RECOMMEND == "ai_recommend"

    def test_enums_are_str_enum(self):
        """枚举都是 str 类型"""
        assert isinstance(CoverType.AI_FRAME, str)
        assert isinstance(TextPosition.TOP, str)
        assert isinstance(TextAnimation.FADE_IN, str)
        assert isinstance(BGMSource.LIBRARY, str)


class TestStrokeConfig:
    """StrokeConfig 测试"""

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

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

    def test_width_max(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_defaults(self):
        config = ShadowConfig()
        assert config.enabled is False
        assert config.blur == 4
        assert config.offset_x == 2
        assert config.offset_y == 2

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

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

    def test_blur_below_min_raises(self):
        with pytest.raises(ValidationError):
            ShadowConfig(blur=-1)

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


class TestCoverConfig:
    """CoverConfig 测试"""

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

    def test_frame_time_ge_zero(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_custom_values(self):
        config = CoverConfig(
            type=CoverType.MANUAL,
            image_url="http://example.com/cover.jpg",
            frame_time=5.5,
        )
        assert config.type == CoverType.MANUAL
        assert config.image_url == "http://example.com/cover.jpg"
        assert config.frame_time == 5.5


class TestTitleConfig:
    """TitleConfig 测试"""

    def test_defaults(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_size_min(self):
        config = TitleConfig(size=12)
        assert config.size == 12

    def test_size_max(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)


class TestSubtitleConfig:
    """SubtitleConfig 测试"""

    def test_defaults(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_size_min(self):
        config = SubtitleConfig(size=12)
        assert config.size == 12

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

    def test_size_out_of_range_raises(self):
        with pytest.raises(ValidationError):
            SubtitleConfig(size=61)

    def test_max_chars_per_line_range(self):
        config = SubtitleConfig(max_chars_per_line=40)
        assert config.max_chars_per_line == 40
        with pytest.raises(ValidationError):
            SubtitleConfig(max_chars_per_line=7)
        with pytest.raises(ValidationError):
            SubtitleConfig(max_chars_per_line=41)

    def test_min_chars_per_segment_range(self):
        config = SubtitleConfig(min_chars_per_segment=20)
        assert config.min_chars_per_segment == 20
        with pytest.raises(ValidationError):
            SubtitleConfig(min_chars_per_segment=1)
        with pytest.raises(ValidationError):
            SubtitleConfig(min_chars_per_segment=21)


class TestBGMConfig:
    """BGMConfig 测试"""

    def test_defaults(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_volume_range(self):
        with pytest.raises(ValidationError):
            BGMConfig(volume=-0.1)
        with pytest.raises(ValidationError):
            BGMConfig(volume=1.1)

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

    def test_sidechain_ratio_range(self):
        with pytest.raises(ValidationError):
            BGMConfig(sidechain_ratio=1.1)
        with pytest.raises(ValidationError):
            BGMConfig(sidechain_ratio=-0.1)

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

    def test_sidechain_release_range(self):
        with pytest.raises(ValidationError):
            BGMConfig(sidechain_release=0.001)
        with pytest.raises(ValidationError):
            BGMConfig(sidechain_release=5.1)

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


class TestExportConfig:
    """ExportConfig 测试"""

    def test_defaults(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_fps_range(self):
        with pytest.raises(ValidationError):
            ExportConfig(fps=14)
        with pytest.raises(ValidationError):
            ExportConfig(fps=61)

    def test_video_bitrate_range(self):
        with pytest.raises(ValidationError):
            ExportConfig(video_bitrate=999)
        with pytest.raises(ValidationError):
            ExportConfig(video_bitrate=20001)

    def test_audio_bitrate_range(self):
        with pytest.raises(ValidationError):
            ExportConfig(audio_bitrate=63)
        with pytest.raises(ValidationError):
            ExportConfig(audio_bitrate=321)


class TestFilterConfig:
    """FilterConfig 测试"""

    def test_defaults(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_intensity_range(self):
        with pytest.raises(ValidationError):
            FilterConfig(intensity=-1)
        with pytest.raises(ValidationError):
            FilterConfig(intensity=101)

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

    def test_contrast_range(self):
        with pytest.raises(ValidationError):
            FilterConfig(contrast=-0.1)
        with pytest.raises(ValidationError):
            FilterConfig(contrast=2.1)

    def test_saturation_range(self):
        with pytest.raises(ValidationError):
            FilterConfig(saturation=-0.1)
        with pytest.raises(ValidationError):
            FilterConfig(saturation=3.1)

    def test_warmth_range(self):
        with pytest.raises(ValidationError):
            FilterConfig(warmth=-1.1)
        with pytest.raises(ValidationError):
            FilterConfig(warmth=1.1)


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

    def test_defaults(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={"text": "自定义标题", "size": 36},
            bgm={"enabled": True, "volume": 0.5},
        )
        assert config.title.text == "自定义标题"
        assert config.title.size == 36
        assert config.title.font == "思源黑体"  # 其他字段默认
        assert config.bgm.enabled is True
        assert config.bgm.volume == 0.5
        assert config.cover.type == CoverType.AI_FRAME  # 未设置的保持默认


class TestEditTemplateConfigSchema:
    """EditTemplateConfigSchema 测试"""

    def test_defaults(self):
        config = EditTemplateConfigSchema()
        assert isinstance(config.cover, CoverConfig)
        assert config.editing_mode == "one_take"
        assert config.transition_enabled is True

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


class TestDefaultConfigs:
    """默认配置常量测试"""

    def test_plan_config_structure(self):
        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_enabled(self):
        assert "transition_enabled" in DEFAULT_EDIT_TEMPLATE_CONFIG
        assert DEFAULT_EDIT_TEMPLATE_CONFIG["transition_enabled"] is True

    def test_template_inherits_from_plan(self):
        """模板配置继承计划配置的所有字段"""
        for key in DEFAULT_EDIT_PLAN_CONFIG:
            assert key in DEFAULT_EDIT_TEMPLATE_CONFIG


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

    def test_none_returns_default_copy(self):
        result = normalize_plan_config(None)
        assert result == DEFAULT_EDIT_PLAN_CONFIG
        # 确保是深拷贝
        result["title"]["text"] = "modified"
        assert DEFAULT_EDIT_PLAN_CONFIG["title"]["text"] == ""

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

    def test_updates_cover_section(self):
        result = normalize_plan_config({"cover": {"type": "manual", "frame_time": 5.0}})
        assert result["cover"]["type"] == "manual"
        assert result["cover"]["frame_time"] == 5.0
        assert result["cover"]["image_url"] == ""  # 默认保留

    def test_updates_title_section(self):
        result = normalize_plan_config({"title": {"text": "hello", "size": 32}})
        assert result["title"]["text"] == "hello"
        assert result["title"]["size"] == 32
        assert result["title"]["font"] == "思源黑体"

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

    def test_updates_editing_mode(self):
        result = normalize_plan_config({"editing_mode": "smart"})
        assert result["editing_mode"] == "smart"

    def test_preserves_extra_fields(self):
        """非标准字段被保留"""
        result = normalize_plan_config({"custom_field": "value", "generation_task_id": "task-1"})
        assert result["custom_field"] == "value"
        assert result["generation_task_id"] == "task-1"

    def test_ignores_non_dict_section(self):
        """section 不是 dict 时忽略"""
        result = normalize_plan_config({"cover": "not_a_dict"})
        assert result["cover"] == DEFAULT_EDIT_PLAN_CONFIG["cover"]

    def test_ignores_non_str_editing_mode(self):
        result = normalize_plan_config({"editing_mode": 123})
        assert result["editing_mode"] == "one_take"

    def test_deep_copy_independence(self):
        """修改结果不影响默认值"""
        result = normalize_plan_config({})
        result["title"]["size"] = 999
        assert DEFAULT_EDIT_PLAN_CONFIG["title"]["size"] == 48


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

    def test_none_returns_default_copy(self):
        result = normalize_template_config(None)
        assert result == DEFAULT_EDIT_TEMPLATE_CONFIG

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

    def test_ignores_non_bool_transition_enabled(self):
        result = normalize_template_config({"transition_enabled": "yes"})
        assert result["transition_enabled"] is True

    def test_updates_sections(self):
        result = normalize_template_config(
            {
                "title": {"text": "模板标题"},
                "bgm": {"enabled": True},
            }
        )
        assert result["title"]["text"] == "模板标题"
        assert result["bgm"]["enabled"] is True

    def test_preserves_extra_fields(self):
        result = normalize_template_config({"custom": "value"})
        assert result["custom"] == "value"

    def test_deep_copy_independence(self):
        result = normalize_template_config({})
        result["title"]["font"] = "CustomFont"
        assert DEFAULT_EDIT_TEMPLATE_CONFIG["title"]["font"] == "思源黑体"
