"""Tests for provider bridges."""

import json
import pytest
from unittest.mock import AsyncMock, MagicMock, patch
from aiohttp import ClientResponseError

from backend.bridge.base import BridgeRequest, BridgeResponse, StreamResult, StreamUsage
from backend.bridge.factory import create_bridge
from backend.bridge.openai_bridge import OpenAIBridge
from backend.bridge.anthropic_bridge import (
    AnthropicBridge,
    _convert_messages_to_anthropic,
    _convert_tools_to_anthropic,
    _convert_tool_choice_to_anthropic,
    _convert_content_to_anthropic,
)
from backend.config import ProviderConfig, ModelConfig


@pytest.fixture
def openai_provider():
    return ProviderConfig(
        id="openai",
        type="openai",
        base_url="https://api.openai.com/v1",
        api_key="sk-test",
        models=[],
    )


@pytest.fixture
def anthropic_provider():
    return ProviderConfig(
        id="anthropic",
        type="anthropic",
        base_url="https://api.anthropic.com",
        api_key="sk-ant-test",
        models=[],
    )


@pytest.fixture
def model():
    return ModelConfig(
        id="test-model",
        input_cost_per_mtok=1.0,
        output_cost_per_mtok=2.0,
        max_context=128000,
    )


@pytest.fixture
def mock_session():
    return MagicMock()


class TestBridgeFactory:
    def test_create_openai_bridge(self, openai_provider, mock_session):
        bridge = create_bridge(openai_provider, mock_session)
        assert isinstance(bridge, OpenAIBridge)
        assert bridge.provider_type == "openai"

    def test_create_anthropic_bridge(self, anthropic_provider, mock_session):
        bridge = create_bridge(anthropic_provider, mock_session)
        assert isinstance(bridge, AnthropicBridge)
        assert bridge.provider_type == "anthropic"

    def test_create_openai_compatible_bridge(self, mock_session):
        provider = ProviderConfig(
            id="local", type="openai-compatible",
            base_url="http://localhost:11434/v1", models=[],
        )
        bridge = create_bridge(provider, mock_session)
        assert isinstance(bridge, OpenAIBridge)

    def test_unsupported_provider_raises(self, mock_session):
        provider = ProviderConfig(
            id="unknown", type="unsupported",
            base_url="http://localhost", models=[],
        )
        with pytest.raises(ValueError, match="Unsupported provider type"):
            create_bridge(provider, mock_session)


class TestOpenAIBridge:
    @pytest.mark.asyncio
    async def test_complete_success(self, openai_provider, model, mock_session):
        mock_response = AsyncMock()
        mock_response.status = 200
        mock_response.json = AsyncMock(return_value={
            "id": "chatcmpl-123",
            "model": "test-model",
            "choices": [{
                "index": 0,
                "message": {"role": "assistant", "content": "Hello!"},
                "finish_reason": "stop",
            }],
            "usage": {"prompt_tokens": 10, "completion_tokens": 5},
        })
        mock_response.__aenter__ = AsyncMock(return_value=mock_response)
        mock_response.__aexit__ = AsyncMock()
        mock_session.post = MagicMock(return_value=mock_response)

        bridge = OpenAIBridge(openai_provider, mock_session)
        request = BridgeRequest(
            messages=[{"role": "user", "content": "Hi"}],
            model="test-model",
        )
        response = await bridge.complete(request, model)

        assert response.content == "Hello!"
        assert response.tokens_in == 10
        assert response.tokens_out == 5
        assert response.finish_reason == "stop"

    @pytest.mark.asyncio
    async def test_complete_error(self, openai_provider, model):
        mock_response = AsyncMock()
        mock_response.status = 500
        mock_response.text = AsyncMock(return_value="Internal error")
        mock_response.request_info = MagicMock()
        mock_response.history = ()
        mock_response.__aenter__ = AsyncMock(return_value=mock_response)
        mock_response.__aexit__ = AsyncMock(return_value=False)
        
        session = MagicMock()
        session.post = MagicMock(return_value=mock_response)

        bridge = OpenAIBridge(openai_provider, session)
        request = BridgeRequest(
            messages=[{"role": "user", "content": "Hi"}],
            model="test-model",
        )
        
        # The bridge raises ClientResponseError on non-200 status
        with pytest.raises(ClientResponseError):
            await bridge.complete(request, model)

    def test_build_url(self, openai_provider, mock_session):
        bridge = OpenAIBridge(openai_provider, mock_session)
        assert bridge._build_url() == "https://api.openai.com/v1/chat/completions"

    def test_build_url_without_v1(self, mock_session):
        provider = ProviderConfig(
            id="test", type="openai",
            base_url="http://localhost:8080", api_key="test", models=[],
        )
        bridge = OpenAIBridge(provider, mock_session)
        assert bridge._build_url() == "http://localhost:8080/v1/chat/completions"

    def test_build_headers(self, openai_provider, mock_session):
        bridge = OpenAIBridge(openai_provider, mock_session)
        headers = bridge._build_headers()
        assert headers["Authorization"] == "Bearer sk-test"
        assert headers["Content-Type"] == "application/json"

    def test_build_body(self, openai_provider, model, mock_session):
        bridge = OpenAIBridge(openai_provider, mock_session)
        request = BridgeRequest(
            messages=[{"role": "user", "content": "Hi"}],
            model="test-model",
            temperature=0.7,
            max_tokens=100,
            stream=False,
        )
        body = bridge._build_body(request, model)
        
        assert body["model"] == "test-model"
        assert body["messages"] == [{"role": "user", "content": "Hi"}]
        assert body["temperature"] == 0.7
        assert body["max_tokens"] == 100
        assert body["stream"] is False


class TestAnthropicBridge:
    def test_build_url(self, anthropic_provider, mock_session):
        bridge = AnthropicBridge(anthropic_provider, mock_session)
        assert bridge._build_url() == "https://api.anthropic.com/v1/messages"

    def test_build_headers(self, anthropic_provider, mock_session):
        bridge = AnthropicBridge(anthropic_provider, mock_session)
        headers = bridge._build_headers()
        assert headers["x-api-key"] == "sk-ant-test"
        assert headers["anthropic-version"] == "2023-06-01"

    @pytest.mark.asyncio
    async def test_complete_success(self, anthropic_provider, model, mock_session):
        mock_response = AsyncMock()
        mock_response.status = 200
        mock_response.json = AsyncMock(return_value={
            "id": "msg-123",
            "model": "test-model",
            "content": [{"type": "text", "text": "Hello!"}],
            "stop_reason": "end_turn",
            "usage": {
                "input_tokens": 10,
                "output_tokens": 5,
                "cache_read_input_tokens": 2,
                "cache_creation_input_tokens": 1,
            },
        })
        mock_response.__aenter__ = AsyncMock(return_value=mock_response)
        mock_response.__aexit__ = AsyncMock()
        mock_session.post = MagicMock(return_value=mock_response)

        bridge = AnthropicBridge(anthropic_provider, mock_session)
        request = BridgeRequest(
            messages=[{"role": "user", "content": "Hi"}],
            model="test-model",
        )
        response = await bridge.complete(request, model)

        assert response.content == "Hello!"
        assert response.tokens_in == 10
        assert response.tokens_out == 5
        assert response.cache_read_tokens == 2
        assert response.cache_creation_tokens == 1


class TestAnthropicMessageConversion:
    def test_simple_messages(self):
        messages = [
            {"role": "user", "content": "Hello"},
            {"role": "assistant", "content": "Hi there"},
        ]
        system, converted = _convert_messages_to_anthropic(messages)
        
        assert system is None
        assert len(converted) == 2
        assert converted[0]["role"] == "user"
        assert converted[1]["role"] == "assistant"

    def test_system_message_extraction(self):
        messages = [
            {"role": "system", "content": "You are helpful."},
            {"role": "user", "content": "Hello"},
        ]
        system, converted = _convert_messages_to_anthropic(messages)
        
        assert system == "You are helpful."
        assert len(converted) == 1
        assert converted[0]["role"] == "user"

    def test_multiple_system_messages(self):
        messages = [
            {"role": "system", "content": "Rule 1"},
            {"role": "system", "content": "Rule 2"},
            {"role": "user", "content": "Hello"},
        ]
        system, converted = _convert_messages_to_anthropic(messages)
        
        assert system == "Rule 1\n\nRule 2"
        assert len(converted) == 1

    def test_tool_calls_conversion(self):
        messages = [
            {"role": "user", "content": "Get weather"},
            {
                "role": "assistant",
                "content": "",
                "tool_calls": [{
                    "id": "call_123",
                    "type": "function",
                    "function": {
                        "name": "get_weather",
                        "arguments": '{"city": "Paris"}',
                    },
                }],
            },
        ]
        system, converted = _convert_messages_to_anthropic(messages)
        
        assert len(converted) == 2
        assert converted[1]["content"][0]["type"] == "tool_use"
        assert converted[1]["content"][0]["name"] == "get_weather"
        assert converted[1]["content"][0]["input"] == {"city": "Paris"}

    def test_tool_result_conversion(self):
        messages = [
            {
                "role": "tool",
                "tool_call_id": "call_123",
                "content": "Sunny, 25°C",
            },
        ]
        system, converted = _convert_messages_to_anthropic(messages)
        
        assert len(converted) == 1
        assert converted[0]["role"] == "user"
        assert converted[0]["content"][0]["type"] == "tool_result"
        assert converted[0]["content"][0]["tool_use_id"] == "call_123"


class TestAnthropicToolConversion:
    def test_convert_tools(self):
        tools = [{
            "type": "function",
            "function": {
                "name": "get_weather",
                "description": "Get weather info",
                "parameters": {
                    "type": "object",
                    "properties": {"city": {"type": "string"}},
                },
            },
        }]
        converted = _convert_tools_to_anthropic(tools)
        
        assert len(converted) == 1
        assert converted[0]["name"] == "get_weather"
        assert converted[0]["description"] == "Get weather info"
        assert "input_schema" in converted[0]

    def test_convert_tools_empty(self):
        assert _convert_tools_to_anthropic(None) is None
        assert _convert_tools_to_anthropic([]) is None


class TestAnthropicToolChoiceConversion:
    def test_auto(self):
        assert _convert_tool_choice_to_anthropic("auto") == {"type": "auto"}

    def test_none(self):
        assert _convert_tool_choice_to_anthropic("none") is None

    def test_required(self):
        assert _convert_tool_choice_to_anthropic("required") == {"type": "any"}

    def test_specific_function(self):
        choice = {"type": "function", "function": {"name": "my_tool"}}
        assert _convert_tool_choice_to_anthropic(choice) == {"type": "tool", "name": "my_tool"}

    def test_null(self):
        assert _convert_tool_choice_to_anthropic(None) is None


class TestAnthropicContentConversion:
    def test_string_passthrough(self):
        assert _convert_content_to_anthropic("hello") == "hello"

    def test_text_parts(self):
        content = [
            {"type": "text", "text": "Hello"},
            {"type": "text", "text": "World"},
        ]
        result = _convert_content_to_anthropic(content)
        assert len(result) == 2
        assert result[0]["text"] == "Hello"

    def test_image_base64(self):
        content = [{
            "type": "image_url",
            "image_url": {"url": "data:image/png;base64,abc123"},
        }]
        result = _convert_content_to_anthropic(content)
        assert len(result) == 1
        assert result[0]["type"] == "image"
        assert result[0]["source"]["type"] == "base64"
        assert result[0]["source"]["media_type"] == "image/png"
        assert result[0]["source"]["data"] == "abc123"


class TestStreamResult:
    def test_stream_result_usage(self):
        async def mock_iter():
            yield "data: test"
        
        result = StreamResult(mock_iter())
        assert isinstance(result.usage, StreamUsage)
        assert result.usage.tokens_in == 0
        assert result.usage.tokens_out == 0

    @pytest.mark.asyncio
    async def test_stream_iteration(self):
        async def mock_iter():
            yield "line1"
            yield "line2"
        
        result = StreamResult(mock_iter())
        lines = [line async for line in result]
        assert lines == ["line1", "line2"]
