"""模板 Use Cases 单元测试 — wave216"""

from __future__ import annotations

from unittest.mock import MagicMock

import pytest

from packages.application.template.commands import (
    CopyTemplateCommand,
    CreateCategoryCommand,
    CreateTemplateCommand,
    ListTemplatesFilter,
    SegmentCommand,
    UpdateTemplateCommand,
    ValidateTemplateCommand,
)
from packages.application.template.use_cases import (
    CopyTemplateUseCase,
    CountTemplatesUseCase,
    CreateCategoryUseCase,
    CreateTemplateUseCase,
    DeleteCategoryUseCase,
    DeleteTemplateUseCase,
    GenerateWarning,
    GetTemplateUsageUseCase,
    GetTemplateUseCase,
    ListCategoriesUseCase,
    ListTagsUseCase,
    ListTemplatesUseCase,
    UpdateTemplateUseCase,
    ValidateResult,
    ValidateTemplateUseCase,
)
from packages.domain.editing_mode import EditingMode
from packages.domain.exceptions import NotFoundError, ValidationError
from packages.domain.template import Template, TemplateCategory, TemplateSegment

# ── helpers ──────────────────────────────────────────────────────────────────


def _make_template(
    template_id="t1",
    user_id="u1",
    name="测试模板",
    mode=EditingMode.ONE_TAKE.value,
    category="",
    estimated_duration=30.0,
    segments=None,
):
    if segments is None:
        segments = [
            TemplateSegment(
                id="s1",
                template_id=template_id,
                segment_order=1,
                duration_min=5.0,
                duration_max=10.0,
            )
        ]
    return Template(
        id=template_id,
        user_id=user_id,
        name=name,
        mode=mode,
        category=category,
        estimated_duration=estimated_duration,
        segments=segments,
    )


def _make_segments(n, *, start_order=1, material_type=None):
    return [
        TemplateSegment(
            id=f"s{i}",
            template_id="t1",
            segment_order=start_order + i - 1,
            duration_min=5.0,
            duration_max=10.0,
            material_type=material_type,
        )
        for i in range(1, n + 1)
    ]


# ── CreateTemplateUseCase ────────────────────────────────────────────────────


class TestCreateTemplateUseCase:
    def test_create_success(self):
        repo = MagicMock()
        repo.create.side_effect = lambda t: t
        repo.create_segments.return_value = None

        uc = CreateTemplateUseCase(repo)
        cmd = CreateTemplateCommand(
            user_id="u1",
            name="我的模板",
            mode=EditingMode.ONE_TAKE.value,
            segments=[SegmentCommand(segment_order=1, duration_min=5, duration_max=10)],
        )
        result = uc.execute(cmd)

        assert result.name == "我的模板"
        assert result.mode == EditingMode.ONE_TAKE.value
        assert result.user_id == "u1"
        assert len(result.segments) == 1
        repo.create.assert_called_once()
        repo.create_segments.assert_called_once()

    def test_create_invalid_mode_raises(self):
        repo = MagicMock()
        uc = CreateTemplateUseCase(repo)
        cmd = CreateTemplateCommand(
            user_id="u1",
            name="test",
            mode="invalid_mode",
        )
        with pytest.raises(ValidationError, match="无效的剪辑模式"):
            uc.execute(cmd)

    def test_create_with_multiple_segments(self):
        repo = MagicMock()
        repo.create.side_effect = lambda t: t
        repo.create_segments.return_value = None

        uc = CreateTemplateUseCase(repo)
        cmd = CreateTemplateCommand(
            user_id="u1",
            name="多片段模板",
            mode=EditingMode.VOICE_OVER.value,
            segments=[
                SegmentCommand(segment_order=1, duration_min=3, duration_max=5, material_type="人物"),
                SegmentCommand(segment_order=2, duration_min=5, duration_max=8, material_type="场景"),
            ],
        )
        result = uc.execute(cmd)
        assert len(result.segments) == 2
        assert result.segments[0].segment_order == 1
        assert result.segments[1].segment_order == 2

    def test_create_with_empty_segments(self):
        repo = MagicMock()
        repo.create.side_effect = lambda t: t
        repo.create_segments.return_value = None

        uc = CreateTemplateUseCase(repo)
        cmd = CreateTemplateCommand(
            user_id="u1",
            name="无片段模板",
            mode=EditingMode.PIP.value,
        )
        result = uc.execute(cmd)
        assert len(result.segments) == 0
        repo.create_segments.assert_called_once_with([])


# ── ListTemplatesUseCase ────────────────────────────────────────────────────


class TestListTemplatesUseCase:
    def test_list_no_filter(self):
        templates = [_make_template("t1"), _make_template("t2")]
        repo = MagicMock()
        repo.list_by_user.return_value = templates

        uc = ListTemplatesUseCase(repo)
        result = uc.execute("u1", skip=0, limit=10)

        assert len(result) == 2
        repo.list_by_user.assert_called_once_with("u1", skip=0, limit=10)

    def test_list_with_filter(self):
        templates = [_make_template("t1")]
        repo = MagicMock()
        repo.list_by_user.return_value = templates

        uc = ListTemplatesUseCase(repo)
        f = ListTemplatesFilter(category="cat1", tag="tag1", keyword="test", mode="one_take")
        result = uc.execute("u1", filter=f)

        assert len(result) == 1
        repo.list_by_user.assert_called_once_with(
            "u1",
            skip=0,
            limit=50,
            category="cat1",
            tag="tag1",
            keyword="test",
            mode="one_take",
        )

    def test_list_pagination(self):
        repo = MagicMock()
        repo.list_by_user.return_value = []

        uc = ListTemplatesUseCase(repo)
        uc.execute("u1", skip=20, limit=10)
        repo.list_by_user.assert_called_once_with("u1", skip=20, limit=10)


# ── CountTemplatesUseCase ───────────────────────────────────────────────────


class TestCountTemplatesUseCase:
    def test_count_no_filter(self):
        repo = MagicMock()
        repo.count_by_user.return_value = 42

        uc = CountTemplatesUseCase(repo)
        result = uc.execute("u1")
        assert result == 42
        repo.count_by_user.assert_called_once_with("u1")

    def test_count_with_filter(self):
        repo = MagicMock()
        repo.count_by_user.return_value = 5

        uc = CountTemplatesUseCase(repo)
        f = ListTemplatesFilter(category="cat1", tag="tag1", keyword="kw", mode="pip")
        result = uc.execute("u1", filter=f)
        assert result == 5
        repo.count_by_user.assert_called_once_with(
            "u1",
            category="cat1",
            tag="tag1",
            keyword="kw",
            mode="pip",
        )


# ── GetTemplateUseCase ──────────────────────────────────────────────────────


class TestGetTemplateUseCase:
    def test_get_found(self):
        template = _make_template()
        repo = MagicMock()
        repo.get.return_value = template

        uc = GetTemplateUseCase(repo)
        result = uc.execute("t1", "u1")
        assert result.id == "t1"
        repo.get.assert_called_once_with("t1", "u1")

    def test_get_not_found(self):
        repo = MagicMock()
        repo.get.return_value = None

        uc = GetTemplateUseCase(repo)
        result = uc.execute("nonexistent", "u1")
        assert result is None


# ── UpdateTemplateUseCase ───────────────────────────────────────────────────


class TestUpdateTemplateUseCase:
    def test_update_name(self):
        existing = _make_template(name="old")
        repo = MagicMock()
        repo.get.return_value = existing
        repo.update.return_value = existing
        repo.list_segments.return_value = existing.segments

        uc = UpdateTemplateUseCase(repo)
        cmd = UpdateTemplateCommand(template_id="t1", user_id="u1", name="new")
        result = uc.execute(cmd)

        assert result.name == "new"
        repo.update.assert_called_once()

    def test_update_mode(self):
        existing = _make_template(mode=EditingMode.ONE_TAKE.value)
        repo = MagicMock()
        repo.get.return_value = existing
        repo.update.return_value = existing
        repo.list_segments.return_value = existing.segments

        uc = UpdateTemplateUseCase(repo)
        cmd = UpdateTemplateCommand(template_id="t1", user_id="u1", mode=EditingMode.PIP.value)
        result = uc.execute(cmd)
        assert result.mode == EditingMode.PIP.value

    def test_update_invalid_mode_raises(self):
        existing = _make_template()
        repo = MagicMock()
        repo.get.return_value = existing

        uc = UpdateTemplateUseCase(repo)
        cmd = UpdateTemplateCommand(template_id="t1", user_id="u1", mode="bad")
        with pytest.raises(ValidationError, match="无效的剪辑模式"):
            uc.execute(cmd)

    def test_update_not_found_raises(self):
        repo = MagicMock()
        repo.get.return_value = None

        uc = UpdateTemplateUseCase(repo)
        cmd = UpdateTemplateCommand(template_id="t999", user_id="u1", name="x")
        with pytest.raises(NotFoundError):
            uc.execute(cmd)

    def test_update_segments(self):
        existing = _make_template(segments=_make_segments(1))
        repo = MagicMock()
        repo.get.return_value = existing
        repo.update.return_value = existing
        repo.delete_segments_by_template.return_value = None
        repo.create_segments.return_value = None

        uc = UpdateTemplateUseCase(repo)
        cmd = UpdateTemplateCommand(
            template_id="t1",
            user_id="u1",
            segments=[
                SegmentCommand(segment_order=1, duration_min=2, duration_max=5),
                SegmentCommand(segment_order=2, duration_min=3, duration_max=6),
            ],
        )
        result = uc.execute(cmd)

        repo.delete_segments_by_template.assert_called_once_with("t1")
        repo.create_segments.assert_called_once()
        assert len(result.segments) == 2

    def test_update_none_fields_not_modified(self):
        existing = _make_template(name="keep_name", category="keep_cat")
        repo = MagicMock()
        repo.get.return_value = existing
        repo.update.return_value = existing
        repo.list_segments.return_value = existing.segments

        uc = UpdateTemplateUseCase(repo)
        # 只传 name=None, category=None 表示不修改
        cmd = UpdateTemplateCommand(template_id="t1", user_id="u1")
        result = uc.execute(cmd)

        assert result.name == "keep_name"
        assert result.category == "keep_cat"


# ── DeleteTemplateUseCase ───────────────────────────────────────────────────


class TestDeleteTemplateUseCase:
    def test_delete_success(self):
        repo = MagicMock()
        repo.delete.return_value = True

        uc = DeleteTemplateUseCase(repo)
        result = uc.execute("t1", "u1")
        assert result is True
        repo.delete.assert_called_once_with("t1", "u1")

    def test_delete_not_found(self):
        repo = MagicMock()
        repo.delete.return_value = False

        uc = DeleteTemplateUseCase(repo)
        result = uc.execute("t999", "u1")
        assert result is False


# ── CopyTemplateUseCase ─────────────────────────────────────────────────────


class TestCopyTemplateUseCase:
    def test_copy_success(self):
        copied = _make_template("t2", name="副本")
        repo = MagicMock()
        repo.get.return_value = _make_template("t1")
        repo.copy_template.return_value = copied

        uc = CopyTemplateUseCase(repo)
        cmd = CopyTemplateCommand(template_id="t1", user_id="u1", new_name="副本")
        result = uc.execute(cmd)

        assert result.name == "副本"
        repo.copy_template.assert_called_once_with("t1", "u1", "副本")

    def test_copy_not_found_raises(self):
        repo = MagicMock()
        repo.get.return_value = None

        uc = CopyTemplateUseCase(repo)
        cmd = CopyTemplateCommand(template_id="t999", user_id="u1", new_name="副本")
        with pytest.raises(NotFoundError):
            uc.execute(cmd)

    def test_copy_empty_name_raises(self):
        repo = MagicMock()
        repo.get.return_value = _make_template()

        uc = CopyTemplateUseCase(repo)
        cmd = CopyTemplateCommand(template_id="t1", user_id="u1", new_name="")
        with pytest.raises(ValidationError, match="新模板名称不能为空"):
            uc.execute(cmd)

    def test_copy_whitespace_name_raises(self):
        repo = MagicMock()
        repo.get.return_value = _make_template()

        uc = CopyTemplateUseCase(repo)
        cmd = CopyTemplateCommand(template_id="t1", user_id="u1", new_name="   ")
        with pytest.raises(ValidationError, match="新模板名称不能为空"):
            uc.execute(cmd)

    def test_copy_name_stripped(self):
        copied = _make_template("t2", name="副本")
        repo = MagicMock()
        repo.get.return_value = _make_template("t1")
        repo.copy_template.return_value = copied

        uc = CopyTemplateUseCase(repo)
        cmd = CopyTemplateCommand(template_id="t1", user_id="u1", new_name="  副本  ")
        result = uc.execute(cmd)
        # 会被strip后传给repository
        repo.copy_template.assert_called_once_with("t1", "u1", "副本")


# ── ValidateTemplateUseCase ─────────────────────────────────────────────────


class TestValidateTemplateUseCase:
    def test_one_take_one_segment_ok(self):
        template = _make_template(mode=EditingMode.ONE_TAKE.value, segments=_make_segments(1))
        repo = MagicMock()
        repo.get.return_value = template

        uc = ValidateTemplateUseCase(repo)
        cmd = ValidateTemplateCommand(template_id="t1", user_id="u1")
        result = uc.execute(cmd)

        assert isinstance(result, ValidateResult)
        assert result.template.id == "t1"
        assert len(result.warnings) == 0

    def test_one_take_zero_segments_raises(self):
        template = _make_template(mode=EditingMode.ONE_TAKE.value, segments=[])
        repo = MagicMock()
        repo.get.return_value = template

        uc = ValidateTemplateUseCase(repo)
        cmd = ValidateTemplateCommand(template_id="t1", user_id="u1")
        with pytest.raises(ValidationError, match="一镜到底模式必须恰好有 1 个片段"):
            uc.execute(cmd)

    def test_one_take_multiple_segments_raises(self):
        template = _make_template(mode=EditingMode.ONE_TAKE.value, segments=_make_segments(3))
        repo = MagicMock()
        repo.get.return_value = template

        uc = ValidateTemplateUseCase(repo)
        cmd = ValidateTemplateCommand(template_id="t1", user_id="u1")
        with pytest.raises(ValidationError):
            uc.execute(cmd)

    def test_voice_over_valid_material_types(self):
        segments = [
            TemplateSegment(
                id="s1", template_id="t1", segment_order=1, duration_min=3, duration_max=5, material_type="人物"
            ),
            TemplateSegment(
                id="s2", template_id="t1", segment_order=2, duration_min=5, duration_max=8, material_type="场景"
            ),
        ]
        template = _make_template(mode=EditingMode.VOICE_OVER.value, segments=segments)
        repo = MagicMock()
        repo.get.return_value = template

        uc = ValidateTemplateUseCase(repo)
        cmd = ValidateTemplateCommand(template_id="t1", user_id="u1")
        result = uc.execute(cmd)
        assert len(result.warnings) == 0

    def test_voice_over_missing_material_type_raises(self):
        segments = [
            TemplateSegment(
                id="s1", template_id="t1", segment_order=1, duration_min=3, duration_max=5, material_type=None
            ),
        ]
        template = _make_template(mode=EditingMode.VOICE_OVER.value, segments=segments)
        repo = MagicMock()
        repo.get.return_value = template

        uc = ValidateTemplateUseCase(repo)
        cmd = ValidateTemplateCommand(template_id="t1", user_id="u1")
        with pytest.raises(ValidationError, match="material_type"):
            uc.execute(cmd)

    def test_voice_over_invalid_material_type_raises(self):
        segments = [
            TemplateSegment(
                id="s1", template_id="t1", segment_order=1, duration_min=3, duration_max=5, material_type="动物"
            ),
        ]
        template = _make_template(mode=EditingMode.VOICE_OVER.value, segments=segments)
        repo = MagicMock()
        repo.get.return_value = template

        uc = ValidateTemplateUseCase(repo)
        cmd = ValidateTemplateCommand(template_id="t1", user_id="u1")
        with pytest.raises(ValidationError, match="material_type"):
            uc.execute(cmd)

    def test_voice_over_second_segment_invalid(self):
        segments = [
            TemplateSegment(
                id="s1", template_id="t1", segment_order=1, duration_min=3, duration_max=5, material_type="人物"
            ),
            TemplateSegment(
                id="s2", template_id="t1", segment_order=2, duration_min=5, duration_max=8, material_type="bad"
            ),
        ]
        template = _make_template(mode=EditingMode.VOICE_OVER.value, segments=segments)
        repo = MagicMock()
        repo.get.return_value = template

        uc = ValidateTemplateUseCase(repo)
        cmd = ValidateTemplateCommand(template_id="t1", user_id="u1")
        with pytest.raises(ValidationError) as exc_info:
            uc.execute(cmd)
        # 报错应该提到片段2
        assert "2" in str(exc_info.value)

    def test_voice_duration_mismatch_warning(self):
        template = _make_template(
            mode=EditingMode.VOICE_OVER.value,
            estimated_duration=100.0,
            segments=_make_segments(2, material_type="人物"),
        )
        repo = MagicMock()
        repo.get.return_value = template

        uc = ValidateTemplateUseCase(repo)
        # 配音时长只有50s，预估100s，偏差50% > 30%
        cmd = ValidateTemplateCommand(template_id="t1", user_id="u1", voiceover_duration=50.0)
        result = uc.execute(cmd)

        assert len(result.warnings) == 1
        w = result.warnings[0]
        assert isinstance(w, GenerateWarning)
        assert w.code == "voiceover_duration_mismatch"
        assert "偏差超过" in w.message

    def test_voice_duration_match_no_warning(self):
        template = _make_template(
            mode=EditingMode.VOICE_OVER.value,
            estimated_duration=100.0,
            segments=_make_segments(1, material_type="人物"),
        )
        repo = MagicMock()
        repo.get.return_value = template

        uc = ValidateTemplateUseCase(repo)
        # 配音时长95s，预估100s，偏差5% < 30%
        cmd = ValidateTemplateCommand(template_id="t1", user_id="u1", voiceover_duration=95.0)
        result = uc.execute(cmd)
        assert len(result.warnings) == 0

    def test_voice_duration_at_30_percent_boundary_lower(self):
        # 恰好 0.7 边界不触发
        template = _make_template(mode=EditingMode.PIP.value, estimated_duration=100.0)
        repo = MagicMock()
        repo.get.return_value = template

        uc = ValidateTemplateUseCase(repo)
        cmd = ValidateTemplateCommand(template_id="t1", user_id="u1", voiceover_duration=70.0)
        result = uc.execute(cmd)
        # 恰好 0.7，不算 < 0.7，应该不触发
        assert len(result.warnings) == 0

    def test_voice_duration_below_70_percent_triggers(self):
        template = _make_template(mode=EditingMode.PIP.value, estimated_duration=100.0)
        repo = MagicMock()
        repo.get.return_value = template

        uc = ValidateTemplateUseCase(repo)
        cmd = ValidateTemplateCommand(template_id="t1", user_id="u1", voiceover_duration=69.0)
        result = uc.execute(cmd)
        assert len(result.warnings) == 1

    def test_voice_duration_above_130_percent_triggers(self):
        template = _make_template(mode=EditingMode.PIP.value, estimated_duration=100.0)
        repo = MagicMock()
        repo.get.return_value = template

        uc = ValidateTemplateUseCase(repo)
        cmd = ValidateTemplateCommand(template_id="t1", user_id="u1", voiceover_duration=131.0)
        result = uc.execute(cmd)
        assert len(result.warnings) == 1

    def test_voice_duration_zero_estimated_skip(self):
        # estimated_duration = 0 不会做比例计算
        template = _make_template(estimated_duration=0.0)
        repo = MagicMock()
        repo.get.return_value = template

        uc = ValidateTemplateUseCase(repo)
        cmd = ValidateTemplateCommand(template_id="t1", user_id="u1", voiceover_duration=10.0)
        result = uc.execute(cmd)
        assert len(result.warnings) == 0

    def test_voiceover_duration_none_no_warning(self):
        template = _make_template()
        repo = MagicMock()
        repo.get.return_value = template

        uc = ValidateTemplateUseCase(repo)
        cmd = ValidateTemplateCommand(template_id="t1", user_id="u1")
        result = uc.execute(cmd)
        assert len(result.warnings) == 0

    def test_validate_not_found_raises(self):
        repo = MagicMock()
        repo.get.return_value = None

        uc = ValidateTemplateUseCase(repo)
        cmd = ValidateTemplateCommand(template_id="t999", user_id="u1")
        with pytest.raises(NotFoundError):
            uc.execute(cmd)

    def test_warning_details_structure(self):
        template = _make_template(estimated_duration=100.0)
        repo = MagicMock()
        repo.get.return_value = template

        uc = ValidateTemplateUseCase(repo)
        cmd = ValidateTemplateCommand(template_id="t1", user_id="u1", voiceover_duration=200.0)
        result = uc.execute(cmd)

        assert len(result.warnings) == 1
        details = result.warnings[0].details
        assert "voiceover_duration" in details
        assert "estimated_duration" in details
        assert "ratio" in details
        assert details["voiceover_duration"] == 200.0
        assert details["estimated_duration"] == 100.0
        assert details["ratio"] == 2.0


# ── Category Use Cases ──────────────────────────────────────────────────────


class TestCategoryUseCases:
    def test_create_category(self):
        cat = TemplateCategory(id="c1", user_id="u1", name="分类A")
        repo = MagicMock()
        repo.create_category.return_value = cat

        uc = CreateCategoryUseCase(repo)
        cmd = CreateCategoryCommand(user_id="u1", name="分类A")
        result = uc.execute(cmd)
        assert result.name == "分类A"
        repo.create_category.assert_called_once()

    def test_list_categories(self):
        cats = [TemplateCategory(id="c1", user_id="u1", name="A"), TemplateCategory(id="c2", user_id="u1", name="B")]
        repo = MagicMock()
        repo.list_categories.return_value = cats

        uc = ListCategoriesUseCase(repo)
        result = uc.execute("u1")
        assert len(result) == 2
        repo.list_categories.assert_called_once_with("u1")

    def test_delete_category(self):
        repo = MagicMock()
        repo.delete_category.return_value = True

        uc = DeleteCategoryUseCase(repo)
        result = uc.execute("c1", "u1")
        assert result is True
        repo.delete_category.assert_called_once_with("c1", "u1")


# ── Tags Use Case ───────────────────────────────────────────────────────────


class TestListTagsUseCase:
    def test_list_tags(self):
        repo = MagicMock()
        repo.list_tags.return_value = ["tag1", "tag2", "tag3"]

        uc = ListTagsUseCase(repo)
        result = uc.execute("u1")
        assert result == ["tag1", "tag2", "tag3"]
        repo.list_tags.assert_called_once_with("u1")


# ── Usage Stats Use Case ────────────────────────────────────────────────────


class TestGetTemplateUsageUseCase:
    def test_get_usage(self):
        repo = MagicMock()
        repo.get_usage_count.return_value = 5

        uc = GetTemplateUsageUseCase(repo)
        result = uc.execute("t1")
        assert result == 5
        repo.get_usage_count.assert_called_once_with("t1")

    def test_get_usage_zero(self):
        repo = MagicMock()
        repo.get_usage_count.return_value = 0

        uc = GetTemplateUsageUseCase(repo)
        result = uc.execute("t999")
        assert result == 0
