"""Unit tests for timmy_serve.voice_tts.

Mocks pyttsx3 so tests run without audio hardware.
"""

import threading
from unittest.mock import MagicMock, patch


class TestVoiceTTSInit:
    """Test VoiceTTS initialization with/without pyttsx3."""

    def test_init_success(self):
        """When pyttsx3 is available, engine initializes with given rate/volume."""
        mock_pyttsx3 = MagicMock()
        mock_engine = MagicMock()
        mock_pyttsx3.init.return_value = mock_engine

        with patch.dict("sys.modules", {"pyttsx3": mock_pyttsx3}):
            from timmy_serve.voice_tts import VoiceTTS

            tts = VoiceTTS(rate=200, volume=0.8)
            assert tts.available is True
            assert tts._rate == 200
            assert tts._volume == 0.8
            mock_engine.setProperty.assert_any_call("rate", 200)
            mock_engine.setProperty.assert_any_call("volume", 0.8)

    def test_init_import_failure(self):
        """When pyttsx3 import fails, VoiceTTS degrades gracefully."""
        with patch.dict("sys.modules", {"pyttsx3": None}):
            # Force reimport by clearing cache
            import sys

            modules_to_clear = [k for k in sys.modules.keys() if "voice_tts" in k]
            for mod in modules_to_clear:
                del sys.modules[mod]

            from timmy_serve.voice_tts import VoiceTTS

            tts = VoiceTTS()
            assert tts.available is False
            assert tts._engine is None


class TestVoiceTTSSpeak:
    """Test VoiceTTS speak methods."""

    def test_speak_skips_when_not_available(self):
        """speak() should skip gracefully when TTS is not available."""
        from timmy_serve.voice_tts import VoiceTTS

        tts = VoiceTTS.__new__(VoiceTTS)
        tts._engine = None
        tts._available = False
        tts._lock = threading.Lock()

        # Should not raise
        tts.speak("hello world")

    def test_speak_sync_skips_when_not_available(self):
        """speak_sync() should skip gracefully when TTS is not available."""
        from timmy_serve.voice_tts import VoiceTTS

        tts = VoiceTTS.__new__(VoiceTTS)
        tts._engine = None
        tts._available = False
        tts._lock = threading.Lock()

        # Should not raise
        tts.speak_sync("hello world")

    def test_speak_runs_in_background_thread(self):
        """speak() should run speech in a background thread."""
        from timmy_serve.voice_tts import VoiceTTS

        tts = VoiceTTS.__new__(VoiceTTS)
        tts._engine = MagicMock()
        tts._available = True
        tts._lock = threading.Lock()

        captured_threads = []
        original_thread = threading.Thread

        def capture_thread(*args, **kwargs):
            t = original_thread(*args, **kwargs)
            captured_threads.append(t)
            return t

        with patch.object(threading, "Thread", side_effect=capture_thread):
            tts.speak("test message")
            # Wait for threads to complete
            for t in captured_threads:
                t.join(timeout=1)

        tts._engine.say.assert_called_with("test message")
        tts._engine.runAndWait.assert_called_once()


class TestVoiceTTSProperties:
    """Test VoiceTTS property setters."""

    def test_set_rate_updates_property(self):
        """set_rate() updates internal rate and engine property."""
        from timmy_serve.voice_tts import VoiceTTS

        tts = VoiceTTS.__new__(VoiceTTS)
        tts._engine = MagicMock()
        tts._rate = 175

        tts.set_rate(220)
        assert tts._rate == 220
        tts._engine.setProperty.assert_called_with("rate", 220)

    def test_set_rate_without_engine(self):
        """set_rate() updates internal rate even when engine is None."""
        from timmy_serve.voice_tts import VoiceTTS

        tts = VoiceTTS.__new__(VoiceTTS)
        tts._engine = None
        tts._rate = 175

        tts.set_rate(220)
        assert tts._rate == 220

    def test_set_volume_clamped_to_max(self):
        """set_volume() clamps volume to maximum of 1.0."""
        from timmy_serve.voice_tts import VoiceTTS

        tts = VoiceTTS.__new__(VoiceTTS)
        tts._engine = MagicMock()
        tts._volume = 0.9

        tts.set_volume(1.5)
        assert tts._volume == 1.0
        tts._engine.setProperty.assert_called_with("volume", 1.0)

    def test_set_volume_clamped_to_min(self):
        """set_volume() clamps volume to minimum of 0.0."""
        from timmy_serve.voice_tts import VoiceTTS

        tts = VoiceTTS.__new__(VoiceTTS)
        tts._engine = MagicMock()
        tts._volume = 0.9

        tts.set_volume(-0.5)
        assert tts._volume == 0.0
        tts._engine.setProperty.assert_called_with("volume", 0.0)

    def test_set_volume_within_range(self):
        """set_volume() accepts values within 0.0-1.0 range."""
        from timmy_serve.voice_tts import VoiceTTS

        tts = VoiceTTS.__new__(VoiceTTS)
        tts._engine = MagicMock()
        tts._volume = 0.9

        tts.set_volume(0.5)
        assert tts._volume == 0.5
        tts._engine.setProperty.assert_called_with("volume", 0.5)


class TestVoiceTTSGetVoices:
    """Test VoiceTTS get_voices() method."""

    def test_get_voices_returns_empty_list_when_no_engine(self):
        """get_voices() returns empty list when engine is None."""
        from timmy_serve.voice_tts import VoiceTTS

        tts = VoiceTTS.__new__(VoiceTTS)
        tts._engine = None

        result = tts.get_voices()
        assert result == []

    def test_get_voices_returns_formatted_voice_list(self):
        """get_voices() returns list of voice dicts with id, name, languages."""
        from timmy_serve.voice_tts import VoiceTTS

        tts = VoiceTTS.__new__(VoiceTTS)

        mock_voice1 = MagicMock()
        mock_voice1.id = "com.apple.voice.compact.en-US.Samantha"
        mock_voice1.name = "Samantha"
        mock_voice1.languages = ["en-US"]

        mock_voice2 = MagicMock()
        mock_voice2.id = "com.apple.voice.compact.en-GB.Daniel"
        mock_voice2.name = "Daniel"
        mock_voice2.languages = ["en-GB"]

        tts._engine = MagicMock()
        tts._engine.getProperty.return_value = [mock_voice1, mock_voice2]

        voices = tts.get_voices()
        assert len(voices) == 2
        assert voices[0]["id"] == "com.apple.voice.compact.en-US.Samantha"
        assert voices[0]["name"] == "Samantha"
        assert voices[0]["languages"] == ["en-US"]
        assert voices[1]["id"] == "com.apple.voice.compact.en-GB.Daniel"
        assert voices[1]["name"] == "Daniel"
        assert voices[1]["languages"] == ["en-GB"]

    def test_get_voices_handles_missing_languages_attr(self):
        """get_voices() handles voices without languages attribute."""
        from timmy_serve.voice_tts import VoiceTTS

        tts = VoiceTTS.__new__(VoiceTTS)

        mock_voice = MagicMock()
        mock_voice.id = "voice1"
        mock_voice.name = "Default Voice"
        # No languages attribute
        del mock_voice.languages

        tts._engine = MagicMock()
        tts._engine.getProperty.return_value = [mock_voice]

        voices = tts.get_voices()
        assert len(voices) == 1
        assert voices[0]["languages"] == []

    def test_get_voices_handles_exception(self):
        """get_voices() returns empty list on exception."""
        from timmy_serve.voice_tts import VoiceTTS

        tts = VoiceTTS.__new__(VoiceTTS)
        tts._engine = MagicMock()
        tts._engine.getProperty.side_effect = RuntimeError("engine error")

        result = tts.get_voices()
        assert result == []


class TestVoiceTTSSetVoice:
    """Test VoiceTTS set_voice() method."""

    def test_set_voice_updates_property(self):
        """set_voice() updates engine voice property when engine exists."""
        from timmy_serve.voice_tts import VoiceTTS

        tts = VoiceTTS.__new__(VoiceTTS)
        tts._engine = MagicMock()

        tts.set_voice("com.apple.voice.compact.en-US.Samantha")
        tts._engine.setProperty.assert_called_with(
            "voice", "com.apple.voice.compact.en-US.Samantha"
        )

    def test_set_voice_skips_when_no_engine(self):
        """set_voice() does nothing when engine is None."""
        from timmy_serve.voice_tts import VoiceTTS

        tts = VoiceTTS.__new__(VoiceTTS)
        tts._engine = None

        # Should not raise
        tts.set_voice("some_voice_id")


class TestVoiceTTSAvailableProperty:
    """Test VoiceTTS available property."""

    def test_available_returns_true_when_initialized(self):
        """available property returns True when engine initialized."""
        from timmy_serve.voice_tts import VoiceTTS

        tts = VoiceTTS.__new__(VoiceTTS)
        tts._available = True

        assert tts.available is True

    def test_available_returns_false_when_not_initialized(self):
        """available property returns False when engine not initialized."""
        from timmy_serve.voice_tts import VoiceTTS

        tts = VoiceTTS.__new__(VoiceTTS)
        tts._available = False

        assert tts.available is False
