"""
Tests for runtime/adapters/grpc.py — GRPCAdapter.

Covers:
    - class loads and is a BaseAdapter subclass
    - endpoint scheme stripping (grpc://, grpcs://)
    - introspect returns fallback stub schema when reflection unavailable
    - introspect stub schema has correct tool structure
    - execute returns error for unknown tool
    - execute returns error on call failure
    - _find_tool lookup works correctly
"""

from __future__ import annotations

from unittest.mock import MagicMock, patch

import pytest

from runtime.adapters.base import BaseAdapter
from runtime.adapters.grpc import GRPCAdapter


# ── Fixtures ─────────────────────────────────────────────────────────────


@pytest.fixture
def adapter() -> GRPCAdapter:
    """Return a fresh GRPCAdapter pointed at a dummy endpoint."""
    return GRPCAdapter("grpc://localhost:50051")


@pytest.fixture
def secure_adapter() -> GRPCAdapter:
    """Return a GRPCAdapter for a secure (grpcs://) endpoint."""
    return GRPCAdapter("grpcs://localhost:50051")


@pytest.fixture
def bare_adapter() -> GRPCAdapter:
    """Return a GRPCAdapter with no scheme prefix."""
    return GRPCAdapter("localhost:50051")


# ── Class loading ────────────────────────────────────────────────────────


class TestClassLoading:
    def test_is_base_adapter_subclass(self) -> None:
        assert issubclass(GRPCAdapter, BaseAdapter)

    def test_instantiates_without_error(self, adapter: GRPCAdapter) -> None:
        assert adapter is not None

    def test_implements_introspect(self, adapter: GRPCAdapter) -> None:
        assert hasattr(adapter, "introspect")
        assert callable(adapter.introspect)

    def test_implements_execute(self, adapter: GRPCAdapter) -> None:
        assert hasattr(adapter, "execute")
        assert callable(adapter.execute)


# ── Endpoint parsing ────────────────────────────────────────────────────


class TestEndpointParsing:
    def test_strips_grpc_scheme(self, adapter: GRPCAdapter) -> None:
        assert adapter._endpoint == "localhost:50051"

    def test_strips_grpcs_scheme(self, secure_adapter: GRPCAdapter) -> None:
        assert secure_adapter._endpoint == "localhost:50051"

    def test_bare_endpoint_unchanged(self, bare_adapter: GRPCAdapter) -> None:
        assert bare_adapter._endpoint == "localhost:50051"

    def test_grpc_not_secure(self, adapter: GRPCAdapter) -> None:
        assert adapter._secure is False

    def test_grpcs_is_secure(self, secure_adapter: GRPCAdapter) -> None:
        assert secure_adapter._secure is True

    def test_bare_not_secure(self, bare_adapter: GRPCAdapter) -> None:
        assert bare_adapter._secure is False


# ── Introspect (fallback stub schema) ───────────────────────────────────


class TestIntrospectStubSchema:
    """When reflection is unavailable, introspect returns a generic 'call' tool."""

    def _get_info(self, adapter: GRPCAdapter) -> dict:
        """Call introspect with grpc import mocked to trigger fallback."""
        with patch("runtime.adapters.grpc._import_grpc") as mock_grpc, \
             patch("runtime.adapters.grpc._import_grpc_reflection", side_effect=ImportError):
            mock_channel = MagicMock()
            mock_grpc.return_value.insecure_channel.return_value = mock_channel
            return adapter.introspect()

    def test_returns_dict(self, adapter: GRPCAdapter) -> None:
        info = self._get_info(adapter)
        assert isinstance(info, dict)

    def test_has_tools_key(self, adapter: GRPCAdapter) -> None:
        info = self._get_info(adapter)
        assert "tools" in info

    def test_has_version_key(self, adapter: GRPCAdapter) -> None:
        info = self._get_info(adapter)
        assert "version" in info
        assert isinstance(info["version"], str)

    def test_has_endpoint_key(self, adapter: GRPCAdapter) -> None:
        info = self._get_info(adapter)
        assert info["endpoint"] == "localhost:50051"

    def test_fallback_has_one_tool(self, adapter: GRPCAdapter) -> None:
        info = self._get_info(adapter)
        assert len(info["tools"]) == 1

    def test_fallback_tool_named_call(self, adapter: GRPCAdapter) -> None:
        info = self._get_info(adapter)
        tool = info["tools"][0]
        assert tool["name"] == "call"

    def test_fallback_tool_has_description(self, adapter: GRPCAdapter) -> None:
        info = self._get_info(adapter)
        tool = info["tools"][0]
        assert "description" in tool
        assert "gRPC" in tool["description"]

    def test_fallback_tool_has_parameters(self, adapter: GRPCAdapter) -> None:
        info = self._get_info(adapter)
        tool = info["tools"][0]
        param_names = [p["name"] for p in tool["parameters"]]
        assert "service" in param_names
        assert "method" in param_names
        assert "payload" in param_names

    def test_service_and_method_params_required(self, adapter: GRPCAdapter) -> None:
        info = self._get_info(adapter)
        tool = info["tools"][0]
        params = {p["name"]: p for p in tool["parameters"]}
        assert params["service"]["required"] is True
        assert params["method"]["required"] is True
        assert params["payload"]["required"] is False

    def test_fallback_tool_service_and_method_are_none(self, adapter: GRPCAdapter) -> None:
        info = self._get_info(adapter)
        tool = info["tools"][0]
        assert tool["service"] is None
        assert tool["method"] is None

    def test_introspect_stable_across_calls(self, adapter: GRPCAdapter) -> None:
        """Calling introspect twice returns equal but distinct tool lists."""
        with patch("runtime.adapters.grpc._import_grpc") as mock_grpc, \
             patch("runtime.adapters.grpc._import_grpc_reflection", side_effect=ImportError):
            mock_grpc.return_value.insecure_channel.return_value = MagicMock()
            first = adapter.introspect()
            second = adapter.introspect()
        assert first["tools"] == second["tools"]


# ── Execute ─────────────────────────────────────────────────────────────


class TestExecute:
    def test_unknown_tool_returns_error(self, adapter: GRPCAdapter) -> None:
        with patch("runtime.adapters.grpc._import_grpc") as mock_grpc, \
             patch("runtime.adapters.grpc._import_grpc_reflection", side_effect=ImportError):
            mock_grpc.return_value.insecure_channel.return_value = MagicMock()
            result = adapter.execute("nonexistent", {})
        assert result["status"] == "error"
        assert "Unknown tool" in result["result"]

    def test_call_tool_missing_service_returns_error(self, adapter: GRPCAdapter) -> None:
        with patch("runtime.adapters.grpc._import_grpc") as mock_grpc, \
             patch("runtime.adapters.grpc._import_grpc_reflection", side_effect=ImportError):
            mock_grpc.return_value.insecure_channel.return_value = MagicMock()
            result = adapter.execute("call", {"method": "Foo"})
        assert result["status"] == "error"

    def test_call_tool_missing_method_returns_error(self, adapter: GRPCAdapter) -> None:
        with patch("runtime.adapters.grpc._import_grpc") as mock_grpc, \
             patch("runtime.adapters.grpc._import_grpc_reflection", side_effect=ImportError):
            mock_grpc.return_value.insecure_channel.return_value = MagicMock()
            result = adapter.execute("call", {"service": "my.Service"})
        assert result["status"] == "error"

    def test_call_tool_success(self, adapter: GRPCAdapter) -> None:
        mock_channel = MagicMock()
        mock_callable = MagicMock(return_value=b'{"ok": true}')
        mock_channel.unary_unary.return_value = mock_callable

        with patch("runtime.adapters.grpc._import_grpc") as mock_grpc, \
             patch("runtime.adapters.grpc._import_grpc_reflection", side_effect=ImportError):
            mock_grpc.return_value.insecure_channel.return_value = mock_channel
            result = adapter.execute("call", {
                "service": "my.Service",
                "method": "DoThing",
                "payload": {"key": "value"},
            })

        assert result["status"] == "ok"
        assert result["result"] == {"ok": True}
        mock_channel.unary_unary.assert_called_once()
        call_args = mock_channel.unary_unary.call_args
        assert call_args[0][0] == "/my.Service/DoThing"

    def test_call_with_string_payload(self, adapter: GRPCAdapter) -> None:
        mock_channel = MagicMock()
        mock_callable = MagicMock(return_value=b'{"parsed": true}')
        mock_channel.unary_unary.return_value = mock_callable

        with patch("runtime.adapters.grpc._import_grpc") as mock_grpc, \
             patch("runtime.adapters.grpc._import_grpc_reflection", side_effect=ImportError):
            mock_grpc.return_value.insecure_channel.return_value = mock_channel
            result = adapter.execute("call", {
                "service": "my.Service",
                "method": "DoThing",
                "payload": '{"key": "val"}',
            })

        assert result["status"] == "ok"
        assert result["result"] == {"parsed": True}


# ── _find_tool ──────────────────────────────────────────────────────────


class TestFindTool:
    def test_finds_existing_tool(self, adapter: GRPCAdapter) -> None:
        with patch("runtime.adapters.grpc._import_grpc") as mock_grpc, \
             patch("runtime.adapters.grpc._import_grpc_reflection", side_effect=ImportError):
            mock_grpc.return_value.insecure_channel.return_value = MagicMock()
            adapter.introspect()  # populate _tools
        assert adapter._find_tool("call") is not None

    def test_returns_none_for_missing_tool(self, adapter: GRPCAdapter) -> None:
        with patch("runtime.adapters.grpc._import_grpc") as mock_grpc, \
             patch("runtime.adapters.grpc._import_grpc_reflection", side_effect=ImportError):
            mock_grpc.return_value.insecure_channel.return_value = MagicMock()
            adapter.introspect()
        assert adapter._find_tool("nonexistent") is None
