"""项目 UseCase 单元测试."""

from __future__ import annotations

from unittest.mock import MagicMock

import pytest

from packages.application.projects import (
    CreateProjectCommand,
    CreateProjectUseCase,
    DeleteProjectUseCase,
    GetProjectUseCase,
    ListProjectsUseCase,
    ShareProjectUseCase,
    UnshareProjectUseCase,
)
from packages.domain import Project


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


@pytest.fixture
def sample_project():
    p = Project.create(owner_user_id="user_1", name="测试项目", description="测试描述")
    p.id = "proj_123"
    return p


class TestListProjectsUseCase:
    """ListProjectsUseCase 测试"""

    def test_list_returns_repo_results(self, mock_repo, sample_project):
        """正常返回 repository 查询结果"""
        mock_repo.find_accessible_projects.return_value = [sample_project]
        use_case = ListProjectsUseCase(mock_repo)

        result = use_case.execute("user_1")

        assert len(result) == 1
        assert result[0].id == "proj_123"
        mock_repo.find_accessible_projects.assert_called_once_with("user_1")

    def test_empty_user_id_raises(self, mock_repo):
        """空 user_id 抛出 ValueError"""
        use_case = ListProjectsUseCase(mock_repo)

        with pytest.raises(ValueError, match="user_id 不能为空"):
            use_case.execute("")

        mock_repo.find_accessible_projects.assert_not_called()

    def test_whitespace_user_id_raises(self, mock_repo):
        """纯空格 user_id 也抛出"""
        use_case = ListProjectsUseCase(mock_repo)

        with pytest.raises(ValueError, match="user_id 不能为空"):
            use_case.execute("   ")

    def test_user_id_stripped(self, mock_repo, sample_project):
        """user_id 会被 strip 后查询"""
        mock_repo.find_accessible_projects.return_value = [sample_project]
        use_case = ListProjectsUseCase(mock_repo)

        use_case.execute("  user_1  ")

        mock_repo.find_accessible_projects.assert_called_once_with("user_1")


class TestGetProjectUseCase:
    """GetProjectUseCase 测试"""

    def test_get_existing_project(self, mock_repo, sample_project):
        """获取存在的项目"""
        mock_repo.find_by_id.return_value = sample_project
        use_case = GetProjectUseCase(mock_repo)

        result = use_case.execute("proj_123")

        assert result is not None
        assert result.id == "proj_123"
        mock_repo.find_by_id.assert_called_once_with("proj_123")

    def test_get_nonexistent_returns_none(self, mock_repo):
        """获取不存在的项目返回 None"""
        mock_repo.find_by_id.return_value = None
        use_case = GetProjectUseCase(mock_repo)

        result = use_case.execute("nonexistent")

        assert result is None

    def test_empty_project_id_raises(self, mock_repo):
        """空 project_id 抛出"""
        use_case = GetProjectUseCase(mock_repo)

        with pytest.raises(ValueError, match="project_id 不能为空"):
            use_case.execute("")

    def test_project_id_stripped(self, mock_repo, sample_project):
        """project_id 会被 strip"""
        mock_repo.find_by_id.return_value = sample_project
        use_case = GetProjectUseCase(mock_repo)

        use_case.execute("  proj_123  ")

        mock_repo.find_by_id.assert_called_once_with("proj_123")


class TestCreateProjectUseCase:
    """CreateProjectUseCase 测试"""

    def test_create_success(self, mock_repo, sample_project):
        """创建成功返回 Project"""
        mock_repo.save.return_value = sample_project
        use_case = CreateProjectUseCase(mock_repo)

        command = CreateProjectCommand(name="新项目", description="新描述")
        result = use_case.execute(command, "user_1")

        assert result.id == "proj_123"
        mock_repo.save.assert_called_once()
        saved = mock_repo.save.call_args[0][0]
        assert isinstance(saved, Project)
        assert saved.owner_user_id == "user_1"
        assert saved.name == "新项目"
        assert saved.description == "新描述"

    def test_create_without_description(self, mock_repo):
        """不传 description 使用默认值"""
        mock_repo.save.side_effect = lambda x: x
        use_case = CreateProjectUseCase(mock_repo)

        command = CreateProjectCommand(name="极简项目")
        result = use_case.execute(command, "user_1")

        assert result.name == "极简项目"
        assert result.description == ""

    def test_create_empty_name_raises(self, mock_repo):
        """空项目名在 domain 层抛出"""
        use_case = CreateProjectUseCase(mock_repo)

        command = CreateProjectCommand(name="")
        with pytest.raises(ValueError, match="项目名称不能为空"):
            use_case.execute(command, "user_1")

        mock_repo.save.assert_not_called()


class TestShareProjectUseCase:
    """ShareProjectUseCase 测试"""

    def test_share_success(self, mock_repo, sample_project):
        """所有者成功共享项目"""
        mock_repo.find_by_id.return_value = sample_project
        mock_repo.save.side_effect = lambda x: x
        use_case = ShareProjectUseCase(mock_repo)

        result = use_case.execute("proj_123", "user_1", "user_2")

        assert "user_2" in result.shared_users
        mock_repo.save.assert_called_once()

    def test_share_nonexistent_project_raises(self, mock_repo):
        """项目不存在时抛出"""
        mock_repo.find_by_id.return_value = None
        use_case = ShareProjectUseCase(mock_repo)

        with pytest.raises(ValueError, match="项目不存在"):
            use_case.execute("noexist", "user_1", "user_2")

    def test_share_not_owner_raises(self, mock_repo, sample_project):
        """非所有者不能共享"""
        mock_repo.find_by_id.return_value = sample_project
        use_case = ShareProjectUseCase(mock_repo)

        with pytest.raises(ValueError, match="只有项目所有者可以共享"):
            use_case.execute("proj_123", "user_other", "user_2")

        mock_repo.save.assert_not_called()

    def test_share_already_shared_no_duplicate(self, mock_repo, sample_project):
        """已共享的用户不会重复添加"""
        sample_project.shared_users = ["user_2"]
        mock_repo.find_by_id.return_value = sample_project
        mock_repo.save.side_effect = lambda x: x
        use_case = ShareProjectUseCase(mock_repo)

        result = use_case.execute("proj_123", "user_1", "user_2")

        assert result.shared_users.count("user_2") == 1
        mock_repo.save.assert_not_called()


class TestUnshareProjectUseCase:
    """UnshareProjectUseCase 测试"""

    def test_unshare_success(self, mock_repo, sample_project):
        """所有者成功取消共享"""
        sample_project.shared_users = ["user_2", "user_3"]
        mock_repo.find_by_id.return_value = sample_project
        mock_repo.save.side_effect = lambda x: x
        use_case = UnshareProjectUseCase(mock_repo)

        result = use_case.execute("proj_123", "user_1", "user_2")

        assert "user_2" not in result.shared_users
        assert "user_3" in result.shared_users
        mock_repo.save.assert_called_once()

    def test_unshare_nonexistent_project_raises(self, mock_repo):
        """项目不存在时抛出"""
        mock_repo.find_by_id.return_value = None
        use_case = UnshareProjectUseCase(mock_repo)

        with pytest.raises(ValueError, match="项目不存在"):
            use_case.execute("noexist", "user_1", "user_2")

    def test_unshare_not_owner_raises(self, mock_repo, sample_project):
        """非所有者不能取消共享"""
        sample_project.shared_users = ["user_2"]
        mock_repo.find_by_id.return_value = sample_project
        use_case = UnshareProjectUseCase(mock_repo)

        with pytest.raises(ValueError, match="只有项目所有者可以取消共享"):
            use_case.execute("proj_123", "user_other", "user_2")

        mock_repo.save.assert_not_called()

    def test_unshare_not_shared_no_save(self, mock_repo, sample_project):
        """用户未被共享时不触发 save"""
        mock_repo.find_by_id.return_value = sample_project
        use_case = UnshareProjectUseCase(mock_repo)

        result = use_case.execute("proj_123", "user_1", "user_not_shared")

        assert result is sample_project
        mock_repo.save.assert_not_called()


class TestDeleteProjectUseCase:
    """DeleteProjectUseCase 测试"""

    def test_delete_owner_success(self, mock_repo, sample_project):
        """所有者删除成功"""
        mock_repo.find_by_id.return_value = sample_project
        mock_repo.delete.return_value = True
        use_case = DeleteProjectUseCase(mock_repo)

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

        assert result is True
        mock_repo.delete.assert_called_once_with("proj_123")

    def test_delete_nonexistent_returns_false(self, mock_repo):
        """删除不存在的项目返回 False"""
        mock_repo.find_by_id.return_value = None
        use_case = DeleteProjectUseCase(mock_repo)

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

        assert result is False
        mock_repo.delete.assert_not_called()

    def test_delete_not_owner_raises(self, mock_repo, sample_project):
        """非所有者删除抛出 PermissionError"""
        mock_repo.find_by_id.return_value = sample_project
        use_case = DeleteProjectUseCase(mock_repo)

        with pytest.raises(PermissionError, match="只有项目所有者可以删除"):
            use_case.execute("proj_123", "user_other")

        mock_repo.delete.assert_not_called()


class TestCreateProjectCommand:
    """CreateProjectCommand 数据类测试"""

    def test_command_fields(self):
        """命令对象字段正确"""
        cmd = CreateProjectCommand(name="test", description="desc")
        assert cmd.name == "test"
        assert cmd.description == "desc"

    def test_command_default_description(self):
        """description 默认空字符串"""
        cmd = CreateProjectCommand(name="test")
        assert cmd.description == ""

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

        assert is_dataclass(CreateProjectCommand)
