"""Tests for CLI telemetry module: OpenTelemetry configuration for agent SDK.

Tests cover:
- configure_agent_telemetry() function
- NucleusSpanExporter class
- NucleusMetricsExporter class
- BaggageSpanProcessor class
- ALLOWED_BAGGAGE_KEYS constant
- _suppress_instrumentation() context manager
"""

from __future__ import annotations

import os
from unittest.mock import MagicMock, Mock, patch

import httpx
import pytest
from opentelemetry import baggage, context
from opentelemetry.context import _SUPPRESS_INSTRUMENTATION_KEY
from opentelemetry.sdk.metrics.export import MetricExportResult
from opentelemetry.sdk.trace import ReadableSpan
from opentelemetry.sdk.trace.export import SpanExportResult

from terminaluse.lib.telemetry.otel_config import (
    ALLOWED_BAGGAGE_KEYS,
    BaggageSpanProcessor,
    NucleusMetricsExporter,
    NucleusSpanExporter,
    _attr_value_to_otlp,
    _suppress_instrumentation,
    configure_agent_telemetry,
)

# =============================================================================
# configure_agent_telemetry() Tests
# =============================================================================


class TestConfigureAgentTelemetry:
    """Tests for the configure_agent_telemetry() function."""

    @pytest.mark.unit
    def test_returns_false_when_base_url_not_set(self):
        """Should return False when TERMINALUSE_BASE_URL is not set."""
        with patch.dict(os.environ, {}, clear=True):
            # Ensure env vars are not set
            os.environ.pop("TERMINALUSE_BASE_URL", None)
            os.environ.pop("TERMINALUSE_AGENT_API_KEY", None)
            result = configure_agent_telemetry()
        assert result is False

    @pytest.mark.unit
    def test_returns_false_when_api_key_not_set(self):
        """Should return False when TERMINALUSE_AGENT_API_KEY is not set."""
        with patch.dict(os.environ, {"TERMINALUSE_BASE_URL": "https://api.example.com"}, clear=True):
            result = configure_agent_telemetry()
        assert result is False

    @pytest.mark.unit
    def test_returns_false_when_both_env_vars_empty(self):
        """Should return False when both env vars are empty strings."""
        with patch.dict(
            os.environ,
            {"TERMINALUSE_BASE_URL": "", "TERMINALUSE_AGENT_API_KEY": ""},
            clear=True,
        ):
            result = configure_agent_telemetry()
        assert result is False

    @pytest.mark.unit
    def test_returns_true_when_env_vars_set(self):
        """Should return True when both required env vars are set."""
        with patch.dict(
            os.environ,
            {
                "TERMINALUSE_BASE_URL": "https://api.example.com",
                "TERMINALUSE_AGENT_API_KEY": "test-api-key",
            },
            clear=True,
        ):
            # Mock the trace and metrics providers to avoid global state changes
            with patch("terminaluse.lib.telemetry.otel_config.trace.set_tracer_provider"):
                with patch("terminaluse.lib.telemetry.otel_config.metrics.set_meter_provider"):
                    result = configure_agent_telemetry()
        assert result is True

    @pytest.mark.unit
    def test_uses_default_service_name_when_not_set(self):
        """Should use 'unknown-agent' as default service name."""
        with patch.dict(
            os.environ,
            {
                "TERMINALUSE_BASE_URL": "https://api.example.com",
                "TERMINALUSE_AGENT_API_KEY": "test-api-key",
            },
            clear=True,
        ):
            with patch("terminaluse.lib.telemetry.otel_config.trace.set_tracer_provider") as mock_set_tracer:
                with patch("terminaluse.lib.telemetry.otel_config.metrics.set_meter_provider"):
                    with patch("terminaluse.lib.telemetry.otel_config.TracerProvider") as mock_tracer_provider:
                        with patch("terminaluse.lib.telemetry.otel_config.Resource.create") as mock_resource:
                            configure_agent_telemetry()
                            # Check that Resource.create was called with default service name
                            mock_resource.assert_called_once()
                            call_args = mock_resource.call_args[0][0]
                            assert call_args["service.name"] == "unknown-agent"
                            assert call_args["service.version"] == "unknown"

    @pytest.mark.unit
    def test_uses_custom_service_name_when_set(self):
        """Should use custom service name from TERMINALUSE_AGENT_NAME."""
        with patch.dict(
            os.environ,
            {
                "TERMINALUSE_BASE_URL": "https://api.example.com",
                "TERMINALUSE_AGENT_API_KEY": "test-api-key",
                "TERMINALUSE_AGENT_NAME": "my-custom-agent",
                "TERMINALUSE_AGENT_VERSION": "1.2.3",
            },
            clear=True,
        ):
            with patch("terminaluse.lib.telemetry.otel_config.trace.set_tracer_provider"):
                with patch("terminaluse.lib.telemetry.otel_config.metrics.set_meter_provider"):
                    with patch("terminaluse.lib.telemetry.otel_config.TracerProvider"):
                        with patch("terminaluse.lib.telemetry.otel_config.Resource.create") as mock_resource:
                            configure_agent_telemetry()
                            call_args = mock_resource.call_args[0][0]
                            assert call_args["service.name"] == "my-custom-agent"
                            assert call_args["service.version"] == "1.2.3"

    @pytest.mark.unit
    def test_configures_tracer_provider_with_baggage_processor(self):
        """Should add BaggageSpanProcessor to the tracer provider."""
        with patch.dict(
            os.environ,
            {
                "TERMINALUSE_BASE_URL": "https://api.example.com",
                "TERMINALUSE_AGENT_API_KEY": "test-api-key",
            },
            clear=True,
        ):
            with patch("terminaluse.lib.telemetry.otel_config.trace.set_tracer_provider"):
                with patch("terminaluse.lib.telemetry.otel_config.metrics.set_meter_provider"):
                    with patch("terminaluse.lib.telemetry.otel_config.TracerProvider") as mock_tracer_provider:
                        mock_instance = MagicMock()
                        mock_tracer_provider.return_value = mock_instance
                        configure_agent_telemetry()
                        # Should have added at least 2 span processors (Baggage + Batch)
                        assert mock_instance.add_span_processor.call_count == 2

    @pytest.mark.unit
    def test_auto_instrument_false_does_not_call_instrument_functions(self):
        """Should NOT call instrumentation functions when auto_instrument=False."""
        with patch.dict(
            os.environ,
            {
                "TERMINALUSE_BASE_URL": "https://api.example.com",
                "TERMINALUSE_AGENT_API_KEY": "test-api-key",
            },
            clear=True,
        ):
            with patch("terminaluse.lib.telemetry.otel_config.trace.set_tracer_provider"):
                with patch("terminaluse.lib.telemetry.otel_config.metrics.set_meter_provider"):
                    with patch("terminaluse.lib.telemetry.otel_config._instrument_httpx") as mock_httpx:
                        with patch("terminaluse.lib.telemetry.otel_config._instrument_fastapi") as mock_fastapi:
                            configure_agent_telemetry(auto_instrument=False)
                            mock_httpx.assert_not_called()
                            mock_fastapi.assert_not_called()

    @pytest.mark.unit
    def test_auto_instrument_true_calls_instrument_functions(self):
        """Should call instrumentation functions when auto_instrument=True."""
        with patch.dict(
            os.environ,
            {
                "TERMINALUSE_BASE_URL": "https://api.example.com",
                "TERMINALUSE_AGENT_API_KEY": "test-api-key",
            },
            clear=True,
        ):
            with patch("terminaluse.lib.telemetry.otel_config.trace.set_tracer_provider"):
                with patch("terminaluse.lib.telemetry.otel_config.metrics.set_meter_provider"):
                    with patch("terminaluse.lib.telemetry.otel_config._instrument_httpx") as mock_httpx:
                        with patch("terminaluse.lib.telemetry.otel_config._instrument_fastapi") as mock_fastapi:
                            configure_agent_telemetry(auto_instrument=True)
                            mock_httpx.assert_called_once()
                            mock_fastapi.assert_called_once()


# =============================================================================
# NucleusSpanExporter Tests
# =============================================================================


class TestNucleusSpanExporter:
    """Tests for the NucleusSpanExporter class."""

    @pytest.mark.unit
    def test_endpoint_construction(self):
        """Should construct endpoint with /traces suffix."""
        exporter = NucleusSpanExporter(endpoint="https://api.example.com", api_key="test-key")
        assert exporter.endpoint == "https://api.example.com/traces"

    @pytest.mark.unit
    def test_endpoint_strips_trailing_slash(self):
        """Should strip trailing slash before adding /traces."""
        exporter = NucleusSpanExporter(endpoint="https://api.example.com/", api_key="test-key")
        assert exporter.endpoint == "https://api.example.com/traces"

    @pytest.mark.unit
    def test_api_key_stored(self):
        """Should store the API key."""
        exporter = NucleusSpanExporter(endpoint="https://api.example.com", api_key="my-secret-key")
        assert exporter.api_key == "my-secret-key"

    @pytest.mark.unit
    def test_export_empty_spans_returns_success(self):
        """Should return SUCCESS for empty span list."""
        exporter = NucleusSpanExporter(endpoint="https://api.example.com", api_key="test-key")
        result = exporter.export([])
        assert result == SpanExportResult.SUCCESS

    @pytest.mark.unit
    def test_export_sends_correct_headers(self):
        """Should send x-agent-api-key header and Content-Type."""
        exporter = NucleusSpanExporter(endpoint="https://api.example.com", api_key="test-api-key")

        mock_span = self._create_mock_span()

        with patch.object(exporter._client, "post") as mock_post:
            mock_response = Mock()
            mock_response.raise_for_status = Mock()
            mock_post.return_value = mock_response

            exporter.export([mock_span])

            mock_post.assert_called_once()
            call_kwargs = mock_post.call_args[1]
            assert call_kwargs["headers"]["x-agent-api-key"] == "test-api-key"
            assert call_kwargs["headers"]["Content-Type"] == "application/json"

    @pytest.mark.unit
    def test_export_includes_traceparent_header(self):
        """Should inject trace context (traceparent) into headers."""
        exporter = NucleusSpanExporter(endpoint="https://api.example.com", api_key="test-api-key")

        mock_span = self._create_mock_span()

        with patch.object(exporter._client, "post") as mock_post:
            mock_response = Mock()
            mock_response.raise_for_status = Mock()
            mock_post.return_value = mock_response

            with patch("terminaluse.lib.telemetry.otel_config.inject") as mock_inject:
                exporter.export([mock_span])
                # inject() should have been called with the headers dict
                mock_inject.assert_called_once()
                injected_headers = mock_inject.call_args[0][0]
                assert "x-agent-api-key" in injected_headers

    @pytest.mark.unit
    def test_export_returns_success_on_200(self):
        """Should return SUCCESS on HTTP 200."""
        exporter = NucleusSpanExporter(endpoint="https://api.example.com", api_key="test-key")

        mock_span = self._create_mock_span()

        with patch.object(exporter._client, "post") as mock_post:
            mock_response = Mock()
            mock_response.raise_for_status = Mock()  # No exception = success
            mock_post.return_value = mock_response

            result = exporter.export([mock_span])
            assert result == SpanExportResult.SUCCESS

    @pytest.mark.unit
    def test_export_returns_failure_on_http_error(self):
        """Should return FAILURE on HTTP error status."""
        exporter = NucleusSpanExporter(endpoint="https://api.example.com", api_key="test-key")

        mock_span = self._create_mock_span()

        with patch.object(exporter._client, "post") as mock_post:
            mock_response = Mock()
            mock_response.status_code = 500
            mock_post.return_value = mock_response
            mock_response.raise_for_status.side_effect = httpx.HTTPStatusError(
                "Server Error",
                request=Mock(),
                response=mock_response,
            )

            result = exporter.export([mock_span])
            assert result == SpanExportResult.FAILURE

    @pytest.mark.unit
    def test_export_returns_failure_on_connection_error(self):
        """Should return FAILURE on connection errors."""
        exporter = NucleusSpanExporter(endpoint="https://api.example.com", api_key="test-key")

        mock_span = self._create_mock_span()

        with patch.object(exporter._client, "post") as mock_post:
            mock_post.side_effect = httpx.ConnectError("Connection refused")

            result = exporter.export([mock_span])
            assert result == SpanExportResult.FAILURE

    @pytest.mark.unit
    def test_export_returns_failure_on_generic_exception(self):
        """Should return FAILURE on generic exceptions."""
        exporter = NucleusSpanExporter(endpoint="https://api.example.com", api_key="test-key")

        mock_span = self._create_mock_span()

        with patch.object(exporter._client, "post") as mock_post:
            mock_post.side_effect = Exception("Something went wrong")

            result = exporter.export([mock_span])
            assert result == SpanExportResult.FAILURE

    @pytest.mark.unit
    def test_shutdown_closes_client(self):
        """Should close the HTTP client on shutdown."""
        exporter = NucleusSpanExporter(endpoint="https://api.example.com", api_key="test-key")

        with patch.object(exporter._client, "close") as mock_close:
            exporter.shutdown()
            mock_close.assert_called_once()

    @pytest.mark.unit
    def test_force_flush_returns_true(self):
        """Should return True for force_flush (no-op)."""
        exporter = NucleusSpanExporter(endpoint="https://api.example.com", api_key="test-key")
        assert exporter.force_flush() is True
        assert exporter.force_flush(timeout_millis=5000) is True

    @pytest.mark.unit
    def test_spans_to_otlp_format(self):
        """Should convert spans to OTLP JSON format."""
        exporter = NucleusSpanExporter(endpoint="https://api.example.com", api_key="test-key")

        mock_span = self._create_mock_span(
            name="test-span",
            trace_id=0x12345678901234567890123456789012,
            span_id=0x1234567890123456,
        )

        otlp_data = exporter._spans_to_otlp([mock_span])

        assert "resourceSpans" in otlp_data
        assert len(otlp_data["resourceSpans"]) > 0

        # Check resource span structure
        resource_span = otlp_data["resourceSpans"][0]
        assert "resource" in resource_span
        assert "scopeSpans" in resource_span

    def _create_mock_span(
        self,
        name: str = "test-span",
        trace_id: int = 0x12345678901234567890123456789012,
        span_id: int = 0x1234567890123456,
    ) -> ReadableSpan:
        """Create a mock ReadableSpan for testing."""
        mock_span = Mock(spec=ReadableSpan)
        mock_span.name = name

        # Mock context
        mock_context = Mock()
        mock_context.trace_id = trace_id
        mock_context.span_id = span_id
        mock_span.context = mock_context

        # Mock resource
        mock_resource = Mock()
        mock_resource.attributes = {"service.name": "test-service"}
        mock_span.resource = mock_resource

        # Mock instrumentation scope
        mock_scope = Mock()
        mock_scope.name = "test-scope"
        mock_scope.version = "1.0.0"
        mock_span.instrumentation_scope = mock_scope

        # Mock timing
        mock_span.start_time = 1704067200000000000  # Example nanosecond timestamp
        mock_span.end_time = 1704067201000000000

        # Mock kind
        mock_kind = Mock()
        mock_kind.value = 1
        mock_span.kind = mock_kind

        # Mock status
        mock_status = Mock()
        mock_status.status_code = Mock()
        mock_status.status_code.value = 0
        mock_span.status = mock_status

        # Mock parent (no parent)
        mock_span.parent = None

        # Mock attributes
        mock_span.attributes = {"test.attr": "value"}

        # Mock events (empty)
        mock_span.events = []

        return mock_span


# =============================================================================
# NucleusMetricsExporter Tests
# =============================================================================


class TestNucleusMetricsExporter:
    """Tests for the NucleusMetricsExporter class."""

    @pytest.mark.unit
    def test_endpoint_construction(self):
        """Should construct endpoint with /agent-metrics suffix."""
        exporter = NucleusMetricsExporter(endpoint="https://api.example.com", api_key="test-key")
        assert exporter.endpoint == "https://api.example.com/agent-metrics"

    @pytest.mark.unit
    def test_endpoint_strips_trailing_slash(self):
        """Should strip trailing slash before adding /agent-metrics."""
        exporter = NucleusMetricsExporter(endpoint="https://api.example.com/", api_key="test-key")
        assert exporter.endpoint == "https://api.example.com/agent-metrics"

    @pytest.mark.unit
    def test_api_key_stored(self):
        """Should store the API key."""
        exporter = NucleusMetricsExporter(endpoint="https://api.example.com", api_key="my-secret-key")
        assert exporter.api_key == "my-secret-key"

    @pytest.mark.unit
    def test_export_empty_metrics_returns_success(self):
        """Should return SUCCESS for empty metrics data."""
        exporter = NucleusMetricsExporter(endpoint="https://api.example.com", api_key="test-key")

        mock_metrics_data = Mock()
        mock_metrics_data.resource_metrics = []

        result = exporter.export(mock_metrics_data)
        assert result == MetricExportResult.SUCCESS

    @pytest.mark.unit
    def test_export_sends_correct_headers(self):
        """Should send x-agent-api-key header and Content-Type."""
        exporter = NucleusMetricsExporter(endpoint="https://api.example.com", api_key="test-api-key")

        mock_metrics_data = self._create_mock_metrics_data()

        with patch.object(exporter._client, "post") as mock_post:
            mock_response = Mock()
            mock_response.raise_for_status = Mock()
            mock_post.return_value = mock_response

            exporter.export(mock_metrics_data)

            mock_post.assert_called_once()
            call_kwargs = mock_post.call_args[1]
            assert call_kwargs["headers"]["x-agent-api-key"] == "test-api-key"
            assert call_kwargs["headers"]["Content-Type"] == "application/json"

    @pytest.mark.unit
    def test_export_includes_trace_context_injection(self):
        """Should inject trace context into headers."""
        exporter = NucleusMetricsExporter(endpoint="https://api.example.com", api_key="test-api-key")

        mock_metrics_data = self._create_mock_metrics_data()

        with patch.object(exporter._client, "post") as mock_post:
            mock_response = Mock()
            mock_response.raise_for_status = Mock()
            mock_post.return_value = mock_response

            with patch("terminaluse.lib.telemetry.otel_config.inject") as mock_inject:
                exporter.export(mock_metrics_data)
                mock_inject.assert_called_once()

    @pytest.mark.unit
    def test_export_returns_success_on_200(self):
        """Should return SUCCESS on HTTP 200."""
        exporter = NucleusMetricsExporter(endpoint="https://api.example.com", api_key="test-key")

        mock_metrics_data = self._create_mock_metrics_data()

        with patch.object(exporter._client, "post") as mock_post:
            mock_response = Mock()
            mock_response.raise_for_status = Mock()
            mock_post.return_value = mock_response

            result = exporter.export(mock_metrics_data)
            assert result == MetricExportResult.SUCCESS

    @pytest.mark.unit
    def test_export_returns_failure_on_http_error(self):
        """Should return FAILURE on HTTP error status."""
        exporter = NucleusMetricsExporter(endpoint="https://api.example.com", api_key="test-key")

        mock_metrics_data = self._create_mock_metrics_data()

        with patch.object(exporter._client, "post") as mock_post:
            mock_response = Mock()
            mock_response.status_code = 500
            mock_post.return_value = mock_response
            mock_response.raise_for_status.side_effect = httpx.HTTPStatusError(
                "Server Error",
                request=Mock(),
                response=mock_response,
            )

            result = exporter.export(mock_metrics_data)
            assert result == MetricExportResult.FAILURE

    @pytest.mark.unit
    def test_export_returns_failure_on_connection_error(self):
        """Should return FAILURE on connection errors."""
        exporter = NucleusMetricsExporter(endpoint="https://api.example.com", api_key="test-key")

        mock_metrics_data = self._create_mock_metrics_data()

        with patch.object(exporter._client, "post") as mock_post:
            mock_post.side_effect = httpx.ConnectError("Connection refused")

            result = exporter.export(mock_metrics_data)
            assert result == MetricExportResult.FAILURE

    @pytest.mark.unit
    def test_export_returns_failure_on_generic_exception(self):
        """Should return FAILURE on generic exceptions."""
        exporter = NucleusMetricsExporter(endpoint="https://api.example.com", api_key="test-key")

        mock_metrics_data = self._create_mock_metrics_data()

        with patch.object(exporter._client, "post") as mock_post:
            mock_post.side_effect = Exception("Something went wrong")

            result = exporter.export(mock_metrics_data)
            assert result == MetricExportResult.FAILURE

    @pytest.mark.unit
    def test_shutdown_closes_client(self):
        """Should close the HTTP client on shutdown."""
        exporter = NucleusMetricsExporter(endpoint="https://api.example.com", api_key="test-key")

        with patch.object(exporter._client, "close") as mock_close:
            exporter.shutdown()
            mock_close.assert_called_once()

    @pytest.mark.unit
    def test_force_flush_returns_true(self):
        """Should return True for force_flush (no-op)."""
        exporter = NucleusMetricsExporter(endpoint="https://api.example.com", api_key="test-key")
        assert exporter.force_flush() is True
        assert exporter.force_flush(timeout_millis=5000) is True

    def _create_mock_metrics_data(self):
        """Create mock MetricsData for testing."""
        mock_metrics_data = Mock()

        # Create a mock resource metric
        mock_resource_metric = Mock()
        mock_resource = Mock()
        mock_resource.attributes = {"service.name": "test-service"}
        mock_resource_metric.resource = mock_resource

        # Create a mock scope metric
        mock_scope_metric = Mock()
        mock_scope = Mock()
        mock_scope.name = "test-scope"
        mock_scope.version = "1.0.0"
        mock_scope_metric.scope = mock_scope
        mock_scope_metric.metrics = []

        mock_resource_metric.scope_metrics = [mock_scope_metric]
        mock_metrics_data.resource_metrics = [mock_resource_metric]

        return mock_metrics_data


# =============================================================================
# BaggageSpanProcessor Tests
# =============================================================================


class TestBaggageSpanProcessor:
    """Tests for the BaggageSpanProcessor class."""

    @pytest.mark.unit
    def test_copies_allowed_baggage_keys_to_span(self):
        """Should copy allowed baggage keys to span attributes with 'baggage.' prefix."""
        processor = BaggageSpanProcessor()

        # Create a mock span
        mock_span = Mock()

        # Create context with baggage
        ctx = baggage.set_baggage("org.id", "org-123")
        ctx = baggage.set_baggage("user.id", "user-456", ctx)
        ctx = baggage.set_baggage("agent.name", "my-agent", ctx)

        processor.on_start(mock_span, ctx)

        # Verify set_attribute was called with prefixed keys
        calls = {call[0][0]: call[0][1] for call in mock_span.set_attribute.call_args_list}
        assert calls["baggage.org.id"] == "org-123"
        assert calls["baggage.user.id"] == "user-456"
        assert calls["baggage.agent.name"] == "my-agent"

    @pytest.mark.unit
    def test_ignores_non_allowed_baggage_keys(self):
        """Should NOT copy baggage keys that are not in the allowlist."""
        processor = BaggageSpanProcessor()

        mock_span = Mock()

        # Create context with non-allowed baggage key
        ctx = baggage.set_baggage("some.random.key", "value")
        ctx = baggage.set_baggage("another.key", "value2", ctx)
        ctx = baggage.set_baggage("org.id", "allowed-value", ctx)  # This one IS allowed

        processor.on_start(mock_span, ctx)

        # Should only have one call for the allowed key
        calls = {call[0][0]: call[0][1] for call in mock_span.set_attribute.call_args_list}
        assert "baggage.org.id" in calls
        assert "baggage.some.random.key" not in calls
        assert "baggage.another.key" not in calls

    @pytest.mark.unit
    def test_uses_current_context_when_parent_context_is_none(self):
        """Should use current context when parent_context is None."""
        processor = BaggageSpanProcessor()

        mock_span = Mock()

        # Set baggage in current context
        ctx = baggage.set_baggage("task.id", "task-789")
        token = context.attach(ctx)

        try:
            processor.on_start(mock_span, parent_context=None)

            calls = {call[0][0]: call[0][1] for call in mock_span.set_attribute.call_args_list}
            assert calls["baggage.task.id"] == "task-789"
        finally:
            context.detach(token)

    @pytest.mark.unit
    def test_converts_value_to_string(self):
        """Should convert baggage values to string."""
        processor = BaggageSpanProcessor()

        mock_span = Mock()

        # Create context with baggage (values are always strings in W3C baggage)
        ctx = baggage.set_baggage("agent.id", "12345")

        processor.on_start(mock_span, ctx)

        calls = {call[0][0]: call[0][1] for call in mock_span.set_attribute.call_args_list}
        assert calls["baggage.agent.id"] == "12345"
        assert isinstance(calls["baggage.agent.id"], str)

    @pytest.mark.unit
    def test_on_end_is_noop(self):
        """on_end should be a no-op (pass)."""
        processor = BaggageSpanProcessor()
        mock_span = Mock()
        # Should not raise
        processor.on_end(mock_span)

    @pytest.mark.unit
    def test_shutdown_is_noop(self):
        """shutdown should be a no-op (pass)."""
        processor = BaggageSpanProcessor()
        # Should not raise
        processor.shutdown()

    @pytest.mark.unit
    def test_force_flush_returns_true(self):
        """force_flush should return True."""
        processor = BaggageSpanProcessor()
        assert processor.force_flush() is True
        assert processor.force_flush(timeout_millis=5000) is True

    @pytest.mark.unit
    def test_handles_empty_baggage(self):
        """Should handle context with no baggage gracefully."""
        processor = BaggageSpanProcessor()

        mock_span = Mock()

        # Empty context (no baggage)
        ctx = context.get_current()

        processor.on_start(mock_span, ctx)

        # Should not have called set_attribute
        mock_span.set_attribute.assert_not_called()


# =============================================================================
# ALLOWED_BAGGAGE_KEYS Tests
# =============================================================================


class TestAllowedBaggageKeys:
    """Tests for the ALLOWED_BAGGAGE_KEYS constant."""

    @pytest.mark.unit
    def test_contains_expected_keys(self):
        """Should contain all expected baggage keys."""
        expected_keys = {
            "org.id",
            "user.id",
            "agent.name",
            "agent.id",
            "branch.id",
            "version.id",
            "task.id",
        }
        assert ALLOWED_BAGGAGE_KEYS == expected_keys

    @pytest.mark.unit
    def test_is_frozenset(self):
        """Should be a frozenset (immutable)."""
        assert isinstance(ALLOWED_BAGGAGE_KEYS, frozenset)

    @pytest.mark.unit
    def test_org_id_is_allowed(self):
        """org.id should be in the allowlist."""
        assert "org.id" in ALLOWED_BAGGAGE_KEYS

    @pytest.mark.unit
    def test_user_id_is_allowed(self):
        """user.id should be in the allowlist."""
        assert "user.id" in ALLOWED_BAGGAGE_KEYS

    @pytest.mark.unit
    def test_agent_name_is_allowed(self):
        """agent.name should be in the allowlist."""
        assert "agent.name" in ALLOWED_BAGGAGE_KEYS

    @pytest.mark.unit
    def test_agent_id_is_allowed(self):
        """agent.id should be in the allowlist."""
        assert "agent.id" in ALLOWED_BAGGAGE_KEYS

    @pytest.mark.unit
    def test_branch_id_is_allowed(self):
        """branch.id should be in the allowlist."""
        assert "branch.id" in ALLOWED_BAGGAGE_KEYS

    @pytest.mark.unit
    def test_version_id_is_allowed(self):
        """version.id should be in the allowlist."""
        assert "version.id" in ALLOWED_BAGGAGE_KEYS

    @pytest.mark.unit
    def test_task_id_is_allowed(self):
        """task.id should be in the allowlist."""
        assert "task.id" in ALLOWED_BAGGAGE_KEYS

    @pytest.mark.unit
    def test_random_key_not_allowed(self):
        """Random keys should NOT be in the allowlist."""
        assert "random.key" not in ALLOWED_BAGGAGE_KEYS
        assert "password" not in ALLOWED_BAGGAGE_KEYS
        assert "secret" not in ALLOWED_BAGGAGE_KEYS


# =============================================================================
# _suppress_instrumentation() Tests
# =============================================================================


class TestSuppressInstrumentation:
    """Tests for the _suppress_instrumentation() context manager."""

    @pytest.mark.unit
    def test_sets_suppression_key_within_context(self):
        """Should set _SUPPRESS_INSTRUMENTATION_KEY to True within context."""
        with _suppress_instrumentation():
            current_ctx = context.get_current()
            suppressed = context.get_value(_SUPPRESS_INSTRUMENTATION_KEY, current_ctx)
            assert suppressed is True

    @pytest.mark.unit
    def test_unsets_suppression_key_after_context(self):
        """Should restore context after exiting the context manager."""
        # Get value before
        before_ctx = context.get_current()
        before_value = context.get_value(_SUPPRESS_INSTRUMENTATION_KEY, before_ctx)

        with _suppress_instrumentation():
            pass

        # Get value after
        after_ctx = context.get_current()
        after_value = context.get_value(_SUPPRESS_INSTRUMENTATION_KEY, after_ctx)

        # Should be restored to original value (likely None)
        assert after_value == before_value

    @pytest.mark.unit
    def test_restores_context_on_exception(self):
        """Should restore context even if an exception is raised."""
        before_ctx = context.get_current()
        before_value = context.get_value(_SUPPRESS_INSTRUMENTATION_KEY, before_ctx)

        try:
            with _suppress_instrumentation():
                raise ValueError("Test exception")
        except ValueError:
            pass

        after_ctx = context.get_current()
        after_value = context.get_value(_SUPPRESS_INSTRUMENTATION_KEY, after_ctx)

        assert after_value == before_value

    @pytest.mark.unit
    def test_nested_suppress_instrumentation(self):
        """Should handle nested context managers correctly."""
        with _suppress_instrumentation():
            outer_ctx = context.get_current()
            outer_value = context.get_value(_SUPPRESS_INSTRUMENTATION_KEY, outer_ctx)
            assert outer_value is True

            with _suppress_instrumentation():
                inner_ctx = context.get_current()
                inner_value = context.get_value(_SUPPRESS_INSTRUMENTATION_KEY, inner_ctx)
                assert inner_value is True

            # After inner context exits, should still be suppressed (outer context)
            mid_ctx = context.get_current()
            mid_value = context.get_value(_SUPPRESS_INSTRUMENTATION_KEY, mid_ctx)
            assert mid_value is True


# =============================================================================
# _attr_value_to_otlp() Tests
# =============================================================================


class TestAttrValueToOtlp:
    """Tests for the _attr_value_to_otlp() helper function."""

    @pytest.mark.unit
    def test_bool_true_conversion(self):
        """Should convert True to boolValue."""
        result = _attr_value_to_otlp(True)
        assert result == {"boolValue": True}

    @pytest.mark.unit
    def test_bool_false_conversion(self):
        """Should convert False to boolValue."""
        result = _attr_value_to_otlp(False)
        assert result == {"boolValue": False}

    @pytest.mark.unit
    def test_int_conversion(self):
        """Should convert int to intValue as string."""
        result = _attr_value_to_otlp(42)
        assert result == {"intValue": "42"}

    @pytest.mark.unit
    def test_negative_int_conversion(self):
        """Should convert negative int to intValue as string."""
        result = _attr_value_to_otlp(-123)
        assert result == {"intValue": "-123"}

    @pytest.mark.unit
    def test_float_conversion(self):
        """Should convert float to doubleValue."""
        result = _attr_value_to_otlp(3.14)
        assert result == {"doubleValue": 3.14}

    @pytest.mark.unit
    def test_string_conversion(self):
        """Should convert string to stringValue."""
        result = _attr_value_to_otlp("hello")
        assert result == {"stringValue": "hello"}

    @pytest.mark.unit
    def test_bytes_conversion(self):
        """Should convert bytes to bytesValue (decoded as UTF-8)."""
        result = _attr_value_to_otlp(b"hello")
        assert result == {"bytesValue": "hello"}

    @pytest.mark.unit
    def test_bytes_with_invalid_utf8(self):
        """Should handle bytes with invalid UTF-8 by replacing invalid chars."""
        result = _attr_value_to_otlp(b"\xff\xfe")
        assert "bytesValue" in result

    @pytest.mark.unit
    def test_list_conversion(self):
        """Should convert list to arrayValue."""
        result = _attr_value_to_otlp([1, "two", 3.0])
        assert result == {
            "arrayValue": {
                "values": [
                    {"intValue": "1"},
                    {"stringValue": "two"},
                    {"doubleValue": 3.0},
                ]
            }
        }

    @pytest.mark.unit
    def test_tuple_conversion(self):
        """Should convert tuple to arrayValue."""
        result = _attr_value_to_otlp((True, False))
        assert result == {
            "arrayValue": {
                "values": [
                    {"boolValue": True},
                    {"boolValue": False},
                ]
            }
        }

    @pytest.mark.unit
    def test_empty_list_conversion(self):
        """Should convert empty list to empty arrayValue."""
        result = _attr_value_to_otlp([])
        assert result == {"arrayValue": {"values": []}}

    @pytest.mark.unit
    def test_unknown_type_converted_to_string(self):
        """Should convert unknown types to stringValue via str()."""
        result = _attr_value_to_otlp({"key": "value"})
        assert result == {"stringValue": "{'key': 'value'}"}

    @pytest.mark.unit
    def test_none_converted_to_string(self):
        """Should convert None to stringValue."""
        result = _attr_value_to_otlp(None)
        assert result == {"stringValue": "None"}
