"""
Tests for Foil OpenTelemetry integration.

Run with:
    cd foil/sdks/python
    pip install -e ".[dev,otel]"
    pytest tests/test_otel.py -v
"""

import os
import time
import pytest
from unittest.mock import MagicMock, patch, PropertyMock


class TestConvertAttributeValue:
    """Tests for attribute value conversion."""

    @pytest.fixture
    def processor(self):
        """Create a FoilSpanProcessor for testing."""
        from foil.otel import FoilSpanProcessor
        return FoilSpanProcessor(api_key="test_key")

    def test_string_value(self, processor):
        """Test string conversion."""
        result = processor._convert_attribute_value("hello")
        assert result == {"stringValue": "hello"}

    def test_bool_value(self, processor):
        """Test boolean conversion."""
        result = processor._convert_attribute_value(True)
        assert result == {"boolValue": True}

        result = processor._convert_attribute_value(False)
        assert result == {"boolValue": False}

    def test_int_value(self, processor):
        """Test integer conversion."""
        result = processor._convert_attribute_value(42)
        assert result == {"intValue": "42"}

    def test_float_value(self, processor):
        """Test float conversion."""
        result = processor._convert_attribute_value(3.14)
        assert result == {"doubleValue": 3.14}

    def test_array_value(self, processor):
        """Test array conversion."""
        result = processor._convert_attribute_value(["a", "b", "c"])
        assert result == {
            "arrayValue": {
                "values": [
                    {"stringValue": "a"},
                    {"stringValue": "b"},
                    {"stringValue": "c"},
                ]
            }
        }

    def test_mixed_array(self, processor):
        """Test mixed type array conversion."""
        result = processor._convert_attribute_value([1, "two", True])
        assert result == {
            "arrayValue": {
                "values": [
                    {"intValue": "1"},
                    {"stringValue": "two"},
                    {"boolValue": True},
                ]
            }
        }

    def test_fallback_to_string(self, processor):
        """Test fallback to string for unknown types."""
        result = processor._convert_attribute_value({"key": "value"})
        assert result == {"stringValue": "{'key': 'value'}"}


class TestFoilSpanProcessor:
    """Tests for FoilSpanProcessor."""

    def test_init_requires_api_key(self):
        """Test that API key is required."""
        from foil.otel import FoilSpanProcessor

        with pytest.raises(ValueError) as exc_info:
            FoilSpanProcessor(api_key="")
        assert "Foil API key is required" in str(exc_info.value)

        with pytest.raises(ValueError):
            FoilSpanProcessor(api_key=None)

    def test_init_with_defaults(self):
        """Test initialization with default values."""
        from foil.otel import FoilSpanProcessor, DEFAULT_ENDPOINT

        processor = FoilSpanProcessor(api_key="test_key")

        assert processor.api_key == "test_key"
        assert processor.endpoint == DEFAULT_ENDPOINT
        assert processor.max_batch_size == 100
        assert processor.scheduled_delay_ms == 5000
        assert processor.export_timeout_ms == 30000

        # Clean up
        processor.shutdown()

    def test_init_with_custom_values(self):
        """Test initialization with custom values."""
        from foil.otel import FoilSpanProcessor

        processor = FoilSpanProcessor(
            api_key="test_key",
            endpoint="http://custom.endpoint/traces",
            max_batch_size=50,
            scheduled_delay_ms=1000,
            export_timeout_ms=5000,
        )

        assert processor.endpoint == "http://custom.endpoint/traces"
        assert processor.max_batch_size == 50
        assert processor.scheduled_delay_ms == 1000
        assert processor.export_timeout_ms == 5000

        # Clean up
        processor.shutdown()

    def test_init_with_env_endpoint(self):
        """Test initialization with environment variable endpoint."""
        from foil.otel import FoilSpanProcessor

        os.environ["FOIL_OTLP_ENDPOINT"] = "http://env.endpoint/traces"

        try:
            processor = FoilSpanProcessor(api_key="test_key")
            assert processor.endpoint == "http://env.endpoint/traces"
            processor.shutdown()
        finally:
            del os.environ["FOIL_OTLP_ENDPOINT"]

    @patch("foil.otel.requests.post")
    def test_export_success(self, mock_post):
        """Test successful export."""
        from foil.otel import FoilSpanProcessor

        mock_response = MagicMock()
        mock_response.status_code = 200
        mock_response.json.return_value = {}
        mock_post.return_value = mock_response

        processor = FoilSpanProcessor(api_key="test_key")

        # Add a mock span directly to pending
        processor._pending_spans.append({
            "traceId": "1234567890abcdef1234567890abcdef",
            "spanId": "1234567890abcdef",
            "name": "test-span",
        })

        processor._export()

        # Verify request was made
        mock_post.assert_called_once()
        call_args = mock_post.call_args

        assert call_args.kwargs["headers"]["Authorization"] == "test_key"
        assert call_args.kwargs["headers"]["Content-Type"] == "application/json"

        # Verify pending spans were cleared
        assert len(processor._pending_spans) == 0

        processor.shutdown()

    @patch("foil.otel.requests.post")
    def test_export_failure_requeues(self, mock_post):
        """Test that failed exports re-queue spans."""
        from foil.otel import FoilSpanProcessor

        mock_post.side_effect = Exception("Network error")

        processor = FoilSpanProcessor(api_key="test_key")

        test_span = {
            "traceId": "1234567890abcdef1234567890abcdef",
            "spanId": "1234567890abcdef",
            "name": "test-span",
        }
        processor._pending_spans.append(test_span)

        processor._export()

        # Verify spans were re-queued
        assert len(processor._pending_spans) == 1
        assert processor._pending_spans[0] == test_span

        processor.shutdown()

    def test_batch_size_triggers_export(self):
        """Test that reaching batch size triggers export."""
        from foil.otel import FoilSpanProcessor

        with patch.object(FoilSpanProcessor, "_export") as mock_export:
            processor = FoilSpanProcessor(api_key="test_key", max_batch_size=2)

            # Create a mock span
            mock_span = MagicMock()
            mock_span.get_span_context.return_value = MagicMock(
                trace_id=0x1234567890ABCDEF1234567890ABCDEF,
                span_id=0x1234567890ABCDEF,
                trace_state=None,
            )
            mock_span.name = "test-span"
            mock_span.kind = MagicMock(value=1)
            mock_span.start_time = 1000000000
            mock_span.end_time = 2000000000
            mock_span.attributes = {}
            mock_span.events = []
            mock_span.links = []
            mock_span.parent = None
            mock_span.status = MagicMock(status_code=MagicMock(value=0), description="")

            # First span shouldn't trigger export
            processor.on_end(mock_span)
            mock_export.assert_not_called()

            # Second span should trigger export (batch size = 2)
            processor.on_end(mock_span)
            mock_export.assert_called_once()

            processor.shutdown()

    def test_shutdown_flushes_spans(self):
        """Test that shutdown flushes pending spans."""
        from foil.otel import FoilSpanProcessor

        with patch.object(FoilSpanProcessor, "force_flush") as mock_flush:
            processor = FoilSpanProcessor(api_key="test_key")
            processor._pending_spans.append({"test": "span"})

            processor.shutdown()

            mock_flush.assert_called_once()
            assert processor._shutdown is True


class TestFoilInit:
    """Tests for Foil.init()."""

    def test_init_requires_api_key(self):
        """Test that API key is required."""
        from foil.otel import Foil

        with pytest.raises(ValueError) as exc_info:
            Foil.init(api_key="")
        assert "Foil API key is required" in str(exc_info.value)

    @patch("foil.otel.FoilSpanProcessor")
    def test_init_sets_service_name(self, mock_processor):
        """Test that init sets OTEL_SERVICE_NAME from agent_name."""
        from foil.otel import Foil

        # Clear any existing service name
        if "OTEL_SERVICE_NAME" in os.environ:
            del os.environ["OTEL_SERVICE_NAME"]

        try:
            Foil.init(api_key="test_key", agent_name="test-agent")
            assert os.environ.get("OTEL_SERVICE_NAME") == "test-agent"
        finally:
            Foil.shutdown()
            if "OTEL_SERVICE_NAME" in os.environ:
                del os.environ["OTEL_SERVICE_NAME"]

    @patch("foil.otel.FoilSpanProcessor")
    def test_init_returns_provider_and_processor(self, mock_processor_class):
        """Test that init returns provider and processor."""
        from foil.otel import Foil

        mock_processor = MagicMock()
        mock_processor_class.return_value = mock_processor

        try:
            result = Foil.init(api_key="test_key")

            assert "provider" in result
            assert "processor" in result
            assert result["processor"] == mock_processor
        finally:
            Foil.shutdown()


class TestAttachments:
    """Tests for attachment helpers."""

    def test_add_attachment_without_otel(self):
        """Test add_attachment returns False when OTEL not available."""
        with patch.dict("sys.modules", {"opentelemetry": None}):
            # This should fail gracefully
            from foil.otel import add_attachment
            # When no active span, should return False
            result = add_attachment(type="image", value="http://example.com/img.png", role="input")
            assert result is False

    def test_add_attachment_invalid_type(self):
        """Test add_attachment validates type."""
        from foil.otel import add_attachment
        from opentelemetry import trace

        with patch.object(trace, "get_current_span") as mock_get_span:
            mock_span = MagicMock()
            mock_span.is_recording.return_value = True
            mock_get_span.return_value = mock_span

            result = add_attachment(type="invalid", value="test", role="input")
            assert result is False

    def test_add_attachment_invalid_role(self):
        """Test add_attachment validates role."""
        from foil.otel import add_attachment
        from opentelemetry import trace

        with patch.object(trace, "get_current_span") as mock_get_span:
            mock_span = MagicMock()
            mock_span.is_recording.return_value = True
            mock_get_span.return_value = mock_span

            result = add_attachment(type="image", value="http://test.com", role="invalid")
            assert result is False

    def test_add_attachment_requires_value(self):
        """Test add_attachment requires value."""
        from foil.otel import add_attachment
        from opentelemetry import trace

        with patch.object(trace, "get_current_span") as mock_get_span:
            mock_span = MagicMock()
            mock_span.is_recording.return_value = True
            mock_get_span.return_value = mock_span

            result = add_attachment(type="image", value="", role="input")
            assert result is False

    def test_attach_image(self):
        """Test attach_image convenience function."""
        from foil.otel import attach_image

        with patch("foil.otel.add_attachment") as mock_add:
            mock_add.return_value = True

            result = attach_image("http://example.com/img.png", "input", "photo.png")

            mock_add.assert_called_once_with(
                type="image",
                value="http://example.com/img.png",
                role="input",
                name="photo.png",
            )
            assert result is True

    def test_attach_code(self):
        """Test attach_code convenience function."""
        from foil.otel import attach_code

        with patch("foil.otel.add_attachment") as mock_add:
            mock_add.return_value = True

            result = attach_code("print('hello')", "output", "python", "script.py")

            mock_add.assert_called_once_with(
                type="code",
                value="print('hello')",
                role="output",
                language="python",
                name="script.py",
            )
            assert result is True

    def test_attach_text(self):
        """Test attach_text convenience function."""
        from foil.otel import attach_text

        with patch("foil.otel.add_attachment") as mock_add:
            mock_add.return_value = True

            result = attach_text("Some document content", "input", "doc.txt")

            mock_add.assert_called_once_with(
                type="text",
                value="Some document content",
                role="input",
                name="doc.txt",
            )
            assert result is True


class TestConvertToOTLP:
    """Tests for span conversion to OTLP format."""

    @pytest.fixture
    def processor(self):
        """Create a FoilSpanProcessor for testing."""
        from foil.otel import FoilSpanProcessor

        processor = FoilSpanProcessor(api_key="test_key")
        yield processor
        processor.shutdown()

    def test_basic_span_conversion(self, processor):
        """Test basic span conversion."""
        mock_span = MagicMock()
        mock_span.get_span_context.return_value = MagicMock(
            trace_id=0x1234567890ABCDEF1234567890ABCDEF,
            span_id=0x1234567890ABCDEF,
            trace_state=None,
        )
        mock_span.name = "test-span"
        mock_span.kind = MagicMock(value=1)
        mock_span.start_time = 1000000000
        mock_span.end_time = 2000000000
        mock_span.attributes = {"key": "value"}
        mock_span.events = []
        mock_span.links = []
        mock_span.parent = None
        mock_span.status = MagicMock(status_code=MagicMock(value=0), description="")

        result = processor._convert_to_otlp(mock_span)

        assert result["name"] == "test-span"
        assert result["traceId"] == "1234567890abcdef1234567890abcdef"
        assert result["spanId"] == "1234567890abcdef"
        assert result["kind"] == 1
        assert result["startTimeUnixNano"] == "1000000000"
        assert result["endTimeUnixNano"] == "2000000000"
        assert len(result["attributes"]) == 1
        assert result["attributes"][0]["key"] == "key"
        assert result["attributes"][0]["value"] == {"stringValue": "value"}

    def test_span_with_parent(self, processor):
        """Test span conversion with parent span ID."""
        mock_span = MagicMock()
        mock_span.get_span_context.return_value = MagicMock(
            trace_id=0x1234567890ABCDEF1234567890ABCDEF,
            span_id=0x1234567890ABCDEF,
            trace_state=None,
        )
        mock_span.name = "child-span"
        mock_span.kind = MagicMock(value=1)
        mock_span.start_time = 1000000000
        mock_span.end_time = 2000000000
        mock_span.attributes = {}
        mock_span.events = []
        mock_span.links = []
        mock_span.parent = MagicMock(span_id=0xABCDEF1234567890)
        mock_span.status = MagicMock(status_code=MagicMock(value=0), description="")

        result = processor._convert_to_otlp(mock_span)

        assert result["parentSpanId"] == "abcdef1234567890"


if __name__ == "__main__":
    pytest.main([__file__, "-v"])
