"""
Tests for Instructor integration.

Covers:
    - from_provider creates extraction span via create()
    - Disabled tracer passthrough
    - Error recording
    - Response model name capture
    - Retry tracking
    - Legacy patch() path
"""

from __future__ import annotations

from typing import Any
from unittest.mock import MagicMock, patch

import pytest


class TestInstructorIntegration:
    """Tests for Instructor from_provider() and create() instrumentation."""

    @pytest.fixture
    def mock_tracer(self):
        tracer = MagicMock()
        tracer.is_enabled = True
        mock_span = MagicMock()
        mock_span.span_id = "abcdef0123456789"
        mock_span.trace_id = "a" * 32
        tracer.start_span.return_value.__enter__ = MagicMock(return_value=mock_span)
        tracer.start_span.return_value.__exit__ = MagicMock(return_value=False)
        return tracer

    @pytest.fixture
    def mock_instructor_module(self):
        """Create a mock instructor module with from_provider and patch."""
        module = MagicMock()

        # Mock from_provider to return a mock client
        mock_client = MagicMock()
        mock_client._risicare_instrumented = False
        mock_client.create = MagicMock(return_value=MagicMock(name="UserModel"))
        mock_client.mode = MagicMock()
        mock_client.mode.value = "TOOLS"
        mock_client.max_retries = 3
        module.from_provider = MagicMock(return_value=mock_client)

        # Mock patch (legacy)
        legacy_client = MagicMock()
        legacy_client._risicare_instrumented = False
        legacy_client.create = MagicMock(return_value=MagicMock(name="LegacyModel"))
        legacy_client.mode = MagicMock()
        legacy_client.mode.value = "TOOLS"
        module.patch = MagicMock(return_value=legacy_client)

        return module

    def test_from_provider_creates_span(self, mock_tracer, mock_instructor_module):
        """from_provider + create() should create an extraction span."""
        from risicare.integrations.instructor._patches import (
            _wrap_create,
            _wrap_from_provider,
        )

        # Simulate from_provider returning a client
        original_from_provider = mock_instructor_module.from_provider
        client = _wrap_from_provider(
            original_from_provider, None, ("openai/gpt-4o",), {}
        )

        # The client should be marked as instrumented
        assert getattr(client, "_risicare_instrumented", False)

        # Now test that create() creates a span
        class UserModel:
            pass

        with patch(
            "risicare.integrations.instructor._patches.get_tracer",
            return_value=mock_tracer,
        ), patch(
            "risicare.integrations.instructor._patches.is_tracing_enabled",
            return_value=True,
        ):
            # Call the wrapped create
            result = client.create(response_model=UserModel, messages=[])

        # Verify span was started
        mock_tracer.start_span.assert_called_once()
        call_kwargs = mock_tracer.start_span.call_args
        assert "instructor.create/UserModel" in str(call_kwargs)

    def test_disabled_tracer_passthrough(self, mock_instructor_module):
        """When tracing is disabled, create() should pass through without overhead."""
        from risicare.integrations.instructor._patches import _wrap_create

        mock_wrapped = MagicMock(return_value="result")
        mock_instance = MagicMock()

        with patch(
            "risicare.integrations.instructor._patches.is_tracing_enabled",
            return_value=False,
        ):
            result = _wrap_create(
                mock_wrapped,
                mock_instance,
                (),
                {"response_model": MagicMock(__name__="TestModel")},
            )

        assert result == "result"
        mock_wrapped.assert_called_once()

    def test_records_error_on_exception(self, mock_tracer):
        """Exceptions during create() should be recorded on the span."""
        from risicare.integrations.instructor._patches import _wrap_create

        error = ValueError("Validation failed")
        mock_wrapped = MagicMock(side_effect=error)
        mock_instance = MagicMock()
        mock_instance.mode = MagicMock(value="TOOLS")
        mock_instance.max_retries = 1

        mock_span = MagicMock()
        mock_tracer.start_span.return_value.__enter__ = MagicMock(
            return_value=mock_span
        )

        with patch(
            "risicare.integrations.instructor._patches.get_tracer",
            return_value=mock_tracer,
        ), patch(
            "risicare.integrations.instructor._patches.is_tracing_enabled",
            return_value=True,
        ):
            with pytest.raises(ValueError, match="Validation failed"):
                _wrap_create(
                    mock_wrapped,
                    mock_instance,
                    (),
                    {"response_model": MagicMock(__name__="TestModel")},
                )

        # Verify error was recorded
        mock_span.record_exception.assert_called_once_with(error)
        mock_span.set_attribute.assert_any_call("error", True)
        mock_span.set_attribute.assert_any_call("error.type", "ValueError")

    def test_captures_response_model_name(self, mock_tracer):
        """The response_model class name should be captured in span attributes."""
        from risicare.integrations.instructor._patches import _wrap_create

        class MyUserModel:
            pass

        mock_wrapped = MagicMock(return_value=MagicMock())
        mock_instance = MagicMock()
        mock_instance.mode = MagicMock(value="JSON_SCHEMA")
        mock_instance.max_retries = 2

        mock_span = MagicMock()
        mock_tracer.start_span.return_value.__enter__ = MagicMock(
            return_value=mock_span
        )

        with patch(
            "risicare.integrations.instructor._patches.get_tracer",
            return_value=mock_tracer,
        ), patch(
            "risicare.integrations.instructor._patches.is_tracing_enabled",
            return_value=True,
        ):
            _wrap_create(
                mock_wrapped,
                mock_instance,
                (),
                {"response_model": MyUserModel},
            )

        # Check span name includes model name
        call_kwargs = mock_tracer.start_span.call_args
        assert "MyUserModel" in str(call_kwargs)

        # Check attributes
        attrs = call_kwargs.kwargs.get("attributes", call_kwargs[1].get("attributes", {}))
        assert attrs.get("framework.instructor.response_model") == "MyUserModel"
        assert attrs.get("framework.instructor.mode") == "JSON_SCHEMA"

    def test_retry_tracking(self, mock_tracer):
        """max_retries should be captured from kwargs or client default."""
        from risicare.integrations.instructor._patches import _wrap_create

        mock_wrapped = MagicMock(return_value=MagicMock())
        mock_instance = MagicMock()
        mock_instance.mode = MagicMock(value="TOOLS")
        mock_instance.max_retries = 5

        mock_span = MagicMock()
        mock_tracer.start_span.return_value.__enter__ = MagicMock(
            return_value=mock_span
        )

        with patch(
            "risicare.integrations.instructor._patches.get_tracer",
            return_value=mock_tracer,
        ), patch(
            "risicare.integrations.instructor._patches.is_tracing_enabled",
            return_value=True,
        ):
            _wrap_create(
                mock_wrapped,
                mock_instance,
                (),
                {
                    "response_model": MagicMock(__name__="TestModel"),
                    "max_retries": 10,
                },
            )

        # Per-call max_retries (10) should take precedence
        attrs = mock_tracer.start_span.call_args.kwargs.get(
            "attributes", mock_tracer.start_span.call_args[1].get("attributes", {})
        )
        assert attrs.get("framework.instructor.max_retries") == 10

    def test_patch_legacy_creates_span(self, mock_tracer, mock_instructor_module):
        """Legacy instructor.patch() path should also wrap the returned client."""
        from risicare.integrations.instructor._patches import _wrap_patch_legacy

        original_patch = mock_instructor_module.patch
        # Save the original create before wrapping
        original_create = original_patch.return_value.create

        client = _wrap_patch_legacy(original_patch, None, (), {})

        # The client should be marked as instrumented
        assert getattr(client, "_risicare_instrumented", False)

        # create() should be a new wrapper function, not the original MagicMock
        assert callable(client.create)
        assert not isinstance(client.create, MagicMock)
