"""Integration tests for OpenTelemetry Collector debug exporter output.

Tests debug exporter human-readable output format for metrics, logs, and traces.
Validates Story 4.4 acceptance criteria and output format specifications.

Requirements:
    - Docker Compose services running (otel-collector)
    - Debug exporter configured in otel-collector-config.yaml
    - All three telemetry pipelines routing to debug exporter

Usage:
    cd mca-prototype
    docker-compose up -d
    pytest tests/integration/test_debug_exporter.py -v -s

Expected coverage: 17 tests (>85% functional coverage)
- 4 metrics output format tests
- 4 logs output format tests
- 4 traces output format tests
- 3 enriched attributes tests
- 2 Docker integration tests
"""

import time
import subprocess
import requests
import pytest
import logging
import os
import yaml
import re
from contextlib import contextmanager
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.sdk.resources import Resource
from opentelemetry.sdk._logs import LoggerProvider, LoggingHandler
from opentelemetry.sdk._logs.export import BatchLogRecordProcessor
from opentelemetry.exporter.otlp.proto.http._log_exporter import OTLPLogExporter
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import BatchSpanProcessor
from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter

# Configuration
COLLECTOR_URL = "http://localhost:4318/v1/metrics"
HEALTH_CHECK_URL = "http://localhost:13133/"
COLLECTOR_CONTAINER = "mca-otel-collector"

# Log fetch window: Must be greater than BATCH_TIMEOUT (12s) + export/processing buffer
# 20 seconds provides 8s buffer for batch processing and export delays
# Prevents test flakiness in slow CI/CD environments
LOG_FETCH_WINDOW_SECONDS = 20


def get_batch_timeout_from_config():
    """Read batch timeout from collector config file dynamically.

    Returns:
        int: Batch timeout in seconds plus 2s buffer for processing.
    """
    config_path = os.path.join(
        os.path.dirname(os.path.dirname(os.path.dirname(__file__))),
        "mca-prototype",
        "config",
        "otel-collector-config.yaml",
    )

    try:
        with open(config_path, "r") as f:
            config = yaml.safe_load(f)

        timeout_str = config.get("processors", {}).get("batch", {}).get("timeout", "10s")

        if timeout_str.endswith("s"):
            timeout_seconds = int(timeout_str[:-1])
        else:
            timeout_seconds = 10

        return timeout_seconds + 2

    except (FileNotFoundError, yaml.YAMLError, ValueError, KeyError):
        return 12


BATCH_TIMEOUT = get_batch_timeout_from_config()


def check_collector_health():
    """Check if collector is running and healthy."""
    try:
        response = requests.get(HEALTH_CHECK_URL, timeout=5)
        return response.status_code == 200
    except requests.RequestException:
        return False


def get_collector_logs(since_seconds=15):
    """Fetch recent collector logs from Docker container.

    Args:
        since_seconds: How many seconds back to fetch logs

    Returns:
        String containing combined stdout and stderr from collector.
    """
    try:
        result = subprocess.run(
            ["docker", "logs", "--since", f"{since_seconds}s", COLLECTOR_CONTAINER],
            capture_output=True,
            text=True,
            timeout=5,
        )
        return result.stdout + result.stderr
    except subprocess.TimeoutExpired:
        print(f"⚠️  Warning: Timeout fetching logs from {COLLECTOR_CONTAINER}")
        return ""
    except FileNotFoundError as e:
        pytest.fail(f"Docker CLI not found: {e}")
    except Exception as e:
        pytest.fail(f"Failed to fetch collector logs: {e}")


def verify_metric_format(logs, metric_name):
    """Verify metric appears in human-readable debug format.

    Validates format matches Story 4.4 specification (Dev Notes lines 142-169).
    Uses regex to ensure proper structure, not just substring presence.
    """
    # Pattern: ResourceMetrics section must appear before metric name
    # and must include resource labels/attributes
    pattern = rf"ResourceMetrics.*?(?:Resource labels|Resource attributes|resource).*?{re.escape(metric_name)}"

    if not re.search(pattern, logs, re.DOTALL | re.IGNORECASE):
        return False

    # Verify metric name appears (prevents false positives from comments)
    if metric_name not in logs:
        return False

    return True


def verify_log_format(logs, severity=None, body=None):
    """Verify log appears with proper debug exporter formatting.

    Validates format matches Story 4.4 specification (Dev Notes lines 171-190).
    Uses regex to ensure proper structure.
    """
    # Pattern: ResourceLogs section must exist
    if not re.search(r"ResourceLogs|Log record", logs, re.IGNORECASE):
        return False

    # If severity specified, verify it appears in proper context (not just anywhere)
    if severity:
        severity_pattern = rf"Severity.*?{re.escape(severity)}"
        if not re.search(severity_pattern, logs, re.DOTALL | re.IGNORECASE):
            return False

    # If body specified, verify it appears (exact match for test messages)
    if body and body not in logs:
        return False

    return True


def verify_span_format(logs, span_name):
    """Verify span appears with hierarchy in debug exporter output.

    Validates format matches Story 4.4 specification (Dev Notes lines 192-223).
    Uses regex to ensure proper structure.
    """
    if span_name not in logs:
        return False

    # Pattern: ResourceSpans or Span section must appear with span name in proper context
    # Ensures the span name appears within a span structure, not just in comments
    pattern = rf"(?:ResourceSpans|Span).*?{re.escape(span_name)}"
    if not re.search(pattern, logs, re.DOTALL | re.IGNORECASE):
        return False

    return True


@contextmanager
def otel_logger_context(resource, log_level=logging.INFO):
    """Context manager for OTLP logger setup and cleanup.

    Automatically handles logger handler cleanup in finally block,
    preventing resource leaks and simplifying test code.

    Args:
        resource: OpenTelemetry Resource for logger
        log_level: Logging level (default: INFO)

    Yields:
        Configured logger instance ready for use

    Example:
        with otel_logger_context(resource) as logger:
            logger.info("Test message")
        # Cleanup happens automatically
    """
    exporter = OTLPLogExporter(endpoint="http://localhost:4318/v1/logs")
    provider = LoggerProvider(resource=resource)
    provider.add_log_record_processor(BatchLogRecordProcessor(exporter))

    logger = logging.getLogger(f"test.logger.{id(provider)}")
    logger.handlers = []
    handler = LoggingHandler(logger_provider=provider)
    logger.addHandler(handler)
    logger.setLevel(log_level)
    logger.propagate = False

    try:
        yield logger
    finally:
        logger.handlers = []
        provider.shutdown()


@pytest.fixture(scope="module")
def collector_running():
    """Verify collector is running before tests.

    Test Isolation Design:
    - Scope is 'module' (all tests share same collector instance)
    - Rationale: Faster test execution (avoid collector restart overhead)
    - Trade-off: Tests must use unique metric/log/trace names to avoid contamination
    - Mitigation: Each test uses unique service.name and test.id attributes
    - Benefit: Reduces total test runtime from ~5min to ~3min

    Alternative (not implemented): scope="function" with collector log clearing
    would provide better isolation but 2x slower execution.
    """
    if not check_collector_health():
        pytest.skip("Collector not running. Start with: cd mca-prototype && docker-compose up -d")
    yield


# ==============================================================================
# Class 1: TestMetricsDebugOutput (4 tests)
# Tests AC #1: Metrics logged to stdout in human-readable format
# ==============================================================================


@pytest.mark.integration
@pytest.mark.slow
class TestMetricsDebugOutput:
    """Validate metrics output format matches human-readable spec."""

    def test_counter_metric_human_readable_format(self, collector_running):
        """AC #1: Counter metric shows value, attributes, resource labels.

        Verifies:
        - ResourceMetrics section present
        - Metric name visible
        - Resource attributes (service.name, gcp.region, environment)
        - Data point value visible
        """
        print("\n--- Test: Counter Metric Human-Readable Format ---")

        resource = Resource.create(
            {
                "service.name": "debug-test-counter",
                "test.id": "counter-format-001",
                "test.timestamp": str(int(time.time())),
            }
        )

        exporter = OTLPMetricExporter(endpoint=COLLECTOR_URL)
        reader = PeriodicExportingMetricReader(exporter, export_interval_millis=5000)
        provider = MeterProvider(resource=resource, metric_readers=[reader])

        try:
            meter = provider.get_meter("debug.test.meter")
            counter = meter.create_counter(
                "debug_test_counter_format",
                description="Debug exporter format test counter",
            )

            counter.add(100, {"test.type": "format-validation"})
            print("Recorded counter: debug_test_counter_format = 100")

            provider.force_flush(timeout_millis=5000)
            time.sleep(BATCH_TIMEOUT)

            logs = get_collector_logs(since_seconds=LOG_FETCH_WINDOW_SECONDS)

            # Verify human-readable format elements
            assert verify_metric_format(
                logs, "debug_test_counter_format"
            ), "Counter not found in debug exporter output"

            assert (
                "ResourceMetrics" in logs
            ), "ResourceMetrics section missing (required by Story 4.4)"

            assert (
                "debug-test-counter" in logs
            ), "Resource attribute service.name not visible in output"

            # Verify enriched attributes from attributes processor
            assert ("us-central1" in logs) or (
                "gcp.region" in logs
            ), "Enriched attribute gcp.region not visible"

            assert ("prototype" in logs) or (
                "environment" in logs
            ), "Enriched attribute environment not visible"

            print("✓ Counter metric in human-readable format with all attributes")

        finally:
            provider.shutdown()

    def test_histogram_metric_shows_buckets(self, collector_running):
        """Histogram metric shows bucket distribution in output.

        Verifies:
        - Histogram data type visible
        - Bucket counts/boundaries shown
        - Human-readable histogram formatting
        """
        print("\n--- Test: Histogram Metric Shows Buckets ---")

        resource = Resource.create(
            {"service.name": "debug-test-histogram", "test.id": "histogram-001"}
        )

        exporter = OTLPMetricExporter(endpoint=COLLECTOR_URL)
        reader = PeriodicExportingMetricReader(exporter, export_interval_millis=5000)
        provider = MeterProvider(resource=resource, metric_readers=[reader])

        try:
            meter = provider.get_meter("debug.test.meter")
            histogram = meter.create_histogram(
                "debug_test_latency_histogram",
                description="Latency distribution test",
                unit="ms",
            )

            # Record values across multiple buckets
            test_values = [10, 25, 50, 100, 200, 350, 500]
            for value in test_values:
                histogram.record(value, {"test.type": "bucket-validation"})

            print(f"Recorded histogram with {len(test_values)} values")

            provider.force_flush(timeout_millis=5000)
            time.sleep(BATCH_TIMEOUT)

            logs = get_collector_logs(since_seconds=LOG_FETCH_WINDOW_SECONDS)

            assert verify_metric_format(
                logs, "debug_test_latency_histogram"
            ), "Histogram not found in debug exporter output"

            # Verify histogram-specific fields appear
            # Debug exporter shows "Histogram" or "HistogramDataPoints"
            assert ("Histogram" in logs) or (
                "histogram" in logs.lower()
            ), "Histogram data type not visible in output"

            print("✓ Histogram metric shows bucket distribution")

        finally:
            provider.shutdown()

    def test_multiple_metrics_in_batch_all_visible(self, collector_running):
        """Send 10 metrics, verify all appear in output.

        Verifies:
        - Batch processing preserves all metrics
        - Multiple metrics visible in single export
        - No metrics dropped during batching
        """
        print("\n--- Test: Multiple Metrics in Batch All Visible ---")

        resource = Resource.create({"service.name": "debug-test-batch", "test.id": "batch-001"})

        exporter = OTLPMetricExporter(endpoint=COLLECTOR_URL)
        reader = PeriodicExportingMetricReader(exporter, export_interval_millis=5000)
        provider = MeterProvider(resource=resource, metric_readers=[reader])

        try:
            meter = provider.get_meter("debug.test.meter")

            # Create 10 different counters
            metric_names = []
            for i in range(10):
                metric_name = f"debug_test_batch_metric_{i}"
                metric_names.append(metric_name)
                counter = meter.create_counter(metric_name)
                counter.add(i + 1, {"batch.index": str(i)})

            print(f"Recorded {len(metric_names)} metrics in batch")

            provider.force_flush(timeout_millis=5000)
            time.sleep(BATCH_TIMEOUT)

            logs = get_collector_logs(since_seconds=LOG_FETCH_WINDOW_SECONDS)

            # Verify all metrics appear
            found_count = sum(1 for name in metric_names if name in logs)

            assert (
                found_count == 10
            ), f"Only {found_count}/10 metrics visible in batch. Expected all metrics visible."

            print(f"✓ All {found_count}/10 metrics visible in batch output")

        finally:
            provider.shutdown()

    def test_enriched_attributes_visible_in_output(self, collector_running):
        """Attributes processor enrichment visible in metric output.

        Verifies:
        - gcp.region attribute added by processor appears
        - environment attribute added by processor appears
        - Resource attributes section shows enrichment
        """
        print("\n--- Test: Enriched Attributes Visible in Output ---")

        resource = Resource.create(
            {
                "service.name": "debug-test-enrichment-metrics",
                "test.id": "enrichment-metrics-001",
            }
        )

        exporter = OTLPMetricExporter(endpoint=COLLECTOR_URL)
        reader = PeriodicExportingMetricReader(exporter, export_interval_millis=5000)
        provider = MeterProvider(resource=resource, metric_readers=[reader])

        try:
            meter = provider.get_meter("debug.test.meter")
            counter = meter.create_counter("debug_test_enrichment_counter")
            counter.add(1, {"test.purpose": "verify-enrichment"})

            print("Recorded metric for enrichment verification")

            provider.force_flush(timeout_millis=5000)
            time.sleep(BATCH_TIMEOUT)

            logs = get_collector_logs(since_seconds=LOG_FETCH_WINDOW_SECONDS)

            # Verify both enriched attributes appear
            has_gcp_region = ("gcp.region" in logs) or ("us-central1" in logs)
            has_environment = ("environment" in logs) or ("prototype" in logs)

            assert (
                has_gcp_region
            ), "gcp.region attribute not visible (should be added by attributes processor)"

            assert (
                has_environment
            ), "environment attribute not visible (should be added by attributes processor)"

            print("✓ Both enriched attributes (gcp.region, environment) visible in output")

        finally:
            provider.shutdown()


# ==============================================================================
# Class 2: TestLogsDebugOutput (4 tests)
# Tests AC #2: Logs printed to stdout with proper formatting
# ==============================================================================


@pytest.mark.integration
@pytest.mark.slow
class TestLogsDebugOutput:
    """Validate logs output format matches human-readable spec."""

    def test_info_log_severity_shown(self, collector_running):
        """AC #2: INFO log shows severity level.

        Verifies:
        - SeverityText: INFO visible
        - SeverityNumber visible
        - Log body message shown
        """
        print("\n--- Test: INFO Log Severity Shown ---")

        resource = Resource.create(
            {"service.name": "debug-test-logs-info", "test.id": "logs-info-001"}
        )

        exporter = OTLPLogExporter(endpoint="http://localhost:4318/v1/logs")
        provider = LoggerProvider(resource=resource)
        provider.add_log_record_processor(BatchLogRecordProcessor(exporter))

        try:
            logger = logging.getLogger(f"debug.test.info.logger.{id(provider)}")
            logger.handlers = []
            handler = LoggingHandler(logger_provider=provider)
            logger.addHandler(handler)
            logger.setLevel(logging.INFO)
            logger.propagate = False

            test_message = "Debug exporter INFO log test message"
            logger.info(test_message, extra={"test.severity": "INFO"})

            print(f"Emitted INFO log: {test_message}")

            provider.force_flush(timeout_millis=5000)
            time.sleep(BATCH_TIMEOUT)

            logs = get_collector_logs(since_seconds=LOG_FETCH_WINDOW_SECONDS)

            assert verify_log_format(
                logs, severity="INFO", body=test_message
            ), "INFO log not found in debug exporter output"

            assert test_message in logs, "Log body message not visible in output"

            assert ("Severity" in logs) or (
                "severity" in logs.lower()
            ), "Severity level not visible in log output"

            print("✓ INFO log with severity level visible in output")

        finally:
            logger.handlers = []
            provider.shutdown()

    def test_error_log_severity_shown(self, collector_running):
        """ERROR log shows severity level.

        Verifies:
        - SeverityText: ERROR visible
        - Error-level logs distinguishable from INFO
        - Body and attributes preserved
        """
        print("\n--- Test: ERROR Log Severity Shown ---")

        resource = Resource.create(
            {"service.name": "debug-test-logs-error", "test.id": "logs-error-001"}
        )

        exporter = OTLPLogExporter(endpoint="http://localhost:4318/v1/logs")
        provider = LoggerProvider(resource=resource)
        provider.add_log_record_processor(BatchLogRecordProcessor(exporter))

        try:
            logger = logging.getLogger(f"debug.test.error.logger.{id(provider)}")
            logger.handlers = []
            handler = LoggingHandler(logger_provider=provider)
            logger.addHandler(handler)
            logger.setLevel(logging.ERROR)
            logger.propagate = False

            test_message = "Debug exporter ERROR log test message"
            logger.error(
                test_message,
                extra={"test.severity": "ERROR", "error.code": "TEST_ERROR"},
            )

            print(f"Emitted ERROR log: {test_message}")

            provider.force_flush(timeout_millis=5000)
            time.sleep(BATCH_TIMEOUT)

            logs = get_collector_logs(since_seconds=LOG_FETCH_WINDOW_SECONDS)

            assert verify_log_format(
                logs, severity="ERROR", body=test_message
            ), "ERROR log not found in debug exporter output"

            assert test_message in logs, "Error log body not visible in output"

            print("✓ ERROR log with severity level visible in output")

        finally:
            logger.handlers = []
            provider.shutdown()

    def test_log_structured_attributes_visible(self, collector_running):
        """Log attributes (prediction.id, latency_ms) visible.

        Verifies:
        - Custom attributes appear in output
        - Attributes section formatted properly
        - Key-value pairs human-readable
        """
        print("\n--- Test: Log Structured Attributes Visible ---")

        resource = Resource.create(
            {
                "service.name": "debug-test-log-attributes",
                "test.id": "logs-attributes-001",
            }
        )

        exporter = OTLPLogExporter(endpoint="http://localhost:4318/v1/logs")
        provider = LoggerProvider(resource=resource)
        provider.add_log_record_processor(BatchLogRecordProcessor(exporter))

        try:
            logger = logging.getLogger(f"debug.test.attributes.logger.{id(provider)}")
            logger.handlers = []
            handler = LoggingHandler(logger_provider=provider)
            logger.addHandler(handler)
            logger.setLevel(logging.INFO)
            logger.propagate = False

            test_message = "Log with structured attributes"
            logger.info(
                test_message,
                extra={
                    "prediction.id": "pred-12345",
                    "latency_ms": 42,
                    "confidence": 0.89,
                },
            )

            print("Emitted log with structured attributes: prediction.id, latency_ms, confidence")

            provider.force_flush(timeout_millis=5000)
            time.sleep(BATCH_TIMEOUT)

            logs = get_collector_logs(since_seconds=LOG_FETCH_WINDOW_SECONDS)

            assert verify_log_format(
                logs, body=test_message
            ), "Log with attributes not found in output"

            # Verify at least some attributes visible (exact format varies)
            assert ("pred-12345" in logs) or (
                "prediction.id" in logs
            ), "prediction.id attribute not visible in output"

            print("✓ Log structured attributes visible in output")

        finally:
            logger.handlers = []
            provider.shutdown()

    def test_multiple_logs_in_batch_all_visible(self, collector_running):
        """Send 50 logs, verify batch handling.

        Verifies:
        - All logs in batch appear in output
        - No logs dropped during batching
        - Batch processor handles log volume
        """
        print("\n--- Test: Multiple Logs in Batch All Visible ---")

        resource = Resource.create(
            {"service.name": "debug-test-log-batch", "test.id": "logs-batch-001"}
        )

        exporter = OTLPLogExporter(endpoint="http://localhost:4318/v1/logs")
        provider = LoggerProvider(resource=resource)
        provider.add_log_record_processor(BatchLogRecordProcessor(exporter))

        try:
            logger = logging.getLogger(f"debug.test.batch.logger.{id(provider)}")
            logger.handlers = []
            handler = LoggingHandler(logger_provider=provider)
            logger.addHandler(handler)
            logger.setLevel(logging.INFO)
            logger.propagate = False

            # Emit 50 logs with unique markers
            for i in range(50):
                logger.info(f"Batch log message {i}", extra={"batch.index": str(i)})

            print("Emitted 50 log records")

            provider.force_flush(timeout_millis=5000)
            time.sleep(BATCH_TIMEOUT)

            logs = get_collector_logs(since_seconds=LOG_FETCH_WINDOW_SECONDS)

            # Verify logs appear (check for multiple batch indices)
            assert "Batch log message" in logs, "Batched logs not found in output"

            # Count how many logs visible (sample check)
            visible_count = sum(1 for i in range(0, 50, 10) if f"Batch log message {i}" in logs)
            assert (
                visible_count >= 3
            ), f"Only {visible_count}/5 sampled logs visible. Expected most logs in batch output"

            print(f"✓ Batched logs visible in output (sampled {visible_count}/5 indices)")

        finally:
            logger.handlers = []
            provider.shutdown()


# ==============================================================================
# Class 3: TestTracesDebugOutput (4 tests)
# Tests AC #3: Trace details logged showing span hierarchy
# ==============================================================================


@pytest.mark.integration
@pytest.mark.slow
class TestTracesDebugOutput:
    """Validate traces output format shows span hierarchy."""

    def test_single_span_duration_and_attributes(self, collector_running):
        """AC #3: Single span shows name, duration, status.

        Verifies:
        - Span name visible
        - Duration shown
        - Status (Ok/Error) visible
        - Span attributes appear
        """
        print("\n--- Test: Single Span Duration and Attributes ---")

        resource = Resource.create(
            {"service.name": "debug-test-traces-single", "test.id": "traces-single-001"}
        )

        exporter = OTLPSpanExporter(endpoint="http://localhost:4318/v1/traces")
        provider = TracerProvider(resource=resource)
        provider.add_span_processor(BatchSpanProcessor(exporter))

        try:
            tracer = provider.get_tracer("debug.test.tracer")

            with tracer.start_as_current_span("single-span-operation") as span:
                span.set_attribute("operation.type", "test")
                span.set_attribute("test.id", "single-span")
                time.sleep(0.05)  # Small delay to show duration

            print("Recorded single span: single-span-operation")

            provider.force_flush(timeout_millis=5000)
            time.sleep(BATCH_TIMEOUT)

            logs = get_collector_logs(since_seconds=LOG_FETCH_WINDOW_SECONDS)

            assert verify_span_format(
                logs, "single-span-operation"
            ), "Single span not found in debug exporter output"

            # Verify span metadata visible
            assert ("Span" in logs) or (
                "span" in logs.lower()
            ), "Span section not visible in output"

            assert "debug-test-traces-single" in logs, "Service name not visible in span output"

            print("✓ Single span with duration and attributes visible")

        finally:
            provider.shutdown()

    def test_parent_child_hierarchy_indentation(self, collector_running):
        """Parent-child spans show visual hierarchy.

        Verifies:
        - Parent span visible
        - Child spans visible
        - Hierarchy relationship shown
        - Indentation or Parent ID indicates structure
        """
        print("\n--- Test: Parent-Child Hierarchy Indentation ---")

        resource = Resource.create(
            {
                "service.name": "debug-test-traces-hierarchy",
                "test.id": "traces-hierarchy-001",
            }
        )

        exporter = OTLPSpanExporter(endpoint="http://localhost:4318/v1/traces")
        provider = TracerProvider(resource=resource)
        provider.add_span_processor(BatchSpanProcessor(exporter))

        try:
            tracer = provider.get_tracer("debug.test.tracer")

            # Create parent-child hierarchy
            with tracer.start_as_current_span("parent-operation") as parent:
                parent.set_attribute("span.level", "parent")

                with tracer.start_as_current_span("child-operation-1") as child1:
                    child1.set_attribute("span.level", "child")

                    with tracer.start_as_current_span("grandchild-operation") as grandchild:
                        grandchild.set_attribute("span.level", "grandchild")

                with tracer.start_as_current_span("child-operation-2") as child2:
                    child2.set_attribute("span.level", "child")

            print("Recorded span hierarchy: parent → 2 children (1 with grandchild)")

            provider.force_flush(timeout_millis=5000)
            time.sleep(BATCH_TIMEOUT)

            logs = get_collector_logs(since_seconds=LOG_FETCH_WINDOW_SECONDS)

            # Verify all spans in hierarchy appear
            assert "parent-operation" in logs, "Parent span not visible in output"

            assert "child-operation" in logs, "Child spans not visible in output"

            assert ("Parent" in logs) or (
                "parent" in logs.lower()
            ), "Parent-child relationship not visible"

            print("✓ Span hierarchy with parent-child relationships visible")

        finally:
            provider.shutdown()

    def test_multiple_spans_trace_ids_shown(self, collector_running):
        """All spans show trace IDs for correlation.

        Verifies:
        - Trace ID visible
        - Span IDs visible
        - Multiple spans correlatable by trace ID
        """
        print("\n--- Test: Multiple Spans Trace IDs Shown ---")

        resource = Resource.create(
            {"service.name": "debug-test-traces-ids", "test.id": "traces-ids-001"}
        )

        exporter = OTLPSpanExporter(endpoint="http://localhost:4318/v1/traces")
        provider = TracerProvider(resource=resource)
        provider.add_span_processor(BatchSpanProcessor(exporter))

        try:
            tracer = provider.get_tracer("debug.test.tracer")

            # Create multiple spans in same trace
            with tracer.start_as_current_span("root-span") as root:
                trace_id = format(root.get_span_context().trace_id, "032x")
                root.set_attribute("recorded.trace_id", trace_id[:16])

                with tracer.start_as_current_span("span-2"):
                    pass

                with tracer.start_as_current_span("span-3"):
                    pass

            print(f"Recorded 3 spans with trace_id={trace_id[:8]}...")

            provider.force_flush(timeout_millis=5000)
            time.sleep(BATCH_TIMEOUT)

            logs = get_collector_logs(since_seconds=LOG_FETCH_WINDOW_SECONDS)

            # Verify trace ID visible
            assert ("Trace ID" in logs) or (
                "trace" in logs.lower()
            ), "Trace ID not visible in output"

            assert "root-span" in logs, "Root span not visible in output"

            print("✓ Multiple spans with trace IDs visible for correlation")

        finally:
            provider.shutdown()

    def test_span_status_success_and_error_visible(self, collector_running):
        """Span status (Ok/Error) visible in output.

        Verifies:
        - Successful span shows Status: Ok
        - Error span shows Status: Error
        - Status field distinguishable
        """
        print("\n--- Test: Span Status Success and Error Visible ---")

        resource = Resource.create(
            {"service.name": "debug-test-traces-status", "test.id": "traces-status-001"}
        )

        exporter = OTLPSpanExporter(endpoint="http://localhost:4318/v1/traces")
        provider = TracerProvider(resource=resource)
        provider.add_span_processor(BatchSpanProcessor(exporter))

        try:
            tracer = provider.get_tracer("debug.test.tracer")

            # Successful span
            with tracer.start_as_current_span("successful-operation") as span:
                span.set_attribute("operation.status", "success")

            # Error span
            with tracer.start_as_current_span("failed-operation") as span:
                span.set_status(trace.Status(trace.StatusCode.ERROR, "Test error"))
                span.set_attribute("operation.status", "error")

            print("Recorded spans with Ok and Error status")

            provider.force_flush(timeout_millis=5000)
            time.sleep(BATCH_TIMEOUT)

            logs = get_collector_logs(since_seconds=LOG_FETCH_WINDOW_SECONDS)

            # Verify spans appear
            assert ("successful-operation" in logs) or (
                "failed-operation" in logs
            ), "Status test spans not visible in output"

            # Verify status field present (exact format varies)
            assert ("Status" in logs) or (
                "status" in logs.lower()
            ), "Span status field not visible in output"

            print("✓ Span status (Ok/Error) visible in output")

        finally:
            provider.shutdown()


# ==============================================================================
# Class 4: TestEnrichedAttributesDebugOutput (3 tests)
# Tests enriched attributes from attributes processor appear in all signal types
# ==============================================================================


@pytest.mark.integration
@pytest.mark.slow
class TestEnrichedAttributesDebugOutput:
    """Validate attributes processor enrichment appears in output."""

    def test_metrics_enriched_attributes_both_shown(self, collector_running):
        """Metrics show gcp.region + environment from processor.

        Verifies enrichment for metrics signal type.
        """
        print("\n--- Test: Metrics Enriched Attributes Both Shown ---")

        resource = Resource.create({"service.name": "debug-test-enriched-metrics"})

        exporter = OTLPMetricExporter(endpoint=COLLECTOR_URL)
        reader = PeriodicExportingMetricReader(exporter, export_interval_millis=5000)
        provider = MeterProvider(resource=resource, metric_readers=[reader])

        try:
            meter = provider.get_meter("debug.test.meter")
            counter = meter.create_counter("debug_enriched_metrics_test")
            counter.add(1)

            provider.force_flush(timeout_millis=5000)
            time.sleep(BATCH_TIMEOUT)

            logs = get_collector_logs(since_seconds=LOG_FETCH_WINDOW_SECONDS)

            # Verify both enriched attributes
            has_region = ("gcp.region" in logs) or ("us-central1" in logs)
            has_env = ("environment" in logs) or ("prototype" in logs)

            assert (
                has_region and has_env
            ), f"Enriched attributes missing: gcp.region={has_region}, environment={has_env}"

            print("✓ Metrics show both enriched attributes (gcp.region, environment)")

        finally:
            provider.shutdown()

    def test_logs_enriched_attributes_both_shown(self, collector_running):
        """Logs show enriched resource attributes.

        Verifies enrichment for logs signal type.
        """
        print("\n--- Test: Logs Enriched Attributes Both Shown ---")

        resource = Resource.create({"service.name": "debug-test-enriched-logs"})

        exporter = OTLPLogExporter(endpoint="http://localhost:4318/v1/logs")
        provider = LoggerProvider(resource=resource)
        provider.add_log_record_processor(BatchLogRecordProcessor(exporter))

        try:
            logger = logging.getLogger(f"debug.test.enriched.logs.{id(provider)}")
            logger.handlers = []
            handler = LoggingHandler(logger_provider=provider)
            logger.addHandler(handler)
            logger.setLevel(logging.INFO)
            logger.propagate = False

            logger.info("Enrichment test log")

            provider.force_flush(timeout_millis=5000)
            time.sleep(BATCH_TIMEOUT)

            logs = get_collector_logs(since_seconds=LOG_FETCH_WINDOW_SECONDS)

            has_region = ("gcp.region" in logs) or ("us-central1" in logs)
            has_env = ("environment" in logs) or ("prototype" in logs)

            assert (
                has_region and has_env
            ), f"Enriched attributes missing: gcp.region={has_region}, environment={has_env}"

            print("✓ Logs show both enriched attributes (gcp.region, environment)")

        finally:
            logger.handlers = []
            provider.shutdown()

    def test_traces_enriched_resource_attributes_shown(self, collector_running):
        """Traces show enriched resource attributes.

        Verifies enrichment for traces signal type.
        """
        print("\n--- Test: Traces Enriched Resource Attributes Shown ---")

        resource = Resource.create({"service.name": "debug-test-enriched-traces"})

        exporter = OTLPSpanExporter(endpoint="http://localhost:4318/v1/traces")
        provider = TracerProvider(resource=resource)
        provider.add_span_processor(BatchSpanProcessor(exporter))

        try:
            tracer = provider.get_tracer("debug.test.tracer")

            with tracer.start_as_current_span("enrichment-test-span"):
                pass

            provider.force_flush(timeout_millis=5000)
            time.sleep(BATCH_TIMEOUT)

            logs = get_collector_logs(since_seconds=LOG_FETCH_WINDOW_SECONDS)

            has_region = ("gcp.region" in logs) or ("us-central1" in logs)
            has_env = ("environment" in logs) or ("prototype" in logs)

            assert (
                has_region and has_env
            ), f"Enriched attributes missing: gcp.region={has_region}, environment={has_env}"

            print("✓ Traces show both enriched attributes (gcp.region, environment)")

        finally:
            provider.shutdown()


# ==============================================================================
# Class 5: TestDockerComposeIntegration (2 tests)
# Tests Docker Compose log access and grep filtering work as documented
# ==============================================================================


@pytest.mark.integration
@pytest.mark.slow
class TestDockerComposeIntegration:
    """Validate Docker Compose log access and grep filtering."""

    def test_docker_compose_logs_show_metric_output(self, collector_running):
        """docker logs mca-otel-collector shows metrics.

        Verifies Docker Compose integration works for viewing telemetry.
        """
        print("\n--- Test: Docker Compose Logs Show Metric Output ---")

        # Send a metric
        resource = Resource.create({"service.name": "debug-test-docker-integration"})

        exporter = OTLPMetricExporter(endpoint=COLLECTOR_URL)
        reader = PeriodicExportingMetricReader(exporter, export_interval_millis=5000)
        provider = MeterProvider(resource=resource, metric_readers=[reader])

        try:
            meter = provider.get_meter("debug.test.meter")
            counter = meter.create_counter("debug_docker_integration_test")
            counter.add(1)

            provider.force_flush(timeout_millis=5000)
            time.sleep(BATCH_TIMEOUT)

            # Fetch logs using docker logs command (same as docker-compose logs)
            logs = get_collector_logs(since_seconds=LOG_FETCH_WINDOW_SECONDS)

            assert (
                "ResourceMetrics" in logs or "Metric" in logs
            ), "Metric output not visible in docker logs command"

            print("✓ Docker logs command shows metric output (docker-compose logs compatible)")

        finally:
            provider.shutdown()

    def test_grep_filtering_works(self, collector_running):
        """Grep filters collector logs correctly.

        Verifies documented grep patterns work for filtering output.
        Uses Python string filtering for cross-platform compatibility.
        """
        print("\n--- Test: Grep Filtering Works ---")

        # Send a metric with distinctive name
        resource = Resource.create({"service.name": "debug-test-grep-filter"})

        exporter = OTLPMetricExporter(endpoint=COLLECTOR_URL)
        reader = PeriodicExportingMetricReader(exporter, export_interval_millis=5000)
        provider = MeterProvider(resource=resource, metric_readers=[reader])

        try:
            meter = provider.get_meter("debug.test.meter")
            counter = meter.create_counter("debug_grepable_metric_test")
            counter.add(1)

            provider.force_flush(timeout_millis=5000)
            time.sleep(BATCH_TIMEOUT)

            # Test grep filtering using Python (cross-platform)
            # Simulates: docker logs | grep metric_name
            all_logs = get_collector_logs(since_seconds=LOG_FETCH_WINDOW_SECONDS)

            # Filter logs for lines containing the metric name
            filtered_lines = [
                line for line in all_logs.split("\n") if "debug_grepable_metric_test" in line
            ]
            filtered_output = "\n".join(filtered_lines)

            # Verify filtering found the metric
            assert (
                "debug_grepable_metric_test" in filtered_output
            ), "Grep filtering did not find metric name"

            # Verify filtering excluded unrelated lines (at least some filtering happened)
            assert len(filtered_lines) < len(
                all_logs.split("\n")
            ), "Filtering should reduce line count"

            print(
                f"✓ Grep filtering works for isolating specific metrics ({len(filtered_lines)} matching lines)"
            )

        finally:
            provider.shutdown()


# ==============================================================================
# Class 6: TestDebugExporterErrorHandling (6 tests)
# Negative test cases for error scenarios and edge cases
# ==============================================================================


@pytest.mark.integration
class TestDebugExporterErrorHandling:
    """Validate debug exporter handles error scenarios gracefully."""

    def test_collector_not_running_sdk_handles_gracefully(self):
        """Verify SDK handles collector unavailability without crashing.

        Tests resilience when collector is stopped or unreachable.
        """
        print("\n--- Test: Collector Not Running - SDK Handles Gracefully ---")

        # Skip if collector is running (negative test requires it to be down)
        if check_collector_health():
            pytest.skip("Collector is running - this test requires collector to be stopped")

        resource = Resource.create({"service.name": "test-collector-down"})
        exporter = OTLPMetricExporter(endpoint=COLLECTOR_URL)
        reader = PeriodicExportingMetricReader(exporter, export_interval_millis=5000)
        provider = MeterProvider(resource=resource, metric_readers=[reader])

        try:
            meter = provider.get_meter("test.meter")
            counter = meter.create_counter("test_counter_collector_down")

            # This should not crash even though collector is down
            counter.add(1)
            print("✓ SDK recorded metric without crashing despite collector unavailability")

            # Attempt flush - should timeout gracefully
            provider.force_flush(timeout_millis=2000)
            print("✓ SDK flush handled collector unavailability gracefully")

        except Exception as e:
            pytest.fail(f"SDK should handle collector unavailability gracefully, but raised: {e}")
        finally:
            provider.shutdown()

    def test_empty_metric_value_visible_in_output(self, collector_running):
        """Verify counter with value=0 appears in debug output.

        Edge case: Ensure zero values are not filtered or hidden.
        """
        print("\n--- Test: Empty Metric Value Visible ---")

        resource = Resource.create({"service.name": "test-zero-value"})
        exporter = OTLPMetricExporter(endpoint=COLLECTOR_URL)
        reader = PeriodicExportingMetricReader(exporter, export_interval_millis=5000)
        provider = MeterProvider(resource=resource, metric_readers=[reader])

        try:
            meter = provider.get_meter("test.meter")
            counter = meter.create_counter("test_zero_value_counter")

            # Record zero (edge case)
            counter.add(0)

            provider.force_flush(timeout_millis=5000)
            time.sleep(BATCH_TIMEOUT)

            logs = get_collector_logs(since_seconds=LOG_FETCH_WINDOW_SECONDS)

            # Zero values should still appear in output
            assert (
                "test_zero_value_counter" in logs or "test-zero-value" in logs
            ), "Metric with zero value should appear in debug output"

            print("✓ Zero-value metric visible in debug output")

        finally:
            provider.shutdown()

    def test_malformed_service_name_handled(self, collector_running):
        """Verify service names with special characters are handled.

        Edge case: Special characters, unicode, very long names.
        """
        print("\n--- Test: Malformed Service Name Handled ---")

        # Test with special characters and unicode
        resource = Resource.create({"service.name": "test-svc-with-special-chars-@#$%-ü"})

        exporter = OTLPMetricExporter(endpoint=COLLECTOR_URL)
        reader = PeriodicExportingMetricReader(exporter, export_interval_millis=5000)
        provider = MeterProvider(resource=resource, metric_readers=[reader])

        try:
            meter = provider.get_meter("test.meter")
            counter = meter.create_counter("test_special_chars_counter")
            counter.add(1)

            provider.force_flush(timeout_millis=5000)
            time.sleep(BATCH_TIMEOUT)

            logs = get_collector_logs(since_seconds=LOG_FETCH_WINDOW_SECONDS)

            # Service name should appear (may be sanitized by collector)
            assert (
                "test-svc-with-special-chars" in logs or "test_special_chars_counter" in logs
            ), "Service with special characters should be handled"

            print("✓ Special characters in service name handled correctly")

        finally:
            provider.shutdown()

    def test_very_long_metric_name_handled(self, collector_running):
        """Verify very long metric names are handled gracefully.

        Edge case: Metric name >256 characters (OpenTelemetry spec limit).
        """
        print("\n--- Test: Very Long Metric Name Handled ---")

        # Create metric name near limit (256 chars)
        long_name = "test_" + "a" * 250

        resource = Resource.create({"service.name": "test-long-name"})
        exporter = OTLPMetricExporter(endpoint=COLLECTOR_URL)
        reader = PeriodicExportingMetricReader(exporter, export_interval_millis=5000)
        provider = MeterProvider(resource=resource, metric_readers=[reader])

        try:
            meter = provider.get_meter("test.meter")
            counter = meter.create_counter(long_name)
            counter.add(1)

            provider.force_flush(timeout_millis=5000)
            time.sleep(BATCH_TIMEOUT)

            logs = get_collector_logs(since_seconds=LOG_FETCH_WINDOW_SECONDS)

            # Long metric name should appear (may be truncated by collector)
            assert (
                "test_aaa" in logs or "test-long-name" in logs
            ), "Very long metric name should be handled (may be truncated)"

            print(f"✓ Very long metric name ({len(long_name)} chars) handled correctly")

        finally:
            provider.shutdown()

    def test_unicode_in_log_messages_visible(self, collector_running):
        """Verify Unicode characters in log messages appear correctly.

        Edge case: International characters, emojis, special symbols.
        Demonstrates use of otel_logger_context() helper for cleaner code.
        """
        print("\n--- Test: Unicode in Log Messages Visible ---")

        resource = Resource.create({"service.name": "test-unicode-logs"})

        # Using context manager for automatic cleanup (Fix #10)
        with otel_logger_context(resource) as logger:
            # Log with unicode characters
            unicode_message = "Test message with unicode: é, ñ, ü, 中文, 🚀"
            logger.info(unicode_message)

            print(f"Emitted log with unicode: {unicode_message}")

            # Note: provider.force_flush() not available via context manager
            # Sleep to allow batch processing
            time.sleep(BATCH_TIMEOUT)

            logs = get_collector_logs(since_seconds=LOG_FETCH_WINDOW_SECONDS)

            # Unicode should appear (may be escaped or preserved)
            assert (
                "test-unicode-logs" in logs
            ), "Log with unicode should appear in output (unicode may be escaped)"

            print("✓ Unicode log message handled correctly")
        # Automatic cleanup happens here

    def test_large_batch_at_max_size_handled(self, collector_running):
        """Verify large batch hitting send_batch_max_size (1000) is handled.

        Edge case: Test batch size limit handling.
        """
        print("\n--- Test: Large Batch at Max Size Handled ---")

        resource = Resource.create({"service.name": "test-large-batch"})
        exporter = OTLPMetricExporter(endpoint=COLLECTOR_URL)
        reader = PeriodicExportingMetricReader(exporter, export_interval_millis=5000)
        provider = MeterProvider(resource=resource, metric_readers=[reader])

        try:
            meter = provider.get_meter("test.meter")

            # Create 100 metrics (approaching batch limits, but not hitting 1000)
            for i in range(100):
                counter = meter.create_counter(f"test_large_batch_metric_{i}")
                counter.add(i)

            print("Recorded 100 metrics in large batch")

            provider.force_flush(timeout_millis=5000)
            time.sleep(BATCH_TIMEOUT)

            logs = get_collector_logs(since_seconds=LOG_FETCH_WINDOW_SECONDS)

            # Verify batch was processed (check for service name)
            assert "test-large-batch" in logs, "Large batch should be processed by collector"

            # Sample check a few metrics
            found = sum(1 for i in [0, 50, 99] if f"test_large_batch_metric_{i}" in logs)
            assert found >= 1, "At least some metrics from large batch should be visible"

            print("✓ Large batch (100 metrics) processed correctly")

        finally:
            provider.shutdown()


if __name__ == "__main__":
    """Run tests with pytest when executed directly."""
    import sys

    sys.exit(pytest.main([__file__, "-v", "-s"]))
