"""Tests for LLM provider instrumentations."""

from unittest.mock import MagicMock, patch, AsyncMock

import pytest


class TestOpenAIInstrumentation:
    """Tests for OpenAI provider instrumentation."""

    @patch("risicare.instrumentation.providers.openai._get_tracer")
    def test_chat_create_creates_span(self, mock_get_tracer):
        """Test that chat.completions.create creates a span."""
        # Setup mock tracer
        mock_span = MagicMock()
        mock_span.__enter__ = MagicMock(return_value=mock_span)
        mock_span.__exit__ = MagicMock(return_value=None)

        mock_tracer = MagicMock()
        mock_tracer.is_enabled = True
        mock_tracer.start_span.return_value = mock_span
        mock_get_tracer.return_value = mock_tracer

        # Import wrapper
        from risicare.instrumentation.providers.openai import _wrap_chat_create

        # Mock wrapped function
        mock_response = MagicMock()
        mock_response.model = "gpt-4o"
        mock_response.id = "chatcmpl-123"
        mock_response.usage = MagicMock(
            prompt_tokens=10,
            completion_tokens=20,
            total_tokens=30,
        )
        mock_response.choices = [
            MagicMock(
                message=MagicMock(content="Hello!", tool_calls=None),
                finish_reason="stop",
            )
        ]

        wrapped = MagicMock(return_value=mock_response)
        instance = MagicMock()

        # Call wrapper
        result = _wrap_chat_create(
            wrapped,
            instance,
            (),
            {"model": "gpt-4o", "messages": [{"role": "user", "content": "Hi"}]},
        )

        # Verify span was created
        mock_tracer.start_span.assert_called_once()
        call_args = mock_tracer.start_span.call_args
        assert "openai.chat.completions.create" in call_args.kwargs["name"]

        # Verify attributes were set
        assert mock_span.set_attribute.called

    @patch("risicare.instrumentation.providers.openai._get_tracer")
    def test_chat_create_disabled_tracer(self, mock_get_tracer):
        """Test that chat.completions.create passes through when tracer disabled."""
        mock_tracer = MagicMock()
        mock_tracer.is_enabled = False
        mock_get_tracer.return_value = mock_tracer

        from risicare.instrumentation.providers.openai import _wrap_chat_create

        mock_response = MagicMock()
        wrapped = MagicMock(return_value=mock_response)
        instance = MagicMock()

        result = _wrap_chat_create(
            wrapped,
            instance,
            (),
            {"model": "gpt-4o", "messages": []},
        )

        # Should call wrapped directly, no span created
        wrapped.assert_called_once()
        mock_tracer.start_span.assert_not_called()

    @patch("risicare.instrumentation.providers.openai._get_tracer")
    def test_chat_create_records_error(self, mock_get_tracer):
        """Test that chat.completions.create records errors."""
        mock_span = MagicMock()
        mock_span.__enter__ = MagicMock(return_value=mock_span)
        mock_span.__exit__ = MagicMock(return_value=None)

        mock_tracer = MagicMock()
        mock_tracer.is_enabled = True
        mock_tracer.start_span.return_value = mock_span
        mock_get_tracer.return_value = mock_tracer

        from risicare.instrumentation.providers.openai import _wrap_chat_create

        # Mock wrapped function that raises
        wrapped = MagicMock(side_effect=Exception("API Error"))
        instance = MagicMock()

        # Should re-raise exception
        with pytest.raises(Exception, match="API Error"):
            _wrap_chat_create(
                wrapped,
                instance,
                (),
                {"model": "gpt-4o", "messages": []},
            )

        # Verify error was recorded
        mock_span.record_exception.assert_called_once()
        mock_span.set_attribute.assert_any_call("error", True)


class TestAnthropicInstrumentation:
    """Tests for Anthropic provider instrumentation."""

    @patch("risicare.instrumentation.providers.anthropic._get_tracer")
    def test_messages_create_creates_span(self, mock_get_tracer):
        """Test that messages.create creates a span."""
        mock_span = MagicMock()
        mock_span.__enter__ = MagicMock(return_value=mock_span)
        mock_span.__exit__ = MagicMock(return_value=None)

        mock_tracer = MagicMock()
        mock_tracer.is_enabled = True
        mock_tracer.start_span.return_value = mock_span
        mock_get_tracer.return_value = mock_tracer

        from risicare.instrumentation.providers.anthropic import _wrap_messages_create

        # Mock response
        mock_response = MagicMock()
        mock_response.model = "claude-3-5-sonnet-20241022"
        mock_response.id = "msg-123"
        mock_response.stop_reason = "end_turn"
        mock_response.usage = MagicMock(input_tokens=10, output_tokens=20)
        mock_response.content = [MagicMock(type="text", text="Hello!")]

        wrapped = MagicMock(return_value=mock_response)
        instance = MagicMock()

        result = _wrap_messages_create(
            wrapped,
            instance,
            (),
            {
                "model": "claude-3-5-sonnet-20241022",
                "messages": [{"role": "user", "content": "Hi"}],
                "max_tokens": 1000,
            },
        )

        # Verify span was created
        mock_tracer.start_span.assert_called_once()
        call_args = mock_tracer.start_span.call_args
        assert "anthropic.messages.create" in call_args.kwargs["name"]


class TestCohereInstrumentation:
    """Tests for Cohere provider instrumentation."""

    @patch("risicare.instrumentation.providers.cohere._get_tracer")
    def test_chat_creates_span(self, mock_get_tracer):
        """Test that chat creates a span."""
        mock_span = MagicMock()
        mock_span.__enter__ = MagicMock(return_value=mock_span)
        mock_span.__exit__ = MagicMock(return_value=None)

        mock_tracer = MagicMock()
        mock_tracer.is_enabled = True
        mock_tracer.start_span.return_value = mock_span
        mock_get_tracer.return_value = mock_tracer

        from risicare.instrumentation.providers.cohere import _wrap_chat

        # Mock response
        mock_response = MagicMock()
        mock_response.generation_id = "gen-123"
        mock_response.text = "Hello!"
        mock_response.meta = MagicMock(
            tokens=MagicMock(input_tokens=10, output_tokens=20)
        )

        wrapped = MagicMock(return_value=mock_response)
        instance = MagicMock()

        result = _wrap_chat(
            wrapped,
            instance,
            (),
            {"message": "Hi", "model": "command-r-plus"},
        )

        # Verify span was created
        mock_tracer.start_span.assert_called_once()
        call_args = mock_tracer.start_span.call_args
        assert "cohere.chat" in call_args.kwargs["name"]


class TestGoogleInstrumentation:
    """Tests for Google GenAI provider instrumentation."""

    @patch("risicare.instrumentation.providers.google._get_tracer")
    def test_generate_content_creates_span(self, mock_get_tracer):
        """Test that generate_content creates a span."""
        mock_span = MagicMock()
        mock_span.__enter__ = MagicMock(return_value=mock_span)
        mock_span.__exit__ = MagicMock(return_value=None)

        mock_tracer = MagicMock()
        mock_tracer.is_enabled = True
        mock_tracer.start_span.return_value = mock_span
        mock_get_tracer.return_value = mock_tracer

        from risicare.instrumentation.providers.google import _wrap_generate_content

        # Mock response
        mock_response = MagicMock()
        mock_response.candidates = [
            MagicMock(
                content=MagicMock(parts=[MagicMock(text="Hello!")]),
                finish_reason="STOP",
            )
        ]
        mock_response.usage_metadata = MagicMock(
            prompt_token_count=10,
            candidates_token_count=20,
            total_token_count=30,
        )

        wrapped = MagicMock(return_value=mock_response)
        instance = MagicMock()
        instance.model_name = "gemini-1.5-pro"

        result = _wrap_generate_content(
            wrapped,
            instance,
            ("Hello",),
            {},
        )

        # Verify span was created
        mock_tracer.start_span.assert_called_once()
        call_args = mock_tracer.start_span.call_args
        assert "google.generate_content" in call_args.kwargs["name"]


class TestMistralInstrumentation:
    """Tests for Mistral provider instrumentation."""

    @patch("risicare.instrumentation.providers.mistral._get_tracer")
    def test_chat_complete_creates_span(self, mock_get_tracer):
        """Test that chat.complete creates a span."""
        mock_span = MagicMock()
        mock_span.__enter__ = MagicMock(return_value=mock_span)
        mock_span.__exit__ = MagicMock(return_value=None)

        mock_tracer = MagicMock()
        mock_tracer.is_enabled = True
        mock_tracer.start_span.return_value = mock_span
        mock_get_tracer.return_value = mock_tracer

        from risicare.instrumentation.providers.mistral import _wrap_chat_complete

        # Mock response
        mock_response = MagicMock()
        mock_response.id = "chat-123"
        mock_response.model = "mistral-large"
        mock_response.usage = MagicMock(
            prompt_tokens=10,
            completion_tokens=20,
            total_tokens=30,
        )
        mock_response.choices = [
            MagicMock(
                message=MagicMock(content="Hello!", tool_calls=None),
                finish_reason="stop",
            )
        ]

        wrapped = MagicMock(return_value=mock_response)
        instance = MagicMock()

        result = _wrap_chat_complete(
            wrapped,
            instance,
            (),
            {
                "model": "mistral-large",
                "messages": [{"role": "user", "content": "Hi"}],
            },
        )

        # Verify span was created
        mock_tracer.start_span.assert_called_once()
        call_args = mock_tracer.start_span.call_args
        assert "mistral.chat.complete" in call_args.kwargs["name"]


# =============================================================================
# GAP-07: New provider tests
# =============================================================================


class TestOpenAIBaseUrlDetection:
    """Tests for OpenAI base_url provider detection (DeepSeek/vLLM)."""

    def test_detects_deepseek_base_url(self):
        """Test that DeepSeek base_url is detected correctly."""
        from risicare.instrumentation.providers.openai import _detect_provider_from_instance

        instance = MagicMock()
        instance._client = MagicMock()
        instance._client.base_url = "https://api.deepseek.com/v1"

        assert _detect_provider_from_instance(instance) == "deepseek"

    def test_detects_groq_base_url(self):
        """Test that Groq base_url is detected correctly."""
        from risicare.instrumentation.providers.openai import _detect_provider_from_instance

        instance = MagicMock()
        instance._client = MagicMock()
        instance._client.base_url = "https://api.groq.com/openai/v1"

        assert _detect_provider_from_instance(instance) == "groq"

    def test_detects_together_base_url(self):
        """Test that Together AI base_url is detected correctly."""
        from risicare.instrumentation.providers.openai import _detect_provider_from_instance

        instance = MagicMock()
        instance._client = MagicMock()
        instance._client.base_url = "https://api.together.xyz/v1"

        assert _detect_provider_from_instance(instance) == "together"

    def test_unknown_host_returns_openai(self):
        """Test that unknown base_url host returns 'openai'."""
        from risicare.instrumentation.providers.openai import _detect_provider_from_instance

        instance = MagicMock()
        instance._client = MagicMock()
        instance._client.base_url = "http://localhost:8000/v1"

        assert _detect_provider_from_instance(instance) == "openai"

    def test_no_client_returns_openai(self):
        """Test that missing _client attribute returns 'openai'."""
        from risicare.instrumentation.providers.openai import _detect_provider_from_instance

        instance = MagicMock(spec=[])  # Empty spec, no _client

        assert _detect_provider_from_instance(instance) == "openai"

    def test_no_base_url_returns_openai(self):
        """Test that missing base_url returns 'openai'."""
        from risicare.instrumentation.providers.openai import _detect_provider_from_instance

        instance = MagicMock()
        instance._client = MagicMock(spec=[])  # No base_url

        assert _detect_provider_from_instance(instance) == "openai"

    def test_detects_xai_base_url(self):
        """Test that xAI (Grok) base_url is detected correctly."""
        from risicare.instrumentation.providers.openai import _detect_provider_from_instance

        instance = MagicMock()
        instance._client = MagicMock()
        instance._client.base_url = "https://api.x.ai/v1"

        assert _detect_provider_from_instance(instance) == "xai"

    def test_detects_fireworks_base_url(self):
        """Test that Fireworks AI base_url is detected correctly."""
        from risicare.instrumentation.providers.openai import _detect_provider_from_instance

        instance = MagicMock()
        instance._client = MagicMock()
        instance._client.base_url = "https://api.fireworks.ai/inference/v1"

        assert _detect_provider_from_instance(instance) == "fireworks"

    def test_detects_baseten_base_url(self):
        """Test that Baseten base_url is detected correctly."""
        from risicare.instrumentation.providers.openai import _detect_provider_from_instance

        instance = MagicMock()
        instance._client = MagicMock()
        instance._client.base_url = "https://inference.baseten.co/v1"

        assert _detect_provider_from_instance(instance) == "baseten"

    def test_detects_novita_base_url(self):
        """Test that Novita AI base_url is detected correctly."""
        from risicare.instrumentation.providers.openai import _detect_provider_from_instance

        instance = MagicMock()
        instance._client = MagicMock()
        instance._client.base_url = "https://api.novita.ai/v3/openai"

        assert _detect_provider_from_instance(instance) == "novita"

    def test_detects_byteplus_base_url(self):
        """Test that BytePlus (Volcengine) base_url is detected correctly."""
        from risicare.instrumentation.providers.openai import _detect_provider_from_instance

        instance = MagicMock()
        instance._client = MagicMock()
        instance._client.base_url = "https://ark.cn-beijing.byteplus.com/api/v3"

        assert _detect_provider_from_instance(instance) == "byteplus"

    @patch("risicare.instrumentation.providers.openai._get_tracer")
    def test_xai_span_name_in_wrapper(self, mock_get_tracer):
        """Test that xAI base_url produces correct span name."""
        mock_span = MagicMock()
        mock_span.__enter__ = MagicMock(return_value=mock_span)
        mock_span.__exit__ = MagicMock(return_value=None)

        mock_tracer = MagicMock()
        mock_tracer.is_enabled = True
        mock_tracer.start_span.return_value = mock_span
        mock_get_tracer.return_value = mock_tracer

        from risicare.instrumentation.providers.openai import _wrap_chat_create

        mock_response = MagicMock()
        mock_response.model = "grok-2"
        mock_response.id = "chatcmpl-xai-123"
        mock_response.usage = MagicMock(
            prompt_tokens=10, completion_tokens=20, total_tokens=30,
        )
        mock_response.choices = [
            MagicMock(
                message=MagicMock(content="Hello!", tool_calls=None),
                finish_reason="stop",
            )
        ]

        wrapped = MagicMock(return_value=mock_response)
        instance = MagicMock()
        instance._client = MagicMock()
        instance._client.base_url = "https://api.x.ai/v1"

        _wrap_chat_create(
            wrapped, instance, (),
            {"model": "grok-2", "messages": [{"role": "user", "content": "Hi"}]},
        )

        call_args = mock_tracer.start_span.call_args
        assert "xai.chat.completions.create" in call_args.kwargs["name"]

    @patch("risicare.instrumentation.providers.openai._get_tracer")
    def test_deepseek_span_name_in_wrapper(self, mock_get_tracer):
        """Test that DeepSeek base_url produces correct span name."""
        mock_span = MagicMock()
        mock_span.__enter__ = MagicMock(return_value=mock_span)
        mock_span.__exit__ = MagicMock(return_value=None)

        mock_tracer = MagicMock()
        mock_tracer.is_enabled = True
        mock_tracer.start_span.return_value = mock_span
        mock_get_tracer.return_value = mock_tracer

        from risicare.instrumentation.providers.openai import _wrap_chat_create

        mock_response = MagicMock()
        mock_response.model = "deepseek-chat"
        mock_response.id = "chatcmpl-ds-123"
        mock_response.usage = MagicMock(
            prompt_tokens=10, completion_tokens=20, total_tokens=30,
        )
        mock_response.choices = [
            MagicMock(
                message=MagicMock(content="Hello!", tool_calls=None),
                finish_reason="stop",
            )
        ]

        wrapped = MagicMock(return_value=mock_response)
        instance = MagicMock()
        instance._client = MagicMock()
        instance._client.base_url = "https://api.deepseek.com/v1"

        _wrap_chat_create(
            wrapped, instance, (),
            {"model": "deepseek-chat", "messages": [{"role": "user", "content": "Hi"}]},
        )

        call_args = mock_tracer.start_span.call_args
        assert "deepseek.chat.completions.create" in call_args.kwargs["name"]


class TestGroqInstrumentation:
    """Tests for Groq provider instrumentation."""

    @patch("risicare.instrumentation.providers.groq._get_tracer")
    def test_chat_create_creates_span(self, mock_get_tracer):
        """Test that chat.completions.create creates a span."""
        mock_span = MagicMock()
        mock_span.__enter__ = MagicMock(return_value=mock_span)
        mock_span.__exit__ = MagicMock(return_value=None)

        mock_tracer = MagicMock()
        mock_tracer.is_enabled = True
        mock_tracer.start_span.return_value = mock_span
        mock_get_tracer.return_value = mock_tracer

        from risicare.instrumentation.providers.groq import _wrap_chat_create

        mock_response = MagicMock()
        mock_response.model = "llama-3.3-70b-versatile"
        mock_response.id = "chatcmpl-groq-123"
        mock_response.usage = MagicMock(
            prompt_tokens=10, completion_tokens=20, total_tokens=30,
        )
        mock_response.choices = [
            MagicMock(
                message=MagicMock(content="Hello!", tool_calls=None),
                finish_reason="stop",
            )
        ]

        wrapped = MagicMock(return_value=mock_response)
        instance = MagicMock()

        result = _wrap_chat_create(
            wrapped, instance, (),
            {"model": "llama-3.3-70b-versatile", "messages": [{"role": "user", "content": "Hi"}]},
        )

        mock_tracer.start_span.assert_called_once()
        call_args = mock_tracer.start_span.call_args
        assert "groq.chat.completions.create" in call_args.kwargs["name"]
        assert mock_span.set_attribute.called

    @patch("risicare.instrumentation.providers.groq._get_tracer")
    def test_chat_create_disabled_tracer(self, mock_get_tracer):
        """Test that disabled tracer passes through without span."""
        mock_tracer = MagicMock()
        mock_tracer.is_enabled = False
        mock_get_tracer.return_value = mock_tracer

        from risicare.instrumentation.providers.groq import _wrap_chat_create

        mock_response = MagicMock()
        wrapped = MagicMock(return_value=mock_response)

        result = _wrap_chat_create(
            wrapped, MagicMock(), (), {"model": "llama-3.3-70b-versatile", "messages": []},
        )

        wrapped.assert_called_once()
        mock_tracer.start_span.assert_not_called()

    @patch("risicare.instrumentation.providers.groq._get_tracer")
    def test_chat_create_records_error(self, mock_get_tracer):
        """Test that exceptions are recorded on the span."""
        mock_span = MagicMock()
        mock_span.__enter__ = MagicMock(return_value=mock_span)
        mock_span.__exit__ = MagicMock(return_value=None)

        mock_tracer = MagicMock()
        mock_tracer.is_enabled = True
        mock_tracer.start_span.return_value = mock_span
        mock_get_tracer.return_value = mock_tracer

        from risicare.instrumentation.providers.groq import _wrap_chat_create

        wrapped = MagicMock(side_effect=Exception("Rate limited"))

        with pytest.raises(Exception, match="Rate limited"):
            _wrap_chat_create(
                wrapped, MagicMock(), (),
                {"model": "llama-3.3-70b-versatile", "messages": []},
            )

        mock_span.record_exception.assert_called_once()
        mock_span.set_attribute.assert_any_call("error", True)

    @patch("risicare.instrumentation.providers.groq._get_tracer")
    def test_chat_create_captures_tokens(self, mock_get_tracer):
        """Test that token usage is captured in span attributes."""
        mock_span = MagicMock()
        mock_span.__enter__ = MagicMock(return_value=mock_span)
        mock_span.__exit__ = MagicMock(return_value=None)

        mock_tracer = MagicMock()
        mock_tracer.is_enabled = True
        mock_tracer.start_span.return_value = mock_span
        mock_get_tracer.return_value = mock_tracer

        from risicare.instrumentation.providers.groq import _wrap_chat_create

        mock_response = MagicMock()
        mock_response.model = "llama-3.3-70b-versatile"
        mock_response.id = "chatcmpl-groq-456"
        mock_response.usage = MagicMock(
            prompt_tokens=15, completion_tokens=25, total_tokens=40,
        )
        mock_response.choices = [
            MagicMock(
                message=MagicMock(content="Response", tool_calls=None),
                finish_reason="stop",
            )
        ]

        wrapped = MagicMock(return_value=mock_response)

        _wrap_chat_create(
            wrapped, MagicMock(), (),
            {"model": "llama-3.3-70b-versatile", "messages": [{"role": "user", "content": "Hi"}]},
        )

        # Verify token attributes were set
        set_attr_calls = {c[0]: c[1] for c in [call.args for call in mock_span.set_attribute.call_args_list] if len(c) == 2}
        assert set_attr_calls.get("gen_ai.usage.prompt_tokens") == 15
        assert set_attr_calls.get("gen_ai.usage.completion_tokens") == 25


class TestTogetherInstrumentation:
    """Tests for Together AI provider instrumentation."""

    @patch("risicare.instrumentation.providers.together._get_tracer")
    def test_chat_create_creates_span(self, mock_get_tracer):
        """Test that chat.completions.create creates a span."""
        mock_span = MagicMock()
        mock_span.__enter__ = MagicMock(return_value=mock_span)
        mock_span.__exit__ = MagicMock(return_value=None)

        mock_tracer = MagicMock()
        mock_tracer.is_enabled = True
        mock_tracer.start_span.return_value = mock_span
        mock_get_tracer.return_value = mock_tracer

        from risicare.instrumentation.providers.together import _wrap_chat_create

        mock_response = MagicMock()
        mock_response.model = "meta-llama/Llama-3.3-70B-Instruct-Turbo"
        mock_response.id = "chatcmpl-tog-123"
        mock_response.usage = MagicMock(
            prompt_tokens=12, completion_tokens=18, total_tokens=30,
        )
        mock_response.choices = [
            MagicMock(
                message=MagicMock(content="Hello!", tool_calls=None),
                finish_reason="stop",
            )
        ]

        wrapped = MagicMock(return_value=mock_response)
        instance = MagicMock()

        result = _wrap_chat_create(
            wrapped, instance, (),
            {"model": "meta-llama/Llama-3.3-70B-Instruct-Turbo", "messages": [{"role": "user", "content": "Hi"}]},
        )

        mock_tracer.start_span.assert_called_once()
        call_args = mock_tracer.start_span.call_args
        assert "together.chat.completions.create" in call_args.kwargs["name"]

    @patch("risicare.instrumentation.providers.together._get_tracer")
    def test_chat_create_disabled_tracer(self, mock_get_tracer):
        """Test that disabled tracer passes through."""
        mock_tracer = MagicMock()
        mock_tracer.is_enabled = False
        mock_get_tracer.return_value = mock_tracer

        from risicare.instrumentation.providers.together import _wrap_chat_create

        mock_response = MagicMock()
        wrapped = MagicMock(return_value=mock_response)

        result = _wrap_chat_create(
            wrapped, MagicMock(), (),
            {"model": "meta-llama/Llama-3.3-70B-Instruct-Turbo", "messages": []},
        )

        wrapped.assert_called_once()
        mock_tracer.start_span.assert_not_called()

    @patch("risicare.instrumentation.providers.together._get_tracer")
    def test_chat_create_records_error(self, mock_get_tracer):
        """Test that exceptions are recorded on the span."""
        mock_span = MagicMock()
        mock_span.__enter__ = MagicMock(return_value=mock_span)
        mock_span.__exit__ = MagicMock(return_value=None)

        mock_tracer = MagicMock()
        mock_tracer.is_enabled = True
        mock_tracer.start_span.return_value = mock_span
        mock_get_tracer.return_value = mock_tracer

        from risicare.instrumentation.providers.together import _wrap_chat_create

        wrapped = MagicMock(side_effect=Exception("Timeout"))

        with pytest.raises(Exception, match="Timeout"):
            _wrap_chat_create(
                wrapped, MagicMock(), (),
                {"model": "meta-llama/Llama-3.3-70B-Instruct-Turbo", "messages": []},
            )

        mock_span.record_exception.assert_called_once()
        mock_span.set_attribute.assert_any_call("error", True)

    @patch("risicare.instrumentation.providers.together._get_tracer")
    def test_chat_create_captures_tokens(self, mock_get_tracer):
        """Test that token usage is captured."""
        mock_span = MagicMock()
        mock_span.__enter__ = MagicMock(return_value=mock_span)
        mock_span.__exit__ = MagicMock(return_value=None)

        mock_tracer = MagicMock()
        mock_tracer.is_enabled = True
        mock_tracer.start_span.return_value = mock_span
        mock_get_tracer.return_value = mock_tracer

        from risicare.instrumentation.providers.together import _wrap_chat_create

        mock_response = MagicMock()
        mock_response.model = "meta-llama/Llama-3.3-70B-Instruct-Turbo"
        mock_response.id = "chatcmpl-tog-456"
        mock_response.usage = MagicMock(
            prompt_tokens=20, completion_tokens=30, total_tokens=50,
        )
        mock_response.choices = [
            MagicMock(
                message=MagicMock(content="Response", tool_calls=None),
                finish_reason="stop",
            )
        ]

        wrapped = MagicMock(return_value=mock_response)

        _wrap_chat_create(
            wrapped, MagicMock(), (),
            {"model": "meta-llama/Llama-3.3-70B-Instruct-Turbo", "messages": [{"role": "user", "content": "Hi"}]},
        )

        set_attr_calls = {c[0]: c[1] for c in [call.args for call in mock_span.set_attribute.call_args_list] if len(c) == 2}
        assert set_attr_calls.get("gen_ai.usage.prompt_tokens") == 20
        assert set_attr_calls.get("gen_ai.usage.completion_tokens") == 30


class TestOllamaInstrumentation:
    """Tests for Ollama provider instrumentation."""

    @patch("risicare.instrumentation.providers.ollama._get_tracer")
    def test_chat_creates_span(self, mock_get_tracer):
        """Test that ollama.chat creates a span."""
        mock_span = MagicMock()
        mock_span.__enter__ = MagicMock(return_value=mock_span)
        mock_span.__exit__ = MagicMock(return_value=None)

        mock_tracer = MagicMock()
        mock_tracer.is_enabled = True
        mock_tracer.start_span.return_value = mock_span
        mock_get_tracer.return_value = mock_tracer

        from risicare.instrumentation.providers.ollama import _wrap_chat

        # Ollama returns dicts, not objects
        mock_response = {
            "model": "llama3.1",
            "message": {"role": "assistant", "content": "Hello!"},
            "done": True,
            "prompt_eval_count": 10,
            "eval_count": 20,
        }

        wrapped = MagicMock(return_value=mock_response)
        instance = None  # Module-level function, no instance

        result = _wrap_chat(
            wrapped, instance, (),
            {"model": "llama3.1", "messages": [{"role": "user", "content": "Hi"}]},
        )

        mock_tracer.start_span.assert_called_once()
        call_args = mock_tracer.start_span.call_args
        assert "ollama.chat" in call_args.kwargs["name"]

    @patch("risicare.instrumentation.providers.ollama._get_tracer")
    def test_chat_disabled_tracer(self, mock_get_tracer):
        """Test that disabled tracer passes through."""
        mock_tracer = MagicMock()
        mock_tracer.is_enabled = False
        mock_get_tracer.return_value = mock_tracer

        from risicare.instrumentation.providers.ollama import _wrap_chat

        mock_response = {"model": "llama3.1", "message": {"role": "assistant", "content": "Hi"}}
        wrapped = MagicMock(return_value=mock_response)

        result = _wrap_chat(
            wrapped, None, (),
            {"model": "llama3.1", "messages": []},
        )

        wrapped.assert_called_once()
        mock_tracer.start_span.assert_not_called()

    @patch("risicare.instrumentation.providers.ollama._get_tracer")
    def test_chat_records_error(self, mock_get_tracer):
        """Test that exceptions are recorded on the span."""
        mock_span = MagicMock()
        mock_span.__enter__ = MagicMock(return_value=mock_span)
        mock_span.__exit__ = MagicMock(return_value=None)

        mock_tracer = MagicMock()
        mock_tracer.is_enabled = True
        mock_tracer.start_span.return_value = mock_span
        mock_get_tracer.return_value = mock_tracer

        from risicare.instrumentation.providers.ollama import _wrap_chat

        wrapped = MagicMock(side_effect=ConnectionError("Connection refused"))

        with pytest.raises(ConnectionError, match="Connection refused"):
            _wrap_chat(
                wrapped, None, (),
                {"model": "llama3.1", "messages": []},
            )

        mock_span.record_exception.assert_called_once()
        mock_span.set_attribute.assert_any_call("error", True)

    @patch("risicare.instrumentation.providers.ollama._get_tracer")
    def test_chat_captures_tokens(self, mock_get_tracer):
        """Test that Ollama token usage (dict-based) is captured."""
        mock_span = MagicMock()
        mock_span.__enter__ = MagicMock(return_value=mock_span)
        mock_span.__exit__ = MagicMock(return_value=None)

        mock_tracer = MagicMock()
        mock_tracer.is_enabled = True
        mock_tracer.start_span.return_value = mock_span
        mock_get_tracer.return_value = mock_tracer

        from risicare.instrumentation.providers.ollama import _wrap_chat

        mock_response = {
            "model": "llama3.1",
            "message": {"role": "assistant", "content": "Response here"},
            "done": True,
            "prompt_eval_count": 15,
            "eval_count": 25,
        }

        wrapped = MagicMock(return_value=mock_response)

        _wrap_chat(
            wrapped, None, (),
            {"model": "llama3.1", "messages": [{"role": "user", "content": "Hi"}]},
        )

        set_attr_calls = {c[0]: c[1] for c in [call.args for call in mock_span.set_attribute.call_args_list] if len(c) == 2}
        assert set_attr_calls.get("gen_ai.usage.prompt_tokens") == 15
        assert set_attr_calls.get("gen_ai.usage.completion_tokens") == 25

    @patch("risicare.instrumentation.providers.ollama._get_tracer")
    def test_generate_creates_span(self, mock_get_tracer):
        """Test that ollama.generate creates a span with correct name."""
        mock_span = MagicMock()
        mock_span.__enter__ = MagicMock(return_value=mock_span)
        mock_span.__exit__ = MagicMock(return_value=None)

        mock_tracer = MagicMock()
        mock_tracer.is_enabled = True
        mock_tracer.start_span.return_value = mock_span
        mock_get_tracer.return_value = mock_tracer

        from risicare.instrumentation.providers.ollama import _wrap_generate

        mock_response = {
            "model": "llama3.1",
            "response": "Generated text",
            "done": True,
            "prompt_eval_count": 8,
            "eval_count": 12,
        }

        wrapped = MagicMock(return_value=mock_response)

        _wrap_generate(
            wrapped, None, (),
            {"model": "llama3.1", "prompt": "Once upon a time"},
        )

        mock_tracer.start_span.assert_called_once()
        call_args = mock_tracer.start_span.call_args
        assert "ollama.generate" in call_args.kwargs["name"]


class TestVertexAIInstrumentation:
    """Tests for Vertex AI provider instrumentation."""

    @patch("risicare.instrumentation.providers.vertex_ai._get_tracer")
    def test_generate_content_creates_span(self, mock_get_tracer):
        """Test that generate_content creates a span."""
        mock_span = MagicMock()
        mock_span.__enter__ = MagicMock(return_value=mock_span)
        mock_span.__exit__ = MagicMock(return_value=None)

        mock_tracer = MagicMock()
        mock_tracer.is_enabled = True
        mock_tracer.start_span.return_value = mock_span
        mock_get_tracer.return_value = mock_tracer

        from risicare.instrumentation.providers.vertex_ai import _wrap_generate_content

        mock_response = MagicMock()
        mock_response.candidates = [
            MagicMock(
                content=MagicMock(parts=[MagicMock(text="Hello!")]),
                finish_reason="STOP",
            )
        ]
        mock_response.usage_metadata = MagicMock(
            prompt_token_count=10,
            candidates_token_count=20,
            total_token_count=30,
        )

        wrapped = MagicMock(return_value=mock_response)
        instance = MagicMock()
        instance._model_name = "gemini-1.5-pro"

        result = _wrap_generate_content(
            wrapped, instance,
            ("Hello",),
            {},
        )

        mock_tracer.start_span.assert_called_once()
        call_args = mock_tracer.start_span.call_args
        assert "vertexai.generate_content" in call_args.kwargs["name"]

    @patch("risicare.instrumentation.providers.vertex_ai._get_tracer")
    def test_generate_content_disabled_tracer(self, mock_get_tracer):
        """Test that disabled tracer passes through."""
        mock_tracer = MagicMock()
        mock_tracer.is_enabled = False
        mock_get_tracer.return_value = mock_tracer

        from risicare.instrumentation.providers.vertex_ai import _wrap_generate_content

        mock_response = MagicMock()
        wrapped = MagicMock(return_value=mock_response)
        instance = MagicMock()
        instance._model_name = "gemini-1.5-pro"

        result = _wrap_generate_content(
            wrapped, instance, ("Hi",), {},
        )

        wrapped.assert_called_once()
        mock_tracer.start_span.assert_not_called()

    @patch("risicare.instrumentation.providers.vertex_ai._get_tracer")
    def test_generate_content_records_error(self, mock_get_tracer):
        """Test that exceptions are recorded on the span."""
        mock_span = MagicMock()
        mock_span.__enter__ = MagicMock(return_value=mock_span)
        mock_span.__exit__ = MagicMock(return_value=None)

        mock_tracer = MagicMock()
        mock_tracer.is_enabled = True
        mock_tracer.start_span.return_value = mock_span
        mock_get_tracer.return_value = mock_tracer

        from risicare.instrumentation.providers.vertex_ai import _wrap_generate_content

        wrapped = MagicMock(side_effect=Exception("Vertex AI Error"))
        instance = MagicMock()
        instance._model_name = "gemini-1.5-pro"

        with pytest.raises(Exception, match="Vertex AI Error"):
            _wrap_generate_content(
                wrapped, instance, ("Hello",), {},
            )

        mock_span.record_exception.assert_called_once()
        mock_span.set_attribute.assert_any_call("error", True)

    @patch("risicare.instrumentation.providers.vertex_ai._get_tracer")
    def test_generate_content_captures_tokens(self, mock_get_tracer):
        """Test that token usage is captured."""
        mock_span = MagicMock()
        mock_span.__enter__ = MagicMock(return_value=mock_span)
        mock_span.__exit__ = MagicMock(return_value=None)

        mock_tracer = MagicMock()
        mock_tracer.is_enabled = True
        mock_tracer.start_span.return_value = mock_span
        mock_get_tracer.return_value = mock_tracer

        from risicare.instrumentation.providers.vertex_ai import _wrap_generate_content

        mock_response = MagicMock()
        mock_response.candidates = [
            MagicMock(
                content=MagicMock(parts=[MagicMock(text="Response")]),
                finish_reason="STOP",
            )
        ]
        mock_response.usage_metadata = MagicMock(
            prompt_token_count=12,
            candidates_token_count=18,
            total_token_count=30,
        )

        wrapped = MagicMock(return_value=mock_response)
        instance = MagicMock()
        instance._model_name = "gemini-1.5-pro"

        _wrap_generate_content(
            wrapped, instance, ("Hello",), {},
        )

        set_attr_calls = {c[0]: c[1] for c in [call.args for call in mock_span.set_attribute.call_args_list] if len(c) == 2}
        assert set_attr_calls.get("gen_ai.usage.prompt_tokens") == 12
        assert set_attr_calls.get("gen_ai.usage.completion_tokens") == 18


class TestBedrockInstrumentation:
    """Tests for Amazon Bedrock provider instrumentation."""

    def _make_bedrock_instance(self):
        """Create a mock botocore client instance for bedrock-runtime."""
        instance = MagicMock()
        instance._service_model = MagicMock()
        instance._service_model.service_name = "bedrock-runtime"
        return instance

    def _make_non_bedrock_instance(self, service_name="s3"):
        """Create a mock botocore client instance for non-bedrock service."""
        instance = MagicMock()
        instance._service_model = MagicMock()
        instance._service_model.service_name = service_name
        return instance

    @patch("risicare.instrumentation.providers.bedrock._get_tracer")
    def test_converse_creates_span(self, mock_get_tracer):
        """Test that Converse operation creates a span."""
        mock_span = MagicMock()
        mock_span.__enter__ = MagicMock(return_value=mock_span)
        mock_span.__exit__ = MagicMock(return_value=None)

        mock_tracer = MagicMock()
        mock_tracer.is_enabled = True
        mock_tracer.start_span.return_value = mock_span
        mock_get_tracer.return_value = mock_tracer

        from risicare.instrumentation.providers.bedrock import _wrap_make_api_call

        mock_response = {
            "output": {"message": {"role": "assistant", "content": [{"text": "Hello!"}]}},
            "usage": {"inputTokens": 10, "outputTokens": 20, "totalTokens": 30},
            "stopReason": "end_turn",
            "metrics": {"latencyMs": 1234},
        }

        wrapped = MagicMock(return_value=mock_response)
        instance = self._make_bedrock_instance()

        result = _wrap_make_api_call(
            wrapped, instance,
            ("Converse", {"modelId": "anthropic.claude-3-5-sonnet-20241022-v2:0", "messages": []}),
            {},
        )

        mock_tracer.start_span.assert_called_once()
        call_args = mock_tracer.start_span.call_args
        assert "bedrock.Converse" in call_args.kwargs["name"]

    @patch("risicare.instrumentation.providers.bedrock._get_tracer")
    def test_non_bedrock_service_passthrough(self, mock_get_tracer):
        """Test that non-bedrock-runtime services pass through unpatched."""
        mock_tracer = MagicMock()
        mock_tracer.is_enabled = True
        mock_get_tracer.return_value = mock_tracer

        from risicare.instrumentation.providers.bedrock import _wrap_make_api_call

        mock_response = MagicMock()
        wrapped = MagicMock(return_value=mock_response)
        instance = self._make_non_bedrock_instance("s3")

        result = _wrap_make_api_call(
            wrapped, instance, ("PutObject", {"Bucket": "test"}), {},
        )

        # Tracer should never be called for non-bedrock services
        mock_tracer.start_span.assert_not_called()
        wrapped.assert_called_once()

    @patch("risicare.instrumentation.providers.bedrock._get_tracer")
    def test_non_converse_operation_passthrough(self, mock_get_tracer):
        """Test that non-Converse/InvokeModel operations pass through."""
        mock_tracer = MagicMock()
        mock_tracer.is_enabled = True
        mock_get_tracer.return_value = mock_tracer

        from risicare.instrumentation.providers.bedrock import _wrap_make_api_call

        mock_response = MagicMock()
        wrapped = MagicMock(return_value=mock_response)
        instance = self._make_bedrock_instance()

        result = _wrap_make_api_call(
            wrapped, instance, ("ListFoundationModels", {}), {},
        )

        mock_tracer.start_span.assert_not_called()
        wrapped.assert_called_once()

    @patch("risicare.instrumentation.providers.bedrock._get_tracer")
    def test_converse_disabled_tracer(self, mock_get_tracer):
        """Test that disabled tracer passes through."""
        mock_tracer = MagicMock()
        mock_tracer.is_enabled = False
        mock_get_tracer.return_value = mock_tracer

        from risicare.instrumentation.providers.bedrock import _wrap_make_api_call

        mock_response = MagicMock()
        wrapped = MagicMock(return_value=mock_response)
        instance = self._make_bedrock_instance()

        result = _wrap_make_api_call(
            wrapped, instance,
            ("Converse", {"modelId": "anthropic.claude-3-haiku-20240307-v1:0", "messages": []}),
            {},
        )

        mock_tracer.start_span.assert_not_called()
        wrapped.assert_called_once()

    @patch("risicare.instrumentation.providers.bedrock._get_tracer")
    def test_converse_records_error(self, mock_get_tracer):
        """Test that exceptions are recorded on the span."""
        mock_span = MagicMock()
        mock_span.__enter__ = MagicMock(return_value=mock_span)
        mock_span.__exit__ = MagicMock(return_value=None)

        mock_tracer = MagicMock()
        mock_tracer.is_enabled = True
        mock_tracer.start_span.return_value = mock_span
        mock_get_tracer.return_value = mock_tracer

        from risicare.instrumentation.providers.bedrock import _wrap_make_api_call

        wrapped = MagicMock(side_effect=Exception("Throttled"))
        instance = self._make_bedrock_instance()

        with pytest.raises(Exception, match="Throttled"):
            _wrap_make_api_call(
                wrapped, instance,
                ("Converse", {"modelId": "anthropic.claude-3-5-sonnet-20241022-v2:0", "messages": []}),
                {},
            )

        mock_span.record_exception.assert_called_once()
        mock_span.set_attribute.assert_any_call("error", True)

    @patch("risicare.instrumentation.providers.bedrock._get_tracer")
    def test_invoke_model_creates_span(self, mock_get_tracer):
        """Test that InvokeModel creates a span."""
        mock_span = MagicMock()
        mock_span.__enter__ = MagicMock(return_value=mock_span)
        mock_span.__exit__ = MagicMock(return_value=None)

        mock_tracer = MagicMock()
        mock_tracer.is_enabled = True
        mock_tracer.start_span.return_value = mock_span
        mock_get_tracer.return_value = mock_tracer

        from risicare.instrumentation.providers.bedrock import _wrap_make_api_call

        mock_response = {
            "contentType": "application/json",
            "body": MagicMock(),  # StreamingBody
        }

        wrapped = MagicMock(return_value=mock_response)
        instance = self._make_bedrock_instance()

        result = _wrap_make_api_call(
            wrapped, instance,
            ("InvokeModel", {"modelId": "amazon.titan-text-express-v1", "body": "{}"}),
            {},
        )

        mock_tracer.start_span.assert_called_once()
        call_args = mock_tracer.start_span.call_args
        assert "bedrock.InvokeModel" in call_args.kwargs["name"]

    def test_shorten_model_id(self):
        """Test _shorten_model_id strips region prefix and version suffix."""
        from risicare.instrumentation.providers.bedrock import _shorten_model_id

        # Full ARN-style model ID (region + provider + model + date-version)
        assert _shorten_model_id("us.anthropic.claude-3-5-sonnet-20241022-v2:0") == "claude-3-5-sonnet"
        # No region prefix (provider + model + date-version)
        assert _shorten_model_id("anthropic.claude-3-haiku-20240307-v1:0") == "claude-3-haiku"
        # Simple model ID (provider + model + plain version)
        assert _shorten_model_id("amazon.titan-text-express-v1") == "titan-text-express"
        # Unknown format passthrough
        assert _shorten_model_id("unknown") == "unknown"


class TestCerebrasInstrumentation:
    """Tests for Cerebras Cloud SDK provider instrumentation."""

    @patch("risicare.instrumentation.providers.cerebras._get_tracer")
    def test_chat_create_creates_span(self, mock_get_tracer):
        """Test that chat.completions.create creates a span."""
        mock_span = MagicMock()
        mock_span.__enter__ = MagicMock(return_value=mock_span)
        mock_span.__exit__ = MagicMock(return_value=None)

        mock_tracer = MagicMock()
        mock_tracer.is_enabled = True
        mock_tracer.start_span.return_value = mock_span
        mock_get_tracer.return_value = mock_tracer

        from risicare.instrumentation.providers.cerebras import _wrap_chat_create

        mock_response = MagicMock()
        mock_response.model = "llama3.1-8b"
        mock_response.id = "chatcmpl-cerebras-123"
        mock_response.usage = MagicMock(
            prompt_tokens=10, completion_tokens=20, total_tokens=30,
        )
        mock_response.time_info = MagicMock(
            queue_time=0.5, prompt_time=1.2, completion_time=3.1,
        )
        mock_response.choices = [
            MagicMock(
                message=MagicMock(content="Hello!", tool_calls=None),
                finish_reason="stop",
            )
        ]

        wrapped = MagicMock(return_value=mock_response)
        instance = MagicMock()

        result = _wrap_chat_create(
            wrapped, instance, (),
            {"model": "llama3.1-8b", "messages": [{"role": "user", "content": "Hi"}]},
        )

        mock_tracer.start_span.assert_called_once()
        call_args = mock_tracer.start_span.call_args
        assert "cerebras.chat.completions.create" in call_args.kwargs["name"]
        assert mock_span.set_attribute.called

    @patch("risicare.instrumentation.providers.cerebras._get_tracer")
    def test_chat_create_disabled_tracer(self, mock_get_tracer):
        """Test that disabled tracer passes through without span."""
        mock_tracer = MagicMock()
        mock_tracer.is_enabled = False
        mock_get_tracer.return_value = mock_tracer

        from risicare.instrumentation.providers.cerebras import _wrap_chat_create

        mock_response = MagicMock()
        wrapped = MagicMock(return_value=mock_response)

        result = _wrap_chat_create(
            wrapped, MagicMock(), (), {"model": "llama3.1-8b", "messages": []},
        )

        wrapped.assert_called_once()
        mock_tracer.start_span.assert_not_called()

    @patch("risicare.instrumentation.providers.cerebras._get_tracer")
    def test_chat_create_records_error(self, mock_get_tracer):
        """Test that exceptions are recorded on the span."""
        mock_span = MagicMock()
        mock_span.__enter__ = MagicMock(return_value=mock_span)
        mock_span.__exit__ = MagicMock(return_value=None)

        mock_tracer = MagicMock()
        mock_tracer.is_enabled = True
        mock_tracer.start_span.return_value = mock_span
        mock_get_tracer.return_value = mock_tracer

        from risicare.instrumentation.providers.cerebras import _wrap_chat_create

        wrapped = MagicMock(side_effect=Exception("Rate limited"))

        with pytest.raises(Exception, match="Rate limited"):
            _wrap_chat_create(
                wrapped, MagicMock(), (),
                {"model": "llama3.1-8b", "messages": []},
            )

        mock_span.record_exception.assert_called_once()
        mock_span.set_attribute.assert_any_call("error", True)

    @patch("risicare.instrumentation.providers.cerebras._get_tracer")
    def test_chat_create_captures_tokens(self, mock_get_tracer):
        """Test that token usage is captured in span attributes."""
        mock_span = MagicMock()
        mock_span.__enter__ = MagicMock(return_value=mock_span)
        mock_span.__exit__ = MagicMock(return_value=None)

        mock_tracer = MagicMock()
        mock_tracer.is_enabled = True
        mock_tracer.start_span.return_value = mock_span
        mock_get_tracer.return_value = mock_tracer

        from risicare.instrumentation.providers.cerebras import _wrap_chat_create

        mock_response = MagicMock()
        mock_response.model = "llama3.1-8b"
        mock_response.id = "chatcmpl-cerebras-456"
        mock_response.usage = MagicMock(
            prompt_tokens=15, completion_tokens=25, total_tokens=40,
        )
        mock_response.time_info = None
        mock_response.choices = [
            MagicMock(
                message=MagicMock(content="Response", tool_calls=None),
                finish_reason="stop",
            )
        ]

        wrapped = MagicMock(return_value=mock_response)

        _wrap_chat_create(
            wrapped, MagicMock(), (),
            {"model": "llama3.1-8b", "messages": [{"role": "user", "content": "Hi"}]},
        )

        set_attr_calls = {c[0]: c[1] for c in [call.args for call in mock_span.set_attribute.call_args_list] if len(c) == 2}
        assert set_attr_calls.get("gen_ai.usage.prompt_tokens") == 15
        assert set_attr_calls.get("gen_ai.usage.completion_tokens") == 25

    @patch("risicare.instrumentation.providers.cerebras._get_tracer")
    def test_chat_create_captures_cerebras_time_info(self, mock_get_tracer):
        """Test that Cerebras-specific time_info is captured."""
        mock_span = MagicMock()
        mock_span.__enter__ = MagicMock(return_value=mock_span)
        mock_span.__exit__ = MagicMock(return_value=None)

        mock_tracer = MagicMock()
        mock_tracer.is_enabled = True
        mock_tracer.start_span.return_value = mock_span
        mock_get_tracer.return_value = mock_tracer

        from risicare.instrumentation.providers.cerebras import _wrap_chat_create

        mock_response = MagicMock()
        mock_response.model = "llama-3.3-70b"
        mock_response.id = "chatcmpl-cerebras-789"
        mock_response.usage = MagicMock(
            prompt_tokens=10, completion_tokens=20, total_tokens=30,
        )
        mock_response.time_info = MagicMock(
            queue_time=0.5, prompt_time=1.2, completion_time=3.1,
        )
        mock_response.choices = [
            MagicMock(
                message=MagicMock(content="Fast response", tool_calls=None),
                finish_reason="stop",
            )
        ]

        wrapped = MagicMock(return_value=mock_response)

        _wrap_chat_create(
            wrapped, MagicMock(), (),
            {"model": "llama-3.3-70b", "messages": [{"role": "user", "content": "Hi"}]},
        )

        set_attr_calls = {c[0]: c[1] for c in [call.args for call in mock_span.set_attribute.call_args_list] if len(c) == 2}
        assert set_attr_calls.get("cerebras.queue_time") == 0.5
        assert set_attr_calls.get("cerebras.prompt_time") == 1.2
        assert set_attr_calls.get("cerebras.completion_time") == 3.1


class TestHuggingFaceInstrumentation:
    """Tests for Hugging Face InferenceClient provider instrumentation."""

    @patch("risicare.instrumentation.providers.huggingface._get_tracer")
    def test_chat_completion_creates_span(self, mock_get_tracer):
        """Test that chat_completion creates a span."""
        mock_span = MagicMock()
        mock_span.__enter__ = MagicMock(return_value=mock_span)
        mock_span.__exit__ = MagicMock(return_value=None)

        mock_tracer = MagicMock()
        mock_tracer.is_enabled = True
        mock_tracer.start_span.return_value = mock_span
        mock_get_tracer.return_value = mock_tracer

        from risicare.instrumentation.providers.huggingface import _wrap_chat_completion

        # ChatCompletionOutput-like response
        mock_response = MagicMock()
        mock_response.model = "meta-llama/Llama-3.3-70B-Instruct"
        mock_response.id = "chatcmpl-hf-123"
        mock_response.usage = MagicMock(
            prompt_tokens=10, completion_tokens=20, total_tokens=30,
        )
        mock_response.choices = [
            MagicMock(
                message=MagicMock(content="Hello!", tool_calls=None),
                finish_reason="stop",
            )
        ]

        wrapped = MagicMock(return_value=mock_response)
        instance = MagicMock()
        instance.model = "meta-llama/Llama-3.3-70B-Instruct"

        result = _wrap_chat_completion(
            wrapped, instance, (),
            {"messages": [{"role": "user", "content": "Hi"}]},
        )

        mock_tracer.start_span.assert_called_once()
        call_args = mock_tracer.start_span.call_args
        assert "huggingface.chat_completion" in call_args.kwargs["name"]

    @patch("risicare.instrumentation.providers.huggingface._get_tracer")
    def test_chat_completion_disabled_tracer(self, mock_get_tracer):
        """Test that disabled tracer passes through."""
        mock_tracer = MagicMock()
        mock_tracer.is_enabled = False
        mock_get_tracer.return_value = mock_tracer

        from risicare.instrumentation.providers.huggingface import _wrap_chat_completion

        mock_response = MagicMock()
        wrapped = MagicMock(return_value=mock_response)
        instance = MagicMock()
        instance.model = "meta-llama/Llama-3.1-8B-Instruct"

        result = _wrap_chat_completion(
            wrapped, instance, (),
            {"messages": []},
        )

        wrapped.assert_called_once()
        mock_tracer.start_span.assert_not_called()

    @patch("risicare.instrumentation.providers.huggingface._get_tracer")
    def test_chat_completion_records_error(self, mock_get_tracer):
        """Test that exceptions are recorded on the span."""
        mock_span = MagicMock()
        mock_span.__enter__ = MagicMock(return_value=mock_span)
        mock_span.__exit__ = MagicMock(return_value=None)

        mock_tracer = MagicMock()
        mock_tracer.is_enabled = True
        mock_tracer.start_span.return_value = mock_span
        mock_get_tracer.return_value = mock_tracer

        from risicare.instrumentation.providers.huggingface import _wrap_chat_completion

        wrapped = MagicMock(side_effect=Exception("Model not found"))
        instance = MagicMock()
        instance.model = "nonexistent-model"

        with pytest.raises(Exception, match="Model not found"):
            _wrap_chat_completion(
                wrapped, instance, (),
                {"messages": []},
            )

        mock_span.record_exception.assert_called_once()
        mock_span.set_attribute.assert_any_call("error", True)

    @patch("risicare.instrumentation.providers.huggingface._get_tracer")
    def test_chat_completion_captures_tokens(self, mock_get_tracer):
        """Test that token usage is captured."""
        mock_span = MagicMock()
        mock_span.__enter__ = MagicMock(return_value=mock_span)
        mock_span.__exit__ = MagicMock(return_value=None)

        mock_tracer = MagicMock()
        mock_tracer.is_enabled = True
        mock_tracer.start_span.return_value = mock_span
        mock_get_tracer.return_value = mock_tracer

        from risicare.instrumentation.providers.huggingface import _wrap_chat_completion

        mock_response = MagicMock()
        mock_response.model = "Qwen/Qwen2.5-72B-Instruct"
        mock_response.id = "chatcmpl-hf-456"
        mock_response.usage = MagicMock(
            prompt_tokens=18, completion_tokens=32, total_tokens=50,
        )
        mock_response.choices = [
            MagicMock(
                message=MagicMock(content="Response", tool_calls=None),
                finish_reason="stop",
            )
        ]

        wrapped = MagicMock(return_value=mock_response)
        instance = MagicMock()
        instance.model = "Qwen/Qwen2.5-72B-Instruct"

        _wrap_chat_completion(
            wrapped, instance, (),
            {"messages": [{"role": "user", "content": "Hi"}]},
        )

        set_attr_calls = {c[0]: c[1] for c in [call.args for call in mock_span.set_attribute.call_args_list] if len(c) == 2}
        assert set_attr_calls.get("gen_ai.usage.prompt_tokens") == 18
        assert set_attr_calls.get("gen_ai.usage.completion_tokens") == 32

    @patch("risicare.instrumentation.providers.huggingface._get_tracer")
    def test_text_generation_creates_span(self, mock_get_tracer):
        """Test that text_generation creates a span with correct name."""
        mock_span = MagicMock()
        mock_span.__enter__ = MagicMock(return_value=mock_span)
        mock_span.__exit__ = MagicMock(return_value=None)

        mock_tracer = MagicMock()
        mock_tracer.is_enabled = True
        mock_tracer.start_span.return_value = mock_span
        mock_get_tracer.return_value = mock_tracer

        from risicare.instrumentation.providers.huggingface import _wrap_text_generation

        # text_generation returns a string by default
        wrapped = MagicMock(return_value="Once upon a time, there was a great kingdom.")
        instance = MagicMock()
        instance.model = "meta-llama/Llama-3.1-8B-Instruct"

        result = _wrap_text_generation(
            wrapped, instance, (),
            {"prompt": "Once upon a time"},
        )

        mock_tracer.start_span.assert_called_once()
        call_args = mock_tracer.start_span.call_args
        assert "huggingface.text_generation" in call_args.kwargs["name"]
        assert result == "Once upon a time, there was a great kingdom."

    @patch("risicare.instrumentation.providers.huggingface._get_tracer")
    def test_chat_completion_uses_model_kwarg(self, mock_get_tracer):
        """Test that model from kwargs takes precedence over instance.model."""
        mock_span = MagicMock()
        mock_span.__enter__ = MagicMock(return_value=mock_span)
        mock_span.__exit__ = MagicMock(return_value=None)

        mock_tracer = MagicMock()
        mock_tracer.is_enabled = True
        mock_tracer.start_span.return_value = mock_span
        mock_get_tracer.return_value = mock_tracer

        from risicare.instrumentation.providers.huggingface import _wrap_chat_completion

        mock_response = MagicMock()
        mock_response.model = "deepseek-ai/DeepSeek-R1"
        mock_response.id = "chatcmpl-hf-789"
        mock_response.usage = MagicMock(
            prompt_tokens=5, completion_tokens=10, total_tokens=15,
        )
        mock_response.choices = [
            MagicMock(
                message=MagicMock(content="Hello!", tool_calls=None),
                finish_reason="stop",
            )
        ]

        wrapped = MagicMock(return_value=mock_response)
        instance = MagicMock()
        instance.model = "default-model"

        _wrap_chat_completion(
            wrapped, instance, (),
            {"model": "deepseek-ai/DeepSeek-R1", "messages": [{"role": "user", "content": "Hi"}]},
        )

        call_args = mock_tracer.start_span.call_args
        assert "deepseek-ai/DeepSeek-R1" in call_args.kwargs["name"]


class TestHooksIntegration:
    """Tests that all new providers are registered in hooks."""

    def test_all_new_providers_in_instrumentors(self):
        """Verify all providers are in the instrumentors map."""
        from risicare.instrumentation.hooks import _get_instrumentors

        instrumentors = _get_instrumentors()

        # Original providers
        assert "openai" in instrumentors
        assert "anthropic" in instrumentors
        assert "cohere" in instrumentors
        assert "google.generativeai" in instrumentors
        assert "mistralai" in instrumentors

        # GAP-07 providers
        assert "groq" in instrumentors
        assert "together" in instrumentors
        assert "ollama" in instrumentors
        assert "botocore" in instrumentors
        assert "vertexai" in instrumentors

        # Extended provider parity (Phase 2 & 3)
        assert "cerebras" in instrumentors
        assert "huggingface_hub" in instrumentors

    def test_supported_modules_count(self):
        """Verify total supported module count after extended provider parity."""
        from risicare.instrumentation.hooks import get_supported_modules

        modules = get_supported_modules()
        # 12 LLM providers (openai, anthropic, cohere, google.generativeai,
        # mistralai, groq, together, ollama, botocore, vertexai, cerebras,
        # huggingface_hub) + 7 framework entries (langchain, langchain_core,
        # langgraph, crewai, autogen, autogen_agentchat, agents) + 1 otel = 20
        assert len(modules) >= 20
