"""VoiceCloneWorkflowService 单元测试。"""

from __future__ import annotations

from unittest.mock import MagicMock, patch

import pytest

from packages.application.cosyvoice_service import (
    CosyVoiceAuthError,
    CosyVoiceError,
    CosyVoiceService,
)
from packages.application.voice_clone.use_cases import (
    VoiceCloneNotFoundError,
    VoiceCloneNotRetryableError,
)
from packages.application.voice_clone.workflow import VoiceCloneWorkflowService
from packages.domain.voice_clone_profile import VoiceCloneProfile, VoiceCloneStatus


def _make_profile(
    *,
    status: VoiceCloneStatus = VoiceCloneStatus.PENDING,
    source_audio_url: str = "https://example.com/audio.wav",
    retry_count: int = 0,
    max_retries: int = 3,
    metadata: dict | None = None,
) -> VoiceCloneProfile:
    """创建测试用 VoiceCloneProfile。"""
    profile = VoiceCloneProfile.create(
        user_id="user-123",
        name="测试音色",
        source_audio_url=source_audio_url,
        max_retries=max_retries,
        metadata=metadata,
    )
    profile.status = status
    profile.retry_count = retry_count
    return profile


def _make_service(
    *,
    repo: MagicMock | None = None,
    cosyvoice: MagicMock | None = None,
) -> VoiceCloneWorkflowService:
    """创建测试用 VoiceCloneWorkflowService。"""
    mock_repo = repo or MagicMock()
    mock_cosyvoice = cosyvoice or MagicMock(spec=CosyVoiceService)
    return VoiceCloneWorkflowService(repository=mock_repo, cosyvoice_service=mock_cosyvoice)


# ── start_clone ──────────────────────────────────────────


class TestStartClone:
    """测试 start_clone 方法。"""

    def test_start_clone_with_async_task(self) -> None:
        """异步模式：提交任务后返回 processing 状态的 profile。"""
        mock_repo = MagicMock()
        mock_cosyvoice = MagicMock(spec=CosyVoiceService)

        # CosyVoice 返回 task_id（异步模式）
        mock_cosyvoice.submit_clone_task.return_value = {
            "task_id": "task-abc",
            "voice_id": "",
            "request_id": "req-123",
        }

        # repo.create 和 repo.update 返回传入的 profile
        mock_repo.create.side_effect = lambda p: p
        mock_repo.update.side_effect = lambda p: p

        service = _make_service(repo=mock_repo, cosyvoice=mock_cosyvoice)
        profile = service.start_clone(
            user_id="user-123",
            name="测试音色",
            source_audio_url="https://example.com/audio.wav",
        )

        assert profile.status == VoiceCloneStatus.PROCESSING
        assert profile.metadata["cosyvoice_task_id"] == "task-abc"
        assert profile.metadata["cosyvoice_request_id"] == "req-123"
        mock_cosyvoice.submit_clone_task.assert_called_once()
        assert mock_repo.create.call_count == 1
        # update 至少调用 2 次：mark_processing + 保存 task_id
        assert mock_repo.update.call_count >= 2

    def test_start_clone_with_sync_result(self) -> None:
        """同步模式：CosyVoice 直接返回 voice_id，profile 变为 ready。"""
        mock_repo = MagicMock()
        mock_cosyvoice = MagicMock(spec=CosyVoiceService)

        mock_cosyvoice.submit_clone_task.return_value = {
            "task_id": "",
            "voice_id": "voice-sync-123",
            "request_id": "req-456",
        }

        mock_repo.create.side_effect = lambda p: p
        mock_repo.update.side_effect = lambda p: p

        service = _make_service(repo=mock_repo, cosyvoice=mock_cosyvoice)
        profile = service.start_clone(
            user_id="user-123",
            name="测试音色",
            source_audio_url="https://example.com/audio.wav",
        )

        assert profile.status == VoiceCloneStatus.READY
        assert profile.voice_id == "voice-sync-123"

    def test_start_clone_cosyvoice_error(self) -> None:
        """CosyVoice 提交失败，profile 标记为 failed。"""
        mock_repo = MagicMock()
        mock_cosyvoice = MagicMock(spec=CosyVoiceService)

        mock_cosyvoice.submit_clone_task.side_effect = CosyVoiceError("API 调用失败")
        mock_repo.create.side_effect = lambda p: p
        mock_repo.update.side_effect = lambda p: p

        service = _make_service(repo=mock_repo, cosyvoice=mock_cosyvoice)
        profile = service.start_clone(
            user_id="user-123",
            name="测试音色",
            source_audio_url="https://example.com/audio.wav",
        )

        assert profile.status == VoiceCloneStatus.FAILED
        assert "API 调用失败" in profile.error_message

    def test_start_clone_auth_error(self) -> None:
        """CosyVoice 认证失败，profile 标记为 failed。"""
        mock_repo = MagicMock()
        mock_cosyvoice = MagicMock(spec=CosyVoiceService)

        mock_cosyvoice.submit_clone_task.side_effect = CosyVoiceAuthError("认证失败")
        mock_repo.create.side_effect = lambda p: p
        mock_repo.update.side_effect = lambda p: p

        service = _make_service(repo=mock_repo, cosyvoice=mock_cosyvoice)
        profile = service.start_clone(
            user_id="user-123",
            name="测试音色",
            source_audio_url="https://example.com/audio.wav",
        )

        assert profile.status == VoiceCloneStatus.FAILED
        assert "认证失败" in profile.error_message

    def test_start_clone_without_audio_url(self) -> None:
        """没有音频 URL 时，profile 保持 pending 状态（P2-1 修复后）。"""
        mock_repo = MagicMock()
        mock_cosyvoice = MagicMock(spec=CosyVoiceService)

        mock_repo.create.side_effect = lambda p: p
        mock_repo.update.side_effect = lambda p: p

        service = _make_service(repo=mock_repo, cosyvoice=mock_cosyvoice)
        profile = service.start_clone(
            user_id="user-123",
            name="测试音色",
            source_audio_url="",
        )

        # P2-1: 没有音频 URL 时不标记 processing，保持 pending
        assert profile.status == VoiceCloneStatus.PENDING
        mock_cosyvoice.submit_clone_task.assert_not_called()


# ── process_clone_result ─────────────────────────────────


class TestProcessCloneResult:
    """测试 process_clone_result 方法。"""

    def test_process_clone_result_success(self) -> None:
        """克隆成功，profile 标记为 ready。"""
        mock_repo = MagicMock()
        profile = _make_profile(status=VoiceCloneStatus.PROCESSING)
        mock_repo.get.return_value = profile
        mock_repo.update.side_effect = lambda p: p

        service = _make_service(repo=mock_repo)
        result = service.process_clone_result(profile.id, "voice-xyz")

        assert result.status == VoiceCloneStatus.READY
        assert result.voice_id == "voice-xyz"

    def test_process_clone_result_not_found(self) -> None:
        """profile 不存在时抛出异常。"""
        mock_repo = MagicMock()
        mock_repo.get.return_value = None

        service = _make_service(repo=mock_repo)
        with pytest.raises(VoiceCloneNotFoundError):
            service.process_clone_result("nonexistent", "voice-xyz")


# ── process_clone_failure ────────────────────────────────


class TestProcessCloneFailure:
    """测试 process_clone_failure 方法。"""

    def test_process_clone_failure(self) -> None:
        """克隆失败，profile 标记为 failed。"""
        mock_repo = MagicMock()
        profile = _make_profile(status=VoiceCloneStatus.PROCESSING)
        mock_repo.get.return_value = profile
        mock_repo.update.side_effect = lambda p: p

        service = _make_service(repo=mock_repo)
        result = service.process_clone_failure(profile.id, "超时错误")

        assert result.status == VoiceCloneStatus.FAILED
        assert result.error_message == "超时错误"

    def test_process_clone_failure_not_found(self) -> None:
        """profile 不存在时抛出异常。"""
        mock_repo = MagicMock()
        mock_repo.get.return_value = None

        service = _make_service(repo=mock_repo)
        with pytest.raises(VoiceCloneNotFoundError):
            service.process_clone_failure("nonexistent", "错误")


# ── retry_clone ──────────────────────────────────────────


class TestRetryClone:
    """测试 retry_clone 方法。"""

    def test_retry_clone_with_async_task(self) -> None:
        """重试成功，异步模式。"""
        mock_repo = MagicMock()
        mock_cosyvoice = MagicMock(spec=CosyVoiceService)

        profile = _make_profile(status=VoiceCloneStatus.FAILED, retry_count=1, max_retries=3)
        mock_repo.get.return_value = profile
        mock_repo.update.side_effect = lambda p: p

        mock_cosyvoice.submit_clone_task.return_value = {
            "task_id": "task-retry",
            "voice_id": "",
            "request_id": "req-retry",
        }

        service = _make_service(repo=mock_repo, cosyvoice=mock_cosyvoice)
        result = service.retry_clone(profile.id, "user-123")

        assert result.status == VoiceCloneStatus.PROCESSING
        assert result.metadata["cosyvoice_task_id"] == "task-retry"
        assert result.retry_count == 2  # prepare_retry 增加了一次

    def test_retry_clone_with_sync_result(self) -> None:
        """重试成功，同步模式。"""
        mock_repo = MagicMock()
        mock_cosyvoice = MagicMock(spec=CosyVoiceService)

        profile = _make_profile(status=VoiceCloneStatus.FAILED, retry_count=0, max_retries=3)
        mock_repo.get.return_value = profile
        mock_repo.update.side_effect = lambda p: p

        mock_cosyvoice.submit_clone_task.return_value = {
            "task_id": "",
            "voice_id": "voice-retry-sync",
            "request_id": "req-retry",
        }

        service = _make_service(repo=mock_repo, cosyvoice=mock_cosyvoice)
        result = service.retry_clone(profile.id, "user-123")

        assert result.status == VoiceCloneStatus.READY
        assert result.voice_id == "voice-retry-sync"

    def test_retry_clone_not_found(self) -> None:
        """profile 不存在时抛出异常。"""
        mock_repo = MagicMock()
        mock_repo.get.return_value = None

        service = _make_service(repo=mock_repo)
        with pytest.raises(VoiceCloneNotFoundError):
            service.retry_clone("nonexistent", "user-123")

    def test_retry_clone_not_retryable(self) -> None:
        """不可重试时抛出异常。"""
        mock_repo = MagicMock()
        profile = _make_profile(status=VoiceCloneStatus.PROCESSING)
        mock_repo.get.return_value = profile

        service = _make_service(repo=mock_repo)
        with pytest.raises(VoiceCloneNotRetryableError):
            service.retry_clone(profile.id, "user-123")

    def test_retry_clone_cosyvoice_error(self) -> None:
        """重试时 CosyVoice 失败，profile 标记为 failed。"""
        mock_repo = MagicMock()
        mock_cosyvoice = MagicMock(spec=CosyVoiceService)

        profile = _make_profile(status=VoiceCloneStatus.FAILED, retry_count=0, max_retries=3)
        mock_repo.get.return_value = profile
        mock_repo.update.side_effect = lambda p: p

        mock_cosyvoice.submit_clone_task.side_effect = CosyVoiceError("重试失败")

        service = _make_service(repo=mock_repo, cosyvoice=mock_cosyvoice)
        result = service.retry_clone(profile.id, "user-123")

        assert result.status == VoiceCloneStatus.FAILED
        assert "重试失败" in result.error_message


# ── poll_and_process_clone ───────────────────────────────


class TestPollAndProcessClone:
    """测试 poll_and_process_clone 方法（P2-2 修复）。"""

    def test_poll_and_process_clone_success(self) -> None:
        """轮询成功：调用 poll_clone_task → process_clone_result。"""
        mock_repo = MagicMock()
        mock_cosyvoice = MagicMock(spec=CosyVoiceService)

        profile = _make_profile(
            status=VoiceCloneStatus.PROCESSING,
            metadata={"cosyvoice_task_id": "task-abc"},
        )
        mock_repo.get.return_value = profile
        mock_repo.update.side_effect = lambda p: p

        mock_cosyvoice.poll_clone_task.return_value = {"voice_id": "voice-poll-xyz"}

        service = _make_service(repo=mock_repo, cosyvoice=mock_cosyvoice)
        result = service.poll_and_process_clone(profile.id)

        assert result.status == VoiceCloneStatus.READY
        assert result.voice_id == "voice-poll-xyz"
        mock_cosyvoice.poll_clone_task.assert_called_once_with("task-abc", timeout=300)

    def test_poll_and_process_clone_no_task_id(self) -> None:
        """metadata 中没有 task_id 时抛出 ValueError。"""
        mock_repo = MagicMock()
        mock_cosyvoice = MagicMock(spec=CosyVoiceService)

        profile = _make_profile(status=VoiceCloneStatus.PROCESSING, metadata={})
        mock_repo.get.return_value = profile

        service = _make_service(repo=mock_repo, cosyvoice=mock_cosyvoice)
        with pytest.raises(ValueError, match="cosyvoice_task_id"):
            service.poll_and_process_clone(profile.id)

    def test_poll_and_process_clone_not_found(self) -> None:
        """profile 不存在时抛出 VoiceCloneNotFoundError。"""
        mock_repo = MagicMock()
        mock_repo.get.return_value = None

        service = _make_service(repo=mock_repo)
        with pytest.raises(VoiceCloneNotFoundError):
            service.poll_and_process_clone("nonexistent")

    def test_poll_and_process_clone_timeout(self) -> None:
        """超时时透传 CosyVoiceTimeoutError（由 Celery task 捕获重试）。"""
        from packages.application.cosyvoice_service import CosyVoiceTimeoutError

        mock_repo = MagicMock()
        mock_cosyvoice = MagicMock(spec=CosyVoiceService)

        profile = _make_profile(
            status=VoiceCloneStatus.PROCESSING,
            metadata={"cosyvoice_task_id": "task-abc"},
        )
        mock_repo.get.return_value = profile

        mock_cosyvoice.poll_clone_task.side_effect = CosyVoiceTimeoutError("超时")

        service = _make_service(repo=mock_repo, cosyvoice=mock_cosyvoice)
        with pytest.raises(CosyVoiceTimeoutError):
            service.poll_and_process_clone(profile.id)
