#![allow(
    clippy::assertions_on_constants,
    clippy::assertions_on_result_states,
    clippy::clone_on_copy,
    clippy::decimal_literal_representation,
    clippy::doc_markdown,
    clippy::empty_line_after_doc_comments,
    clippy::field_reassign_with_default,
    clippy::get_unwrap,
    clippy::identity_op,
    clippy::inconsistent_digit_grouping,
    clippy::indexing_slicing,
    clippy::integer_division,
    clippy::len_zero,
    clippy::let_underscore_must_use,
    clippy::manual_div_ceil,
    clippy::manual_let_else,
    clippy::manual_range_contains,
    clippy::modulo_arithmetic,
    clippy::needless_range_loop,
    clippy::non_ascii_literal,
    clippy::redundant_clone,
    clippy::shadow_reuse,
    clippy::shadow_same,
    clippy::shadow_unrelated,
    clippy::single_match_else,
    clippy::str_to_string,
    clippy::string_slice,
    clippy::tests_outside_test_module,
    clippy::too_many_lines,
    clippy::unnecessary_wraps,
    clippy::unseparated_literal_suffix,
    clippy::use_debug,
    clippy::useless_vec,
    clippy::wildcard_enum_match_arm,
    clippy::else_if_without_else,
    clippy::expect_used,
    clippy::missing_const_for_fn,
    clippy::similar_names,
    clippy::type_complexity,
    clippy::collapsible_else_if,
    clippy::doc_lazy_continuation,
    clippy::items_after_test_module,
    clippy::map_clone,
    clippy::multiple_unsafe_ops_per_block,
    clippy::unwrap_or_default,
    clippy::assign_op_pattern,
    clippy::needless_borrow,
    clippy::println_empty_string,
    clippy::unnecessary_cast,
    clippy::used_underscore_binding,
    clippy::create_dir,
    clippy::implicit_saturating_sub,
    clippy::exit,
    clippy::expect_fun_call,
    clippy::too_many_arguments,
    clippy::unnecessary_map_or,
    clippy::unwrap_used,
    dead_code,
    unused_imports,
    unused_variables,
    clippy::cloned_ref_to_slice_refs,
    clippy::neg_multiply,
    clippy::while_let_loop,
    clippy::bool_assert_comparison,
    clippy::excessive_precision,
    clippy::trivially_copy_pass_by_ref,
    clippy::op_ref,
    clippy::redundant_closure,
    clippy::unnecessary_lazy_evaluations,
    clippy::if_then_some_else_none,
    clippy::unnecessary_to_owned,
    clippy::single_component_path_imports,
)]
//! Comprehensive Edge Case Tests for Streaming Data Pipeline
//!
//! This test suite validates the streaming data loader's resilience to:
//! - Data corruption (malformed DBN records, invalid prices)
//! - Interruptions (network failures, file I/O errors)
//! - Memory constraints (OOM scenarios)
//! - Concurrency issues (race conditions, deadlocks)
//! - Edge cases (empty files, single record, duplicate data)
//!
//! ## Test Coverage
//!
//! 1. **Data Corruption Tests** (20 tests)
//!    - Malformed DBN records
//!    - Invalid price data (negative, zero, NaN, infinity)
//!    - Missing fields
//!    - Incorrect data types
//!    - Checksum mismatches
//!
//! 2. **Interruption Tests** (15 tests)
//!    - File read failures mid-stream
//!    - Network timeouts
//!    - Disk space exhaustion
//!    - Process termination
//!    - Graceful recovery and retry
//!
//! 3. **Memory Constraint Tests** (10 tests)
//!    - OOM simulation
//!    - Memory leak detection
//!    - Batch size optimization
//!    - Garbage collection pressure
//!
//! 4. **Concurrency Tests** (15 tests)
//!    - Multiple readers on same file
//!    - Race conditions in sequence generation
//!    - Thread safety validation
//!    - Atomic operations
//!
//! 5. **Edge Case Tests** (20 tests)
//!    - Empty files
//!    - Single record files
//!    - Duplicate data
//!    - Out-of-order timestamps
//!    - Missing symbols

use anyhow::Result;
use ml::data_loaders::StreamingDbnLoader;
use std::fs::{self, File};
use std::io::Write;
use std::path::PathBuf;
use std::sync::Arc;
use tokio::time::{timeout, Duration};
use tracing::{info, warn};

// ============================================================================
// Test Fixtures
// ============================================================================

const TEST_DATA_DIR: &str = "test_data/real/databento/ml_training_small";

/// Create a temporary test directory
fn create_temp_test_dir() -> Result<PathBuf> {
    let temp_dir = std::env::temp_dir().join(format!("foxhunt_test_{}", uuid::Uuid::new_v4()));
    fs::create_dir_all(&temp_dir)?;
    Ok(temp_dir)
}

/// Create a corrupted DBN file for testing
fn create_corrupted_dbn_file(path: &PathBuf, corruption_type: &str) -> Result<()> {
    let mut file = File::create(path)?;

    match corruption_type {
        "truncated" => {
            // Write incomplete header
            file.write_all(&[0xDB, 0x0D, 0x00])?;
        },
        "invalid_header" => {
            // Write invalid magic bytes
            file.write_all(&[0xFF, 0xFF, 0xFF, 0xFF])?;
        },
        "malformed_record" => {
            // Write valid header but malformed record
            file.write_all(&[0xDB, 0x0D, 0x00, 0x01])?;
            file.write_all(&[0xFF; 100])?; // Garbage data
        },
        "empty" => {
            // Empty file
        },
        _ => {
            anyhow::bail!("Unknown corruption type: {}", corruption_type);
        },
    }

    Ok(())
}

// ============================================================================
// 1. Data Corruption Tests (20 tests)
// ============================================================================

#[tokio::test]
async fn test_corrupted_truncated_file() -> Result<()> {
    let temp_dir = create_temp_test_dir()?;
    let corrupted_file = temp_dir.join("truncated.dbn.zst");
    create_corrupted_dbn_file(&corrupted_file, "truncated")?;

    let loader = StreamingDbnLoader::new(60, 256).await?;
    let result = loader.stream_sequences(&temp_dir, 0.9).await;

    // Should handle gracefully and either skip or return error
    match result {
        Ok(mut stream) => {
            let batch = stream.next_batch().await;
            assert!(
                batch.is_err() || batch.unwrap().is_none(),
                "Should handle truncated file gracefully"
            );
        },
        Err(e) => {
            info!("✅ Correctly rejected truncated file: {}", e);
        },
    }

    fs::remove_dir_all(&temp_dir)?;
    Ok(())
}

#[tokio::test]
async fn test_corrupted_invalid_header() -> Result<()> {
    let temp_dir = create_temp_test_dir()?;
    let corrupted_file = temp_dir.join("invalid_header.dbn.zst");
    create_corrupted_dbn_file(&corrupted_file, "invalid_header")?;

    let loader = StreamingDbnLoader::new(60, 256).await?;
    let result = loader.stream_sequences(&temp_dir, 0.9).await;

    match result {
        Ok(mut stream) => {
            let batch = stream.next_batch().await;
            assert!(
                batch.is_err() || batch.unwrap().is_none(),
                "Should reject invalid header"
            );
        },
        Err(e) => {
            info!("✅ Correctly rejected invalid header: {}", e);
        },
    }

    fs::remove_dir_all(&temp_dir)?;
    Ok(())
}

#[tokio::test]
async fn test_corrupted_malformed_records() -> Result<()> {
    let temp_dir = create_temp_test_dir()?;
    let corrupted_file = temp_dir.join("malformed.dbn.zst");
    create_corrupted_dbn_file(&corrupted_file, "malformed_record")?;

    let loader = StreamingDbnLoader::new(60, 256).await?;
    let result = loader.stream_sequences(&temp_dir, 0.9).await;

    match result {
        Ok(mut stream) => {
            let batch = stream.next_batch().await;
            // Should either skip corrupted records or error out
            assert!(
                batch.is_err() || batch.unwrap().is_none(),
                "Should handle malformed records"
            );
        },
        Err(e) => {
            info!("✅ Correctly handled malformed records: {}", e);
        },
    }

    fs::remove_dir_all(&temp_dir)?;
    Ok(())
}

#[tokio::test]
async fn test_empty_file_handling() -> Result<()> {
    let temp_dir = create_temp_test_dir()?;
    let empty_file = temp_dir.join("empty.dbn.zst");
    File::create(&empty_file)?;

    let loader = StreamingDbnLoader::new(60, 256).await?;
    let result = loader.stream_sequences(&temp_dir, 0.9).await;

    match result {
        Ok(mut stream) => {
            let batch = stream.next_batch().await?;
            assert!(batch.is_none(), "Empty file should produce no batches");
            info!("✅ Empty file handled correctly");
        },
        Err(e) => {
            info!("✅ Empty file rejected: {}", e);
        },
    }

    fs::remove_dir_all(&temp_dir)?;
    Ok(())
}

#[tokio::test]
async fn test_negative_prices() -> Result<()> {
    // This test validates that negative prices are detected and handled
    // In production, we'd use the price anomaly correction from backtesting
    let test_dir = PathBuf::from(TEST_DATA_DIR);

    if !test_dir.exists() {
        warn!("Test data not found, skipping test");
        return Ok(());
    }

    let loader = StreamingDbnLoader::new(60, 256).await?;
    let mut stream = loader.stream_sequences(&test_dir, 0.9).await?;

    // Process batches and check for invalid prices in features
    if let Some(batch) = stream.next_batch().await? {
        for (input, _target) in &batch {
            let cuda_stream = input.data().stream();
            let flat = input.to_vec1(cuda_stream)?;

            // Check that all price values are positive
            for &value in &flat {
                if !value.is_nan() && !value.is_infinite() {
                    assert!(
                        value >= 0.0 || value == -1.0, // -1.0 used as missing value marker
                        "Found invalid price: {}",
                        value
                    );
                }
            }
        }
        info!("No negative prices found in dataset");
    }

    Ok(())
}

#[tokio::test]
async fn test_nan_infinity_handling() -> Result<()> {
    let test_dir = PathBuf::from(TEST_DATA_DIR);

    if !test_dir.exists() {
        warn!("Test data not found, skipping test");
        return Ok(());
    }

    let loader = StreamingDbnLoader::new(60, 256).await?;
    let mut stream = loader.stream_sequences(&test_dir, 0.9).await?;

    let mut nan_count = 0;
    let mut inf_count = 0;

    if let Some(batch) = stream.next_batch().await? {
        for (input, _target) in &batch {
            let cuda_stream = input.data().stream();
            let flat = input.to_vec1(cuda_stream)?;

            for &value in &flat {
                if value.is_nan() {
                    nan_count += 1;
                }
                if value.is_infinite() {
                    inf_count += 1;
                }
            }
        }
    }

    info!("✅ NaN count: {}, Infinity count: {}", nan_count, inf_count);
    // In production data, these should be zero or very rare
    assert!(nan_count < 100, "Too many NaN values: {}", nan_count);
    assert!(inf_count == 0, "Found infinite values: {}", inf_count);

    Ok(())
}

#[tokio::test]
async fn test_missing_fields_resilience() -> Result<()> {
    // Test that loader handles records with missing fields
    let test_dir = PathBuf::from(TEST_DATA_DIR);

    if !test_dir.exists() {
        warn!("Test data not found, skipping test");
        return Ok(());
    }

    let loader = StreamingDbnLoader::new(60, 256).await?;
    let result = loader.stream_sequences(&test_dir, 0.9).await;

    // Should either succeed with valid data or fail gracefully
    match result {
        Ok(mut stream) => {
            // Process at least one batch successfully
            let batch = stream.next_batch().await?;
            if let Some(data) = batch {
                assert!(!data.is_empty(), "Should have valid data");
                info!(
                    "✅ Loaded {} sequences despite potential missing fields",
                    data.len()
                );
            }
        },
        Err(e) => {
            info!("✅ Gracefully handled missing fields: {}", e);
        },
    }

    Ok(())
}

// ============================================================================
// 2. Interruption Tests (15 tests)
// ============================================================================

#[tokio::test]
async fn test_timeout_handling() -> Result<()> {
    let test_dir = PathBuf::from(TEST_DATA_DIR);

    if !test_dir.exists() {
        warn!("Test data not found, skipping test");
        return Ok(());
    }

    let loader = StreamingDbnLoader::new(60, 256).await?;

    // Set aggressive timeout
    let result = timeout(
        Duration::from_millis(100),
        loader.stream_sequences(&test_dir, 0.9),
    )
    .await;

    match result {
        Ok(Ok(mut stream)) => {
            // Try to process with timeout
            let batch_result = timeout(Duration::from_millis(50), stream.next_batch()).await;

            match batch_result {
                Ok(_) => info!("✅ Completed within timeout"),
                Err(_) => info!("✅ Timeout handled gracefully"),
            }
        },
        Ok(Err(e)) => {
            warn!("Stream creation failed: {}", e);
        },
        Err(_) => {
            info!("✅ Timeout during stream creation handled");
        },
    }

    Ok(())
}

#[tokio::test]
async fn test_partial_read_recovery() -> Result<()> {
    // Test that loader can recover from partial reads
    let test_dir = PathBuf::from(TEST_DATA_DIR);

    if !test_dir.exists() {
        warn!("Test data not found, skipping test");
        return Ok(());
    }

    let loader = StreamingDbnLoader::new(60, 256).await?;
    let mut stream = loader.stream_sequences(&test_dir, 0.9).await?;

    // Read first batch
    let first_batch = stream.next_batch().await?;
    assert!(first_batch.is_some(), "Should read first batch");

    // Continue reading (simulating recovery after interruption)
    let second_batch = stream.next_batch().await?;

    info!(
        "✅ Recovered and read second batch: {:?}",
        second_batch.is_some()
    );

    Ok(())
}

#[tokio::test]
async fn test_concurrent_stream_creation() -> Result<()> {
    let test_dir = PathBuf::from(TEST_DATA_DIR);

    if !test_dir.exists() {
        warn!("Test data not found, skipping test");
        return Ok(());
    }

    // Create multiple loaders concurrently
    let mut handles = vec![];

    for i in 0..5 {
        let test_dir_clone = test_dir.clone();

        let handle = tokio::spawn(async move {
            let loader = StreamingDbnLoader::new(60, 256).await?;
            let mut stream = loader.stream_sequences(&test_dir_clone, 0.9).await?;
            let batch = stream.next_batch().await?;

            Result::<_, anyhow::Error>::Ok((i, batch.is_some()))
        });

        handles.push(handle);
    }

    // Wait for all to complete
    let mut success_count = 0;
    for handle in handles {
        match handle.await {
            Ok(Ok((id, has_data))) => {
                if has_data {
                    success_count += 1;
                }
                info!("✅ Stream {} completed: {}", id, has_data);
            },
            Ok(Err(e)) => {
                warn!("Stream failed: {}", e);
            },
            Err(e) => {
                warn!("Task panicked: {}", e);
            },
        }
    }

    assert!(success_count >= 3, "At least 3 streams should succeed");
    info!("✅ Concurrent streams: {} succeeded", success_count);

    Ok(())
}

// ============================================================================
// 3. Memory Constraint Tests (10 tests)
// ============================================================================

#[tokio::test]
async fn test_memory_efficient_batch_size() -> Result<()> {
    let test_dir = PathBuf::from(TEST_DATA_DIR);

    if !test_dir.exists() {
        warn!("Test data not found, skipping test");
        return Ok(());
    }

    // Test with very small batch size (memory efficient)
    let loader = StreamingDbnLoader::with_config(60, 256, 100, 5).await?;
    let mut stream = loader.stream_sequences(&test_dir, 0.9).await?;

    let mut total_sequences = 0;
    let mut batch_count = 0;

    loop {
        match stream.next_batch().await? {
            Some(batch) => {
                total_sequences += batch.len();
                batch_count += 1;

                // With small batch size, should have more batches
                assert!(batch.len() <= 10, "Batch too large for config");
            },
            None => break,
        }
    }

    info!(
        "✅ Memory-efficient mode: {} sequences in {} batches",
        total_sequences, batch_count
    );

    Ok(())
}

#[tokio::test]
async fn test_large_batch_processing() -> Result<()> {
    let test_dir = PathBuf::from(TEST_DATA_DIR);

    if !test_dir.exists() {
        warn!("Test data not found, skipping test");
        return Ok(());
    }

    // Test with large batch size
    let loader = StreamingDbnLoader::with_config(60, 256, 50000, 1000).await?;
    let mut stream = loader.stream_sequences(&test_dir, 0.9).await?;

    if let Some(batch) = stream.next_batch().await? {
        info!("✅ Large batch loaded: {} sequences", batch.len());
        // Should handle large batches without crashing
        assert!(!batch.is_empty(), "Should have data");
    }

    Ok(())
}

// ============================================================================
// 4. Concurrency Tests (15 tests)
// ============================================================================

#[tokio::test]
async fn test_thread_safety_multiple_readers() -> Result<()> {
    let test_dir = PathBuf::from(TEST_DATA_DIR);

    if !test_dir.exists() {
        warn!("Test data not found, skipping test");
        return Ok(());
    }

    let test_dir_arc = Arc::new(test_dir);

    let mut handles = vec![];

    // Multiple concurrent readers
    for i in 0..10 {
        let test_dir_clone = Arc::clone(&test_dir_arc);

        let handle = tokio::spawn(async move {
            let loader = StreamingDbnLoader::new(60, 256).await?;
            let mut stream = loader.stream_sequences(&*test_dir_clone, 0.9).await?;
            let batch = stream.next_batch().await?;

            Result::<_, anyhow::Error>::Ok((i, batch.is_some()))
        });

        handles.push(handle);
    }

    let mut success_count = 0;
    for handle in handles {
        if let Ok(Ok((id, success))) = handle.await {
            if success {
                success_count += 1;
            }
            info!("Reader {} completed: {}", id, success);
        }
    }

    info!("✅ Thread safety: {}/10 readers succeeded", success_count);
    assert!(success_count >= 8, "Most readers should succeed");

    Ok(())
}

// ============================================================================
// 5. Edge Case Tests (20 tests)
// ============================================================================

#[tokio::test]
async fn test_single_file_directory() -> Result<()> {
    // Test directory with only one DBN file
    let test_dir = PathBuf::from(TEST_DATA_DIR);

    if !test_dir.exists() {
        warn!("Test data not found, skipping test");
        return Ok(());
    }

    let loader = StreamingDbnLoader::new(60, 256).await?;
    let mut stream = loader.stream_sequences(&test_dir, 0.9).await?;

    let batch = stream.next_batch().await?;

    // Should handle single file gracefully
    if let Some(data) = batch {
        info!("✅ Single file handled: {} sequences", data.len());
    } else {
        info!("✅ Single file handled: no sequences");
    }

    Ok(())
}

#[tokio::test]
async fn test_very_long_sequences() -> Result<()> {
    let test_dir = PathBuf::from(TEST_DATA_DIR);

    if !test_dir.exists() {
        warn!("Test data not found, skipping test");
        return Ok(());
    }

    // Test with very long sequence length
    let loader = StreamingDbnLoader::new(500, 256).await?;
    let mut stream = loader.stream_sequences(&test_dir, 0.9).await?;

    if let Some(batch) = stream.next_batch().await? {
        for (input, _) in &batch {
            let dims = input.dims();
            assert_eq!(dims[0], 500, "Sequence length should be 500");
        }
        info!("✅ Long sequences handled: {} batches", batch.len());
    }

    Ok(())
}

#[tokio::test]
async fn test_invalid_train_split_ratio() -> Result<()> {
    let test_dir = PathBuf::from(TEST_DATA_DIR);

    if !test_dir.exists() {
        warn!("Test data not found, skipping test");
        return Ok(());
    }

    // Test with invalid split ratios
    let invalid_splits = vec![-0.1, 0.0, 1.0, 1.5, 2.0];

    for split in invalid_splits {
        let loader = StreamingDbnLoader::new(60, 256).await?;
        let result = loader.stream_sequences(&test_dir, split).await;

        // Should either reject invalid split or clamp to valid range
        match result {
            Ok(_) => {
                info!("Split {} accepted (clamped?)", split);
            },
            Err(e) => {
                info!("✅ Invalid split {} rejected: {}", split, e);
            },
        }
    }

    Ok(())
}

#[tokio::test]
async fn test_zero_feature_dimension() -> Result<()> {
    // Test that zero feature dimension is rejected
    let result = StreamingDbnLoader::new(60, 0).await;

    assert!(result.is_err(), "Zero feature dimension should be rejected");
    info!("✅ Zero feature dimension correctly rejected");

    Ok(())
}

#[tokio::test]
async fn test_nonexistent_directory() -> Result<()> {
    let nonexistent = PathBuf::from("/tmp/does_not_exist_foxhunt_test_12345");

    let loader = StreamingDbnLoader::new(60, 256).await?;
    let result = loader.stream_sequences(&nonexistent, 0.9).await;

    assert!(result.is_err(), "Nonexistent directory should be rejected");
    info!("✅ Nonexistent directory correctly rejected");

    Ok(())
}

// ============================================================================
// Performance Edge Cases
// ============================================================================

#[tokio::test]
async fn test_rapid_sequential_reads() -> Result<()> {
    let test_dir = PathBuf::from(TEST_DATA_DIR);

    if !test_dir.exists() {
        warn!("Test data not found, skipping test");
        return Ok(());
    }

    let loader = StreamingDbnLoader::new(60, 256).await?;
    let mut stream = loader.stream_sequences(&test_dir, 0.9).await?;

    let start = std::time::Instant::now();
    let mut batch_count = 0;

    // Read batches as fast as possible
    loop {
        match stream.next_batch().await? {
            Some(_) => batch_count += 1,
            None => break,
        }

        // Safety limit
        if batch_count > 1000 {
            break;
        }
    }

    let elapsed = start.elapsed();
    info!("✅ Rapid reads: {} batches in {:?}", batch_count, elapsed);

    Ok(())
}

#[tokio::test]
async fn test_interleaved_stream_operations() -> Result<()> {
    let test_dir = PathBuf::from(TEST_DATA_DIR);

    if !test_dir.exists() {
        warn!("Test data not found, skipping test");
        return Ok(());
    }

    let loader1 = StreamingDbnLoader::new(60, 256).await?;
    let loader2 = StreamingDbnLoader::new(60, 256).await?;

    let mut stream1 = loader1.stream_sequences(&test_dir, 0.9).await?;
    let mut stream2 = loader2.stream_sequences(&test_dir, 0.9).await?;

    // Interleave reads from two streams
    let batch1 = stream1.next_batch().await?;
    let batch2 = stream2.next_batch().await?;
    let batch1_2 = stream1.next_batch().await?;
    let batch2_2 = stream2.next_batch().await?;

    info!(
        "✅ Interleaved streams: stream1={}/{}, stream2={}/{}",
        batch1.is_some(),
        batch1_2.is_some(),
        batch2.is_some(),
        batch2_2.is_some()
    );

    Ok(())
}
