"""Tests for POST /tts/preview endpoint."""

from __future__ import annotations

from dataclasses import dataclass
from unittest.mock import MagicMock, patch

import pytest
from pydantic import ValidationError


@dataclass
class FakeSynthesizeResult:
    audio_url: str
    duration: float = 0.0
    file_size: int = 0
    request_id: str = ""


class TestTTSPreviewEndpoint:
    """Integration-style tests for the /tts/preview route."""

    def _make_client(self, app):
        from fastapi.testclient import TestClient

        return TestClient(app)

    def test_schema_preview_request_validation(self):
        """TTSPreviewRequest rejects text > 200 chars and empty voice_id."""
        from app.schemas.tts import TTSPreviewRequest

        # Valid
        req = TTSPreviewRequest(text="hello", voice_id="v1")
        assert req.text == "hello"
        assert req.voice_id == "v1"
        assert req.speed == 1.0

        # Empty voice_id rejected
        with pytest.raises(ValidationError):
            TTSPreviewRequest(text="hello", voice_id="")

        # Text > 200 chars rejected
        with pytest.raises(ValidationError):
            TTSPreviewRequest(text="a" * 201, voice_id="v1")

    def test_schema_preview_response(self):
        """TTSPreviewResponse serialization."""
        from app.schemas.tts import TTSPreviewResponse

        resp = TTSPreviewResponse(audio_url="https://example.com/audio.mp3")
        assert resp.audio_url == "https://example.com/audio.mp3"
        assert resp.duration is None

        resp2 = TTSPreviewResponse(audio_url="https://x.com/a.mp3", duration=3.5)
        assert resp2.duration == 3.5

    def test_preview_success(self):
        """Successful preview returns audio_url."""
        from app.schemas.tts import TTSPreviewRequest
        from fastapi import FastAPI

        app = FastAPI()

        # We need to register the route with proper dependencies
        from app.api.routes.tts import router

        app.include_router(router, prefix="/tts")

        # Override dependencies
        from app.auth import get_current_user
        from app.dependencies import get_cosyvoice_service

        fake_user = MagicMock()
        fake_user.user.id = "user-1"
        app.dependency_overrides[get_current_user] = lambda: fake_user

        mock_service = MagicMock()
        mock_service.synthesize_speech.return_value = FakeSynthesizeResult(
            audio_url="https://cosyvoice.example.com/audio.mp3",
            duration=2.5,
        )
        app.dependency_overrides[get_cosyvoice_service] = lambda: mock_service

        from app.dependencies import get_voice_clone_profile_repository

        mock_clone_repo = MagicMock()
        mock_clone_repo.get.return_value = None  # no profile found = preset voice
        app.dependency_overrides[get_voice_clone_profile_repository] = lambda: mock_clone_repo

        client = self._make_client(app)
        resp = client.post(
            "/tts/preview",
            json={"text": "你好世界", "voice_id": "longxiaochun"},
        )
        assert resp.status_code == 200
        data = resp.json()
        assert data["audio_url"] == "https://cosyvoice.example.com/audio.mp3"
        assert data["duration"] == 2.5

        mock_service.synthesize_speech.assert_called_once_with(
            text="你好世界",
            voice_id="longxiaochun",
            speed=1.0,
        )

    def test_preview_with_speed(self):
        """Custom speed is passed through to CosyVoice."""
        from fastapi import FastAPI

        app = FastAPI()
        from app.api.routes.tts import router

        app.include_router(router, prefix="/tts")

        from app.auth import get_current_user
        from app.dependencies import get_cosyvoice_service

        fake_user = MagicMock()
        fake_user.user.id = "user-1"
        app.dependency_overrides[get_current_user] = lambda: fake_user

        mock_service = MagicMock()
        mock_service.synthesize_speech.return_value = FakeSynthesizeResult(
            audio_url="https://x.com/a.mp3",
            duration=0.0,
        )
        app.dependency_overrides[get_cosyvoice_service] = lambda: mock_service

        from app.dependencies import get_voice_clone_profile_repository

        mock_clone_repo = MagicMock()
        mock_clone_repo.get.return_value = None  # no profile found = preset voice
        app.dependency_overrides[get_voice_clone_profile_repository] = lambda: mock_clone_repo

        client = self._make_client(app)
        resp = client.post(
            "/tts/preview",
            json={"text": "测试", "voice_id": "v1", "speed": 1.5},
        )
        assert resp.status_code == 200
        data = resp.json()
        assert data["audio_url"] == "https://x.com/a.mp3"
        assert data["duration"] is None  # 0.0 -> None

        mock_service.synthesize_speech.assert_called_once_with(
            text="测试",
            voice_id="v1",
            speed=1.5,
        )

    def test_preview_cosyvoice_error_returns_502(self):
        """CosyVoice failure returns 502."""
        from fastapi import FastAPI

        from packages.application.cosyvoice_service import CosyVoiceError

        app = FastAPI()
        from app.api.routes.tts import router

        app.include_router(router, prefix="/tts")

        from app.auth import get_current_user
        from app.dependencies import get_cosyvoice_service

        fake_user = MagicMock()
        fake_user.user.id = "user-1"
        app.dependency_overrides[get_current_user] = lambda: fake_user

        mock_service = MagicMock()
        mock_service.synthesize_speech.side_effect = CosyVoiceError("API timeout")
        app.dependency_overrides[get_cosyvoice_service] = lambda: mock_service

        from app.dependencies import get_voice_clone_profile_repository

        mock_clone_repo = MagicMock()
        mock_clone_repo.get.return_value = None  # no profile found = preset voice
        app.dependency_overrides[get_voice_clone_profile_repository] = lambda: mock_clone_repo

        client = self._make_client(app)
        resp = client.post(
            "/tts/preview",
            json={"text": "测试", "voice_id": "v1"},
        )
        assert resp.status_code == 502
        assert "TTS 合成失败" in resp.json()["detail"]

    def test_preview_value_error_returns_400(self):
        """Invalid params return 400."""
        from fastapi import FastAPI

        app = FastAPI()
        from app.api.routes.tts import router

        app.include_router(router, prefix="/tts")

        from app.auth import get_current_user
        from app.dependencies import get_cosyvoice_service

        fake_user = MagicMock()
        fake_user.user.id = "user-1"
        app.dependency_overrides[get_current_user] = lambda: fake_user

        mock_service = MagicMock()
        mock_service.synthesize_speech.side_effect = ValueError("text 不能为空")
        app.dependency_overrides[get_cosyvoice_service] = lambda: mock_service

        from app.dependencies import get_voice_clone_profile_repository

        mock_clone_repo = MagicMock()
        mock_clone_repo.get.return_value = None  # no profile found = preset voice
        app.dependency_overrides[get_voice_clone_profile_repository] = lambda: mock_clone_repo

        client = self._make_client(app)
        resp = client.post(
            "/tts/preview",
            json={"text": "测试", "voice_id": "v1"},
        )
        assert resp.status_code == 400
        assert "text 不能为空" in resp.json()["detail"]

    def test_preview_text_too_long_returns_422(self):
        """Text > 200 chars is rejected by Pydantic validation."""
        from fastapi import FastAPI

        app = FastAPI()
        from app.api.routes.tts import router

        app.include_router(router, prefix="/tts")

        from app.auth import get_current_user
        from app.dependencies import get_cosyvoice_service

        fake_user = MagicMock()
        fake_user.user.id = "user-1"
        app.dependency_overrides[get_current_user] = lambda: fake_user

        mock_service = MagicMock()
        app.dependency_overrides[get_cosyvoice_service] = lambda: mock_service

        from app.dependencies import get_voice_clone_profile_repository

        mock_clone_repo = MagicMock()
        mock_clone_repo.get.return_value = None  # no profile found = preset voice
        app.dependency_overrides[get_voice_clone_profile_repository] = lambda: mock_clone_repo

        client = self._make_client(app)
        resp = client.post(
            "/tts/preview",
            json={"text": "a" * 201, "voice_id": "v1"},
        )
        assert resp.status_code == 422

    def test_preview_empty_voice_id_returns_422(self):
        """Empty voice_id is rejected by Pydantic validation."""
        from fastapi import FastAPI

        app = FastAPI()
        from app.api.routes.tts import router

        app.include_router(router, prefix="/tts")

        from app.auth import get_current_user
        from app.dependencies import get_cosyvoice_service

        fake_user = MagicMock()
        fake_user.user.id = "user-1"
        app.dependency_overrides[get_current_user] = lambda: fake_user

        mock_service = MagicMock()
        app.dependency_overrides[get_cosyvoice_service] = lambda: mock_service

        from app.dependencies import get_voice_clone_profile_repository

        mock_clone_repo = MagicMock()
        mock_clone_repo.get.return_value = None  # no profile found = preset voice
        app.dependency_overrides[get_voice_clone_profile_repository] = lambda: mock_clone_repo

        client = self._make_client(app)
        resp = client.post(
            "/tts/preview",
            json={"text": "hello", "voice_id": ""},
        )
        assert resp.status_code == 422

    def test_preview_clone_voice_resolves_to_cosyvoice_id(self):
        """Clone voice UUID is resolved to CosyVoice voice_id."""
        from dataclasses import dataclass

        from fastapi import FastAPI

        app = FastAPI()
        from app.api.routes.tts import router

        app.include_router(router, prefix="/tts")

        from app.auth import get_current_user
        from app.dependencies import get_cosyvoice_service, get_voice_clone_profile_repository

        fake_user = MagicMock()
        fake_user.user.id = "user-1"
        app.dependency_overrides[get_current_user] = lambda: fake_user

        mock_service = MagicMock()
        mock_service.synthesize_speech.return_value = FakeSynthesizeResult(
            audio_url="https://x.com/cloned.mp3",
            duration=1.8,
        )
        app.dependency_overrides[get_cosyvoice_service] = lambda: mock_service

        # Mock voice clone profile with voice_id
        mock_profile = MagicMock()
        mock_profile.user_id = "user-1"
        mock_profile.voice_id = "cosyvoice_actual_voice_123"
        mock_clone_repo = MagicMock()
        mock_clone_repo.get.return_value = mock_profile
        app.dependency_overrides[get_voice_clone_profile_repository] = lambda: mock_clone_repo

        client = self._make_client(app)
        # Frontend sends the profile UUID as voice_id
        resp = client.post(
            "/tts/preview",
            json={"text": "克隆音色测试", "voice_id": "abc123-uuid-of-profile"},
        )
        assert resp.status_code == 200
        data = resp.json()
        assert data["audio_url"] == "https://x.com/cloned.mp3"

        # Verify CosyVoice was called with the resolved voice_id, not the UUID
        mock_service.synthesize_speech.assert_called_once_with(
            text="克隆音色测试",
            voice_id="cosyvoice_actual_voice_123",
            speed=1.0,
        )
        # Verify repo was queried with the UUID
        mock_clone_repo.get.assert_called_once_with("abc123-uuid-of-profile")

    def test_preview_clone_voice_incomplete_returns_400(self):
        """Clone profile with empty voice_id returns 400."""
        from fastapi import FastAPI

        app = FastAPI()
        from app.api.routes.tts import router

        app.include_router(router, prefix="/tts")

        from app.auth import get_current_user
        from app.dependencies import get_voice_clone_profile_repository

        fake_user = MagicMock()
        fake_user.user.id = "user-1"
        app.dependency_overrides[get_current_user] = lambda: fake_user

        # Mock voice clone profile with empty voice_id (clone not finished)
        mock_profile = MagicMock()
        mock_profile.user_id = "user-1"
        mock_profile.voice_id = ""
        mock_clone_repo = MagicMock()
        mock_clone_repo.get.return_value = mock_profile
        app.dependency_overrides[get_voice_clone_profile_repository] = lambda: mock_clone_repo

        client = self._make_client(app)
        resp = client.post(
            "/tts/preview",
            json={"text": "测试未完成克隆", "voice_id": "abc123-uuid"},
        )
        assert resp.status_code == 400
        assert "音色克隆尚未完成" in resp.json()["detail"]

    def test_preview_preset_voice_passthrough(self):
        """Preset voice ID (not a profile UUID) passes through unchanged."""
        from fastapi import FastAPI

        app = FastAPI()
        from app.api.routes.tts import router

        app.include_router(router, prefix="/tts")

        from app.auth import get_current_user
        from app.dependencies import get_cosyvoice_service, get_voice_clone_profile_repository

        fake_user = MagicMock()
        fake_user.user.id = "user-1"
        app.dependency_overrides[get_current_user] = lambda: fake_user

        mock_service = MagicMock()
        mock_service.synthesize_speech.return_value = FakeSynthesizeResult(
            audio_url="https://x.com/preset.mp3",
            duration=2.0,
        )
        app.dependency_overrides[get_cosyvoice_service] = lambda: mock_service

        # Mock repo returns None (preset voice, not a clone profile)
        mock_clone_repo = MagicMock()
        mock_clone_repo.get.return_value = None
        app.dependency_overrides[get_voice_clone_profile_repository] = lambda: mock_clone_repo

        client = self._make_client(app)
        resp = client.post(
            "/tts/preview",
            json={"text": "预设音色测试", "voice_id": "longxiaoxia_v3"},
        )
        assert resp.status_code == 200

        # Verify CosyVoice was called with the original preset voice_id
        mock_service.synthesize_speech.assert_called_once_with(
            text="预设音色测试",
            voice_id="longxiaoxia_v3",
            speed=1.0,
        )

    def test_preview_clone_voice_wrong_user_returns_403(self):
        """Accessing another user's clone profile returns 403."""
        from fastapi import FastAPI

        app = FastAPI()
        from app.api.routes.tts import router

        app.include_router(router, prefix="/tts")

        from app.auth import get_current_user
        from app.dependencies import get_voice_clone_profile_repository

        fake_user = MagicMock()
        fake_user.user.id = "user-1"
        app.dependency_overrides[get_current_user] = lambda: fake_user

        # Mock profile belonging to a different user
        mock_profile = MagicMock()
        mock_profile.user_id = "user-2"
        mock_profile.voice_id = "cosyvoice_voice_xyz"
        mock_clone_repo = MagicMock()
        mock_clone_repo.get.return_value = mock_profile
        app.dependency_overrides[get_voice_clone_profile_repository] = lambda: mock_clone_repo

        client = self._make_client(app)
        resp = client.post(
            "/tts/preview",
            json={"text": "越权测试", "voice_id": "other-user-profile-uuid"},
        )
        assert resp.status_code == 403
        assert "无权访问该音色" in resp.json()["detail"]
