"""
配音库（Voice Library）Use Case 回归测试

测试目标：
1. CreateVoiceLibraryUseCase - 创建配音库条目，验证 voice_id 字段映射正确（PR#74 P0 bug 修复）
2. UpdateVoiceLibraryUseCase - 更新配音库条目，验证 voice_id 字段映射正确
3. 配额逻辑覆盖 - free=10, basic=100, premium=100
4. 边界条件与异常场景
"""

from unittest.mock import Mock

import pytest

from packages.application.voice_library.commands import (
    CreateVoiceLibraryCommand,
    UpdateVoiceLibraryCommand,
)
from packages.application.voice_library.use_cases import (
    CreateVoiceLibraryUseCase,
    DeleteVoiceLibraryUseCase,
    GetVoiceLibraryUseCase,
    ListVoiceLibraryUseCase,
    NotFoundError,
    QuotaExceededError,
    UpdateVoiceLibraryUseCase,
)
from packages.domain.voice_library import VoiceLibraryItem

# ---------------------------------------------------------------------------
# Fixtures
# ---------------------------------------------------------------------------


@pytest.fixture
def mock_repo():
    """创建 Mock 仓储"""
    repo = Mock()
    repo.count_by_user = Mock(return_value=0)
    repo.create = Mock(side_effect=lambda item: item)
    repo.update = Mock(side_effect=lambda item: item)
    repo.get = Mock(return_value=None)
    repo.delete = Mock(return_value=True)
    repo.list_by_user = Mock(return_value=[])
    return repo


@pytest.fixture
def create_use_case(mock_repo):
    return CreateVoiceLibraryUseCase(repository=mock_repo)


@pytest.fixture
def update_use_case(mock_repo):
    return UpdateVoiceLibraryUseCase(repository=mock_repo)


@pytest.fixture
def sample_create_command():
    """标准创建命令"""
    return CreateVoiceLibraryCommand(
        user_id="user-001",
        name="测试配音",
        text="你好世界",
        voice_provider="aliyun",
        voice_id="voice-abc-123",
        voice_name="小云",
        audio_url="https://oss.example.com/audio/abc.wav",
        duration=3.5,
        file_size=56000,
        status="completed",
        project_id="proj-001",
        tags=["测试", "中文"],
        metadata_={"source": "unit_test"},
    )


@pytest.fixture
def existing_voice_item():
    """模拟已存在的配音条目"""
    return VoiceLibraryItem(
        id="existing-voice-001",
        user_id="user-001",
        name="旧配音",
        text="旧文本",
        voice_provider="old_provider",
        voice_id="old-voice-id",
        voice_name="旧声音",
        audio_url="https://oss.example.com/old.wav",
        duration=1.0,
        file_size=16000,
        status="completed",
        project_id="proj-001",
        tags=["旧"],
        metadata_={},
    )


# ===========================================================================
# 1. CreateVoiceLibraryUseCase 测试
# ===========================================================================


class TestCreateVoiceLibraryUseCase:
    """配音库创建 UseCase 测试"""

    def test_create_success_all_fields(self, create_use_case, mock_repo, sample_create_command):
        """测试创建成功 - 所有字段完整传入"""
        result = create_use_case.execute(sample_create_command, plan_name="free")

        assert result is not None
        assert result.user_id == "user-001"
        assert result.name == "测试配音"
        assert result.text == "你好世界"
        assert result.voice_provider == "aliyun"
        assert result.voice_name == "小云"
        assert result.audio_url == "https://oss.example.com/audio/abc.wav"
        assert result.duration == 3.5
        assert result.file_size == 56000
        assert result.status == "completed"
        assert result.project_id == "proj-001"
        assert result.tags == ["测试", "中文"]
        assert result.metadata_ == {"source": "unit_test"}

        mock_repo.count_by_user.assert_called_once_with("user-001")
        mock_repo.create.assert_called_once()

    def test_create_voice_id_field_mapping(self, create_use_case, mock_repo):
        """
        【P0 回归】验证 voice_id 字段映射正确

        PR#74 修复了 command.id 被错误使用的问题。
        此测试确保 CreateVoiceLibraryCommand 中的 voice_id 字段
        被正确传递到 VoiceLibraryItem 的 voice_id 属性上，
        而非被其他字段（如 item 自身的 id）覆盖。
        """
        command = CreateVoiceLibraryCommand(
            user_id="user-001",
            name="voice_id 回归测试",
            voice_id="specific-voice-id-xyz",
            voice_provider="azure",
            voice_name="Azure Xiaoxiao",
        )

        result = create_use_case.execute(command, plan_name="free")

        # 核心断言：voice_id 必须来自 command.voice_id
        assert result.voice_id == "specific-voice-id-xyz", "voice_id 应来自 command.voice_id，而非其他字段"
        # 同时确保 item 自身生成的 id 与 voice_id 不同
        assert result.id != "specific-voice-id-xyz", "item.id（UUID）不应与 voice_id 混淆"

    def test_create_voice_id_empty_string(self, create_use_case, mock_repo):
        """测试 voice_id 为空字符串的合法场景"""
        command = CreateVoiceLibraryCommand(
            user_id="user-001",
            name="无 voice_id 配音",
            voice_id="",
            voice_provider="custom",
        )

        result = create_use_case.execute(command, plan_name="free")

        assert result.voice_id == ""

    def test_create_default_values(self, create_use_case, mock_repo):
        """测试默认值填充"""
        command = CreateVoiceLibraryCommand(
            user_id="user-001",
            name="最小化创建",
        )

        result = create_use_case.execute(command, plan_name="free")

        assert result.text == ""
        assert result.voice_provider == ""
        assert result.voice_id == ""
        assert result.voice_name == ""
        assert result.audio_url == ""
        assert result.duration == 0
        assert result.file_size == 0
        assert result.status == "completed"
        assert result.project_id is None
        assert result.tags == []
        assert result.metadata_ == {}

    def test_create_generates_uuid(self, create_use_case, mock_repo):
        """测试创建时自动生成 UUID 作为 id"""
        command = CreateVoiceLibraryCommand(
            user_id="user-001",
            name="UUID 测试",
        )

        result = create_use_case.execute(command, plan_name="free")

        assert result.id is not None
        assert len(result.id) == 32  # uuid4().hex 长度为 32
        assert result.id.isalnum()


# ===========================================================================
# 2. 配额逻辑测试（Create 时的配额检查）
# ===========================================================================


class TestCreateVoiceLibraryQuota:
    """配音库创建配额检查测试"""

    def test_quota_free_plan_under_limit(self, create_use_case, mock_repo, sample_create_command):
        """free 套餐（上限10），当前 5 个，允许创建"""
        mock_repo.count_by_user.return_value = 5

        result = create_use_case.execute(sample_create_command, plan_name="free")

        assert result is not None
        mock_repo.create.assert_called_once()

    def test_quota_free_plan_at_limit(self, create_use_case, mock_repo, sample_create_command):
        """free 套餐（上限10），当前 10 个，拒绝创建"""
        mock_repo.count_by_user.return_value = 10

        with pytest.raises(QuotaExceededError) as exc_info:
            create_use_case.execute(sample_create_command, plan_name="free")

        assert exc_info.value.dimension == "max_voiceovers"
        assert exc_info.value.limit == 10
        assert exc_info.value.used == 10
        mock_repo.create.assert_not_called()

    def test_quota_free_plan_over_limit(self, create_use_case, mock_repo, sample_create_command):
        """free 套餐（上限10），当前 15 个，拒绝创建"""
        mock_repo.count_by_user.return_value = 15

        with pytest.raises(QuotaExceededError) as exc_info:
            create_use_case.execute(sample_create_command, plan_name="free")

        assert exc_info.value.dimension == "max_voiceovers"
        assert exc_info.value.limit == 10
        assert exc_info.value.used == 15

    def test_quota_free_plan_just_under_limit(self, create_use_case, mock_repo, sample_create_command):
        """free 套餐（上限10），当前 9 个，允许创建（边界）"""
        mock_repo.count_by_user.return_value = 9

        result = create_use_case.execute(sample_create_command, plan_name="free")

        assert result is not None
        mock_repo.create.assert_called_once()

    def test_quota_basic_plan_under_limit(self, create_use_case, mock_repo, sample_create_command):
        """basic 套餐（上限100），当前 50 个，允许创建"""
        mock_repo.count_by_user.return_value = 50

        result = create_use_case.execute(sample_create_command, plan_name="basic")

        assert result is not None
        mock_repo.create.assert_called_once()

    def test_quota_basic_plan_at_limit(self, create_use_case, mock_repo, sample_create_command):
        """basic 套餐（上限100），当前 100 个，拒绝创建"""
        mock_repo.count_by_user.return_value = 100

        with pytest.raises(QuotaExceededError) as exc_info:
            create_use_case.execute(sample_create_command, plan_name="basic")

        assert exc_info.value.dimension == "max_voiceovers"
        assert exc_info.value.limit == 100
        assert exc_info.value.used == 100

    def test_quota_basic_plan_just_under_limit(self, create_use_case, mock_repo, sample_create_command):
        """basic 套餐（上限100），当前 99 个，允许创建（边界）"""
        mock_repo.count_by_user.return_value = 99

        result = create_use_case.execute(sample_create_command, plan_name="basic")

        assert result is not None
        mock_repo.create.assert_called_once()

    def test_quota_premium_plan_under_limit(self, create_use_case, mock_repo, sample_create_command):
        """premium 套餐（上限100），当前 50 个，允许创建"""
        mock_repo.count_by_user.return_value = 50

        result = create_use_case.execute(sample_create_command, plan_name="premium")

        assert result is not None
        mock_repo.create.assert_called_once()

    def test_quota_premium_plan_at_limit(self, create_use_case, mock_repo, sample_create_command):
        """premium 套餐（上限100），当前 100 个，拒绝创建"""
        mock_repo.count_by_user.return_value = 100

        with pytest.raises(QuotaExceededError) as exc_info:
            create_use_case.execute(sample_create_command, plan_name="premium")

        assert exc_info.value.dimension == "max_voiceovers"
        assert exc_info.value.limit == 100
        assert exc_info.value.used == 100

    def test_quota_premium_plan_just_under_limit(self, create_use_case, mock_repo, sample_create_command):
        """premium 套餐（上限100），当前 99 个，允许创建（边界）"""
        mock_repo.count_by_user.return_value = 99

        result = create_use_case.execute(sample_create_command, plan_name="premium")

        assert result is not None
        mock_repo.create.assert_called_once()

    def test_quota_zero_usage(self, create_use_case, mock_repo, sample_create_command):
        """新用户零使用量，所有套餐均可创建"""
        mock_repo.count_by_user.return_value = 0

        for plan in ["free", "basic", "premium"]:
            mock_repo.create.reset_mock()
            mock_repo.count_by_user.reset_mock()
            mock_repo.count_by_user.return_value = 0

            result = create_use_case.execute(sample_create_command, plan_name=plan)
            assert result is not None, f"{plan} 套餐零使用量应允许创建"

    def test_quota_unknown_plan_defaults_to_zero(self, create_use_case, mock_repo, sample_create_command):
        """未知套餐名默认配额为 0，即使 0 使用量也无法创建"""
        mock_repo.count_by_user.return_value = 0

        with pytest.raises(QuotaExceededError):
            create_use_case.execute(sample_create_command, plan_name="unknown_plan")

    def test_quota_exceeded_error_attributes(self, create_use_case, mock_repo, sample_create_command):
        """QuotaExceededError 异常属性完整性"""
        mock_repo.count_by_user.return_value = 10

        with pytest.raises(QuotaExceededError) as exc_info:
            create_use_case.execute(sample_create_command, plan_name="free")

        err = exc_info.value
        assert hasattr(err, "dimension")
        assert hasattr(err, "limit")
        assert hasattr(err, "used")
        assert "max_voiceovers" in str(err)
        assert "10" in str(err)


# ===========================================================================
# 3. UpdateVoiceLibraryUseCase 测试
# ===========================================================================


class TestUpdateVoiceLibraryUseCase:
    """配音库更新 UseCase 测试"""

    def test_update_success_all_fields(self, update_use_case, mock_repo, existing_voice_item):
        """测试全字段更新成功"""
        mock_repo.get.return_value = existing_voice_item

        command = UpdateVoiceLibraryCommand(
            id="existing-voice-001",
            user_id="user-001",
            name="更新后的名称",
            text="更新后的文本",
            voice_provider="new_provider",
            voice_id="new-voice-id-456",
            voice_name="新声音",
            audio_url="https://oss.example.com/new.wav",
            duration=5.0,
            file_size=80000,
            status="processing",
            tags=["新标签"],
            metadata_={"updated": True},
        )

        result = update_use_case.execute(command)

        assert result.name == "更新后的名称"
        assert result.text == "更新后的文本"
        assert result.voice_provider == "new_provider"
        assert result.voice_name == "新声音"
        assert result.audio_url == "https://oss.example.com/new.wav"
        assert result.duration == 5.0
        assert result.file_size == 80000
        assert result.status == "processing"
        assert result.tags == ["新标签"]
        assert result.metadata_ == {"updated": True}

        mock_repo.update.assert_called_once()

    def test_update_voice_id_field_mapping(self, update_use_case, mock_repo, existing_voice_item):
        """
        【P0 回归】验证 update 时 voice_id 字段映射正确

        PR#74 修复了 API 路由层将 command.id 错误传给 voice_id 的 bug。
        此测试确保 UpdateVoiceLibraryCommand 中 voice_id 字段
        被正确写入 VoiceLibraryItem.voice_id，而非被 item.id 覆盖。
        """
        mock_repo.get.return_value = existing_voice_item

        command = UpdateVoiceLibraryCommand(
            id="existing-voice-001",
            user_id="user-001",
            voice_id="completely-different-voice-id",
        )

        result = update_use_case.execute(command)

        # 核心断言：voice_id 应被更新为新值
        assert result.voice_id == "completely-different-voice-id", "voice_id 应被更新为 command.voice_id 的值"
        # item 自身的 id 保持不变
        assert result.id == "existing-voice-001"

    def test_update_partial_only_voice_id(self, update_use_case, mock_repo, existing_voice_item):
        """测试仅更新 voice_id 一个字段"""
        mock_repo.get.return_value = existing_voice_item

        command = UpdateVoiceLibraryCommand(
            id="existing-voice-001",
            user_id="user-001",
            voice_id="only-voice-id-changed",
        )

        result = update_use_case.execute(command)

        assert result.voice_id == "only-voice-id-changed"
        # 其他字段保持不变
        assert result.name == "旧配音"
        assert result.text == "旧文本"
        assert result.voice_provider == "old_provider"
        assert result.voice_name == "旧声音"
        assert result.audio_url == "https://oss.example.com/old.wav"
        assert result.duration == 1.0
        assert result.file_size == 16000

    def test_update_partial_only_name(self, update_use_case, mock_repo, existing_voice_item):
        """测试仅更新 name"""
        mock_repo.get.return_value = existing_voice_item

        command = UpdateVoiceLibraryCommand(
            id="existing-voice-001",
            user_id="user-001",
            name="仅改名",
        )

        result = update_use_case.execute(command)

        assert result.name == "仅改名"
        assert result.voice_id == "old-voice-id"  # voice_id 不变

    def test_update_not_found(self, update_use_case, mock_repo):
        """测试更新不存在的条目"""
        mock_repo.get.return_value = None

        command = UpdateVoiceLibraryCommand(
            id="nonexistent-id",
            user_id="user-001",
            name="不存在",
        )

        with pytest.raises(NotFoundError, match="nonexistent-id"):
            update_use_case.execute(command)

        mock_repo.update.assert_not_called()

    def test_update_wrong_user(self, update_use_case, mock_repo):
        """测试用户隔离 - 不能更新其他用户的条目"""
        mock_repo.get.return_value = None  # repo 返回 None 表示找不到（不同 user_id）

        command = UpdateVoiceLibraryCommand(
            id="existing-voice-001",
            user_id="other-user-999",
            name="恶意修改",
        )

        with pytest.raises(NotFoundError):
            update_use_case.execute(command)

    def test_update_none_fields_not_changed(self, update_use_case, mock_repo, existing_voice_item):
        """测试 None 字段不覆盖原有值"""
        mock_repo.get.return_value = existing_voice_item

        command = UpdateVoiceLibraryCommand(
            id="existing-voice-001",
            user_id="user-001",
            # 所有可选字段保持 None
        )

        result = update_use_case.execute(command)

        # 所有字段应保持不变
        assert result.name == "旧配音"
        assert result.text == "旧文本"
        assert result.voice_id == "old-voice-id"
        assert result.voice_provider == "old_provider"
        assert result.voice_name == "旧声音"
        assert result.audio_url == "https://oss.example.com/old.wav"
        assert result.duration == 1.0
        assert result.file_size == 16000
        assert result.status == "completed"

    def test_update_voice_id_empty_string(self, update_use_case, mock_repo, existing_voice_item):
        """测试 voice_id 更新为空字符串（合法场景：清除 voice_id）"""
        mock_repo.get.return_value = existing_voice_item

        command = UpdateVoiceLibraryCommand(
            id="existing-voice-001",
            user_id="user-001",
            voice_id="",
        )

        result = update_use_case.execute(command)

        assert result.voice_id == ""


# ===========================================================================
# 4. DeleteVoiceLibraryUseCase 测试
# ===========================================================================


class TestDeleteVoiceLibraryUseCase:
    """配音库删除 UseCase 测试"""

    def test_delete_success(self, mock_repo):
        """测试删除成功"""
        mock_repo.delete.return_value = True
        use_case = DeleteVoiceLibraryUseCase(repository=mock_repo)

        result = use_case.execute("voice-001", "user-001")

        assert result is True
        mock_repo.delete.assert_called_once_with("voice-001", "user-001")

    def test_delete_not_found(self, mock_repo):
        """测试删除不存在的条目"""
        mock_repo.delete.return_value = False
        use_case = DeleteVoiceLibraryUseCase(repository=mock_repo)

        result = use_case.execute("nonexistent", "user-001")

        assert result is False


# ===========================================================================
# 5. GetVoiceLibraryUseCase 测试
# ===========================================================================


class TestGetVoiceLibraryUseCase:
    """配音库查询 UseCase 测试"""

    def test_get_existing(self, mock_repo):
        """测试查询存在的条目"""
        expected = VoiceLibraryItem(
            id="v-001",
            user_id="user-001",
            name="测试",
            voice_id="voice-xyz",
        )
        mock_repo.get.return_value = expected
        use_case = GetVoiceLibraryUseCase(repository=mock_repo)

        result = use_case.execute("v-001", "user-001")

        assert result is not None
        assert result.id == "v-001"
        assert result.voice_id == "voice-xyz"
        mock_repo.get.assert_called_once_with("v-001", "user-001")

    def test_get_not_found(self, mock_repo):
        """测试查询不存在的条目"""
        mock_repo.get.return_value = None
        use_case = GetVoiceLibraryUseCase(repository=mock_repo)

        result = use_case.execute("nonexistent", "user-001")

        assert result is None


# ===========================================================================
# 6. ListVoiceLibraryUseCase 测试
# ===========================================================================


class TestListVoiceLibraryUseCase:
    """配音库列表 UseCase 测试"""

    def test_list_default(self, mock_repo):
        """测试默认列表查询"""
        items = [
            VoiceLibraryItem(id="v1", user_id="user-001", name="A"),
            VoiceLibraryItem(id="v2", user_id="user-001", name="B"),
        ]
        mock_repo.list_by_user.return_value = items
        mock_repo.count_by_user.return_value = 2
        use_case = ListVoiceLibraryUseCase(repository=mock_repo)

        result_items, total = use_case.execute("user-001")

        assert len(result_items) == 2
        assert total == 2
        mock_repo.list_by_user.assert_called_once_with("user-001", status=None, skip=0, limit=50)

    def test_list_with_status_filter(self, mock_repo):
        """测试按状态筛选"""
        mock_repo.list_by_user.return_value = []
        use_case = ListVoiceLibraryUseCase(repository=mock_repo)

        use_case.execute("user-001", status="completed", skip=10, limit=20)

        mock_repo.list_by_user.assert_called_once_with("user-001", status="completed", skip=10, limit=20)

    def test_list_empty(self, mock_repo):
        """测试空列表"""
        mock_repo.list_by_user.return_value = []
        mock_repo.count_by_user.return_value = 0
        use_case = ListVoiceLibraryUseCase(repository=mock_repo)

        items, total = use_case.execute("user-001")

        assert items == []
        assert total == 0


# ===========================================================================
# 7. voice_id 与 id 字段隔离专项回归测试
# ===========================================================================


class TestVoiceIdFieldIsolation:
    """
    PR#74 P0 Bug 回归：voice_id 与 item.id 字段隔离

    原 bug：API 路由层误将 command.id（item 主键）用作 voice_id，
    导致 voice_id 字段值错误。本测试类从 UseCase 层验证
    这两个字段在整个 CRUD 生命周期中互不干扰。
    """

    def test_create_id_and_voice_id_are_independent(self, create_use_case, mock_repo):
        """创建时 id 自动生成，voice_id 来自 command"""
        command = CreateVoiceLibraryCommand(
            user_id="user-001",
            name="隔离测试",
            voice_id="tts-voice-001",
            voice_provider="openai",
        )

        result = create_use_case.execute(command, plan_name="free")

        assert result.id != result.voice_id, "id 和 voice_id 应为不同值"
        assert result.voice_id == "tts-voice-001"
        assert len(result.id) == 32  # UUID hex

    def test_update_voice_id_does_not_change_id(self, update_use_case, mock_repo):
        """更新 voice_id 不影响 item 主键 id"""
        existing = VoiceLibraryItem(
            id="stable-id-001",
            user_id="user-001",
            name="测试",
            voice_id="old-voice",
        )
        mock_repo.get.return_value = existing

        command = UpdateVoiceLibraryCommand(
            id="stable-id-001",
            user_id="user-001",
            voice_id="new-voice-999",
        )

        result = update_use_case.execute(command)

        assert result.id == "stable-id-001", "item 主键 id 不应改变"
        assert result.voice_id == "new-voice-999", "voice_id 应被更新"

    def test_create_then_update_voice_id_preserves_id(self, create_use_case, update_use_case, mock_repo):
        """创建后再更新 voice_id，id 始终不变"""
        # 创建
        create_cmd = CreateVoiceLibraryCommand(
            user_id="user-001",
            name="生命周期测试",
            voice_id="initial-voice",
        )
        created = create_use_case.execute(create_cmd, plan_name="free")
        original_id = created.id

        # 更新
        mock_repo.get.return_value = created
        update_cmd = UpdateVoiceLibraryCommand(
            id=original_id,
            user_id="user-001",
            voice_id="updated-voice",
        )
        updated = update_use_case.execute(update_cmd)

        assert updated.id == original_id, "经过创建和更新，id 应保持一致"
        assert updated.voice_id == "updated-voice"
        assert updated.voice_id != "initial-voice"
