"""Unit tests for the Mumble voice bridge integration."""

from __future__ import annotations

import struct
import sys
from unittest.mock import MagicMock, patch

import pytest

pytestmark = pytest.mark.unit

# ── Helpers ───────────────────────────────────────────────────────────────────


def _pcm_silence(ms: int = 10, sample_rate: int = 48000) -> bytes:
    """Return *ms* milliseconds of 16-bit 48 kHz silent PCM."""
    n = sample_rate * ms // 1000
    return struct.pack(f"<{n}h", *([0] * n))


def _pcm_tone(ms: int = 10, sample_rate: int = 48000, amplitude: int = 16000) -> bytes:
    """Return *ms* milliseconds of a constant-amplitude 16-bit PCM signal."""
    import math

    n = sample_rate * ms // 1000
    freq = 440  # Hz
    samples = [int(amplitude * math.sin(2 * math.pi * freq * i / sample_rate)) for i in range(n)]
    return struct.pack(f"<{n}h", *samples)


# ── _rms helper ───────────────────────────────────────────────────────────────


class TestRmsHelper:
    """Tests for the internal _rms() energy function."""

    def test_silence_is_zero(self):
        from integrations.mumble.bridge import _rms

        assert _rms(_pcm_silence()) == 0.0

    def test_empty_bytes_is_zero(self):
        from integrations.mumble.bridge import _rms

        assert _rms(b"") == 0.0

    def test_tone_has_positive_rms(self):
        from integrations.mumble.bridge import _rms

        rms = _rms(_pcm_tone(amplitude=16000))
        assert 0.0 < rms <= 1.0

    def test_louder_tone_has_higher_rms(self):
        from integrations.mumble.bridge import _rms

        quiet = _rms(_pcm_tone(amplitude=1000))
        loud = _rms(_pcm_tone(amplitude=20000))
        assert loud > quiet

    def test_max_amplitude_rms_near_one(self):
        from integrations.mumble.bridge import _rms

        # All samples at max positive value
        n = 480
        pcm = struct.pack(f"<{n}h", *([32767] * n))
        rms = _rms(pcm)
        assert rms > 0.99


# ── MumbleBridge unit tests ───────────────────────────────────────────────────


class TestMumbleBridgeProperties:
    def test_initial_state(self):
        from integrations.mumble.bridge import MumbleBridge

        bridge = MumbleBridge()
        assert not bridge.connected
        assert not bridge.running

    def test_singleton_exists(self):
        from integrations.mumble.bridge import MumbleBridge, mumble_bridge

        assert isinstance(mumble_bridge, MumbleBridge)


class TestMumbleBridgeStart:
    def test_start_disabled_returns_false(self):
        """start() returns False when MUMBLE_ENABLED=false."""
        from integrations.mumble.bridge import MumbleBridge

        bridge = MumbleBridge()
        mock_settings = MagicMock()
        mock_settings.mumble_enabled = False
        with patch("config.settings", mock_settings):
            result = bridge.start()
        assert result is False
        assert not bridge.connected

    def test_start_missing_pymumble_returns_false(self):
        """start() returns False gracefully when pymumble_py3 is absent."""
        from integrations.mumble.bridge import MumbleBridge

        bridge = MumbleBridge()
        mock_settings = MagicMock()
        mock_settings.mumble_enabled = True
        with (
            patch("config.settings", mock_settings),
            patch.dict(sys.modules, {"pymumble_py3": None}),
        ):
            result = bridge.start()
        assert result is False
        assert not bridge.connected

    def test_start_already_connected_returns_true(self):
        """start() short-circuits when already connected."""
        from integrations.mumble.bridge import MumbleBridge

        bridge = MumbleBridge()
        bridge._connected = True
        mock_settings = MagicMock()
        mock_settings.mumble_enabled = True
        with patch("config.settings", mock_settings):
            result = bridge.start()
        assert result is True

    def test_start_connection_error_returns_false(self):
        """start() returns False and stays clean when Mumble raises."""
        from integrations.mumble.bridge import MumbleBridge

        bridge = MumbleBridge()
        mock_settings = MagicMock()
        mock_settings.mumble_enabled = True
        mock_settings.mumble_host = "127.0.0.1"
        mock_settings.mumble_port = 64738
        mock_settings.mumble_user = "Timmy"
        mock_settings.mumble_password = ""

        mock_mumble_module = MagicMock()
        mock_mumble_module.Mumble.side_effect = ConnectionRefusedError("refused")

        with (
            patch("config.settings", mock_settings),
            patch.dict(sys.modules, {"pymumble_py3": mock_mumble_module}),
        ):
            result = bridge.start()

        assert result is False
        assert not bridge.connected
        assert bridge._client is None


class TestMumbleBridgeStop:
    def test_stop_when_not_connected_is_noop(self):
        from integrations.mumble.bridge import MumbleBridge

        bridge = MumbleBridge()
        bridge.stop()  # Must not raise
        assert not bridge.connected
        assert not bridge.running

    def test_stop_clears_state(self):
        from integrations.mumble.bridge import MumbleBridge

        bridge = MumbleBridge()
        bridge._connected = True
        bridge._running = True
        mock_client = MagicMock()
        bridge._client = mock_client

        bridge.stop()

        mock_client.stop.assert_called_once()
        assert not bridge.connected
        assert not bridge.running
        assert bridge._client is None

    def test_stop_tolerates_client_error(self):
        """stop() cleans up state even when client.stop() raises."""
        from integrations.mumble.bridge import MumbleBridge

        bridge = MumbleBridge()
        bridge._connected = True
        bridge._running = True
        mock_client = MagicMock()
        mock_client.stop.side_effect = RuntimeError("already stopped")
        bridge._client = mock_client

        bridge.stop()  # Must not propagate

        assert not bridge.connected


# ── Audio send ────────────────────────────────────────────────────────────────


class TestMumbleBridgeSendAudio:
    def test_send_audio_when_not_connected_is_noop(self):
        from integrations.mumble.bridge import MumbleBridge

        bridge = MumbleBridge()
        pcm = _pcm_tone()
        bridge.send_audio(pcm)  # Must not raise

    def test_send_audio_enqueues_data(self):
        from integrations.mumble.bridge import MumbleBridge

        bridge = MumbleBridge()
        bridge._connected = True
        bridge._client = MagicMock()

        pcm = _pcm_tone(ms=20)
        bridge.send_audio(pcm)

        assert len(bridge._audio_queue) == 1
        assert bridge._audio_queue[0] == pcm

    def test_send_audio_multiple_chunks(self):
        from integrations.mumble.bridge import MumbleBridge

        bridge = MumbleBridge()
        bridge._connected = True
        bridge._client = MagicMock()

        for _ in range(3):
            bridge.send_audio(_pcm_tone(ms=10))

        assert len(bridge._audio_queue) == 3


# ── Audio callbacks ───────────────────────────────────────────────────────────


class TestMumbleBridgeAudioCallbacks:
    def test_add_and_trigger_callback(self):
        from integrations.mumble.bridge import MumbleBridge

        bridge = MumbleBridge()
        received: list[tuple[str, bytes]] = []

        def cb(username: str, pcm: bytes):
            received.append((username, pcm))

        bridge.add_audio_callback(cb)

        # Simulate sound received
        fake_user = {"name": "Alexander"}
        fake_chunk = MagicMock()
        fake_chunk.pcm = _pcm_tone()
        bridge._on_sound_received(fake_user, fake_chunk)

        assert len(received) == 1
        assert received[0][0] == "Alexander"

    def test_remove_callback(self):
        from integrations.mumble.bridge import MumbleBridge

        bridge = MumbleBridge()
        received: list = []

        def cb(username: str, pcm: bytes):
            received.append(username)

        bridge.add_audio_callback(cb)
        bridge.remove_audio_callback(cb)

        fake_user = {"name": "Alexander"}
        fake_chunk = MagicMock()
        fake_chunk.pcm = _pcm_tone()
        bridge._on_sound_received(fake_user, fake_chunk)

        assert received == []

    def test_remove_nonexistent_callback_is_noop(self):
        from integrations.mumble.bridge import MumbleBridge

        bridge = MumbleBridge()

        def cb(u, p):
            pass

        bridge.remove_audio_callback(cb)  # Must not raise

    def test_on_sound_received_no_callbacks(self):
        from integrations.mumble.bridge import MumbleBridge

        bridge = MumbleBridge()
        fake_user = {"name": "Test"}
        fake_chunk = MagicMock()
        fake_chunk.pcm = _pcm_tone()
        bridge._on_sound_received(fake_user, fake_chunk)  # Must not raise

    def test_on_sound_received_missing_user_key(self):
        """Falls back to 'unknown' when user dict has no 'name' key."""
        from integrations.mumble.bridge import MumbleBridge

        received_names: list[str] = []

        bridge = MumbleBridge()
        bridge.add_audio_callback(lambda u, p: received_names.append(u))

        fake_chunk = MagicMock()
        fake_chunk.pcm = _pcm_tone()
        bridge._on_sound_received({}, fake_chunk)

        assert received_names == ["unknown"]

    def test_callback_exception_does_not_propagate(self):
        """A crashing callback must not bubble up to the Mumble thread."""
        from integrations.mumble.bridge import MumbleBridge

        bridge = MumbleBridge()

        def bad_cb(u, p):
            raise RuntimeError("oops")

        bridge.add_audio_callback(bad_cb)

        fake_chunk = MagicMock()
        fake_chunk.pcm = _pcm_tone()
        bridge._on_sound_received({"name": "X"}, fake_chunk)  # Must not raise


# ── Push-to-talk ──────────────────────────────────────────────────────────────


class TestPushToTalk:
    def test_ptt_context_sets_and_clears_flag(self):
        from integrations.mumble.bridge import MumbleBridge

        bridge = MumbleBridge()
        assert not bridge._ptt_active

        with bridge.push_to_talk():
            assert bridge._ptt_active

        assert not bridge._ptt_active

    def test_ptt_clears_on_exception(self):
        from integrations.mumble.bridge import MumbleBridge

        bridge = MumbleBridge()
        try:
            with bridge.push_to_talk():
                raise ValueError("test")
        except ValueError:
            pass

        assert not bridge._ptt_active


# ── VAD send_pcm_buffer ───────────────────────────────────────────────────────


class TestSendPcmBuffer:
    def test_vad_suppresses_silence(self):
        """VAD mode must not call sound_output.add_sound for silent PCM."""
        from integrations.mumble.bridge import MumbleBridge

        bridge = MumbleBridge()
        mock_client = MagicMock()
        bridge._client = mock_client

        mock_settings = MagicMock()
        mock_settings.mumble_audio_mode = "vad"
        mock_settings.mumble_vad_threshold = 0.02

        with patch("config.settings", mock_settings):
            bridge._send_pcm_buffer(_pcm_silence(ms=50))

        mock_client.sound_output.add_sound.assert_not_called()

    def test_vad_transmits_tone(self):
        """VAD mode must send audible PCM frames."""
        from integrations.mumble.bridge import MumbleBridge

        bridge = MumbleBridge()
        mock_client = MagicMock()
        bridge._client = mock_client

        mock_settings = MagicMock()
        mock_settings.mumble_audio_mode = "vad"
        mock_settings.mumble_vad_threshold = 0.01

        with patch("config.settings", mock_settings):
            bridge._send_pcm_buffer(_pcm_tone(ms=50, amplitude=16000))

        assert mock_client.sound_output.add_sound.call_count > 0

    def test_ptt_suppresses_when_inactive(self):
        """PTT mode must not send when _ptt_active is False."""
        from integrations.mumble.bridge import MumbleBridge

        bridge = MumbleBridge()
        mock_client = MagicMock()
        bridge._client = mock_client
        bridge._ptt_active = False

        mock_settings = MagicMock()
        mock_settings.mumble_audio_mode = "ptt"
        mock_settings.mumble_vad_threshold = 0.02

        with patch("config.settings", mock_settings):
            bridge._send_pcm_buffer(_pcm_tone(ms=50, amplitude=16000))

        mock_client.sound_output.add_sound.assert_not_called()

    def test_ptt_sends_when_active(self):
        """PTT mode must send when _ptt_active is True."""
        from integrations.mumble.bridge import MumbleBridge

        bridge = MumbleBridge()
        mock_client = MagicMock()
        bridge._client = mock_client
        bridge._ptt_active = True

        mock_settings = MagicMock()
        mock_settings.mumble_audio_mode = "ptt"
        mock_settings.mumble_vad_threshold = 0.02

        with patch("config.settings", mock_settings):
            bridge._send_pcm_buffer(_pcm_tone(ms=50, amplitude=16000))

        assert mock_client.sound_output.add_sound.call_count > 0

    def test_no_client_is_noop(self):
        """_send_pcm_buffer is a no-op when client is None."""
        from integrations.mumble.bridge import MumbleBridge

        bridge = MumbleBridge()
        bridge._client = None
        bridge._send_pcm_buffer(_pcm_tone(ms=20))  # Must not raise


# ── TTS pipeline ──────────────────────────────────────────────────────────────


class TestTtsToPcm:
    def test_no_tts_engines_returns_none(self):
        """_tts_to_pcm returns None gracefully when no engine is available."""
        from integrations.mumble.bridge import MumbleBridge

        bridge = MumbleBridge()
        with (
            patch.dict(sys.modules, {"piper": None, "piper.voice": None, "pyttsx3": None}),
        ):
            result = bridge._tts_to_pcm("Hello world")
        assert result is None

    def test_speak_when_not_connected_is_noop(self):
        """speak() must be a safe no-op when bridge is not connected."""
        from integrations.mumble.bridge import MumbleBridge

        bridge = MumbleBridge()
        bridge._connected = False
        bridge.speak("Hello")  # Must not raise

    def test_speak_calls_send_audio_when_tts_succeeds(self):
        """speak() calls send_audio when _tts_to_pcm returns bytes."""
        from integrations.mumble.bridge import MumbleBridge

        bridge = MumbleBridge()
        bridge._connected = True
        bridge._client = MagicMock()

        fake_pcm = _pcm_tone(ms=200)
        with patch.object(bridge, "_tts_to_pcm", return_value=fake_pcm):
            with patch.object(bridge, "send_audio") as mock_send:
                bridge.speak("Hello Timmy")

        mock_send.assert_called_once_with(fake_pcm)

    def test_speak_does_not_call_send_when_tts_fails(self):
        """speak() does not call send_audio when TTS returns None."""
        from integrations.mumble.bridge import MumbleBridge

        bridge = MumbleBridge()
        bridge._connected = True
        bridge._client = MagicMock()

        with patch.object(bridge, "_tts_to_pcm", return_value=None):
            with patch.object(bridge, "send_audio") as mock_send:
                bridge.speak("Hello")

        mock_send.assert_not_called()


# ── Config settings integration ───────────────────────────────────────────────


class TestMumbleSettings:
    def test_settings_have_mumble_fields(self):
        """Settings object exposes all required Mumble configuration fields."""
        from config import settings

        assert hasattr(settings, "mumble_enabled")
        assert hasattr(settings, "mumble_host")
        assert hasattr(settings, "mumble_port")
        assert hasattr(settings, "mumble_user")
        assert hasattr(settings, "mumble_password")
        assert hasattr(settings, "mumble_channel")
        assert hasattr(settings, "mumble_audio_mode")
        assert hasattr(settings, "mumble_vad_threshold")
        assert hasattr(settings, "mumble_silence_ms")

    def test_default_mumble_disabled(self):
        """Mumble is disabled by default (opt-in only)."""
        from config import settings

        assert settings.mumble_enabled is False

    def test_default_mumble_port(self):
        from config import settings

        assert settings.mumble_port == 64738

    def test_default_audio_mode(self):
        from config import settings

        assert settings.mumble_audio_mode == "vad"

    def test_default_vad_threshold(self):
        from config import settings

        assert 0.0 < settings.mumble_vad_threshold < 1.0
