"""Voice loop behavior regressions for App Mode stability."""

from __future__ import annotations

import threading
import time
from typing import Any

import pytest

import voice_client
from app.services.voice_runtime import voice_runtime
from app.services.voice_service import (
    AssistantReply,
    AudioInputProbe,
    STTSelection,
    _normalize_listen_timeout_seconds,
)


def _base_config(**overrides: Any) -> voice_client.VoiceClientConfig:
    base = voice_client.VoiceClientConfig(
        server="http://127.0.0.1:8000",
        api_key=None,
        wake_word="Orion",
        wake_aliases=(),
        disable_wake_word=False,
        vosk_model_path=None,
        stt_backend="whisper",
        whisper_model="small",
        whisper_language="es",
        whisper_beam_size=3,
        whisper_compute="int8",
        whisper_vad_filter=True,
        whisper_condition_on_previous_text=False,
        whisper_temperature=0.0,
        whisper_no_speech_threshold=None,
        whisper_log_prob_threshold=None,
        mic_device=None,
        mic_sample_rate=16000,
        voice_debug=False,
        listen_timeout=1.0,
        phrase_time_limit=2.0,
        user_eos_silence_seconds=1.0,
        ui_post_tts_hold_seconds=0.0,
        request_timeout=3,
        once=False,
        no_tts=True,
    )
    if not overrides:
        return base
    return voice_client.VoiceClientConfig(**{**base.__dict__, **overrides})


def test_voice_loop_starts_idle_without_ptt_capture(monkeypatch: pytest.MonkeyPatch) -> None:
    """Voice loop should stay idle until PTT is explicitly activated."""
    stop_event = threading.Event()

    class FakeTranscriber:
        def __init__(self) -> None:
            self.calls = 0

        def listen_push_to_talk(self, **_kwargs: Any) -> str | None:
            self.calls += 1
            return None

    fake = FakeTranscriber()
    events: list[dict[str, Any]] = []

    monkeypatch.setattr(
        voice_client,
        "select_stt_backend",
        lambda **_kwargs: STTSelection(provider=fake, backend="whisper"),
    )
    monkeypatch.setattr(voice_client, "probe_audio_input", lambda *_args, **_kwargs: AudioInputProbe(True, 1, "Built-in"))
    monkeypatch.setattr(voice_client, "build_speaker", lambda: None)

    timer = threading.Timer(0.25, stop_event.set)
    timer.start()
    try:
        voice_runtime.reset()
        exit_code = voice_client.run_voice_client(
            config=_base_config(disable_wake_word=False),
            event_callback=events.append,
            stop_event=stop_event,
        )
    finally:
        timer.cancel()

    state_events = [event for event in events if event.get("type") == "state"]
    assert exit_code == 0
    assert fake.calls == 0
    assert all(event.get("state") != "listening" for event in state_events)


def test_ptt_start_runs_single_voice_cycle(monkeypatch: pytest.MonkeyPatch) -> None:
    """When PTT is active, one utterance should go through transcribe/query path."""
    query_calls: list[str] = []

    class FakeTranscriber:
        def listen_push_to_talk(self, **kwargs: Any) -> str | None:
            keep_listening = kwargs["keep_listening"]
            assert keep_listening() is True
            voice_runtime.stop_listening()
            return "hola"

    fake = FakeTranscriber()
    events: list[dict[str, Any]] = []

    monkeypatch.setattr(
        voice_client,
        "select_stt_backend",
        lambda **_kwargs: STTSelection(provider=fake, backend="whisper"),
    )
    monkeypatch.setattr(voice_client, "probe_audio_input", lambda *_args, **_kwargs: AudioInputProbe(True, 1, "Built-in"))
    monkeypatch.setattr(voice_client, "build_speaker", lambda: None)
    monkeypatch.setattr(
        voice_client,
        "query_assistant",
        lambda **kwargs: query_calls.append(kwargs["message"]) or AssistantReply(intent="GENERAL", response="ok"),
    )

    voice_runtime.reset()
    voice_runtime.start_listening()
    exit_code = voice_client.run_voice_client(
        config=_base_config(disable_wake_word=False, once=True),
        event_callback=events.append,
    )

    state_events = [event for event in events if event.get("type") == "state"]
    assert exit_code == 0
    assert query_calls == ["hola"]
    assert any(event.get("state") == "listening" for event in state_events)
    assert any(event.get("state") == "transcribing" for event in state_events)
    assert any(event.get("state") == "thinking" for event in state_events)


def test_no_microphone_keeps_voice_loop_paused_without_capture_attempts(monkeypatch: pytest.MonkeyPatch) -> None:
    """When microphone is unavailable, loop should not spam capture attempts."""
    stop_event = threading.Event()

    class FakeTranscriber:
        def __init__(self) -> None:
            self.calls = 0

        def listen_push_to_talk(self, **_kwargs: Any) -> str | None:
            self.calls += 1
            return None

    fake = FakeTranscriber()
    events: list[dict[str, Any]] = []

    monkeypatch.setattr(
        voice_client,
        "select_stt_backend",
        lambda **_kwargs: STTSelection(provider=fake, backend="whisper"),
    )
    monkeypatch.setattr(
        voice_client,
        "probe_audio_input",
        lambda *_args, **_kwargs: AudioInputProbe(
            available=False,
            device_index=None,
            device_name="none",
            error="No microphone detected / input unavailable",
        ),
    )
    monkeypatch.setattr(voice_client, "build_speaker", lambda: None)

    timer = threading.Timer(0.35, stop_event.set)
    timer.start()
    try:
        voice_runtime.reset()
        voice_runtime.start_listening()
        exit_code = voice_client.run_voice_client(
            config=_base_config(disable_wake_word=False),
            event_callback=events.append,
            stop_event=stop_event,
        )
    finally:
        timer.cancel()

    assert exit_code == 0
    assert fake.calls == 0
    assert any("No microphone detected / input unavailable" in str(event.get("text", "")) for event in events if event.get("type") == "response")


def test_listen_timeout_zero_keeps_ptt_wait_open() -> None:
    """PTT timeout=0 must remain open-ended to avoid rapid listen/restart loops."""
    assert _normalize_listen_timeout_seconds(None) == 0.0
    assert _normalize_listen_timeout_seconds(0.0) == 0.0
    assert _normalize_listen_timeout_seconds(-1.0) == 0.0
    assert _normalize_listen_timeout_seconds(0.05) == 0.2
    assert _normalize_listen_timeout_seconds(2.5) == 2.5


def test_stop_request_interrupts_speaking_cycle(monkeypatch: pytest.MonkeyPatch) -> None:
    """Stop request should interrupt active TTS playback and return to cooldown/idle."""
    stop_event = threading.Event()
    speaker_started = threading.Event()

    class FakeTranscriber:
        def listen_push_to_talk(self, **kwargs: Any) -> str | None:
            keep_listening = kwargs["keep_listening"]
            assert keep_listening() is True
            voice_runtime.stop_listening()
            return "hola"

    class FakeSpeaker:
        def __init__(self) -> None:
            self.interrupted = False

        def speak(self, text: str, *, should_stop=None) -> None:  # type: ignore[no-untyped-def]
            _ = text
            speaker_started.set()
            deadline = time.monotonic() + 2.0
            while time.monotonic() < deadline:
                if callable(should_stop) and should_stop():
                    self.interrupted = True
                    return
                time.sleep(0.02)

    fake = FakeTranscriber()
    fake_speaker = FakeSpeaker()
    events: list[dict[str, Any]] = []

    monkeypatch.setattr(
        voice_client,
        "select_stt_backend",
        lambda **_kwargs: STTSelection(provider=fake, backend="whisper"),
    )
    monkeypatch.setattr(voice_client, "probe_audio_input", lambda *_args, **_kwargs: AudioInputProbe(True, 1, "Built-in"))
    monkeypatch.setattr(voice_client, "build_speaker", lambda: fake_speaker)
    monkeypatch.setattr(
        voice_client,
        "query_assistant",
        lambda **_kwargs: AssistantReply(intent="GENERAL", response="respuesta larga"),
    )

    voice_runtime.reset()
    voice_runtime.start_listening()
    exit_holder: dict[str, int] = {}

    def _runner() -> None:
        exit_holder["code"] = voice_client.run_voice_client(
            config=_base_config(disable_wake_word=False, once=False, no_tts=False),
            event_callback=events.append,
            stop_event=stop_event,
        )

    thread = threading.Thread(target=_runner, daemon=True)
    thread.start()

    assert speaker_started.wait(timeout=2.0), "speaker did not start"
    stop_result = voice_runtime.request_response_stop()
    assert stop_result["accepted"] is True

    timer = threading.Timer(0.6, stop_event.set)
    timer.start()
    try:
        thread.join(timeout=5.0)
    finally:
        timer.cancel()
        stop_event.set()

    assert exit_holder.get("code") == 0
    assert fake_speaker.interrupted is True
    state_events = [event for event in events if event.get("type") == "state"]
    assert any(event.get("state") == "audio_finished" for event in state_events)


def test_stop_request_during_thinking_discards_late_response(monkeypatch: pytest.MonkeyPatch) -> None:
    """If stop arrives during thinking, late assistant result must be discarded."""
    stop_event = threading.Event()
    query_started = threading.Event()

    class FakeTranscriber:
        def listen_push_to_talk(self, **kwargs: Any) -> str | None:
            keep_listening = kwargs["keep_listening"]
            assert keep_listening() is True
            voice_runtime.stop_listening()
            return "hola"

    fake = FakeTranscriber()
    events: list[dict[str, Any]] = []

    def _slow_query(**_kwargs: Any) -> AssistantReply:
        query_started.set()
        time.sleep(0.45)
        return AssistantReply(intent="GENERAL", response="late-response")

    monkeypatch.setattr(
        voice_client,
        "select_stt_backend",
        lambda **_kwargs: STTSelection(provider=fake, backend="whisper"),
    )
    monkeypatch.setattr(voice_client, "probe_audio_input", lambda *_args, **_kwargs: AudioInputProbe(True, 1, "Built-in"))
    monkeypatch.setattr(voice_client, "build_speaker", lambda: None)
    monkeypatch.setattr(voice_client, "query_assistant", _slow_query)

    voice_runtime.reset()
    voice_runtime.start_listening()
    exit_holder: dict[str, int] = {}

    def _runner() -> None:
        exit_holder["code"] = voice_client.run_voice_client(
            config=_base_config(disable_wake_word=False, once=False, no_tts=True),
            event_callback=events.append,
            stop_event=stop_event,
        )

    thread = threading.Thread(target=_runner, daemon=True)
    thread.start()

    assert query_started.wait(timeout=2.0), "query did not start"
    stop_result = voice_runtime.request_response_stop()
    assert stop_result["accepted"] is True

    timer = threading.Timer(0.9, stop_event.set)
    timer.start()
    try:
        thread.join(timeout=5.0)
    finally:
        timer.cancel()
        stop_event.set()

    assert exit_holder.get("code") == 0
    response_texts = [str(event.get("text", "")) for event in events if event.get("type") == "response"]
    assert "late-response" not in response_texts
