//! Feature Extraction Pipeline Integration Tests
//!
//! Validates the 51-dimension feature extraction pipeline with both real DBN data
//! and synthetic OHLCV bars. Verifies:
//! - No NaN/Inf values in output
//! - Correct feature vector dimensions (51)
//! - Reasonable value ranges
//! - Streaming vs batch consistency

use ml::data_loaders::DbnSequenceLoader;
use ml::features::extraction::{extract_ml_features, FeatureExtractor, FeatureVector};
use ml::types::OHLCVBar;

use chrono::{Duration, Utc};
use std::path::Path;

// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------

/// Generate `count` synthetic OHLCV bars with a gentle upward trend and
/// realistic spreads, starting from a base price around 4500 (ES futures).
fn synthetic_bars(count: usize) -> Vec<OHLCVBar> {
    let base_price = 4500.0_f64;
    let now = Utc::now();

    (0..count)
        .map(|i| {
            let drift = i as f64 * 0.05; // small upward drift
            let noise = ((i as f64 * 0.7).sin()) * 2.0; // deterministic oscillation
            let close = base_price + drift + noise;
            let open = close - 0.3 + ((i as f64 * 0.3).cos()) * 0.5;
            let high = close.max(open) + 1.0 + (i as f64 * 0.2).sin().abs();
            let low = close.min(open) - 1.0 - (i as f64 * 0.4).cos().abs();
            let volume = 10_000.0 + (i as f64 * 0.9).sin() * 3_000.0;

            OHLCVBar {
                timestamp: now + Duration::minutes(i as i64),
                open,
                high,
                low,
                close,
                volume: volume.max(100.0), // floor at 100 to avoid zero-volume
            }
        })
        .collect()
}

// ---------------------------------------------------------------------------
// Test 1 -- Real DBN data through DbnSequenceLoader
// ---------------------------------------------------------------------------

#[tokio::test]
async fn test_feature_extraction_from_dbn_data() {
    // Resolve the test_data directory relative to the workspace root.
    // The DBN file lives at test_data/ES_FUT_180d.dbn; DbnSequenceLoader
    // scans a *directory* for .dbn files.
    let dbn_dir = Path::new(env!("CARGO_MANIFEST_DIR"))
        .parent()
        .expect("tests/ has a parent")
        .join("test_data");

    let dbn_file = dbn_dir.join("ES_FUT_180d.dbn");
    if !dbn_file.exists() {
        eprintln!(
            "SKIP: test_feature_extraction_from_dbn_data -- {} not found",
            dbn_file.display()
        );
        return;
    }

    // DbnSequenceLoader::new(seq_len, d_model) -- Wave A config expects d_model=26
    let loader_result = DbnSequenceLoader::new(60, 26).await;
    let mut loader = match loader_result {
        Ok(l) => l,
        Err(e) => {
            eprintln!(
                "SKIP: DbnSequenceLoader::new failed (acceptable in CI): {}",
                e
            );
            return;
        }
    };

    // load_sequences expects a directory containing .dbn files
    let result = loader.load_sequences(&dbn_dir, 0.8).await;
    match result {
        Ok((train, val)) => {
            // At least one of train/val should be non-empty
            assert!(
                !train.is_empty() || !val.is_empty(),
                "DbnSequenceLoader produced zero sequences from real DBN data"
            );
            eprintln!(
                "OK: DbnSequenceLoader produced {} train + {} val sequences",
                train.len(),
                val.len()
            );
        }
        Err(e) => {
            // Some CI environments may not support DBN decoding -- skip gracefully
            eprintln!(
                "SKIP: load_sequences failed (acceptable in CI): {}",
                e
            );
        }
    }
}

// ---------------------------------------------------------------------------
// Test 2 -- Synthetic bars: dimension check
// ---------------------------------------------------------------------------

#[test]
fn test_feature_extraction_synthetic_bars_dimensions() {
    let bars = synthetic_bars(200);

    let features = extract_ml_features(&bars).expect("extract_ml_features should succeed on 200 synthetic bars");

    // With 200 bars and warmup=50, we expect 150 feature vectors
    // (indices 50..199 inclusive)
    assert!(
        !features.is_empty(),
        "Feature extraction must produce at least one vector"
    );
    assert_eq!(
        features.len(),
        150,
        "Expected 150 feature vectors (200 bars - 50 warmup), got {}",
        features.len()
    );

    // Each feature vector must have exactly 51 dimensions
    for (idx, fv) in features.iter().enumerate() {
        assert_eq!(
            fv.len(),
            51,
            "Feature vector at index {} has {} dims, expected 51",
            idx,
            fv.len()
        );

        // No NaN or Inf
        for (dim, &val) in fv.iter().enumerate() {
            assert!(
                val.is_finite(),
                "NaN/Inf at feature vector {}, dimension {}: {}",
                idx,
                dim,
                val
            );
        }
    }
}

// ---------------------------------------------------------------------------
// Test 3 -- Value ranges
// ---------------------------------------------------------------------------

#[test]
fn test_feature_extraction_value_ranges() {
    let bars = synthetic_bars(200);

    let features = extract_ml_features(&bars).expect("extract_ml_features should succeed");

    let mut global_min = f64::INFINITY;
    let mut global_max = f64::NEG_INFINITY;

    for fv in &features {
        for &val in fv.iter() {
            assert!(val.is_finite(), "Non-finite value found: {}", val);
            if val < global_min {
                global_min = val;
            }
            if val > global_max {
                global_max = val;
            }
        }
    }

    // Feature extraction uses safe_clip / safe_normalize which bound most
    // features.  The volume normalization (safe_normalize(vol, 0, 1_000_000))
    // and other clipping functions keep values well within [-100, 100].
    assert!(
        global_min >= -100.0,
        "Global min {} is below -100.0",
        global_min
    );
    assert!(
        global_max <= 100.0,
        "Global max {} exceeds 100.0",
        global_max
    );

    eprintln!(
        "Value range check passed: min={:.6}, max={:.6}",
        global_min, global_max
    );
}

// ---------------------------------------------------------------------------
// Test 4 -- Streaming vs batch consistency
// ---------------------------------------------------------------------------

#[test]
fn test_streaming_feature_extractor_matches_batch() {
    let bars = synthetic_bars(200);
    const WARMUP_PERIOD: usize = 50;

    // ---- Batch path ----
    let batch_features =
        extract_ml_features(&bars).expect("batch extract_ml_features should succeed");

    // ---- Streaming path ----
    let mut extractor = FeatureExtractor::new();
    let mut streaming_features: Vec<FeatureVector> = Vec::new();

    for (i, bar) in bars.iter().enumerate() {
        extractor
            .update(bar)
            .expect("FeatureExtractor::update should succeed");

        if i >= WARMUP_PERIOD {
            let fv = extractor
                .extract_current_features()
                .expect("extract_current_features should succeed");
            streaming_features.push(fv);
        }
    }

    // Same number of vectors
    assert_eq!(
        batch_features.len(),
        streaming_features.len(),
        "Batch produced {} vectors but streaming produced {}",
        batch_features.len(),
        streaming_features.len()
    );

    // Element-wise equality within tolerance
    let tolerance = 1e-10;
    for (vec_idx, (batch_fv, stream_fv)) in batch_features
        .iter()
        .zip(streaming_features.iter())
        .enumerate()
    {
        for (dim, (&b, &s)) in batch_fv.iter().zip(stream_fv.iter()).enumerate() {
            let diff = (b - s).abs();
            assert!(
                diff <= tolerance,
                "Mismatch at vector {}, dim {}: batch={} stream={} diff={}",
                vec_idx,
                dim,
                b,
                s,
                diff
            );
        }
    }

    eprintln!(
        "Streaming vs batch consistency verified for {} vectors x 51 dims",
        batch_features.len()
    );
}
