"""Tests for the shared LLM speech adapter and speech job handlers."""

from __future__ import annotations

import asyncio
import base64
import json
import tempfile
from pathlib import Path

import httpx
import pytest
from fastapi import FastAPI
from fastapi.testclient import TestClient

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

import job_handlers  # noqa: E402
import tts_asr  # noqa: E402
from audit_store import BaseAuditStore  # noqa: E402


def _wav_bytes(duration_ms: int = 100) -> bytes:
    sample_rate = 16000
    frames = max(1, int(sample_rate * duration_ms / 1000))
    data = b"".join((i % 32768).to_bytes(2, "little", signed=False) for i in range(frames))
    data_size = len(data)
    return (
        b"RIFF" + (36 + data_size).to_bytes(4, "little")
        + b"WAVE"
        + b"fmt " + (16).to_bytes(4, "little")
        + (1).to_bytes(2, "little")
        + (1).to_bytes(2, "little")
        + sample_rate.to_bytes(4, "little")
        + sample_rate.to_bytes(4, "little")
        + (2).to_bytes(2, "little")
        + (16).to_bytes(2, "little")
        + b"data" + data_size.to_bytes(4, "little")
        + data
    )


def _run_async(coro):
    return asyncio.run(coro)


class _CaptureAuditStore(BaseAuditStore):
    def __init__(self) -> None:
        self.llm_calls: list[dict] = []

    def record_llm_call(self, payload: dict) -> None:
        self.llm_calls.append(payload)


def test_tts_calls_shared_llm_speech_endpoint(monkeypatch):
    captured: dict[str, object] = {}
    monkeypatch.setattr(tts_asr, "LLM_API_KEY", "test-api-key")
    monkeypatch.setattr(tts_asr, "TTS_MODEL_ID", "Qwen3-TTS-12Hz-1.7B-VoiceDesign-8bit")

    def transport(request: httpx.Request):
        captured["url"] = str(request.url)
        captured["headers"] = dict(request.headers)
        captured["json"] = json.loads(request.read().decode("utf-8"))
        return httpx.Response(200, content=b"speech-ok", headers={"x-request-id": "tts-req-1"})

    async def run():
        client = httpx.AsyncClient(
            base_url="https://speech.example/v1",
            transport=httpx.MockTransport(transport),
        )
        try:
            tts_asr._httpx_client = client
            return await tts_asr.generate_tts_response(
                "你好世界",
                instruct="A warm Mandarin voice.",
                speaker="Vivian",
                output_format="wav",
            )
        finally:
            await client.aclose()
            tts_asr._httpx_client = None

    result = _run_async(run())

    assert result["format"] == "wav"
    assert result["speaker"] == "Vivian"
    assert result["model"] == "Qwen3-TTS-12Hz-1.7B-VoiceDesign-8bit"
    assert result["upstream_request_id"] == "tts-req-1"
    assert base64.b64decode(result["audio_base64"]) == b"speech-ok"
    assert captured["url"] == "https://speech.example/v1/audio/speech"
    assert captured["headers"]["authorization"] == "Bearer test-api-key"
    payload = captured["json"]
    assert payload["model"] == "Qwen3-TTS-12Hz-1.7B-VoiceDesign-8bit"
    assert payload["voice"] == "Vivian"
    assert payload["input"] == "你好世界"
    assert payload["instructions"] == "A warm Mandarin voice."
    assert "instruction" not in payload


def test_tts_uses_nonempty_default_instructions(monkeypatch):
    captured: dict[str, object] = {}

    def transport(request: httpx.Request):
        captured["json"] = json.loads(request.read().decode("utf-8"))
        return httpx.Response(200, content=b"speech-ok")

    async def run():
        client = httpx.AsyncClient(
            base_url="https://speech.example/v1",
            transport=httpx.MockTransport(transport),
        )
        try:
            tts_asr._httpx_client = client
            return await tts_asr.generate_tts_response("你好世界")
        finally:
            await client.aclose()
            tts_asr._httpx_client = None

    _run_async(run())

    payload = captured["json"]
    assert payload["instructions"] == tts_asr.DEFAULT_TTS_INSTRUCTIONS
    assert payload["instructions"].strip()


def test_asr_calls_shared_llm_transcriptions_endpoint(monkeypatch):
    captured: dict[str, object] = {}
    monkeypatch.setattr(tts_asr, "LLM_API_KEY", "test-api-key")
    monkeypatch.setattr(tts_asr, "ASR_MODEL_ID", "Qwen3-ASR-0.6B-8bit")

    def transport(request: httpx.Request):
        captured["url"] = str(request.url)
        captured["headers"] = dict(request.headers)
        captured["content"] = request.read()
        return httpx.Response(200, json={"text": "hello world", "language": "zh"}, headers={"x-request-id": "asr-req-1"})

    async def run():
        client = httpx.AsyncClient(
            base_url="https://speech.example/v1",
            transport=httpx.MockTransport(transport),
        )
        try:
            tts_asr._httpx_client = client
            return await tts_asr.generate_asr_response(_wav_bytes(), language="zh-CN")
        finally:
            await client.aclose()
            tts_asr._httpx_client = None

    result = _run_async(run())

    assert result["text"] == "hello world"
    assert result["language"] == "zh"
    assert result["model"] == "Qwen3-ASR-0.6B-8bit"
    assert result["upstream_request_id"] == "asr-req-1"
    assert captured["url"] == "https://speech.example/v1/audio/transcriptions"
    assert captured["headers"]["authorization"] == "Bearer test-api-key"
    content = captured["content"]
    assert b'name="model"' in content
    assert b"Qwen3-ASR-0.6B-8bit" in content
    assert b'name="language"' in content
    assert b"zh" in content


def test_invalid_tts_text_returns_http_exception():
    with pytest.raises(tts_asr.HTTPException) as exc:
        _run_async(tts_asr._call_tts_api("", speaker="Vivian"))
    assert exc.value.status_code == 400


def test_invalid_asr_audio_returns_http_exception():
    with pytest.raises(tts_asr.HTTPException) as exc:
        _run_async(tts_asr._call_asr_api(b"", language="zh-CN"))
    assert exc.value.status_code == 400


def test_status_config_routes(monkeypatch):
    app = FastAPI()
    app.include_router(tts_asr.meta_router)
    monkeypatch.setattr(tts_asr, "LLM_BASE_URL", "https://speech.example/v1")
    monkeypatch.setattr(tts_asr, "LLM_API_KEY", "")
    monkeypatch.setattr(tts_asr, "TTS_MODEL_ID", "Qwen3-TTS-12Hz-1.7B-VoiceDesign-8bit")
    monkeypatch.setattr(tts_asr, "ASR_MODEL_ID", "Qwen3-ASR-0.6B-8bit")

    with TestClient(app) as client:
        status = client.get("/status")
        config = client.get("/config")

    assert status.status_code == 200
    assert config.status_code == 200
    assert status.json()["llm_url"] == "https://speech.example/v1"
    assert status.json()["tts_model"] == "Qwen3-TTS-12Hz-1.7B-VoiceDesign-8bit"
    assert status.json()["asr_model"] == "Qwen3-ASR-0.6B-8bit"
    assert status.json()["status"]["api_key_configured"] is False
    assert status.json()["status"]["max_connections"] == tts_asr.SPEECH_MAX_CONNECTIONS


def test_tts_concurrent_requests_respect_connection_limit(monkeypatch):
    monkeypatch.setattr(tts_asr, "SPEECH_MAX_CONNECTIONS", 4)
    monkeypatch.setattr(tts_asr, "SPEECH_MAX_KEEPALIVE_CONNECTIONS", 1)

    class LimitedClient:
        def __init__(self):
            self.semaphore = asyncio.Semaphore(4)
            self.active = 0
            self.max_active = 0

        async def post(self, url: str, **kwargs):
            async with self.semaphore:
                self.active += 1
                self.max_active = max(self.max_active, self.active)
                await asyncio.sleep(0.01)
                self.active -= 1
                return httpx.Response(200, content=b"speech-ok", request=httpx.Request("POST", f"https://speech.example{url}"))

    async def run():
        client = LimitedClient()

        async def get_client():
            return client

        monkeypatch.setattr(tts_asr, "_get_speech_client", get_client)
        await asyncio.gather(*(tts_asr.generate_tts_response(f"文本 {index}") for index in range(20)))
        return client

    client = _run_async(run())
    assert client.max_active <= 4


def test_tts_asr_handlers_record_audit(monkeypatch):
    audit_store = _CaptureAuditStore()

    async def fake_tts(*args, **kwargs):
        return {
            "audio_base64": base64.b64encode(b"ok").decode("utf-8"),
            "format": "wav",
            "duration_ms": 1200,
            "audio_bytes": 2,
            "text_chars": 2,
            "speaker": "Vivian",
            "model": "Qwen3-TTS-12Hz-1.7B-VoiceDesign-8bit",
            "request_ms": 45,
            "upstream_request_id": "tts-upstream",
        }

    async def fake_asr(*args, **kwargs):
        return {
            "text": "hello world",
            "language": "zh",
            "audio_bytes": len(_wav_bytes()),
            "model": "Qwen3-ASR-0.6B-8bit",
            "request_ms": 80,
            "upstream_request_id": "asr-upstream",
        }

    monkeypatch.setattr(job_handlers, "generate_tts_response", fake_tts)
    monkeypatch.setattr(job_handlers, "generate_asr_response", fake_asr)
    monkeypatch.setattr(job_handlers, "get_audit_store", lambda *_args, **_kwargs: audit_store)

    base_payload = {
        "request_id": "req-1",
        "risk": {
            "request_id": "req-1",
            "session_hash": "session",
            "ip_hash": "ip",
            "estimated_input_tokens": 12,
            "estimated_cost": 0.0,
            "policy": {
                "job_type": "tts",
                "model": "Qwen3-TTS-12Hz-1.7B-VoiceDesign-8bit",
                "profile": "speech_tts",
                "max_output_tokens": 0,
            },
        },
        "job_context": {
            "created_at": 1000,
            "started_at": 1200,
            "queue_ms": 200,
        },
    }

    async def run():
        events = []

        async def emit(event: str, data: dict):
            events.append((event, data))

        tts_payload = {
            **base_payload,
            "text": "你好",
            "speaker": "Vivian",
            "format": "wav",
        }
        await job_handlers.tts_handler(tts_payload, emit, lambda: False)

        with tempfile.NamedTemporaryFile(suffix=".wav", delete=False) as handle:
            handle.write(_wav_bytes())
            audio_path = handle.name

        try:
            asr_payload = {
                **base_payload,
                "risk": {
                    **base_payload["risk"],
                    "policy": {
                        "job_type": "asr",
                        "model": "Qwen3-ASR-0.6B-8bit",
                        "profile": "speech_asr",
                        "max_output_tokens": 0,
                    },
                },
                "input_path": audio_path,
                "language": "zh-CN",
            }
            await job_handlers.asr_handler(asr_payload, emit, lambda: False)
        finally:
            job_handlers._safe_unlink(audio_path)

        return events

    events = _run_async(run())

    assert any(event == "result" for event, _data in events)
    assert len(audit_store.llm_calls) == 2
    tts_audit = audit_store.llm_calls[0]
    asr_audit = audit_store.llm_calls[1]
    assert tts_audit["job_type"] == "tts"
    assert tts_audit["queue_ms"] == 200
    assert tts_audit["metadata"]["duration_ms"] == 1200
    assert tts_audit["metadata"]["upstream_request_id"] == "tts-upstream"
    assert asr_audit["job_type"] == "asr"
    assert asr_audit["metadata"]["language"] == "zh"
    assert asr_audit["metadata"]["upstream_request_id"] == "asr-upstream"
