"""Tests for STT plugin dispatch in tools/transcription_tools.py.

Covers the resolution invariants of the new plugin dispatcher (follow-up
to #30398 — STT pluggability):

1. Built-in provider names short-circuit — plugins NEVER win over a
   built-in. Even if a plugin somehow ended up in the registry with a
   built-in name (which the registry blocks), the dispatcher re-checks
   defensively.
2. Unknown name with no plugin → returns None (caller surfaces the
   "provider_not_registered" error).
3. Unknown name with plugin registered → dispatches, returns result.
4. Plugin exceptions are caught and converted to the standard error
   envelope.
5. Plugin returning non-dict → caught with error envelope.
6. Plugin result has ``provider`` field stamped if missing.
"""

from __future__ import annotations

import pytest

from agent import transcription_registry
from agent.transcription_provider import TranscriptionProvider
from tools import transcription_tools


class _FakeProvider(TranscriptionProvider):
    def __init__(
        self,
        name: str,
        result: dict | None = None,
        raise_exc: BaseException | None = None,
        available: bool = True,
        available_raises: BaseException | None = None,
    ):
        self._name = name
        self._result = result
        self._raise_exc = raise_exc
        self._available = available
        self._available_raises = available_raises
        self.last_call: dict | None = None

    @property
    def name(self) -> str:
        return self._name

    def is_available(self) -> bool:
        if self._available_raises is not None:
            raise self._available_raises
        return self._available

    def transcribe(self, file_path: str, **kw):
        self.last_call = {"file_path": file_path, "kwargs": dict(kw)}
        if self._raise_exc is not None:
            raise self._raise_exc
        if self._result is not None:
            return self._result
        return {"success": True, "transcript": "fake transcript", "provider": self._name}


@pytest.fixture(autouse=True)
def _reset_registry():
    transcription_registry._reset_for_tests()
    yield
    transcription_registry._reset_for_tests()


@pytest.fixture
def sample_audio_file(tmp_path):
    """Return a real, small input so E2E tests exercise validation too."""
    audio_path = tmp_path / "audio.mp3"
    audio_path.write_bytes(b"fake audio data")
    return str(audio_path)


# ---------------------------------------------------------------------------
# Built-in always wins
# ---------------------------------------------------------------------------


class TestBuiltinAlwaysWins:
    """Built-in STT provider names short-circuit the dispatcher.

    Even with a plugin registered (which the registry would reject —
    but the dispatcher is defensive), built-in names return None so
    the caller's elif chain handles them natively.
    """

    @pytest.mark.parametrize(
        "builtin",
        ["local", "local_command", "groq", "openai", "mistral", "xai"],
    )
    def test_dispatcher_short_circuits_builtin(self, builtin):
        result = transcription_tools._dispatch_to_plugin_provider(
            "/tmp/audio.mp3", builtin,
        )
        assert result is None, (
            f"Built-in {builtin!r} must short-circuit plugin dispatch."
        )


    def test_dispatcher_short_circuits_builtin_case_insensitive(self):
        for variant in ("OPENAI", "OpenAI", "  openai  ", "oPeNaI"):
            assert (
                transcription_tools._dispatch_to_plugin_provider(
                    "/tmp/audio.mp3", variant,
                ) is None
            )


# ---------------------------------------------------------------------------
# Unknown names
# ---------------------------------------------------------------------------


class TestPluginDispatch:
    def test_registered_plugin_called(self):
        provider = _FakeProvider(name="openrouter")
        transcription_registry.register_provider(provider)

        result = transcription_tools._dispatch_to_plugin_provider(
            "/tmp/audio.mp3", "openrouter",
        )
        assert result is not None
        assert result["success"] is True
        assert result["transcript"] == "fake transcript"
        assert result["provider"] == "openrouter"
        assert provider.last_call is not None
        assert provider.last_call["file_path"] == "/tmp/audio.mp3"

    def test_unregistered_name_returns_none(self):
        """Unknown name + no plugin → return None so the caller surfaces
        the legacy 'No STT provider available' error."""
        result = transcription_tools._dispatch_to_plugin_provider(
            "/tmp/audio.mp3", "unknown-stt",
        )
        assert result is None


    def test_provider_field_stamped_if_missing(self):
        """If a plugin forgets to set ``provider`` in its result, the
        dispatcher stamps it from the registered name."""
        provider = _FakeProvider(
            name="openrouter",
            result={"success": True, "transcript": "hi"},  # no provider key
        )
        transcription_registry.register_provider(provider)

        result = transcription_tools._dispatch_to_plugin_provider(
            "/tmp/audio.mp3", "openrouter",
        )
        assert result is not None
        assert result["provider"] == "openrouter"


# ---------------------------------------------------------------------------
# End-to-end via transcribe_audio
# ---------------------------------------------------------------------------


class TestTranscribeAudioE2E:
    """transcribe_audio() routes plugin dispatch correctly when the
    configured name is unknown to the built-in branches.

    Provider resolution is mocked to isolate routing, while a real small
    input keeps the validation chain active.
    """

    def test_unknown_name_with_plugin_dispatches(self, sample_audio_file):
        from unittest.mock import patch
        provider = _FakeProvider(name="openrouter")
        transcription_registry.register_provider(provider)

        with patch("tools.transcription_tools._load_stt_config", return_value={"provider": "openrouter"}), \
             patch("tools.transcription_tools.is_stt_enabled", return_value=True), \
             patch("tools.transcription_tools._get_provider", return_value="openrouter"):
            result = transcription_tools.transcribe_audio(sample_audio_file)

        assert result["success"] is True
        assert result["transcript"] == "fake transcript"
        assert result["provider"] == "openrouter"


    def test_builtin_name_does_not_consult_plugin_registry(self, sample_audio_file):
        """Even if a plugin's name collides with a built-in (which the
        registry blocks, but defense in depth matters), transcribe_audio
        with provider='groq' goes through the legacy elif chain, never
        the plugin dispatcher."""
        from unittest.mock import patch
        # Register a plugin that WOULD respond to 'openrouter' — but
        # we're asking for 'groq', so it shouldn't be called.
        provider = _FakeProvider(name="openrouter")
        transcription_registry.register_provider(provider)

        with patch("tools.transcription_tools._load_stt_config", return_value={"provider": "groq"}), \
             patch("tools.transcription_tools._get_provider", return_value="groq"), \
             patch("tools.transcription_tools._transcribe_groq",
                   return_value={"success": True, "transcript": "from groq", "provider": "groq"}) as mock_groq:
            result = transcription_tools.transcribe_audio(sample_audio_file)

        assert result["provider"] == "groq"
        assert result["transcript"] == "from groq"
        mock_groq.assert_called_once()
        # Plugin was never called
        assert provider.last_call is None

    def test_oversized_plugin_file_is_rejected_before_dispatch(self, tmp_path):
        from unittest.mock import patch

        provider = _FakeProvider(name="openrouter")
        transcription_registry.register_provider(provider)
        audio_path = tmp_path / "oversized.mp3"
        with audio_path.open("wb") as audio_file:
            audio_file.seek(transcription_tools.MAX_FILE_SIZE)
            audio_file.write(b"\0")

        with patch("tools.transcription_tools._load_stt_config", return_value={"provider": "openrouter"}), \
             patch("tools.transcription_tools.is_stt_enabled", return_value=True), \
             patch("tools.transcription_tools._get_provider", return_value="openrouter"):
            result = transcription_tools.transcribe_audio(str(audio_path))

        assert result["success"] is False
        assert "File too large" in result["error"]
        assert provider.last_call is None


# ---------------------------------------------------------------------------
# Availability gating (codex review feedback on PR #30493)
# ---------------------------------------------------------------------------


class TestAvailabilityGate:
    """When the configured plugin reports ``is_available() == False``,
    the dispatcher MUST short-circuit with a clear unavailability
    envelope instead of routing the call into a plugin that'll crash.

    The user explicitly set ``stt.provider: <plugin>`` so falling
    through to the generic "No STT provider available" message would
    be misleading — surface the plugin's own unavailability instead.
    """

    def test_unavailable_plugin_returns_envelope_not_none(self):
        provider = _FakeProvider(name="openrouter", available=False)
        transcription_registry.register_provider(provider)

        result = transcription_tools._dispatch_to_plugin_provider(
            "/tmp/audio.mp3", "openrouter",
        )
        assert result is not None, (
            "Unavailable plugin must return an envelope, not None — "
            "otherwise we fall through to the generic auto-detect error "
            "even though the user explicitly opted into this plugin."
        )
        assert result["success"] is False
        assert result["provider"] == "openrouter"
        assert "not available" in result["error"]
        # Plugin's transcribe MUST NOT have been called
        assert provider.last_call is None


    def test_unavailable_plugin_at_transcribe_audio_level(self, sample_audio_file):
        """End-to-end: ``stt.provider: openrouter`` + plugin reports
        unavailable → ``transcribe_audio`` returns the unavailability
        envelope, NOT the generic "No STT provider available" message.
        """
        from unittest.mock import patch
        provider = _FakeProvider(name="openrouter", available=False)
        transcription_registry.register_provider(provider)

        with patch("tools.transcription_tools._load_stt_config", return_value={"provider": "openrouter"}), \
             patch("tools.transcription_tools.is_stt_enabled", return_value=True), \
             patch("tools.transcription_tools._get_provider", return_value="openrouter"):
            result = transcription_tools.transcribe_audio(sample_audio_file)

        assert result["success"] is False
        # Must surface the plugin's unavailability — NOT the generic
        # "No STT provider available" auto-detect-failure message.
        assert "not available" in result["error"]
        assert "No STT provider available" not in result["error"]
        assert result["provider"] == "openrouter"


# ---------------------------------------------------------------------------
# Language forwarding from config (codex review feedback on PR #30493)
# ---------------------------------------------------------------------------


class TestLanguageForwardingFromConfig:
    """``transcribe_audio`` must forward ``stt.<provider>.language``
    from config to the plugin (mirrors how built-ins read
    ``stt.local.language``).
    """

    def test_language_read_from_provider_namespaced_config(self, sample_audio_file):
        """``stt.openrouter.language: ja`` reaches the plugin's
        transcribe() call as language='ja'."""
        from unittest.mock import patch
        provider = _FakeProvider(name="openrouter")
        transcription_registry.register_provider(provider)

        stt_config = {
            "provider": "openrouter",
            "openrouter": {"language": "ja"},
        }
        with patch("tools.transcription_tools._load_stt_config", return_value=stt_config), \
             patch("tools.transcription_tools.is_stt_enabled", return_value=True), \
             patch("tools.transcription_tools._get_provider", return_value="openrouter"):
            transcription_tools.transcribe_audio(sample_audio_file)

        assert provider.last_call is not None
        assert provider.last_call["kwargs"]["language"] == "ja"


    def test_non_dict_provider_namespace_does_not_crash(self, sample_audio_file):
        """If someone accidentally writes ``stt.openrouter: "foo"`` (a
        string instead of a dict), we should not crash — treat as
        empty config."""
        from unittest.mock import patch
        provider = _FakeProvider(name="openrouter")
        transcription_registry.register_provider(provider)

        stt_config = {"provider": "openrouter", "openrouter": "garbage"}
        with patch("tools.transcription_tools._load_stt_config", return_value=stt_config), \
             patch("tools.transcription_tools.is_stt_enabled", return_value=True), \
             patch("tools.transcription_tools._get_provider", return_value="openrouter"):
            result = transcription_tools.transcribe_audio(sample_audio_file)

        # Should still dispatch successfully (config is just ignored)
        assert result["success"] is True
        assert provider.last_call["kwargs"]["language"] is None
        assert provider.last_call["kwargs"]["model"] is None
