import json
import sys
from types import ModuleType

import pytest

from mcpbridge.adapters import (
    AnthropicAdapter,
    AzureOpenAIAdapter,
    BedrockAdapter,
    CohereAdapter,
    GeminiAdapter,
    GroqAdapter,
    MistralAdapter,
    OllamaAdapter,
    OpenAIAdapter,
    OpenAICompatibleAdapter,
    TogetherAdapter,
)
from mcpbridge.adapters.base import ToolResult
from mcpbridge.config import LLMConfig


def _install_dummy(monkeypatch, name: str) -> None:
    mod = ModuleType(name)
    monkeypatch.setitem(sys.modules, name, mod)


@pytest.fixture(autouse=True)
def dummy_provider_sdks(monkeypatch):
    # openai
    _install_dummy(monkeypatch, "openai")
    # anthropic
    _install_dummy(monkeypatch, "anthropic")
    # google.generativeai
    google = ModuleType("google")
    genai = ModuleType("google.generativeai")
    monkeypatch.setitem(sys.modules, "google", google)
    monkeypatch.setitem(sys.modules, "google.generativeai", genai)
    # other vendor packages
    _install_dummy(monkeypatch, "mistralai")
    _install_dummy(monkeypatch, "cohere")
    _install_dummy(monkeypatch, "groq")
    _install_dummy(monkeypatch, "ollama")
    _install_dummy(monkeypatch, "together")
    _install_dummy(monkeypatch, "boto3")


def test_openai_compatible_extract_and_append_tool_results():
    adapter = OpenAICompatibleAdapter(LLMConfig(provider="openai_compatible", model="m", api_key="k"))
    response = {
        "choices": [
            {
                "message": {
                    "content": "preamble",
                    "tool_calls": [
                        {"id": "1", "function": {"name": "toolA", "arguments": json.dumps({"x": 1})}}
                    ],
                }
            }
        ]
    }
    tcs = adapter.extract_tool_calls(response)
    assert len(tcs) == 1
    assert tcs[0].id == "1"
    assert tcs[0].name == "toolA"
    assert tcs[0].arguments["x"] == 1

    messages = []
    tool_results = [ToolResult(tool_call_id="1", name="toolA", content="RESULT")]
    messages2 = adapter.append_tool_results(messages, response, tool_results)
    assert messages2[-1]["role"] == "tool"
    assert messages2[-1]["content"] == "RESULT"


@pytest.mark.parametrize(
    "adapter_cls, provider, response_shape",
    [
        (OpenAIAdapter, "openai", {"choices": [{"message": {"content": "hi", "tool_calls": [{"id": "1", "function": {"name": "toolA", "arguments": "{\"x\":1}"}}]}}]}),
        (AzureOpenAIAdapter, "azure_openai", {"choices": [{"message": {"content": "hi", "tool_calls": [{"id": "1", "function": {"name": "toolA", "arguments": "{\"x\":1}"}}]}}]}),
        (GroqAdapter, "groq", {"choices": [{"message": {"content": "hi", "tool_calls": [{"id": "1", "function": {"name": "toolA", "arguments": "{\"x\":1}"}}]}}]}),
        (TogetherAdapter, "together", {"choices": [{"message": {"content": "hi", "tool_calls": [{"id": "1", "function": {"name": "toolA", "arguments": "{\"x\":1}"}}]}}]}),
        (MistralAdapter, "mistral", {"choices": [{"message": {"content": "hi", "tool_calls": [{"id": "1", "function": {"name": "toolA", "arguments": "{\"x\":1}"}}]}}]}),
    ],
)
def test_openai_style_extract_tool_calls(adapter_cls, provider, response_shape):
    llm_cfg_kwargs = {"provider": provider, "model": "m", "api_key": "k"}
    if provider == "azure_openai":
        llm_cfg_kwargs["azure_deployment"] = "dep"
        llm_cfg_kwargs["base_url"] = "https://example.openai.azure.com"
        llm_cfg_kwargs["azure_api_version"] = "2024-02-01"
    adapter = adapter_cls(LLMConfig(**llm_cfg_kwargs))

    tcs = adapter.extract_tool_calls(response_shape)
    assert len(tcs) == 1
    assert tcs[0].id == "1"
    assert tcs[0].name == "toolA"


def test_ollama_extract_tool_calls():
    adapter = OllamaAdapter(LLMConfig(provider="ollama", model="m", base_url="http://localhost:11434"))
    response = {"message": {"content": "hi", "tool_calls": [{"id": "1", "function": {"name": "toolA", "arguments": {"x": 1}}}]}}
    tcs = adapter.extract_tool_calls(response)
    assert len(tcs) == 1
    assert tcs[0].arguments["x"] == 1


def test_anthropic_extract_and_append_tool_results():
    adapter = AnthropicAdapter(LLMConfig(provider="anthropic", model="m", api_key="k"))
    response = {
        "content": [
            {"type": "text", "text": "preamble"},
            {"type": "tool_use", "id": "t1", "name": "toolA", "input": {"x": 1}},
        ]
    }
    tcs = adapter.extract_tool_calls(response)
    assert len(tcs) == 1
    assert tcs[0].id == "t1"
    assert tcs[0].arguments["x"] == 1

    messages = []
    tool_results = [ToolResult(tool_call_id="t1", name="toolA", content="R1")]
    messages2 = adapter.append_tool_results(messages, response, tool_results)
    assert messages2[-1]["role"] == "user"
    assert messages2[-1]["content"][0]["type"] == "tool_result"
    assert messages2[-1]["content"][0]["tool_use_id"] == "t1"


def test_gemini_extract_and_append_tool_results():
    adapter = GeminiAdapter(LLMConfig(provider="gemini", model="m", api_key="k"))
    response = {"candidates": [{"content": {"parts": [{"function_call": {"id": "fc1", "name": "toolA", "args": {"x": 1}}}, {"text": "hi"}]}}]}
    tcs = adapter.extract_tool_calls(response)
    assert len(tcs) == 1
    assert tcs[0].id == "fc1"
    assert tcs[0].arguments["x"] == 1

    messages = []
    tool_results = [ToolResult(tool_call_id="fc1", name="toolA", content="R2")]
    messages2 = adapter.append_tool_results(messages, response, tool_results)
    assert messages2[0]["role"] == "assistant"
    assert messages2[1]["role"] == "user"
    assert "function_response" in messages2[1]["parts"][0]


def test_gemini_extract_tool_calls_missing_id_falls_back_to_index():
    adapter = GeminiAdapter(LLMConfig(provider="gemini", model="m", api_key="k"))
    response = {
        "candidates": [
            {
                "content": {
                    "parts": [
                        # Gemini variant that omits `id`/`tool_call_id`.
                        {"function_call": {"name": "toolA", "args": {"x": 1}}},
                        {"text": "hi"},
                    ]
                }
            }
        ]
    }

    tcs = adapter.extract_tool_calls(response)
    assert len(tcs) == 1
    assert tcs[0].id == "0"
    assert tcs[0].name == "toolA"
    assert tcs[0].arguments["x"] == 1

    messages: list[dict] = []
    tool_results = [ToolResult(tool_call_id=tcs[0].id, name="toolA", content="R2")]
    messages2 = adapter.append_tool_results(messages, response, tool_results)
    assert messages2[0]["role"] == "assistant"
    assert messages2[1]["role"] == "user"
    fr = messages2[1]["parts"][0].get("function_response")
    assert isinstance(fr, dict)
    assert fr.get("id") == "0"


def test_cohere_extract_and_append_tool_results():
    adapter = CohereAdapter(LLMConfig(provider="cohere", model="m", api_key="k"))
    response = {"tool_calls": [{"id": "c1", "name": "toolA", "arguments": "{\"x\":1}"}], "text": "done"}
    tcs = adapter.extract_tool_calls(response)
    assert len(tcs) == 1
    assert tcs[0].id == "c1"
    messages = []
    tool_results = [ToolResult(tool_call_id="c1", name="toolA", content="R3")]
    messages2 = adapter.append_tool_results(messages, response, tool_results)
    assert messages2[1]["role"] == "tool"


def test_bedrock_extract_and_append_tool_results():
    adapter = BedrockAdapter(LLMConfig(provider="bedrock", model="m", aws_region="us-east-1"))
    response = {
        "output": {
            "message": {
                "content": [
                    {"toolUse": {"toolUseId": "b1", "name": "toolA", "input": {"x": 1}}},
                    {"text": {"text": "hello"}},
                ]
            }
        }
    }
    tcs = adapter.extract_tool_calls(response)
    assert len(tcs) == 1
    assert tcs[0].id == "b1"
    messages = []
    tool_results = [ToolResult(tool_call_id="b1", name="toolA", content="R4")]
    messages2 = adapter.append_tool_results(messages, response, tool_results)
    assert messages2[1]["role"] == "user"
    tr = messages2[1]["content"][0]["toolResult"]
    assert tr["toolUseId"] == "b1"

