from dataclasses import FrozenInstanceError

"""domain 层剩余小模块单测 - bgm_utils/exceptions/editing_mode/recipe/template/template_version/template_clip_config/preset_bgm/preset_voices/title_library/voice_library"""

import pytest

from packages.domain.bgm_utils import merge_bgm_config
from packages.domain.editing_mode import EditingMode
from packages.domain.exceptions import (
    DomainError,
    NotFoundError,
    QuotaExceededError,
    ValidationError,
)
from packages.domain.preset_bgm import (
    PRESET_BGM_LIBRARY,
    PresetBGM,
    get_preset_bgm,
    list_preset_bgm_by_style,
    search_preset_bgm,
)
from packages.domain.preset_voices import (
    PRESET_VOICES,
    PresetVoice,
    get_preset_voice_by_id,
    get_preset_voices,
    is_preset_voice,
)
from packages.domain.recipe import Recipe, RecipeItem
from packages.domain.template import Template, TemplateCategory, 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

# ── EditingMode ───────────────────────────────────────────────────────────────


class TestEditingMode:
    """EditingMode 枚举测试"""

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

    def test_str_enum(self):
        assert isinstance(EditingMode.ONE_TAKE, str)

    def test_four_modes(self):
        assert len(EditingMode) == 4


# ── Exceptions ────────────────────────────────────────────────────────────────


class TestDomainExceptions:
    """异常类测试"""

    def test_domain_error_base(self):
        err = DomainError("test")
        assert isinstance(err, Exception)
        assert str(err) == "test"

    def test_not_found_error(self):
        err = NotFoundError("not found")
        assert isinstance(err, DomainError)
        assert str(err) == "not found"

    def test_validation_error(self):
        err = ValidationError("invalid")
        assert isinstance(err, DomainError)
        assert str(err) == "invalid"

    def test_quota_exceeded_error(self):
        err = QuotaExceededError(dimension="storage", limit=100, used=150)
        assert isinstance(err, DomainError)
        assert err.dimension == "storage"
        assert err.limit == 100
        assert err.used == 150
        assert "storage" in str(err)
        assert "150/100" in str(err)

    def test_not_found_is_domain_error(self):
        assert issubclass(NotFoundError, DomainError)

    def test_validation_is_domain_error(self):
        assert issubclass(ValidationError, DomainError)

    def test_quota_exceeded_is_domain_error(self):
        assert issubclass(QuotaExceededError, DomainError)


# ── BGM Utils ────────────────────────────────────────────────────────────────


class TestMergeBgmConfig:
    """merge_bgm_config 函数测试"""

    def test_user_empty_returns_template_copy(self):
        template = {"enabled": True, "volume": 0.5, "source": "library"}
        result = merge_bgm_config(template, {})
        assert result == template
        # 确保是拷贝不是引用
        result["volume"] = 0.9
        assert template["volume"] == 0.5

    def test_template_empty_returns_user_copy(self):
        user = {"enabled": False, "volume": 0.3}
        result = merge_bgm_config({}, user)
        assert result == user

    def test_user_none_returns_template(self):
        template = {"enabled": True, "volume": 0.5}
        result = merge_bgm_config(template, None)  # type: ignore
        assert result == template

    def test_template_none_returns_user(self):
        user = {"enabled": True, "volume": 0.5}
        result = merge_bgm_config(None, user)  # type: ignore
        assert result == user

    def test_user_overrides_template(self):
        template = {"volume": 0.3, "source": "library", "asset_id": "tpl-1"}
        user = {"volume": 0.8, "asset_id": "user-1"}
        result = merge_bgm_config(template, user)
        assert result["volume"] == 0.8
        assert result["asset_id"] == "user-1"
        assert result["source"] == "library"  # 模板保留

    def test_enabled_special_handling_user_not_set(self):
        """用户没传 enabled 时保留模板的 enabled"""
        template = {"enabled": True, "volume": 0.5}
        user = {"volume": 0.8}
        result = merge_bgm_config(template, user)
        assert result["enabled"] is True  # 保留模板值

    def test_enabled_user_explicit_false(self):
        """用户显式传了 enabled=False 则覆盖"""
        template = {"enabled": True, "volume": 0.5}
        user = {"enabled": False}
        result = merge_bgm_config(template, user)
        assert result["enabled"] is False

    def test_enabled_user_explicit_true(self):
        """用户显式传了 enabled=True 则覆盖"""
        template = {"enabled": False, "volume": 0.5}
        user = {"enabled": True}
        result = merge_bgm_config(template, user)
        assert result["enabled"] is True

    def test_full_merge(self):
        """完整合并场景"""
        template = {
            "enabled": True,
            "source": "library",
            "volume": 0.3,
            "fade_in": 0.0,
            "fade_out": 0.0,
            "loop_enabled": True,
        }
        user = {
            "volume": 0.7,
            "asset_id": "my-bgm",
            "fade_in": 1.0,
        }
        result = merge_bgm_config(template, user)
        assert result["enabled"] is True  # 保留模板
        assert result["volume"] == 0.7  # 用户覆盖
        assert result["source"] == "library"  # 模板保留
        assert result["asset_id"] == "my-bgm"  # 用户新增
        assert result["fade_in"] == 1.0  # 用户覆盖
        assert result["fade_out"] == 0.0  # 模板保留
        assert result["loop_enabled"] is True  # 模板保留


# ── Recipe ───────────────────────────────────────────────────────────────────


class TestRecipe:
    """Recipe / RecipeItem 测试"""

    def test_recipe_item_create(self):
        item = RecipeItem(id="item-1", recipe_id="r1", item_type="asset", item_id="a1")
        assert item.id == "item-1"
        assert item.recipe_id == "r1"
        assert item.item_type == "asset"
        assert item.item_id == "a1"
        assert item.position == 0
        assert item.metadata_ == {}

    def test_recipe_create(self):
        recipe = Recipe(id="r1", user_id="u1", name="我的配方")
        assert recipe.id == "r1"
        assert recipe.user_id == "u1"
        assert recipe.name == "我的配方"
        assert recipe.description == ""
        assert recipe.items == []
        assert recipe.is_active is True
        assert recipe.generation_params == {}
        assert recipe.created_at is not None

    def test_recipe_with_items(self):
        items = [
            RecipeItem(id="i1", recipe_id="r1", item_type="asset", item_id="a1", position=0),
            RecipeItem(id="i2", recipe_id="r1", item_type="title", item_id="t1", position=1),
        ]
        recipe = Recipe(id="r1", user_id="u1", name="test", items=items)
        assert len(recipe.items) == 2
        assert recipe.items[0].item_type == "asset"
        assert recipe.items[1].position == 1

    def test_recipe_items_independent_list(self):
        """不同 recipe 的 items 是独立列表"""
        r1 = Recipe(id="r1", user_id="u1", name="r1")
        r2 = Recipe(id="r2", user_id="u1", name="r2")
        assert r1.items is not r2.items


# ── Template ─────────────────────────────────────────────────────────────────


class TestTemplate:
    """Template 相关实体测试"""

    def test_template_segment(self):
        seg = TemplateSegment(
            id="seg-1",
            template_id="t1",
            segment_order=0,
            duration_min=3.0,
            duration_max=5.0,
        )
        assert seg.id == "seg-1"
        assert seg.template_id == "t1"
        assert seg.segment_order == 0
        assert seg.duration_min == 3.0
        assert seg.duration_max == 5.0
        assert seg.material_type is None
        assert seg.created_at is not None

    def test_template_create(self):
        tpl = Template(
            id="t1",
            user_id="u1",
            name="口播模板",
            mode="voice_over",
        )
        assert tpl.id == "t1"
        assert tpl.user_id == "u1"
        assert tpl.name == "口播模板"
        assert tpl.mode == "voice_over"
        assert tpl.category == ""
        assert tpl.tags == []
        assert tpl.segments == []
        assert tpl.is_active is True
        assert tpl.estimated_duration == 0.0

    def test_template_with_segments(self):
        segs = [
            TemplateSegment(id="s1", template_id="t1", segment_order=0, duration_min=2, duration_max=4),
            TemplateSegment(id="s2", template_id="t1", segment_order=1, duration_min=5, duration_max=8),
        ]
        tpl = Template(id="t1", user_id="u1", name="test", mode="one_take", segments=segs)
        assert len(tpl.segments) == 2
        assert tpl.segments[0].segment_order == 0

    def test_template_category(self):
        cat = TemplateCategory(id="cat-1", user_id="u1", name="热门")
        assert cat.id == "cat-1"
        assert cat.user_id == "u1"
        assert cat.name == "热门"
        assert cat.created_at is not None


# ── TemplateVersion ──────────────────────────────────────────────────────────


class TestEditTemplateVersion:
    """EditTemplateVersion 测试"""

    def test_create_basic(self):
        v = EditTemplateVersion.create(template_id="t1", version=1)
        assert v.id
        assert len(v.id) == 32
        assert v.template_id == "t1"
        assert v.version == 1
        assert v.name == ""
        assert v.editing_mode == "one_take"
        assert v.config == {}
        assert v.clip_configs == []
        assert v.change_note == ""
        assert v.published_by == ""

    def test_create_with_details(self):
        config = {"title": {"size": 36}}
        clips = [{"clip_type": "intro"}, {"clip_type": "outro"}]
        v = EditTemplateVersion.create(
            template_id="t1",
            version=2,
            name="V2 优化版",
            editing_mode="voice_over",
            config=config,
            clip_configs=clips,
            change_note="优化了节奏",
            published_by="user-1",
        )
        assert v.version == 2
        assert v.name == "V2 优化版"
        assert v.editing_mode == "voice_over"
        assert v.config == config
        assert v.clip_configs == clips
        assert v.change_note == "优化了节奏"
        assert v.published_by == "user-1"

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

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


# ── TemplateClipConfig ───────────────────────────────────────────────────────


class TestTemplateClipConfig:
    """TemplateClipConfig + 枚举测试"""

    def test_clip_type_values(self):
        assert ClipType.INTRO == "intro"
        assert ClipType.MAIN == "main"
        assert ClipType.TRANSITION == "transition"
        assert ClipType.OUTRO == "outro"
        assert ClipType.TITLE == "title"
        assert ClipType.SUBTITLE == "subtitle"

    def test_clip_type_count(self):
        assert len(ClipType) == 6

    def test_transition_effect_values(self):
        assert TransitionEffect.CUT == "cut"
        assert TransitionEffect.FADE == "fade"
        assert TransitionEffect.SLIDE_LEFT == "slide_left"
        assert TransitionEffect.SLIDE_RIGHT == "slide_right"
        assert TransitionEffect.DISSOLVE == "dissolve"
        assert TransitionEffect.WIPE == "wipe"

    def test_transition_effect_count(self):
        assert len(TransitionEffect) == 6

    def test_template_clip_config(self):
        clip = TemplateClipConfig(
            id="clip-1",
            template_id="t1",
            clip_type=ClipType.MAIN,
            order=1,
            min_duration=2.0,
            max_duration=5.0,
        )
        assert clip.id == "clip-1"
        assert clip.template_id == "t1"
        assert clip.clip_type == ClipType.MAIN
        assert clip.order == 1
        assert clip.min_duration == 2.0
        assert clip.max_duration == 5.0

    def test_str_enum(self):
        assert isinstance(ClipType.INTRO, str)
        assert isinstance(TransitionEffect.FADE, str)


# ── PresetBGM ────────────────────────────────────────────────────────────────


class TestPresetBGM:
    """PresetBGM + 查询函数测试"""

    def test_preset_bgm_create(self):
        bgm = PresetBGM(
            id="bgm_test_001",
            name="测试音乐",
            style="upbeat",
            duration=120.0,
        )
        assert bgm.id == "bgm_test_001"
        assert bgm.name == "测试音乐"
        assert bgm.style == "upbeat"
        assert bgm.duration == 120.0
        assert bgm.artist == ""
        assert bgm.tags == []
        assert bgm.audio_url == ""

    def test_preset_bgm_frozen(self):
        bgm = PresetBGM(id="t1", name="t", style="x", duration=10.0)
        with pytest.raises(FrozenInstanceError):
            bgm.name = "改名"

    def test_library_not_empty(self):
        assert len(PRESET_BGM_LIBRARY) > 0

    def test_all_presets_have_required_fields(self):
        for bgm in PRESET_BGM_LIBRARY:
            assert bgm.id
            assert bgm.name
            assert bgm.style
            assert bgm.duration > 0

    def test_get_preset_bgm_existing(self):
        first = PRESET_BGM_LIBRARY[0]
        result = get_preset_bgm(first.id)
        assert result is not None
        assert result.id == first.id

    def test_get_preset_bgm_nonexistent(self):
        assert get_preset_bgm("nonexistent_bgm") is None

    def test_list_preset_bgm_by_style(self):
        upbeat = list_preset_bgm_by_style("upbeat")
        assert len(upbeat) > 0
        assert all(b.style == "upbeat" for b in upbeat)

    def test_list_preset_bgm_by_style_empty(self):
        result = list_preset_bgm_by_style("nonexistent_style")
        assert result == []

    def test_search_preset_bgm_by_name(self):
        result = search_preset_bgm("阳光")
        assert len(result) >= 1
        assert any("阳光" in b.name for b in result)

    def test_search_preset_bgm_by_tag(self):
        result = search_preset_bgm("轻快")
        assert len(result) >= 1
        assert any(any("轻快" in t for t in b.tags) for b in result)

    def test_search_preset_bgm_empty_result(self):
        result = search_preset_bgm("xyz_not_exist_keyword")
        assert result == []


# ── PresetVoices ─────────────────────────────────────────────────────────────


class TestPresetVoices:
    """PresetVoice + 查询函数测试"""

    def test_preset_voice_create(self):
        v = PresetVoice(
            voice_id="test_voice",
            name="测试音色",
            description="测试用",
            gender="female",
        )
        assert v.voice_id == "test_voice"
        assert v.name == "测试音色"
        assert v.gender == "female"
        assert v.language == "zh-CN"
        assert v.preview_url == ""
        assert v.tags is None

    def test_preset_voice_to_dict(self):
        v = PresetVoice(
            voice_id="v1",
            name="音色1",
            description="desc",
            gender="male",
            language="zh-CN",
            tags=["温柔", "男声"],
        )
        d = v.to_dict()
        assert d["voice_id"] == "v1"
        assert d["name"] == "音色1"
        assert d["description"] == "desc"
        assert d["gender"] == "male"
        assert d["language"] == "zh-CN"
        assert d["tags"] == ["温柔", "男声"]

    def test_preset_voice_to_dict_no_tags(self):
        v = PresetVoice(voice_id="v1", name="t", description="d", gender="female")
        d = v.to_dict()
        assert d["tags"] == []

    def test_preset_voice_frozen(self):
        v = PresetVoice(voice_id="v1", name="t", description="d", gender="female")
        with pytest.raises(FrozenInstanceError):
            v.name = "改名"

    def test_preset_voices_list_not_empty(self):
        assert len(PRESET_VOICES) > 0

    def test_get_preset_voices(self):
        voices = get_preset_voices()
        assert len(voices) == len(PRESET_VOICES)
        assert all(isinstance(v, PresetVoice) for v in voices)

    def test_get_preset_voice_by_id_existing(self):
        first = PRESET_VOICES[0]
        result = get_preset_voice_by_id(first.voice_id)
        assert result is not None
        assert result.voice_id == first.voice_id

    def test_get_preset_voice_by_id_nonexistent(self):
        assert get_preset_voice_by_id("nonexistent_voice") is None

    def test_is_preset_voice_true(self):
        first = PRESET_VOICES[0]
        assert is_preset_voice(first.voice_id) is True

    def test_is_preset_voice_false(self):
        assert is_preset_voice("fake_voice_id") is False


# ── TitleLibrary ─────────────────────────────────────────────────────────────


class TestTitleLibraryItem:
    """TitleLibraryItem 测试"""

    def test_create_basic(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.tags == []
        assert item.usage_count == 0
        assert item.is_active is True
        assert item.created_at is not None

    def test_create_with_details(self):
        item = TitleLibraryItem(
            id="t1",
            user_id="u1",
            name="爆款标题",
            text="三个方法教你...",
            category="爆款",
            description="高点击率",
            tags=["热门", "干货"],
            usage_count=100,
        )
        assert item.category == "爆款"
        assert item.description == "高点击率"
        assert item.tags == ["热门", "干货"]
        assert item.usage_count == 100


# ── VoiceLibrary ─────────────────────────────────────────────────────────────


class TestVoiceLibraryItem:
    """VoiceLibraryItem 测试"""

    def test_create_basic(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.project_id is None
        assert item.tags == []
        assert item.created_at is not None

    def test_create_with_details(self):
        item = VoiceLibraryItem(
            id="v1",
            user_id="u1",
            name="产品介绍",
            text="大家好，今天给大家介绍...",
            voice_provider="cosyvoice",
            voice_id="voice_001",
            voice_name="温柔女声",
            audio_url="http://cdn/audio.mp3",
            duration=30.5,
            file_size=102400,
            status="processing",
            project_id="proj-1",
            tags=["产品", "介绍"],
        )
        assert item.voice_provider == "cosyvoice"
        assert item.voice_id == "voice_001"
        assert item.audio_url == "http://cdn/audio.mp3"
        assert item.duration == 30.5
        assert item.file_size == 102400
        assert item.status == "processing"
        assert item.project_id == "proj-1"
        assert item.tags == ["产品", "介绍"]
