"""E2E tests for error recovery and resilience scenarios.

Tests verify SDK gracefully handles collector downtime, GCP unavailability,
and registry failures while maintaining data integrity with proper verification.
"""

import time
import subprocess
import pytest
from pathlib import Path
from mca_sdk import MCAClient


@pytest.mark.integration
@pytest.mark.error_scenario
class TestErrorScenariosE2E:
    """End-to-end tests for error handling and resilience."""

    def test_collector_down_sdk_buffers_and_retries(
        self,
        docker_compose_environment,
        gcp_monitoring_client,
        gcp_project_id,
        wait_for_telemetry,
        cleanup_collector_exports,
    ):
        """Test SDK buffers telemetry when collector is down and retries when up.

        Given: SDK client configured with collector endpoint
        When: Collector is stopped, SDK records metrics, collector restarted
        Then: Buffered metrics are eventually exported and counted
        """
        # Arrange: Start with healthy collector
        client = MCAClient(
            service_name="e2e-buffer-test",
            model_id="e2e-buf-mdl-001",
            team_name="e2e-test-team",
            model_category="internal",
            model_type="regression",
            otel_endpoint=docker_compose_environment["collector_otlp"],
            otel_protocol="http",
            enable_buffering=True,
            max_queue_size=1000,
        )

        start_time = time.time()
        num_before = 1
        num_during = 5
        num_after = 1
        expected_total = num_before + num_during + num_after

        # Record initial metric while collector is healthy
        for i in range(num_before):
            client.record_prediction(latency=0.1, prediction_id=f"buf-pred-before-{i}")

        # Wait for initial metrics to export
        time.sleep(2)

        # Act: Stop collector using docker command
        compose_file = Path(__file__).parent.parent.parent / "docker-compose.yml"

        # Determine docker compose command
        try:
            subprocess.run(["docker", "compose", "version"], check=True, capture_output=True)
            docker_cmd = ["docker", "compose"]
        except (subprocess.CalledProcessError, FileNotFoundError):
            docker_cmd = ["docker-compose"]

        print("🛑 Stopping OTel Collector...")
        subprocess.run(
            docker_cmd + ["-f", str(compose_file), "stop", "otel-collector"],
            cwd=compose_file.parent,
            check=True,
            capture_output=True,
        )

        # Record metrics while collector is down - should buffer
        for i in range(num_during):
            client.record_prediction(latency=0.1 + (i * 0.01), prediction_id=f"buf-pred-during-{i}")

        wait_for_telemetry(2, "buffering period")

        # Restart collector
        print("🔄 Restarting OTel Collector...")
        subprocess.run(
            docker_cmd + ["-f", str(compose_file), "start", "otel-collector"],
            cwd=compose_file.parent,
            check=True,
            capture_output=True,
        )

        # Wait for collector to be healthy
        wait_for_telemetry(10, "collector recovery")

        # Record final metric after recovery
        for i in range(num_after):
            client.record_prediction(latency=0.15, prediction_id=f"buf-pred-after-{i}")

        # Force flush buffered data
        client.shutdown()
        wait_for_telemetry(5, "final buffer flush")

        # Assert: Count total metrics received
        end_time = time.time()
        metrics = gcp_monitoring_client.query_metrics(
            project_id=gcp_project_id,
            metric_type="mca.internal.regression.predictions",
            start_time=start_time,
            end_time=end_time,
        )

        # Verify we got most metrics (allow some loss during downtime if SDK doesn't support buffering yet)
        actual_count = len(metrics)
        min_expected = num_before + num_after  # At minimum, before and after should succeed

        assert actual_count >= min_expected, (
            f"Expected at least {min_expected} metrics (before+after), got {actual_count}. "
            f"Total expected with buffering: {expected_total}"
        )

        # If all metrics arrived, buffering worked perfectly
        if actual_count == expected_total:
            print(f"✅ Perfect buffering: {actual_count}/{expected_total} metrics delivered")
        else:
            print(f"⚠️  Partial buffering: {actual_count}/{expected_total} metrics delivered")

    def test_gcp_unavailable_sdk_gracefully_degrades(
        self, docker_compose_environment, wait_for_telemetry, cleanup_collector_exports
    ):
        """Test SDK continues operating when GCP services are unavailable.

        Given: GCP backend is unreachable (file exporter may fail)
        When: SDK records telemetry
        Then: SDK logs degradation but continues normal operation without crashing
        """
        # Arrange: Client configured to export to GCP (which is mocked/unavailable)
        client = MCAClient(
            service_name="e2e-graceful-degrade",
            model_id="e2e-deg-mdl-001",
            team_name="e2e-test-team",
            model_category="internal",
            model_type="classification",
            otel_endpoint=docker_compose_environment["collector_otlp"],
            otel_protocol="http",
        )

        # Act: Record metrics (collector may fail to export to GCP, but SDK should be fine)
        try:
            for i in range(10):
                client.record_prediction(latency=0.1, prediction_id=f"deg-pred-{i}")

            # SDK should complete without exceptions
            client.shutdown()

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

        # Assert: SDK operated normally despite backend issues
        assert True, "SDK gracefully handled potential GCP unavailability"

    def test_registry_unavailable_sdk_uses_local_config(
        self, docker_compose_environment, wait_for_telemetry, cleanup_collector_exports
    ):
        """Test SDK falls back to local config when registry is unavailable.

        Given: Registry API is unreachable
        When: SDK initializes with local config
        Then: SDK operates normally using local configuration
        """
        # Arrange: Configure client with registry URL that's unavailable
        client = MCAClient(
            service_name="e2e-registry-fallback",
            model_id="e2e-reg-mdl-001",
            team_name="e2e-test-team",
            model_category="internal",
            model_type="regression",
            otel_endpoint=docker_compose_environment["collector_otlp"],
            otel_protocol="http",
            registry_url="http://unreachable-registry:9999",  # Intentionally unreachable
            registry_enabled=False,  # Disable to avoid blocking on unavailable registry
        )

        # Act: Record prediction with local config fallback
        try:
            client.record_prediction(latency=0.12, prediction_id="reg-fallback-pred-001")

            client.shutdown()

        except Exception as e:
            pytest.fail(f"SDK should fallback to local config, but raised: {e}")

        # Assert: SDK used local config successfully
        assert client.config.service_name == "e2e-registry-fallback"
        assert client.config.model_id == "e2e-reg-mdl-001"

    def test_network_timeout_does_not_block_sdk(
        self, docker_compose_environment, wait_for_telemetry, cleanup_collector_exports
    ):
        """Test SDK doesn't block application when network is slow.

        Given: SDK configured with short timeout
        When: Network is slow/unresponsive
        Then: SDK times out gracefully without blocking application
        """
        # Arrange: Use non-routable IP to simulate slow network
        client = MCAClient(
            service_name="e2e-timeout-test",
            model_id="e2e-timeout-mdl-001",
            team_name="e2e-test-team",
            model_category="internal",
            model_type="regression",
            otel_endpoint="http://192.0.2.1:4318",  # TEST-NET-1 (non-routable)
            otel_protocol="http",
            export_timeout_ms=100,  # Very short timeout
            enable_buffering=True,
        )

        # Act: Record metric - should timeout but not block
        start_time = time.time()

        try:
            client.record_prediction(latency=0.1, prediction_id="timeout-pred-001")

            # Shutdown should also not block
            client.shutdown()

        except Exception:
            # Timeouts are acceptable in this scenario
            pass

        elapsed_time = time.time() - start_time

        # Assert: Operation completed quickly despite network issue
        # Should timeout within ~2 seconds (not hang indefinitely)
        assert elapsed_time < 5.0, f"SDK blocked for {elapsed_time:.2f}s - should timeout faster"

    def test_data_integrity_during_collector_restart(
        self,
        docker_compose_environment,
        gcp_monitoring_client,
        gcp_project_id,
        wait_for_telemetry,
        cleanup_collector_exports,
    ):
        """Test minimal data loss during collector restart.

        Given: SDK is actively sending telemetry
        When: Collector restarts mid-stream
        Then: Most telemetry is eventually delivered with minimal loss
        """
        # Arrange
        client = MCAClient(
            service_name="e2e-integrity-test",
            model_id="e2e-int-mdl-001",
            team_name="e2e-test-team",
            model_category="internal",
            model_type="regression",
            otel_endpoint=docker_compose_environment["collector_otlp"],
            otel_protocol="http",
            enable_buffering=True,
        )

        compose_file = Path(__file__).parent.parent.parent / "docker-compose.yml"

        # Determine docker compose command
        try:
            subprocess.run(["docker", "compose", "version"], check=True, capture_output=True)
            docker_cmd = ["docker", "compose"]
        except (subprocess.CalledProcessError, FileNotFoundError):
            docker_cmd = ["docker-compose"]

        num_predictions = 20
        start_time = time.time()

        # Act: Send metrics, restart collector mid-stream
        prediction_ids = []

        for i in range(num_predictions):
            pred_id = f"integrity-pred-{i:03d}"
            prediction_ids.append(pred_id)

            client.record_prediction(latency=0.1, prediction_id=pred_id)

            # Restart collector at midpoint
            if i == num_predictions // 2:
                print("🔄 Restarting collector during active telemetry...")
                subprocess.run(
                    docker_cmd + ["-f", str(compose_file), "restart", "otel-collector"],
                    cwd=compose_file.parent,
                    check=False,
                    capture_output=True,
                )
                wait_for_telemetry(5, "collector restart")

        client.shutdown()
        wait_for_telemetry(10, "final telemetry delivery")

        # Assert: Count delivered metrics
        end_time = time.time()
        metrics = gcp_monitoring_client.query_metrics(
            project_id=gcp_project_id,
            metric_type="mca.internal.regression.predictions",
            start_time=start_time,
            end_time=end_time,
        )

        delivered_count = len(metrics)

        # We expect most metrics to arrive
        # Allow up to 20% loss during restart (some may be in flight)
        min_acceptable = int(num_predictions * 0.8)

        assert delivered_count >= min_acceptable, (
            f"Data integrity failure: {delivered_count}/{num_predictions} metrics delivered. "
            f"Expected at least {min_acceptable} (80% delivery rate)"
        )

        # Calculate and report delivery rate
        delivery_rate = (delivered_count / num_predictions) * 100
        print(
            f"📊 Data integrity: {delivered_count}/{num_predictions} delivered ({delivery_rate:.1f}%)"
        )

        # Verify no duplicate prediction IDs
        received_pred_ids = []
        for metric in metrics:
            attrs = metric.get("attributes", {})
            pred_id = attrs.get("prediction.id") or attrs.get("prediction_id")
            if pred_id:
                received_pred_ids.append(pred_id)

        # Check for duplicates
        unique_count = len(set(received_pred_ids))
        assert (
            unique_count == delivered_count
        ), f"Duplicate metrics detected: {delivered_count} total, {unique_count} unique"
