"""Tests for the voice AI pipeline with mocked models."""

import pytest
import numpy as np
from unittest.mock import MagicMock, AsyncMock, patch

from roohai.pipeline import Rooh


@pytest.fixture
def mock_pipeline(sample_config):
    """Create a Rooh with mocked model loading."""
    with patch("roohai.pipeline._register_from_config"):
        # Patch load_config to return a proper config dict
        with patch("roohai.pipeline.load_config") as mock_load:
            mock_load.return_value = {
                "pipeline": {"stt": "whisper-tiny", "tts": "piper", "llm": "bedrock-claude"},
                "audio": {"sample_rate": 16000},
            }
            p = Rooh(sample_config)

    # Inject mock models
    p.stt = MagicMock()
    p.stt.transcribe.return_value = "hello world"
    p.stt.is_loaded = True

    p.tts = MagicMock()
    p.tts.synthesize.return_value = (np.zeros(100, dtype=np.float32), 24000)
    p.tts.is_loaded = True

    p.llm = MagicMock()
    p.llm.chat = AsyncMock(return_value="Hi there!")

    return p


class TestRooh:
    async def test_transcribe(self, mock_pipeline, mock_audio_bytes):
        """Rooh.transcribe should return text from STT model."""
        result = await mock_pipeline.transcribe(mock_audio_bytes)
        assert result == "hello world"
        mock_pipeline.stt.transcribe.assert_called_once()

    async def test_chat(self, mock_pipeline):
        """Rooh.chat should return LLM response."""
        result = await mock_pipeline.chat("hello")
        assert result == "Hi there!"
        mock_pipeline.llm.chat.assert_called_once_with("hello", history=None, system_prompt=None, session_id=None)

    async def test_synthesize(self, mock_pipeline):
        """Rooh.synthesize should return WAV bytes."""
        result = await mock_pipeline.synthesize("hello")
        assert isinstance(result, bytes)
        assert result[:4] == b"RIFF"  # WAV header
        mock_pipeline.tts.synthesize.assert_called_once_with("hello")

    async def test_process_audio_end_to_end(self, mock_pipeline, mock_audio_bytes):
        """Full pipeline should return transcription, response, and audio."""
        transcription, response, audio_bytes = await mock_pipeline.process_audio(mock_audio_bytes)
        assert transcription == "hello world"
        assert response == "Hi there!"
        assert isinstance(audio_bytes, bytes)

    async def test_transcribe_no_stt_raises(self, mock_pipeline):
        """Should raise RuntimeError if no STT model loaded."""
        mock_pipeline.stt = None
        with pytest.raises(RuntimeError, match="No STT model loaded"):
            await mock_pipeline.transcribe(b"audio")

    async def test_chat_no_llm_raises(self, mock_pipeline):
        """Should raise RuntimeError if no LLM model loaded."""
        mock_pipeline.llm = None
        with pytest.raises(RuntimeError, match="No LLM model loaded"):
            await mock_pipeline.chat("hello")

    async def test_synthesize_no_tts_raises(self, mock_pipeline):
        """Should raise RuntimeError if no TTS model loaded."""
        mock_pipeline.tts = None
        with pytest.raises(RuntimeError, match="No TTS model loaded"):
            await mock_pipeline.synthesize("hello")

    def test_swap_model_stt(self, mock_pipeline):
        """swap_model should replace STT model."""
        from roohai.registry import registry

        new_stt = MagicMock()
        with patch.object(registry, "create_stt", return_value=new_stt):
            mock_pipeline.swap_model("stt", "new-stt")
        assert mock_pipeline.stt is new_stt
        new_stt.load.assert_called_once()

    def test_swap_model_invalid_type_raises(self, mock_pipeline):
        """swap_model should raise ValueError for unknown type."""
        with pytest.raises(ValueError, match="Unknown model type"):
            mock_pipeline.swap_model("invalid", "name")


class TestRoohHooks:
    async def test_hook_replaces_llm(self, mock_pipeline):
        """When a hook is set, it should be called instead of llm.chat."""
        hook = AsyncMock(return_value="hooked!")
        mock_pipeline.set_llm_hook(hook)
        result = await mock_pipeline.chat("hello")
        assert result == "hooked!"
        hook.assert_awaited_once()
        mock_pipeline.llm.chat.assert_not_awaited()

    async def test_hook_receives_resolved_system_prompt(self, mock_pipeline):
        """Hook should receive the resolved system prompt."""
        mock_pipeline.set_system_prompt("default prompt")
        hook = AsyncMock(return_value="ok")
        mock_pipeline.set_llm_hook(hook)
        await mock_pipeline.chat("hello")
        hook.assert_awaited_once_with("hello", history=None, system_prompt="default prompt", session_id=None)

    async def test_hook_explicit_system_prompt_overrides(self, mock_pipeline):
        """Explicit system_prompt should override the default."""
        mock_pipeline.set_system_prompt("default prompt")
        hook = AsyncMock(return_value="ok")
        mock_pipeline.set_llm_hook(hook)
        await mock_pipeline.chat("hello", system_prompt="override")
        hook.assert_awaited_once_with("hello", history=None, system_prompt="override", session_id=None)

    async def test_remove_hook_restores_default(self, mock_pipeline):
        """set_llm_hook(None) should restore default LLM behaviour."""
        hook = AsyncMock(return_value="hooked!")
        mock_pipeline.set_llm_hook(hook)
        await mock_pipeline.chat("hello")
        assert hook.await_count == 1

        mock_pipeline.set_llm_hook(None)
        result = await mock_pipeline.chat("hello")
        assert result == "Hi there!"
        mock_pipeline.llm.chat.assert_awaited()

    async def test_hook_works_with_no_llm(self, mock_pipeline):
        """Hook should work even when pipeline.llm is None."""
        mock_pipeline.llm = None
        hook = AsyncMock(return_value="hooked!")
        mock_pipeline.set_llm_hook(hook)
        result = await mock_pipeline.chat("hello")
        assert result == "hooked!"

    async def test_process_audio_uses_hook(self, mock_pipeline, mock_audio_bytes):
        """process_audio should use the hook end-to-end."""
        hook = AsyncMock(return_value="hooked response")
        mock_pipeline.set_llm_hook(hook)
        transcription, response, audio_bytes = await mock_pipeline.process_audio(mock_audio_bytes)
        assert transcription == "hello world"
        assert response == "hooked response"
        assert isinstance(audio_bytes, bytes)
        hook.assert_awaited_once()
