"""Domain 小模块合集单元测试。

覆盖零测试的小 domain 模块：
- EditingMode 枚举
- Template / TemplateSegment
- TemplateClipConfig + ClipType + TransitionEffect
- EditTemplateVersion
- VoiceLibraryItem
- TitleLibraryItem
- Recipe / RecipeItem
"""

from datetime import datetime, timezone

import pytest

from packages.domain.editing_mode import EditingMode
from packages.domain.recipe import RecipeItem
from packages.domain.template import TemplateSegment
from packages.domain.template_clip_config import (
    ClipType,
    TemplateClipConfig,
    TransitionEffect,
)
from packages.domain.template_version import EditTemplateVersion
from packages.domain.title_library import TitleLibraryItem
from packages.domain.voice_library import VoiceLibraryItem


class TestEditingMode:
    def test_all_modes_exist(self):
        assert EditingMode.ONE_TAKE.value == "one_take"
        assert EditingMode.PIP.value == "pip"
        assert EditingMode.VOICE_OVER.value == "voice_over"
        assert EditingMode.VOICE_PIP.value == "voice_pip"

    def test_from_string(self):
        assert EditingMode("one_take") == EditingMode.ONE_TAKE
        assert EditingMode("voice_over") == EditingMode.VOICE_OVER

    def test_invalid_mode_raises(self):
        with pytest.raises(ValueError):
            EditingMode("invalid_mode")

    def test_is_str_enum(self):
        # StrEnum 的值是字符串，可以直接比较
        assert EditingMode.ONE_TAKE == "one_take"


class TestTemplateSegment:
    def test_create_minimal(self):
        seg = TemplateSegment(
            id="seg1",
            template_id="tpl1",
            segment_order=1,
            duration_min=5.0,
            duration_max=10.0,
        )
        assert seg.id == "seg1"
        assert seg.template_id == "tpl1"
        assert seg.segment_order == 1
        assert seg.duration_min == 5.0
        assert seg.duration_max == 10.0
        assert seg.material_type is None
        assert isinstance(seg.created_at, datetime)

    def test_create_with_material_type(self):
        seg = TemplateSegment(
            id="seg2",
            template_id="tpl1",
            segment_order=2,
            duration_min=3.0,
            duration_max=8.0,
            material_type="人物",
        )
        assert seg.material_type == "人物"


class TestClipType:
    def test_basic_types_exist(self):
        assert hasattr(ClipType, "MAIN")
        assert hasattr(ClipType, "INTRO")
        assert hasattr(ClipType, "OUTRO")
        assert hasattr(ClipType, "TRANSITION")

    def test_values_are_strings(self):
        for ct in ClipType:
            assert isinstance(ct.value, str)


class TestTransitionEffect:
    def test_effects_exist(self):
        assert TransitionEffect.CUT.value == "cut"
        assert TransitionEffect.FADE.value == "fade"
        assert TransitionEffect.DISSOLVE.value == "dissolve"
        # 至少有 5 种以上转场效果
        assert len(list(TransitionEffect)) >= 5


class TestTemplateClipConfig:
    def test_create_minimal(self):
        config = TemplateClipConfig.create(
            template_id="tpl1",
            clip_type=ClipType.MAIN,
            order=1,
            min_duration=3.0,
            max_duration=8.0,
        )
        assert config.id is not None
        assert config.template_id == "tpl1"
        assert config.clip_type == ClipType.MAIN
        assert config.order == 1
        assert config.min_duration == 3.0
        assert config.max_duration == 8.0

    def test_create_with_string_type(self):
        config = TemplateClipConfig.create(
            template_id="tpl1",
            clip_type="intro",
            order=0,
            min_duration=2.0,
            max_duration=5.0,
        )
        assert config.clip_type == ClipType.INTRO

    def test_has_duration_range_true(self):
        config = TemplateClipConfig.create(
            template_id="t1",
            clip_type=ClipType.MAIN,
            order=1,
            min_duration=3.0,
            max_duration=8.0,
        )
        assert config.has_duration_range is True

    def test_has_duration_range_false_when_both_zero(self):
        config = TemplateClipConfig.create(
            template_id="t1",
            clip_type=ClipType.MAIN,
            order=1,
        )
        assert config.has_duration_range is False

    def test_default_duration_midpoint(self):
        config = TemplateClipConfig.create(
            template_id="t1",
            clip_type=ClipType.MAIN,
            order=1,
            min_duration=4.0,
            max_duration=6.0,
        )
        assert config.default_duration == pytest.approx(5.0)

    def test_default_duration_when_only_max(self):
        config = TemplateClipConfig.create(
            template_id="t1",
            clip_type=ClipType.MAIN,
            order=1,
            max_duration=5.0,
        )
        assert config.default_duration == 5.0

    def test_create_negative_min_duration_raises(self):
        with pytest.raises(ValueError, match="min_duration"):
            TemplateClipConfig.create(
                template_id="t1",
                clip_type=ClipType.MAIN,
                order=1,
                min_duration=-1.0,
            )

    def test_create_min_greater_than_max_raises(self):
        with pytest.raises(ValueError, match="min_duration.*max_duration"):
            TemplateClipConfig.create(
                template_id="t1",
                clip_type=ClipType.MAIN,
                order=1,
                min_duration=10.0,
                max_duration=5.0,
            )

    def test_create_empty_template_id_raises(self):
        with pytest.raises(ValueError, match="template_id"):
            TemplateClipConfig.create(
                template_id="",
                clip_type=ClipType.MAIN,
                order=1,
            )

    def test_default_transition_is_cut(self):
        config = TemplateClipConfig.create(
            template_id="t1",
            clip_type=ClipType.MAIN,
            order=1,
        )
        assert config.transition_effect == TransitionEffect.CUT

    def test_custom_transition_effect(self):
        config = TemplateClipConfig.create(
            template_id="t1",
            clip_type=ClipType.MAIN,
            order=1,
            transition_effect="fade",
        )
        assert config.transition_effect == TransitionEffect.FADE


class TestEditTemplateVersion:
    def test_create_minimal(self):
        version = EditTemplateVersion.create(
            template_id="tpl1",
            version=1,
        )
        assert version.id is not None
        assert len(version.id) == 32
        assert version.template_id == "tpl1"
        assert version.version == 1
        assert version.config == {}
        assert version.clip_configs == []
        assert version.published_by == ""
        assert version.change_note == ""
        assert version.name == ""
        assert version.editing_mode == "one_take"
        assert isinstance(version.created_at, datetime)

    def test_create_with_config_and_clip_configs(self):
        version = EditTemplateVersion.create(
            template_id="tpl1",
            version=2,
            config={"layout": "one_take"},
            clip_configs=[{"clip_id": "c1", "type": "main"}],
            published_by="user1",
            change_note="添加了片头效果",
        )
        assert version.config == {"layout": "one_take"}
        assert len(version.clip_configs) == 1
        assert version.published_by == "user1"
        assert version.change_note == "添加了片头效果"

    def test_create_with_name_and_mode(self):
        version = EditTemplateVersion.create(
            template_id="t1",
            version=1,
            name="v1.0 正式版",
            editing_mode="voice_over",
        )
        assert version.name == "v1.0 正式版"
        assert version.editing_mode == "voice_over"

    def test_create_unique_ids(self):
        v1 = EditTemplateVersion.create("t1", 1)
        v2 = EditTemplateVersion.create("t1", 2)
        assert v1.id != v2.id

    def test_none_config_defaults_to_empty_dict(self):
        version = EditTemplateVersion.create("t1", 1, config=None)
        assert version.config == {}

    def test_none_clip_configs_defaults_to_empty_list(self):
        version = EditTemplateVersion.create("t1", 1, clip_configs=None)
        assert version.clip_configs == []


class TestVoiceLibraryItem:
    def test_create_minimal(self):
        item = VoiceLibraryItem(
            id="v1",
            user_id="u1",
            name="我的配音",
        )
        assert item.id == "v1"
        assert item.user_id == "u1"
        assert item.name == "我的配音"
        assert item.text == ""
        assert item.voice_provider == ""
        assert item.duration == 0
        assert item.status == "completed"
        assert item.tags == []
        assert item.project_id is None
        assert isinstance(item.created_at, datetime)

    def test_create_with_all_fields(self):
        item = VoiceLibraryItem(
            id="v2",
            user_id="u1",
            name="产品介绍",
            text="欢迎来到我们的产品",
            voice_provider="cosyvoice",
            voice_id="voice_001",
            voice_name="温柔女声",
            audio_url="https://cdn/v2.mp3",
            duration=30.5,
            file_size=102400,
            status="processing",
            project_id="proj1",
            tags=["产品", "介绍"],
        )
        assert item.text == "欢迎来到我们的产品"
        assert item.voice_provider == "cosyvoice"
        assert item.voice_id == "voice_001"
        assert item.audio_url == "https://cdn/v2.mp3"
        assert item.duration == 30.5
        assert item.file_size == 102400
        assert item.status == "processing"
        assert item.project_id == "proj1"
        assert item.tags == ["产品", "介绍"]


class TestTitleLibraryItem:
    def test_create_minimal(self):
        item = TitleLibraryItem(
            id="t1",
            user_id="u1",
            name="爆款标题1",
            text="这是一个爆款标题",
        )
        assert item.id == "t1"
        assert item.user_id == "u1"
        assert item.name == "爆款标题1"
        assert item.text == "这是一个爆款标题"
        assert item.category == "default"
        assert item.description == ""
        assert item.tags == []
        assert item.usage_count == 0
        assert item.is_active is True

    def test_create_with_category(self):
        item = TitleLibraryItem(
            id="t2",
            user_id="u1",
            name="美食标题",
            text="太好吃了！",
            category="美食",
        )
        assert item.category == "美食"

    def test_inactive_item(self):
        item = TitleLibraryItem(
            id="t3",
            user_id="u1",
            name="旧标题",
            text="旧文案",
            is_active=False,
        )
        assert item.is_active is False

    def test_usage_count_increment(self):
        item = TitleLibraryItem(
            id="t4",
            user_id="u1",
            name="T",
            text="T",
        )
        item.usage_count += 1
        assert item.usage_count == 1


class TestRecipeItem:
    def test_create_minimal(self):
        item = RecipeItem(
            id="ri1",
            recipe_id="r1",
            item_type="asset",
            item_id="asset_001",
        )
        assert item.id == "ri1"
        assert item.recipe_id == "r1"
        assert item.item_type == "asset"
        assert item.item_id == "asset_001"
        assert item.position == 0
        assert item.metadata_ == {}

    def test_create_with_position_and_metadata(self):
        item = RecipeItem(
            id="ri2",
            recipe_id="r1",
            item_type="title",
            item_id="title_001",
            position=2,
            metadata_={"style": "bold"},
        )
        assert item.position == 2
        assert item.metadata_ == {"style": "bold"}

    def test_item_types_variety(self):
        asset_item = RecipeItem(id="a", recipe_id="r", item_type="asset", item_id="i1")
        title_item = RecipeItem(id="t", recipe_id="r", item_type="title", item_id="i2")
        voice_item = RecipeItem(id="v", recipe_id="r", item_type="voice", item_id="i3")
        assert asset_item.item_type == "asset"
        assert title_item.item_type == "title"
        assert voice_item.item_type == "voice"
