//! Edge Case Tests for Early Stopping
//!
//! This module tests extreme and unusual scenarios that early stopping must handle:
//! 1. Zero variance data
//! 2. NaN/Inf handling
//! 3. Single epoch training
//! 4. Concurrent trial execution
//! 5. Extreme parameter values
//! 6. Memory/resource limits

use tracing::info;

// ============================================================================
// ZERO VARIANCE TESTS
// ============================================================================

#[test]
fn test_early_stopping_constant_loss() {
    // Training stuck at constant loss (model not learning)
    let constant_losses = vec![1.5; 50];
    
    let window = 10;
    let min_improvement = 0.1;
    
    // Should detect plateau immediately after 2*window epochs
    let epoch = 25;
    let recent_avg = constant_losses[epoch-window..epoch].iter().sum::<f64>() / window as f64;
    let older_avg = constant_losses[epoch-2*window..epoch-window].iter().sum::<f64>() / window as f64;
    
    let improvement = ((older_avg - recent_avg) / older_avg * 100.0).abs();
    
    info!(loss = constant_losses[0], improvement, "Constant loss test");
    
    assert!(improvement < min_improvement, "Should detect constant loss as plateau");
    assert_eq!(recent_avg, older_avg, "Averages should be identical");
}

#[test]
fn test_early_stopping_near_zero_loss() {
    // Training converged to near-perfect loss
    let near_zero_losses = vec![1e-10; 20];
    
    // Should still apply plateau detection
    let window = 5;
    let recent_avg = near_zero_losses[15..20].iter().sum::<f64>() / window as f64;
    let older_avg = near_zero_losses[10..15].iter().sum::<f64>() / window as f64;
    
    let improvement = if older_avg > 0.0 {
        ((older_avg - recent_avg) / older_avg * 100.0).abs()
    } else {
        0.0
    };
    
    info!(loss = near_zero_losses[0], improvement, "Near-zero loss test");
    
    // Should detect as plateau
    assert!(improvement < 0.1);
}

// ============================================================================
// NaN/INF HANDLING TESTS
// ============================================================================

#[test]
fn test_early_stopping_nan_detection() {
    let losses_with_nan = vec![1.0, 0.9, 0.8, f64::NAN, 0.6];
    
    // Verify NaN detection
    let has_nan = losses_with_nan.iter().any(|l| l.is_nan());
    assert!(has_nan, "Should detect NaN");
    
    // Find NaN location
    let nan_index = losses_with_nan.iter()
        .position(|l| l.is_nan())
        .expect("Should find NaN");
    
    info!(nan_index, "NaN detected at epoch");
    assert_eq!(nan_index, 3);
    
    // In production, this would mark trial as FAILED
}

#[test]
fn test_early_stopping_inf_detection() {
    let losses_with_inf = vec![1.0, 0.9, f64::INFINITY, 0.7];
    
    // Verify Inf detection
    let has_inf = losses_with_inf.iter().any(|l| l.is_infinite());
    assert!(has_inf, "Should detect Inf");
    
    let inf_index = losses_with_inf.iter()
        .position(|l| l.is_infinite())
        .expect("Should find Inf");
    
    info!(inf_index, "Inf detected at epoch");
    assert_eq!(inf_index, 2);
}

#[test]
fn test_early_stopping_negative_inf() {
    let losses = vec![1.0, 0.5, f64::NEG_INFINITY, 0.3];
    
    let has_neg_inf = losses.iter().any(|l| l.is_infinite() && l.is_sign_negative());
    assert!(has_neg_inf, "Should detect negative infinity");
    
    // Negative infinity is also invalid
    for (i, &loss) in losses.iter().enumerate() {
        if loss.is_infinite() {
            info!(epoch = i, loss, "Invalid loss at epoch");
        }
    }
}

// ============================================================================
// SINGLE EPOCH TESTS
// ============================================================================

#[test]
fn test_early_stopping_single_epoch_training() {
    let config = ml::trainers::dqn::DQNHyperparameters {
        epochs: 1,
        min_epochs_before_stopping: 50,
        ..Default::default()
    };
    
    // With only 1 epoch, early stopping should never trigger
    assert!(config.epochs < config.min_epochs_before_stopping);
    
    info!(epochs = config.epochs, min_epochs_before_stopping = config.min_epochs_before_stopping, "Single epoch config: early stopping will NOT trigger");
}

#[test]
fn test_early_stopping_two_epochs() {
    // With 2 epochs, cannot calculate plateau (need 2*window)
    let losses = vec![1.0, 0.9];
    let window = 5;
    
    // Cannot calculate plateau with < 2*window epochs
    assert!(losses.len() < window * 2, "Not enough epochs for plateau detection");
    
    info!(losses = ?losses, window, need = window * 2, "Two epoch test: insufficient epochs for plateau detection");
}

// ============================================================================
// CONCURRENT EXECUTION TESTS
// ============================================================================

#[test]
fn test_early_stopping_thread_safety_simulation() {
    use std::sync::{Arc, Mutex};
    use std::thread;
    
    // Simulate multiple trials running concurrently
    #[derive(Debug, Clone)]
    struct TrialState {
        trial_id: usize,
        best_loss: f64,
        patience_counter: usize,
        stopped: bool,
    }
    
    let trials = Arc::new(Mutex::new(vec![
        TrialState { trial_id: 0, best_loss: f64::INFINITY, patience_counter: 0, stopped: false },
        TrialState { trial_id: 1, best_loss: f64::INFINITY, patience_counter: 0, stopped: false },
        TrialState { trial_id: 2, best_loss: f64::INFINITY, patience_counter: 0, stopped: false },
    ]));
    
    let mut handles = vec![];
    
    // Spawn threads to update trials concurrently
    for i in 0..3 {
        let trials_clone = Arc::clone(&trials);
        let handle = thread::spawn(move || {
            let new_loss = 1.0 - (i as f64 * 0.1);
            
            let mut trials = trials_clone.lock().unwrap();
            trials[i].best_loss = new_loss;
            trials[i].patience_counter = 0;
            
            info!(trial = i, new_loss, "Trial updated");
        });
        handles.push(handle);
    }
    
    // Wait for all threads
    for handle in handles {
        handle.join().unwrap();
    }
    
    // Verify state isolation (no race conditions)
    let final_trials = trials.lock().unwrap();
    for trial in final_trials.iter() {
        info!(trial_id = trial.trial_id, best_loss = trial.best_loss, stopped = trial.stopped, "Trial final state");
        assert!(trial.best_loss < f64::INFINITY, "Trial {} should have been updated", trial.trial_id);
    }
}

// ============================================================================
// EXTREME PARAMETER TESTS
// ============================================================================

#[test]
fn test_early_stopping_zero_patience() {
    // Patience = 0 means stop immediately on first non-improvement
    let patience = 0;
    let losses = vec![1.0, 0.9, 0.95]; // Improvement, then worse
    
    let mut no_improvement_count = 0;
    let mut best_loss = f64::INFINITY;
    
    for (epoch, &loss) in losses.iter().enumerate() {
        if loss < best_loss {
            best_loss = loss;
            no_improvement_count = 0;
        } else {
            no_improvement_count += 1;
        }
        
        if no_improvement_count > patience {
            info!(epoch, "Stopped at epoch with zero patience");
            assert_eq!(epoch, 2, "Should stop immediately at first non-improvement");
            return;
        }
    }
}

#[test]
fn test_early_stopping_infinite_patience() {
    // Infinite patience means never stop
    let patience = usize::MAX;
    let losses = vec![1.0, 1.1, 1.2, 1.3, 1.4]; // Always getting worse
    
    let mut no_improvement_count = 0;
    let mut best_loss = f64::INFINITY;
    
    for (epoch, &loss) in losses.iter().enumerate() {
        if loss < best_loss {
            best_loss = loss;
            no_improvement_count = 0;
        } else {
            no_improvement_count += 1;
        }
        
        assert!(no_improvement_count < patience, "Should never exhaust infinite patience");
        info!(epoch, no_improvement_count, "Epoch no_improvement count");
    }
    
    assert_eq!(no_improvement_count, 4, "Counted 4 non-improvements but didn't stop");
}

#[test]
fn test_early_stopping_window_size_one() {
    // Window size 1 means compare consecutive epochs
    let losses = vec![1.0, 0.9, 0.89, 0.88, 0.87, 0.86];
    let window = 1;
    let min_improvement = 5.0; // 5%
    
    for epoch in 2..losses.len() {
        let recent = losses[epoch];
        let older = losses[epoch-1];
        let improvement = ((older - recent) / older * 100.0).abs();
        
        info!(epoch, improvement, "Epoch improvement");
        if improvement < min_improvement {
            info!(min_improvement, "Plateau detected");
        }
    }
}

#[test]
fn test_early_stopping_very_large_window() {
    // Window size = total epochs means very conservative stopping
    let losses = vec![1.0; 100];
    let window = 50; // Half of total epochs
    let min_improvement = 0.1;
    
    // Can only check plateau after 2*window epochs
    let check_epoch = 100;
    if check_epoch >= window * 2 {
        let recent_avg = losses[check_epoch-window..check_epoch].iter().sum::<f64>() / window as f64;
        let older_avg = losses[check_epoch-2*window..check_epoch-window].iter().sum::<f64>() / window as f64;
        let improvement = ((older_avg - recent_avg) / older_avg * 100.0).abs();
        
        info!(window, improvement, "Large window test");
        assert!(improvement < min_improvement);
    }
}

// ============================================================================
// MEMORY/RESOURCE LIMIT TESTS
// ============================================================================

#[test]
fn test_early_stopping_loss_history_memory() {
    // Test memory usage with very long training
    let num_epochs = 10_000;
    let mut loss_history: Vec<f64> = Vec::with_capacity(num_epochs);
    
    // Simulate training
    for epoch in 0..num_epochs {
        let loss = 1.0 / (epoch as f64 + 1.0);
        loss_history.push(loss);
    }
    
    let memory_bytes = loss_history.len() * std::mem::size_of::<f64>();
    info!(num_epochs, memory_bytes, memory_kb = memory_bytes as f64 / 1024.0, "Loss history memory usage");
    
    // Verify reasonable memory usage
    assert!(memory_bytes < 1_000_000, "Should use < 1 MB for 10k epochs");
}

#[test]
fn test_early_stopping_with_batch_size_zero() {
    // Batch size 0 should be rejected before training starts
    let config = ml::trainers::dqn::DQNHyperparameters {
        batch_size: 0,
        ..Default::default()
    };
    
    // In production, this would be caught by validation
    info!(batch_size = config.batch_size, "Invalid batch size");
    assert_eq!(config.batch_size, 0);
    // Note: actual validation would happen in DQNTrainer::new()
}

#[test]
fn test_early_stopping_max_batch_size() {
    // Maximum batch size for RTX 3050 Ti
    let config = ml::trainers::dqn::DQNHyperparameters {
        batch_size: 230, // Max for 4GB GPU
        ..Default::default()
    };
    
    info!(batch_size = config.batch_size, gpu = "RTX 3050 Ti (4GB)", "Max batch size config");
    
    assert_eq!(config.batch_size, 230);
}

// ============================================================================
// NUMERICAL STABILITY TESTS
// ============================================================================

#[test]
fn test_early_stopping_very_small_losses() {
    // Test with losses near machine epsilon
    let epsilon = f64::EPSILON;
    let losses = vec![
        epsilon * 10.0,
        epsilon * 9.0,
        epsilon * 8.0,
        epsilon * 7.0,
    ];
    
    info!("Very small losses (near epsilon)");
    for (i, &loss) in losses.iter().enumerate() {
        info!(epoch = i, loss, "Epoch loss");
    }

    // Should still calculate improvements correctly
    let improvement = (losses[0] - losses[3]) / losses[0];
    info!(improvement_pct = improvement * 100.0, "Total improvement");
    
    assert!(improvement > 0.0, "Should detect improvement even for tiny losses");
}

#[test]
fn test_early_stopping_very_large_losses() {
    // Test with very large losses
    let losses = vec![1e10, 9e9, 8e9, 7e9];
    
    info!("Very large losses");
    for (i, &loss) in losses.iter().enumerate() {
        info!(epoch = i, loss, "Epoch loss");
    }

    // Should handle large numbers without overflow
    let improvement = (losses[0] - losses[3]) / losses[0] * 100.0;
    info!(improvement_pct = improvement, "Total improvement");
    
    assert!(improvement > 0.0 && improvement < 100.0);
}

#[test]
fn test_early_stopping_loss_precision() {
    // Test floating point precision in improvement calculation
    let loss1 = 1.0;
    let loss2 = 1.0 + 1e-15; // Tiny difference
    
    let improvement = ((loss1 - loss2).abs() / loss1 * 100.0).abs();
    
    info!(loss1, loss2, improvement, "Precision test");
    
    // Should handle near-identical values
    assert!(improvement < 1e-10, "Should recognize near-identical losses");
}

// ============================================================================
// BOUNDARY CONDITION TESTS
// ============================================================================

#[test]
fn test_early_stopping_at_min_epochs_boundary() {
    // Test behavior exactly at min_epochs threshold
    let min_epochs = 50;
    let epoch = 50;
    
    // At min_epochs, early stopping should be allowed
    assert!(epoch >= min_epochs, "Should allow early stopping at min_epochs");
    
    // One epoch before, should NOT be allowed
    let epoch_before = 49;
    assert!(epoch_before < min_epochs, "Should NOT allow stopping before min_epochs");
}

#[test]
fn test_early_stopping_at_window_boundary() {
    // Test plateau detection exactly at 2*window epochs
    let window = 10;
    let losses = vec![1.0; 20]; // Exactly 2*window epochs
    
    assert_eq!(losses.len(), window * 2);
    
    // Should be able to detect plateau
    let recent_avg = losses[window..].iter().sum::<f64>() / window as f64;
    let older_avg = losses[..window].iter().sum::<f64>() / window as f64;
    
    assert_eq!(recent_avg, older_avg);
    info!("Plateau detection at exact window boundary: success");
}

#[test]
fn test_early_stopping_one_epoch_before_window() {
    // Test with exactly 2*window - 1 epochs (cannot detect plateau)
    let window = 10;
    let losses = vec![1.0; 19]; // One less than 2*window
    
    assert_eq!(losses.len(), window * 2 - 1);
    
    // Cannot calculate plateau (not enough epochs)
    info!(epochs = losses.len(), need = window * 2, "Cannot detect plateau: insufficient epochs");
}
