import asyncio
import pytest
from unittest.mock import AsyncMock, MagicMock, patch
from dataclasses import dataclass
from typing import Optional

from navig.voice.session_manager import VoiceSessionManager, SessionConfig, SessionState, VoiceSession, get_session_manager

@pytest.fixture
def config():
    return SessionConfig(
        silence_timeout_seconds=0.01,
        max_listen_seconds=0.1,
        min_audio_ms=0,
        log_timing=True,
    )

@pytest.fixture
async def manager(config):
    m = VoiceSessionManager(config=config)
    await m.start()
    yield m
    await m.stop()

@pytest.mark.asyncio
async def test_lifecycle_activate_limit(manager, config):
    assert manager._running is True
    
    config.max_concurrent_sessions = 1
    s1 = await manager.activate("echo")
    assert s1 is not None
    
    s2 = await manager.activate("echo")
    assert s2 is None  # max concurrent reached
    
    manager._running = False
    s3 = await manager.activate("echo")
    assert s3 is None  # manager stopped
    
@pytest.mark.asyncio
async def test_feed_audio_and_stop_listening(manager):
    await manager.feed_audio(b"123")  # no active session, ignored
    
    s = await manager.activate("echo")
    for _ in range(5):
        await manager.feed_audio(b"chunk")
    
    await manager.stop_listening()
    await asyncio.sleep(0.05)
    
    assert s.state in (SessionState.PROCESSING, SessionState.IDLE, SessionState.ERROR)
    
@pytest.mark.asyncio
async def test_interrupt(manager):
    await manager.interrupt()  # no active session, ignored
    
    s = await manager.activate("echo")
    await asyncio.sleep(0.01)  # let the task start
    await manager.interrupt()
    await asyncio.sleep(0.05)
    
    assert s.state == SessionState.IDLE
    
@pytest.mark.asyncio
async def test_run_session_full_pipeline(config):
    stt_mock = AsyncMock(return_value="hello")
    llm_mock = AsyncMock(return_value="world")
    tts_mock = AsyncMock(return_value="/audio.mp3")
    
    event_bridge = MagicMock()
    event_bridge.push_direct = AsyncMock()
    
    on_state_change = AsyncMock()
    on_complete = AsyncMock()
    
    m = VoiceSessionManager(
        config=config,
        stt_fn=stt_mock,
        llm_fn=llm_mock,
        tts_fn=tts_mock,
        event_bridge=event_bridge,
        on_state_change=on_state_change,
        on_session_complete=on_complete
    )
    await m.start()
    
    s = await m.activate("echo")
    await m.feed_audio(b"chunk")
    await m.stop_listening()
    
    await asyncio.sleep(0.1)
    
    assert s.transcript == "hello"
    assert s.response_text == "world"
    assert s.audio_path == "/audio.mp3"
    assert s.state == SessionState.IDLE
    
    assert stt_mock.await_count == 1
    assert llm_mock.await_count == 1
    assert tts_mock.await_count == 1
    assert on_complete.await_count == 1
    assert on_state_change.await_count >= 1
    assert event_bridge.push_direct.await_count >= 1
    
    await m.stop()

@pytest.mark.asyncio
async def test_pipeline_timeouts_and_errors(config):
    # Simulate timeout natively in STT
    async def stt_timeout(sess):
        raise asyncio.TimeoutError()
        
    m1 = VoiceSessionManager(config=config, stt_fn=stt_timeout)
    await m1.start()
    s_id = "test1"
    m1._sessions[s_id] = VoiceSession(id=s_id, state=SessionState.WAKE_DETECTED, keyword="x", score=1.0)
    m1._interrupt_events[s_id] = asyncio.Event()
    m1._audio_queues[s_id] = asyncio.Queue()
    await m1._audio_queues[s_id].put(b"phony_audio")
    await m1._audio_queues[s_id].put(None)
    
    await m1._run_session(s_id)
    assert m1._sessions[s_id].state == SessionState.IDLE
    
    # Simulate Error natively in STT
    async def stt_error(sess):
        raise ValueError("simulated error")
        
    m2 = VoiceSessionManager(config=config, stt_fn=stt_error)
    await m2.start()
    m2._sessions[s_id] = VoiceSession(id=s_id, state=SessionState.WAKE_DETECTED, keyword="x", score=1.0)
    m2._interrupt_events[s_id] = asyncio.Event()
    m2._audio_queues[s_id] = asyncio.Queue()
    await m2._audio_queues[s_id].put(b"phony_audio")
    await m2._audio_queues[s_id].put(None)
    
    await m2._run_session(s_id)
    assert m2._sessions[s_id].state == SessionState.IDLE
    
@pytest.mark.asyncio
async def test_collect_audio_max_duration():
    c = SessionConfig(max_listen_seconds=0.01)
    m = VoiceSessionManager(config=c)
    
    q = asyncio.Queue()
    evt = asyncio.Event()
    
    # Will immediately exit loop because max duration hit
    out = await m._collect_audio("id", q, evt)
    assert out == b""

@pytest.mark.asyncio
async def test_notify_echo_bridge():
    m = VoiceSessionManager(config=SessionConfig(echo_bridge_url="http://fake"))
    
    class DummyClient:
        async def __aenter__(self): return self
        async def __aexit__(self, *args): pass
        async def post(self, *args, **kwargs): pass
        
    with patch("aiohttp.ClientSession", return_value=DummyClient()):
        await m._notify_echo_bridge("s1", "hi", 1.0)
        
    # Test without echo url
    m.config.echo_bridge_url = None
    await m._notify_echo_bridge("s2", "hi", 1.0)

def test_singleton():
    m1 = get_session_manager()
    m2 = get_session_manager()
    assert m1 is m2
