"""process_voice_clone Celery 任务单元测试。

关键：voice_clone.py 在模块级别 import worker_app.db.SessionLocal，
而 worker_app.db 会在导入时调用 ensure_database_exists() 尝试连接 PostgreSQL。
因此必须在 @patch 装饰器解析模块路径之前，将 worker_app.db 预注入 sys.modules。

注意：production code 使用 VoiceCloneWorkflowService（非直接 CosyVoiceService），
Celery bind=True 任务的底层函数签名为 (self, profile_id)，
CosyVoiceService 在 voice_clone.py 中被实例化传入 workflow，必须 mock 防止真实初始化。

跨环境兼容：
  Python 3.13 + Celery 5.4.0 → import 返回 Celery Proxy
    → _get_current_object() 返回 Task 实例 → .run 是 bound method（self 已绑定）
    → 调用方式：task.run(profile_id)，retry mock 在 task.run.retry
  Python 3.10 + Celery 5.4.0 → import 返回原始函数（装饰器未生效）
    → 签名 (self, profile_id)，需手动传 mock_self
    → 调用方式：func(mock_self, profile_id)，retry mock 在 mock_self.retry
"""

from __future__ import annotations

import sys
from pathlib import Path
from unittest.mock import MagicMock, patch

import pytest

# worker_app 在 apps/worker 下，需要加入 sys.path
sys.path.insert(0, str(Path(__file__).resolve().parents[2] / "apps" / "worker"))

# ── 预注入 mock 模块，防止 worker_app.db 触发真实数据库连接 ──
_mock_db_module = MagicMock()
_mock_db_module.SessionLocal = MagicMock()
sys.modules.setdefault("worker_app.db", _mock_db_module)

if "worker_app" in sys.modules:
    sys.modules["worker_app"].db = _mock_db_module

from celery.exceptions import Retry

from packages.application.cosyvoice_service import (
    CosyVoiceError,
    CosyVoiceTimeoutError,
)
from packages.application.voice_clone.use_cases import VoiceCloneNotFoundError


def _make_mock_profile(
    *,
    voice_id: str = "voice-xyz",
    status: str = "ready",
) -> MagicMock:
    """创建测试用 mock profile。"""
    profile = MagicMock()
    profile.voice_id = voice_id
    profile.status = status
    return profile


def _resolve_task(task_obj):
    """解析 Celery 任务对象，返回 (callable, mock_self_or_none)。

    跨环境兼容 Celery Proxy / Task 实例 / 原始函数三种情况。

    Returns:
        tuple: (callable, mock_self)
            - Proxy/Task: callable 是 bound method task.run，mock_self=None
            - 原始函数: callable 是原始函数，mock_self 需由调用方提供
    """
    # Case 1: Celery Proxy → 提取 Task 实例的 .run（bound method）
    if hasattr(task_obj, "_get_current_object"):
        real_task = task_obj._get_current_object()
        return real_task.run, None
    # Case 2: Celery Task 实例（非 Proxy）
    if hasattr(task_obj, "run") and hasattr(task_obj, "retry"):
        return task_obj.run, None
    # Case 3: 原始函数（CI 环境中装饰器未生效）
    return task_obj, MagicMock()


# ── 成功场景 ──────────────────────────────────────────────


class TestProcessVoiceCloneSuccess:
    """测试成功场景。"""

    @patch("worker_app.tasks.voice_clone.SessionLocal")
    @patch("worker_app.tasks.voice_clone.CosyVoiceService")
    @patch("worker_app.tasks.voice_clone.VoiceCloneWorkflowService")
    @patch("worker_app.tasks.voice_clone.SQLAlchemyVoiceCloneProfileRepository")
    def test_process_voice_clone_success(
        self,
        mock_repo_cls: MagicMock,
        mock_workflow_cls: MagicMock,
        mock_cosy_cls: MagicMock,
        mock_session_local: MagicMock,
    ) -> None:
        """克隆成功：轮询返回 voice_id，profile 标记为 ready。"""
        mock_session = MagicMock()
        mock_repo = MagicMock()
        mock_workflow = MagicMock()

        mock_repo_cls.return_value = mock_repo
        mock_workflow_cls.return_value = mock_workflow

        mock_result = _make_mock_profile(voice_id="voice-xyz")
        mock_workflow.poll_and_process_clone.return_value = mock_result

        mock_session_local.return_value = mock_session

        from worker_app.tasks.voice_clone import process_voice_clone

        func, mock_self = _resolve_task(process_voice_clone)
        args = (mock_self, "profile-123") if mock_self else ("profile-123",)
        result = func(*args)

        assert result["ok"] is True
        assert result["voice_id"] == "voice-xyz"
        mock_workflow.poll_and_process_clone.assert_called_once_with(
            "profile-123",
            timeout=300,
        )
        mock_session.commit.assert_called_once()
        mock_session.close.assert_called_once()

    @patch("worker_app.tasks.voice_clone.SessionLocal")
    @patch("worker_app.tasks.voice_clone.CosyVoiceService")
    @patch("worker_app.tasks.voice_clone.VoiceCloneWorkflowService")
    @patch("worker_app.tasks.voice_clone.SQLAlchemyVoiceCloneProfileRepository")
    def test_process_voice_clone_profile_not_found(
        self,
        mock_repo_cls: MagicMock,
        mock_workflow_cls: MagicMock,
        mock_cosy_cls: MagicMock,
        mock_session_local: MagicMock,
    ) -> None:
        """profile 不存在时 workflow 抛 VoiceCloneNotFoundError，返回 failed。"""
        mock_session = MagicMock()
        mock_repo = MagicMock()
        mock_workflow = MagicMock()

        mock_repo_cls.return_value = mock_repo
        mock_workflow_cls.return_value = mock_workflow

        mock_workflow.poll_and_process_clone.side_effect = VoiceCloneNotFoundError("Voice clone nonexistent not found")

        mock_session_local.return_value = mock_session

        from worker_app.tasks.voice_clone import process_voice_clone

        func, mock_self = _resolve_task(process_voice_clone)
        args = (mock_self, "nonexistent") if mock_self else ("nonexistent",)
        result = func(*args)

        assert result["ok"] is False
        assert "not found" in result["error"].lower()
        mock_session.rollback.assert_called_once()
        mock_session.close.assert_called_once()


# ── 超时场景 ──────────────────────────────────────────────


class TestProcessVoiceCloneTimeout:
    """测试超时场景。"""

    @patch("worker_app.tasks.voice_clone.SessionLocal")
    @patch("worker_app.tasks.voice_clone.CosyVoiceService")
    @patch("worker_app.tasks.voice_clone.VoiceCloneWorkflowService")
    @patch("worker_app.tasks.voice_clone.SQLAlchemyVoiceCloneProfileRepository")
    def test_process_voice_clone_timeout_retries(
        self,
        mock_repo_cls: MagicMock,
        mock_workflow_cls: MagicMock,
        mock_cosy_cls: MagicMock,
        mock_session_local: MagicMock,
    ) -> None:
        """超时时调用 self.retry() 进行重试，Retry 异常向上传播。"""
        mock_session = MagicMock()
        mock_repo = MagicMock()
        mock_workflow = MagicMock()

        mock_repo_cls.return_value = mock_repo
        mock_workflow_cls.return_value = mock_workflow

        mock_workflow.poll_and_process_clone.side_effect = CosyVoiceTimeoutError("任务超时")

        mock_session_local.return_value = mock_session

        from worker_app.tasks.voice_clone import process_voice_clone

        func, mock_self = _resolve_task(process_voice_clone)

        # 设置 retry mock：根据环境不同，retry 在不同对象上
        if mock_self is None:
            # Proxy/Task 环境：retry 在 Task 实例上（func 是 bound method task.run）
            real_task = process_voice_clone._get_current_object()
            mock_retry = MagicMock()
            mock_retry.side_effect = Retry("retrying")
            with patch.object(real_task, "retry", mock_retry):
                with pytest.raises(Retry):
                    func("profile-123")
                mock_retry.assert_called_once()
        else:
            # 原始函数环境：retry 在 mock_self 上
            mock_self.retry.side_effect = Retry("retrying")
            with pytest.raises(Retry):
                func(mock_self, "profile-123")
            mock_self.retry.assert_called_once()

        mock_session.rollback.assert_called_once()
        mock_session.close.assert_called_once()


# ── 失败场景 ──────────────────────────────────────────────


class TestProcessVoiceCloneFailure:
    """测试失败场景。"""

    @patch("worker_app.tasks.voice_clone.SessionLocal")
    @patch("worker_app.tasks.voice_clone.CosyVoiceService")
    @patch("worker_app.tasks.voice_clone.VoiceCloneWorkflowService")
    @patch("worker_app.tasks.voice_clone.SQLAlchemyVoiceCloneProfileRepository")
    def test_process_voice_clone_cosyvoice_error(
        self,
        mock_repo_cls: MagicMock,
        mock_workflow_cls: MagicMock,
        mock_cosy_cls: MagicMock,
        mock_session_local: MagicMock,
    ) -> None:
        """CosyVoice 错误：profile 标记为 failed。"""
        mock_session = MagicMock()
        mock_repo = MagicMock()
        mock_workflow = MagicMock()

        mock_repo_cls.return_value = mock_repo
        mock_workflow_cls.return_value = mock_workflow

        mock_workflow.poll_and_process_clone.side_effect = CosyVoiceError("克隆失败")

        mock_session_local.return_value = mock_session

        from worker_app.tasks.voice_clone import process_voice_clone

        func, mock_self = _resolve_task(process_voice_clone)
        args = (mock_self, "profile-123") if mock_self else ("profile-123",)
        result = func(*args)

        assert result["ok"] is False
        assert "克隆失败" in result["error"]
        mock_session.rollback.assert_called_once()
        mock_session.close.assert_called_once()

    @patch("worker_app.tasks.voice_clone.SessionLocal")
    @patch("worker_app.tasks.voice_clone.CosyVoiceService")
    @patch("worker_app.tasks.voice_clone.VoiceCloneWorkflowService")
    @patch("worker_app.tasks.voice_clone.SQLAlchemyVoiceCloneProfileRepository")
    def test_process_voice_clone_unexpected_error(
        self,
        mock_repo_cls: MagicMock,
        mock_workflow_cls: MagicMock,
        mock_cosy_cls: MagicMock,
        mock_session_local: MagicMock,
    ) -> None:
        """意外异常：profile 标记为 failed。"""
        mock_session = MagicMock()
        mock_repo = MagicMock()
        mock_workflow = MagicMock()

        mock_repo_cls.return_value = mock_repo
        mock_workflow_cls.return_value = mock_workflow

        mock_workflow.poll_and_process_clone.side_effect = RuntimeError("未知错误")

        mock_session_local.return_value = mock_session

        from worker_app.tasks.voice_clone import process_voice_clone

        func, mock_self = _resolve_task(process_voice_clone)
        args = (mock_self, "profile-123") if mock_self else ("profile-123",)
        result = func(*args)

        assert result["ok"] is False
        assert "未知错误" in result["error"]
        mock_session.rollback.assert_called_once()
        mock_session.close.assert_called_once()

    @patch("worker_app.tasks.voice_clone.SessionLocal")
    @patch("worker_app.tasks.voice_clone.CosyVoiceService")
    @patch("worker_app.tasks.voice_clone.VoiceCloneWorkflowService")
    @patch("worker_app.tasks.voice_clone.SQLAlchemyVoiceCloneProfileRepository")
    def test_process_voice_clone_no_task_id(
        self,
        mock_repo_cls: MagicMock,
        mock_workflow_cls: MagicMock,
        mock_cosy_cls: MagicMock,
        mock_session_local: MagicMock,
    ) -> None:
        """metadata 中没有 cosyvoice_task_id 时返回 failed。"""
        mock_session = MagicMock()
        mock_repo = MagicMock()
        mock_workflow = MagicMock()

        mock_repo_cls.return_value = mock_repo
        mock_workflow_cls.return_value = mock_workflow

        mock_workflow.poll_and_process_clone.side_effect = CosyVoiceError("missing task_id")

        mock_session_local.return_value = mock_session

        from worker_app.tasks.voice_clone import process_voice_clone

        func, mock_self = _resolve_task(process_voice_clone)
        args = (mock_self, "profile-123") if mock_self else ("profile-123",)
        result = func(*args)

        assert result["ok"] is False
        assert "task_id" in result["error"]
        mock_session.rollback.assert_called_once()
        mock_session.close.assert_called_once()
