//! Schema type definitions for altimate-core.
//!
//! These types represent the schema that SQL queries are validated against.
//! Schemas can be loaded from YAML, JSON, or DDL files.

use crate::glossary::types::Glossary;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;

/// Top-level schema definition.
///
/// Contains all tables and their column definitions, plus an optional
/// SQL dialect that affects parsing behavior.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SchemaDefinition {
    /// Schema format version
    #[serde(default = "default_version")]
    pub version: String,

    /// SQL dialect for parsing. Affects keyword recognition and syntax rules.
    #[serde(default)]
    pub dialect: SqlDialect,

    /// Database/catalog name for fully-qualified table references.
    /// Maps to DataFusion's catalog name. Defaults to "altimate-core".
    /// Set this to match your data warehouse's database name
    /// (e.g., "EBATES_PROD" for Snowflake) so queries using
    /// `DATABASE.SCHEMA.TABLE` resolve correctly.
    #[serde(default)]
    pub database: Option<String>,

    /// Schema name for fully-qualified table references.
    /// Maps to DataFusion's schema name. Defaults to "public".
    /// Set this to match your data warehouse's schema name
    /// (e.g., "DW", "CAMPAIGN_ASSET") so queries using
    /// `DATABASE.SCHEMA.TABLE` resolve correctly.
    #[serde(default)]
    pub schema_name: Option<String>,

    /// Table definitions keyed by table name
    pub tables: HashMap<String, TableDef>,

    /// Business glossary: maps business terms to schema elements
    #[serde(default)]
    pub glossary: Option<Glossary>,
}

fn default_version() -> String {
    "1".to_string()
}

/// Table-level statistics for cost estimation.
///
/// All fields optional — existing schemas work unchanged (backwards compatible).
/// When provided, enables static cost estimation for cloud DWH billing prediction.
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct TableStatistics {
    /// Approximate row count
    #[serde(default)]
    pub row_count: Option<u64>,

    /// Average row size in bytes
    #[serde(default)]
    pub avg_row_bytes: Option<u32>,

    /// Whether the table is partitioned
    #[serde(default)]
    pub partitioned: bool,

    /// Partition column names
    #[serde(default)]
    pub partition_columns: Vec<String>,

    /// Groups of correlated columns (don't multiply selectivity independently).
    /// Inspired by SQL Server 2016 CE research on multi-predicate selectivity.
    /// E.g., `[["status", "type"]]` means status and type are correlated.
    #[serde(default)]
    pub correlated_columns: Vec<Vec<String>>,

    /// Number of micro-partitions / storage blocks for this table.
    /// When available, improves LIMIT estimation accuracy by computing
    /// single-partition size as `total_bytes / partition_count` instead
    /// of using the conservative 50K rows-per-partition heuristic.
    /// For Snowflake, this is `GET_DDL`'s `TOTAL_PARTITION_COUNT`.
    #[serde(default)]
    pub partition_count: Option<u64>,

    /// Actual compressed storage bytes (from Snowflake ACTIVE_BYTES or similar).
    /// When available, replaces `row_count * avg_row_bytes` for more accurate
    /// table size estimation (eliminates compression estimation error).
    #[serde(default)]
    pub total_bytes: Option<u64>,

    /// Historical median bytes_scanned per query touching this table.
    /// From production query history (e.g., table_scan_profiles.json).
    /// Used by v4 estimator as Tier 1: best single predictor of actual scan cost.
    #[serde(default)]
    pub historical_scan_median: Option<u64>,
}

/// Per-column statistics for selectivity estimation.
///
/// Inspired by PostgreSQL's `pg_stats` view and the SafeBound (SIGMOD 2023)
/// and LpBound (SIGMOD 2025 Best Paper) research on cardinality estimation.
/// Even simple per-column statistics dramatically improve selectivity estimates.
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct ColumnStatistics {
    /// Number of distinct values. Positive = exact count, negative = fraction of rows
    /// (e.g., -1.0 means all values unique, -0.5 means 50% unique).
    /// Follows PostgreSQL's `n_distinct` convention.
    /// Used for equality selectivity: `sel = 1 / n_distinct`.
    #[serde(default)]
    pub n_distinct: Option<f64>,

    /// Fraction of NULL values (0.0 to 1.0).
    /// Used to adjust selectivity: `sel *= (1.0 - null_fraction)`.
    #[serde(default)]
    pub null_fraction: Option<f64>,

    /// Minimum value as string (for range selectivity estimation).
    #[serde(default)]
    pub min_value: Option<String>,

    /// Maximum value as string (for range selectivity estimation).
    #[serde(default)]
    pub max_value: Option<String>,

    /// Most common values with their frequencies (top-K).
    /// Used for skewed distributions where uniform assumptions fail.
    #[serde(default)]
    pub most_common_values: Vec<McvEntry>,

    /// Average logical byte size per value for this column.
    /// When provided, enables precise per-column byte estimation instead of
    /// uniform column-size assumptions. For Snowflake, can be derived from
    /// TABLE_STORAGE_METRICS.ACTIVE_BYTES divided by column count approximation.
    #[serde(default)]
    pub avg_bytes: Option<u64>,

    /// Compression ratio hint for storage cost estimation.
    /// Only affects storage cost, not query cost (which uses logical bytes).
    /// Typical values: 3.0-8.0 for integers, 5.0-20.0 for short strings.
    #[serde(default)]
    pub compression_ratio: Option<f64>,
}

/// A most-common-value entry with its frequency.
///
/// Represents a (value, frequency) pair from a column's MCV list.
/// Frequency is a fraction (0.0 to 1.0) of rows containing this value.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct McvEntry {
    /// The value as a string representation.
    pub value: String,
    /// Fraction of rows with this value (0.0 to 1.0).
    pub frequency: f64,
}

/// A table definition with columns and constraints.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TableDef {
    /// Human-readable description (exposed to agents for context)
    #[serde(default)]
    pub description: Option<String>,

    /// Database/catalog override for this table. When set, the table is
    /// registered under this catalog instead of the top-level `database` default.
    #[serde(default)]
    pub database: Option<String>,

    /// Schema override for this table. When set, the table is registered
    /// under this schema instead of the top-level `schema_name` default.
    /// Together with `database`, enables a single YAML file to describe
    /// tables across multiple catalogs and schemas.
    #[serde(default)]
    pub schema: Option<String>,

    /// Column definitions
    pub columns: Vec<ColumnDef>,

    /// Primary key columns
    #[serde(default)]
    pub primary_key: Option<Vec<String>>,

    /// Foreign key constraints
    #[serde(default)]
    pub foreign_keys: Vec<ForeignKeyDef>,

    /// Table statistics for cost estimation (optional)
    #[serde(default)]
    pub statistics: Option<TableStatistics>,

    /// Clustering/sort key columns that determine physical data ordering.
    /// When filters target clustering keys, partition pruning is more effective
    /// because data is physically sorted, enabling zone map skipping.
    /// Example: `["order_date"]` for a Snowflake table clustered on order_date.
    #[serde(default)]
    pub clustering_keys: Vec<String>,
}

/// A column definition with type and metadata.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ColumnDef {
    /// Column name
    pub name: String,

    /// SQL data type as a string (e.g., "INT", "VARCHAR", "TIMESTAMP", "DECIMAL(10,2)")
    #[serde(rename = "type")]
    pub data_type: String,

    /// Whether the column allows NULL values
    #[serde(default = "default_true")]
    pub nullable: bool,

    /// Human-readable description (exposed to agents for context)
    #[serde(default)]
    pub description: Option<String>,

    /// Semantic tags (e.g., "pii", "fiscal", "deprecated")
    #[serde(default)]
    pub tags: Vec<String>,

    /// Business synonyms (e.g., ["revenue", "sales", "amount"])
    #[serde(default)]
    pub synonyms: Vec<String>,

    /// Per-column statistics for selectivity estimation (optional).
    /// When present, enables research-grade cardinality estimation.
    #[serde(default)]
    pub statistics: Option<ColumnStatistics>,
}

fn default_true() -> bool {
    true
}

/// Foreign key constraint definition.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ForeignKeyDef {
    /// Columns in this table that form the foreign key
    pub columns: Vec<String>,

    /// The referenced table and columns
    pub references: ForeignKeyRef,
}

/// The target of a foreign key reference.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ForeignKeyRef {
    /// Referenced table name
    pub table: String,

    /// Referenced column names
    pub columns: Vec<String>,
}

/// SQL dialect for parsing and validation.
///
/// Supports 32 dialects, matching the full set from the `polyglot-sql` crate.
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum SqlDialect {
    #[default]
    Generic,
    Snowflake,
    Postgres,
    BigQuery,
    Databricks,
    DuckDB,
    Athena,
    ClickHouse,
    CockroachDB,
    DataFusion,
    Doris,
    Dremio,
    Drill,
    Druid,
    Dune,
    Exasol,
    Fabric,
    Hive,
    Materialize,
    MySQL,
    Oracle,
    Presto,
    Redshift,
    RisingWave,
    SingleStore,
    Solr,
    Spark,
    SQLite,
    StarRocks,
    Tableau,
    Teradata,
    TiDB,
    Trino,
    TSQL,
}

impl std::fmt::Display for SqlDialect {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            SqlDialect::Generic => write!(f, "generic"),
            SqlDialect::Snowflake => write!(f, "snowflake"),
            SqlDialect::Postgres => write!(f, "postgres"),
            SqlDialect::BigQuery => write!(f, "bigquery"),
            SqlDialect::Databricks => write!(f, "databricks"),
            SqlDialect::DuckDB => write!(f, "duckdb"),
            SqlDialect::Athena => write!(f, "athena"),
            SqlDialect::ClickHouse => write!(f, "clickhouse"),
            SqlDialect::CockroachDB => write!(f, "cockroachdb"),
            SqlDialect::DataFusion => write!(f, "datafusion"),
            SqlDialect::Doris => write!(f, "doris"),
            SqlDialect::Dremio => write!(f, "dremio"),
            SqlDialect::Drill => write!(f, "drill"),
            SqlDialect::Druid => write!(f, "druid"),
            SqlDialect::Dune => write!(f, "dune"),
            SqlDialect::Exasol => write!(f, "exasol"),
            SqlDialect::Fabric => write!(f, "fabric"),
            SqlDialect::Hive => write!(f, "hive"),
            SqlDialect::Materialize => write!(f, "materialize"),
            SqlDialect::MySQL => write!(f, "mysql"),
            SqlDialect::Oracle => write!(f, "oracle"),
            SqlDialect::Presto => write!(f, "presto"),
            SqlDialect::Redshift => write!(f, "redshift"),
            SqlDialect::RisingWave => write!(f, "risingwave"),
            SqlDialect::SingleStore => write!(f, "singlestore"),
            SqlDialect::Solr => write!(f, "solr"),
            SqlDialect::Spark => write!(f, "spark"),
            SqlDialect::SQLite => write!(f, "sqlite"),
            SqlDialect::StarRocks => write!(f, "starrocks"),
            SqlDialect::Tableau => write!(f, "tableau"),
            SqlDialect::Teradata => write!(f, "teradata"),
            SqlDialect::TiDB => write!(f, "tidb"),
            SqlDialect::Trino => write!(f, "trino"),
            SqlDialect::TSQL => write!(f, "tsql"),
        }
    }
}

impl ColumnDef {
    /// Resolve `n_distinct` to an absolute count given the table's row count.
    ///
    /// PostgreSQL convention: positive values are exact counts, negative values
    /// are fractions of the row count (e.g., -1.0 means every row is unique,
    /// -0.5 means 50% of rows have unique values).
    ///
    /// Returns `None` if no statistics or no `n_distinct` is set.
    pub fn n_distinct(&self, table_row_count: u64) -> Option<f64> {
        let stats = self.statistics.as_ref()?;
        let nd = stats.n_distinct?;
        if nd >= 0.0 {
            Some(nd)
        } else {
            // Negative: fraction of row count (e.g., -1.0 = all unique)
            Some(nd.abs() * table_row_count as f64)
        }
    }
}

impl TableDef {
    /// Look up column-level statistics by column name.
    ///
    /// Returns `None` if the column doesn't exist or has no statistics.
    pub fn column_stats(&self, col_name: &str) -> Option<&ColumnStatistics> {
        self.columns
            .iter()
            .find(|c| c.name == col_name)?
            .statistics
            .as_ref()
    }
}

impl SchemaDefinition {
    /// Get all table names in the schema
    pub fn table_names(&self) -> Vec<&str> {
        self.tables.keys().map(|s| s.as_str()).collect()
    }

    /// Get all column names for a given table
    pub fn column_names(&self, table: &str) -> Option<Vec<&str>> {
        self.tables
            .get(table)
            .map(|t| t.columns.iter().map(|c| c.name.as_str()).collect())
    }

    /// Get all column names across all tables (for global fuzzy matching)
    pub fn all_column_names(&self) -> Vec<(&str, &str)> {
        self.tables
            .iter()
            .flat_map(|(table, def)| {
                def.columns
                    .iter()
                    .map(move |col| (table.as_str(), col.name.as_str()))
            })
            .collect()
    }
}

/// Estimate logical bytes per value for a column based on its SQL data type.
/// Uses column statistics avg_bytes if available, otherwise heuristic from type.
/// Returns uncompressed/logical bytes (what Snowflake BYTES_SCANNED reports).
pub fn estimate_column_logical_bytes(data_type: &str, stats: Option<&ColumnStatistics>) -> u64 {
    // If stats provide explicit avg_bytes, use that
    if let Some(s) = stats
        && let Some(avg) = s.avg_bytes
    {
        return avg;
    }

    let upper = data_type.to_uppercase();
    let upper = upper.trim();

    // Extract precision from types like VARCHAR(255), DECIMAL(10,2)
    let extract_paren = |s: &str| -> Option<u64> {
        s.find('(')
            .and_then(|start| {
                s[start + 1..]
                    .find(')')
                    .map(|end| &s[start + 1..start + 1 + end])
            })
            .and_then(|inner| inner.split(',').next())
            .and_then(|n| n.trim().parse::<u64>().ok())
    };

    if upper.starts_with("BOOL") {
        return 1;
    }
    if upper.starts_with("TINYINT") || upper.starts_with("INT1") {
        return 1;
    }
    if upper.starts_with("SMALLINT") || upper.starts_with("INT2") {
        return 2;
    }
    if upper == "INT" || upper == "INTEGER" || upper.starts_with("INT4") {
        return 4;
    }
    if upper.starts_with("BIGINT") || upper.starts_with("INT8") || upper == "NUMBER" {
        return 8;
    }
    if upper.starts_with("FLOAT") && !upper.starts_with("FLOAT8") {
        return 4;
    }
    if upper.starts_with("REAL") {
        return 4;
    }
    if upper.starts_with("DOUBLE") || upper.starts_with("FLOAT8") {
        return 8;
    }
    if upper.starts_with("DECIMAL") || upper.starts_with("NUMERIC") {
        return 16;
    }
    if upper == "DATE" {
        return 4;
    }
    if upper.starts_with("TIME") && !upper.starts_with("TIMESTAMP") {
        return 8;
    }
    if upper.starts_with("TIMESTAMP") || upper.starts_with("DATETIME") {
        return 8;
    }
    if upper.starts_with("VARCHAR") || upper.starts_with("STRING") || upper.starts_with("TEXT") {
        return extract_paren(upper).map(|n| n.min(200)).unwrap_or(50);
    }
    if upper.starts_with("CHAR") {
        return extract_paren(upper).unwrap_or(10);
    }
    if upper.starts_with("BINARY") || upper.starts_with("VARBINARY") || upper.starts_with("BYTES") {
        return extract_paren(upper).unwrap_or(100);
    }
    if upper.starts_with("UUID") {
        return 16;
    }
    if upper.starts_with("VARIANT") || upper.starts_with("OBJECT") || upper.starts_with("ARRAY") {
        return 200;
    }
    if upper.starts_with("JSON") || upper.starts_with("JSONB") {
        return 200;
    }
    if upper.starts_with("GEOGRAPHY") || upper.starts_with("GEOMETRY") {
        return 200;
    }

    50 // Default for unknown types
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_parse_column_statistics_full() {
        let yaml = r#"
version: "1"
tables:
  orders:
    columns:
      - name: status
        type: VARCHAR
        nullable: false
        statistics:
          n_distinct: 5
          null_fraction: 0.0
          min_value: "cancelled"
          max_value: "shipped"
          most_common_values:
            - { value: "completed", frequency: 0.60 }
            - { value: "pending", frequency: 0.20 }
"#;
        let schema: SchemaDefinition = serde_yaml::from_str(yaml).unwrap();
        let col = &schema.tables["orders"].columns[0];
        assert_eq!(col.name, "status");
        let stats = col.statistics.as_ref().unwrap();
        assert_eq!(stats.n_distinct, Some(5.0));
        assert_eq!(stats.null_fraction, Some(0.0));
        assert_eq!(stats.min_value.as_deref(), Some("cancelled"));
        assert_eq!(stats.max_value.as_deref(), Some("shipped"));
        assert_eq!(stats.most_common_values.len(), 2);
        assert_eq!(stats.most_common_values[0].value, "completed");
        assert!((stats.most_common_values[0].frequency - 0.60).abs() < f64::EPSILON);
        assert_eq!(stats.most_common_values[1].value, "pending");
        assert!((stats.most_common_values[1].frequency - 0.20).abs() < f64::EPSILON);
    }

    #[test]
    fn test_parse_column_statistics_partial() {
        let yaml = r#"
version: "1"
tables:
  orders:
    columns:
      - name: id
        type: INT
        nullable: false
        statistics:
          n_distinct: -1.0
"#;
        let schema: SchemaDefinition = serde_yaml::from_str(yaml).unwrap();
        let stats = schema.tables["orders"].columns[0]
            .statistics
            .as_ref()
            .unwrap();
        assert_eq!(stats.n_distinct, Some(-1.0));
        assert_eq!(stats.null_fraction, None);
        assert_eq!(stats.min_value, None);
        assert_eq!(stats.max_value, None);
        assert!(stats.most_common_values.is_empty());
    }

    #[test]
    fn test_backwards_compat_no_statistics() {
        let yaml = r#"
version: "1"
tables:
  users:
    columns:
      - name: id
        type: INT
        nullable: false
      - name: name
        type: VARCHAR
"#;
        let schema: SchemaDefinition = serde_yaml::from_str(yaml).unwrap();
        let col = &schema.tables["users"].columns[0];
        assert!(col.statistics.is_none());
    }

    #[test]
    fn test_backwards_compat_no_correlated_columns() {
        let yaml = r#"
version: "1"
tables:
  orders:
    columns:
      - name: id
        type: INT
    statistics:
      row_count: 1000
      avg_row_bytes: 64
"#;
        let schema: SchemaDefinition = serde_yaml::from_str(yaml).unwrap();
        let stats = schema.tables["orders"].statistics.as_ref().unwrap();
        assert!(stats.correlated_columns.is_empty());
    }

    #[test]
    fn test_parse_correlated_columns() {
        let yaml = r#"
version: "1"
tables:
  orders:
    columns:
      - name: status
        type: VARCHAR
      - name: region
        type: VARCHAR
    statistics:
      row_count: 1000000
      correlated_columns:
        - ["status", "region"]
"#;
        let schema: SchemaDefinition = serde_yaml::from_str(yaml).unwrap();
        let stats = schema.tables["orders"].statistics.as_ref().unwrap();
        assert_eq!(stats.correlated_columns.len(), 1);
        assert_eq!(stats.correlated_columns[0], vec!["status", "region"]);
    }

    #[test]
    fn test_parse_multiple_correlation_groups() {
        let yaml = r#"
version: "1"
tables:
  orders:
    columns:
      - name: status
        type: VARCHAR
      - name: region
        type: VARCHAR
      - name: channel
        type: VARCHAR
      - name: payment_type
        type: VARCHAR
    statistics:
      row_count: 1000000
      correlated_columns:
        - ["status", "region"]
        - ["channel", "payment_type"]
"#;
        let schema: SchemaDefinition = serde_yaml::from_str(yaml).unwrap();
        let stats = schema.tables["orders"].statistics.as_ref().unwrap();
        assert_eq!(stats.correlated_columns.len(), 2);
        assert_eq!(stats.correlated_columns[0], vec!["status", "region"]);
        assert_eq!(stats.correlated_columns[1], vec!["channel", "payment_type"]);
    }

    #[test]
    fn test_n_distinct_positive_exact_count() {
        let col = ColumnDef {
            name: "status".to_string(),
            data_type: "VARCHAR".to_string(),
            nullable: false,
            description: None,
            tags: vec![],
            synonyms: vec![],
            statistics: Some(ColumnStatistics {
                n_distinct: Some(5.0),
                ..Default::default()
            }),
        };
        assert_eq!(col.n_distinct(1_000_000), Some(5.0));
    }

    #[test]
    fn test_n_distinct_negative_fraction() {
        let col = ColumnDef {
            name: "id".to_string(),
            data_type: "INT".to_string(),
            nullable: false,
            description: None,
            tags: vec![],
            synonyms: vec![],
            statistics: Some(ColumnStatistics {
                n_distinct: Some(-1.0),
                ..Default::default()
            }),
        };
        assert_eq!(col.n_distinct(1_000_000), Some(1_000_000.0));
    }

    #[test]
    fn test_n_distinct_negative_half() {
        let col = ColumnDef {
            name: "user_id".to_string(),
            data_type: "INT".to_string(),
            nullable: false,
            description: None,
            tags: vec![],
            synonyms: vec![],
            statistics: Some(ColumnStatistics {
                n_distinct: Some(-0.5),
                ..Default::default()
            }),
        };
        assert_eq!(col.n_distinct(1_000_000), Some(500_000.0));
    }

    #[test]
    fn test_n_distinct_no_statistics() {
        let col = ColumnDef {
            name: "id".to_string(),
            data_type: "INT".to_string(),
            nullable: false,
            description: None,
            tags: vec![],
            synonyms: vec![],
            statistics: None,
        };
        assert_eq!(col.n_distinct(1_000_000), None);
    }

    #[test]
    fn test_n_distinct_no_value() {
        let col = ColumnDef {
            name: "id".to_string(),
            data_type: "INT".to_string(),
            nullable: false,
            description: None,
            tags: vec![],
            synonyms: vec![],
            statistics: Some(ColumnStatistics {
                n_distinct: None,
                null_fraction: Some(0.0),
                ..Default::default()
            }),
        };
        assert_eq!(col.n_distinct(1_000_000), None);
    }

    #[test]
    fn test_column_stats_found() {
        let table = TableDef {
            description: None,
            database: None,
            schema: None,
            columns: vec![
                ColumnDef {
                    name: "id".to_string(),
                    data_type: "INT".to_string(),
                    nullable: false,
                    description: None,
                    tags: vec![],
                    synonyms: vec![],
                    statistics: Some(ColumnStatistics {
                        n_distinct: Some(-1.0),
                        null_fraction: Some(0.0),
                        ..Default::default()
                    }),
                },
                ColumnDef {
                    name: "status".to_string(),
                    data_type: "VARCHAR".to_string(),
                    nullable: true,
                    description: None,
                    tags: vec![],
                    synonyms: vec![],
                    statistics: Some(ColumnStatistics {
                        n_distinct: Some(5.0),
                        ..Default::default()
                    }),
                },
            ],
            primary_key: None,
            foreign_keys: vec![],
            statistics: None,
            clustering_keys: vec![],
        };
        let stats = table.column_stats("status").unwrap();
        assert_eq!(stats.n_distinct, Some(5.0));
    }

    #[test]
    fn test_column_stats_not_found() {
        let table = TableDef {
            description: None,
            database: None,
            schema: None,
            columns: vec![ColumnDef {
                name: "id".to_string(),
                data_type: "INT".to_string(),
                nullable: false,
                description: None,
                tags: vec![],
                synonyms: vec![],
                statistics: None,
            }],
            primary_key: None,
            foreign_keys: vec![],
            statistics: None,
            clustering_keys: vec![],
        };
        assert!(table.column_stats("nonexistent").is_none());
        assert!(table.column_stats("id").is_none());
    }

    #[test]
    fn test_mcv_entries_roundtrip() {
        let stats = ColumnStatistics {
            n_distinct: Some(3.0),
            null_fraction: None,
            min_value: None,
            max_value: None,
            most_common_values: vec![
                McvEntry {
                    value: "active".to_string(),
                    frequency: 0.7,
                },
                McvEntry {
                    value: "inactive".to_string(),
                    frequency: 0.2,
                },
                McvEntry {
                    value: "suspended".to_string(),
                    frequency: 0.1,
                },
            ],
            avg_bytes: None,
            compression_ratio: None,
        };
        let json = serde_json::to_string(&stats).unwrap();
        let parsed: ColumnStatistics = serde_json::from_str(&json).unwrap();
        assert_eq!(parsed.most_common_values.len(), 3);
        assert_eq!(parsed.most_common_values[0].value, "active");
        assert!((parsed.most_common_values[0].frequency - 0.7).abs() < f64::EPSILON);
    }

    #[test]
    fn test_benchmark_warehouse_schema_loads() {
        let path = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
            .join("../../tests/fixtures/schemas/benchmark_warehouse.yaml");
        let content = std::fs::read_to_string(&path).unwrap();
        let schema: SchemaDefinition = serde_yaml::from_str(&content).unwrap();
        assert!(schema.tables.contains_key("fact_orders"));
        assert!(schema.tables.contains_key("dim_customers"));

        let orders = &schema.tables["fact_orders"];
        let id_stats = orders.column_stats("id");
        assert!(id_stats.is_some(), "fact_orders.id should have statistics");
        assert_eq!(id_stats.unwrap().n_distinct, Some(-1.0));

        let status_stats = orders.column_stats("status");
        assert!(
            status_stats.is_some(),
            "fact_orders.status should have statistics"
        );
        assert!(!status_stats.unwrap().most_common_values.is_empty());

        let table_stats = orders.statistics.as_ref().unwrap();
        assert!(
            !table_stats.correlated_columns.is_empty(),
            "fact_orders should have correlated_columns"
        );
    }
}
