//! Embedding Batcher - High-throughput batch embedding generation
//!
//! Provides async batching for embedding requests to maximize GPU/model utilization.
//!
//! # Architecture
//!
//! ```text
//! ┌─────────────────────────────────────────────────────────────┐
//! │                   EmbeddingBatcher                          │
//! │                                                             │
//! │  Request 1 ──┐                                              │
//! │  Request 2 ──┼──► AsyncQueue ──► BatchAggregator ──► Model  │
//! │  Request 3 ──┘         │              │               │     │
//! │       ...              ▼              ▼               ▼     │
//! │              [Backpressure]    [Batch when full    [Batch   │
//! │               Control]         or timeout]         Inference]
//! │                                                             │
//! └─────────────────────────────────────────────────────────────┘
//! ```

use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
use std::sync::Arc;
use std::time::{Duration, Instant};

use parking_lot::Mutex;
use tokio::sync::{mpsc, oneshot};

use super::error::PerceptionError;
use super::traits::PerceptionEngine;

// ============================================================================
// Configuration
// ============================================================================

/// Configuration for the embedding batcher
#[derive(Debug, Clone)]
pub struct EmbeddingBatcherConfig {
    /// Maximum number of pending embedding requests
    pub queue_capacity: usize,
    
    /// Maximum batch size for processing
    pub max_batch_size: usize,
    
    /// Maximum time to wait before flushing a partial batch (ms)
    pub flush_interval_ms: u64,
    
    /// Timeout for sending to queue (ms), 0 = no timeout
    pub send_timeout_ms: u64,
    
    /// Queue fill ratio at which slowdown begins (0.0 - 1.0)
    pub queue_low_watermark: f64,
    
    /// Queue fill ratio at which severe slowdown applies (0.0 - 1.0)
    pub queue_high_watermark: f64,
    
    /// Queue fill ratio at which requests are rejected (0.0 - 1.0)
    pub reject_threshold: f64,
}

impl Default for EmbeddingBatcherConfig {
    fn default() -> Self {
        Self {
            queue_capacity: 10_000,
            max_batch_size: 32,  // Typical batch size for embedding models
            flush_interval_ms: 50,
            send_timeout_ms: 5000,
            queue_low_watermark: 0.5,
            queue_high_watermark: 0.8,
            reject_threshold: 0.95,
        }
    }
}

impl EmbeddingBatcherConfig {
    /// Config optimized for high throughput (larger batches, longer wait)
    pub fn high_throughput() -> Self {
        Self {
            queue_capacity: 50_000,
            max_batch_size: 64,
            flush_interval_ms: 100,
            send_timeout_ms: 10000,
            queue_low_watermark: 0.6,
            queue_high_watermark: 0.85,
            reject_threshold: 0.95,
        }
    }
    
    /// Config optimized for low latency (smaller batches, faster flush)
    pub fn low_latency() -> Self {
        Self {
            queue_capacity: 5_000,
            max_batch_size: 16,
            flush_interval_ms: 10,
            send_timeout_ms: 2000,
            queue_low_watermark: 0.4,
            queue_high_watermark: 0.7,
            reject_threshold: 0.9,
        }
    }
}

// ============================================================================
// Request and Response Types
// ============================================================================

/// An embedding request
pub struct EmbeddingRequest {
    /// The text to embed
    pub text: String,
    
    /// Channel to send the result back
    pub response_tx: oneshot::Sender<EmbeddingResponse>,
    
    /// Request creation timestamp
    pub created_at: Instant,
    
    /// Sequence number
    pub sequence: u64,
}

/// Response for an embedding request
#[derive(Debug, Clone)]
pub struct EmbeddingResponse {
    /// The generated embedding
    pub embedding: Result<Vec<f32>, String>,
    
    /// Time spent in queue (microseconds)
    pub queue_time_us: u64,
    
    /// Time spent in batch processing (microseconds)
    pub processing_time_us: u64,
    
    /// Batch size this request was processed in
    pub batch_size: usize,
}

impl EmbeddingResponse {
    /// Create a successful response
    pub fn success(
        embedding: Vec<f32>,
        queue_time_us: u64,
        processing_time_us: u64,
        batch_size: usize,
    ) -> Self {
        Self {
            embedding: Ok(embedding),
            queue_time_us,
            processing_time_us,
            batch_size,
        }
    }
    
    /// Create a failed response
    pub fn failure(error: String, queue_time_us: u64) -> Self {
        Self {
            embedding: Err(error),
            queue_time_us,
            processing_time_us: 0,
            batch_size: 0,
        }
    }
}

// ============================================================================
// Metrics
// ============================================================================

/// Metrics for embedding batcher
#[derive(Debug, Default)]
pub struct EmbeddingBatcherMetrics {
    /// Total requests submitted
    pub total_requests: AtomicU64,
    
    /// Successfully processed requests
    pub processed_requests: AtomicU64,
    
    /// Failed requests
    pub failed_requests: AtomicU64,
    
    /// Rejected requests (due to backpressure)
    pub rejected_requests: AtomicU64,
    
    /// Total batches processed
    pub batches_processed: AtomicU64,
    
    /// Total processing time in microseconds
    pub total_processing_time_us: AtomicU64,
    
    /// Total queue wait time in microseconds
    pub total_queue_time_us: AtomicU64,
    
    /// Current queue size
    pub current_queue_size: AtomicU64,
}

impl EmbeddingBatcherMetrics {
    /// Create new metrics instance
    pub fn new() -> Self {
        Self::default()
    }
    
    /// Record a submitted request
    pub fn record_request(&self) {
        self.total_requests.fetch_add(1, Ordering::Relaxed);
    }
    
    /// Record a processed batch
    pub fn record_batch(&self, batch_size: usize, processing_time_us: u64, queue_time_us: u64) {
        self.processed_requests.fetch_add(batch_size as u64, Ordering::Relaxed);
        self.batches_processed.fetch_add(1, Ordering::Relaxed);
        self.total_processing_time_us.fetch_add(processing_time_us, Ordering::Relaxed);
        self.total_queue_time_us.fetch_add(queue_time_us, Ordering::Relaxed);
    }
    
    /// Record a failure
    pub fn record_failure(&self) {
        self.failed_requests.fetch_add(1, Ordering::Relaxed);
    }
    
    /// Record a rejection
    pub fn record_rejection(&self) {
        self.rejected_requests.fetch_add(1, Ordering::Relaxed);
    }
    
    /// Update queue size
    pub fn update_queue_size(&self, size: usize) {
        self.current_queue_size.store(size as u64, Ordering::Relaxed);
    }
    
    /// Get a snapshot of current metrics
    pub fn snapshot(&self) -> MetricsSnapshot {
        MetricsSnapshot {
            total_requests: self.total_requests.load(Ordering::Relaxed),
            processed_requests: self.processed_requests.load(Ordering::Relaxed),
            failed_requests: self.failed_requests.load(Ordering::Relaxed),
            rejected_requests: self.rejected_requests.load(Ordering::Relaxed),
            batches_processed: self.batches_processed.load(Ordering::Relaxed),
            total_processing_time_us: self.total_processing_time_us.load(Ordering::Relaxed),
            total_queue_time_us: self.total_queue_time_us.load(Ordering::Relaxed),
            current_queue_size: self.current_queue_size.load(Ordering::Relaxed),
        }
    }
}

/// Snapshot of metrics at a point in time
#[derive(Debug, Clone)]
pub struct MetricsSnapshot {
    pub total_requests: u64,
    pub processed_requests: u64,
    pub failed_requests: u64,
    pub rejected_requests: u64,
    pub batches_processed: u64,
    pub total_processing_time_us: u64,
    pub total_queue_time_us: u64,
    pub current_queue_size: u64,
}

impl MetricsSnapshot {
    /// Calculate average batch size
    pub fn avg_batch_size(&self) -> f64 {
        if self.batches_processed == 0 {
            0.0
        } else {
            self.processed_requests as f64 / self.batches_processed as f64
        }
    }
    
    /// Calculate average processing time per request (microseconds)
    pub fn avg_processing_time_us(&self) -> f64 {
        if self.processed_requests == 0 {
            0.0
        } else {
            self.total_processing_time_us as f64 / self.processed_requests as f64
        }
    }
    
    /// Calculate average queue wait time (microseconds)
    pub fn avg_queue_time_us(&self) -> f64 {
        if self.processed_requests == 0 {
            0.0
        } else {
            self.total_queue_time_us as f64 / self.processed_requests as f64
        }
    }
    
    /// Calculate throughput (requests per second) given elapsed time
    pub fn throughput(&self, elapsed: Duration) -> f64 {
        if elapsed.is_zero() {
            0.0
        } else {
            self.processed_requests as f64 / elapsed.as_secs_f64()
        }
    }
}

// ============================================================================
// Errors
// ============================================================================

/// Errors from embedding batcher
#[derive(Debug, Clone, thiserror::Error)]
pub enum EmbeddingBatcherError {
    #[error("Embedding queue is full")]
    QueueFull,
    
    #[error("Batcher is closed")]
    BatcherClosed,
    
    #[error("Request timed out")]
    Timeout,
    
    #[error("Embedding failed: {0}")]
    EmbeddingFailed(String),
    
    #[error("Response channel closed")]
    ResponseChannelClosed,
}

impl From<PerceptionError> for EmbeddingBatcherError {
    fn from(err: PerceptionError) -> Self {
        EmbeddingBatcherError::EmbeddingFailed(err.to_string())
    }
}

// ============================================================================
// Backpressure
// ============================================================================

/// Backpressure action
#[derive(Debug, Clone, PartialEq)]
pub enum BackpressureAction {
    Accept,
    Slowdown(Duration),
    Reject,
}

// ============================================================================
// EmbeddingBatcher
// ============================================================================

/// High-throughput embedding batcher
///
/// Accumulates embedding requests and processes them in batches for better
/// GPU/model utilization.
pub struct EmbeddingBatcher {
    /// Sender for enqueueing requests
    sender: mpsc::Sender<EmbeddingRequest>,
    
    /// Receiver (moved to worker)
    receiver: Mutex<Option<mpsc::Receiver<EmbeddingRequest>>>,
    
    /// The perception engine
    engine: Arc<dyn PerceptionEngine>,
    
    /// Configuration
    config: EmbeddingBatcherConfig,
    
    /// Metrics
    metrics: Arc<EmbeddingBatcherMetrics>,
    
    /// Sequence counter
    sequence: AtomicU64,
    
    /// Whether the batcher is running
    running: AtomicBool,
}

impl EmbeddingBatcher {
    /// Create a new embedding batcher
    pub fn new(engine: Arc<dyn PerceptionEngine>, config: EmbeddingBatcherConfig) -> Self {
        let (sender, receiver) = mpsc::channel(config.queue_capacity);
        
        Self {
            sender,
            receiver: Mutex::new(Some(receiver)),
            engine,
            config,
            metrics: Arc::new(EmbeddingBatcherMetrics::new()),
            sequence: AtomicU64::new(0),
            running: AtomicBool::new(false),
        }
    }
    
    /// Get the next sequence number
    fn next_sequence(&self) -> u64 {
        self.sequence.fetch_add(1, Ordering::SeqCst)
    }
    
    /// Get current queue length (approximate)
    pub fn queue_len(&self) -> usize {
        self.config.queue_capacity - self.sender.capacity()
    }
    
    /// Get queue fill ratio (0.0 - 1.0)
    pub fn queue_fill_ratio(&self) -> f64 {
        self.queue_len() as f64 / self.config.queue_capacity as f64
    }
    
    /// Evaluate backpressure action
    fn evaluate_backpressure(&self) -> BackpressureAction {
        let fill_ratio = self.queue_fill_ratio();
        
        if fill_ratio >= self.config.reject_threshold {
            BackpressureAction::Reject
        } else if fill_ratio >= self.config.queue_high_watermark {
            let ratio = (fill_ratio - self.config.queue_high_watermark) / 
                (self.config.reject_threshold - self.config.queue_high_watermark);
            let delay_ms = (10.0 * (2.0_f64).powf(ratio * 4.0)).min(100.0) as u64;
            BackpressureAction::Slowdown(Duration::from_millis(delay_ms))
        } else if fill_ratio >= self.config.queue_low_watermark {
            let ratio = (fill_ratio - self.config.queue_low_watermark) / 
                (self.config.queue_high_watermark - self.config.queue_low_watermark);
            let delay_ms = (1.0 * (2.0_f64).powf(ratio * 2.0)).min(10.0) as u64;
            BackpressureAction::Slowdown(Duration::from_millis(delay_ms))
        } else {
            BackpressureAction::Accept
        }
    }
    
    /// Embed text asynchronously (returns when embedding is ready)
    pub async fn embed(&self, text: String) -> Result<Vec<f32>, EmbeddingBatcherError> {
        // Check backpressure
        match self.evaluate_backpressure() {
            BackpressureAction::Accept => {}
            BackpressureAction::Slowdown(duration) => {
                tokio::time::sleep(duration).await;
            }
            BackpressureAction::Reject => {
                self.metrics.record_rejection();
                return Err(EmbeddingBatcherError::QueueFull);
            }
        }
        
        let (response_tx, response_rx) = oneshot::channel();
        let sequence = self.next_sequence();
        
        let request = EmbeddingRequest {
            text,
            response_tx,
            created_at: Instant::now(),
            sequence,
        };
        
        self.metrics.record_request();
        self.metrics.update_queue_size(self.queue_len());
        
        // Send to queue
        if self.config.send_timeout_ms > 0 {
            match tokio::time::timeout(
                Duration::from_millis(self.config.send_timeout_ms),
                self.sender.send(request)
            ).await {
                Ok(Ok(())) => {}
                Ok(Err(_)) => {
                    self.metrics.record_failure();
                    return Err(EmbeddingBatcherError::BatcherClosed);
                }
                Err(_) => {
                    self.metrics.record_rejection();
                    return Err(EmbeddingBatcherError::Timeout);
                }
            }
        } else {
            self.sender.send(request).await.map_err(|_| {
                self.metrics.record_failure();
                EmbeddingBatcherError::BatcherClosed
            })?;
        }
        
        // Wait for response
        let response = response_rx.await
            .map_err(|_| EmbeddingBatcherError::ResponseChannelClosed)?;
        
        response.embedding.map_err(EmbeddingBatcherError::EmbeddingFailed)
    }
    
    /// Embed multiple texts (batch submission)
    pub async fn embed_batch(&self, texts: Vec<String>) -> Vec<Result<Vec<f32>, EmbeddingBatcherError>> {
        let mut results = Vec::with_capacity(texts.len());
        for text in texts {
            results.push(self.embed(text).await);
        }
        results
    }
    
    /// Take the receiver (for worker thread)
    pub fn take_receiver(&self) -> Option<mpsc::Receiver<EmbeddingRequest>> {
        self.receiver.lock().take()
    }
    
    /// Get metrics
    pub fn metrics(&self) -> &Arc<EmbeddingBatcherMetrics> {
        &self.metrics
    }
    
    /// Get configuration
    pub fn config(&self) -> &EmbeddingBatcherConfig {
        &self.config
    }
    
    /// Get engine reference
    pub fn engine(&self) -> &Arc<dyn PerceptionEngine> {
        &self.engine
    }
    
    /// Check if batcher is running
    pub fn is_running(&self) -> bool {
        self.running.load(Ordering::Relaxed)
    }
    
    /// Set running state
    pub fn set_running(&self, running: bool) {
        self.running.store(running, Ordering::Relaxed);
    }
}

// ============================================================================
// Worker Functions
// ============================================================================

/// Process a batch of embedding requests
fn process_batch(
    batch: Vec<EmbeddingRequest>,
    engine: &dyn PerceptionEngine,
    metrics: &EmbeddingBatcherMetrics,
) {
    if batch.is_empty() {
        return;
    }
    
    let batch_size = batch.len();
    let start = Instant::now();
    
    // Calculate total queue time
    let total_queue_time_us: u64 = batch.iter()
        .map(|r| r.created_at.elapsed().as_micros() as u64)
        .sum();
    
    // Extract texts for batch processing
    let texts: Vec<&str> = batch.iter().map(|r| r.text.as_str()).collect();
    
    // Try batch processing first
    let results = engine.conceptualize_batch(&texts);
    
    let processing_time_us = start.elapsed().as_micros() as u64;
    
    match results {
        Ok(embeddings) => {
            // Send successful responses
            for (request, embedding) in batch.into_iter().zip(embeddings.into_iter()) {
                let queue_time_us = request.created_at.elapsed().as_micros() as u64;
                let response = EmbeddingResponse::success(
                    embedding,
                    queue_time_us,
                    processing_time_us / batch_size as u64,
                    batch_size,
                );
                let _ = request.response_tx.send(response);
            }
            metrics.record_batch(batch_size, processing_time_us, total_queue_time_us);
        }
        Err(e) => {
            // Send error responses
            let error_msg = e.to_string();
            for request in batch {
                let queue_time_us = request.created_at.elapsed().as_micros() as u64;
                let response = EmbeddingResponse::failure(error_msg.clone(), queue_time_us);
                let _ = request.response_tx.send(response);
                metrics.record_failure();
            }
        }
    }
}

/// Start the embedding batcher worker
///
/// This function spawns a worker task that processes embedding requests in batches.
pub async fn start_embedding_worker(
    batcher: Arc<EmbeddingBatcher>,
) -> Result<(), EmbeddingBatcherError> {
    let receiver = batcher.take_receiver()
        .ok_or(EmbeddingBatcherError::BatcherClosed)?;
    
    batcher.set_running(true);
    
    let config = batcher.config().clone();
    let metrics = batcher.metrics().clone();
    let engine = batcher.engine().clone();
    
    let batcher_clone = batcher.clone();
    
    tokio::spawn(async move {
        let mut receiver = receiver;
        let mut batch: Vec<EmbeddingRequest> = Vec::with_capacity(config.max_batch_size);
        let mut last_flush = Instant::now();
        let flush_interval = Duration::from_millis(config.flush_interval_ms);
        
        loop {
            // Try to receive with timeout for batch flushing
            match tokio::time::timeout(flush_interval, receiver.recv()).await {
                Ok(Some(request)) => {
                    batch.push(request);
                    
                    // Process batch if full
                    if batch.len() >= config.max_batch_size {
                        let batch_to_process = std::mem::take(&mut batch);
                        process_batch(batch_to_process, engine.as_ref(), &metrics);
                        last_flush = Instant::now();
                    }
                }
                Ok(None) => {
                    // Channel closed, process remaining
                    if !batch.is_empty() {
                        process_batch(std::mem::take(&mut batch), engine.as_ref(), &metrics);
                    }
                    batcher_clone.set_running(false);
                    break;
                }
                Err(_) => {
                    // Timeout - flush partial batch if we have items
                    if !batch.is_empty() && last_flush.elapsed() >= flush_interval {
                        let batch_to_process = std::mem::take(&mut batch);
                        process_batch(batch_to_process, engine.as_ref(), &metrics);
                        last_flush = Instant::now();
                    }
                }
            }
            
            metrics.update_queue_size(batcher_clone.queue_len());
        }
    });
    
    Ok(())
}

// ============================================================================
// Tests
// ============================================================================

#[cfg(test)]
mod tests {
    use super::*;
    use super::super::traits::DummyPerceptionEngine;
    
    fn create_test_batcher(batch_size: usize) -> Arc<EmbeddingBatcher> {
        let engine = Arc::new(DummyPerceptionEngine::new(384));
        let config = EmbeddingBatcherConfig {
            queue_capacity: 1000,
            max_batch_size: batch_size,
            flush_interval_ms: 10,
            send_timeout_ms: 1000,
            ..Default::default()
        };
        Arc::new(EmbeddingBatcher::new(engine, config))
    }
    
    #[test]
    fn test_config_defaults() {
        let config = EmbeddingBatcherConfig::default();
        assert_eq!(config.queue_capacity, 10_000);
        assert_eq!(config.max_batch_size, 32);
    }
    
    #[test]
    fn test_config_high_throughput() {
        let config = EmbeddingBatcherConfig::high_throughput();
        assert_eq!(config.queue_capacity, 50_000);
        assert_eq!(config.max_batch_size, 64);
    }
    
    #[test]
    fn test_config_low_latency() {
        let config = EmbeddingBatcherConfig::low_latency();
        assert_eq!(config.queue_capacity, 5_000);
        assert_eq!(config.max_batch_size, 16);
    }
    
    #[test]
    fn test_batcher_creation() {
        let batcher = create_test_batcher(32);
        assert_eq!(batcher.queue_len(), 0);
        assert!(!batcher.is_running());
    }
    
    #[test]
    fn test_metrics_snapshot() {
        let metrics = EmbeddingBatcherMetrics::new();
        
        metrics.record_request();
        metrics.record_request();
        metrics.record_batch(2, 1000, 500);
        metrics.record_failure();
        metrics.record_rejection();
        
        let snapshot = metrics.snapshot();
        assert_eq!(snapshot.total_requests, 2);
        assert_eq!(snapshot.processed_requests, 2);
        assert_eq!(snapshot.batches_processed, 1);
        assert_eq!(snapshot.failed_requests, 1);
        assert_eq!(snapshot.rejected_requests, 1);
        assert_eq!(snapshot.avg_batch_size(), 2.0);
    }
    
    #[test]
    fn test_embedding_response() {
        let response = EmbeddingResponse::success(vec![0.1, 0.2], 100, 50, 10);
        assert!(response.embedding.is_ok());
        assert_eq!(response.queue_time_us, 100);
        assert_eq!(response.processing_time_us, 50);
        assert_eq!(response.batch_size, 10);
        
        let error_response = EmbeddingResponse::failure("test error".to_string(), 100);
        assert!(error_response.embedding.is_err());
    }
    
    #[test]
    fn test_backpressure_evaluation() {
        let batcher = create_test_batcher(32);
        
        // Empty queue - should accept
        assert_eq!(batcher.evaluate_backpressure(), BackpressureAction::Accept);
    }
    
    #[tokio::test]
    async fn test_embed_single() {
        let batcher = create_test_batcher(32);
        
        // Start worker
        start_embedding_worker(batcher.clone()).await.unwrap();
        assert!(batcher.is_running());
        
        // Submit single embedding request
        let result = batcher.embed("hello world".to_string()).await;
        assert!(result.is_ok());
        
        let embedding = result.unwrap();
        assert_eq!(embedding.len(), 384);
    }
    
    #[tokio::test]
    async fn test_embed_multiple() {
        let batcher = create_test_batcher(32);
        
        start_embedding_worker(batcher.clone()).await.unwrap();
        
        // Submit multiple requests
        let texts: Vec<String> = (0..10)
            .map(|i| format!("text {}", i))
            .collect();
        
        let results = batcher.embed_batch(texts).await;
        
        assert_eq!(results.len(), 10);
        for result in results {
            assert!(result.is_ok());
            assert_eq!(result.unwrap().len(), 384);
        }
    }
    
    #[tokio::test]
    async fn test_batch_processing() {
        let batcher = create_test_batcher(5); // Small batch size
        
        start_embedding_worker(batcher.clone()).await.unwrap();
        
        // Submit more than batch size
        let texts: Vec<String> = (0..20)
            .map(|i| format!("text {}", i))
            .collect();
        
        let results = batcher.embed_batch(texts).await;
        
        assert_eq!(results.len(), 20);
        
        // Wait for metrics to update
        tokio::time::sleep(Duration::from_millis(50)).await;
        
        let snapshot = batcher.metrics().snapshot();
        assert_eq!(snapshot.total_requests, 20);
        assert_eq!(snapshot.processed_requests, 20);
        // Should have multiple batches
        assert!(snapshot.batches_processed >= 4); // 20 / 5 = 4 batches
    }
    
    #[tokio::test]
    async fn test_metrics_tracking() {
        let batcher = create_test_batcher(10);
        
        start_embedding_worker(batcher.clone()).await.unwrap();
        
        // Submit requests
        for i in 0..5 {
            let _ = batcher.embed(format!("text {}", i)).await;
        }
        
        let snapshot = batcher.metrics().snapshot();
        assert_eq!(snapshot.total_requests, 5);
        assert_eq!(snapshot.processed_requests, 5);
        assert!(snapshot.total_processing_time_us > 0);
    }
    
    #[tokio::test]
    async fn test_concurrent_requests() {
        let batcher = create_test_batcher(32);
        
        start_embedding_worker(batcher.clone()).await.unwrap();
        
        // Submit many concurrent requests
        let mut handles = Vec::new();
        for i in 0..100 {
            let batcher = batcher.clone();
            handles.push(tokio::spawn(async move {
                batcher.embed(format!("concurrent text {}", i)).await
            }));
        }
        
        // Wait for all to complete
        for handle in handles {
            let result: Result<Result<Vec<f32>, EmbeddingBatcherError>, _> = handle.await;
            assert!(result.is_ok());
            assert!(result.unwrap().is_ok());
        }
        
        let snapshot = batcher.metrics().snapshot();
        assert_eq!(snapshot.total_requests, 100);
        assert_eq!(snapshot.processed_requests, 100);
    }
}
