"""Integration tests for OpenTelemetry SDK instrumentation.

Tests verify MeterProvider, LoggerProvider, and TracerProvider initialization,
metric creation (counter, histogram), and graceful failure when OTLP endpoints
are unreachable.
"""

import pytest
import logging
from unittest.mock import patch, Mock
import requests.exceptions

from opentelemetry import metrics, trace
from opentelemetry.sdk.metrics import MeterProvider
from opentelemetry.sdk.metrics.export import PeriodicExportingMetricReader
from opentelemetry.exporter.otlp.proto.http.metric_exporter import OTLPMetricExporter
from opentelemetry.exporter.otlp.proto.http._log_exporter import OTLPLogExporter
from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter
from opentelemetry.sdk._logs import LoggerProvider, LoggingHandler
from opentelemetry.sdk._logs.export import BatchLogRecordProcessor
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import BatchSpanProcessor
from opentelemetry.sdk.resources import Resource


class TestMeterProviderInitialization:
    """Test MeterProvider initialization without errors."""

    def test_meter_provider_creates_successfully(self, meter_provider):
        """Test that MeterProvider initializes without error."""
        assert meter_provider is not None
        assert isinstance(meter_provider, MeterProvider)

    def test_meter_provider_with_resource_attributes(self, meter_provider, resource_attributes):
        """Test that MeterProvider includes resource attributes."""
        resource = meter_provider._sdk_config.resource

        assert resource.attributes["service.name"] == resource_attributes["service.name"]
        assert resource.attributes["model.id"] == resource_attributes["model.id"]
        assert resource.attributes["model.version"] == resource_attributes["model.version"]
        assert resource.attributes["team.name"] == resource_attributes["team.name"]

    def test_meter_provider_shutdown_gracefully(self, meter_provider):
        """Test that MeterProvider shuts down without error."""
        # Should not raise exception
        meter_provider.shutdown()


class TestMetricCreation:
    """Test metric creation and recording."""

    def test_counter_creation(self, meter_provider):
        """Test that counter metric can be created."""
        meter = meter_provider.get_meter("test.meter")
        counter = meter.create_counter("test_counter", description="Test counter")
        assert counter is not None

    def test_histogram_creation(self, meter_provider):
        """Test that histogram metric can be created."""
        meter = meter_provider.get_meter("test.meter")
        histogram = meter.create_histogram("test_histogram", description="Test histogram", unit="s")
        assert histogram is not None

    def test_counter_add_records_value(self, meter_provider, in_memory_metric_reader):
        """Test that counter.add() records values correctly."""
        meter = meter_provider.get_meter("test.meter")
        counter = meter.create_counter("predictions_total")

        # Record values
        counter.add(5)
        counter.add(3)

        # Force collection
        meter_provider.force_flush()

        # Verify data
        metrics_data = in_memory_metric_reader.get_metrics_data()
        assert metrics_data is not None

        resource_metrics = metrics_data.resource_metrics
        assert len(resource_metrics) > 0

        scope_metrics = resource_metrics[0].scope_metrics
        assert len(scope_metrics) > 0

        # Find counter and verify sum
        found = False
        for metric in scope_metrics[0].metrics:
            if metric.name == "predictions_total":
                assert metric.data.data_points[0].value == 8  # 5 + 3
                found = True
                break
        assert found, "Counter metric not found"

    def test_histogram_record_records_value(self, meter_provider, in_memory_metric_reader):
        """Test that histogram.record() records values correctly."""
        meter = meter_provider.get_meter("test.meter")
        histogram = meter.create_histogram("latency_seconds", unit="s")

        # Record values
        histogram.record(0.1)
        histogram.record(0.2)
        histogram.record(0.3)

        # Force collection
        meter_provider.force_flush()

        # Verify data
        metrics_data = in_memory_metric_reader.get_metrics_data()
        resource_metrics = metrics_data.resource_metrics
        scope_metrics = resource_metrics[0].scope_metrics

        # Find histogram
        found = False
        for metric in scope_metrics[0].metrics:
            if metric.name == "latency_seconds":
                # Histogram should have count of 3
                assert metric.data.data_points[0].count == 3
                found = True
                break
        assert found, "Histogram metric not found"

    def test_multiple_metrics_with_attributes(self, meter_provider, in_memory_metric_reader):
        """Test creating multiple metrics with attributes."""
        meter = meter_provider.get_meter("test.meter")
        counter = meter.create_counter("requests")
        histogram = meter.create_histogram("duration")

        # Record with attributes
        counter.add(1, {"endpoint": "/predict", "status": "success"})
        histogram.record(0.15, {"endpoint": "/predict"})

        meter_provider.force_flush()

        metrics_data = in_memory_metric_reader.get_metrics_data()
        assert metrics_data is not None


class TestLoggerProviderInitialization:
    """Test LoggerProvider initialization."""

    def test_logger_provider_creates_successfully(self, logger_provider):
        """Test that LoggerProvider initializes without error."""
        assert logger_provider is not None
        assert isinstance(logger_provider, LoggerProvider)

    def test_logger_provider_with_resource_attributes(self, logger_provider, resource_attributes):
        """Test that LoggerProvider includes resource attributes."""
        resource = logger_provider.resource

        assert resource.attributes["service.name"] == resource_attributes["service.name"]
        assert resource.attributes["model.id"] == resource_attributes["model.id"]

    def test_logger_provider_force_flush(self, logger_provider):
        """Test that LoggerProvider force_flush() works."""
        result = logger_provider.force_flush()
        assert result is True


class TestTracerProviderInitialization:
    """Test TracerProvider initialization."""

    def test_tracer_provider_creates_successfully(self, tracer_provider):
        """Test that TracerProvider initializes without error."""
        assert tracer_provider is not None
        assert isinstance(tracer_provider, TracerProvider)

    def test_tracer_provider_with_resource_attributes(self, tracer_provider, resource_attributes):
        """Test that TracerProvider includes resource attributes."""
        resource = tracer_provider.resource

        assert resource.attributes["service.name"] == resource_attributes["service.name"]
        assert resource.attributes["model.id"] == resource_attributes["model.id"]

    def test_tracer_provider_force_flush(self, tracer_provider):
        """Test that TracerProvider force_flush() works."""
        result = tracer_provider.force_flush()
        assert result is True

    def test_span_creation(self, tracer_provider, in_memory_span_exporter):
        """Test that spans can be created and exported."""
        tracer = tracer_provider.get_tracer("test.tracer")

        with tracer.start_as_current_span("test_span") as span:
            span.set_attribute("test.key", "test_value")

        # Get exported spans
        spans = in_memory_span_exporter.get_finished_spans()
        assert len(spans) == 1
        assert spans[0].name == "test_span"
        assert spans[0].attributes["test.key"] == "test_value"


class TestGracefulFailure:
    """Test graceful failure when OTLP endpoints are unreachable."""

    @patch("opentelemetry.exporter.otlp.proto.http.metric_exporter.OTLPMetricExporter.export")
    def test_otlp_metric_exporter_network_failure(self, mock_export, resource):
        """Test that metric export failure doesn't crash application."""
        # Make export fail
        mock_export.return_value = False

        # Create provider with real OTLP exporter
        otlp_exporter = OTLPMetricExporter(endpoint="http://unreachable:4318/v1/metrics")
        reader = PeriodicExportingMetricReader(otlp_exporter, export_interval_millis=1000)

        # Should not raise exception
        try:
            provider = MeterProvider(resource=resource, metric_readers=[reader])
            meter = provider.get_meter("test")
            counter = meter.create_counter("test_counter")
            counter.add(1)
            provider.force_flush(timeout_millis=5000)
        except Exception as e:
            pytest.fail(f"Unexpected exception raised: {e}")
        finally:
            provider.shutdown()

    @patch("opentelemetry.exporter.otlp.proto.http._log_exporter.OTLPLogExporter.export")
    def test_otlp_log_exporter_network_failure(self, mock_export, resource):
        """Test that log export failure doesn't crash application."""
        mock_export.return_value = False

        otlp_exporter = OTLPLogExporter(endpoint="http://unreachable:4318/v1/logs")
        processor = BatchLogRecordProcessor(otlp_exporter)

        try:
            provider = LoggerProvider(resource=resource)
            provider.add_log_record_processor(processor)
            provider.force_flush(timeout_millis=5000)
        except Exception as e:
            pytest.fail(f"Unexpected exception raised: {e}")
        finally:
            provider.shutdown()

    @patch("opentelemetry.exporter.otlp.proto.http.trace_exporter.OTLPSpanExporter.export")
    def test_otlp_trace_exporter_network_failure(self, mock_export, resource):
        """Test that trace export failure doesn't crash application."""
        mock_export.return_value = False

        otlp_exporter = OTLPSpanExporter(endpoint="http://unreachable:4318/v1/traces")
        processor = BatchSpanProcessor(otlp_exporter)

        try:
            provider = TracerProvider(resource=resource)
            provider.add_span_processor(processor)
            tracer = provider.get_tracer("test")
            with tracer.start_as_current_span("test"):
                pass
            provider.force_flush(timeout_millis=5000)
        except Exception as e:
            pytest.fail(f"Unexpected exception raised: {e}")
        finally:
            provider.shutdown()

    def test_application_continues_on_export_failure(self, resource):
        """Test that application logic continues when export fails."""
        with patch(
            "opentelemetry.exporter.otlp.proto.http.metric_exporter.OTLPMetricExporter.export"
        ) as mock_export:
            mock_export.return_value = False

            exporter = OTLPMetricExporter(endpoint="http://unreachable:4318/v1/metrics")
            reader = PeriodicExportingMetricReader(exporter, export_interval_millis=1000)
            provider = MeterProvider(resource=resource, metric_readers=[reader])

            meter = provider.get_meter("test")
            counter = meter.create_counter("operations")

            # Application logic continues despite export failure
            for i in range(10):
                counter.add(1)

            provider.force_flush(timeout_millis=5000)
            provider.shutdown()

            # Should have attempted to export
            assert mock_export.called


class TestResourceAttributes:
    """Test resource attributes propagation across signals."""

    def test_resource_attributes_propagate_to_metrics(
        self, meter_provider, in_memory_metric_reader, resource_attributes
    ):
        """Test that resource attributes are included in metric data."""
        meter = meter_provider.get_meter("test")
        counter = meter.create_counter("test")
        counter.add(1)

        meter_provider.force_flush()

        metrics_data = in_memory_metric_reader.get_metrics_data()
        resource = metrics_data.resource_metrics[0].resource

        assert resource.attributes["service.name"] == resource_attributes["service.name"]
        assert resource.attributes["model.id"] == resource_attributes["model.id"]

    def test_resource_attributes_propagate_to_traces(
        self, tracer_provider, in_memory_span_exporter, resource_attributes
    ):
        """Test that resource attributes are included in trace data."""
        tracer = tracer_provider.get_tracer("test")

        with tracer.start_as_current_span("test"):
            pass

        spans = in_memory_span_exporter.get_finished_spans()
        resource = spans[0].resource

        assert resource.attributes["service.name"] == resource_attributes["service.name"]
        assert resource.attributes["model.id"] == resource_attributes["model.id"]
