"""小领域模块组合单测 — template_clip_config / tag / exceptions / editing_mode."""

from __future__ import annotations

import pytest

from packages.domain.editing_mode import EditingMode
from packages.domain.exceptions import (
    DomainError,
    NotFoundError,
    QuotaExceededError,
    ValidationError,
)
from packages.domain.tag import Tag
from packages.domain.template_clip_config import (
    ClipType,
    TemplateClipConfig,
    TransitionEffect,
)

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


class TestEditingMode:
    def test_one_take(self):
        assert EditingMode.ONE_TAKE == "one_take"

    def test_pip(self):
        assert EditingMode.PIP == "pip"

    def test_voice_over(self):
        assert EditingMode.VOICE_OVER == "voice_over"

    def test_voice_pip(self):
        assert EditingMode.VOICE_PIP == "voice_pip"

    def test_from_string(self):
        assert EditingMode("one_take") == EditingMode.ONE_TAKE
        assert EditingMode("pip") == EditingMode.PIP

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

    def test_is_str(self):
        # StrEnum 实例本身就是 str
        assert isinstance(EditingMode.ONE_TAKE, str)
        assert EditingMode.ONE_TAKE + "_suffix" == "one_take_suffix"


# ── 异常类 ─────────────────────────────────────────────────────────────────


class TestExceptions:
    def test_domain_error_is_exception(self):
        err = DomainError("test")
        assert isinstance(err, Exception)
        assert str(err) == "test"

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

    def test_validation_error_inherits_domain(self):
        err = ValidationError("bad input")
        assert isinstance(err, DomainError)
        assert str(err) == "bad input"

    def test_quota_exceeded_error_attributes(self):
        err = QuotaExceededError(dimension="storage_gb", limit=100, used=150)
        assert err.dimension == "storage_gb"
        assert err.limit == 100
        assert err.used == 150
        assert "storage_gb" in str(err)
        assert "150" in str(err)
        assert "100" in str(err)

    def test_quota_exceeded_is_domain_error(self):
        err = QuotaExceededError("x", 10, 20)
        assert isinstance(err, DomainError)

    def test_catch_domain_error_catches_all(self):
        """所有领域异常都能被 DomainError catch."""
        for cls in [NotFoundError, ValidationError, QuotaExceededError]:
            try:
                if cls == QuotaExceededError:
                    raise cls("dim", 10, 20)
                raise cls("msg")
            except DomainError:
                pass
            else:
                pytest.fail(f"{cls.__name__} 未被 DomainError 捕获")


# ── Tag ────────────────────────────────────────────────────────────────────


class TestTag:
    def test_create_basic(self):
        tag = Tag.create(user_id="user1", name="旅行")
        assert tag.id
        assert tag.user_id == "user1"
        assert tag.name == "旅行"

    def test_create_name_stripped(self):
        tag = Tag.create(user_id="u1", name="  美食  ")
        assert tag.name == "美食"

    def test_create_empty_name_raises(self):
        with pytest.raises(ValueError, match="标签名称不能为空"):
            Tag.create(user_id="u1", name="")

    def test_create_whitespace_name_raises(self):
        with pytest.raises(ValueError, match="标签名称不能为空"):
            Tag.create(user_id="u1", name="   ")

    def test_create_has_created_at(self):
        tag = Tag.create(user_id="u1", name="t1")
        assert tag.created_at is not None

    def test_unique_ids(self):
        t1 = Tag.create(user_id="u1", name="t1")
        t2 = Tag.create(user_id="u1", name="t2")
        assert t1.id != t2.id


# ── ClipType 枚举 ─────────────────────────────────────────────────────────


class TestClipType:
    def test_all_types(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_from_string(self):
        assert ClipType("main") == ClipType.MAIN
        assert ClipType("intro") == ClipType.INTRO

    def test_invalid_raises(self):
        with pytest.raises(ValueError):
            ClipType("invalid")


# ── TransitionEffect 枚举 ─────────────────────────────────────────────────


class TestTransitionEffect:
    def test_all_effects(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_from_string(self):
        assert TransitionEffect("fade") == TransitionEffect.FADE
        assert TransitionEffect("cut") == TransitionEffect.CUT

    def test_invalid_raises(self):
        with pytest.raises(ValueError):
            TransitionEffect("invalid_effect")


# ── TemplateClipConfig.create ──────────────────────────────────────────────


class TestTemplateClipConfigCreate:
    def test_basic_creation(self):
        clip = TemplateClipConfig.create(
            template_id="tmpl_1",
            clip_type=ClipType.MAIN,
            order=1,
        )
        assert clip.id
        assert clip.template_id == "tmpl_1"
        assert clip.clip_type == ClipType.MAIN
        assert clip.order == 1
        assert clip.min_duration == 0.0
        assert clip.max_duration == 0.0
        assert clip.text_template == ""
        assert clip.material_requirements == {}
        assert clip.transition_effect == TransitionEffect.CUT
        assert clip.config == {}

    def test_clip_type_string(self):
        clip = TemplateClipConfig.create(
            template_id="t1",
            clip_type="intro",
            order=0,
        )
        assert clip.clip_type == ClipType.INTRO

    def test_template_id_stripped(self):
        clip = TemplateClipConfig.create(
            template_id="  tmpl_1  ",
            clip_type=ClipType.MAIN,
            order=1,
        )
        assert clip.template_id == "tmpl_1"

    def test_empty_template_id_raises(self):
        with pytest.raises(ValueError, match="template_id 不能为空"):
            TemplateClipConfig.create(template_id="", clip_type=ClipType.MAIN, order=1)

    def test_whitespace_template_id_raises(self):
        with pytest.raises(ValueError, match="template_id 不能为空"):
            TemplateClipConfig.create(template_id="  ", clip_type=ClipType.MAIN, order=1)

    def test_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_negative_max_duration_raises(self):
        with pytest.raises(ValueError, match="max_duration 不能为负数"):
            TemplateClipConfig.create(
                template_id="t1",
                clip_type=ClipType.MAIN,
                order=1,
                max_duration=-1.0,
            )

    def test_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_zero_min_and_max_ok(self):
        clip = TemplateClipConfig.create(
            template_id="t1",
            clip_type=ClipType.MAIN,
            order=1,
            min_duration=0,
            max_duration=0,
        )
        assert clip.min_duration == 0
        assert clip.max_duration == 0

    def test_min_zero_max_positive_ok(self):
        clip = TemplateClipConfig.create(
            template_id="t1",
            clip_type=ClipType.MAIN,
            order=1,
            min_duration=0,
            max_duration=10.0,
        )
        assert clip.max_duration == 10.0

    def test_min_equals_max_ok(self):
        clip = TemplateClipConfig.create(
            template_id="t1",
            clip_type=ClipType.MAIN,
            order=1,
            min_duration=5.0,
            max_duration=5.0,
        )
        assert clip.min_duration == 5.0
        assert clip.max_duration == 5.0

    def test_with_text_template(self):
        clip = TemplateClipConfig.create(
            template_id="t1",
            clip_type=ClipType.MAIN,
            order=1,
            text_template="  欢迎收看 {channel}  ",
        )
        # text_template 会 strip
        assert clip.text_template == "欢迎收看 {channel}"

    def test_with_material_requirements(self):
        reqs = {"material_type": "video", "min_duration": 3}
        clip = TemplateClipConfig.create(
            template_id="t1",
            clip_type=ClipType.MAIN,
            order=1,
            material_requirements=reqs,
        )
        assert clip.material_requirements == reqs

    def test_material_requirements_none_defaults_empty(self):
        clip = TemplateClipConfig.create(
            template_id="t1",
            clip_type=ClipType.MAIN,
            order=1,
            material_requirements=None,
        )
        assert clip.material_requirements == {}

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

    def test_with_config(self):
        config = {"speed": 1.5, "filter": "vibrance"}
        clip = TemplateClipConfig.create(
            template_id="t1",
            clip_type=ClipType.MAIN,
            order=1,
            config=config,
        )
        assert clip.config == config

    def test_config_none_defaults_empty(self):
        clip = TemplateClipConfig.create(
            template_id="t1",
            clip_type=ClipType.MAIN,
            order=1,
            config=None,
        )
        assert clip.config == {}

    def test_created_at_and_updated_at(self):
        clip = TemplateClipConfig.create(
            template_id="t1",
            clip_type=ClipType.MAIN,
            order=1,
        )
        assert clip.created_at is not None
        assert clip.updated_at is not None

    def test_unique_ids(self):
        c1 = TemplateClipConfig.create(template_id="t1", clip_type=ClipType.MAIN, order=1)
        c2 = TemplateClipConfig.create(template_id="t1", clip_type=ClipType.MAIN, order=2)
        assert c1.id != c2.id


# ── TemplateClipConfig 属性 ────────────────────────────────────────────────


class TestTemplateClipConfigProperties:
    def test_has_duration_range_false_both_zero(self):
        clip = TemplateClipConfig.create(
            template_id="t1",
            clip_type=ClipType.MAIN,
            order=1,
        )
        assert clip.has_duration_range is False

    def test_has_duration_range_true_min_only(self):
        clip = TemplateClipConfig.create(
            template_id="t1",
            clip_type=ClipType.MAIN,
            order=1,
            min_duration=2.0,
        )
        assert clip.has_duration_range is True

    def test_has_duration_range_true_max_only(self):
        clip = TemplateClipConfig.create(
            template_id="t1",
            clip_type=ClipType.MAIN,
            order=1,
            max_duration=10.0,
        )
        assert clip.has_duration_range is True

    def test_has_duration_range_true_both_set(self):
        clip = TemplateClipConfig.create(
            template_id="t1",
            clip_type=ClipType.MAIN,
            order=1,
            min_duration=2.0,
            max_duration=10.0,
        )
        assert clip.has_duration_range is True

    def test_default_duration_zero(self):
        clip = TemplateClipConfig.create(
            template_id="t1",
            clip_type=ClipType.MAIN,
            order=1,
        )
        assert clip.default_duration == 0.0

    def test_default_duration_min_only(self):
        clip = TemplateClipConfig.create(
            template_id="t1",
            clip_type=ClipType.MAIN,
            order=1,
            min_duration=3.0,
        )
        assert clip.default_duration == 3.0

    def test_default_duration_max_only(self):
        clip = TemplateClipConfig.create(
            template_id="t1",
            clip_type=ClipType.MAIN,
            order=1,
            max_duration=10.0,
        )
        assert clip.default_duration == 10.0

    def test_default_duration_both_midpoint(self):
        clip = TemplateClipConfig.create(
            template_id="t1",
            clip_type=ClipType.MAIN,
            order=1,
            min_duration=2.0,
            max_duration=8.0,
        )
        assert clip.default_duration == 5.0

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