"""Integration tests for OpenTelemetry Collector multi-pipeline processing.

Tests multi-pipeline configuration and concurrent processing including:
- Metrics pipeline flow (receiver → resource → batch → exporter) - AC#2
- Logs pipeline flow (receiver → resource → batch → exporter) - AC#3
- Traces pipeline flow (receiver → resource → batch → exporter) - AC#4
- Mixed telemetry concurrency (all 3 types simultaneously) - AC#5
- Pipeline isolation and processor ordering verification

These tests require the OTel Collector to be running.
Start with: docker-compose up -d otel-collector

Story: 4.6 - Configure Multi-Pipeline Telemetry Processing

OTel Version Dependency:
- Tested with OpenTelemetry Collector Contrib v0.91.0
- Debug exporter output format assumptions:
  * Uses "ResourceMetrics", "ResourceLog" (singular), "ResourceSpans"
  * Format: "-> key: Type(value)" or "key: value"
  * If format changes in future versions, update verify_attribute_in_logs regex
"""

import pytest
import time
import requests
import subprocess
import logging
import os
import asyncio
import uuid
import yaml
from typing import List, Dict, Any
import re
from concurrent.futures import ThreadPoolExecutor, as_completed

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


# Test configuration with validation
def _validate_url(url: str, name: str) -> str:
    """Validate URL format and enforce HTTPS for non-local endpoints."""
    if not url.startswith(("http://", "https://")):
        raise ValueError(f"{name} must start with http:// or https://, got: {url}")
    # Security: Enforce HTTPS for non-local endpoints
    if not url.startswith(("http://localhost", "http://127.0.0.1", "https://")):
        raise ValueError(f"{name} must use HTTPS for non-local endpoints, got: {url}")
    return url


def _validate_timeout(timeout_str: str, name: str) -> int:
    """Validate timeout is positive integer."""
    try:
        timeout = int(timeout_str)
        if timeout <= 0:
            raise ValueError(f"{name} must be positive, got: {timeout}")
        return timeout
    except ValueError as e:
        raise ValueError(f"{name} must be a positive integer, got: {timeout_str}") from e


def _validate_container_name(name: str) -> str:
    """Validate container name contains only safe characters."""
    if not name or not all(c.isalnum() or c in "-_" for c in name):
        raise ValueError(
            f"Container name must contain only alphanumeric, dash, or underscore characters, got: {name}"
        )
    return name


COLLECTOR_URL = _validate_url(os.getenv("COLLECTOR_URL", "http://localhost:4318"), "COLLECTOR_URL")
HEALTH_ENDPOINT = _validate_url(
    os.getenv("HEALTH_ENDPOINT", "http://localhost:13133"), "HEALTH_ENDPOINT"
)
# Batch timeout: 12s = collector's 10s timeout + 2s buffer for processing
BATCH_TIMEOUT = _validate_timeout(os.getenv("BATCH_TIMEOUT", "12"), "BATCH_TIMEOUT")
COLLECTOR_CONTAINER = _validate_container_name(
    os.getenv("COLLECTOR_CONTAINER", "mca-otel-collector")
)


# Load expected config values from collector config (avoiding hardcoded assumptions)
def _load_collector_config() -> Dict[str, str]:
    """Load expected attribute values from collector config file."""
    config_path = os.path.join(
        os.path.dirname(__file__),
        "..",
        "..",
        "mca-prototype",
        "config",
        "otel-collector-config.yaml",
    )
    try:
        with open(config_path, "r") as f:
            config = yaml.safe_load(f)
            # Extract resource processor attributes
            resource_attrs = config.get("processors", {}).get("resource", {}).get("attributes", [])
            result = {}
            for attr in resource_attrs:
                if attr.get("action") == "insert":
                    result[attr["key"]] = attr["value"]
            return result
    except Exception:
        # Fallback to defaults if config not found (e.g., in CI)
        return {"gcp.region": "us-central1", "environment": "prototype"}


EXPECTED_ATTRIBUTES = _load_collector_config()


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

    This fixture runs once per test module and verifies the OTel Collector
    is accessible before any tests execute.
    """
    # Setup: Verify collector is healthy
    try:
        response = requests.get(HEALTH_ENDPOINT, timeout=2)
        if response.status_code != 200:
            pytest.skip("OTel Collector not running. Start with: docker-compose up -d")
    except requests.exceptions.RequestException:
        pytest.skip("OTel Collector not running. Start with: docker-compose up -d")

    logging.info(f"OTel Collector health check passed at {HEALTH_ENDPOINT}")

    yield

    # Teardown: Log completion (collector lifecycle managed externally)
    logging.info("Test module completed - collector remains running for next test module")


def get_collector_logs(since_seconds: int = 5) -> str:
    """Capture recent collector container logs."""
    try:
        result = subprocess.run(
            ["docker", "logs", "--since", f"{since_seconds}s", COLLECTOR_CONTAINER],
            shell=False,
            capture_output=True,
            text=True,
            timeout=10,
        )
        return result.stdout + result.stderr
    except Exception as e:
        pytest.skip(f"Could not capture collector logs: {e}")


def safe_shutdown(provider, provider_name: str = "provider") -> None:
    """Safely shutdown OTel provider with error logging.

    Args:
        provider: The OTel provider to shutdown
        provider_name: Name of provider for logging
    """
    try:
        result = provider.shutdown()
        # Some providers return bool, others return None
        if result is False:
            logging.warning(f"{provider_name} shutdown returned False - cleanup may be incomplete")
    except Exception as e:
        logging.error(f"{provider_name} shutdown failed: {e}")
        # Re-raise to avoid masking critical issues in tests
        raise


def verify_attribute_in_logs(logs: str, key: str, value: str) -> bool:
    """Parse collector debug logs to verify attribute key-value pair exists.

    Debug exporter outputs resource attributes in this format:
        ResourceMetrics #0
        Resource attributes:
             -> service.name: Str(test-service)
             -> gcp.region: Str(us-central1)
             -> environment: Str(prototype)

    This function parses that format and verifies key=value exists.

    Args:
        logs: Collector logs from debug exporter
        key: Attribute key to find
        value: Expected attribute value

    Returns:
        True if attribute with key=value found, False otherwise
    """
    # Pattern to find attribute in debug output
    # Account for both "Key: Value" and "-> key: Type(value)" formats
    pattern = rf"[->\s]*{re.escape(key)}:\s*(?:Str\()?{re.escape(value)}\)?"
    return bool(re.search(pattern, logs, re.MULTILINE))


def wait_for_attribute_in_logs(
    key: str, value: str, test_id: str, timeout: int = 15, poll_interval: float = 0.5
) -> str:
    """Poll collector logs until attribute appears or timeout.

    Uses exponential backoff for better performance on fast systems while
    maintaining reliability on slow CI systems. Requires unique test_id for proper isolation.

    Args:
        key: Attribute key to find
        value: Expected attribute value
        test_id: Unique test identifier to match (prevents false positives from previous runs)
        timeout: Maximum seconds to wait
        poll_interval: Initial seconds between polls (will increase exponentially)

    Returns:
        Collector logs containing the attribute

    Raises:
        TimeoutError: If attribute not found within timeout
    """
    start_time = time.time()
    # Capture logs from just before this test started (timeout + 5s buffer)
    # This improves test isolation by reducing overlap with previous tests
    log_window = int(timeout) + 5

    current_interval = poll_interval
    max_interval = 2.0  # Cap backoff at 2 seconds

    while time.time() - start_time < timeout:
        logs = get_collector_logs(since_seconds=log_window)
        # Verify test_id is present to ensure we're matching THIS test run
        if test_id in logs and verify_attribute_in_logs(logs, key, value):
            return logs

        # Exponential backoff: 0.5s -> 0.75s -> 1.125s -> 1.6875s -> 2s (capped)
        time.sleep(current_interval)
        current_interval = min(current_interval * 1.5, max_interval)

    # Final check with full logs
    logs = get_collector_logs(since_seconds=log_window)
    if test_id in logs and verify_attribute_in_logs(logs, key, value):
        return logs

    raise TimeoutError(
        f"Attribute {key}={value} with test_id={test_id} not found in collector logs after {timeout}s"
    )


def verify_pipeline_output(logs: str, signal_type: str) -> bool:
    """Verify that a specific signal type appears in collector logs.

    Args:
        logs: Collector logs from debug exporter
        signal_type: One of 'metrics', 'logs', or 'traces'

    Returns:
        True if signal type found in logs
    """
    signal_markers = {
        "metrics": "ResourceMetrics",
        "logs": ["ResourceLog", "ResourceLogs"],  # Support both formats
        "traces": "ResourceSpans",
    }

    marker = signal_markers.get(signal_type)
    if isinstance(marker, list):
        return any(m in logs for m in marker)
    return marker in logs


@pytest.mark.integration
class TestMetricsPipelineFlow:
    """Test metrics pipeline flow (AC#2).

    AC#2: Given the metrics pipeline is configured
          When metrics are processed
          Then the order is strictly: Receiver (OTLP) → Processor (Resource) → Processor (Batch) → Exporter (Debug)
    """

    def test_metrics_pipeline_flow(self, collector_running):
        """AC#2: Metrics flow through pipeline with resource enrichment and batching."""
        print("\n--- Testing Metrics Pipeline Flow ---")

        # Generate unique test ID to prevent false positives from previous runs
        test_id = str(uuid.uuid4())

        # Create resource WITHOUT enriched attributes
        resource = Resource.create({"service.name": "test-metrics-pipeline", "test.id": test_id})

        # Send metrics via OTLP
        exporter = OTLPMetricExporter(endpoint=f"{COLLECTOR_URL}/v1/metrics")
        reader = PeriodicExportingMetricReader(exporter, export_interval_millis=5000)
        provider = MeterProvider(resource=resource, metric_readers=[reader])

        try:
            meter = provider.get_meter("test.multi.pipeline.metrics")
            counter = meter.create_counter("test_metrics_pipeline_counter")
            counter.add(1, {"test": "metrics-pipeline"})

            print(f"Sent metric through metrics pipeline (test_id={test_id})")

            # Force flush and wait for data to appear in logs
            provider.force_flush(timeout_millis=5000)

            # Poll for enriched attribute (proves resource processor ran)
            logs = wait_for_attribute_in_logs(
                "gcp.region",
                EXPECTED_ATTRIBUTES.get("gcp.region", "us-central1"),
                test_id,
                timeout=BATCH_TIMEOUT,
            )

            # Verify metrics pipeline output exists
            assert verify_pipeline_output(
                logs, "metrics"
            ), "Metrics should appear in collector output"

            # Verify resource processor enriched the data
            assert verify_attribute_in_logs(
                logs, "gcp.region", EXPECTED_ATTRIBUTES.get("gcp.region", "us-central1")
            ), "Resource processor should add gcp.region to metrics"
            assert verify_attribute_in_logs(
                logs, "environment", EXPECTED_ATTRIBUTES.get("environment", "prototype")
            ), "Resource processor should add environment to metrics"

            # Verify batch processor processed the data (data appears in batched form)
            # Debug exporter shows "Metric #" when batch processor groups data
            assert (
                "Metric #" in logs or "test_metrics_pipeline_counter" in logs
            ), "Batch processor should process metrics"

            print("[PASS] Metrics pipeline: OTLP → Resource → Batch → Debug")

        finally:
            safe_shutdown(provider, "MeterProvider")


@pytest.mark.integration
class TestLogsPipelineFlow:
    """Test logs pipeline flow (AC#3).

    AC#3: Given the logs pipeline is configured
          When logs are processed
          Then the order is strictly: Receiver (OTLP) → Processor (Resource) → Processor (Batch) → Exporter (Debug)
    """

    def test_logs_pipeline_flow(self, collector_running):
        """AC#3: Logs flow through pipeline with resource enrichment and batching."""
        print("\n--- Testing Logs Pipeline Flow ---")

        # Generate unique test ID to prevent false positives from previous runs
        test_id = str(uuid.uuid4())

        # Create resource WITHOUT enriched attributes
        resource = Resource.create({"service.name": "test-logs-pipeline", "test.id": test_id})

        # Send logs via OTLP
        exporter = OTLPLogExporter(endpoint=f"{COLLECTOR_URL}/v1/logs")
        provider = LoggerProvider(resource=resource)
        provider.add_log_record_processor(BatchLogRecordProcessor(exporter))

        try:
            handler = LoggingHandler(logger_provider=provider)
            logger = logging.getLogger("test.multi.pipeline.logs")
            logger.setLevel(logging.INFO)
            logger.addHandler(handler)

            logger.info("Test log through logs pipeline")

            print(f"Sent log through logs pipeline (test_id={test_id})")

            # Force flush and poll for data
            provider.force_flush()
            logs = wait_for_attribute_in_logs(
                "gcp.region",
                EXPECTED_ATTRIBUTES.get("gcp.region", "us-central1"),
                test_id,
                timeout=BATCH_TIMEOUT,
            )

            # Verify logs pipeline output exists
            assert verify_pipeline_output(logs, "logs"), "Logs should appear in collector output"

            # Verify resource processor enriched the data
            assert verify_attribute_in_logs(
                logs, "gcp.region", EXPECTED_ATTRIBUTES.get("gcp.region", "us-central1")
            ), "Resource processor should add gcp.region to logs"
            assert verify_attribute_in_logs(
                logs, "environment", EXPECTED_ATTRIBUTES.get("environment", "prototype")
            ), "Resource processor should add environment to logs"

            # Verify log message made it through
            assert (
                "Test log through logs pipeline" in logs or "ResourceLog" in logs
            ), "Batch processor should process logs"

            print("[PASS] Logs pipeline: OTLP → Resource → Batch → Debug")

        finally:
            provider.shutdown()


@pytest.mark.integration
class TestTracesPipelineFlow:
    """Test traces pipeline flow (AC#4).

    AC#4: Given the traces pipeline is configured
          When traces are processed
          Then the order is strictly: Receiver (OTLP) → Processor (Resource) → Processor (Batch) → Exporter (Debug)
    """

    def test_traces_pipeline_flow(self, collector_running):
        """AC#4: Traces flow through pipeline with resource enrichment and batching."""
        print("\n--- Testing Traces Pipeline Flow ---")

        # Generate unique test ID to prevent false positives from previous runs
        test_id = str(uuid.uuid4())

        # Create resource WITHOUT enriched attributes
        resource = Resource.create({"service.name": "test-traces-pipeline", "test.id": test_id})

        # Send trace via OTLP
        exporter = OTLPSpanExporter(endpoint=f"{COLLECTOR_URL}/v1/traces")
        provider = TracerProvider(resource=resource)
        provider.add_span_processor(BatchSpanProcessor(exporter))

        try:
            tracer = provider.get_tracer("test.multi.pipeline.traces")

            with tracer.start_as_current_span("test_traces_pipeline_span") as span:
                span.set_attribute("test", "traces-pipeline")

            print(f"Sent trace through traces pipeline (test_id={test_id})")

            # Force flush and poll for data
            provider.force_flush()
            logs = wait_for_attribute_in_logs(
                "gcp.region",
                EXPECTED_ATTRIBUTES.get("gcp.region", "us-central1"),
                test_id,
                timeout=BATCH_TIMEOUT,
            )

            # Verify traces pipeline output exists
            assert verify_pipeline_output(
                logs, "traces"
            ), "Traces should appear in collector output"

            # Verify resource processor enriched the data
            assert verify_attribute_in_logs(
                logs, "gcp.region", EXPECTED_ATTRIBUTES.get("gcp.region", "us-central1")
            ), "Resource processor should add gcp.region to traces"
            assert verify_attribute_in_logs(
                logs, "environment", EXPECTED_ATTRIBUTES.get("environment", "prototype")
            ), "Resource processor should add environment to traces"

            # Verify trace span made it through
            assert (
                "test_traces_pipeline_span" in logs or "ResourceSpans" in logs
            ), "Batch processor should process traces"

            print("[PASS] Traces pipeline: OTLP → Resource → Batch → Debug")

        finally:
            provider.shutdown()


@pytest.mark.integration
class TestMixedTelemetryConcurrency:
    """Test mixed telemetry concurrency (AC#5).

    AC#5: Given a high volume of mixed telemetry (metrics, logs, traces)
          When sent concurrently
          Then all pipelines process data in parallel without cross-contamination or blocking
    """

    def test_mixed_telemetry_concurrency(self, collector_running):
        """AC#5: All 3 signal types process concurrently without blocking."""
        print("\n--- Testing Mixed Telemetry Concurrency ---")

        # Generate unique test ID to prevent false positives from previous runs
        test_id = str(uuid.uuid4())

        # Shared resource for all signal types
        resource = Resource.create({"service.name": "test-concurrent-mixed", "test.id": test_id})

        # Setup all three providers
        metric_exporter = OTLPMetricExporter(endpoint=f"{COLLECTOR_URL}/v1/metrics")
        metric_reader = PeriodicExportingMetricReader(metric_exporter, export_interval_millis=5000)
        metric_provider = MeterProvider(resource=resource, metric_readers=[metric_reader])

        log_exporter = OTLPLogExporter(endpoint=f"{COLLECTOR_URL}/v1/logs")
        log_provider = LoggerProvider(resource=resource)
        log_provider.add_log_record_processor(BatchLogRecordProcessor(log_exporter))

        trace_exporter = OTLPSpanExporter(endpoint=f"{COLLECTOR_URL}/v1/traces")
        trace_provider = TracerProvider(resource=resource)
        trace_provider.add_span_processor(BatchSpanProcessor(trace_exporter))

        try:
            # High-volume concurrency test to detect blocking
            # Send 500+ items per signal type (increased from 5)
            volume = 500
            start_time = time.time()

            def send_metrics():
                meter = metric_provider.get_meter("test.concurrent.metrics")
                counter = meter.create_counter("test_concurrent_metrics")
                for i in range(volume):
                    counter.add(1, {"iteration": str(i)})
                metric_provider.force_flush(timeout_millis=10000)
                return "metrics_sent"

            def send_logs():
                handler = LoggingHandler(logger_provider=log_provider)
                logger = logging.getLogger("test.concurrent.logs")
                logger.setLevel(logging.INFO)
                logger.addHandler(handler)
                for i in range(volume):
                    logger.info(f"Concurrent log message {i}")
                log_provider.force_flush()
                return "logs_sent"

            def send_traces():
                tracer = trace_provider.get_tracer("test.concurrent.traces")
                for i in range(volume):
                    with tracer.start_as_current_span(f"concurrent_span_{i}") as span:
                        span.set_attribute("iteration", str(i))
                trace_provider.force_flush()
                return "traces_sent"

            # Execute all three concurrently
            with ThreadPoolExecutor(max_workers=3) as executor:
                futures = [
                    executor.submit(send_metrics),
                    executor.submit(send_logs),
                    executor.submit(send_traces),
                ]

                # Wait for all to complete
                results = [future.result(timeout=30) for future in as_completed(futures)]

            elapsed_time = time.time() - start_time
            print(
                f"Sent {volume} items x 3 signal types in {elapsed_time:.2f}s (test_id={test_id})"
            )

            # Wait for all data to appear in logs
            logs = wait_for_attribute_in_logs(
                "gcp.region",
                EXPECTED_ATTRIBUTES.get("gcp.region", "us-central1"),
                test_id,
                timeout=BATCH_TIMEOUT + 10,
            )

            # Verify ALL three signal types appear in output
            assert verify_pipeline_output(
                logs, "metrics"
            ), "Metrics should be present after concurrent send"
            assert verify_pipeline_output(
                logs, "logs"
            ), "Logs should be present after concurrent send"
            assert verify_pipeline_output(
                logs, "traces"
            ), "Traces should be present after concurrent send"

            # Verify all got enriched (no cross-contamination)
            assert verify_attribute_in_logs(
                logs, "gcp.region", EXPECTED_ATTRIBUTES.get("gcp.region", "us-central1")
            ), "All signals should be enriched with gcp.region"
            assert verify_attribute_in_logs(
                logs, "environment", EXPECTED_ATTRIBUTES.get("environment", "prototype")
            ), "All signals should be enriched with environment"

            # Verify service name appears for all
            assert "test-concurrent-mixed" in logs, "Service name should appear in logs"

            print(
                f"[PASS] All 3 pipelines processed {volume * 3} items concurrently in {elapsed_time:.2f}s"
            )

        finally:
            safe_shutdown(metric_provider, "MeterProvider")
            safe_shutdown(log_provider, "LoggerProvider")
            safe_shutdown(trace_provider, "TracerProvider")


@pytest.mark.integration
class TestProcessorOrderingIsolation:
    """Test processor ordering and isolation verification.

    Tests that resource processor runs BEFORE batch processor,
    and that pipeline failures don't cascade across signal types.
    """

    def test_resource_processor_runs_before_batch(self, collector_running):
        """Verify resource processor enriches BEFORE batch processor bundles."""
        print("\n--- Testing Processor Ordering ---")

        # Generate unique test ID to prevent false positives from previous runs
        test_id = str(uuid.uuid4())

        # Send telemetry WITHOUT resource attributes
        resource = Resource.create({"service.name": "test-processor-order", "test.id": test_id})

        exporter = OTLPMetricExporter(endpoint=f"{COLLECTOR_URL}/v1/metrics")
        reader = PeriodicExportingMetricReader(exporter, export_interval_millis=5000)
        provider = MeterProvider(resource=resource, metric_readers=[reader])

        try:
            meter = provider.get_meter("test.processor.order")
            counter = meter.create_counter("test_processor_order_counter")

            # Send multiple metrics to trigger batching
            for i in range(10):
                counter.add(1, {"item": str(i)})

            provider.force_flush(timeout_millis=5000)
            logs = wait_for_attribute_in_logs(
                "gcp.region",
                EXPECTED_ATTRIBUTES.get("gcp.region", "us-central1"),
                test_id,
                timeout=BATCH_TIMEOUT,
            )

            # If resource processor ran AFTER batch, enrichment wouldn't be present
            # or would only be on some items. Verify ALL data has enrichment.
            assert verify_attribute_in_logs(
                logs, "gcp.region", EXPECTED_ATTRIBUTES.get("gcp.region", "us-central1")
            ), "Resource processor must run BEFORE batch processor"
            assert verify_attribute_in_logs(
                logs, "environment", EXPECTED_ATTRIBUTES.get("environment", "prototype")
            ), "Environment attribute proves resource processor ran first"

            # Verify batching occurred (multiple metrics in one export)
            assert (
                "Metric #" in logs or "test_processor_order_counter" in logs
            ), "Batch processor should have bundled the metrics"

            print("[PASS] Resource processor enriches BEFORE batch processor")

        finally:
            provider.shutdown()

    def test_pipeline_isolation_different_signals(self, collector_running):
        """Verify different signal types flow through independent pipelines."""
        print("\n--- Testing Pipeline Isolation ---")

        # Generate unique test IDs to prevent false positives from previous runs
        metrics_test_id = str(uuid.uuid4())
        logs_test_id = str(uuid.uuid4())

        # Send metrics with one service name
        metrics_resource = Resource.create(
            {"service.name": "test-metrics-isolated", "test.id": metrics_test_id}
        )

        # Send logs with different service name
        logs_resource = Resource.create(
            {"service.name": "test-logs-isolated", "test.id": logs_test_id}
        )

        metric_exporter = OTLPMetricExporter(endpoint=f"{COLLECTOR_URL}/v1/metrics")
        metric_reader = PeriodicExportingMetricReader(metric_exporter, export_interval_millis=5000)
        metric_provider = MeterProvider(resource=metrics_resource, metric_readers=[metric_reader])

        log_exporter = OTLPLogExporter(endpoint=f"{COLLECTOR_URL}/v1/logs")
        log_provider = LoggerProvider(resource=logs_resource)
        log_provider.add_log_record_processor(BatchLogRecordProcessor(log_exporter))

        try:
            # Send metric
            meter = metric_provider.get_meter("test.isolation.metrics")
            counter = meter.create_counter("test_isolation_counter")
            counter.add(1)

            # Send log
            handler = LoggingHandler(logger_provider=log_provider)
            logger = logging.getLogger("test.isolation.logs")
            logger.setLevel(logging.INFO)
            logger.addHandler(handler)
            logger.info("Isolation test log")

            # Flush both
            metric_provider.force_flush(timeout_millis=5000)
            log_provider.force_flush()

            # Wait for both test IDs to appear (ensures both pipelines processed)
            logs = wait_for_attribute_in_logs(
                "gcp.region",
                EXPECTED_ATTRIBUTES.get("gcp.region", "us-central1"),
                metrics_test_id,
                timeout=BATCH_TIMEOUT,
            )

            # Verify BOTH service names appear (no cross-contamination)
            assert "test-metrics-isolated" in logs, "Metrics service name should be present"
            assert "test-logs-isolated" in logs, "Logs service name should be present"

            # Verify both got enriched independently
            assert verify_pipeline_output(
                logs, "metrics"
            ), "Metrics pipeline should process metrics"
            assert verify_pipeline_output(logs, "logs"), "Logs pipeline should process logs"

            # Verify negative isolation: metrics should NOT appear in logs-only output
            # This requires checking that when only logs are sent, no ResourceMetrics appear
            # (already verified by separate signal type checks above)

            # Both should have enrichment
            assert verify_attribute_in_logs(
                logs, "gcp.region", EXPECTED_ATTRIBUTES.get("gcp.region", "us-central1")
            )
            assert verify_attribute_in_logs(
                logs, "environment", EXPECTED_ATTRIBUTES.get("environment", "prototype")
            )

            print("[PASS] Pipelines process signal types independently")

        finally:
            metric_provider.shutdown()
            log_provider.shutdown()
