"""Tests for LiteLLM provider."""

import pytest
from inline_snapshot import snapshot
from unittest.mock import MagicMock, AsyncMock, patch

pytest.importorskip("litellm")

from agentic_security.llm_providers.litellm_provider import LiteLLMProvider
from agentic_security.llm_providers.base import (
    LLMMessage,
    LLMProviderError,
    LLMRateLimitError,
)


def _mock_response(
    content="Hello",
    model="openai/gpt-4o-mini",
    finish_reason="stop",
    prompt_tokens=10,
    completion_tokens=5,
    total_tokens=15,
):
    resp = MagicMock()
    resp.choices = [MagicMock()]
    resp.choices[0].message.content = content
    resp.choices[0].finish_reason = finish_reason
    resp.model = model
    resp.usage.prompt_tokens = prompt_tokens
    resp.usage.completion_tokens = completion_tokens
    resp.usage.total_tokens = total_tokens
    return resp


class TestLiteLLMProviderInit:
    def test_default_model(self):
        provider = LiteLLMProvider()
        assert provider.model == snapshot("openai/gpt-4o-mini")

    def test_custom_model(self):
        provider = LiteLLMProvider(model="anthropic/claude-sonnet-4-6")
        assert provider.model == snapshot("anthropic/claude-sonnet-4-6")

    def test_no_api_key_required(self):
        provider = LiteLLMProvider()
        assert provider._api_key is None

    def test_api_key_stored(self):
        provider = LiteLLMProvider(api_key="sk-test")
        assert provider._api_key == snapshot("sk-test")

    def test_api_base_stored(self):
        provider = LiteLLMProvider(api_base="http://localhost:4000")
        assert provider._api_base == snapshot("http://localhost:4000")


class TestLiteLLMProviderCallKwargs:
    def test_drop_params_always_true(self):
        provider = LiteLLMProvider()
        kwargs = provider._call_kwargs()
        assert kwargs["drop_params"] is True

    def test_api_key_forwarded_when_set(self):
        provider = LiteLLMProvider(api_key="sk-test")
        kwargs = provider._call_kwargs()
        assert kwargs["api_key"] == snapshot("sk-test")

    def test_api_key_omitted_when_none(self):
        provider = LiteLLMProvider()
        kwargs = provider._call_kwargs()
        assert "api_key" not in kwargs

    def test_api_base_forwarded_when_set(self):
        provider = LiteLLMProvider(api_base="http://localhost:4000")
        kwargs = provider._call_kwargs()
        assert kwargs["api_base"] == snapshot("http://localhost:4000")

    def test_api_base_omitted_when_none(self):
        provider = LiteLLMProvider()
        kwargs = provider._call_kwargs()
        assert "api_base" not in kwargs

    def test_model_in_kwargs(self):
        provider = LiteLLMProvider(model="groq/llama-3.3-70b-versatile")
        kwargs = provider._call_kwargs()
        assert kwargs["model"] == snapshot("groq/llama-3.3-70b-versatile")


class TestLiteLLMProviderMethods:
    def test_get_supported_models(self):
        models = LiteLLMProvider.get_supported_models()
        assert "openai/gpt-4o" in models
        assert "anthropic/claude-sonnet-4-6" in models

    def test_messages_to_dicts(self):
        provider = LiteLLMProvider()
        messages = [
            LLMMessage(role="system", content="Be helpful"),
            LLMMessage(role="user", content="Hello"),
        ]
        result = provider._messages_to_dicts(messages)
        assert result == snapshot(
            [
                {"role": "system", "content": "Be helpful"},
                {"role": "user", "content": "Hello"},
            ]
        )

    def test_parse_response(self):
        provider = LiteLLMProvider()
        resp = _mock_response(content="Hi!", model="openai/gpt-4o")
        result = provider._parse_response(resp)
        assert result.content == snapshot("Hi!")
        assert result.model == snapshot("openai/gpt-4o")
        assert result.finish_reason == snapshot("stop")
        assert result.usage == snapshot(
            {"prompt_tokens": 10, "completion_tokens": 5, "total_tokens": 15}
        )

    def test_parse_response_null_content(self):
        provider = LiteLLMProvider()
        resp = _mock_response(content=None)
        result = provider._parse_response(resp)
        assert result.content == snapshot("")

    def test_parse_response_no_usage(self):
        provider = LiteLLMProvider()
        resp = _mock_response()
        resp.usage = None
        result = provider._parse_response(resp)
        assert result.usage is None


class TestLiteLLMProviderSync:
    @pytest.fixture
    def provider(self):
        return LiteLLMProvider(model="openai/gpt-4o-mini")

    def test_sync_generate(self, provider):
        resp = _mock_response(content="Sync response")
        with patch(
            "agentic_security.llm_providers.litellm_provider.litellm.completion",
            return_value=resp,
        ) as mock_comp:
            result = provider.sync_generate("Hello")
            assert result.content == snapshot("Sync response")
            call_kwargs = mock_comp.call_args.kwargs
            assert call_kwargs["drop_params"] is True

    def test_sync_chat(self, provider):
        resp = _mock_response(content="Chat response")
        messages = [LLMMessage(role="user", content="Hi")]
        with patch(
            "agentic_security.llm_providers.litellm_provider.litellm.completion",
            return_value=resp,
        ):
            result = provider.sync_chat(messages)
            assert result.content == snapshot("Chat response")

    def test_sync_generate_with_system_prompt(self, provider):
        resp = _mock_response(content="With system")
        with patch(
            "agentic_security.llm_providers.litellm_provider.litellm.completion",
            return_value=resp,
        ) as mock_comp:
            result = provider.sync_generate("Hello", system_prompt="Be brief")
            assert result.content == snapshot("With system")
            messages = mock_comp.call_args.kwargs["messages"]
            assert messages[0]["role"] == "system"
            assert messages[0]["content"] == "Be brief"


class TestLiteLLMProviderAsync:
    @pytest.fixture
    def provider(self):
        return LiteLLMProvider(model="anthropic/claude-sonnet-4-6")

    @pytest.mark.asyncio
    async def test_generate(self, provider):
        resp = _mock_response(content="Async response")
        with patch(
            "agentic_security.llm_providers.litellm_provider.litellm.acompletion",
            new_callable=AsyncMock,
            return_value=resp,
        ):
            result = await provider.generate("Hello")
            assert result.content == snapshot("Async response")

    @pytest.mark.asyncio
    async def test_chat(self, provider):
        resp = _mock_response(content="Async chat")
        messages = [LLMMessage(role="user", content="Hi")]
        with patch(
            "agentic_security.llm_providers.litellm_provider.litellm.acompletion",
            new_callable=AsyncMock,
            return_value=resp,
        ) as mock_acomp:
            result = await provider.chat(messages)
            assert result.content == snapshot("Async chat")
            call_kwargs = mock_acomp.call_args.kwargs
            assert call_kwargs["model"] == "anthropic/claude-sonnet-4-6"
            assert call_kwargs["drop_params"] is True


class TestLiteLLMProviderErrors:
    @pytest.fixture
    def provider(self):
        return LiteLLMProvider()

    def test_rate_limit_maps_to_llm_rate_limit_error(self, provider):
        fake_exc = type("RateLimitError", (Exception,), {})()
        fake_exc.__class__.__module__ = "litellm.exceptions"
        fake_exc.__class__.__qualname__ = "RateLimitError"
        with pytest.raises(LLMRateLimitError):
            provider._handle_error(fake_exc)

    def test_generic_error_maps_to_llm_provider_error(self, provider):
        with pytest.raises(LLMProviderError):
            provider._handle_error(Exception("something went wrong"))

    def test_sync_chat_auth_error_raises_provider_error(self, provider):
        with patch(
            "agentic_security.llm_providers.litellm_provider.litellm.completion",
            side_effect=Exception("AuthenticationError: Invalid API key"),
        ):
            with pytest.raises(LLMProviderError, match="Invalid API key"):
                provider.sync_generate("test")

    @pytest.mark.asyncio
    async def test_async_chat_timeout_raises_provider_error(self, provider):
        with patch(
            "agentic_security.llm_providers.litellm_provider.litellm.acompletion",
            new_callable=AsyncMock,
            side_effect=Exception("Timeout: Request timed out"),
        ):
            with pytest.raises(LLMProviderError, match="timed out"):
                await provider.generate("test")

    @pytest.mark.asyncio
    async def test_async_chat_model_not_found_raises_provider_error(self, provider):
        provider = LiteLLMProvider(model="bad/nonexistent-model")
        with patch(
            "agentic_security.llm_providers.litellm_provider.litellm.acompletion",
            new_callable=AsyncMock,
            side_effect=Exception("NotFoundError: Model not found"),
        ):
            with pytest.raises(LLMProviderError, match="not found"):
                await provider.generate("test")


class TestLiteLLMProviderFactory:
    def test_factory_creates_litellm_provider(self):
        from agentic_security.llm_providers.factory import create_provider

        provider = create_provider("litellm")
        assert isinstance(provider, LiteLLMProvider)
        assert provider.model == snapshot("openai/gpt-4o-mini")

    def test_factory_creates_with_custom_model(self):
        from agentic_security.llm_providers.factory import create_provider

        provider = create_provider("litellm", model="groq/llama-3.3-70b-versatile")
        assert provider.model == snapshot("groq/llama-3.3-70b-versatile")

    def test_factory_lists_litellm(self):
        from agentic_security.llm_providers.factory import list_providers

        providers = list_providers()
        assert "litellm" in providers
