#![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,
)]
//! Test DbnSequenceLoader produces correct 256-dimensional features
//!
//! Validates that the fixed DbnSequenceLoader correctly extracts and pads
//! features to exactly 256 dimensions for MAMBA-2 training.

use anyhow::Result;
// candle eliminated — test uses native APIs
use cudarc::driver::CudaContext;
use ml::data_loaders::DbnSequenceLoader;
use std::env;
use std::path::PathBuf;
use std::sync::Arc;
use tracing::info;
use tracing::warn;

/// Get test data directory path
fn get_test_data_dir() -> PathBuf {
    // Try CARGO_MANIFEST_DIR first (works in tests)
    if let Ok(manifest_dir) = env::var("CARGO_MANIFEST_DIR") {
        PathBuf::from(manifest_dir)
            .parent()
            .unwrap()
            .join("test_data/real/databento/ml_training_small")
    } else {
        // Fallback to relative path from project root
        PathBuf::from("test_data/real/databento/ml_training_small")
    }
}

#[tokio::test]
async fn test_feature_dimension_256() -> Result<()> {
    info!("Testing DbnSequenceLoader 256-dimensional features");

    let test_dir = get_test_data_dir();

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

    // Create loader with d_model=256
    let mut loader = DbnSequenceLoader::with_limits(60, 256, Some(10), 10).await?;
    info!("Created DbnSequenceLoader (seq_len=60, d_model=256, max=10, stride=10)");

    // Load sequences
    info!(test_dir = ?test_dir, "Loading sequences");
    let (train_data, val_data) = loader.load_sequences(&test_dir, 0.9).await?;

    let total_sequences = train_data.len() + val_data.len();
    info!(
        total_sequences,
        train = train_data.len(),
        val = val_data.len(),
        "Loaded sequences"
    );

    // Verify at least some data was loaded
    assert!(total_sequences > 0, "Should load at least some sequences");

    // Test 1: Verify input tensor dimensions
    info!("Test 1: Verifying input tensor dimensions");
    for (idx, (input, _target)) in train_data.iter().take(5).enumerate() {
        let input_dims = input.dims();

        info!(idx, input_dims = ?input_dims, "Sequence input shape");

        // Input should be [batch=1, seq_len=60, d_model=256]
        assert_eq!(
            input_dims.len(),
            3,
            "Input should be 3D (batch, seq_len, features), got {:?}",
            input_dims
        );
        assert_eq!(
            input_dims[0], 1,
            "Batch dimension should be 1, got {}",
            input_dims[0]
        );
        assert_eq!(
            input_dims[1], 60,
            "Sequence length should be 60, got {}",
            input_dims[1]
        );
        assert_eq!(
            input_dims[2], 256,
            "Feature dimension should be 256, got {}",
            input_dims[2]
        );
    }
    info!("All input tensors have correct shape [1, 60, 256]");

    // Test 2: Verify target tensor dimensions
    info!("Test 2: Verifying target tensor dimensions");
    for (idx, (_input, target)) in train_data.iter().take(5).enumerate() {
        let target_dims = target.dims();

        info!(idx, target_dims = ?target_dims, "Sequence target shape");

        // Target should be [batch=1, timesteps=1, d_model=256]
        assert_eq!(
            target_dims.len(),
            3,
            "Target should be 3D, got {:?}",
            target_dims
        );
        assert_eq!(
            target_dims[0], 1,
            "Target batch should be 1, got {}",
            target_dims[0]
        );
        assert_eq!(
            target_dims[1], 1,
            "Target timesteps should be 1, got {}",
            target_dims[1]
        );
        assert_eq!(
            target_dims[2], 256,
            "Target feature dim should be 256, got {}",
            target_dims[2]
        );
    }
    info!("All target tensors have correct shape [1, 1, 256]");

    // Test 3: Verify feature values are normalized (not all zeros/NaN)
    info!("Test 3: Verifying feature normalization");
    let (first_input, _) = &train_data[0];
    let stream = CudaContext::new(0)
        .and_then(|ctx| ctx.new_stream())
        .expect("CUDA required for readback");
    let values_f32 = first_input.to_host(&stream)?;
    let values: Vec<f64> = values_f32.iter().map(|&v| v as f64).collect();

    // Check for NaN values
    let nan_count = values.iter().filter(|v| v.is_nan()).count();
    assert_eq!(nan_count, 0, "Found {} NaN values in features", nan_count);
    info!("No NaN values detected");

    // Check for all-zero sequences (should have some variation)
    let non_zero_count = values.iter().filter(|v| v.abs() > 1e-6).count();
    let non_zero_ratio = non_zero_count as f64 / values.len() as f64;
    info!(
        non_zero_count,
        total = values.len(),
        non_zero_pct = non_zero_ratio * 100.0,
        "Non-zero values"
    );

    assert!(
        non_zero_ratio > 0.01,
        "Features appear to be all zeros (only {:.1}% non-zero)",
        non_zero_ratio * 100.0
    );

    // Check value range (normalized features should be roughly in [-5, 5] range)
    let min_val = values.iter().cloned().fold(f64::INFINITY, f64::min);
    let max_val = values.iter().cloned().fold(f64::NEG_INFINITY, f64::max);
    let mean = values.iter().sum::<f64>() / values.len() as f64;

    info!(min_val, max_val, mean, "Value range");

    // Normalized features should not have extreme outliers
    assert!(
        min_val > -100.0 && max_val < 100.0,
        "Feature values seem unnormalized: range [{:.2}, {:.2}]",
        min_val,
        max_val
    );

    info!("Features are properly normalized");

    // Test 4: Verify validation data has same properties
    info!("Test 4: Verifying validation data");
    if !val_data.is_empty() {
        let (val_input, val_target) = &val_data[0];

        assert_eq!(
            val_input.dims(),
            &[1, 60, 256],
            "Validation input should be [1, 60, 256]"
        );
        assert_eq!(
            val_target.dims(),
            &[1, 1, 256],
            "Validation target should be [1, 1, 256]"
        );

        info!("Validation data shapes correct");
        info!(val_count = val_data.len(), "Validation sequences verified");
    } else {
        warn!("No validation data (split ratio may be too high)");
    }

    info!(
        total_sequences,
        train = train_data.len(),
        val = val_data.len(),
        "ALL TESTS PASSED: feature_dim=256, input=[1,60,256], target=[1,1,256], normalization=valid"
    );

    Ok(())
}

#[tokio::test]
async fn test_extract_features_dimension() -> Result<()> {
    info!("Testing extract_features() returns 256 dimensions");

    let test_dir = get_test_data_dir();

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

    // Create loader
    let mut loader = DbnSequenceLoader::new(60, 256).await?;
    info!("Created DbnSequenceLoader");

    // Load sequences
    let (train_data, _) = loader.load_sequences(&test_dir, 0.9).await?;

    assert!(!train_data.is_empty(), "Should have training data");

    // Get first sequence and verify it was created with 256-dim features
    let (input, _) = &train_data[0];

    // Input is [1, 60, 256] where 256 is the feature dimension
    let feature_dim = input.dims()[2];

    info!(feature_dim, "Feature dimension from tensor");
    assert_eq!(
        feature_dim, 256,
        "Feature dimension should be 256, got {}",
        feature_dim
    );

    info!("extract_features() correctly produces 256-dimensional features");

    Ok(())
}

#[tokio::test]
async fn test_different_d_model_values() -> Result<()> {
    info!("Testing different d_model values (128, 256, 512)");

    let test_dir = get_test_data_dir();

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

    // Test different d_model values
    let d_models = vec![128, 256, 512];

    for d_model in d_models {
        info!(d_model, "Testing d_model");

        let mut loader = DbnSequenceLoader::with_limits(60, d_model, Some(5), 10).await?;
        let (train_data, _) = loader.load_sequences(&test_dir, 0.9).await?;

        if !train_data.is_empty() {
            let (input, target) = &train_data[0];

            // Verify input shape
            assert_eq!(
                input.dims()[2],
                d_model,
                "Input feature dim should be {}",
                d_model
            );

            // Verify target shape
            assert_eq!(
                target.dims()[2],
                d_model,
                "Target feature dim should be {}",
                d_model
            );

            info!(
                d_model,
                input_dims = ?input.dims(),
                target_dims = ?target.dims(),
                "d_model dimensions verified"
            );
        }
    }

    info!("All d_model values produce correct dimensions");

    Ok(())
}

#[tokio::test]
async fn test_sequence_temporal_ordering() -> Result<()> {
    info!("Testing temporal ordering of sequences");

    let test_dir = get_test_data_dir();

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

    // Create loader with stride=1 to get consecutive sequences
    let mut loader = DbnSequenceLoader::with_limits(10, 256, Some(3), 1).await?;
    let (train_data, _) = loader.load_sequences(&test_dir, 0.9).await?;

    if train_data.len() >= 2 {
        info!("Comparing consecutive sequences");

        let (seq1_input, _) = &train_data[0];
        let (seq2_input, _) = &train_data[1];

        // With stride=1, the second sequence should be a shifted version of the first
        // seq1: [t0, t1, t2, ..., t9]
        // seq2: [t1, t2, t3, ..., t10]

        let stream = CudaContext::new(0)
            .and_then(|ctx| ctx.new_stream())
            .expect("CUDA required for readback");

        // Download both to host for comparison
        let seq1_host = seq1_input.to_host(&stream)?;
        let seq2_host = seq2_input.to_host(&stream)?;

        // seq1_input shape: [1, 10, 256], seq2_input shape: [1, 10, 256]
        let d_model = seq1_input.shape()[2]; // 256

        // Extract last 9 timesteps from seq1 and first 9 from seq2
        // seq1[0, 1..10, :] vs seq2[0, 0..9, :]
        let mut max_diff: f64 = 0.0;
        for t in 0..9 {
            for f in 0..d_model {
                let seq1_val = seq1_host[(t + 1) * d_model + f] as f64;
                let seq2_val = seq2_host[t * d_model + f] as f64;
                let diff = (seq1_val - seq2_val).abs();
                if diff > max_diff {
                    max_diff = diff;
                }
            }
        }

        info!(max_diff, "Max difference between overlapping windows");

        assert!(
            max_diff < 1e-6,
            "Consecutive sequences should overlap with stride=1, max_diff={}",
            max_diff
        );
    }

    info!("Temporal ordering verified");

    Ok(())
}

#[tokio::test]
async fn test_batch_processing() -> Result<()> {
    info!("Testing batch processing with multiple sequences");

    let test_dir = get_test_data_dir();

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

    // Load multiple sequences
    let mut loader = DbnSequenceLoader::with_limits(60, 256, Some(100), 10).await?;
    let (train_data, val_data) = loader.load_sequences(&test_dir, 0.8).await?;

    let total = train_data.len() + val_data.len();
    info!(total, "Loaded sequences");

    // Verify all sequences have consistent dimensions
    let mut valid_count = 0;
    for (input, target) in train_data.iter().chain(val_data.iter()) {
        if input.dims() == &[1, 60, 256] && target.dims() == &[1, 1, 256] {
            valid_count += 1;
        }
    }

    info!(valid_count, total, "Sequences with correct dimensions");
    assert_eq!(
        valid_count, total,
        "All sequences should have correct dimensions"
    );

    info!("Batch processing verified");

    Ok(())
}
