"""Sustained load testing for MCA SDK.

This module validates that the SDK maintains stable performance and overhead
under sustained high-throughput load conditions.

Acceptance Criteria (AC 1, 5):
- Sustained 1,000+ requests/second for 10 minutes
- SDK overhead <50ms p95 (<100ms absolute max)
- Zero telemetry drops (with healthy collector)
- CPU usage remains stable (no gradual increase)
- Idle CPU usage <1% (background threads)
"""

import gc
import logging
import os
import statistics
import threading
import time
from typing import Dict, List, Tuple

import psutil
import pytest

from mca_sdk import MCAClient
from .performance_helpers import get_collector_endpoint

# Mark as performance/slow test
pytestmark = [pytest.mark.performance, pytest.mark.slow]

logger = logging.getLogger(__name__)


def measure_cpu_usage(duration_seconds: float = 5.0) -> float:
    """Measure average CPU usage percentage over specified duration.

    Args:
        duration_seconds: How long to measure CPU usage

    Returns:
        Average CPU usage as percentage (0-100)
    """
    process = psutil.Process()
    samples: List[float] = []
    start = time.time()

    while time.time() - start < duration_seconds:
        cpu_percent = process.cpu_percent(interval=0.1)
        samples.append(cpu_percent)

    return statistics.mean(samples) if samples else 0.0


def measure_idle_cpu(client: MCAClient, duration_seconds: float = 10.0) -> Dict[str, float]:
    """Measure CPU usage when SDK is idle (no predictions).

    Tests background thread CPU consumption (Registry Refresh, Export Loop).

    Args:
        client: MCAClient instance
        duration_seconds: How long to measure

    Returns:
        Dict with idle CPU statistics
    """
    # Let background threads settle
    time.sleep(2.0)

    # Force garbage collection before measurement
    gc.collect()

    # Measure CPU with no activity
    cpu_samples: List[float] = []
    process = psutil.Process()
    start = time.time()

    while time.time() - start < duration_seconds:
        cpu = process.cpu_percent(interval=0.5)
        cpu_samples.append(cpu)

    avg_cpu = statistics.mean(cpu_samples) if cpu_samples else 0.0
    max_cpu = max(cpu_samples) if cpu_samples else 0.0

    return {
        "average_cpu_percent": avg_cpu,
        "max_cpu_percent": max_cpu,
        "samples": len(cpu_samples),
        "duration_seconds": duration_seconds,
    }


def run_sustained_load_test(
    target_rps: int = 1000,
    duration_minutes: int = 10,
    report_interval_seconds: int = 60,
    collector_endpoint: str = None,
) -> Dict:
    """Run sustained load test at target RPS for specified duration.

    Args:
        target_rps: Target requests per second
        duration_minutes: Test duration in minutes
        report_interval_seconds: How often to report progress
        collector_endpoint: OTel collector endpoint (uses env var or localhost if None)

    Returns:
        Dict containing test results and statistics
    """
    duration_seconds = duration_minutes * 60

    # Get collector endpoint from environment if not specified
    if collector_endpoint is None:
        collector_endpoint = get_collector_endpoint()

    # Initialize client
    client = MCAClient(
        service_name="sustained_load_test",
        model_id="load_test_model",
        team_name="performance_team",
        model_category="internal",
        model_type="regression",
        collector_endpoint=collector_endpoint,
        buffering_enabled=True,
        max_queue_size=10000,  # Large queue for sustained load
    )

    # Tracking
    latencies: List[float] = []
    cpu_samples: List[Tuple[float, float]] = []  # (timestamp, cpu_percent)
    request_count = 0
    interval = 1.0 / target_rps

    # Process handle for CPU monitoring
    process = psutil.Process()

    logger.info("Starting sustained load test:")
    logger.info(f"  Target RPS: {target_rps}")
    logger.info(f"  Duration: {duration_minutes} minutes ({duration_seconds}s)")
    logger.info(f"  Interval: {interval * 1000:.3f}ms between requests")
    logger.info(f"  Collector: {collector_endpoint}")

    start_time = time.time()
    last_report = start_time
    last_request_count = 0

    # Main load loop
    try:
        while time.time() - start_time < duration_seconds:
            loop_start = time.perf_counter()

            # Record prediction (measure SDK overhead)
            req_start = time.perf_counter()
            client.record_prediction(latency=0.001)  # Simulated 1ms model latency
            req_end = time.perf_counter()

            sdk_overhead_ms = (req_end - req_start) * 1000
            latencies.append(sdk_overhead_ms)
            request_count += 1

            # Sample CPU periodically (every 100 requests to avoid overhead)
            if request_count % 100 == 0:
                cpu = process.cpu_percent(interval=0.01)
                cpu_samples.append((time.time(), cpu))

            # Progress reporting
            elapsed = time.time() - start_time
            if elapsed - (last_report - start_time) >= report_interval_seconds:
                requests_since_last = request_count - last_request_count
                actual_rps = requests_since_last / (elapsed - (last_report - start_time))

                # Calculate current window latencies
                window_latencies = latencies[-requests_since_last:]
                if window_latencies:
                    window_p50 = statistics.median(window_latencies)
                    window_p95 = statistics.quantiles(window_latencies, n=20)[18]  # 95th percentile

                    logger.info(
                        f"  [{int(elapsed / 60)}m {int(elapsed % 60)}s] "
                        f"RPS: {actual_rps:.0f} | "
                        f"Requests: {request_count:,} | "
                        f"p50: {window_p50:.3f}ms | "
                        f"p95: {window_p95:.3f}ms"
                    )

                last_report = time.time()
                last_request_count = request_count

            # Maintain target RPS using sleep (non-busy-wait)
            # Calculate remaining time in the interval
            loop_elapsed = time.perf_counter() - loop_start
            sleep_time = interval - loop_elapsed

            if sleep_time > 0:
                time.sleep(sleep_time)

    except KeyboardInterrupt:
        logger.warning("Test interrupted by user")

    finally:
        elapsed_time = time.time() - start_time
        actual_rps = request_count / elapsed_time if elapsed_time > 0 else 0

        # Get final buffer stats to calculate drops
        buffer_stats = client.buffer_stats() if hasattr(client, "buffer_stats") else {}

        # Calculate total dropped items across all signals
        dropped_count = 0
        if buffer_stats and "per_signal_stats" in buffer_stats:
            for signal_name, signal_stats in buffer_stats["per_signal_stats"].items():
                dropped_count += signal_stats.get("items_dropped", 0)

        # Shutdown client
        client.shutdown()

    # Calculate statistics
    latencies.sort()
    p50 = statistics.median(latencies) if latencies else 0
    p95 = (
        statistics.quantiles(latencies, n=20)[18]
        if len(latencies) > 20
        else (latencies[-1] if latencies else 0)
    )
    p99 = (
        statistics.quantiles(latencies, n=100)[98]
        if len(latencies) > 100
        else (latencies[-1] if latencies else 0)
    )
    avg = statistics.mean(latencies) if latencies else 0

    # CPU analysis
    cpu_values = [cpu for _, cpu in cpu_samples]
    avg_cpu = statistics.mean(cpu_values) if cpu_values else 0
    max_cpu = max(cpu_values) if cpu_values else 0

    # CPU stability check (gradual increase detection)
    cpu_stable = True
    if len(cpu_samples) >= 10:
        first_half = cpu_values[: len(cpu_values) // 2]
        second_half = cpu_values[len(cpu_values) // 2 :]
        avg_first_half = statistics.mean(first_half)
        avg_second_half = statistics.mean(second_half)
        cpu_increase_percent = (
            ((avg_second_half - avg_first_half) / avg_first_half * 100) if avg_first_half > 0 else 0
        )
        cpu_stable = cpu_increase_percent < 10  # Less than 10% increase is stable
    else:
        cpu_increase_percent = 0

    # Results
    results = {
        "duration_seconds": elapsed_time,
        "duration_minutes": elapsed_time / 60,
        "target_rps": target_rps,
        "actual_rps": actual_rps,
        "total_requests": request_count,
        "dropped_count": dropped_count,
        "latency": {
            "p50_ms": p50,
            "p95_ms": p95,
            "p99_ms": p99,
            "avg_ms": avg,
            "min_ms": min(latencies) if latencies else 0,
            "max_ms": max(latencies) if latencies else 0,
        },
        "cpu": {
            "average_percent": avg_cpu,
            "max_percent": max_cpu,
            "samples": len(cpu_samples),
            "stable": cpu_stable,
            "increase_percent": cpu_increase_percent,
        },
        "acceptance_criteria": {
            "p95_under_50ms": p95 < 50,
            "no_drops": dropped_count == 0,
            "cpu_stable": cpu_stable,
        },
    }

    # Print results
    logger.info("\n" + "=" * 60)
    logger.info("SUSTAINED LOAD TEST RESULTS")
    logger.info("=" * 60)
    logger.info(
        f"Duration: {results['duration_minutes']:.1f} minutes ({results['duration_seconds']:.0f}s)"
    )
    logger.info(f"Requests: {results['total_requests']:,}")
    logger.info(f"Target RPS: {target_rps} | Actual RPS: {actual_rps:.1f}")
    logger.info("")
    logger.info("SDK Overhead Latency:")
    logger.info(f"  Average: {avg:.3f}ms")
    logger.info(f"  p50:     {p50:.3f}ms")
    logger.info(f"  p95:     {p95:.3f}ms")
    logger.info(f"  p99:     {p99:.3f}ms")
    logger.info(f"  Min:     {results['latency']['min_ms']:.3f}ms")
    logger.info(f"  Max:     {results['latency']['max_ms']:.3f}ms")
    logger.info("")
    logger.info("CPU Usage:")
    logger.info(f"  Average: {avg_cpu:.2f}%")
    logger.info(f"  Max:     {max_cpu:.2f}%")
    logger.info(
        f"  Stable:  {'YES' if cpu_stable else 'NO'} (increase: {cpu_increase_percent:+.1f}%)"
    )
    logger.info("")
    logger.info("Acceptance Criteria:")
    logger.info(
        f"  p95 < 50ms:      {'PASS' if results['acceptance_criteria']['p95_under_50ms'] else 'FAIL'} ({p95:.3f}ms)"
    )
    logger.info(
        f"  Zero drops:      {'PASS' if results['acceptance_criteria']['no_drops'] else 'FAIL'} ({dropped_count} dropped)"
    )
    logger.info(
        f"  CPU stable:      {'PASS' if results['acceptance_criteria']['cpu_stable'] else 'FAIL'}"
    )
    logger.info("=" * 60)

    return results


@pytest.mark.performance
@pytest.mark.slow
@pytest.mark.timeout(720)  # 12 minutes timeout for 10-minute test
def test_sustained_load_1000_rps_10_minutes():
    """Validate SDK maintains stable performance under sustained 1000 RPS load for 10 minutes.

    Acceptance Criteria:
    - AC 1.1: Sustained load of 1,000+ requests/second for 10 minutes
    - AC 1.2: SDK maintains <50ms overhead (p95)
    - AC 1.3: Zero telemetry items dropped (with healthy collector)
    - AC 1.4: CPU usage remains stable without gradual increase
    """
    results = run_sustained_load_test(
        target_rps=1000,
        duration_minutes=10,
        report_interval_seconds=60,
    )

    # Assertions
    assert (
        results["actual_rps"] >= 900
    ), f"Failed to maintain target RPS: {results['actual_rps']:.0f} < 900 (target: 1000 RPS)"

    assert (
        results["latency"]["p95_ms"] < 50
    ), f"p95 latency exceeds 50ms: {results['latency']['p95_ms']:.3f}ms"

    assert (
        results["dropped_count"] == 0
    ), f"Telemetry drops detected: {results['dropped_count']} items dropped"

    assert results["cpu"]["stable"], (
        f"CPU usage not stable: increased by {results['cpu']['increase_percent']:.1f}% "
        f"over test duration"
    )


@pytest.mark.performance
def test_idle_cpu_usage():
    """Validate background threads consume negligible CPU when idle.

    Acceptance Criteria:
    - AC 5: Background threads consume <1% CPU when application is idle
    """
    client = MCAClient(
        service_name="idle_cpu_test",
        model_id="idle_test_model",
        team_name="performance_team",
        model_category="internal",
        model_type="regression",
        collector_endpoint=get_collector_endpoint(),
    )

    try:
        logger.info("Measuring idle CPU usage (background threads only)...")
        idle_stats = measure_idle_cpu(client, duration_seconds=10.0)

        logger.info("\nIdle CPU Usage:")
        logger.info(f"  Average: {idle_stats['average_cpu_percent']:.2f}%")
        logger.info(f"  Max:     {idle_stats['max_cpu_percent']:.2f}%")
        logger.info(f"  Duration: {idle_stats['duration_seconds']:.0f}s")
        logger.info(f"  Samples: {idle_stats['samples']}")

        # AC 5: Idle CPU must be <1%
        assert (
            idle_stats["average_cpu_percent"] < 1.0
        ), f"Idle CPU usage exceeds 1%: {idle_stats['average_cpu_percent']:.2f}%"

        logger.info("\nIdle CPU Test: PASS (<1%)")

    finally:
        client.shutdown()


if __name__ == "__main__":
    # Configure logging
    logging.basicConfig(level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s")

    # Run sustained load test
    results = run_sustained_load_test(
        target_rps=1000,
        duration_minutes=10,
        report_interval_seconds=60,
    )

    # Exit with status
    all_pass = all(results["acceptance_criteria"].values())
    exit(0 if all_pass else 1)
