import os
import sys
import asyncio
import types
import pytest
from pathlib import Path
from unittest.mock import MagicMock, patch

BACKEND_DIR = Path(__file__).resolve().parents[1]
if str(BACKEND_DIR) not in sys.path:
    sys.path.insert(0, str(BACKEND_DIR))


def _make_mlx_stub():
    """Create minimal MLX stub for testing without Apple Silicon"""
    mlx = types.SimpleNamespace()
    mlx.core = types.SimpleNamespace()
    mx_array = type('mx.array', (), {'item': lambda self: 1})
    mlx.core.array = mx_array
    mlx.nn = types.SimpleNamespace()
    return mlx


def _make_mlx_audio_stub():
    """Create minimal mlx-audio stub"""
    stt = types.SimpleNamespace()
    stt.utils = types.SimpleNamespace()

    def mock_load(path, **kwargs):
        model = MagicMock()
        return model

    stt.utils.load = mock_load  # type: ignore

    qwen3_asr_mod = types.SimpleNamespace()
    qwen3_asr_mod.Qwen3ASRModel = type('Qwen3ASRModel', (), {})
    qwen3_asr_mod.ForcedAlignerModel = type('ForcedAlignerModel', (), {})
    stt.models = types.SimpleNamespace()  # type: ignore
    stt.models.qwen3_asr = qwen3_asr_mod  # type: ignore

    audio = types.SimpleNamespace()
    audio.stt = stt  # type: ignore
    return audio


def _reload_tts_asr_with_mocks():
    """Reload tts_asr with mocked MLX dependencies"""
    for mod_name in list(sys.modules.keys()):
        if 'tts_asr' in mod_name or 'mlx' in mod_name:
            del sys.modules[mod_name]

    mlx_stub = _make_mlx_stub()
    sys.modules['mlx'] = mlx_stub  # type: ignore
    sys.modules['mlx.core'] = mlx_stub.core  # type: ignore
    sys.modules['mlx.nn'] = mlx_stub.nn  # type: ignore

    audio_stub = _make_mlx_audio_stub()
    sys.modules['mlx-audio'] = audio_stub  # type: ignore
    sys.modules['mlx_audio'] = audio_stub  # type: ignore
    sys.modules['mlx_audio.stt'] = audio_stub.stt  # type: ignore
    sys.modules['mlx_audio.stt.utils'] = audio_stub.stt.utils  # type: ignore
    sys.modules['mlx_audio.stt.models'] = audio_stub.stt.models  # type: ignore
    sys.modules['mlx_audio.stt.models.qwen3_asr'] = audio_stub.stt.models.qwen3_asr  # type: ignore

    import tts_asr
    return tts_asr


@pytest.fixture(autouse=True)
def _clean_env():
    """Clean ASR-related env vars before/after each test"""
    saved = {}
    for k in ['HF_ENDPOINT']:
        saved[k] = os.environ.get(k)
        if k in os.environ:
            del os.environ[k]
    yield
    for k, v in saved.items():
        if v is not None:
            os.environ[k] = v  # type: ignore (unused var)


class TestRequestResponseModels:
    """Pydantic 数据模型测试"""

    def test_tts_request_defaults(self):
        tts = _reload_tts_asr_with_mocks()
        req = tts.TTSRequest(text="hello")
        assert req.text == "hello"
        assert req.speaker == "Vivian"

    def test_asr_request_defaults(self):
        tts = _reload_tts_asr_with_mocks()
        req = tts.ASRRequest(audio_base64="dGVzdA==")
        assert req.audio_base64 == "dGVzdA=="
        assert req.language == "zh-CN"

    def test_asr_request_custom_language(self):
        tts = _reload_tts_asr_with_mocks()
        req = tts.ASRRequest(audio_base64="dGVzdA==", language="en")
        assert req.language == "en"

    def test_model_status_defaults(self):
        tts = _reload_tts_asr_with_mocks()
        status = tts.ModelStatus(tts_loaded=False, asr_loaded=True, device="cpu")
        assert not status.tts_loaded
        assert status.asr_loaded


class TestDeviceDetection:
    """设备检测测试"""

    def test_device_map_returns_string(self):
        tts = _reload_tts_asr_with_mocks()
        device = tts._get_device_map()
        assert isinstance(device, str)


class TestModelLoading:
    """模型加载测试"""

    def test_load_asr_skips_when_mlx_unavailable(self):
        """mlx_audio 未安装时应跳过 ASR"""
        for mod_name in list(sys.modules.keys()):
            if 'tts_asr' in mod_name or 'mlx' in mod_name:
                del sys.modules[mod_name]

        # Don't inject mlx stubs — simulate missing MLX
        import tts_asr  # noqa: F811

        assert tts_asr.Qwen3ASRModel is None
        tts_asr._load_asr_models()  # should not crash
        assert tts_asr._asr_model is None

    def test_load_asr_from_path_success(self):
        tts = _reload_tts_asr_with_mocks()
        # Mock snapshot_download to return a path, mock stt_load to succeed
        with patch('backend.tts_asr.snapshot_download', return_value='/fake/path'):  # type: ignore
            tts._load_asr_from_path('/fake/path')

        assert tts._asr_model is not None  # type: ignore (MagicMock)


class TestWarmupFunctions:
    """预热函数测试"""

    def test_warmup_functions_callable(self):
        tts = _reload_tts_asr_with_mocks()
        assert callable(tts._warmup_tts)  # type: ignore (unused var)
        assert callable(tts._warmup_all)

    def test_warmup_asr_skips_when_mlx_unavailable(self):
        for mod_name in list(sys.modules.keys()):
            if 'tts_asr' in mod_name or 'mlx' in mod_name:
                del sys.modules[mod_name]

        import tts_asr  # noqa: F811
        assert tts_asr.Qwen3ASRModel is None

    def test_warmup_all_runs_without_error(self):
        tts = _reload_tts_asr_with_mocks()

        # Set global models so warmup returns immediately without actual loading
        tts._tts_model = MagicMock()

        async def run():  # type: ignore (unused var)
            await tts._warmup_all()

        asyncio.get_event_loop().run_until_complete(run())  # type: ignore


class TestRouteRegistration:
    """路由注册测试"""

    def test_register_function_exists(self):
        tts = _reload_tts_asr_with_mocks()
        assert callable(tts.register_tts_asr_routes)

    def test_router_prefix(self):
        tts = _reload_tts_asr_with_mocks()
        assert hasattr(tts.router, 'routes')


class TestModelConstants:
    """模型常量测试"""

    def test_asr_model_id(self):
        tts = _reload_tts_asr_with_mocks()
        assert 'Qwen3-ASR' in tts.ASR_MODEL_ID_MS

    def test_align_model_id(self):
        tts = _reload_tts_asr_with_mocks()
        assert 'ForcedAligner' in tts.ALIGN_MODEL_ID_MS
