"""
EditTemplateService 单元测试

覆盖（30+ 测试用例）：
- 模板 CRUD：创建、查询、更新、软删除
- 名称去重校验
- 片段配置 CRUD
- 片段配置重排序
- 复合查询 get_template_with_configs
- 异常处理：不存在、参数校验
"""

from __future__ import annotations

import os
import sys
from datetime import datetime, timezone
from pathlib import Path
from typing import Any, List, Optional
from unittest.mock import MagicMock, patch

os.environ.setdefault("JWT_SECRET_KEY", "unit-test-secret-key-for-testing")
os.environ.setdefault("DATABASE_URL", "sqlite:///test.db")

import pytest

sys.path.insert(0, str(Path(__file__).resolve().parents[2] / "apps" / "api"))

from packages.domain.edit_template import EditTemplate, EditTemplateStatus
from packages.domain.template_clip_config import ClipType, TemplateClipConfig, TransitionEffect

# ---------------------------------------------------------------------------
# Stub Repositories
# ---------------------------------------------------------------------------


class StubEditTemplateRepository:
    """内存中的 EditTemplate 仓储 stub"""

    def __init__(self) -> None:
        self._templates: dict[str, EditTemplate] = {}
        self._counter = 0

    def _next_id(self) -> str:
        self._counter += 1
        return f"tpl-{self._counter:03d}"

    def list_all(
        self,
        *,
        template_type: Optional[str] = None,
        status: Optional[EditTemplateStatus] = None,
        skip: int = 0,
        limit: int = 50,
    ) -> List[EditTemplate]:
        items = list(self._templates.values())
        if template_type:
            items = [t for t in items if t.template_type == template_type]
        if status:
            items = [t for t in items if t.status == status]
        return items[skip : skip + limit]

    def list_active(
        self,
        *,
        template_type: Optional[str] = None,
        skip: int = 0,
        limit: int = 50,
    ) -> List[EditTemplate]:
        items = [t for t in self._templates.values() if t.status == EditTemplateStatus.ACTIVE]
        if template_type:
            items = [t for t in items if t.template_type == template_type]
        return items[skip : skip + limit]

    def get(self, template_id: str) -> Optional[EditTemplate]:
        return self._templates.get(template_id)

    def create(self, template: EditTemplate) -> EditTemplate:
        if not template.id:
            template = EditTemplate(
                id=self._next_id(),
                name=template.name,
                description=template.description,
                template_type=template.template_type,
                config=template.config,
                preview_url=template.preview_url,
                sort_weight=template.sort_weight,
                status=template.status,
                created_at=datetime.now(timezone.utc),
                updated_at=datetime.now(timezone.utc),
            )
        self._templates[template.id] = template
        return template

    def update(self, template: EditTemplate) -> EditTemplate:
        self._templates[template.id] = template
        return template

    def delete(self, template_id: str) -> bool:
        return self._templates.pop(template_id, None) is not None

    def count(
        self,
        *,
        template_type: Optional[str] = None,
        status: Optional[EditTemplateStatus] = None,
    ) -> int:
        items = list(self._templates.values())
        if template_type:
            items = [t for t in items if t.template_type == template_type]
        if status:
            items = [t for t in items if t.status == status]
        return len(items)


class StubTemplateClipConfigRepository:
    """内存中的 TemplateClipConfig 仓储 stub"""

    def __init__(self) -> None:
        self._configs: dict[str, TemplateClipConfig] = {}
        self._counter = 0

    def _next_id(self) -> str:
        self._counter += 1
        return f"cfg-{self._counter:03d}"

    def list_by_template(
        self,
        template_id: str,
        *,
        clip_type: Optional[ClipType] = None,
        skip: int = 0,
        limit: int = 100,
    ) -> List[TemplateClipConfig]:
        items = [c for c in self._configs.values() if c.template_id == template_id]
        if clip_type:
            items = [c for c in items if c.clip_type == clip_type]
        items.sort(key=lambda c: c.order)
        return items[skip : skip + limit]

    def get(self, config_id: str) -> Optional[TemplateClipConfig]:
        return self._configs.get(config_id)

    def create(self, config: TemplateClipConfig) -> TemplateClipConfig:
        if not config.id:
            config = TemplateClipConfig(
                id=self._next_id(),
                template_id=config.template_id,
                clip_type=config.clip_type,
                order=config.order,
                min_duration=config.min_duration,
                max_duration=config.max_duration,
                text_template=config.text_template,
                material_requirements=config.material_requirements,
                transition_effect=config.transition_effect,
                config=config.config,
                created_at=datetime.now(timezone.utc),
                updated_at=datetime.now(timezone.utc),
            )
        self._configs[config.id] = config
        return config

    def update(self, config: TemplateClipConfig) -> TemplateClipConfig:
        self._configs[config.id] = config
        return config

    def delete(self, config_id: str) -> bool:
        return self._configs.pop(config_id, None) is not None

    def delete_by_template(self, template_id: str) -> int:
        ids = [cid for cid, c in self._configs.items() if c.template_id == template_id]
        for cid in ids:
            del self._configs[cid]
        return len(ids)

    def count(self, template_id: str) -> int:
        return len([c for c in self._configs.values() if c.template_id == template_id])


# ---------------------------------------------------------------------------
# Service under test (inject stub repos)
# ---------------------------------------------------------------------------


def _make_service():
    """创建使用 stub 仓储的 EditTemplateService"""
    from app.services.edit_template_service import EditTemplateService

    db = MagicMock()
    svc = EditTemplateService(db)
    svc._template_repo = StubEditTemplateRepository()
    svc._clip_config_repo = StubTemplateClipConfigRepository()
    return svc


# ===========================================================================
# 模板 CRUD 测试
# ===========================================================================


class TestEditTemplateServiceCRUD:
    """模板 CRUD 测试"""

    def test_create_template_success(self):
        svc = _make_service()
        t = svc.create_template(name="测试模板", description="描述")
        assert t.name == "测试模板"
        assert t.description == "描述"
        assert t.status == EditTemplateStatus.ACTIVE
        assert t.id

    def test_create_template_strips_name(self):
        svc = _make_service()
        t = svc.create_template(name="  测试  ")
        assert t.name == "测试"

    def test_create_template_empty_name_raises(self):
        svc = _make_service()
        with pytest.raises(ValueError, match="模板名称不能为空"):
            svc.create_template(name="   ")

    def test_create_template_duplicate_name_raises(self):
        svc = _make_service()
        svc.create_template(name="重复名称")
        with pytest.raises(ValueError, match="模板名称已存在"):
            svc.create_template(name="重复名称")

    def test_create_template_inactive_name_can_reuse(self):
        svc = _make_service()
        t = svc.create_template(name="可复用")
        svc.deactivate_template(t.id)
        # inactive 的名称可以复用
        t2 = svc.create_template(name="可复用")
        assert t2.name == "可复用"
        assert t2.id != t.id

    def test_get_template(self):
        svc = _make_service()
        created = svc.create_template(name="查询测试")
        fetched = svc.get_template(created.id)
        assert fetched is not None
        assert fetched.id == created.id

    def test_get_template_returns_none(self):
        svc = _make_service()
        assert svc.get_template("nonexistent") is None

    def test_get_template_or_raise(self):
        svc = _make_service()
        created = svc.create_template(name="查询测试")
        fetched = svc.get_template_or_raise(created.id)
        assert fetched.id == created.id

    def test_get_template_or_raise_not_found(self):
        svc = _make_service()
        with pytest.raises(ValueError, match="模板不存在"):
            svc.get_template_or_raise("nonexistent")

    def test_list_templates(self):
        svc = _make_service()
        svc.create_template(name="模板1")
        svc.create_template(name="模板2")
        result = svc.list_templates()
        assert len(result) == 2

    def test_list_templates_with_type_filter(self):
        svc = _make_service()
        svc.create_template(name="默认", template_type="default")
        svc.create_template(name="Vlog", template_type="vlog")
        result = svc.list_templates(template_type="vlog")
        assert len(result) == 1
        assert result[0].name == "Vlog"

    def test_list_templates_active_only(self):
        svc = _make_service()
        t1 = svc.create_template(name="活跃")
        t2 = svc.create_template(name="停用")
        svc.deactivate_template(t2.id)
        result = svc.list_templates(active_only=True)
        assert len(result) == 1
        assert result[0].name == "活跃"

    def test_count_templates(self):
        svc = _make_service()
        svc.create_template(name="模板1")
        svc.create_template(name="模板2")
        assert svc.count_templates() == 2

    def test_update_template_name(self):
        svc = _make_service()
        t = svc.create_template(name="原名")
        updated = svc.update_template(t.id, name="新名")
        assert updated.name == "新名"

    def test_update_template_duplicate_name_raises(self):
        svc = _make_service()
        svc.create_template(name="已存在")
        t2 = svc.create_template(name="另一个")
        with pytest.raises(ValueError, match="模板名称已存在"):
            svc.update_template(t2.id, name="已存在")

    def test_update_template_not_found_raises(self):
        svc = _make_service()
        with pytest.raises(ValueError, match="模板不存在"):
            svc.update_template("nonexistent", name="新名")

    def test_deactivate_template(self):
        svc = _make_service()
        t = svc.create_template(name="要停用的")
        result = svc.deactivate_template(t.id)
        assert result.status == EditTemplateStatus.INACTIVE

    def test_deactivate_template_not_found_raises(self):
        svc = _make_service()
        with pytest.raises(ValueError, match="模板不存在"):
            svc.deactivate_template("nonexistent")


# ===========================================================================
# 片段配置管理测试
# ===========================================================================


class TestClipConfigManagement:
    """片段配置管理测试"""

    def test_create_clip_config(self):
        svc = _make_service()
        t = svc.create_template(name="模板")
        cfg = svc.create_clip_config(
            template_id=t.id,
            clip_type=ClipType.INTRO,
            order=0,
            min_duration=1.0,
            max_duration=5.0,
        )
        assert cfg.template_id == t.id
        assert cfg.clip_type == ClipType.INTRO
        assert cfg.order == 0
        assert cfg.min_duration == 1.0
        assert cfg.max_duration == 5.0

    def test_create_clip_config_with_string_clip_type(self):
        svc = _make_service()
        t = svc.create_template(name="模板")
        cfg = svc.create_clip_config(
            template_id=t.id,
            clip_type="main",
            order=1,
        )
        assert cfg.clip_type == ClipType.MAIN

    def test_list_clip_configs(self):
        svc = _make_service()
        t = svc.create_template(name="模板")
        svc.create_clip_config(t.id, ClipType.INTRO, 0)
        svc.create_clip_config(t.id, ClipType.MAIN, 1)
        svc.create_clip_config(t.id, ClipType.OUTRO, 2)
        result = svc.list_clip_configs(t.id)
        assert len(result) == 3
        # 按 order 排序
        assert result[0].order == 0
        assert result[1].order == 1
        assert result[2].order == 2

    def test_list_clip_configs_by_type(self):
        svc = _make_service()
        t = svc.create_template(name="模板")
        svc.create_clip_config(t.id, ClipType.INTRO, 0)
        svc.create_clip_config(t.id, ClipType.MAIN, 1)
        result = svc.list_clip_configs(t.id, clip_type=ClipType.MAIN)
        assert len(result) == 1
        assert result[0].clip_type == ClipType.MAIN

    def test_get_clip_config(self):
        svc = _make_service()
        t = svc.create_template(name="模板")
        cfg = svc.create_clip_config(t.id, ClipType.INTRO, 0)
        fetched = svc.get_clip_config(cfg.id)
        assert fetched is not None
        assert fetched.id == cfg.id

    def test_get_clip_config_not_found(self):
        svc = _make_service()
        assert svc.get_clip_config("nonexistent") is None

    def test_get_clip_config_or_raise(self):
        svc = _make_service()
        with pytest.raises(ValueError, match="片段配置不存在"):
            svc.get_clip_config_or_raise("nonexistent")

    def test_update_clip_config(self):
        svc = _make_service()
        t = svc.create_template(name="模板")
        cfg = svc.create_clip_config(t.id, ClipType.INTRO, 0, min_duration=1.0)
        updated = svc.update_clip_config(cfg.id, min_duration=2.0, max_duration=10.0)
        assert updated.min_duration == 2.0
        assert updated.max_duration == 10.0

    def test_delete_clip_config(self):
        svc = _make_service()
        t = svc.create_template(name="模板")
        cfg = svc.create_clip_config(t.id, ClipType.INTRO, 0)
        assert svc.delete_clip_config(cfg.id) is True
        assert svc.get_clip_config(cfg.id) is None

    def test_delete_clip_config_not_found(self):
        svc = _make_service()
        assert svc.delete_clip_config("nonexistent") is False

    def test_reorder_clip_configs(self):
        svc = _make_service()
        t = svc.create_template(name="模板")
        c1 = svc.create_clip_config(t.id, ClipType.INTRO, 0)
        c2 = svc.create_clip_config(t.id, ClipType.MAIN, 1)
        c3 = svc.create_clip_config(t.id, ClipType.OUTRO, 2)

        # 反转顺序
        reordered = svc.reorder_clip_configs(t.id, [c3.id, c2.id, c1.id])
        assert len(reordered) == 3
        assert reordered[0].id == c3.id
        assert reordered[0].order == 0
        assert reordered[1].id == c2.id
        assert reordered[1].order == 1
        assert reordered[2].id == c1.id
        assert reordered[2].order == 2

    def test_reorder_clip_configs_mismatch_raises(self):
        svc = _make_service()
        t = svc.create_template(name="模板")
        c1 = svc.create_clip_config(t.id, ClipType.INTRO, 0)
        svc.create_clip_config(t.id, ClipType.MAIN, 1)

        with pytest.raises(ValueError, match="配置 ID 列表与模板下的配置不匹配"):
            svc.reorder_clip_configs(t.id, [c1.id])  # 缺少一个


# ===========================================================================
# 复合查询测试
# ===========================================================================


class TestCompositeQueries:
    """复合查询测试"""

    def test_get_template_with_configs(self):
        svc = _make_service()
        t = svc.create_template(name="模板")
        svc.create_clip_config(t.id, ClipType.INTRO, 0)
        svc.create_clip_config(t.id, ClipType.MAIN, 1)

        result = svc.get_template_with_configs(t.id)
        assert result["template"].id == t.id
        assert len(result["clip_configs"]) == 2

    def test_get_template_with_configs_not_found(self):
        svc = _make_service()
        with pytest.raises(ValueError, match="模板不存在"):
            svc.get_template_with_configs("nonexistent")

    def test_get_template_with_configs_empty(self):
        svc = _make_service()
        t = svc.create_template(name="空模板")
        result = svc.get_template_with_configs(t.id)
        assert len(result["clip_configs"]) == 0
