// Copyright 2025 Adobe. All rights reserved.
// This file is licensed to you under the Apache License,
// Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0)
// or the MIT license (http://opensource.org/licenses/MIT),
// at your option.
//
// Unless required by applicable law or agreed to in writing,
// this software is distributed on an "AS IS" BASIS, WITHOUT
// WARRANTIES OR REPRESENTATIONS OF ANY KIND, either express or
// implied. See the LICENSE-MIT and LICENSE-APACHE files for the
// specific language governing permissions and limitations under
// each license.

use crate::analyze::common::{
    AVG_FILE_SIZE_MEDIUM_THRESHOLD, COMPACTION_TARGET_LARGE_AVG, COMPACTION_TARGET_MEDIUM_AVG,
    COMPACTION_TARGET_SMALL_AVG, SMALL_FILE_THRESHOLD_BYTES, TARGET_COMPACTION_SIZE_BYTES,
};
use crate::analyze::delta::DeltaAnalyzer;
#[cfg(feature = "hudi")]
use crate::analyze::hudi::HudiAnalyzer;
use crate::analyze::iceberg::IcebergAnalyzer;
#[cfg(feature = "lance")]
use crate::analyze::lance::analyze::LanceAnalyzer;
use crate::analyze::metrics::{
    FileCompactionMetrics, FileInfo, HealthMetrics, HealthReport, PartitionInfo, TimedLikeMetrics,
};
use crate::analyze::table_analyzer::TableAnalyzer;
use crate::reader::delta::reader::DeltaReader;
#[cfg(feature = "hudi")]
use crate::reader::hudi::reader::HudiReader;
use crate::reader::iceberg::reader::IcebergReader;
#[cfg(feature = "lance")]
use crate::reader::lance::reader::LanceReader;
use crate::storage::{FileMetadata, StorageConfig, StorageProvider, StorageProviderFactory};
use crate::util::helpers::{
    detect_table_type, measure_dur, measure_dur_async, measure_dur_with_error,
};
use std::collections::{HashMap, HashSet, LinkedList};
use std::error::Error;
use std::sync::Arc;
use std::time::{Duration, SystemTime, UNIX_EPOCH};
use tracing::{info, warn};

/// Builder for constructing an `Analyzer` instance.
///
/// This builder provides a fluent API for configuring and creating an `Analyzer`.
/// The builder pattern allows for easy extension with additional configuration options
/// in the future.
///
/// # Examples
///
/// ```no_run
/// use lake_pulse::analyze::Analyzer;
/// use lake_pulse::storage::StorageConfig;
///
/// # async fn example() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
/// let storage_config = StorageConfig::local()
///     .with_option("path", "/path/to/data");
///
/// // Simple case with defaults
/// let analyzer = Analyzer::builder(storage_config.clone())
///     .build()
///     .await?;
///
/// // With custom parallelism
/// let analyzer = Analyzer::builder(storage_config)
///     .with_parallelism(10)
///     .build()
///     .await?;
/// # Ok(())
/// # }
/// ```
pub struct AnalyzerBuilder {
    config: StorageConfig,
    parallelism: Option<usize>,
}

impl AnalyzerBuilder {
    /// Creates a new `AnalyzerBuilder` with the given storage configuration.
    ///
    /// # Arguments
    ///
    /// * `config` - The storage configuration to use for the analyzer
    ///
    /// # Returns
    ///
    /// A new `AnalyzerBuilder` instance with default parallelism settings.
    ///
    /// # Examples
    ///
    /// ```no_run
    /// use lake_pulse::analyze::analyzer::AnalyzerBuilder;
    /// use lake_pulse::storage::StorageConfig;
    ///
    /// let config = StorageConfig::local().with_option("path", "/data");
    /// let builder = AnalyzerBuilder::new(config);
    /// ```
    pub fn new(config: StorageConfig) -> Self {
        Self {
            config,
            parallelism: None,
        }
    }

    /// Sets the desired parallelism for metadata file processing.
    ///
    /// # Arguments
    ///
    /// * `parallelism` - The desired level of parallelism (number of concurrent tasks)
    ///
    /// # Returns
    ///
    /// The `AnalyzerBuilder` instance with updated parallelism setting (for method chaining).
    ///
    /// ```no_run
    /// use lake_pulse::analyze::analyzer::AnalyzerBuilder;
    /// use lake_pulse::storage::StorageConfig;
    ///
    /// let config = StorageConfig::local().with_option("path", "/data");
    /// let builder = AnalyzerBuilder::new(config).with_parallelism(10);
    /// ```
    pub fn with_parallelism(mut self, parallelism: usize) -> Self {
        self.parallelism = Some(parallelism);
        self
    }

    /// Builds the `Analyzer` instance.
    ///
    /// This method performs the async initialization of the storage provider
    /// and constructs the final `Analyzer` instance.
    ///
    /// # Returns
    ///
    /// A `Result` containing:
    /// * `Ok(Analyzer)` - A fully initialized analyzer ready to analyze tables
    /// * `Err(Box<dyn Error + Send + Sync>)` - If the analyzer cannot be built
    ///
    /// # Errors
    ///
    /// Returns an error if the storage provider cannot be initialized.
    ///
    /// # Examples
    ///
    /// ```no_run
    /// use lake_pulse::analyze::Analyzer;
    /// use lake_pulse::storage::StorageConfig;
    ///
    /// # async fn example() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
    /// let config = StorageConfig::local().with_option("path", "/data");
    /// let analyzer = Analyzer::builder(config)
    ///     .with_parallelism(5)
    ///     .build()
    ///     .await?;
    /// # Ok(())
    /// # }
    /// ```
    pub async fn build(self) -> Result<Analyzer, Box<dyn Error + Send + Sync>> {
        let storage_provider = StorageProviderFactory::from_config(self.config).await?;
        Ok(Analyzer {
            storage_provider,
            parallelism: self.parallelism.unwrap_or(1),
        })
    }
}

/// Main analyzer for data lake table health analysis.
///
/// The `Analyzer` provides comprehensive health analysis for data lake tables
/// across multiple formats (Delta Lake, Apache Iceberg, Apache Hudi, Lance) and
/// storage providers (AWS S3, Azure Data Lake, GCS, Local filesystem).
///
/// # Features
///
/// - Automatic table format detection
/// - Parallel file processing for improved performance
/// - Comprehensive health metrics including:
///   - File size distribution
///   - Partition analysis
///   - Data skew detection
///   - Unreferenced file detection
///   - Schema evolution tracking
///   - Time travel metrics
///   - Deletion vector analysis
///   - Compaction opportunities
///
/// # Examples
///
/// ## Basic Usage
///
/// ```no_run
/// use lake_pulse::{Analyzer, StorageConfig};
///
/// # async fn example() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
/// // Configure storage for local filesystem
/// let config = StorageConfig::local()
///     .with_option("path", "./examples/data");
///
/// // Create analyzer
/// let analyzer = Analyzer::builder(config)
///     .build()
///     .await?;
///
/// // Analyze a table (auto-detects format)
/// let report = analyzer.analyze("delta_dataset").await?;
/// println!("{}", report);
/// # Ok(())
/// # }
/// ```
///
/// ## Using Parallelism for Large Tables
///
/// ```no_run
/// use lake_pulse::{Analyzer, StorageConfig};
///
/// # async fn example() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
/// let config = StorageConfig::aws()
///     .with_option("bucket", "my-bucket")
///     .with_option("region", "us-east-1");
///
/// // Use parallelism for faster processing of large tables
/// let analyzer = Analyzer::builder(config)
///     .with_parallelism(10)
///     .build()
///     .await?;
///
/// let report = analyzer.analyze("large-table").await?;
/// # Ok(())
/// # }
/// ```
///
/// For complete examples, see the [`examples/`](https://github.com/aionescu_adobe/lake-pulse/tree/main/examples) directory.
pub struct Analyzer {
    storage_provider: Arc<dyn StorageProvider>,
    parallelism: usize,
}

impl Analyzer {
    /// Creates a new `AnalyzerBuilder` for constructing an `Analyzer` instance.
    ///
    /// This is the preferred way to create an `Analyzer`. Use the builder pattern
    /// to configure the analyzer before calling `.build().await`.
    ///
    /// # Arguments
    ///
    /// * `config` - The storage configuration to use
    ///
    /// # Returns
    ///
    /// A new `AnalyzerBuilder` instance for configuring and building an analyzer.
    ///
    /// # Examples
    ///
    /// ```no_run
    /// use lake_pulse::analyze::Analyzer;
    /// use lake_pulse::storage::StorageConfig;
    ///
    /// # async fn example() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
    /// let config = StorageConfig::local().with_option("path", "/data");
    /// let analyzer = Analyzer::builder(config)
    ///     .with_parallelism(10)
    ///     .build()
    ///     .await?;
    /// # Ok(())
    /// # }
    /// ```
    pub fn builder(config: StorageConfig) -> AnalyzerBuilder {
        AnalyzerBuilder::new(config)
    }

    /// Analyze a table at the given location and generate a comprehensive health report.
    ///
    /// This method performs a complete analysis of the table including:
    /// - File listing and categorization
    /// - Metadata processing
    /// - Referenced file discovery
    /// - Unreferenced file detection
    /// - Table-specific metrics extraction (Delta, Iceberg, Hudi, Lance)
    /// - Health metrics calculation
    ///
    /// # Arguments
    ///
    /// * `location` - The path to the table relative to the storage provider's base path
    ///
    /// # Returns
    ///
    /// A `Result` containing:
    /// * `Ok(HealthReport)` - A comprehensive health report with metrics and recommendations
    /// * `Err(Box<dyn Error + Send + Sync>)` - If the analysis fails
    ///
    /// # Errors
    ///
    /// This function will return an error if:
    /// * The storage connection cannot be validated
    /// * Partitions cannot be discovered
    /// * Files cannot be listed
    /// * Metadata files cannot be processed
    /// * Table-specific readers fail to open or extract metrics
    pub async fn analyze(
        &self,
        location: &str,
    ) -> Result<HealthReport, Box<dyn Error + Send + Sync>> {
        let mut internal_metrics: LinkedList<(&str, SystemTime, Duration)> = LinkedList::new();
        let mut metrics = HealthMetrics::new();

        info!(
            "Analyzing, base_path={}/{}",
            self.storage_provider.base_path(),
            location
        );

        measure_dur_async(
            "validate_connection_dur",
            &mut internal_metrics,
            || async { self.storage_provider.validate_connection(location).await },
            Some(|_| "Successfully validated connection".to_string()),
        )
        .await?;

        let partitions = measure_dur_async(
            "discover_partitions",
            &mut internal_metrics,
            || async {
                self.storage_provider
                    .discover_partitions(location, vec![])
                    .await
            },
            Some(|c: &Vec<String>| format!("Discovered partitions count={}", c.len())),
        )
        .await?;

        let list_files_start = SystemTime::now();
        let all_objects = measure_dur_async(
            "list_files_parallel",
            &mut internal_metrics,
            || async {
                self.storage_provider
                    .list_files_parallel(location, partitions.clone(), self.parallelism)
                    .await
            },
            Some(|c: &Vec<FileMetadata>| format!("Listed files count={}", c.len())),
        )
        .await?;

        // Detect table type and instantiate appropriate analyzer
        let table_type: String = measure_dur(
            "detect_table_type",
            &mut internal_metrics,
            || detect_table_type(&all_objects),
            Some(|t: String| format!("Detected table type={}", t)),
        )
        .await;

        // Instantiate the appropriate table analyzer based on detected type
        let table_analyzer: Arc<dyn TableAnalyzer> = match table_type.as_str() {
            "delta" => Arc::new(DeltaAnalyzer::new(
                Arc::clone(&self.storage_provider),
                self.parallelism,
            )),
            #[cfg(feature = "hudi")]
            "hudi" => Arc::new(HudiAnalyzer::new(
                Arc::clone(&self.storage_provider),
                self.parallelism,
            )),
            "iceberg" => Arc::new(IcebergAnalyzer::new(
                Arc::clone(&self.storage_provider),
                self.parallelism,
            )),
            #[cfg(feature = "lance")]
            "lance" => Arc::new(LanceAnalyzer::new(
                Arc::clone(&self.storage_provider),
                self.parallelism,
            )),
            _ => {
                return Err(format!(
                    "Unknown, unsupported or not enabled table type={}. \
                    Check documentation on what other table formats you can enable.",
                    table_type
                )
                .into());
            }
        };

        let (data_files, metadata_files) = measure_dur(
            "categorize_files",
            &mut internal_metrics,
            || table_analyzer.categorize_files(all_objects.clone()),
            Some(|(data, meta): (Vec<FileMetadata>, Vec<FileMetadata>)| {
                format!(
                    "Categorized data files count={}, metadata files count={}",
                    data.len(),
                    meta.len()
                )
            }),
        )
        .await;

        let referenced_files = measure_dur_async(
            "find_referenced_files",
            &mut internal_metrics,
            || async { table_analyzer.find_referenced_files(&metadata_files).await },
            Some(|c: &Vec<String>| format!("Found referenced files, count={}", c.len())),
        )
        .await?;

        metrics.total_files = data_files.len();
        metrics.total_size_bytes = data_files.iter().map(|f| f.size).sum();

        let referenced_set: HashSet<String> = referenced_files.into_iter().collect();
        let unreferenced_files = measure_dur(
            "find_unreferenced_files",
            &mut internal_metrics,
            || {
                data_files
                    .iter()
                    .filter(|f| !referenced_set.iter().any(|i| f.path.contains(i)))
                    .map(|f| FileInfo {
                        path: format!("{}/{}", self.storage_provider.base_path(), f.path),
                        size_bytes: f.size,
                        last_modified: f.last_modified.map(|m| m.to_rfc3339()),
                        is_referenced: false,
                    })
                    .collect::<Vec<FileInfo>>()
            },
            Some(|c: Vec<FileInfo>| format!("Found unreferenced files, count={}", c.len())),
        )
        .await;

        metrics.unreferenced_files = unreferenced_files;
        metrics.unreferenced_size_bytes = metrics
            .unreferenced_files
            .iter()
            .map(|f| f.size_bytes)
            .sum();

        metrics.partitions = measure_dur_with_error(
            "analyze_partitioning",
            &mut internal_metrics,
            || self.analyze_partitioning(&data_files),
            Some(|c: &Vec<PartitionInfo>| format!("Analyzed partitions count={}", c.len())),
        )?;

        metrics.partition_count = metrics.partitions.len();

        let data_files_total_size: u64 = data_files.iter().map(|f| f.size).sum();

        measure_dur_async(
            "update_metrics_from_metadata",
            &mut internal_metrics,
            || async {
                table_analyzer
                    .update_metrics_from_metadata(
                        &metadata_files,
                        data_files_total_size,
                        data_files.len(),
                        &mut metrics,
                    )
                    .await
            },
            Some(|_| "Updated metrics from metadata".to_string()),
        )
        .await?;

        let (small_files, medium_files, large_files, very_large_files) =
            measure_dur(
                "calculate_file_size_distribution",
                &mut internal_metrics,
                || { self.calculate_file_size_distribution(&data_files) },
                Some(|(small, medium, large, very_large): (usize, usize, usize, usize)| {
                    format!(
                        "Calculated file size distribution, small={}, medium={}, large={}, very_large={}",
                        small, medium, large, very_large
                    )
                }),
            ).await;

        metrics.file_size_distribution.small_files = small_files;
        metrics.file_size_distribution.medium_files = medium_files;
        metrics.file_size_distribution.large_files = large_files;
        metrics.file_size_distribution.very_large_files = very_large_files;

        if metrics.total_files > 0 {
            metrics.avg_file_size_bytes =
                metrics.total_size_bytes as f64 / metrics.total_files as f64;
        }

        let (
            metadata_file_count,
            metadata_total_size_bytes,
            avg_metadata_file_size,
            metadata_growth_rate,
        ) = measure_dur(
            "calculate_metadata_health",
            &mut internal_metrics,
            || { self.calculate_metadata_health(&metadata_files) },
            Some(|(count, size, avg, growth): (usize, u64, f64, f64)| {
                format!(
                    "Calculated metadata health, file_count={}, bytesize={}, avg_bytesize={}, growth_rate={}",
                    count, size, avg, growth
                )
            }),
        ).await;

        metrics.metadata_health.metadata_file_count = metadata_file_count;
        metrics.metadata_health.metadata_total_size_bytes = metadata_total_size_bytes;
        metrics.metadata_health.avg_metadata_file_size = avg_metadata_file_size;
        metrics.metadata_health.metadata_growth_rate = metadata_growth_rate;

        measure_dur(
            "calculate_data_skew",
            &mut internal_metrics,
            || metrics.calculate_data_skew(),
            None,
        )
        .await;

        measure_dur(
            "calculate_snapshot_health",
            &mut internal_metrics,
            || metrics.calculate_snapshot_health(metadata_files.len()),
            None,
        )
        .await;

        metrics.file_compaction = measure_dur_async(
            "analyze_file_compaction",
            &mut internal_metrics,
            || async {
                if let Some(file_compaction) = metrics.file_compaction.clone() {
                    let z_order_opportunity = file_compaction.z_order_opportunity;
                    let z_order_columns = file_compaction.z_order_columns;
                    self.analyze_file_compaction(
                        &data_files,
                        z_order_opportunity,
                        z_order_columns.clone(),
                    )
                    .await
                } else {
                    Ok(None)
                }
            },
            Some(|_| "Calculated compaction metrics".to_string()),
        )
        .await?;

        measure_dur(
            "generate_recommendations",
            &mut internal_metrics,
            || metrics.generate_recommendations(),
            Some(|_| "Generated recommendations".to_string()),
        )
        .await;

        metrics.health_score = measure_dur(
            "calculate_health_score",
            &mut internal_metrics,
            || metrics.calculate_health_score(),
            Some(|score: f64| format!("Calculated health score={}", score)),
        )
        .await;

        let table_path = self.storage_provider.uri_from_path(location);

        let analyze_after_validation_dur = list_files_start.elapsed()?;
        internal_metrics.push_back((
            "analyze_after_validation_dur",
            list_files_start,
            analyze_after_validation_dur,
        ));

        info!("Analysis took={}", analyze_after_validation_dur.as_millis());

        // Only try to open Delta reader for Delta tables
        if table_type == "delta" {
            measure_dur_async(
                "delta_reader",
                &mut internal_metrics,
                || async {
                    let delta_reader = DeltaReader::open(
                        self.storage_provider.uri_from_path(location).as_str(),
                        &self.storage_provider.clean_options(),
                    )
                    .await;

                    match delta_reader {
                        Ok(reader) => {
                            metrics.delta_table_specific_metrics =
                                Some(reader.extract_metrics().await?);
                            Ok(())
                        }
                        Err(e) => {
                            warn!("Failed to open Delta reader: {}", e);
                            Err(e)
                        }
                    }
                },
                Some(|_| "Opened Delta reader".to_string()),
            )
            .await?;
        } else if table_type == "iceberg" {
            measure_dur_async(
                "iceberg_reader",
                &mut internal_metrics,
                || async {
                    let iceberg_reader = IcebergReader::open(
                        self.storage_provider.uri_from_path(location).as_str(),
                        &self.storage_provider.clean_options(),
                    )
                    .await;

                    match iceberg_reader {
                        Ok(reader) => {
                            metrics.iceberg_table_specific_metrics =
                                Some(reader.extract_metrics().await?);
                            Ok(())
                        }
                        Err(e) => {
                            warn!("Failed to open Iceberg reader: {}", e);
                            Err(e)
                        }
                    }
                },
                Some(|_| "Opened Iceberg reader".to_string()),
            )
            .await?;
        }

        #[cfg(feature = "hudi")]
        if table_type == "hudi" {
            measure_dur_async(
                "hudi_reader",
                &mut internal_metrics,
                || async {
                    let storage_options = HashMap::new();
                    let hudi_reader = HudiReader::open(
                        self.storage_provider.uri_from_path(location).as_str(),
                        &storage_options,
                    )
                    .await;

                    match hudi_reader {
                        Ok(reader) => {
                            metrics.hudi_table_specific_metrics =
                                Some(reader.extract_metrics().await?);
                            Ok(())
                        }
                        Err(e) => {
                            warn!("Failed to open Hudi reader: {}", e);
                            Err(e)
                        }
                    }
                },
                Some(|_| "Opened Hudi reader".to_string()),
            )
            .await?;
        }

        #[cfg(feature = "lance")]
        if table_type == "lance" {
            measure_dur_async(
                "lance_reader",
                &mut internal_metrics,
                || async {
                    let lance_reader =
                        LanceReader::open(self.storage_provider.uri_from_path(location).as_str())
                            .await;

                    match lance_reader {
                        Ok(reader) => {
                            metrics.lance_table_specific_metrics =
                                Some(reader.extract_metrics().await?);
                            Ok(())
                        }
                        Err(e) => {
                            warn!("Failed to open Lance reader: {}", e);
                            Err(e)
                        }
                    }
                },
                Some(|_| "Opened Lance reader".to_string()),
            )
            .await?;
        }

        let timed_metrics: LinkedList<(String, u128, u128)> = internal_metrics
            .iter()
            .map(
                |(k, st, dur)| -> Result<(String, u128, u128), Box<dyn Error + Send + Sync>> {
                    Ok((
                        k.to_string(),
                        st.duration_since(UNIX_EPOCH)?.as_millis(),
                        dur.as_millis(),
                    ))
                },
            )
            .collect::<Result<LinkedList<(_, _, _)>, _>>()?;

        let report = HealthReport {
            table_path,
            table_type,
            analysis_timestamp: chrono::Utc::now().to_rfc3339(),
            metrics: metrics.clone(),
            health_score: metrics.health_score,
            timed_metrics: TimedLikeMetrics {
                duration_collection: timed_metrics,
            },
        };

        Ok(report)
    }

    fn analyze_partitioning(
        &self,
        data_files: &Vec<FileMetadata>,
    ) -> Result<Vec<PartitionInfo>, Box<dyn Error + Send + Sync>> {
        let mut partition_map: HashMap<String, PartitionInfo> = HashMap::new();

        for file in data_files {
            // Extract partition information from file path
            // Delta Lake typically uses partition columns in the path like: col1=value1/col2=value2/file.parquet
            let path_parts: Vec<&str> = file.path.split('/').collect();
            let mut partition_values = HashMap::new();
            let mut _file_name = "";

            for part in &path_parts {
                if part.contains('=') {
                    let kv: Vec<&str> = part.split('=').collect();
                    if kv.len() == 2 {
                        partition_values.insert(kv[0].to_string(), kv[1].to_string());
                    }
                } else if part.ends_with(".parquet") {
                    _file_name = part;
                }
            }

            let partition_key = serde_json::to_string(&partition_values).unwrap_or_default();

            let partition_info =
                partition_map
                    .entry(partition_key)
                    .or_insert_with(|| PartitionInfo {
                        partition_values: partition_values.clone(),
                        file_count: 0,
                        total_size_bytes: 0,
                        avg_file_size_bytes: 0.0,
                        files: Vec::new(),
                    });

            partition_info.file_count += 1;
            partition_info.total_size_bytes += file.size;
            partition_info.files.push(FileInfo {
                path: format!("{}/{}", self.storage_provider.base_path(), file.path),
                size_bytes: file.size,
                last_modified: file.last_modified.map(|m| m.to_rfc3339()),
                is_referenced: true, // We'll update this later
            });
        }

        // Calculate averages for each partition
        for partition in partition_map.values_mut() {
            if partition.file_count > 0 {
                partition.avg_file_size_bytes =
                    partition.total_size_bytes as f64 / partition.file_count as f64;
            }
        }

        let partitions = partition_map.into_values().collect::<Vec<PartitionInfo>>();

        Ok(partitions)
    }

    fn calculate_file_size_distribution(
        &self,
        data_files: &Vec<FileMetadata>,
    ) -> (usize, usize, usize, usize) {
        let mut small_files = 0;
        let mut medium_files = 0;
        let mut large_files = 0;
        let mut very_large_files = 0;

        for file in data_files {
            let size_mb = file.size as f64 / (1024.0 * 1024.0);

            if size_mb < 16.0 {
                small_files += 1;
            } else if size_mb < 128.0 {
                medium_files += 1;
            } else if size_mb < 1024.0 {
                large_files += 1;
            } else {
                very_large_files += 1;
            }
        }
        (small_files, medium_files, large_files, very_large_files)
    }

    /// Calculate metadata health metrics.
    ///
    /// # Arguments
    ///
    /// * `metadata_files` - Vector of metadata file information
    ///
    /// # Returns
    ///
    /// A tuple containing:
    /// * `usize` - Metadata file count
    /// * `u64` - Total metadata size in bytes
    /// * `f64` - Metadata health score (average file size)
    /// * `f64` - Metadata growth rate (currently placeholder, returns 0.0)
    pub fn calculate_metadata_health(
        &self,
        metadata_files: &[FileMetadata],
    ) -> (usize, u64, f64, f64) {
        let mut metadata_health: f64 = 0.0;
        let metadata_file_count = metadata_files.len();
        let metadata_total_size_bytes: u64 = metadata_files.iter().map(|f| f.size).sum();

        if !metadata_files.is_empty() {
            metadata_health = metadata_total_size_bytes as f64 / metadata_files.len() as f64;
        }

        // Estimate growth rate (simplified - would need historical data for accuracy)
        let metadata_growth_rate = 0.0; // Placeholder

        (
            metadata_file_count,
            metadata_total_size_bytes,
            metadata_health,
            metadata_growth_rate,
        )
    }

    async fn analyze_file_compaction(
        &self,
        data_files: &Vec<FileMetadata>,
        z_order_opportunity: bool,
        z_order_columns: Vec<String>,
    ) -> Result<Option<FileCompactionMetrics>, Box<dyn Error + Send + Sync>> {
        let mut small_files_count = 0;
        let mut small_files_size = 0u64;
        let mut potential_compaction_files = 0;
        let mut estimated_savings = 0u64;

        // Analyze file sizes for compaction opportunities
        for file in data_files {
            let file_size = file.size;
            if file_size < SMALL_FILE_THRESHOLD_BYTES {
                small_files_count += 1;
                small_files_size += file_size;
                potential_compaction_files += 1;
            }
        }

        // Calculate potential savings
        if small_files_count > 1 {
            let files_per_target = (TARGET_COMPACTION_SIZE_BYTES as f64
                / (small_files_size as f64 / small_files_count as f64))
                .ceil() as usize;
            let target_files = (small_files_count as f64 / files_per_target as f64).ceil() as usize;
            let estimated_target_size = target_files as u64 * TARGET_COMPACTION_SIZE_BYTES / 2; // Conservative estimate
            estimated_savings = small_files_size.saturating_sub(estimated_target_size);
        }

        let compaction_opportunity = self.calculate_compaction_opportunity(
            small_files_count,
            small_files_size,
            data_files.len(),
        );
        let recommended_target_size = self.calculate_recommended_target_size(data_files);
        let compaction_priority =
            self.calculate_compaction_priority(compaction_opportunity, small_files_count);

        Ok(Some(FileCompactionMetrics {
            compaction_opportunity_score: compaction_opportunity,
            small_files_count,
            small_files_size_bytes: small_files_size,
            potential_compaction_files,
            estimated_compaction_savings_bytes: estimated_savings,
            recommended_target_file_size_bytes: recommended_target_size,
            compaction_priority,
            z_order_opportunity,
            z_order_columns,
        }))
    }

    fn calculate_compaction_opportunity(
        &self,
        small_files: usize,
        small_files_size: u64,
        total_files: usize,
    ) -> f64 {
        if total_files == 0 {
            return 0.0;
        }

        let small_file_ratio = small_files as f64 / total_files as f64;
        let _size_ratio = small_files_size as f64 / (small_files_size as f64 + 1.0); // Avoid division by zero

        if small_file_ratio > 0.8 {
            1.0
        } else if small_file_ratio > 0.6 {
            0.8
        } else if small_file_ratio > 0.4 {
            0.6
        } else if small_file_ratio > 0.2 {
            0.4
        } else {
            0.2
        }
    }

    fn calculate_recommended_target_size(&self, data_files: &[FileMetadata]) -> u64 {
        if data_files.is_empty() {
            return COMPACTION_TARGET_SMALL_AVG;
        }

        let total_size = data_files.iter().map(|f| f.size).sum::<u64>();
        let avg_size = total_size as f64 / data_files.len() as f64;

        // Recommend target size based on current average
        if avg_size < SMALL_FILE_THRESHOLD_BYTES as f64 {
            COMPACTION_TARGET_SMALL_AVG
        } else if avg_size < AVG_FILE_SIZE_MEDIUM_THRESHOLD {
            COMPACTION_TARGET_MEDIUM_AVG
        } else {
            COMPACTION_TARGET_LARGE_AVG
        }
    }

    fn calculate_compaction_priority(&self, opportunity_score: f64, small_files: usize) -> String {
        if opportunity_score > 0.8 || small_files > 100 {
            "critical".to_string()
        } else if opportunity_score > 0.6 || small_files > 50 {
            "high".to_string()
        } else if opportunity_score > 0.4 || small_files > 20 {
            "medium".to_string()
        } else {
            "low".to_string()
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::storage::error::StorageResult;
    use crate::storage::{FileMetadata, StorageProvider};
    use async_trait::async_trait;
    use chrono::Utc;

    // Mock storage provider for testing
    struct MockStorageProvider {
        base_path: String,
        options: HashMap<String, String>,
    }

    #[async_trait]
    impl StorageProvider for MockStorageProvider {
        fn base_path(&self) -> &str {
            &self.base_path
        }

        fn uri_from_path(&self, path: &str) -> String {
            format!("{}/{}", self.base_path, path)
        }

        async fn validate_connection(&self, _location: &str) -> StorageResult<()> {
            Ok(())
        }

        async fn list_files(
            &self,
            _path: &str,
            _recursive: bool,
        ) -> StorageResult<Vec<FileMetadata>> {
            Ok(vec![])
        }

        async fn discover_partitions(
            &self,
            _path: &str,
            _exclude_prefixes: Vec<&str>,
        ) -> StorageResult<Vec<String>> {
            Ok(vec![])
        }

        async fn list_files_parallel(
            &self,
            _path: &str,
            _partitions: Vec<String>,
            _parallelism: usize,
        ) -> StorageResult<Vec<FileMetadata>> {
            Ok(vec![])
        }

        async fn read_file(&self, _path: &str) -> StorageResult<Vec<u8>> {
            Ok(vec![])
        }

        async fn exists(&self, _path: &str) -> StorageResult<bool> {
            Ok(true)
        }

        async fn get_metadata(&self, _path: &str) -> StorageResult<FileMetadata> {
            Ok(FileMetadata {
                path: "test".to_string(),
                size: 0,
                last_modified: None,
            })
        }

        fn options(&self) -> &HashMap<String, String> {
            &self.options
        }

        fn clean_options(&self) -> HashMap<String, String> {
            HashMap::new()
        }
    }

    // Configurable mock for testing analyze() method
    struct ConfigurableMockStorageProvider {
        base_path: String,
        options: HashMap<String, String>,
        files: Vec<FileMetadata>,
        file_contents: HashMap<String, Vec<u8>>,
    }

    #[async_trait]
    impl StorageProvider for ConfigurableMockStorageProvider {
        fn base_path(&self) -> &str {
            &self.base_path
        }

        fn uri_from_path(&self, path: &str) -> String {
            format!("{}/{}", self.base_path, path)
        }

        async fn validate_connection(&self, _location: &str) -> StorageResult<()> {
            Ok(())
        }

        async fn list_files(
            &self,
            _path: &str,
            _recursive: bool,
        ) -> StorageResult<Vec<FileMetadata>> {
            Ok(self.files.clone())
        }

        async fn discover_partitions(
            &self,
            _path: &str,
            _exclude_prefixes: Vec<&str>,
        ) -> StorageResult<Vec<String>> {
            Ok(vec!["".to_string()])
        }

        async fn list_files_parallel(
            &self,
            _path: &str,
            _partitions: Vec<String>,
            _parallelism: usize,
        ) -> StorageResult<Vec<FileMetadata>> {
            Ok(self.files.clone())
        }

        async fn read_file(&self, path: &str) -> StorageResult<Vec<u8>> {
            Ok(self
                .file_contents
                .get(path)
                .cloned()
                .unwrap_or_else(Vec::new))
        }

        async fn exists(&self, _path: &str) -> StorageResult<bool> {
            Ok(true)
        }

        async fn get_metadata(&self, path: &str) -> StorageResult<FileMetadata> {
            Ok(self
                .files
                .iter()
                .find(|f| f.path == path)
                .cloned()
                .unwrap_or(FileMetadata {
                    path: path.to_string(),
                    size: 0,
                    last_modified: None,
                }))
        }

        fn options(&self) -> &HashMap<String, String> {
            &self.options
        }

        fn clean_options(&self) -> HashMap<String, String> {
            HashMap::new()
        }
    }

    // Helper function to create a mock Analyzer for testing
    fn create_mock_analyzer() -> Analyzer {
        Analyzer {
            storage_provider: Arc::new(MockStorageProvider {
                base_path: "/tmp/test".to_string(),
                options: HashMap::new(),
            }),
            parallelism: 1,
        }
    }

    fn create_file_metadata(path: &str, size: u64) -> FileMetadata {
        FileMetadata {
            path: path.to_string(),
            size,
            last_modified: Some(Utc::now()),
        }
    }

    // AnalyzerBuilder tests
    #[test]
    fn test_analyzer_builder_new() {
        let config = StorageConfig::local().with_option("path", "/data");
        let builder = AnalyzerBuilder::new(config.clone());

        assert_eq!(builder.config.storage_type, config.storage_type);
        assert_eq!(builder.parallelism, None);
    }

    #[test]
    fn test_analyzer_builder_with_parallelism() {
        let config = StorageConfig::local().with_option("path", "/data");
        let builder = AnalyzerBuilder::new(config).with_parallelism(10);

        assert_eq!(builder.parallelism, Some(10));
    }

    #[test]
    fn test_analyzer_builder_chaining() {
        let config = StorageConfig::local().with_option("path", "/data");
        let builder = AnalyzerBuilder::new(config)
            .with_parallelism(5)
            .with_parallelism(10); // Should override

        assert_eq!(builder.parallelism, Some(10));
    }

    // calculate_file_size_distribution tests
    #[test]
    fn test_calculate_file_size_distribution_empty() {
        let analyzer = create_mock_analyzer();
        let files = vec![];

        let (small, medium, large, very_large) = analyzer.calculate_file_size_distribution(&files);

        assert_eq!(small, 0);
        assert_eq!(medium, 0);
        assert_eq!(large, 0);
        assert_eq!(very_large, 0);
    }

    #[test]
    fn test_calculate_file_size_distribution_small_files() {
        let analyzer = create_mock_analyzer();
        let files = vec![
            create_file_metadata("file1.parquet", 1024 * 1024), // 1MB
            create_file_metadata("file2.parquet", 10 * 1024 * 1024), // 10MB
            create_file_metadata("file3.parquet", 15 * 1024 * 1024), // 15MB
        ];

        let (small, medium, large, very_large) = analyzer.calculate_file_size_distribution(&files);

        assert_eq!(small, 3); // All < 16MB
        assert_eq!(medium, 0);
        assert_eq!(large, 0);
        assert_eq!(very_large, 0);
    }

    #[test]
    fn test_calculate_file_size_distribution_medium_files() {
        let analyzer = create_mock_analyzer();
        let files = vec![
            create_file_metadata("file1.parquet", 20 * 1024 * 1024), // 20MB
            create_file_metadata("file2.parquet", 64 * 1024 * 1024), // 64MB
            create_file_metadata("file3.parquet", 100 * 1024 * 1024), // 100MB
        ];

        let (small, medium, large, very_large) = analyzer.calculate_file_size_distribution(&files);

        assert_eq!(small, 0);
        assert_eq!(medium, 3); // All >= 16MB and < 128MB
        assert_eq!(large, 0);
        assert_eq!(very_large, 0);
    }

    #[test]
    fn test_calculate_file_size_distribution_large_files() {
        let analyzer = create_mock_analyzer();
        let files = vec![
            create_file_metadata("file1.parquet", 200 * 1024 * 1024), // 200MB
            create_file_metadata("file2.parquet", 512 * 1024 * 1024), // 512MB
            create_file_metadata("file3.parquet", 900 * 1024 * 1024), // 900MB
        ];

        let (small, medium, large, very_large) = analyzer.calculate_file_size_distribution(&files);

        assert_eq!(small, 0);
        assert_eq!(medium, 0);
        assert_eq!(large, 3); // All >= 128MB and < 1024MB
        assert_eq!(very_large, 0);
    }

    #[test]
    fn test_calculate_file_size_distribution_very_large_files() {
        let analyzer = create_mock_analyzer();
        let files = vec![
            create_file_metadata("file1.parquet", 1100 * 1024 * 1024), // 1.1GB
            create_file_metadata("file2.parquet", 2048 * 1024 * 1024), // 2GB
        ];

        let (small, medium, large, very_large) = analyzer.calculate_file_size_distribution(&files);

        assert_eq!(small, 0);
        assert_eq!(medium, 0);
        assert_eq!(large, 0);
        assert_eq!(very_large, 2); // All >= 1024MB
    }

    #[test]
    fn test_calculate_file_size_distribution_mixed() {
        let analyzer = create_mock_analyzer();
        let files = vec![
            create_file_metadata("file1.parquet", 5 * 1024 * 1024), // 5MB - small
            create_file_metadata("file2.parquet", 50 * 1024 * 1024), // 50MB - medium
            create_file_metadata("file3.parquet", 500 * 1024 * 1024), // 500MB - large
            create_file_metadata("file4.parquet", 1500 * 1024 * 1024), // 1.5GB - very large
        ];

        let (small, medium, large, very_large) = analyzer.calculate_file_size_distribution(&files);

        assert_eq!(small, 1);
        assert_eq!(medium, 1);
        assert_eq!(large, 1);
        assert_eq!(very_large, 1);
    }

    // calculate_metadata_health tests
    #[test]
    fn test_calculate_metadata_health_empty() {
        let analyzer = create_mock_analyzer();
        let files = vec![];

        let (count, total_size, avg_size, growth_rate) = analyzer.calculate_metadata_health(&files);

        assert_eq!(count, 0);
        assert_eq!(total_size, 0);
        assert_eq!(avg_size, 0.0);
        assert_eq!(growth_rate, 0.0);
    }

    #[test]
    fn test_calculate_metadata_health_single_file() {
        let analyzer = create_mock_analyzer();
        let files = vec![create_file_metadata("_delta_log/00000.json", 1024)];

        let (count, total_size, avg_size, growth_rate) = analyzer.calculate_metadata_health(&files);

        assert_eq!(count, 1);
        assert_eq!(total_size, 1024);
        assert_eq!(avg_size, 1024.0);
        assert_eq!(growth_rate, 0.0); // Placeholder
    }

    #[test]
    fn test_calculate_metadata_health_multiple_files() {
        let analyzer = create_mock_analyzer();
        let files = vec![
            create_file_metadata("_delta_log/00000.json", 1024),
            create_file_metadata("_delta_log/00001.json", 2048),
            create_file_metadata("_delta_log/00002.json", 3072),
        ];

        let (count, total_size, avg_size, growth_rate) = analyzer.calculate_metadata_health(&files);

        assert_eq!(count, 3);
        assert_eq!(total_size, 6144);
        assert_eq!(avg_size, 2048.0); // (1024 + 2048 + 3072) / 3
        assert_eq!(growth_rate, 0.0); // Placeholder
    }

    // calculate_compaction_opportunity tests
    #[test]
    fn test_calculate_compaction_opportunity_no_files() {
        let analyzer = create_mock_analyzer();

        let score = analyzer.calculate_compaction_opportunity(0, 0, 0);

        assert_eq!(score, 0.0);
    }

    #[test]
    fn test_calculate_compaction_opportunity_no_small_files() {
        let analyzer = create_mock_analyzer();

        let score = analyzer.calculate_compaction_opportunity(0, 0, 100);

        assert_eq!(score, 0.2); // 0% small files
    }

    #[test]
    fn test_calculate_compaction_opportunity_low() {
        let analyzer = create_mock_analyzer();

        let score = analyzer.calculate_compaction_opportunity(25, 1024, 100);

        assert_eq!(score, 0.4); // 25% small files
    }

    #[test]
    fn test_calculate_compaction_opportunity_medium() {
        let analyzer = create_mock_analyzer();

        let score = analyzer.calculate_compaction_opportunity(50, 1024, 100);

        assert_eq!(score, 0.6); // 50% small files
    }

    #[test]
    fn test_calculate_compaction_opportunity_high() {
        let analyzer = create_mock_analyzer();

        let score = analyzer.calculate_compaction_opportunity(70, 1024, 100);

        assert_eq!(score, 0.8); // 70% small files
    }

    #[test]
    fn test_calculate_compaction_opportunity_critical() {
        let analyzer = create_mock_analyzer();

        let score = analyzer.calculate_compaction_opportunity(90, 1024, 100);

        assert_eq!(score, 1.0); // 90% small files
    }

    // calculate_recommended_target_size tests
    #[test]
    fn test_calculate_recommended_target_size_empty() {
        let analyzer = create_mock_analyzer();
        let files = vec![];

        let target_size = analyzer.calculate_recommended_target_size(&files);

        assert_eq!(target_size, 128 * 1024 * 1024); // 128MB default
    }

    #[test]
    fn test_calculate_recommended_target_size_small_avg() {
        let analyzer = create_mock_analyzer();
        let files = vec![
            create_file_metadata("file1.parquet", 5 * 1024 * 1024),
            create_file_metadata("file2.parquet", 10 * 1024 * 1024),
        ];

        let target_size = analyzer.calculate_recommended_target_size(&files);

        assert_eq!(target_size, 128 * 1024 * 1024); // 128MB for small avg
    }

    #[test]
    fn test_calculate_recommended_target_size_medium_avg() {
        let analyzer = create_mock_analyzer();
        let files = vec![
            create_file_metadata("file1.parquet", 30 * 1024 * 1024),
            create_file_metadata("file2.parquet", 50 * 1024 * 1024),
        ];

        let target_size = analyzer.calculate_recommended_target_size(&files);

        assert_eq!(target_size, 256 * 1024 * 1024); // 256MB for medium avg
    }

    #[test]
    fn test_calculate_recommended_target_size_large_avg() {
        let analyzer = create_mock_analyzer();
        let files = vec![
            create_file_metadata("file1.parquet", 100 * 1024 * 1024),
            create_file_metadata("file2.parquet", 200 * 1024 * 1024),
        ];

        let target_size = analyzer.calculate_recommended_target_size(&files);

        assert_eq!(target_size, 512 * 1024 * 1024); // 512MB for large avg
    }

    // calculate_compaction_priority tests
    #[test]
    fn test_calculate_compaction_priority_low() {
        let analyzer = create_mock_analyzer();

        let priority = analyzer.calculate_compaction_priority(0.3, 10);

        assert_eq!(priority, "low");
    }

    #[test]
    fn test_calculate_compaction_priority_medium() {
        let analyzer = create_mock_analyzer();

        let priority = analyzer.calculate_compaction_priority(0.5, 25);

        assert_eq!(priority, "medium");
    }

    #[test]
    fn test_calculate_compaction_priority_high_by_score() {
        let analyzer = create_mock_analyzer();

        let priority = analyzer.calculate_compaction_priority(0.7, 30);

        assert_eq!(priority, "high");
    }

    #[test]
    fn test_calculate_compaction_priority_high_by_count() {
        let analyzer = create_mock_analyzer();

        let priority = analyzer.calculate_compaction_priority(0.3, 60);

        assert_eq!(priority, "high");
    }

    #[test]
    fn test_calculate_compaction_priority_critical_by_score() {
        let analyzer = create_mock_analyzer();

        let priority = analyzer.calculate_compaction_priority(0.9, 50);

        assert_eq!(priority, "critical");
    }

    #[test]
    fn test_calculate_compaction_priority_critical_by_count() {
        let analyzer = create_mock_analyzer();

        let priority = analyzer.calculate_compaction_priority(0.5, 150);

        assert_eq!(priority, "critical");
    }

    // analyze_partitioning tests
    #[test]
    fn test_analyze_partitioning_empty() {
        let analyzer = create_mock_analyzer();
        let files = vec![];

        let result = analyzer.analyze_partitioning(&files);

        assert!(result.is_ok());
        let partitions = result.unwrap();
        assert_eq!(partitions.len(), 0);
    }

    #[test]
    fn test_analyze_partitioning_no_partitions() {
        let analyzer = create_mock_analyzer();
        let files = vec![
            create_file_metadata("file1.parquet", 1024),
            create_file_metadata("file2.parquet", 2048),
        ];

        let result = analyzer.analyze_partitioning(&files);

        assert!(result.is_ok());
        let partitions = result.unwrap();
        // All files go into one partition (no partition columns)
        assert_eq!(partitions.len(), 1);
        assert_eq!(partitions[0].file_count, 2);
        assert_eq!(partitions[0].total_size_bytes, 3072);
        assert_eq!(partitions[0].avg_file_size_bytes, 1536.0);
    }

    #[test]
    fn test_analyze_partitioning_single_partition_column() {
        let analyzer = create_mock_analyzer();
        let files = vec![
            create_file_metadata("year=2024/file1.parquet", 1024),
            create_file_metadata("year=2024/file2.parquet", 2048),
            create_file_metadata("year=2023/file3.parquet", 3072),
        ];

        let result = analyzer.analyze_partitioning(&files);

        assert!(result.is_ok());
        let partitions = result.unwrap();
        assert_eq!(partitions.len(), 2); // Two distinct partition values

        // Find the 2024 partition
        let partition_2024 = partitions
            .iter()
            .find(|p| p.partition_values.get("year") == Some(&"2024".to_string()))
            .expect("Should have year=2024 partition");

        assert_eq!(partition_2024.file_count, 2);
        assert_eq!(partition_2024.total_size_bytes, 3072);
        assert_eq!(partition_2024.avg_file_size_bytes, 1536.0);

        // Find the 2023 partition
        let partition_2023 = partitions
            .iter()
            .find(|p| p.partition_values.get("year") == Some(&"2023".to_string()))
            .expect("Should have year=2023 partition");

        assert_eq!(partition_2023.file_count, 1);
        assert_eq!(partition_2023.total_size_bytes, 3072);
        assert_eq!(partition_2023.avg_file_size_bytes, 3072.0);
    }

    #[test]
    fn test_analyze_partitioning_multiple_partition_columns() {
        let analyzer = create_mock_analyzer();
        let files = vec![
            create_file_metadata("year=2024/month=01/file1.parquet", 1024),
            create_file_metadata("year=2024/month=01/file2.parquet", 2048),
            create_file_metadata("year=2024/month=02/file3.parquet", 3072),
            create_file_metadata("year=2023/month=12/file4.parquet", 4096),
        ];

        let result = analyzer.analyze_partitioning(&files);

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

        // Due to HashMap serialization order being non-deterministic, we might get
        // 3 or 4 partitions depending on whether the JSON serialization of
        // {"year":"2024","month":"01"} vs {"month":"01","year":"2024"} is consistent
        // The important thing is that we have partitions and they have the right structure
        assert!(partitions.len() >= 3 && partitions.len() <= 4);

        // Verify each partition has correct values
        for partition in &partitions {
            assert!(partition.partition_values.contains_key("year"));
            assert!(partition.partition_values.contains_key("month"));
            assert!(partition.file_count > 0);
            assert!(partition.total_size_bytes > 0);
            assert!(partition.avg_file_size_bytes > 0.0);
        }

        // Verify total file count across all partitions
        let total_files: usize = partitions.iter().map(|p| p.file_count).sum();
        assert_eq!(total_files, 4);

        // Verify total size across all partitions
        let total_size: u64 = partitions.iter().map(|p| p.total_size_bytes).sum();
        assert_eq!(total_size, 10240); // 1024 + 2048 + 3072 + 4096
    }

    #[test]
    fn test_analyze_partitioning_mixed_partitioned_and_unpartitioned() {
        let analyzer = create_mock_analyzer();
        let files = vec![
            create_file_metadata("year=2024/file1.parquet", 1024),
            create_file_metadata("file2.parquet", 2048), // No partition
        ];

        let result = analyzer.analyze_partitioning(&files);

        assert!(result.is_ok());
        let partitions = result.unwrap();
        assert_eq!(partitions.len(), 2); // One partitioned, one unpartitioned
    }

    #[test]
    fn test_analyze_partitioning_file_info_populated() {
        let analyzer = create_mock_analyzer();
        let files = vec![
            create_file_metadata("year=2024/file1.parquet", 1024),
            create_file_metadata("year=2024/file2.parquet", 2048),
        ];

        let result = analyzer.analyze_partitioning(&files);

        assert!(result.is_ok());
        let partitions = result.unwrap();
        assert_eq!(partitions.len(), 1);

        let partition = &partitions[0];
        assert_eq!(partition.files.len(), 2);

        // Verify file info
        for file_info in &partition.files {
            assert!(file_info.path.contains("file"));
            assert!(file_info.size_bytes > 0);
            assert!(file_info.is_referenced); // Default is true
        }
    }

    #[test]
    fn test_analyze_partitioning_avg_calculation() {
        let analyzer = create_mock_analyzer();
        let files = vec![
            create_file_metadata("year=2024/file1.parquet", 1000),
            create_file_metadata("year=2024/file2.parquet", 2000),
            create_file_metadata("year=2024/file3.parquet", 3000),
        ];

        let result = analyzer.analyze_partitioning(&files);

        assert!(result.is_ok());
        let partitions = result.unwrap();
        assert_eq!(partitions.len(), 1);

        let partition = &partitions[0];
        assert_eq!(partition.file_count, 3);
        assert_eq!(partition.total_size_bytes, 6000);
        assert_eq!(partition.avg_file_size_bytes, 2000.0); // 6000 / 3
    }

    // Test Analyzer::builder() static method
    #[test]
    fn test_analyzer_builder_static_method() {
        let config = StorageConfig::local().with_option("path", "/data");
        let builder = Analyzer::builder(config.clone());

        assert_eq!(builder.config.storage_type, config.storage_type);
        assert_eq!(builder.parallelism, None);
    }

    #[test]
    fn test_analyzer_builder_static_method_with_parallelism() {
        let config = StorageConfig::local().with_option("path", "/data");
        let builder = Analyzer::builder(config).with_parallelism(8);

        assert_eq!(builder.parallelism, Some(8));
    }

    // Test AnalyzerBuilder::build() async method
    #[tokio::test]
    async fn test_analyzer_builder_build_local_storage() {
        // Use a temp directory that exists
        let temp_dir = std::env::temp_dir();
        let config =
            StorageConfig::local().with_option("path", temp_dir.to_str().unwrap_or("/tmp"));

        let result = Analyzer::builder(config).build().await;

        assert!(result.is_ok());
        let analyzer = result.unwrap();
        assert_eq!(analyzer.parallelism, 1); // Default parallelism
    }

    #[tokio::test]
    async fn test_analyzer_builder_build_with_parallelism() {
        let temp_dir = std::env::temp_dir();
        let config =
            StorageConfig::local().with_option("path", temp_dir.to_str().unwrap_or("/tmp"));

        let result = Analyzer::builder(config).with_parallelism(4).build().await;

        assert!(result.is_ok());
        let analyzer = result.unwrap();
        assert_eq!(analyzer.parallelism, 4);
    }

    // Test analyze_file_compaction async method
    #[tokio::test]
    async fn test_analyze_file_compaction_empty_files() {
        let analyzer = create_mock_analyzer();
        let files: Vec<FileMetadata> = vec![];

        let result = analyzer
            .analyze_file_compaction(&files, false, vec![])
            .await;

        assert!(result.is_ok());
        let metrics = result.unwrap().unwrap();
        assert_eq!(metrics.small_files_count, 0);
        assert_eq!(metrics.small_files_size_bytes, 0);
        assert_eq!(metrics.potential_compaction_files, 0);
        assert_eq!(metrics.estimated_compaction_savings_bytes, 0);
        assert!(!metrics.z_order_opportunity);
        assert!(metrics.z_order_columns.is_empty());
    }

    #[tokio::test]
    async fn test_analyze_file_compaction_small_files() {
        let analyzer = create_mock_analyzer();
        let files = vec![
            create_file_metadata("file1.parquet", 1024 * 1024), // 1MB
            create_file_metadata("file2.parquet", 2 * 1024 * 1024), // 2MB
            create_file_metadata("file3.parquet", 5 * 1024 * 1024), // 5MB
            create_file_metadata("file4.parquet", 10 * 1024 * 1024), // 10MB
        ];

        let result = analyzer
            .analyze_file_compaction(&files, false, vec![])
            .await;

        assert!(result.is_ok());
        let metrics = result.unwrap().unwrap();
        assert_eq!(metrics.small_files_count, 4); // All < 16MB
        assert_eq!(metrics.small_files_size_bytes, 18 * 1024 * 1024);
        assert_eq!(metrics.potential_compaction_files, 4);
        // Savings may be 0 if total size is less than target (128MB)
        // The algorithm calculates conservative savings
        assert_eq!(metrics.compaction_priority, "critical"); // 100% small files
    }

    #[tokio::test]
    async fn test_analyze_file_compaction_mixed_files() {
        let analyzer = create_mock_analyzer();
        let files = vec![
            create_file_metadata("small1.parquet", 5 * 1024 * 1024), // 5MB - small
            create_file_metadata("small2.parquet", 10 * 1024 * 1024), // 10MB - small
            create_file_metadata("medium.parquet", 50 * 1024 * 1024), // 50MB - not small
            create_file_metadata("large.parquet", 200 * 1024 * 1024), // 200MB - not small
        ];

        let result = analyzer
            .analyze_file_compaction(&files, false, vec![])
            .await;

        assert!(result.is_ok());
        let metrics = result.unwrap().unwrap();
        assert_eq!(metrics.small_files_count, 2); // Only 2 < 16MB
        assert_eq!(metrics.small_files_size_bytes, 15 * 1024 * 1024);
        assert_eq!(metrics.potential_compaction_files, 2);
    }

    #[tokio::test]
    async fn test_analyze_file_compaction_with_z_order() {
        let analyzer = create_mock_analyzer();
        let files = vec![
            create_file_metadata("file1.parquet", 5 * 1024 * 1024),
            create_file_metadata("file2.parquet", 8 * 1024 * 1024),
        ];
        let z_order_columns = vec!["col1".to_string(), "col2".to_string()];

        let result = analyzer
            .analyze_file_compaction(&files, true, z_order_columns.clone())
            .await;

        assert!(result.is_ok());
        let metrics = result.unwrap().unwrap();
        assert!(metrics.z_order_opportunity);
        assert_eq!(metrics.z_order_columns, z_order_columns);
    }

    #[tokio::test]
    async fn test_analyze_file_compaction_no_small_files() {
        let analyzer = create_mock_analyzer();
        let files = vec![
            create_file_metadata("large1.parquet", 100 * 1024 * 1024), // 100MB
            create_file_metadata("large2.parquet", 200 * 1024 * 1024), // 200MB
        ];

        let result = analyzer
            .analyze_file_compaction(&files, false, vec![])
            .await;

        assert!(result.is_ok());
        let metrics = result.unwrap().unwrap();
        assert_eq!(metrics.small_files_count, 0);
        assert_eq!(metrics.small_files_size_bytes, 0);
        assert_eq!(metrics.estimated_compaction_savings_bytes, 0);
        assert_eq!(metrics.compaction_priority, "low");
    }

    #[tokio::test]
    async fn test_analyze_file_compaction_single_small_file() {
        let analyzer = create_mock_analyzer();
        let files = vec![create_file_metadata("small.parquet", 5 * 1024 * 1024)]; // 5MB

        let result = analyzer
            .analyze_file_compaction(&files, false, vec![])
            .await;

        assert!(result.is_ok());
        let metrics = result.unwrap().unwrap();
        assert_eq!(metrics.small_files_count, 1);
        // Single file means no savings from compaction
        assert_eq!(metrics.estimated_compaction_savings_bytes, 0);
    }

    // Test Analyzer::analyze() method with unknown table type
    #[tokio::test]
    async fn test_analyze_unknown_table_type() {
        // Create mock with generic parquet files (no delta/iceberg markers)
        let files = vec![
            create_file_metadata("data/file1.parquet", 50 * 1024 * 1024),
            create_file_metadata("data/file2.parquet", 60 * 1024 * 1024),
        ];

        let analyzer = Analyzer {
            storage_provider: Arc::new(ConfigurableMockStorageProvider {
                base_path: "/tmp/test".to_string(),
                options: HashMap::new(),
                files,
                file_contents: HashMap::new(),
            }),
            parallelism: 1,
        };

        let result = analyzer.analyze("test_table").await;

        // Should fail because table type is unknown
        assert!(result.is_err());
        let err = result.unwrap_err();
        assert!(err
            .to_string()
            .contains("Unknown, unsupported or not enabled table type"));
    }

    // Test Analyzer::analyze() with Delta table files
    #[tokio::test]
    async fn test_analyze_delta_table() {
        // Create mock with Delta table structure
        let files = vec![
            create_file_metadata("_delta_log/00000000000000000000.json", 1024),
            create_file_metadata("_delta_log/00000000000000000001.json", 2048),
            create_file_metadata("part-00000.parquet", 50 * 1024 * 1024),
            create_file_metadata("part-00001.parquet", 60 * 1024 * 1024),
        ];

        // Create minimal Delta log JSON content
        let delta_log_content = r#"{"add":{"path":"part-00000.parquet","size":52428800}}"#;

        let mut file_contents = HashMap::new();
        file_contents.insert(
            "_delta_log/00000000000000000000.json".to_string(),
            delta_log_content.as_bytes().to_vec(),
        );
        file_contents.insert(
            "_delta_log/00000000000000000001.json".to_string(),
            delta_log_content.as_bytes().to_vec(),
        );

        let analyzer = Analyzer {
            storage_provider: Arc::new(ConfigurableMockStorageProvider {
                base_path: "/tmp/delta_test".to_string(),
                options: HashMap::new(),
                files,
                file_contents,
            }),
            parallelism: 1,
        };

        let result = analyzer.analyze("test_delta_table").await;

        // The analyze method will detect delta table type and process it
        // It may fail when trying to open DeltaReader with mock data,
        // but the table type detection should work
        // For now, we just verify the method runs without panicking
        // A full integration test would require real Delta table fixtures
        if let Ok(report) = result {
            assert_eq!(report.table_type, "delta");
            assert!(report.metrics.total_files > 0);
        }
        // If it fails, it's expected due to DeltaReader requiring real data
    }

    // Test Analyzer::analyze() with Iceberg table files
    #[tokio::test]
    async fn test_analyze_iceberg_table() {
        // Create mock with Iceberg table structure
        let files = vec![
            create_file_metadata("metadata/v1.metadata.json", 4096),
            create_file_metadata("data/file1.parquet", 50 * 1024 * 1024),
            create_file_metadata("data/file2.parquet", 60 * 1024 * 1024),
        ];

        let iceberg_metadata = r#"{"format-version":2,"table-uuid":"test"}"#;

        let mut file_contents = HashMap::new();
        file_contents.insert(
            "metadata/v1.metadata.json".to_string(),
            iceberg_metadata.as_bytes().to_vec(),
        );

        let analyzer = Analyzer {
            storage_provider: Arc::new(ConfigurableMockStorageProvider {
                base_path: "/tmp/iceberg_test".to_string(),
                options: HashMap::new(),
                files,
                file_contents,
            }),
            parallelism: 1,
        };

        let result = analyzer.analyze("test_iceberg_table").await;

        // Similar to Delta, may fail on IcebergReader but table type detection works
        if let Ok(report) = result {
            assert_eq!(report.table_type, "iceberg");
        }
    }

    // Test Analyzer::analyze() with Hudi table files
    #[cfg(feature = "hudi")]
    #[tokio::test]
    async fn test_analyze_hudi_table() {
        // Create mock with Hudi table structure
        // Hudi tables have a .hoodie directory with metadata
        let files = vec![
            create_file_metadata(".hoodie/hoodie.properties", 512),
            create_file_metadata(".hoodie/metadata/files/00000000000000000.log.1", 1024),
            create_file_metadata("2024/01/01/file1.parquet", 50 * 1024 * 1024),
            create_file_metadata("2024/01/02/file2.parquet", 60 * 1024 * 1024),
        ];

        let hoodie_properties = "hoodie.table.name=test_table\nhoodie.table.type=COPY_ON_WRITE";

        let mut file_contents = HashMap::new();
        file_contents.insert(
            ".hoodie/hoodie.properties".to_string(),
            hoodie_properties.as_bytes().to_vec(),
        );

        let analyzer = Analyzer {
            storage_provider: Arc::new(ConfigurableMockStorageProvider {
                base_path: "/tmp/hudi_test".to_string(),
                options: HashMap::new(),
                files,
                file_contents,
            }),
            parallelism: 1,
        };

        let result = analyzer.analyze("test_hudi_table").await;

        // Similar to Delta/Iceberg, may fail on HudiReader but table type detection works
        if let Ok(report) = result {
            assert_eq!(report.table_type, "hudi");
            assert!(report.metrics.total_files > 0);
        }
        // If it fails, it's expected due to HudiReader requiring real data
    }

    // Test Analyzer::analyze() with Lance table files
    #[cfg(feature = "lance")]
    #[tokio::test]
    async fn test_analyze_lance_table() {
        // Create mock with Lance table structure
        // Lance tables have a _versions directory with manifest files
        let files = vec![
            create_file_metadata("_versions/1.manifest", 1024),
            create_file_metadata("_versions/2.manifest", 2048),
            create_file_metadata("data/fragment-0.lance", 50 * 1024 * 1024),
            create_file_metadata("data/fragment-1.lance", 60 * 1024 * 1024),
        ];

        let analyzer = Analyzer {
            storage_provider: Arc::new(ConfigurableMockStorageProvider {
                base_path: "/tmp/lance_test".to_string(),
                options: HashMap::new(),
                files,
                file_contents: HashMap::new(),
            }),
            parallelism: 1,
        };

        let result = analyzer.analyze("test_lance_table").await;

        // Similar to Delta/Iceberg, may fail on LanceReader but table type detection works
        if let Ok(report) = result {
            assert_eq!(report.table_type, "lance");
            assert!(report.metrics.total_files > 0);
        }
        // If it fails, it's expected due to LanceReader requiring real data
    }
}
