"""Tests for LLM provider abstraction."""

from unittest.mock import AsyncMock, MagicMock, patch

import pytest

from fliiq.runtime.llm.providers import (
    AnthropicLLM,
    GeminiLLM,
    LLMConfig,
    LLMProvider,
    LLMResponse,
    OpenAILLM,
    ToolCall,
    create_llm,
)
from fliiq.runtime.llm.failover import FailoverLLM


def test_factory_returns_anthropic():
    config = LLMConfig(provider=LLMProvider.ANTHROPIC, api_key="test-key")
    llm = create_llm(config)
    assert isinstance(llm, AnthropicLLM)


def test_factory_returns_openai():
    config = LLMConfig(provider=LLMProvider.OPENAI, api_key="test-key")
    llm = create_llm(config)
    assert isinstance(llm, OpenAILLM)


def test_factory_returns_gemini():
    config = LLMConfig(provider=LLMProvider.GEMINI, api_key="test-key")
    llm = create_llm(config)
    assert isinstance(llm, GeminiLLM)


def test_config_requires_api_key():
    with pytest.raises(Exception):
        LLMConfig(provider=LLMProvider.ANTHROPIC, api_key="")


def test_config_default_model_is_none():
    config = LLMConfig(provider=LLMProvider.ANTHROPIC, api_key="test-key")
    assert config.model is None


def test_default_model_applied():
    config = LLMConfig(provider=LLMProvider.ANTHROPIC, api_key="test-key")
    llm = create_llm(config)
    assert llm.model == "claude-sonnet-4-5-20250929"


def test_custom_model_preserved():
    config = LLMConfig(provider=LLMProvider.ANTHROPIC, api_key="test-key", model="claude-3-haiku-20240307")
    llm = create_llm(config)
    assert llm.model == "claude-3-haiku-20240307"


def test_failover_requires_configs():
    with pytest.raises(ValueError, match="at least one"):
        FailoverLLM([])


async def test_failover_tries_second_on_first_failure():
    """First provider fails, second succeeds."""
    mock_response = LLMResponse(content="ok", model="test", provider=LLMProvider.OPENAI)

    provider1 = AsyncMock()
    provider1.generate = AsyncMock(side_effect=RuntimeError("provider1 down"))
    provider1.config = LLMConfig(provider=LLMProvider.ANTHROPIC, api_key="k1")

    provider2 = AsyncMock()
    provider2.generate = AsyncMock(return_value=mock_response)
    provider2.config = LLMConfig(provider=LLMProvider.OPENAI, api_key="k2")

    configs = [
        LLMConfig(provider=LLMProvider.ANTHROPIC, api_key="k1"),
        LLMConfig(provider=LLMProvider.OPENAI, api_key="k2"),
    ]
    failover = FailoverLLM(configs)
    # Replace internal providers with mocks
    failover._providers = [provider1, provider2]

    result = await failover.generate([{"role": "user", "content": "hi"}])
    assert result.content == "ok"
    provider1.generate.assert_called_once()
    provider2.generate.assert_called_once()


async def test_failover_raises_when_all_fail():
    provider1 = AsyncMock()
    provider1.generate = AsyncMock(side_effect=RuntimeError("down"))
    provider1.config = LLMConfig(provider=LLMProvider.ANTHROPIC, api_key="k1")

    configs = [LLMConfig(provider=LLMProvider.ANTHROPIC, api_key="k1")]
    failover = FailoverLLM(configs)
    failover._providers = [provider1]

    with pytest.raises(RuntimeError, match="All LLM providers failed"):
        await failover.generate([{"role": "user", "content": "hi"}])


# --- Tool use tests ---


def test_llm_response_backward_compat():
    """LLMResponse works without new fields (backward compatibility)."""
    resp = LLMResponse(content="hello", model="test", provider=LLMProvider.ANTHROPIC)
    assert resp.content == "hello"
    assert resp.tool_calls == []
    assert resp.stop_reason == "end_turn"
    assert resp.raw_content is None


def test_llm_response_with_tool_calls():
    """LLMResponse accepts tool_calls and stop_reason."""
    tc = ToolCall(id="tc_1", name="get_time", input={"tz": "UTC"})
    resp = LLMResponse(
        content="",
        model="test",
        provider=LLMProvider.ANTHROPIC,
        tool_calls=[tc],
        stop_reason="tool_use",
        raw_content=[{"type": "tool_use", "id": "tc_1", "name": "get_time", "input": {"tz": "UTC"}}],
    )
    assert len(resp.tool_calls) == 1
    assert resp.tool_calls[0].name == "get_time"
    assert resp.stop_reason == "tool_use"


def test_tool_call_model():
    """ToolCall model holds id, name, input."""
    tc = ToolCall(id="tc_abc", name="search", input={"query": "hello"})
    assert tc.id == "tc_abc"
    assert tc.name == "search"
    assert tc.input == {"query": "hello"}


async def test_anthropic_tool_use_parsing():
    """AnthropicLLM parses tool_use content blocks correctly."""
    config = LLMConfig(provider=LLMProvider.ANTHROPIC, api_key="test-key")
    llm = AnthropicLLM(config)

    # Mock the Anthropic response with tool_use blocks
    mock_text_block = MagicMock()
    mock_text_block.type = "text"
    mock_text_block.text = "Let me check the time."

    mock_tool_block = MagicMock()
    mock_tool_block.type = "tool_use"
    mock_tool_block.id = "toolu_123"
    mock_tool_block.name = "get_current_time"
    mock_tool_block.input = {"timezone": "UTC"}

    mock_usage = MagicMock()
    mock_usage.input_tokens = 50
    mock_usage.output_tokens = 30

    mock_response = MagicMock()
    mock_response.content = [mock_text_block, mock_tool_block]
    mock_response.model = "claude-sonnet-4-20250514"
    mock_response.stop_reason = "tool_use"
    mock_response.usage = mock_usage

    llm._client.messages.create = AsyncMock(return_value=mock_response)

    result = await llm.generate(
        [{"role": "user", "content": "what time is it?"}],
        tools=[{"name": "get_current_time", "description": "Get time", "input_schema": {"type": "object"}}],
    )

    assert result.content == "Let me check the time."
    assert len(result.tool_calls) == 1
    assert result.tool_calls[0].id == "toolu_123"
    assert result.tool_calls[0].name == "get_current_time"
    assert result.tool_calls[0].input == {"timezone": "UTC"}
    assert result.stop_reason == "tool_use"
    assert len(result.raw_content) == 2


async def test_anthropic_text_only_backward_compat():
    """AnthropicLLM still works for text-only responses (no tools)."""
    config = LLMConfig(provider=LLMProvider.ANTHROPIC, api_key="test-key")
    llm = AnthropicLLM(config)

    mock_text_block = MagicMock()
    mock_text_block.type = "text"
    mock_text_block.text = "Hello there!"

    mock_usage = MagicMock()
    mock_usage.input_tokens = 10
    mock_usage.output_tokens = 5

    mock_response = MagicMock()
    mock_response.content = [mock_text_block]
    mock_response.model = "claude-sonnet-4-20250514"
    mock_response.stop_reason = "end_turn"
    mock_response.usage = mock_usage

    llm._client.messages.create = AsyncMock(return_value=mock_response)

    result = await llm.generate([{"role": "user", "content": "hi"}])

    assert result.content == "Hello there!"
    assert result.tool_calls == []
    assert result.stop_reason == "end_turn"


async def test_failover_passes_tools():
    """FailoverLLM passes tools param through to providers."""
    mock_response = LLMResponse(
        content="", model="test", provider=LLMProvider.ANTHROPIC,
        tool_calls=[ToolCall(id="tc_1", name="test", input={})],
        stop_reason="tool_use",
    )

    provider1 = AsyncMock()
    provider1.generate = AsyncMock(return_value=mock_response)
    provider1.config = LLMConfig(provider=LLMProvider.ANTHROPIC, api_key="k1")

    configs = [LLMConfig(provider=LLMProvider.ANTHROPIC, api_key="k1")]
    failover = FailoverLLM(configs)
    failover._providers = [provider1]

    tools = [{"name": "test", "description": "test tool", "input_schema": {"type": "object"}}]
    result = await failover.generate([{"role": "user", "content": "hi"}], tools=tools)

    provider1.generate.assert_called_once_with(
        [{"role": "user", "content": "hi"}], None, tools=tools
    )
    assert len(result.tool_calls) == 1
