"""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。

Celery 5.x 中 @task(bind=True) 装饰后，task.run 是绑定方法（self 已绑定），
直接调用 task(profile_id) 即可，不需要手动传 self。
"""

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,
    CosyVoiceService,
    CosyVoiceTimeoutError,
)
from packages.domain.voice_clone_profile import VoiceCloneProfile, VoiceCloneStatus


def _make_profile(
    *,
    status: VoiceCloneStatus = VoiceCloneStatus.PROCESSING,
    metadata: dict | None = None,
) -> VoiceCloneProfile:
    """创建测试用 VoiceCloneProfile。"""
    if metadata is None:
        metadata = {"cosyvoice_task_id": "task-abc"}
    profile = VoiceCloneProfile.create(
        user_id="user-123",
        name="测试音色",
        source_audio_url="https://example.com/audio.wav",
        max_retries=3,
        metadata=metadata,
    )
    profile.status = status
    return profile


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


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

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

        profile = _make_profile()
        mock_repo.get.return_value = profile
        mock_repo.update.side_effect = lambda p: p
        mock_repo_cls.return_value = mock_repo

        mock_service.poll_clone_task.return_value = {"voice_id": "voice-xyz"}
        mock_service_cls.return_value = mock_service

        _mock_db_module.SessionLocal.return_value = mock_session

        from worker_app.tasks.voice_clone import process_voice_clone

        # bind=True → run 是绑定方法，直接调用 task(profile_id)
        result = process_voice_clone("profile-123")

        assert result["ok"] is True
        assert result["voice_id"] == "voice-xyz"
        mock_service.poll_clone_task.assert_called_once_with("task-abc", timeout=300)
        mock_session.commit.assert_called_once()
        mock_session.close.assert_called_once()

    @patch("worker_app.tasks.voice_clone.SQLAlchemyVoiceCloneProfileRepository")
    @patch("worker_app.tasks.voice_clone.CosyVoiceService")
    def test_process_voice_clone_profile_not_found(self, mock_service_cls: MagicMock, mock_repo_cls: MagicMock) -> None:
        """profile 不存在时返回 failed。"""
        mock_session = MagicMock()
        mock_repo = MagicMock()
        mock_repo.get.return_value = None
        mock_repo_cls.return_value = mock_repo

        _mock_db_module.SessionLocal.return_value = mock_session

        from worker_app.tasks.voice_clone import process_voice_clone

        result = process_voice_clone("nonexistent")

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


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


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

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

        profile = _make_profile()
        mock_repo.get.return_value = profile
        mock_repo_cls.return_value = mock_repo

        mock_service.poll_clone_task.side_effect = CosyVoiceTimeoutError("任务超时")
        mock_service_cls.return_value = mock_service

        _mock_db_module.SessionLocal.return_value = mock_session

        from worker_app.tasks.voice_clone import process_voice_clone

        # mock task.retry 使其抛出 Retry（模拟 Celery 行为）
        with patch.object(process_voice_clone, "retry", side_effect=Retry("retrying")):
            with pytest.raises(Retry):
                process_voice_clone("profile-123")

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


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


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

    @patch("worker_app.tasks.voice_clone.SQLAlchemyVoiceCloneProfileRepository")
    @patch("worker_app.tasks.voice_clone.CosyVoiceService")
    def test_process_voice_clone_cosyvoice_error(self, mock_service_cls: MagicMock, mock_repo_cls: MagicMock) -> None:
        """CosyVoice 错误：profile 标记为 failed。"""
        mock_session = MagicMock()
        mock_repo = MagicMock()
        mock_service = MagicMock(spec=CosyVoiceService)

        profile = _make_profile()
        mock_repo.get.return_value = profile
        mock_repo.update.side_effect = lambda p: p
        mock_repo_cls.return_value = mock_repo

        mock_service.poll_clone_task.side_effect = CosyVoiceError("克隆失败")
        mock_service_cls.return_value = mock_service

        _mock_db_module.SessionLocal.return_value = mock_session

        from worker_app.tasks.voice_clone import process_voice_clone

        result = process_voice_clone("profile-123")

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

    @patch("worker_app.tasks.voice_clone.SQLAlchemyVoiceCloneProfileRepository")
    @patch("worker_app.tasks.voice_clone.CosyVoiceService")
    def test_process_voice_clone_unexpected_error(self, mock_service_cls: MagicMock, mock_repo_cls: MagicMock) -> None:
        """意外异常：profile 标记为 failed。"""
        mock_session = MagicMock()
        mock_repo = MagicMock()
        mock_service = MagicMock(spec=CosyVoiceService)

        profile = _make_profile()
        mock_repo.get.return_value = profile
        mock_repo.update.side_effect = lambda p: p
        mock_repo_cls.return_value = mock_repo

        mock_service.poll_clone_task.side_effect = RuntimeError("未知错误")
        mock_service_cls.return_value = mock_service

        _mock_db_module.SessionLocal.return_value = mock_session

        from worker_app.tasks.voice_clone import process_voice_clone

        result = process_voice_clone("profile-123")

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

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

        # 显式传入空 dict，确保没有 cosyvoice_task_id
        profile = _make_profile(metadata={})
        mock_repo.get.return_value = profile
        mock_repo.update.side_effect = lambda p: p
        mock_repo_cls.return_value = mock_repo

        _mock_db_module.SessionLocal.return_value = mock_session

        from worker_app.tasks.voice_clone import process_voice_clone

        result = process_voice_clone("profile-123")

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