"""配方 Recipe UseCase 单元测试."""

from __future__ import annotations

from unittest.mock import MagicMock, patch

import pytest

from packages.application.recipe.commands import (
    CreateRecipeCommand,
    RecipeItemCommand,
    UpdateRecipeCommand,
)
from packages.application.recipe.use_cases import (
    CreateRecipeUseCase,
    DeleteRecipeUseCase,
    FeatureDisabledError,
    GetRecipeUseCase,
    ListRecipesUseCase,
    UpdateRecipeUseCase,
    UseRecipeResult,
    UseRecipeUseCase,
)
from packages.domain.exceptions import NotFoundError
from packages.domain.recipe import Recipe, RecipeItem


def _make_recipe(id: str, name: str, user_id: str = "user_1", item_count: int = 0) -> Recipe:
    items = [
        RecipeItem(
            id=f"item_{i}",
            recipe_id=id,
            item_type="asset",
            item_id=f"asset_{i}",
            position=i,
        )
        for i in range(item_count)
    ]
    return Recipe(
        id=id,
        user_id=user_id,
        name=name,
        description="测试配方",
        template_id="tmpl_1",
        generation_params={"resolution": "1080p"},
        items=items,
        is_active=True,
        metadata_={},
    )


@pytest.fixture
def mock_repo():
    return MagicMock()


class TestListRecipesUseCase:
    """ListRecipesUseCase 测试"""

    def test_list_returns_results(self, mock_repo):
        """正常返回配方列表"""
        recipe = _make_recipe("r1", "配方1")
        mock_repo.list_by_user.return_value = [recipe]
        use_case = ListRecipesUseCase(mock_repo)

        result = use_case.execute("user_1")

        assert len(result) == 1
        assert result[0].id == "r1"
        mock_repo.list_by_user.assert_called_once_with("user_1", skip=0, limit=50)

    def test_list_with_pagination(self, mock_repo):
        """带分页参数"""
        mock_repo.list_by_user.return_value = []
        use_case = ListRecipesUseCase(mock_repo)

        use_case.execute("user_1", skip=5, limit=10)

        mock_repo.list_by_user.assert_called_once_with("user_1", skip=5, limit=10)

    def test_empty_list(self, mock_repo):
        """空列表"""
        mock_repo.list_by_user.return_value = []
        use_case = ListRecipesUseCase(mock_repo)

        result = use_case.execute("user_1")

        assert result == []


class TestGetRecipeUseCase:
    """GetRecipeUseCase 测试"""

    def test_get_existing(self, mock_repo):
        """获取存在的配方"""
        recipe = _make_recipe("r1", "配方1", item_count=3)
        mock_repo.get.return_value = recipe
        use_case = GetRecipeUseCase(mock_repo)

        result = use_case.execute("r1", "user_1")

        assert result is not None
        assert result.id == "r1"
        assert len(result.items) == 3
        mock_repo.get.assert_called_once_with("r1", "user_1")

    def test_get_nonexistent_returns_none(self, mock_repo):
        """获取不存在的配方返回 None"""
        mock_repo.get.return_value = None
        use_case = GetRecipeUseCase(mock_repo)

        result = use_case.execute("noexist", "user_1")

        assert result is None


class TestCreateRecipeUseCase:
    """CreateRecipeUseCase 测试"""

    def test_create_without_items(self, mock_repo):
        """创建不带items的配方"""
        mock_repo.create.side_effect = lambda x: x
        use_case = CreateRecipeUseCase(mock_repo)

        command = CreateRecipeCommand(
            user_id="user_1",
            name="新配方",
            description="测试",
            template_id="tmpl_1",
            generation_params={"key": "value"},
            items=[],
        )
        result = use_case.execute(command)

        assert isinstance(result, Recipe)
        assert result.name == "新配方"
        assert result.user_id == "user_1"
        assert result.template_id == "tmpl_1"
        assert result.generation_params == {"key": "value"}
        assert result.items == []
        mock_repo.create.assert_called_once()
        mock_repo.create_items.assert_not_called()

    def test_create_with_items(self, mock_repo):
        """创建带items的配方"""
        mock_repo.create.side_effect = lambda x: x
        mock_repo.create_items.side_effect = lambda items: items
        use_case = CreateRecipeUseCase(mock_repo)

        command = CreateRecipeCommand(
            user_id="user_1",
            name="带素材配方",
            items=[
                RecipeItemCommand(item_type="asset", item_id="a1", position=0),
                RecipeItemCommand(item_type="title", item_id="t1", position=1),
                RecipeItemCommand(item_type="voice", item_id="v1", position=2),
            ],
        )
        result = use_case.execute(command)

        assert len(result.items) == 3
        assert result.items[0].item_type == "asset"
        assert result.items[1].item_type == "title"
        assert result.items[2].item_type == "voice"
        mock_repo.create.assert_called_once()
        mock_repo.create_items.assert_called_once()
        created_items = mock_repo.create_items.call_args[0][0]
        assert len(created_items) == 3

    def test_create_with_default_values(self, mock_repo):
        """使用默认值创建"""
        mock_repo.create.side_effect = lambda x: x
        use_case = CreateRecipeUseCase(mock_repo)

        command = CreateRecipeCommand(user_id="user_1", name="极简配方")
        result = use_case.execute(command)

        assert result.description == ""
        assert result.template_id == ""
        assert result.generation_params == {}
        assert result.items == []
        assert result.metadata_ == {}


class TestUpdateRecipeUseCase:
    """UpdateRecipeUseCase 测试"""

    def test_update_name(self, mock_repo):
        """更新配方名称"""
        recipe = _make_recipe("r1", "旧名称")
        mock_repo.get.return_value = recipe
        mock_repo.update.side_effect = lambda x: x
        mock_repo.list_items.return_value = []
        use_case = UpdateRecipeUseCase(mock_repo)

        command = UpdateRecipeCommand(recipe_id="r1", user_id="user_1", name="新名称")
        result = use_case.execute(command)

        assert result.name == "新名称"
        # 其他不变
        assert result.description == "测试配方"
        assert result.template_id == "tmpl_1"
        mock_repo.get.assert_called_once_with("r1", "user_1")
        mock_repo.update.assert_called_once()
        # 没传items时从repository加载
        mock_repo.list_items.assert_called_once_with("r1")

    def test_update_multiple_fields(self, mock_repo):
        """同时更新多个字段"""
        recipe = _make_recipe("r1", "旧")
        mock_repo.get.return_value = recipe
        mock_repo.update.side_effect = lambda x: x
        mock_repo.list_items.return_value = []
        use_case = UpdateRecipeUseCase(mock_repo)

        command = UpdateRecipeCommand(
            recipe_id="r1",
            user_id="user_1",
            description="新描述",
            template_id="tmpl_new",
            generation_params={"new": "params"},
        )
        result = use_case.execute(command)

        assert result.description == "新描述"
        assert result.template_id == "tmpl_new"
        assert result.generation_params == {"new": "params"}

    def test_update_items_replaces_old(self, mock_repo):
        """更新items时删除旧的并创建新的"""
        recipe = _make_recipe("r1", "配方", item_count=2)
        mock_repo.get.return_value = recipe
        mock_repo.update.side_effect = lambda x: x
        mock_repo.create_items.side_effect = lambda items: items
        use_case = UpdateRecipeUseCase(mock_repo)

        command = UpdateRecipeCommand(
            recipe_id="r1",
            user_id="user_1",
            items=[
                RecipeItemCommand(item_type="asset", item_id="new_a", position=0),
                RecipeItemCommand(item_type="title", item_id="new_t", position=1),
            ],
        )
        result = use_case.execute(command)

        mock_repo.delete_items_by_recipe.assert_called_once_with("r1")
        mock_repo.create_items.assert_called_once()
        assert len(result.items) == 2
        assert result.items[0].item_id == "new_a"

    def test_update_empty_items_list(self, mock_repo):
        """更新为空items列表也会替换"""
        recipe = _make_recipe("r1", "配方", item_count=3)
        mock_repo.get.return_value = recipe
        mock_repo.update.side_effect = lambda x: x
        mock_repo.create_items.return_value = []
        use_case = UpdateRecipeUseCase(mock_repo)

        command = UpdateRecipeCommand(recipe_id="r1", user_id="user_1", items=[])
        result = use_case.execute(command)

        mock_repo.delete_items_by_recipe.assert_called_once()
        mock_repo.create_items.assert_called_once_with([])
        assert result.items == []

    def test_update_nonexistent_raises(self, mock_repo):
        """更新不存在的配方抛出 NotFoundError"""
        mock_repo.get.return_value = None
        use_case = UpdateRecipeUseCase(mock_repo)

        command = UpdateRecipeCommand(recipe_id="noexist", user_id="user_1", name="新名称")
        with pytest.raises(NotFoundError, match="not found"):
            use_case.execute(command)

        mock_repo.update.assert_not_called()


class TestDeleteRecipeUseCase:
    """DeleteRecipeUseCase 测试"""

    def test_delete_success(self, mock_repo):
        """删除成功"""
        mock_repo.delete.return_value = True
        use_case = DeleteRecipeUseCase(mock_repo)

        result = use_case.execute("r1", "user_1")

        assert result is True
        mock_repo.delete.assert_called_once_with("r1", "user_1")

    def test_delete_nonexistent_returns_false(self, mock_repo):
        """删除不存在的返回 False"""
        mock_repo.delete.return_value = False
        use_case = DeleteRecipeUseCase(mock_repo)

        result = use_case.execute("noexist", "user_1")

        assert result is False


class TestUseRecipeUseCase:
    """UseRecipeUseCase 使用配方测试"""

    def test_use_recipe_premium_enabled(self, mock_repo):
        """premium用户可以使用配方"""
        recipe = _make_recipe("r1", "配方1", item_count=2)
        mock_repo.get.return_value = recipe
        use_case = UseRecipeUseCase(mock_repo)

        # 用 patch mock feature_flags
        with patch("packages.application.recipe.use_cases.feature_flags") as mock_ff:
            mock_ff.is_enabled.return_value = True
            result = use_case.execute("r1", "user_1", user_plan="premium")

        assert isinstance(result, UseRecipeResult)
        assert result.recipe.id == "r1"
        assert isinstance(result.warnings, list)
        mock_repo.get.assert_called_once_with("r1", "user_1")

    def test_use_recipe_basic_enabled(self, mock_repo):
        """basic用户可以使用配方"""
        recipe = _make_recipe("r1", "配方1")
        mock_repo.get.return_value = recipe
        use_case = UseRecipeUseCase(mock_repo)

        with patch("packages.application.recipe.use_cases.feature_flags") as mock_ff:
            mock_ff.is_enabled.return_value = True
            result = use_case.execute("r1", "user_1", user_plan="basic")

        assert result.recipe.id == "r1"

    def test_use_recipe_feature_disabled(self, mock_repo):
        """功能未启用时抛出 FeatureDisabledError"""
        use_case = UseRecipeUseCase(mock_repo)

        with patch("packages.application.recipe.use_cases.feature_flags") as mock_ff:
            mock_ff.is_enabled.return_value = False
            with pytest.raises(FeatureDisabledError, match="仅对基础版和高级版"):
                use_case.execute("r1", "user_1", user_plan="free")

        mock_repo.get.assert_not_called()

    def test_use_recipe_not_found(self, mock_repo):
        """配方不存在时抛出 NotFoundError"""
        mock_repo.get.return_value = None
        use_case = UseRecipeUseCase(mock_repo)

        with patch("packages.application.recipe.use_cases.feature_flags") as mock_ff:
            mock_ff.is_enabled.return_value = True
            with pytest.raises(NotFoundError, match="not found"):
                use_case.execute("noexist", "user_1", user_plan="premium")


class TestRecipeCommands:
    """命令数据类测试"""

    def test_create_recipe_command_fields(self):
        """CreateRecipeCommand 字段"""
        cmd = CreateRecipeCommand(
            user_id="u1",
            name="测试",
            description="desc",
            template_id="t1",
            generation_params={"a": 1},
            items=[RecipeItemCommand(item_type="asset", item_id="a1", position=0)],
            metadata_={"key": "val"},
        )
        assert cmd.user_id == "u1"
        assert cmd.name == "测试"
        assert cmd.description == "desc"
        assert cmd.template_id == "t1"
        assert cmd.generation_params == {"a": 1}
        assert len(cmd.items) == 1
        assert cmd.items[0].item_type == "asset"
        assert cmd.metadata_ == {"key": "val"}

    def test_recipe_item_command_defaults(self):
        """RecipeItemCommand 默认值"""
        cmd = RecipeItemCommand(item_type="asset", item_id="a1")
        assert cmd.position == 0
        assert cmd.metadata_ == {}

    def test_update_recipe_command_defaults_none(self):
        """UpdateRecipeCommand 字段默认None"""
        cmd = UpdateRecipeCommand(recipe_id="r1", user_id="u1")
        assert cmd.name is None
        assert cmd.description is None
        assert cmd.template_id is None
        assert cmd.generation_params is None
        assert cmd.items is None
        assert cmd.metadata_ is None

    def test_commands_are_dataclasses(self):
        """都是 dataclass"""
        from dataclasses import is_dataclass

        assert is_dataclass(CreateRecipeCommand)
        assert is_dataclass(UpdateRecipeCommand)
        assert is_dataclass(RecipeItemCommand)
        assert is_dataclass(UseRecipeResult)
