"""Tests for auto-instrumentation functionality."""

import pytest
from unittest.mock import Mock, patch, MagicMock
from opentelemetry.sdk.trace import TracerProvider

from coalex.otel.auto_instrument import (
    auto_instrument,
    instrument_openai,
    instrument_llamaindex,
    instrument_langchain,
    instrument_vertexai,
    instrument_bedrock,
    instrument_anthropic,
    _instrument_library,
)


class TestAutoInstrument:
    """Test cases for auto_instrument function."""

    @patch('coalex.otel.auto_instrument._instrument_library')
    def test_auto_instrument_all_available(self, mock_inst):
        """Test auto-instrumentation when all libraries are available."""
        mock_provider = Mock(spec=TracerProvider)
        mock_inst.return_value = "success"

        results = auto_instrument(tracer_provider=mock_provider)

        # Should attempt to instrument all configured libraries
        assert len(results) >= 14  # openai, llamaindex, langchain, vertexai, bedrock, etc.
        assert mock_inst.call_count >= 14

    @patch('coalex.otel.auto_instrument._instrument_library')
    def test_auto_instrument_with_include_filter(self, mock_inst):
        """Test auto-instrumentation with include filter."""
        mock_provider = Mock(spec=TracerProvider)
        mock_inst.return_value = "success"

        results = auto_instrument(
            tracer_provider=mock_provider,
            include_libraries=["openai", "langchain"]
        )

        # Should only instrument openai and langchain
        assert "openai" in results
        assert "langchain" in results
        assert results["openai"] == "success"
        assert results["langchain"] == "success"
        # Should not attempt vertexai or others
        assert "vertexai" not in results
        assert "bedrock" not in results

    @patch('coalex.otel.auto_instrument._instrument_library')
    def test_auto_instrument_with_exclude_filter(self, mock_inst):
        """Test auto-instrumentation with exclude filter."""
        mock_provider = Mock(spec=TracerProvider)
        mock_inst.return_value = "success"

        results = auto_instrument(
            tracer_provider=mock_provider,
            exclude_libraries=["vertexai", "bedrock"]
        )

        # Should skip vertexai and bedrock
        assert results.get("vertexai") == "excluded"
        assert results.get("bedrock") == "excluded"
        # Should attempt others
        assert "openai" in results
        assert "langchain" in results

    @patch('coalex.otel.auto_instrument._instrument_library')
    def test_auto_instrument_mixed_results(self, mock_inst):
        """Test auto-instrumentation with mixed success/failure results."""
        mock_provider = Mock(spec=TracerProvider)

        # Simulate mixed results
        def side_effect(config, *args, **kwargs):
            name = config["name"]
            if name == "openai":
                return "success"
            elif name == "langchain":
                return "not_installed"
            elif name == "vertexai":
                return "error"
            else:
                return "success"

        mock_inst.side_effect = side_effect

        results = auto_instrument(tracer_provider=mock_provider)

        # Should have mixed results
        assert results["openai"] == "success"
        assert results["langchain"] == "not_installed"
        assert results["vertexai"] == "error"

    @patch('coalex.otel.auto_instrument.logger')
    @patch('coalex.otel.auto_instrument._instrument_library')
    def test_auto_instrument_logs_summary(self, mock_inst, mock_logger):
        """Test that auto_instrument logs a summary."""
        mock_inst.return_value = "success"

        auto_instrument(include_libraries=["openai", "langchain"])

        # Should log info with summary
        assert mock_logger.info.called
        info_call = str(mock_logger.info.call_args)
        assert "Auto-instrumentation complete" in info_call


class TestInstrumentLibrary:
    """Test cases for _instrument_library function."""

    def test_instrument_library_not_installed(self):
        """Test instrumentation when library is not installed."""
        config = {
            "name": "test_lib",
            "package": "nonexistent.package.that.does.not.exist",
            "class": "TestInstrumentor",
            "description": "Test Library"
        }

        result = _instrument_library(
            config=config,
            tracer_provider=None,
            suppress_errors=True
        )

        assert result == "not_installed"

    @patch('builtins.__import__')
    def test_instrument_library_error_during_import(self, mock_import):
        """Test instrumentation when an error occurs during import."""
        # Make import raise an exception other than ImportError
        def import_side_effect(name, *args, **kwargs):
            if name == "test.package":
                raise RuntimeError("Package initialization error")
            return __import__(name, *args, **kwargs)

        mock_import.side_effect = import_side_effect

        config = {
            "name": "test_lib",
            "package": "test.package",
            "class": "TestInstrumentor",
            "description": "Test Library"
        }

        result = _instrument_library(
            config=config,
            tracer_provider=None,
            suppress_errors=True
        )

        # Should return error when non-ImportError exceptions occur
        assert result == "error"

    @patch('builtins.__import__')
    def test_instrument_library_success(self, mock_import):
        """Test successful library instrumentation."""
        # Setup mock instrumentor
        mock_instrumentor = MagicMock()
        mock_instrumentor._is_instrumented_by_opentelemetry = False

        mock_class = Mock(return_value=mock_instrumentor)
        mock_module = Mock()
        setattr(mock_module, 'TestInstrumentor', mock_class)

        def import_side_effect(name, *args, **kwargs):
            if name == "test.package":
                return mock_module
            return __import__(name, *args, **kwargs)

        mock_import.side_effect = import_side_effect

        config = {
            "name": "test_lib",
            "package": "test.package",
            "class": "TestInstrumentor",
            "description": "Test Library"
        }

        mock_provider = Mock(spec=TracerProvider)

        result = _instrument_library(
            config=config,
            tracer_provider=mock_provider,
            suppress_errors=True
        )

        assert result == "success"
        mock_instrumentor.instrument.assert_called_once_with(tracer_provider=mock_provider)

    @patch('builtins.__import__')
    def test_instrument_library_already_instrumented(self, mock_import):
        """Test when library is already instrumented."""
        # Setup mock instrumentor that's already instrumented
        mock_instrumentor = MagicMock()
        mock_instrumentor._is_instrumented_by_opentelemetry = True

        mock_class = Mock(return_value=mock_instrumentor)
        mock_module = Mock()
        setattr(mock_module, 'TestInstrumentor', mock_class)

        def import_side_effect(name, *args, **kwargs):
            if name == "test.package":
                return mock_module
            return __import__(name, *args, **kwargs)

        mock_import.side_effect = import_side_effect

        config = {
            "name": "test_lib",
            "package": "test.package",
            "class": "TestInstrumentor",
            "description": "Test Library"
        }

        result = _instrument_library(
            config=config,
            tracer_provider=None,
            suppress_errors=True
        )

        assert result == "already_instrumented"
        # Should not call instrument() again
        mock_instrumentor.instrument.assert_not_called()

    @patch('builtins.__import__')
    def test_instrument_library_with_error_suppression(self, mock_import):
        """Test error suppression during instrumentation."""
        # Setup mock that raises during instrument()
        mock_instrumentor = MagicMock()
        mock_instrumentor._is_instrumented_by_opentelemetry = False
        mock_instrumentor.instrument.side_effect = RuntimeError("Instrumentation failed")

        mock_class = Mock(return_value=mock_instrumentor)
        mock_module = Mock()
        setattr(mock_module, 'TestInstrumentor', mock_class)

        def import_side_effect(name, *args, **kwargs):
            if name == "test.package":
                return mock_module
            return __import__(name, *args, **kwargs)

        mock_import.side_effect = import_side_effect

        config = {
            "name": "test_lib",
            "package": "test.package",
            "class": "TestInstrumentor",
            "description": "Test Library"
        }

        result = _instrument_library(
            config=config,
            tracer_provider=None,
            suppress_errors=True
        )

        # Should return error status instead of raising
        assert result == "error"

    @patch('builtins.__import__')
    def test_instrument_library_raises_without_suppression(self, mock_import):
        """Test that errors are raised when suppress_errors=False."""
        # Setup mock that raises during instrument()
        mock_instrumentor = MagicMock()
        mock_instrumentor._is_instrumented_by_opentelemetry = False
        mock_instrumentor.instrument.side_effect = RuntimeError("Instrumentation failed")

        mock_class = Mock(return_value=mock_instrumentor)
        mock_module = Mock()
        setattr(mock_module, 'TestInstrumentor', mock_class)

        def import_side_effect(name, *args, **kwargs):
            if name == "test.package":
                return mock_module
            return __import__(name, *args, **kwargs)

        mock_import.side_effect = import_side_effect

        config = {
            "name": "test_lib",
            "package": "test.package",
            "class": "TestInstrumentor",
            "description": "Test Library"
        }

        # Should raise when suppression is disabled
        with pytest.raises(RuntimeError, match="Instrumentation failed"):
            _instrument_library(
                config=config,
                tracer_provider=None,
                suppress_errors=False
            )

    @patch('builtins.__import__')
    def test_instrument_library_without_tracer_provider(self, mock_import):
        """Test instrumentation without providing tracer_provider."""
        # Setup mock instrumentor
        mock_instrumentor = MagicMock()
        mock_instrumentor._is_instrumented_by_opentelemetry = False

        mock_class = Mock(return_value=mock_instrumentor)
        mock_module = Mock()
        setattr(mock_module, 'TestInstrumentor', mock_class)

        def import_side_effect(name, *args, **kwargs):
            if name == "test.package":
                return mock_module
            return __import__(name, *args, **kwargs)

        mock_import.side_effect = import_side_effect

        config = {
            "name": "test_lib",
            "package": "test.package",
            "class": "TestInstrumentor",
            "description": "Test Library"
        }

        result = _instrument_library(
            config=config,
            tracer_provider=None,
            suppress_errors=True
        )

        assert result == "success"
        # Should call instrument without tracer_provider parameter
        mock_instrumentor.instrument.assert_called_once_with()


class TestSpecificInstrumentors:
    """Test cases for specific instrumentor functions."""

    @patch('coalex.otel.auto_instrument._instrument_library')
    def test_instrument_openai(self, mock_inst):
        """Test OpenAI-specific instrumentation."""
        mock_inst.return_value = "success"
        mock_provider = Mock(spec=TracerProvider)

        result = instrument_openai(tracer_provider=mock_provider)

        assert result is True
        mock_inst.assert_called_once()

        # Verify correct config was passed
        call_args = mock_inst.call_args
        config = call_args[1]['config']
        assert config['name'] == 'openai'
        assert config['package'] == 'openinference.instrumentation.openai'
        assert config['class'] == 'OpenAIInstrumentor'

    @patch('coalex.otel.auto_instrument._instrument_library')
    def test_instrument_llamaindex(self, mock_inst):
        """Test LlamaIndex-specific instrumentation."""
        mock_inst.return_value = "success"
        mock_provider = Mock(spec=TracerProvider)

        result = instrument_llamaindex(tracer_provider=mock_provider)

        assert result is True

        call_args = mock_inst.call_args
        config = call_args[1]['config']
        assert config['name'] == 'llamaindex'
        assert config['package'] == 'openinference.instrumentation.llamaindex'

    @patch('coalex.otel.auto_instrument._instrument_library')
    def test_instrument_langchain(self, mock_inst):
        """Test LangChain-specific instrumentation."""
        mock_inst.return_value = "success"
        mock_provider = Mock(spec=TracerProvider)

        result = instrument_langchain(tracer_provider=mock_provider)

        assert result is True

        call_args = mock_inst.call_args
        config = call_args[1]['config']
        assert config['name'] == 'langchain'

    @patch('coalex.otel.auto_instrument._instrument_library')
    def test_instrument_vertexai(self, mock_inst):
        """Test VertexAI-specific instrumentation."""
        mock_inst.return_value = "success"
        mock_provider = Mock(spec=TracerProvider)

        result = instrument_vertexai(tracer_provider=mock_provider)

        assert result is True

        call_args = mock_inst.call_args
        config = call_args[1]['config']
        assert config['name'] == 'vertexai'

    @patch('coalex.otel.auto_instrument._instrument_library')
    def test_instrument_bedrock(self, mock_inst):
        """Test Bedrock-specific instrumentation."""
        mock_inst.return_value = "success"
        mock_provider = Mock(spec=TracerProvider)

        result = instrument_bedrock(tracer_provider=mock_provider)

        assert result is True

        call_args = mock_inst.call_args
        config = call_args[1]['config']
        assert config['name'] == 'bedrock'

    @patch('coalex.otel.auto_instrument._instrument_library')
    def test_instrument_anthropic(self, mock_inst):
        """Test Anthropic-specific instrumentation."""
        mock_inst.return_value = "success"
        mock_provider = Mock(spec=TracerProvider)

        result = instrument_anthropic(tracer_provider=mock_provider)

        assert result is True

        call_args = mock_inst.call_args
        config = call_args[1]['config']
        assert config['name'] == 'anthropic'

    @patch('coalex.otel.auto_instrument._instrument_library')
    def test_instrument_failure_returns_false(self, mock_inst):
        """Test that instrumentation failure returns False."""
        mock_inst.return_value = "error"

        result = instrument_openai()

        # Should return False on error
        assert result is False

    @patch('coalex.otel.auto_instrument._instrument_library')
    def test_instrument_not_installed_returns_false(self, mock_inst):
        """Test that not_installed status returns False."""
        mock_inst.return_value = "not_installed"

        result = instrument_langchain()

        # Should return False when not installed
        assert result is False
