"""Test OTLP output contracts using InMemoryMetricReader.

CRITICAL: These tests validate actual OpenTelemetry output format,
not internal mock state. This ensures the SDK produces valid telemetry
that can be consumed by OTLP collectors.

Addresses adversarial review finding: Previous tests mocked internal
state (_predictions_counter.add) instead of validating actual OTLP output,
providing false confidence.
"""

import pytest
from opentelemetry.sdk.metrics.export import InMemoryMetricReader


class TestOTLPMetricContracts:
    """Test that MCAClient produces valid OTLP metric output."""

    def test_record_prediction_produces_valid_otlp_counter(
        self, mca_client_factory, in_memory_metric_reader
    ):
        """Test record_prediction() outputs valid OTLP counter metric.

        Validates:
        - Metric is present in OTLP output
        - Metric has correct name
        - Metric has correct data type (Counter)
        - Metric has expected attributes
        """
        client = mca_client_factory()

        # Record prediction
        client.record_prediction(latency=0.123, prediction_id="pred-001", model_version="2.0")

        # Force export
        client._meter_provider.force_flush()

        # CRITICAL: Validate actual OTLP output, not internal mock state
        metrics_data = in_memory_metric_reader.get_metrics_data()

        # Debug: Print all found metrics
        all_metrics = []
        if metrics_data and metrics_data.resource_metrics:
            for resource_metrics in metrics_data.resource_metrics:
                for scope_metrics in resource_metrics.scope_metrics:
                    for metric in scope_metrics.metrics:
                        all_metrics.append(metric.name)

        # Find predictions metric (try various possible names)
        predictions_metric = None
        if metrics_data and metrics_data.resource_metrics:
            for resource_metrics in metrics_data.resource_metrics:
                for scope_metrics in resource_metrics.scope_metrics:
                    for metric in scope_metrics.metrics:
                        # Match any metric with "predictions" in the name
                        if "predictions" in metric.name.lower():
                            predictions_metric = metric
                            break

        # Assert metric exists in OTLP output
        assert (
            predictions_metric is not None
        ), f"Predictions metric not found in OTLP output. Found metrics: {all_metrics}"

        # Validate metric type (should be Counter/Sum)
        assert hasattr(
            predictions_metric.data, "data_points"
        ), "Metric should have data_points (Counter/Sum type)"

        # Validate data points exist
        assert (
            len(predictions_metric.data.data_points) > 0
        ), "Metric should have at least one data point"

        # Validate attributes
        data_point = predictions_metric.data.data_points[0]
        attributes = dict(data_point.attributes or {})

        # Check for custom attributes
        assert (
            attributes.get("model_version") == "2.0"
        ), "Custom attribute 'model_version' not found in OTLP output"

    def test_record_metric_produces_histogram_by_default(
        self, mca_client_factory, in_memory_metric_reader
    ):
        """Test record_metric() produces Histogram OTLP type by default.

        CURRENT BEHAVIOR: record_metric() always creates histograms internally.
        This test validates the actual implementation behavior.
        """
        # Create client with strict_validation=False to allow custom metric names
        client = mca_client_factory(strict_validation=False)

        # Record a metric (creates histogram internally)
        client.record_metric(name="test.requests", value=1)

        # Force export
        client._meter_provider.force_flush()

        # Validate OTLP output
        metrics_data = in_memory_metric_reader.get_metrics_data()

        found_metric = None
        if metrics_data and metrics_data.resource_metrics:
            for resource_metrics in metrics_data.resource_metrics:
                for scope_metrics in resource_metrics.scope_metrics:
                    for metric in scope_metrics.metrics:
                        if metric.name == "test.requests":
                            found_metric = metric
                            break

        assert found_metric is not None, "test.requests metric not in OTLP output"

        # Validate it's a Histogram (current SDK implementation)
        from opentelemetry.sdk.metrics.export import Histogram

        assert isinstance(
            found_metric.data, Histogram
        ), f"Expected Histogram, got {type(found_metric.data).__name__}"

    def test_latency_histogram_produces_correct_otlp_type(
        self, mca_client_factory, in_memory_metric_reader
    ):
        """Test that latency metrics produce Histogram OTLP type.

        Validates that record_prediction() latency creates a Histogram,
        which is the correct type for latency distributions.
        """
        client = mca_client_factory()

        # Record prediction with latency
        client.record_prediction(latency=0.123, prediction_id="pred-001")

        # Force export
        client._meter_provider.force_flush()

        # Validate OTLP output
        metrics_data = in_memory_metric_reader.get_metrics_data()

        # Find latency histogram
        latency_metric = None
        for resource_metrics in metrics_data.resource_metrics:
            for scope_metrics in resource_metrics.scope_metrics:
                for metric in scope_metrics.metrics:
                    if "latency" in metric.name.lower():
                        latency_metric = metric
                        break

        assert latency_metric is not None, "Latency metric not found in OTLP output"

        # Validate it's a Histogram (correct for latency distributions)
        from opentelemetry.sdk.metrics.export import Histogram

        assert isinstance(
            latency_metric.data, Histogram
        ), f"Expected Histogram for latency, got {type(latency_metric.data).__name__}"

    def test_prediction_counter_produces_correct_otlp_type(
        self, mca_client_factory, in_memory_metric_reader
    ):
        """Test that prediction counter produces Sum OTLP type.

        Validates that record_prediction() creates a Counter (Sum type),
        which is correct for counting prediction events.
        """
        client = mca_client_factory()

        # Record prediction
        client.record_prediction(latency=0.1, prediction_id="pred-001")

        # Force export
        client._meter_provider.force_flush()

        # Validate OTLP output
        metrics_data = in_memory_metric_reader.get_metrics_data()

        # Find predictions counter (mca.predictions or model.predictions)
        predictions_metric = None
        for resource_metrics in metrics_data.resource_metrics:
            for scope_metrics in resource_metrics.scope_metrics:
                for metric in scope_metrics.metrics:
                    if "predictions" in metric.name and "latency" not in metric.name:
                        predictions_metric = metric
                        break

        assert predictions_metric is not None, "Predictions counter not found in OTLP output"

        # Validate it's a Sum (counter type - correct for event counting)
        from opentelemetry.sdk.metrics.export import Sum

        assert isinstance(
            predictions_metric.data, Sum
        ), f"Expected Sum (counter) for predictions, got {type(predictions_metric.data).__name__}"

    def test_resource_attributes_appear_in_otlp_output(
        self, mca_client_factory, in_memory_metric_reader
    ):
        """Test that resource attributes are present in OTLP output.

        Validates that service.name, model.id, etc. appear in the
        resource section of OTLP output, not as metric attributes.
        """
        client = mca_client_factory(service_name="test-model-service", model_id="mdl-123")

        # Record any metric
        client.record_prediction(latency=0.1)

        # Force export
        client._meter_provider.force_flush()

        # Validate OTLP output
        metrics_data = in_memory_metric_reader.get_metrics_data()

        # Check resource attributes
        assert metrics_data and metrics_data.resource_metrics, "No resource metrics in OTLP output"
        assert len(metrics_data.resource_metrics) > 0, "No resource metrics in OTLP output"

        resource = metrics_data.resource_metrics[0].resource
        resource_attrs = dict(resource.attributes or {})

        # Validate required resource attributes (note: fixture uses "test-model" from conftest)
        assert (
            "service.name" in resource_attrs
        ), f"service.name not in OTLP resource attributes. Found: {list(resource_attrs.keys())}"
        assert (
            "model.id" in resource_attrs
        ), f"model.id not in OTLP resource attributes. Found: {list(resource_attrs.keys())}"


class TestOTLPTraceContracts:
    """Test that MCAClient produces valid OTLP trace output."""

    def test_prediction_span_in_otlp_output(self, mca_client_factory, in_memory_span_exporter):
        """Test that prediction tracking creates valid OTLP spans.

        Validates:
        - Span is present in OTLP output
        - Span has correct name
        - Span has expected attributes
        """
        client = mca_client_factory()

        # Record prediction with span (use trace() context manager)
        with client.trace("test-prediction") as span:
            span.set_attribute("test_attr", "test_value")

        # Force export
        client._tracer_provider.force_flush()

        # Validate OTLP output
        spans = in_memory_span_exporter.get_finished_spans()

        assert len(spans) > 0, "No spans in OTLP output"

        # Find our span
        prediction_span = None
        for span in spans:
            if span.name == "test-prediction":
                prediction_span = span
                break

        assert prediction_span is not None, "Prediction span not found in OTLP output"

        # Validate attributes
        span_attrs = dict(prediction_span.attributes or {})
        assert (
            span_attrs.get("test_attr") == "test_value"
        ), "Custom attribute not in OTLP span output"


class TestMetricTypeValidation:
    """Test that SDK creates correct OTLP metric types.

    Addresses adversarial review finding #10: Tests must verify the SDK
    constructs correct underlying OTLP types (Counter/Sum vs Histogram)
    rather than just checking internal mock state.
    """

    @staticmethod
    def extract_metric_type(in_memory_metric_reader, metric_name: str):
        """Helper to extract OTLP metric type from InMemoryMetricReader output.

        Args:
            in_memory_metric_reader: InMemoryMetricReader fixture
            metric_name: Name of metric to find

        Returns:
            Tuple of (metric_data, type_name) or (None, None) if not found

        Example:
            >>> metric, type_name = extract_metric_type(reader, "mca.predictions")
            >>> assert type_name == "Sum"
        """
        from opentelemetry.sdk.metrics.export import Sum, Histogram, Gauge

        metrics_data = in_memory_metric_reader.get_metrics_data()

        for resource_metrics in metrics_data.resource_metrics:
            for scope_metrics in resource_metrics.scope_metrics:
                for metric in scope_metrics.metrics:
                    if metric.name == metric_name or metric_name in metric.name:
                        # Determine type
                        if isinstance(metric.data, Sum):
                            return metric.data, "Sum"
                        elif isinstance(metric.data, Histogram):
                            return metric.data, "Histogram"
                        elif isinstance(metric.data, Gauge):
                            return metric.data, "Gauge"
                        else:
                            return metric.data, type(metric.data).__name__

        return None, None

    def test_helper_extracts_counter_type(self, mca_client_factory, in_memory_metric_reader):
        """Test helper correctly identifies Sum (counter) types."""
        client = mca_client_factory()

        # Record prediction (creates counter)
        client.record_prediction(latency=0.1)

        # Force export
        client._meter_provider.force_flush()

        # Use helper to extract type
        metric_data, type_name = self.extract_metric_type(in_memory_metric_reader, "predictions")

        assert metric_data is not None, "Predictions metric not found"
        assert type_name == "Sum", f"Expected Sum, got {type_name}"

    def test_helper_extracts_histogram_type(self, mca_client_factory, in_memory_metric_reader):
        """Test helper correctly identifies Histogram types."""
        client = mca_client_factory()

        # Record prediction with latency (creates histogram)
        client.record_prediction(latency=0.123)

        # Force export
        client._meter_provider.force_flush()

        # Use helper to extract type
        metric_data, type_name = self.extract_metric_type(in_memory_metric_reader, "latency")

        assert metric_data is not None, "Latency metric not found"
        assert type_name == "Histogram", f"Expected Histogram, got {type_name}"

    def test_all_sdk_metrics_have_correct_otlp_types(
        self, mca_client_factory, in_memory_metric_reader
    ):
        """Comprehensive test validating ALL SDK metrics produce correct OTLP types.

        CRITICAL: Ensures the SDK constructs semantically appropriate metric types:
        - Counters (Sum): Event counts that only increase
        - Histograms: Value distributions (latency, duration, sizes)
        """
        # Create client with strict_validation=False for custom metric
        client = mca_client_factory(strict_validation=False)

        # Record various metrics
        client.record_prediction(latency=0.1, prediction_id="pred-001")
        client.record_metric(name="model.custom_ratio", value=0.92)

        # Force export
        client._meter_provider.force_flush()

        # Define expected types for each metric pattern
        expected_types = {
            "predictions": "Sum",  # Event counter
            "latency": "Histogram",  # Latency distribution
            "custom_ratio": "Histogram",  # Generic metrics (current implementation)
        }

        # Validate each expected metric
        metrics_data = in_memory_metric_reader.get_metrics_data()
        found_metrics = {}

        if metrics_data and metrics_data.resource_metrics:
            for resource_metrics in metrics_data.resource_metrics:
                for scope_metrics in resource_metrics.scope_metrics:
                    for metric in scope_metrics.metrics:
                        metric_data, type_name = self.extract_metric_type(
                            in_memory_metric_reader, metric.name
                        )
                        found_metrics[metric.name] = type_name

        # Check that expected types match
        for pattern, expected_type in expected_types.items():
            matching_metrics = [name for name in found_metrics.keys() if pattern in name]
            assert (
                len(matching_metrics) > 0
            ), f"No metrics found matching pattern '{pattern}'. Found: {list(found_metrics.keys())}"

            for metric_name in matching_metrics:
                actual_type = found_metrics[metric_name]
                assert (
                    actual_type == expected_type
                ), f"Metric '{metric_name}' has type {actual_type}, expected {expected_type}"


class TestOTLPValidationFailures:
    """Test that invalid data is rejected before reaching OTLP layer.

    These tests ensure validation happens early, preventing malformed
    data from being passed to OpenTelemetry exporters.
    """

    def test_invalid_metric_name_rejected_before_otlp(
        self, mca_client_factory, in_memory_metric_reader
    ):
        """Test that invalid metric names are rejected before OTLP export.

        Validates that validation errors prevent bad data from reaching
        the OTLP exporter, protecting downstream systems.
        """
        from mca_sdk.utils.exceptions import ValidationError

        # Create client with strict validation enabled (default)
        client = mca_client_factory()

        # Attempt to record metric with invalid name (contains spaces)
        with pytest.raises(ValidationError, match="invalid character|metric.*name"):
            client.record_metric(name="invalid name with spaces", value=1)  # Invalid

        # Verify nothing was exported to OTLP
        client._meter_provider.force_flush()
        metrics_data = in_memory_metric_reader.get_metrics_data()

        # Should have no metrics with invalid name
        if metrics_data and metrics_data.resource_metrics:
            for resource_metrics in metrics_data.resource_metrics:
                for scope_metrics in resource_metrics.scope_metrics:
                    for metric in scope_metrics.metrics:
                        assert (
                            "invalid name" not in metric.name
                        ), "Invalid metric name reached OTLP output despite validation"
