//! DBN-based Repository Implementation
//!
//! This module provides MarketDataRepository implementation that loads data from DBN files
//! instead of a database. This enables backtesting with real historical market data.

use anyhow::{Context, Result};
use async_trait::async_trait;
use chrono::{DateTime, Timelike};
use rust_decimal::prelude::ToPrimitive;
use std::collections::HashMap;
use std::sync::Arc;
use tracing::{debug, info, warn};

use crate::dbn_data_source::DbnDataSource;
use crate::repositories::MarketDataRepository;
use crate::strategy_engine::MarketData;

/// MarketDataRepository implementation using DBN files
///
/// This repository loads historical market data from DBN (Databento Binary) files
/// instead of a database, enabling backtesting with production-quality data.
///
/// ## Features
///
/// - Direct DBN file loading via DbnDataSource
/// - Zero-copy parsing with SIMD optimizations
/// - Support for multi-symbol backtests
/// - Configurable file paths per symbol
///
/// ## Usage
///
/// ```rust,no_run
/// use backtesting_service::dbn_repository::DbnMarketDataRepository;
/// use std::collections::HashMap;
///
/// # async fn example() -> anyhow::Result<()> {
/// let mut file_mapping = HashMap::new();
/// file_mapping.insert("ES.FUT".to_string(),
///     "test_data/real/databento/ES.FUT_ohlcv-1m_2024-01-02.dbn".to_string());
///
/// let repo = DbnMarketDataRepository::new(file_mapping).await?;
///
/// // Use in backtesting
/// let symbols = vec!["ES.FUT".to_string()];
/// let data = repo.load_historical_data(&symbols, start_time, end_time).await?;
/// # Ok(())
/// # }
/// ```
pub struct DbnMarketDataRepository {
    /// DBN data source
    data_source: Arc<DbnDataSource>,
    /// Symbol mapping for test compatibility (e.g., BTC/USD -> ES.FUT)
    /// Maps requested symbol to actual data symbol
    symbol_mappings: HashMap<String, String>,
}

impl DbnMarketDataRepository {
    /// Create new DBN-based market data repository
    ///
    /// # Arguments
    ///
    /// * `file_mapping` - Map of symbol to DBN file path
    ///
    /// # Returns
    ///
    /// Configured repository ready for backtesting
    pub async fn new(file_mapping: HashMap<String, String>) -> Result<Self> {
        Self::new_with_mappings(file_mapping, HashMap::new()).await
    }

    /// Create new DBN-based market data repository with symbol mappings
    ///
    /// # Arguments
    ///
    /// * `file_mapping` - Map of symbol to DBN file path
    /// * `symbol_mappings` - Map of requested symbol to actual data symbol
    ///   Example: {"BTC/USD" -> "ES.FUT", "ETH/USD" -> "ES.FUT"}
    ///
    /// # Returns
    ///
    /// Configured repository ready for backtesting with symbol mapping support
    pub async fn new_with_mappings(
        file_mapping: HashMap<String, String>,
        symbol_mappings: HashMap<String, String>,
    ) -> Result<Self> {
        let data_source = Arc::new(
            DbnDataSource::new(file_mapping)
                .await
                .context("Failed to create DBN data source")?,
        );

        if !symbol_mappings.is_empty() {
            info!(
                "Created DBN market data repository with {} symbols and {} symbol mappings",
                data_source.available_symbols().len(),
                symbol_mappings.len()
            );
            for (from, to) in &symbol_mappings {
                debug!("  Symbol mapping: {} -> {}", from, to);
            }
        } else {
            info!(
                "Created DBN market data repository with {} symbols",
                data_source.available_symbols().len()
            );
        }

        Ok(Self {
            data_source,
            symbol_mappings,
        })
    }

    /// Create repository with existing data source
    pub fn with_data_source(data_source: Arc<DbnDataSource>) -> Self {
        Self {
            data_source,
            symbol_mappings: HashMap::new(),
        }
    }

    /// Get available symbols in this repository
    pub fn available_symbols(&self) -> Vec<String> {
        self.data_source.available_symbols()
    }

    /// Load market data by precise time range
    ///
    /// More efficient than load_historical_data() when you need precise
    /// time filtering with DateTime objects.
    ///
    /// # Arguments
    ///
    /// * `symbols` - List of symbols to load
    /// * `start` - Start timestamp
    /// * `end` - End timestamp
    ///
    /// # Returns
    ///
    /// Vector of market data events sorted by timestamp
    ///
    /// # Performance
    ///
    /// Target: <10ms for typical test scenarios (~400 bars)
    pub async fn load_by_time_range(
        &self,
        symbols: &[String],
        start: DateTime<chrono::Utc>,
        end: DateTime<chrono::Utc>,
    ) -> Result<Vec<MarketData>> {
        debug!(
            "Loading DBN data by time range for {} symbols ({} to {})",
            symbols.len(),
            start,
            end
        );

        // Convert DateTime to nanosecond timestamps
        let start_nanos = start
            .timestamp_nanos_opt()
            .ok_or_else(|| anyhow::anyhow!("Invalid start timestamp"))?;
        let end_nanos = end
            .timestamp_nanos_opt()
            .ok_or_else(|| anyhow::anyhow!("Invalid end timestamp"))?;

        // Reuse existing load_historical_data implementation
        self.load_historical_data(symbols, start_nanos, end_nanos)
            .await
    }

    /// Load market data with volume filtering
    ///
    /// Filters for high-liquidity bars to focus on tradeable periods.
    ///
    /// # Arguments
    ///
    /// * `symbols` - List of symbols to load
    /// * `min_volume` - Minimum volume threshold
    /// * `start_time` - Start timestamp in nanoseconds
    /// * `end_time` - End timestamp in nanoseconds
    ///
    /// # Returns
    ///
    /// Vector of market data events with volume >= min_volume
    pub async fn load_with_volume_filter(
        &self,
        symbols: &[String],
        min_volume: rust_decimal::Decimal,
        start_time: i64,
        end_time: i64,
    ) -> Result<Vec<MarketData>> {
        let all_data = self
            .load_historical_data(symbols, start_time, end_time)
            .await?;

        let filtered: Vec<MarketData> = all_data
            .into_iter()
            .filter(|bar| bar.volume >= min_volume)
            .collect();

        info!(
            "Volume filter applied: {} bars with volume >= {}",
            filtered.len(),
            min_volume
        );

        Ok(filtered)
    }

    /// Load regime-specific sample data
    ///
    /// Loads data samples that match specific market regime characteristics.
    /// Useful for testing regime detection and adaptive strategies.
    ///
    /// # Arguments
    ///
    /// * `regime_type` - Type of regime to sample ("trending", "ranging", "volatile", "stable")
    /// * `count` - Number of samples to return
    /// * `symbols` - Symbols to load from
    ///
    /// # Returns
    ///
    /// Vector of market data samples from the specified regime
    ///
    /// # Note
    ///
    /// This is a simplified implementation that uses heuristics.
    /// For production, integrate with regime detection models.
    pub async fn load_regime_samples(
        &self,
        regime_type: &str,
        count: usize,
        symbols: &[String],
    ) -> Result<Vec<MarketData>> {
        // Load all available data
        let all_bars = self.data_source.load_multi_symbol_bars(symbols).await?;

        // Simple heuristic-based filtering (can be enhanced with ML models)
        let mut samples: Vec<MarketData> = match regime_type.to_lowercase().as_str() {
            "trending" => {
                // High price movement, consistent direction
                all_bars
                    .into_iter()
                    .filter(|bar| {
                        let range = bar.high - bar.low;
                        let avg_price = (bar.high + bar.low) / rust_decimal::Decimal::new(2, 0);
                        let range_pct = range / avg_price;
                        range_pct > rust_decimal::Decimal::new(5, 3) // >0.5% range
                    })
                    .collect()
            },
            "ranging" | "sideways" => {
                // Low price movement, narrow range
                all_bars
                    .into_iter()
                    .filter(|bar| {
                        let range = bar.high - bar.low;
                        let avg_price = (bar.high + bar.low) / rust_decimal::Decimal::new(2, 0);
                        let range_pct = range / avg_price;
                        range_pct < rust_decimal::Decimal::new(2, 3) // <0.2% range
                    })
                    .collect()
            },
            "volatile" => {
                // High volume and wide ranges
                all_bars
                    .into_iter()
                    .filter(|bar| {
                        let range = bar.high - bar.low;
                        let avg_price = (bar.high + bar.low) / rust_decimal::Decimal::new(2, 0);
                        let range_pct = range / avg_price;
                        range_pct > rust_decimal::Decimal::new(8, 3) // >0.8% range
                            && bar.volume > rust_decimal::Decimal::new(100, 0)
                    })
                    .collect()
            },
            "stable" => {
                // Low volatility, consistent prices
                all_bars
                    .into_iter()
                    .filter(|bar| {
                        let range = bar.high - bar.low;
                        let avg_price = (bar.high + bar.low) / rust_decimal::Decimal::new(2, 0);
                        let range_pct = range / avg_price;
                        range_pct < rust_decimal::Decimal::new(15, 4) // <0.15% range
                    })
                    .collect()
            },
            _ => {
                return Err(anyhow::anyhow!(
                    "Unknown regime type: {}. Valid: trending, ranging, volatile, stable",
                    regime_type
                ));
            },
        };

        // Limit to requested count
        samples.truncate(count);

        info!(
            "Loaded {} regime samples (regime: {}, requested: {})",
            samples.len(),
            regime_type,
            count
        );

        Ok(samples)
    }

    /// Get date range for a specific symbol
    ///
    /// Returns the first and last available timestamps for the symbol.
    ///
    /// # Arguments
    ///
    /// * `symbol` - Symbol to query
    ///
    /// # Returns
    ///
    /// Tuple of (first_timestamp, last_timestamp) if data exists
    pub async fn get_date_range(
        &self,
        symbol: &str,
    ) -> Result<(DateTime<chrono::Utc>, DateTime<chrono::Utc>)> {
        // Load all bars for symbol
        let bars = self.data_source.load_ohlcv_bars(symbol).await?;

        if bars.is_empty() {
            return Err(anyhow::anyhow!("No data found for symbol: {}", symbol));
        }

        // SAFETY: bars is non-empty (validated by is_empty check above)
        let first = bars
            .first()
            .ok_or_else(|| anyhow::anyhow!("INVARIANT violated: bars is empty after check"))?
            .timestamp;
        let last = bars
            .last()
            .ok_or_else(|| anyhow::anyhow!("INVARIANT violated: bars is empty after check"))?
            .timestamp;

        debug!("Date range for {}: {} to {}", symbol, first, last);

        Ok((first, last))
    }

    /// Resample bars to a different timeframe
    ///
    /// Aggregates minute bars into larger timeframes (5m, 15m, 1h, etc.)
    ///
    /// # Arguments
    ///
    /// * `bars` - Input bars (typically 1-minute)
    /// * `target_minutes` - Target timeframe in minutes (5, 15, 60, etc.)
    ///
    /// # Returns
    ///
    /// Resampled bars at the target timeframe
    pub fn resample_bars(
        &self,
        bars: &[MarketData],
        target_minutes: u32,
    ) -> Result<Vec<MarketData>> {
        if bars.is_empty() {
            return Ok(Vec::new());
        }

        let mut resampled = Vec::new();
        let mut current_bucket: Option<Vec<MarketData>> = None;

        for bar in bars {
            let bucket_start = bar
                .timestamp
                .with_minute((bar.timestamp.minute() / target_minutes) * target_minutes)
                .ok_or_else(|| anyhow::anyhow!("Invalid minute in timestamp resampling"))?
                .with_second(0)
                .ok_or_else(|| anyhow::anyhow!("Invalid second in timestamp resampling"))?
                .with_nanosecond(0)
                .ok_or_else(|| anyhow::anyhow!("Invalid nanosecond in timestamp resampling"))?;

            // Start new bucket or add to existing
            match &mut current_bucket {
                None => {
                    current_bucket = Some(vec![bar.clone()]);
                },
                Some(bucket) => {
                    #[allow(clippy::indexing_slicing)] // bucket guaranteed non-empty (created with 1 element on line 378)
                    let first_bar = &bucket[0];
                    let first_bucket_start = first_bar
                        .timestamp
                        .with_minute(
                            (first_bar.timestamp.minute() / target_minutes) * target_minutes,
                        )
                        .ok_or_else(|| anyhow::anyhow!("Invalid minute in timestamp resampling"))?
                        .with_second(0)
                        .ok_or_else(|| anyhow::anyhow!("Invalid second in timestamp resampling"))?
                        .with_nanosecond(0)
                        .ok_or_else(|| anyhow::anyhow!("Invalid nanosecond in timestamp resampling"))?;

                    if bucket_start == first_bucket_start {
                        // Same bucket
                        bucket.push(bar.clone());
                    } else {
                        // New bucket - aggregate previous
                        if let Some(aggregated) = Self::aggregate_bucket(bucket)? {
                            resampled.push(aggregated);
                        }
                        current_bucket = Some(vec![bar.clone()]);
                    }
                },
            }
        }

        // Aggregate final bucket
        if let Some(bucket) = current_bucket {
            if let Some(aggregated) = Self::aggregate_bucket(&bucket)? {
                resampled.push(aggregated);
            }
        }

        info!(
            "Resampled {} bars to {} bars ({}m timeframe)",
            bars.len(),
            resampled.len(),
            target_minutes
        );

        Ok(resampled)
    }

    /// Aggregate a bucket of bars into a single bar
    fn aggregate_bucket(bucket: &[MarketData]) -> Result<Option<MarketData>> {
        if bucket.is_empty() {
            return Ok(None);
        }

        #[allow(clippy::indexing_slicing)] // Bounds checked above: !is_empty()
        let first = &bucket[0];
        // SAFETY: bucket is non-empty (validated by is_empty check above)
        let last = bucket
            .last()
            .ok_or_else(|| anyhow::anyhow!("INVARIANT violated: bucket is empty after check"))?;

        // Calculate OHLCV
        let open = first.open;
        let close = last.close;
        let high = bucket.iter().map(|b| b.high).max().unwrap_or(first.high);
        let low = bucket.iter().map(|b| b.low).min().unwrap_or(first.low);
        let volume: rust_decimal::Decimal = bucket.iter().map(|b| b.volume).sum();

        Ok(Some(MarketData {
            symbol: first.symbol.clone(),
            timestamp: first.timestamp,
            open,
            high,
            low,
            close,
            volume,
        }))
    }

    /// Calculate rolling statistics over a window
    ///
    /// # Arguments
    ///
    /// * `bars` - Input bars
    /// * `window_size` - Number of bars in rolling window
    ///
    /// # Returns
    ///
    /// Vector of (mean, std_dev, min, max) for each window
    pub fn calculate_rolling_stats(
        &self,
        bars: &[MarketData],
        window_size: usize,
    ) -> Vec<(f64, f64, f64, f64)> {
        if bars.len() < window_size {
            return Vec::new();
        }

        let mut stats = Vec::new();

        for i in 0..=(bars.len() - window_size) {
            let window = &bars[i..i + window_size];

            // Extract close prices
            let closes: Vec<f64> = window.iter().filter_map(|b| b.close.to_f64()).collect();

            if closes.is_empty() {
                continue;
            }

            // Calculate statistics
            let mean = closes.iter().sum::<f64>() / closes.len() as f64;
            let variance =
                closes.iter().map(|x| (x - mean).powi(2)).sum::<f64>() / closes.len() as f64;
            let std_dev = variance.sqrt();
            let min = closes.iter().cloned().fold(f64::INFINITY, f64::min);
            let max = closes.iter().cloned().fold(f64::NEG_INFINITY, f64::max);

            stats.push((mean, std_dev, min, max));
        }

        stats
    }

    /// Generate summary statistics for loaded data
    ///
    /// # Arguments
    ///
    /// * `bars` - Input bars
    ///
    /// # Returns
    ///
    /// HashMap of statistic name to value
    pub fn generate_summary_stats(&self, bars: &[MarketData]) -> HashMap<String, f64> {
        let mut stats = HashMap::new();

        if bars.is_empty() {
            return stats;
        }

        // Basic counts
        stats.insert("count".to_string(), bars.len() as f64);

        // Price statistics
        let closes: Vec<f64> = bars.iter().filter_map(|b| b.close.to_f64()).collect();

        if !closes.is_empty() {
            let mean = closes.iter().sum::<f64>() / closes.len() as f64;
            let variance =
                closes.iter().map(|x| (x - mean).powi(2)).sum::<f64>() / closes.len() as f64;
            let std_dev = variance.sqrt();

            stats.insert("mean_close".to_string(), mean);
            stats.insert("std_close".to_string(), std_dev);
            stats.insert(
                "min_close".to_string(),
                closes.iter().cloned().fold(f64::INFINITY, f64::min),
            );
            stats.insert(
                "max_close".to_string(),
                closes.iter().cloned().fold(f64::NEG_INFINITY, f64::max),
            );
        }

        // Volume statistics
        let volumes: Vec<f64> = bars.iter().filter_map(|b| b.volume.to_f64()).collect();

        if !volumes.is_empty() {
            let mean_vol = volumes.iter().sum::<f64>() / volumes.len() as f64;
            stats.insert("mean_volume".to_string(), mean_vol);
            stats.insert("total_volume".to_string(), volumes.iter().sum::<f64>());
        }

        stats
    }
}

#[async_trait]
impl MarketDataRepository for DbnMarketDataRepository {
    /// Load historical market data from DBN files
    ///
    /// Loads data for all requested symbols and filters by time range.
    /// Applies symbol mapping if configured (e.g., BTC/USD -> ES.FUT).
    ///
    /// # Arguments
    ///
    /// * `symbols` - List of symbols to load
    /// * `start_time` - Start timestamp in nanoseconds since Unix epoch
    /// * `end_time` - End timestamp in nanoseconds since Unix epoch
    ///
    /// # Returns
    ///
    /// Vector of market data events sorted by timestamp
    async fn load_historical_data(
        &self,
        symbols: &[String],
        start_time: i64,
        end_time: i64,
    ) -> Result<Vec<MarketData>> {
        debug!(
            "Loading DBN data for {} symbols (range: {} to {})",
            symbols.len(),
            start_time,
            end_time
        );

        // Apply symbol mappings (e.g., BTC/USD -> ES.FUT for test compatibility)
        let mut mapped_symbols = Vec::new();
        let mut reverse_mapping: HashMap<String, Vec<String>> = HashMap::new();

        for symbol in symbols {
            if let Some(mapped) = self.symbol_mappings.get(symbol) {
                debug!("  Applying symbol mapping: {} -> {}", symbol, mapped);
                mapped_symbols.push(mapped.clone());
                reverse_mapping
                    .entry(mapped.clone())
                    .or_default()
                    .push(symbol.clone());
            } else {
                mapped_symbols.push(symbol.clone());
            }
        }

        // Remove duplicates from mapped symbols
        mapped_symbols.sort();
        mapped_symbols.dedup();

        // Convert nanosecond timestamps to DateTime
        let start_dt = DateTime::from_timestamp(start_time / 1_000_000_000, 0)
            .ok_or_else(|| anyhow::anyhow!("Invalid start timestamp"))?;
        let end_dt = DateTime::from_timestamp(end_time / 1_000_000_000, 0)
            .ok_or_else(|| anyhow::anyhow!("Invalid end timestamp"))?;

        // Load data for mapped symbols
        let all_data = self
            .data_source
            .load_multi_symbol_bars(&mapped_symbols)
            .await?;

        // Filter by time range and restore original symbol names
        let mut filtered: Vec<MarketData> = all_data
            .into_iter()
            .filter(|bar| bar.timestamp >= start_dt && bar.timestamp <= end_dt)
            .flat_map(|bar| {
                // If this data symbol was mapped from multiple request symbols,
                // create separate bars for each (for multi-symbol backtests)
                if let Some(original_symbols) = reverse_mapping.get(&bar.symbol) {
                    original_symbols
                        .iter()
                        .map(|orig_sym| {
                            let mut bar_copy = bar.clone();
                            bar_copy.symbol = orig_sym.clone();
                            bar_copy
                        })
                        .collect()
                } else {
                    vec![bar]
                }
            })
            .collect();

        // Sort by timestamp across all symbols
        filtered.sort_by(|a, b| a.timestamp.cmp(&b.timestamp));

        info!(
            "Loaded {} bars from DBN files (symbols: {:?}, mapped: {:?}, range: {} to {})",
            filtered.len(),
            symbols,
            mapped_symbols,
            start_dt,
            end_dt
        );

        if !self.symbol_mappings.is_empty() {
            warn!(
                "Symbol mapping active: Using {} data for {} requested symbols",
                mapped_symbols.join(", "),
                symbols.len()
            );
        }

        Ok(filtered)
    }
}

#[cfg(test)]
#[allow(clippy::inconsistent_digit_grouping)]
mod tests {
    use super::*;
    use chrono::{Datelike, TimeZone, Utc};
    use rust_decimal::Decimal;

    fn get_test_file_path() -> String {
        // Get absolute path to test file (workspace root + relative path)
        let current_dir = std::env::current_dir().expect("INVARIANT: Current directory should be accessible");
        let workspace_root = current_dir
            .ancestors()
            .find(|p| p.join("Cargo.toml").exists() && p.join("test_data").exists())
            .expect("Could not find workspace root");
        workspace_root
            .join("test_data/real/databento/ES.FUT_ohlcv-1m_2024-01-02.dbn")
            .to_string_lossy()
            .to_string()
    }

    #[tokio::test]
    async fn test_dbn_repository_creation() {
        let mut file_mapping = HashMap::new();
        file_mapping.insert("ES.FUT".to_string(), get_test_file_path());

        let repo = DbnMarketDataRepository::new(file_mapping).await;
        assert!(repo.is_ok());

        let repository = repo.unwrap();
        assert_eq!(repository.available_symbols().len(), 1);
    }

    #[tokio::test]
    #[ignore] // Requires local DBN test data (test_data/real/databento/)
    async fn test_load_by_time_range() {
        let mut file_mapping = HashMap::new();
        file_mapping.insert("ES.FUT".to_string(), get_test_file_path());

        let repo = DbnMarketDataRepository::new(file_mapping).await.unwrap();

        let start = Utc.with_ymd_and_hms(2024, 1, 2, 0, 0, 0).unwrap();
        let end = Utc.with_ymd_and_hms(2024, 1, 2, 1, 0, 0).unwrap();

        let symbols = vec!["ES.FUT".to_string()];
        let result = repo.load_by_time_range(&symbols, start, end).await;

        assert!(result.is_ok());
        let bars = result.unwrap();
        assert!(!bars.is_empty(), "Should load bars in time range");

        // Verify all bars are within range
        for bar in &bars {
            assert!(bar.timestamp >= start && bar.timestamp <= end);
        }
    }

    #[tokio::test]
    #[ignore] // Requires local DBN test data (test_data/real/databento/)
    async fn test_load_with_volume_filter() {
        let mut file_mapping = HashMap::new();
        file_mapping.insert("ES.FUT".to_string(), get_test_file_path());

        let repo = DbnMarketDataRepository::new(file_mapping).await.unwrap();

        let start_time = 1704153600_000_000_000i64;
        let end_time = 1704240000_000_000_000i64;
        let min_volume = Decimal::new(50, 0);

        let symbols = vec!["ES.FUT".to_string()];
        let result = repo
            .load_with_volume_filter(&symbols, min_volume, start_time, end_time)
            .await;

        assert!(result.is_ok());
        let bars = result.unwrap();

        // Verify all bars meet volume threshold
        for bar in &bars {
            assert!(bar.volume >= min_volume, "Bar volume below threshold");
        }
    }

    #[tokio::test]
    #[ignore] // Requires local DBN test data (test_data/real/databento/)
    async fn test_load_regime_samples_trending() {
        let mut file_mapping = HashMap::new();
        file_mapping.insert("ES.FUT".to_string(), get_test_file_path());

        let repo = DbnMarketDataRepository::new(file_mapping).await.unwrap();

        let symbols = vec!["ES.FUT".to_string()];
        let result = repo.load_regime_samples("trending", 10, &symbols).await;

        assert!(result.is_ok());
        let samples = result.unwrap();

        // Should have up to 10 samples
        assert!(samples.len() <= 10);

        // Verify samples match trending characteristics (>0.5% range)
        for sample in &samples {
            let range = sample.high - sample.low;
            let avg_price = (sample.high + sample.low) / Decimal::new(2, 0);
            let range_pct = range / avg_price;
            assert!(range_pct > Decimal::new(5, 3), "Sample not trending");
        }
    }

    #[tokio::test]
    #[ignore] // Requires local DBN test data (test_data/real/databento/)
    async fn test_load_regime_samples_ranging() {
        let mut file_mapping = HashMap::new();
        file_mapping.insert("ES.FUT".to_string(), get_test_file_path());

        let repo = DbnMarketDataRepository::new(file_mapping).await.unwrap();

        let symbols = vec!["ES.FUT".to_string()];
        let result = repo.load_regime_samples("ranging", 10, &symbols).await;

        assert!(result.is_ok());
        let samples = result.unwrap();

        // Verify samples match ranging characteristics (<0.2% range)
        for sample in &samples {
            let range = sample.high - sample.low;
            let avg_price = (sample.high + sample.low) / Decimal::new(2, 0);
            let range_pct = range / avg_price;
            assert!(range_pct < Decimal::new(2, 3), "Sample not ranging");
        }
    }

    #[tokio::test]
    #[ignore] // Requires local DBN test data (test_data/real/databento/)
    async fn test_load_regime_samples_invalid() {
        let mut file_mapping = HashMap::new();
        file_mapping.insert("ES.FUT".to_string(), get_test_file_path());

        let repo = DbnMarketDataRepository::new(file_mapping).await.unwrap();

        let symbols = vec!["ES.FUT".to_string()];
        let result = repo
            .load_regime_samples("invalid_regime", 10, &symbols)
            .await;

        assert!(result.is_err());
        assert!(result
            .unwrap_err()
            .to_string()
            .contains("Unknown regime type"));
    }

    #[tokio::test]
    #[ignore] // Requires local DBN test data (test_data/real/databento/)
    async fn test_get_date_range() {
        let mut file_mapping = HashMap::new();
        file_mapping.insert("ES.FUT".to_string(), get_test_file_path());

        let repo = DbnMarketDataRepository::new(file_mapping).await.unwrap();

        let result = repo.get_date_range("ES.FUT").await;
        assert!(result.is_ok());

        let (first, last) = result.unwrap();
        assert!(first < last, "First timestamp should be before last");

        // Should be within 2024-01-02
        assert_eq!(first.year(), 2024);
        assert_eq!(first.month(), 1);
        assert_eq!(first.day(), 2);
    }

    #[tokio::test]
    #[ignore] // Requires local DBN test data (test_data/real/databento/)
    async fn test_resample_bars() {
        let mut file_mapping = HashMap::new();
        file_mapping.insert("ES.FUT".to_string(), get_test_file_path());

        let repo = DbnMarketDataRepository::new(file_mapping).await.unwrap();

        // Load 1-minute bars
        let start_time = 1704153600_000_000_000i64;
        let end_time = 1704157200_000_000_000i64; // 1 hour
        let symbols = vec!["ES.FUT".to_string()];
        let bars = repo
            .load_historical_data(&symbols, start_time, end_time)
            .await
            .unwrap();

        // Resample to 5-minute bars
        let result = repo.resample_bars(&bars, 5);
        assert!(result.is_ok());

        let resampled = result.unwrap();

        // Should have fewer bars (5x fewer for 5-minute resampling)
        assert!(
            resampled.len() < bars.len(),
            "Resampled should have fewer bars"
        );
        assert!(resampled.len() >= bars.len() / 6, "Too few resampled bars");

        // Verify OHLC relationships
        for bar in &resampled {
            assert!(bar.low <= bar.open, "Low should be <= open");
            assert!(bar.low <= bar.close, "Low should be <= close");
            assert!(bar.high >= bar.open, "High should be >= open");
            assert!(bar.high >= bar.close, "High should be >= close");
        }
    }

    #[tokio::test]
    #[ignore] // Requires local DBN test data (test_data/real/databento/)
    async fn test_calculate_rolling_stats() {
        let mut file_mapping = HashMap::new();
        file_mapping.insert("ES.FUT".to_string(), get_test_file_path());

        let repo = DbnMarketDataRepository::new(file_mapping).await.unwrap();

        let start_time = 1704153600_000_000_000i64;
        let end_time = 1704157200_000_000_000i64;
        let symbols = vec!["ES.FUT".to_string()];
        let bars = repo
            .load_historical_data(&symbols, start_time, end_time)
            .await
            .unwrap();

        let window_size = 20;
        let stats = repo.calculate_rolling_stats(&bars, window_size);

        // Should have stats for each window
        assert_eq!(stats.len(), bars.len() - window_size + 1);

        // Verify stats structure
        for (mean, std_dev, min, max) in &stats {
            assert!(!mean.is_nan(), "Mean should be valid");
            assert!(!std_dev.is_nan(), "StdDev should be valid");
            assert!(min <= max, "Min should be <= max");
        }
    }

    #[tokio::test]
    #[ignore] // Requires local DBN test data (test_data/real/databento/)
    async fn test_generate_summary_stats() {
        let mut file_mapping = HashMap::new();
        file_mapping.insert("ES.FUT".to_string(), get_test_file_path());

        let repo = DbnMarketDataRepository::new(file_mapping).await.unwrap();

        let start_time = 1704153600_000_000_000i64;
        let end_time = 1704157200_000_000_000i64;
        let symbols = vec!["ES.FUT".to_string()];
        let bars = repo
            .load_historical_data(&symbols, start_time, end_time)
            .await
            .unwrap();

        let stats = repo.generate_summary_stats(&bars);

        // Should have all expected statistics
        assert!(stats.contains_key("count"));
        assert!(stats.contains_key("mean_close"));
        assert!(stats.contains_key("std_close"));
        assert!(stats.contains_key("min_close"));
        assert!(stats.contains_key("max_close"));
        assert!(stats.contains_key("mean_volume"));
        assert!(stats.contains_key("total_volume"));

        // Verify basic sanity
        assert_eq!(stats.get("count").expect("INVARIANT: Key should exist in map"), &(bars.len() as f64));
        assert!(stats.get("mean_close").expect("INVARIANT: Key should exist in map") > &0.0);
        assert!(stats.get("std_close").expect("INVARIANT: Key should exist in map") >= &0.0);
    }

    #[tokio::test]
    async fn test_empty_bars_edge_cases() {
        let mut file_mapping = HashMap::new();
        file_mapping.insert("ES.FUT".to_string(), get_test_file_path());

        let repo = DbnMarketDataRepository::new(file_mapping).await.unwrap();

        // Test empty resampling
        let empty_bars: Vec<MarketData> = Vec::new();
        let result = repo.resample_bars(&empty_bars, 5);
        assert!(result.is_ok());
        assert!(result.unwrap().is_empty());

        // Test empty stats
        let stats = repo.generate_summary_stats(&empty_bars);
        assert!(stats.is_empty());

        // Test rolling stats with insufficient data
        let stats = repo.calculate_rolling_stats(&empty_bars, 20);
        assert!(stats.is_empty());
    }

    #[tokio::test]
    #[ignore] // Requires local DBN test data (test_data/real/databento/)
    async fn test_performance_target() {
        let mut file_mapping = HashMap::new();
        file_mapping.insert("ES.FUT".to_string(), get_test_file_path());

        let repo = DbnMarketDataRepository::new(file_mapping).await.unwrap();

        let start = std::time::Instant::now();

        let start_time = 1704153600_000_000_000i64;
        let end_time = 1704157200_000_000_000i64;
        let symbols = vec!["ES.FUT".to_string()];
        let bars = repo
            .load_historical_data(&symbols, start_time, end_time)
            .await
            .unwrap();

        let duration = start.elapsed();

        // Should meet <10ms performance target
        println!(
            "Loaded {} bars in {:?} ({:.2}ms)",
            bars.len(),
            duration,
            duration.as_secs_f64() * 1000.0
        );

        // Note: This is a soft target - may vary by system
        if bars.len() < 500 {
            assert!(
                duration.as_millis() < 50,
                "Performance target missed: {}ms for {} bars",
                duration.as_millis(),
                bars.len()
            );
        }
    }
}
