from __future__ import annotations

import json
from types import SimpleNamespace
from unittest.mock import AsyncMock, MagicMock

import pytest

from app.llm.providers.anthropic import AnthropicProvider
from app.llm.providers.deepseek import DeepSeekProvider
from app.llm.providers.gemini import GeminiProvider
from app.llm.providers.openai import OpenAIProvider


def _openai_chat_response(
    *,
    text: str = "",
    tool_calls: list | None = None,
    finish_reason: str = "stop",
    model: str = "gpt-4o",
):
    message = SimpleNamespace(content=text, tool_calls=tool_calls or [])
    choice = SimpleNamespace(message=message, finish_reason=finish_reason)
    return SimpleNamespace(
        choices=[choice],
        usage=SimpleNamespace(prompt_tokens=11, completion_tokens=7),
        model=model,
    )


def _openai_tool_call(call_id: str, name: str, arguments: dict):
    return SimpleNamespace(
        id=call_id,
        function=SimpleNamespace(name=name, arguments=json.dumps(arguments)),
    )


@pytest.fixture
def mock_openai_client():
    client = MagicMock()
    client.chat = MagicMock()
    client.chat.completions = MagicMock()
    client.chat.completions.create = AsyncMock()
    return client


@pytest.mark.asyncio
async def test_openai_provider_complete(mock_openai_client):
    mock_openai_client.chat.completions.create.return_value = _openai_chat_response(
        text="Hello from OpenAI"
    )
    provider = OpenAIProvider(client=mock_openai_client, api_key="sk-test")

    result = await provider.complete(
        [{"role": "user", "content": "Hi"}],
        model="gpt-4o",
    )

    assert result.text == "Hello from OpenAI"
    assert result.model == "gpt-4o"
    assert result.prompt_tokens == 11
    assert result.completion_tokens == 7
    assert result.latency_ms >= 0


@pytest.mark.asyncio
async def test_openai_provider_complete_with_tools(mock_openai_client):
    mock_openai_client.chat.completions.create.return_value = _openai_chat_response(
        text="Reading…",
        tool_calls=[_openai_tool_call("call-1", "read_file", {"path": "README.md"})],
        finish_reason="tool_calls",
    )
    provider = OpenAIProvider(client=mock_openai_client, api_key="sk-test")

    result = await provider.complete_with_tools(
        [{"role": "user", "content": "read README"}],
        [
            {
                "type": "function",
                "function": {
                    "name": "read_file",
                    "description": "Read a file",
                    "parameters": {"type": "object", "properties": {}},
                },
            }
        ],
        model="gpt-4o",
    )

    assert result.finish_reason == "tool_calls"
    assert result.model == "gpt-4o"
    assert len(result.tool_calls) == 1
    assert result.tool_calls[0].id == "call-1"
    assert result.tool_calls[0].name == "read_file"
    assert result.tool_calls[0].arguments == {"path": "README.md"}


@pytest.mark.asyncio
async def test_deepseek_provider_complete_with_tools(mock_openai_client):
    mock_openai_client.chat.completions.create.return_value = _openai_chat_response(
        text="",
        tool_calls=[
            _openai_tool_call("ds-1", "run_command", {"command": "ls"}),
        ],
        finish_reason="tool_calls",
        model="deepseek-chat",
    )
    provider = DeepSeekProvider(
        client=mock_openai_client,
        api_key="sk-deepseek",
        base_url="https://api.deepseek.com",
    )

    result = await provider.complete_with_tools(
        [{"role": "user", "content": "list files"}],
        [
            {
                "type": "function",
                "function": {
                    "name": "run_command",
                    "description": "Run a shell command",
                    "parameters": {"type": "object", "properties": {}},
                },
            }
        ],
        model="deepseek-chat",
    )

    assert result.model == "deepseek-chat"
    assert result.finish_reason == "tool_calls"
    assert result.tool_calls[0].name == "run_command"
    assert result.tool_calls[0].arguments == {"command": "ls"}


@pytest.mark.asyncio
async def test_anthropic_provider_complete():
    client = MagicMock()
    client.messages = MagicMock()
    client.messages.create = AsyncMock(
        return_value=SimpleNamespace(
            content=[SimpleNamespace(type="text", text="Hello from Claude")],
            usage=SimpleNamespace(input_tokens=9, output_tokens=4),
            stop_reason="end_turn",
        )
    )
    provider = AnthropicProvider(client=client, api_key="sk-ant")

    result = await provider.complete(
        [
            {"role": "system", "content": "Be brief."},
            {"role": "user", "content": "Hi"},
        ],
        model="claude-sonnet-4-6",
    )

    assert result.text == "Hello from Claude"
    assert result.model == "claude-sonnet-4-6"
    assert result.prompt_tokens == 9
    assert result.completion_tokens == 4

    kwargs = client.messages.create.await_args.kwargs
    assert kwargs["model"] == "claude-sonnet-4-6"
    assert kwargs["system"] == "Be brief."
    assert kwargs["messages"] == [{"role": "user", "content": "Hi"}]


@pytest.mark.asyncio
async def test_anthropic_provider_complete_with_tools():
    client = MagicMock()
    client.messages = MagicMock()
    client.messages.create = AsyncMock(
        return_value=SimpleNamespace(
            content=[
                SimpleNamespace(type="text", text="Reading…"),
                SimpleNamespace(
                    type="tool_use",
                    id="toolu_1",
                    name="read_file",
                    input={"path": "README.md"},
                ),
            ],
            usage=SimpleNamespace(input_tokens=12, output_tokens=8),
            stop_reason="tool_use",
        )
    )
    provider = AnthropicProvider(client=client, api_key="sk-ant")

    result = await provider.complete_with_tools(
        [{"role": "user", "content": "read README"}],
        [
            {
                "name": "read_file",
                "description": "Read a file",
                "input_schema": {
                    "type": "object",
                    "properties": {"path": {"type": "string"}},
                },
            }
        ],
        model="claude-sonnet-4-6",
    )

    assert result.text == "Reading…"
    assert result.finish_reason == "tool_calls"
    assert result.model == "claude-sonnet-4-6"
    assert len(result.tool_calls) == 1
    assert result.tool_calls[0].id == "toolu_1"
    assert result.tool_calls[0].name == "read_file"
    assert result.tool_calls[0].arguments == {"path": "README.md"}

    kwargs = client.messages.create.await_args.kwargs
    assert kwargs["tools"][0]["name"] == "read_file"
    assert kwargs["tool_choice"] == {"type": "auto"}


@pytest.mark.asyncio
async def test_gemini_provider_complete(monkeypatch):
    response = SimpleNamespace(
        text="Hello from Gemini",
        candidates=[
            SimpleNamespace(
                content=SimpleNamespace(parts=[SimpleNamespace(text="Hello from Gemini")]),
                finish_reason=SimpleNamespace(name="STOP"),
            )
        ],
        usage_metadata=SimpleNamespace(prompt_token_count=5, candidates_token_count=3),
        function_calls=None,
    )

    aio_models = MagicMock()
    aio_models.generate_content = AsyncMock(return_value=response)
    client = MagicMock()
    client.aio = MagicMock()
    client.aio.models = aio_models

    # Avoid constructing real FunctionDeclaration / Tool types when tools=None path.
    provider = GeminiProvider(client=client, api_key="sk-gem")

    result = await provider.complete(
        [{"role": "user", "content": "Hi"}],
        model="gemini-2.5-flash",
    )

    assert result.text == "Hello from Gemini"
    assert result.model == "gemini-2.5-flash"
    assert result.prompt_tokens == 5
    assert result.completion_tokens == 3


@pytest.mark.asyncio
async def test_gemini_provider_complete_with_tools(monkeypatch):
    part_text = SimpleNamespace(text="Reading…", function_call=None)
    part_fn = SimpleNamespace(
        text=None,
        function_call=SimpleNamespace(
            name="read_file",
            args={"path": "README.md"},
            id="",
            arguments=None,
        ),
    )
    response = SimpleNamespace(
        text="Reading…",
        candidates=[
            SimpleNamespace(
                content=SimpleNamespace(parts=[part_text, part_fn]),
                finish_reason=SimpleNamespace(name="STOP"),
            )
        ],
        usage_metadata=SimpleNamespace(prompt_token_count=6, candidates_token_count=4),
        function_calls=None,
    )

    aio_models = MagicMock()
    aio_models.generate_content = AsyncMock(return_value=response)
    client = MagicMock()
    client.aio = MagicMock()
    client.aio.models = aio_models

    # Construct with real types (HttpOptions), then stub config builders so
    # FunctionDeclaration schema validation does not depend on the live SDK.
    provider = GeminiProvider(client=client, api_key="sk-gem")

    fake_types = MagicMock()
    fake_types.AutomaticFunctionCallingConfig = MagicMock(
        side_effect=lambda **kwargs: SimpleNamespace(**kwargs)
    )
    fake_types.FunctionDeclaration = MagicMock(
        side_effect=lambda **kwargs: SimpleNamespace(**kwargs)
    )
    fake_types.Tool = MagicMock(side_effect=lambda **kwargs: SimpleNamespace(**kwargs))
    fake_types.ToolConfig = MagicMock(side_effect=lambda **kwargs: SimpleNamespace(**kwargs))
    fake_types.FunctionCallingConfig = MagicMock(
        side_effect=lambda **kwargs: SimpleNamespace(**kwargs)
    )
    fake_types.GenerateContentConfig = MagicMock(
        side_effect=lambda **kwargs: SimpleNamespace(**kwargs)
    )
    monkeypatch.setattr("app.llm.providers.gemini.types", fake_types)

    result = await provider.complete_with_tools(
        [{"role": "user", "content": "read README"}],
        [
            {
                "name": "read_file",
                "description": "Read a file",
                "parameters": {
                    "type": "object",
                    "properties": {"path": {"type": "string"}},
                },
            }
        ],
        model="gemini-2.5-flash",
    )

    assert result.text == "Reading…"
    assert result.model == "gemini-2.5-flash"
    assert result.finish_reason == "tool_calls"
    assert len(result.tool_calls) == 1
    assert result.tool_calls[0].name == "read_file"
    assert result.tool_calls[0].arguments == {"path": "README.md"}
