"""Unit tests for Phase 8 任务 2.02: TemplateClipConfig + EditPlanClip 数据模型."""

import pytest

from packages.domain.edit_plan_clip import (
    EditPlanClip,
    EditPlanClipStatus,
)
from packages.domain.template_clip_config import (
    ClipType,
    TemplateClipConfig,
    TransitionEffect,
)

# ── TemplateClipConfig 领域实体测试 ─────────────────────────────────────────


class TestTemplateClipConfig:
    """TemplateClipConfig 领域实体测试"""

    def test_create_basic(self):
        """基本创建"""
        config = TemplateClipConfig.create(
            template_id="tpl_001",
            clip_type=ClipType.INTRO,
            order=0,
        )
        assert config.template_id == "tpl_001"
        assert config.clip_type == ClipType.INTRO
        assert config.order == 0
        assert config.min_duration == 0.0
        assert config.max_duration == 0.0
        assert config.text_template == ""
        assert config.material_requirements == {}
        assert config.transition_effect == TransitionEffect.CUT
        assert config.config == {}
        assert config.id  # 自动生成 ID

    def test_create_with_all_fields(self):
        """完整字段创建"""
        config = TemplateClipConfig.create(
            template_id="tpl_001",
            clip_type=ClipType.MAIN,
            order=1,
            min_duration=3.0,
            max_duration=10.0,
            text_template="欢迎使用{product_name}",
            material_requirements={"type": "video", "min_resolution": "1080p"},
            transition_effect=TransitionEffect.FADE,
            config={"speed": 1.0},
        )
        assert config.min_duration == 3.0
        assert config.max_duration == 10.0
        assert config.text_template == "欢迎使用{product_name}"
        assert config.material_requirements == {"type": "video", "min_resolution": "1080p"}
        assert config.transition_effect == TransitionEffect.FADE
        assert config.config == {"speed": 1.0}

    def test_create_with_string_enum_values(self):
        """字符串枚举值创建"""
        config = TemplateClipConfig.create(
            template_id="tpl_001",
            clip_type="outro",
            order=2,
            transition_effect="slide_left",
        )
        assert config.clip_type == ClipType.OUTRO
        assert config.transition_effect == TransitionEffect.SLIDE_LEFT

    def test_create_empty_template_id_raises(self):
        """空 template_id 报错"""
        with pytest.raises(ValueError, match="template_id 不能为空"):
            TemplateClipConfig.create(template_id="", clip_type=ClipType.INTRO, order=0)

    def test_create_whitespace_template_id_raises(self):
        """空白 template_id 报错"""
        with pytest.raises(ValueError, match="template_id 不能为空"):
            TemplateClipConfig.create(template_id="   ", clip_type=ClipType.INTRO, order=0)

    def test_create_negative_min_duration_raises(self):
        """负数 min_duration 报错"""
        with pytest.raises(ValueError, match="min_duration 不能为负数"):
            TemplateClipConfig.create(template_id="tpl_001", clip_type=ClipType.INTRO, order=0, min_duration=-1.0)

    def test_create_negative_max_duration_raises(self):
        """负数 max_duration 报错"""
        with pytest.raises(ValueError, match="max_duration 不能为负数"):
            TemplateClipConfig.create(template_id="tpl_001", clip_type=ClipType.INTRO, order=0, max_duration=-1.0)

    def test_create_min_greater_than_max_raises(self):
        """min_duration > max_duration 报错"""
        with pytest.raises(ValueError, match="min_duration 不能大于 max_duration"):
            TemplateClipConfig.create(
                template_id="tpl_001",
                clip_type=ClipType.INTRO,
                order=0,
                min_duration=10.0,
                max_duration=5.0,
            )

    def test_has_duration_range(self):
        """has_duration_range 属性"""
        config_no_range = TemplateClipConfig.create(template_id="tpl_001", clip_type=ClipType.INTRO, order=0)
        assert config_no_range.has_duration_range is False

        config_with_range = TemplateClipConfig.create(
            template_id="tpl_001", clip_type=ClipType.INTRO, order=0, min_duration=3.0, max_duration=10.0
        )
        assert config_with_range.has_duration_range is True

    def test_default_duration(self):
        """default_duration 属性"""
        # 无时长范围
        config_no_range = TemplateClipConfig.create(template_id="tpl_001", clip_type=ClipType.INTRO, order=0)
        assert config_no_range.default_duration == 0.0

        # 只有 min
        config_min_only = TemplateClipConfig.create(
            template_id="tpl_001", clip_type=ClipType.INTRO, order=0, min_duration=5.0
        )
        assert config_min_only.default_duration == 5.0

        # 只有 max
        config_max_only = TemplateClipConfig.create(
            template_id="tpl_001", clip_type=ClipType.INTRO, order=0, max_duration=10.0
        )
        assert config_max_only.default_duration == 10.0

        # 两者都有 → 平均值
        config_both = TemplateClipConfig.create(
            template_id="tpl_001", clip_type=ClipType.INTRO, order=0, min_duration=4.0, max_duration=10.0
        )
        assert config_both.default_duration == 7.0

    def test_clip_type_enum_values(self):
        """ClipType 枚举值"""
        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_transition_effect_enum_values(self):
        """TransitionEffect 枚举值"""
        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_timestamps_auto_set(self):
        """创建时自动设置时间戳"""
        config = TemplateClipConfig.create(template_id="tpl_001", clip_type=ClipType.INTRO, order=0)
        assert config.created_at is not None
        assert config.updated_at is not None


# ── EditPlanClip 领域实体测试 ───────────────────────────────────────────────


class TestEditPlanClip:
    """EditPlanClip 领域实体测试"""

    def test_create_basic(self):
        """基本创建"""
        clip = EditPlanClip.create(plan_id="plan_001", clip_type="main", order=0)
        assert clip.plan_id == "plan_001"
        assert clip.clip_type == "main"
        assert clip.order == 0
        assert clip.status == EditPlanClipStatus.PENDING
        assert clip.template_clip_config_id == ""
        assert clip.asset_id == ""
        assert clip.text_content == ""
        assert clip.start_time == 0.0
        assert clip.duration == 0.0
        assert clip.transition_effect == "cut"
        assert clip.config == {}
        assert clip.id

    def test_create_with_all_fields(self):
        """完整字段创建"""
        clip = EditPlanClip.create(
            plan_id="plan_001",
            clip_type="intro",
            order=0,
            template_clip_config_id="cfg_001",
            asset_id="asset_001",
            text_content="欢迎",
            start_time=0.0,
            duration=5.0,
            transition_effect="fade",
            config={"zoom": 1.2},
        )
        assert clip.template_clip_config_id == "cfg_001"
        assert clip.asset_id == "asset_001"
        assert clip.text_content == "欢迎"
        assert clip.duration == 5.0
        assert clip.transition_effect == "fade"
        assert clip.config == {"zoom": 1.2}

    def test_create_empty_plan_id_raises(self):
        """空 plan_id 报错"""
        with pytest.raises(ValueError, match="plan_id 不能为空"):
            EditPlanClip.create(plan_id="", clip_type="main", order=0)

    def test_create_empty_clip_type_raises(self):
        """空 clip_type 报错"""
        with pytest.raises(ValueError, match="clip_type 不能为空"):
            EditPlanClip.create(plan_id="plan_001", clip_type="", order=0)

    def test_create_negative_start_time_raises(self):
        """负数 start_time 报错"""
        with pytest.raises(ValueError, match="start_time 不能为负数"):
            EditPlanClip.create(plan_id="plan_001", clip_type="main", order=0, start_time=-1.0)

    def test_create_negative_duration_raises(self):
        """负数 duration 报错"""
        with pytest.raises(ValueError, match="duration 不能为负数"):
            EditPlanClip.create(plan_id="plan_001", clip_type="main", order=0, duration=-1.0)

    def test_assign_asset(self):
        """分配素材"""
        clip = EditPlanClip.create(plan_id="plan_001", clip_type="main", order=0)
        assert clip.has_asset is False
        clip.assign_asset("asset_001")
        assert clip.asset_id == "asset_001"
        assert clip.has_asset is True

    def test_assign_asset_empty_raises(self):
        """分配空素材报错"""
        clip = EditPlanClip.create(plan_id="plan_001", clip_type="main", order=0)
        with pytest.raises(ValueError, match="asset_id 不能为空"):
            clip.assign_asset("")

    def test_status_transitions(self):
        """状态流转: pending → ready → rendered"""
        clip = EditPlanClip.create(plan_id="plan_001", clip_type="main", order=0)
        assert clip.status == EditPlanClipStatus.PENDING

        clip.mark_ready()
        assert clip.status == EditPlanClipStatus.READY

        clip.mark_rendered()
        assert clip.status == EditPlanClipStatus.RENDERED

    def test_status_transition_pending_to_failed(self):
        """状态流转: pending → ready → failed"""
        clip = EditPlanClip.create(plan_id="plan_001", clip_type="main", order=0)
        clip.mark_ready()
        clip.mark_failed()
        assert clip.status == EditPlanClipStatus.FAILED

    def test_mark_ready_from_non_pending_raises(self):
        """非 pending 状态标记 ready 报错"""
        clip = EditPlanClip.create(plan_id="plan_001", clip_type="main", order=0)
        clip.mark_ready()
        with pytest.raises(ValueError, match="只有 pending 状态"):
            clip.mark_ready()

    def test_mark_rendered_from_non_ready_raises(self):
        """非 ready 状态标记 rendered 报错"""
        clip = EditPlanClip.create(plan_id="plan_001", clip_type="main", order=0)
        with pytest.raises(ValueError, match="只有 ready 状态"):
            clip.mark_rendered()

    def test_mark_failed_from_non_ready_raises(self):
        """非 ready 状态标记 failed 报错"""
        clip = EditPlanClip.create(plan_id="plan_001", clip_type="main", order=0)
        with pytest.raises(ValueError, match="只有 ready 状态"):
            clip.mark_failed()

    def test_end_time_property(self):
        """end_time 属性"""
        clip = EditPlanClip.create(plan_id="plan_001", clip_type="main", order=0, start_time=5.0, duration=10.0)
        assert clip.end_time == 15.0

    def test_has_asset_property(self):
        """has_asset 属性"""
        clip_no_asset = EditPlanClip.create(plan_id="plan_001", clip_type="main", order=0)
        assert clip_no_asset.has_asset is False

        clip_with_asset = EditPlanClip.create(plan_id="plan_001", clip_type="main", order=0, asset_id="asset_001")
        assert clip_with_asset.has_asset is True

    def test_edit_plan_clip_status_enum(self):
        """EditPlanClipStatus 枚举值"""
        assert EditPlanClipStatus.PENDING == "pending"
        assert EditPlanClipStatus.READY == "ready"
        assert EditPlanClipStatus.RENDERED == "rendered"
        assert EditPlanClipStatus.FAILED == "failed"


# ── Repository 集成测试（使用 SQLite 内存数据库）────────────────────────────

from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker

from packages.adapters.sqlalchemy_impl.edit_plan_clip_repository import (
    SQLAlchemyEditPlanClipRepository,
)
from packages.adapters.sqlalchemy_impl.models import Base
from packages.adapters.sqlalchemy_impl.template_clip_config_repository import (
    SQLAlchemyTemplateClipConfigRepository,
)


@pytest.fixture
def db_session():
    """创建内存数据库 session"""
    engine = create_engine("sqlite:///:memory:")
    Base.metadata.create_all(engine)
    Session = sessionmaker(bind=engine)
    session = Session()
    yield session
    session.close()


class TestTemplateClipConfigRepository:
    """TemplateClipConfig 仓储测试"""

    def test_create_and_get(self, db_session):
        """创建并获取"""
        repo = SQLAlchemyTemplateClipConfigRepository(db_session)
        config = TemplateClipConfig.create(template_id="tpl_001", clip_type=ClipType.INTRO, order=0, min_duration=2.0)
        created = repo.create(config)
        assert created.id == config.id

        fetched = repo.get(config.id)
        assert fetched is not None
        assert fetched.template_id == "tpl_001"
        assert fetched.clip_type == ClipType.INTRO
        assert fetched.min_duration == 2.0

    def test_list_by_template(self, db_session):
        """按模板列出"""
        repo = SQLAlchemyTemplateClipConfigRepository(db_session)
        for i in range(3):
            repo.create(TemplateClipConfig.create(template_id="tpl_001", clip_type=ClipType.MAIN, order=i))
        repo.create(TemplateClipConfig.create(template_id="tpl_002", clip_type=ClipType.INTRO, order=0))

        results = repo.list_by_template("tpl_001")
        assert len(results) == 3
        assert all(r.template_id == "tpl_001" for r in results)
        # 按 order 排序
        assert results[0].order == 0
        assert results[1].order == 1
        assert results[2].order == 2

    def test_list_by_template_with_clip_type_filter(self, db_session):
        """按模板+类型过滤"""
        repo = SQLAlchemyTemplateClipConfigRepository(db_session)
        repo.create(TemplateClipConfig.create(template_id="tpl_001", clip_type=ClipType.INTRO, order=0))
        repo.create(TemplateClipConfig.create(template_id="tpl_001", clip_type=ClipType.MAIN, order=1))
        repo.create(TemplateClipConfig.create(template_id="tpl_001", clip_type=ClipType.MAIN, order=2))

        results = repo.list_by_template("tpl_001", clip_type=ClipType.MAIN)
        assert len(results) == 2
        assert all(r.clip_type == ClipType.MAIN for r in results)

    def test_update(self, db_session):
        """更新"""
        repo = SQLAlchemyTemplateClipConfigRepository(db_session)
        config = TemplateClipConfig.create(template_id="tpl_001", clip_type=ClipType.INTRO, order=0)
        repo.create(config)

        config.min_duration = 5.0
        config.max_duration = 15.0
        config.text_template = "Hello {name}"
        updated = repo.update(config)
        assert updated.min_duration == 5.0
        assert updated.max_duration == 15.0
        assert updated.text_template == "Hello {name}"

    def test_delete(self, db_session):
        """删除"""
        repo = SQLAlchemyTemplateClipConfigRepository(db_session)
        config = TemplateClipConfig.create(template_id="tpl_001", clip_type=ClipType.INTRO, order=0)
        repo.create(config)
        assert repo.delete(config.id) is True
        assert repo.get(config.id) is None
        assert repo.delete("nonexistent") is False

    def test_delete_by_template(self, db_session):
        """按模板批量删除"""
        repo = SQLAlchemyTemplateClipConfigRepository(db_session)
        for i in range(3):
            repo.create(TemplateClipConfig.create(template_id="tpl_001", clip_type=ClipType.MAIN, order=i))
        deleted = repo.delete_by_template("tpl_001")
        assert deleted == 3
        assert repo.count(template_id="tpl_001") == 0

    def test_count(self, db_session):
        """统计"""
        repo = SQLAlchemyTemplateClipConfigRepository(db_session)
        assert repo.count() == 0
        repo.create(TemplateClipConfig.create(template_id="tpl_001", clip_type=ClipType.INTRO, order=0))
        repo.create(TemplateClipConfig.create(template_id="tpl_001", clip_type=ClipType.MAIN, order=1))
        assert repo.count() == 2
        assert repo.count(template_id="tpl_001") == 2
        assert repo.count(template_id="tpl_999") == 0


class TestEditPlanClipRepository:
    """EditPlanClip 仓储测试"""

    def test_create_and_get(self, db_session):
        """创建并获取"""
        repo = SQLAlchemyEditPlanClipRepository(db_session)
        clip = EditPlanClip.create(plan_id="plan_001", clip_type="main", order=0, duration=5.0)
        created = repo.create(clip)
        assert created.id == clip.id

        fetched = repo.get(clip.id)
        assert fetched is not None
        assert fetched.plan_id == "plan_001"
        assert fetched.clip_type == "main"
        assert fetched.duration == 5.0
        assert fetched.status == EditPlanClipStatus.PENDING

    def test_list_by_plan(self, db_session):
        """按计划列出"""
        repo = SQLAlchemyEditPlanClipRepository(db_session)
        for i in range(3):
            repo.create(EditPlanClip.create(plan_id="plan_001", clip_type="main", order=i))
        repo.create(EditPlanClip.create(plan_id="plan_002", clip_type="intro", order=0))

        results = repo.list_by_plan("plan_001")
        assert len(results) == 3
        assert all(r.plan_id == "plan_001" for r in results)
        assert results[0].order == 0
        assert results[1].order == 1
        assert results[2].order == 2

    def test_list_by_plan_with_status_filter(self, db_session):
        """按计划+状态过滤"""
        repo = SQLAlchemyEditPlanClipRepository(db_session)
        clip1 = EditPlanClip.create(plan_id="plan_001", clip_type="main", order=0)
        clip2 = EditPlanClip.create(plan_id="plan_001", clip_type="main", order=1)
        repo.create(clip1)
        repo.create(clip2)

        clip1.mark_ready()
        repo.update(clip1)

        results = repo.list_by_plan("plan_001", status=EditPlanClipStatus.READY)
        assert len(results) == 1
        assert results[0].status == EditPlanClipStatus.READY

    def test_update(self, db_session):
        """更新"""
        repo = SQLAlchemyEditPlanClipRepository(db_session)
        clip = EditPlanClip.create(plan_id="plan_001", clip_type="main", order=0)
        repo.create(clip)

        clip.assign_asset("asset_001")
        clip.duration = 10.0
        updated = repo.update(clip)
        assert updated.asset_id == "asset_001"
        assert updated.duration == 10.0

    def test_delete(self, db_session):
        """删除"""
        repo = SQLAlchemyEditPlanClipRepository(db_session)
        clip = EditPlanClip.create(plan_id="plan_001", clip_type="main", order=0)
        repo.create(clip)
        assert repo.delete(clip.id) is True
        assert repo.get(clip.id) is None
        assert repo.delete("nonexistent") is False

    def test_delete_by_plan(self, db_session):
        """按计划批量删除"""
        repo = SQLAlchemyEditPlanClipRepository(db_session)
        for i in range(3):
            repo.create(EditPlanClip.create(plan_id="plan_001", clip_type="main", order=i))
        deleted = repo.delete_by_plan("plan_001")
        assert deleted == 3
        assert repo.count(plan_id="plan_001") == 0

    def test_count(self, db_session):
        """统计"""
        repo = SQLAlchemyEditPlanClipRepository(db_session)
        assert repo.count() == 0
        repo.create(EditPlanClip.create(plan_id="plan_001", clip_type="main", order=0))
        repo.create(EditPlanClip.create(plan_id="plan_001", clip_type="main", order=1))
        assert repo.count() == 2
        assert repo.count(plan_id="plan_001") == 2
        assert repo.count(plan_id="plan_999") == 0

    def test_count_with_status(self, db_session):
        """按状态统计"""
        repo = SQLAlchemyEditPlanClipRepository(db_session)
        clip1 = EditPlanClip.create(plan_id="plan_001", clip_type="main", order=0)
        clip2 = EditPlanClip.create(plan_id="plan_001", clip_type="main", order=1)
        repo.create(clip1)
        repo.create(clip2)

        clip1.mark_ready()
        repo.update(clip1)

        assert repo.count(status=EditPlanClipStatus.PENDING) == 1
        assert repo.count(status=EditPlanClipStatus.READY) == 1
