//! Comprehensive tests for research-driven cost estimation features.
//!
//! Tests cover:
//! - Column statistics deserialization from YAML (with/without stats)
//! - Stats-driven selectivity for equality, range, IN list, IS NULL, LIKE predicates
//! - MCV hit vs miss selectivity
//! - n_distinct positive (absolute) vs negative (fraction) handling
//! - Range selectivity with min/max bounds (numeric and date)
//! - Confidence interval ordering (optimistic <= expected <= pessimistic)
//! - Correlation group handling (correlated vs uncorrelated columns)
//! - Join selectivity (FK join, n_distinct-based, default fallback)
//! - CostRange presence/absence in estimate output
//! - Full integration: queries with column stats produce different cost than without
//! - Edge cases: zero n_distinct, 100% null_fraction, empty MCV list, no min/max

use altimate_core::complexity::cost_estimator::estimate_cost;
use altimate_core::complexity::types::{
    CostRange, CostTier, EstimationConfidence, SelectivityEstimate,
};
use altimate_core::explainer::types::{JoinKind, PlanOperation, PlanStep};
use altimate_core::schema::types::{
    ColumnDef, ColumnStatistics, ForeignKeyDef, ForeignKeyRef, McvEntry, SchemaDefinition,
    SqlDialect, TableDef, TableStatistics,
};
use std::collections::HashMap;
use std::path::Path;

// ============================================================================
// Helper: build schemas with varying levels of column statistics
// ============================================================================

/// Schema with rich column-level statistics on orders and customers tables.
fn schema_with_rich_stats() -> SchemaDefinition {
    let mut tables = HashMap::new();
    tables.insert(
        "orders".to_string(),
        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), // all unique
                        null_fraction: Some(0.0),
                        min_value: Some("1".to_string()),
                        max_value: Some("1000000".to_string()),
                        most_common_values: vec![],

                        avg_bytes: None,
                        compression_ratio: None,
                    }),
                },
                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),
                        null_fraction: Some(0.01),
                        min_value: None,
                        max_value: None,
                        most_common_values: vec![
                            McvEntry {
                                value: "completed".to_string(),
                                frequency: 0.60,
                            },
                            McvEntry {
                                value: "pending".to_string(),
                                frequency: 0.20,
                            },
                            McvEntry {
                                value: "cancelled".to_string(),
                                frequency: 0.10,
                            },
                        ],

                        avg_bytes: None,
                        compression_ratio: None,
                    }),
                },
                ColumnDef {
                    name: "amount".to_string(),
                    data_type: "DECIMAL(10,2)".to_string(),
                    nullable: false,
                    description: None,
                    tags: vec![],
                    synonyms: vec![],
                    statistics: Some(ColumnStatistics {
                        n_distinct: Some(-0.8), // 80% unique (fraction)
                        null_fraction: Some(0.0),
                        min_value: Some("0.01".to_string()),
                        max_value: Some("99999.99".to_string()),
                        most_common_values: vec![],

                        avg_bytes: None,
                        compression_ratio: None,
                    }),
                },
                ColumnDef {
                    name: "region".to_string(),
                    data_type: "VARCHAR".to_string(),
                    nullable: true,
                    description: None,
                    tags: vec![],
                    synonyms: vec![],
                    statistics: Some(ColumnStatistics {
                        n_distinct: Some(50.0),
                        null_fraction: Some(0.02),
                        min_value: None,
                        max_value: None,
                        most_common_values: vec![McvEntry {
                            value: "US-East".to_string(),
                            frequency: 0.25,
                        }],

                        avg_bytes: None,
                        compression_ratio: None,
                    }),
                },
                ColumnDef {
                    name: "notes".to_string(),
                    data_type: "VARCHAR".to_string(),
                    nullable: true,
                    description: None,
                    tags: vec![],
                    synonyms: vec![],
                    statistics: Some(ColumnStatistics {
                        n_distinct: None,
                        null_fraction: Some(0.40),
                        min_value: None,
                        max_value: None,
                        most_common_values: vec![],

                        avg_bytes: None,
                        compression_ratio: None,
                    }),
                },
                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(50000.0),
                        null_fraction: Some(0.0),
                        min_value: None,
                        max_value: None,
                        most_common_values: vec![],

                        avg_bytes: None,
                        compression_ratio: None,
                    }),
                },
                ColumnDef {
                    name: "order_date".to_string(),
                    data_type: "DATE".to_string(),
                    nullable: false,
                    description: None,
                    tags: vec![],
                    synonyms: vec![],
                    statistics: Some(ColumnStatistics {
                        n_distinct: Some(730.0),
                        null_fraction: Some(0.0),
                        min_value: Some("2023-01-01".to_string()),
                        max_value: Some("2024-12-31".to_string()),
                        most_common_values: vec![],

                        avg_bytes: None,
                        compression_ratio: None,
                    }),
                },
                ColumnDef {
                    name: "channel".to_string(),
                    data_type: "VARCHAR".to_string(),
                    nullable: true,
                    description: None,
                    tags: vec![],
                    synonyms: vec![],
                    statistics: Some(ColumnStatistics {
                        n_distinct: Some(4.0),
                        null_fraction: Some(0.0),
                        min_value: None,
                        max_value: None,
                        most_common_values: vec![
                            McvEntry {
                                value: "web".to_string(),
                                frequency: 0.55,
                            },
                            McvEntry {
                                value: "mobile".to_string(),
                                frequency: 0.30,
                            },
                        ],

                        avg_bytes: None,
                        compression_ratio: None,
                    }),
                },
            ],
            primary_key: Some(vec!["id".to_string()]),
            foreign_keys: vec![ForeignKeyDef {
                columns: vec!["user_id".to_string()],
                references: ForeignKeyRef {
                    table: "customers".to_string(),
                    columns: vec!["id".to_string()],
                },
            }],
            statistics: Some(TableStatistics {
                row_count: Some(1_000_000),
                avg_row_bytes: Some(256),
                partitioned: true,
                partition_columns: vec!["order_date".to_string()],
                correlated_columns: vec![
                    vec!["status".to_string(), "region".to_string()],
                    vec!["channel".to_string(), "region".to_string()],
                ],
                ..Default::default()
            }),

            clustering_keys: vec![],
        },
    );
    tables.insert(
        "customers".to_string(),
        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: "name".to_string(),
                    data_type: "VARCHAR".to_string(),
                    nullable: true,
                    description: None,
                    tags: vec![],
                    synonyms: vec![],
                    statistics: None,
                },
                ColumnDef {
                    name: "segment".to_string(),
                    data_type: "VARCHAR".to_string(),
                    nullable: true,
                    description: None,
                    tags: vec![],
                    synonyms: vec![],
                    statistics: Some(ColumnStatistics {
                        n_distinct: Some(4.0),
                        null_fraction: Some(0.0),
                        min_value: None,
                        max_value: None,
                        most_common_values: vec![
                            McvEntry {
                                value: "consumer".to_string(),
                                frequency: 0.60,
                            },
                            McvEntry {
                                value: "business".to_string(),
                                frequency: 0.25,
                            },
                        ],

                        avg_bytes: None,
                        compression_ratio: None,
                    }),
                },
            ],
            primary_key: Some(vec!["id".to_string()]),
            foreign_keys: vec![],
            statistics: Some(TableStatistics {
                row_count: Some(50_000),
                avg_row_bytes: Some(128),
                partitioned: false,
                partition_columns: vec![],
                correlated_columns: vec![],
                ..Default::default()
            }),

            clustering_keys: vec![],
        },
    );
    SchemaDefinition {
        version: "1".to_string(),
        dialect: SqlDialect::BigQuery,
        tables,
        database: None,
        schema_name: None,
        glossary: None,
    }
}

/// Schema with table-level stats but NO column-level statistics.
fn schema_without_column_stats() -> SchemaDefinition {
    let mut tables = HashMap::new();
    tables.insert(
        "orders".to_string(),
        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,
                },
                ColumnDef {
                    name: "status".to_string(),
                    data_type: "VARCHAR".to_string(),
                    nullable: true,
                    description: None,
                    tags: vec![],
                    synonyms: vec![],
                    statistics: None,
                },
                ColumnDef {
                    name: "amount".to_string(),
                    data_type: "DECIMAL(10,2)".to_string(),
                    nullable: false,
                    description: None,
                    tags: vec![],
                    synonyms: vec![],
                    statistics: None,
                },
            ],
            primary_key: None,
            foreign_keys: vec![],
            statistics: Some(TableStatistics {
                row_count: Some(1_000_000),
                avg_row_bytes: Some(256),
                partitioned: false,
                partition_columns: vec![],
                correlated_columns: vec![],
                ..Default::default()
            }),

            clustering_keys: vec![],
        },
    );
    SchemaDefinition {
        version: "1".to_string(),
        dialect: SqlDialect::BigQuery,
        tables,
        database: None,
        schema_name: None,
        glossary: None,
    }
}

/// Schema with NO statistics at all.
fn schema_no_stats() -> SchemaDefinition {
    let mut tables = HashMap::new();
    tables.insert(
        "orders".to_string(),
        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![],
        },
    );
    SchemaDefinition {
        version: "1".to_string(),
        dialect: SqlDialect::Generic,
        tables,
        database: None,
        schema_name: None,
        glossary: None,
    }
}

/// Helper to build a simple table scan PlanStep.
fn table_scan(table: &str, columns: &[&str], filters: &[&str]) -> PlanStep {
    PlanStep {
        step: 1,
        operation: PlanOperation::TableScan {
            table: table.to_string(),
            columns_read: columns.iter().map(|c| c.to_string()).collect(),
            filters: filters.iter().map(|f| f.to_string()).collect(),
        },
        notes: None,
    }
}

/// Helper to build a join PlanStep.
fn join_step(step_num: usize, kind: JoinKind, left: &str, right: &str, cond: &str) -> PlanStep {
    PlanStep {
        step: step_num,
        operation: PlanOperation::Join {
            join_type: kind,
            left: left.to_string(),
            right: right.to_string(),
            condition: cond.to_string(),
        },
        notes: None,
    }
}

// ============================================================================
// Section 1: Column statistics deserialization from YAML
// ============================================================================

#[test]
fn yaml_full_column_statistics_roundtrip() {
    let yaml = r#"
version: "1"
tables:
  orders:
    columns:
      - name: status
        type: VARCHAR
        nullable: false
        statistics:
          n_distinct: 5
          null_fraction: 0.01
          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];
    let stats = col.statistics.as_ref().unwrap();
    assert_eq!(stats.n_distinct, Some(5.0));
    assert_eq!(stats.null_fraction, Some(0.01));
    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);
}

#[test]
fn yaml_partial_column_statistics_only_n_distinct() {
    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 yaml_no_column_statistics_backwards_compat() {
    let yaml = r#"
version: "1"
tables:
  users:
    columns:
      - name: id
        type: INT
        nullable: false
"#;
    let schema: SchemaDefinition = serde_yaml::from_str(yaml).unwrap();
    assert!(schema.tables["users"].columns[0].statistics.is_none());
}

#[test]
fn yaml_correlated_columns_parse() {
    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 yaml_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: VARCHAR
    statistics:
      row_count: 1000000
      correlated_columns:
        - ["status", "region"]
        - ["channel", "payment"]
"#;
    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);
}

#[test]
fn yaml_mcv_entries_roundtrip_json() {
    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,
            },
        ],

        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(), 2);
    assert!((parsed.most_common_values[0].frequency - 0.7).abs() < f64::EPSILON);
}

// ============================================================================
// Section 2: n_distinct positive vs negative handling
// ============================================================================

#[test]
fn 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 n_distinct_negative_all_unique() {
    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 n_distinct_negative_half_unique() {
    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 n_distinct_negative_eighty_percent_unique() {
    let col = ColumnDef {
        name: "amount".to_string(),
        data_type: "DECIMAL(10,2)".to_string(),
        nullable: false,
        description: None,
        tags: vec![],
        synonyms: vec![],
        statistics: Some(ColumnStatistics {
            n_distinct: Some(-0.8),
            ..Default::default()
        }),
    };
    assert_eq!(col.n_distinct(1_000_000), Some(800_000.0));
}

#[test]
fn n_distinct_none_returns_none() {
    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,
            ..Default::default()
        }),
    };
    assert_eq!(col.n_distinct(1_000_000), None);
}

#[test]
fn n_distinct_no_statistics_returns_none() {
    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);
}

// ============================================================================
// Section 3: Stats-driven selectivity for various predicate types
// ============================================================================

#[test]
fn stats_equality_n_distinct_selectivity() {
    // status has n_distinct=5, null_fraction=0.01
    // For an unknown value: 1/5 * (1 - 0.01) = 0.198
    let schema = schema_with_rich_stats();
    let steps = vec![table_scan(
        "orders",
        &["id", "status"],
        &["status = 'rare_value'"],
    )];
    let result = estimate_cost(&steps, &schema, SqlDialect::BigQuery);
    // stats-based selectivity should be used
    assert!(result.cost_range.is_some(), "CostRange should be present");
    assert!(result.table_breakdown[0].has_statistics);
    // selectivity ~0.198 -> 198K rows out of 1M
    let expected_rows_approx = 198_000u64;
    let actual_rows = result.table_breakdown[0].estimated_rows;
    // Allow 20% tolerance
    assert!(
        (actual_rows as f64 - expected_rows_approx as f64).abs()
            < expected_rows_approx as f64 * 0.2,
        "Expected ~{expected_rows_approx} rows, got {actual_rows}"
    );
}

#[test]
fn stats_equality_mcv_hit_high_frequency() {
    // "completed" has MCV frequency 0.60, null_fraction=0.01
    // selectivity = 0.60 * 0.99 = 0.594 -> 594K rows
    let schema = schema_with_rich_stats();
    let steps = vec![table_scan(
        "orders",
        &["id", "status"],
        &["status = 'completed'"],
    )];
    let result = estimate_cost(&steps, &schema, SqlDialect::BigQuery);
    assert!(result.cost_range.is_some());
    let actual_rows = result.table_breakdown[0].estimated_rows;
    // ~594K rows
    assert!(
        actual_rows > 500_000,
        "MCV hit 'completed' (60%) should produce >500K rows, got {actual_rows}"
    );
    assert!(
        actual_rows < 700_000,
        "MCV hit 'completed' (60%) should produce <700K rows, got {actual_rows}"
    );
}

#[test]
fn stats_equality_mcv_hit_low_frequency() {
    // "cancelled" has MCV frequency 0.10
    // selectivity = 0.10 * 0.99 = 0.099 -> ~99K rows
    let schema = schema_with_rich_stats();
    let steps = vec![table_scan(
        "orders",
        &["id", "status"],
        &["status = 'cancelled'"],
    )];
    let result = estimate_cost(&steps, &schema, SqlDialect::BigQuery);
    let actual_rows = result.table_breakdown[0].estimated_rows;
    assert!(
        actual_rows > 80_000 && actual_rows < 120_000,
        "MCV hit 'cancelled' (10%) should produce ~99K rows, got {actual_rows}"
    );
}

#[test]
fn stats_equality_mcv_miss_falls_to_n_distinct() {
    // Value not in MCV list -> 1/n_distinct * null_factor
    // 1/5 * 0.99 = 0.198 -> ~198K rows
    let schema = schema_with_rich_stats();
    let steps = vec![table_scan(
        "orders",
        &["id", "status"],
        &["status = 'unknown_value'"],
    )];
    let result = estimate_cost(&steps, &schema, SqlDialect::BigQuery);
    let actual_rows = result.table_breakdown[0].estimated_rows;
    assert!(
        actual_rows > 150_000 && actual_rows < 250_000,
        "MCV miss should use n_distinct: ~198K rows, got {actual_rows}"
    );
}

#[test]
fn stats_range_greater_than() {
    // amount: min=0.01, max=99999.99
    // "amount > 50000" -> (99999.99 - 50000) / (99999.99 - 0.01) ~= 0.5
    let schema = schema_with_rich_stats();
    let steps = vec![table_scan("orders", &["id", "amount"], &["amount > 50000"])];
    let result = estimate_cost(&steps, &schema, SqlDialect::BigQuery);
    assert!(result.cost_range.is_some());
    let sel = result.table_breakdown[0].selectivity;
    assert!(
        (sel - 0.5).abs() < 0.05,
        "amount > 50000 selectivity should be ~0.5, got {sel}"
    );
}

#[test]
fn stats_range_less_than() {
    // "amount < 10000" -> (10000 - 0.01) / 99999.98 ~= 0.1
    let schema = schema_with_rich_stats();
    let steps = vec![table_scan("orders", &["id", "amount"], &["amount < 10000"])];
    let result = estimate_cost(&steps, &schema, SqlDialect::BigQuery);
    let sel = result.table_breakdown[0].selectivity;
    assert!(
        (sel - 0.1).abs() < 0.02,
        "amount < 10000 selectivity should be ~0.1, got {sel}"
    );
}

#[test]
fn stats_range_between() {
    // "amount BETWEEN 20000 AND 40000" -> (40000 - 20000) / 99999.98 ~= 0.2
    let schema = schema_with_rich_stats();
    let steps = vec![table_scan(
        "orders",
        &["id", "amount"],
        &["amount BETWEEN 20000 AND 40000"],
    )];
    let result = estimate_cost(&steps, &schema, SqlDialect::BigQuery);
    let sel = result.table_breakdown[0].selectivity;
    assert!(
        (sel - 0.2).abs() < 0.03,
        "BETWEEN 20000 AND 40000 selectivity should be ~0.2, got {sel}"
    );
}

#[test]
fn stats_range_near_max_high_selectivity() {
    // "amount > 1" -> nearly all rows: (99999.99 - 1) / 99999.98 ~= 1.0
    let schema = schema_with_rich_stats();
    let steps = vec![table_scan("orders", &["id", "amount"], &["amount > 1"])];
    let result = estimate_cost(&steps, &schema, SqlDialect::BigQuery);
    let sel = result.table_breakdown[0].selectivity;
    assert!(
        sel > 0.95,
        "amount > 1 should have near-1.0 selectivity, got {sel}"
    );
}

#[test]
fn stats_range_near_min_low_selectivity() {
    // "amount < 100" -> (100 - 0.01) / 99999.98 ~= 0.001
    let schema = schema_with_rich_stats();
    let steps = vec![table_scan("orders", &["id", "amount"], &["amount < 100"])];
    let result = estimate_cost(&steps, &schema, SqlDialect::BigQuery);
    let sel = result.table_breakdown[0].selectivity;
    assert!(
        sel < 0.01,
        "amount < 100 should have very low selectivity, got {sel}"
    );
}

#[test]
fn stats_in_list_selectivity() {
    // status n_distinct=5, IN ('completed', 'pending') -> 2/5 * 0.99 = 0.396
    let schema = schema_with_rich_stats();
    let steps = vec![table_scan(
        "orders",
        &["id", "status"],
        &["status IN ('completed', 'pending')"],
    )];
    let result = estimate_cost(&steps, &schema, SqlDialect::BigQuery);
    assert!(result.cost_range.is_some());
    let sel = result.table_breakdown[0].selectivity;
    assert!(
        (sel - 0.396).abs() < 0.05,
        "IN list (2 of 5) selectivity should be ~0.396, got {sel}"
    );
}

#[test]
fn stats_in_list_many_values() {
    // IN with 4 values out of 5 distinct -> 4/5 * 0.99 = 0.792
    let schema = schema_with_rich_stats();
    let steps = vec![table_scan(
        "orders",
        &["id", "status"],
        &["status IN ('completed', 'pending', 'cancelled', 'refunded')"],
    )];
    let result = estimate_cost(&steps, &schema, SqlDialect::BigQuery);
    let sel = result.table_breakdown[0].selectivity;
    assert!(
        (sel - 0.792).abs() < 0.05,
        "IN list (4 of 5) selectivity should be ~0.792, got {sel}"
    );
}

#[test]
fn stats_is_null_selectivity() {
    // notes has null_fraction=0.40
    let schema = schema_with_rich_stats();
    let steps = vec![table_scan("orders", &["id", "notes"], &["notes IS NULL"])];
    let result = estimate_cost(&steps, &schema, SqlDialect::BigQuery);
    assert!(result.cost_range.is_some());
    let sel = result.table_breakdown[0].selectivity;
    assert!(
        (sel - 0.40).abs() < 0.05,
        "IS NULL with 40% null_fraction should be ~0.40, got {sel}"
    );
}

#[test]
fn stats_is_not_null_selectivity() {
    // notes has null_fraction=0.40, so IS NOT NULL = 0.60
    let schema = schema_with_rich_stats();
    let steps = vec![table_scan(
        "orders",
        &["id", "notes"],
        &["notes IS NOT NULL"],
    )];
    let result = estimate_cost(&steps, &schema, SqlDialect::BigQuery);
    let sel = result.table_breakdown[0].selectivity;
    assert!(
        (sel - 0.60).abs() < 0.05,
        "IS NOT NULL with 40% null_fraction should be ~0.60, got {sel}"
    );
}

#[test]
fn stats_like_falls_back_to_heuristic() {
    // LIKE doesn't use column stats, falls back to 5% heuristic
    let schema = schema_with_rich_stats();
    let steps = vec![table_scan(
        "orders",
        &["id", "status"],
        &["status LIKE 'comp%'"],
    )];
    let result = estimate_cost(&steps, &schema, SqlDialect::BigQuery);
    let sel = result.table_breakdown[0].selectivity;
    assert!(
        (sel - 0.05).abs() < 0.01,
        "LIKE should use heuristic ~0.05, got {sel}"
    );
}

// ============================================================================
// Section 4: Confidence interval ordering
// ============================================================================

#[test]
fn confidence_interval_ordering_equality() {
    let schema = schema_with_rich_stats();
    let steps = vec![table_scan(
        "orders",
        &["id", "status"],
        &["status = 'completed'"],
    )];
    let result = estimate_cost(&steps, &schema, SqlDialect::BigQuery);
    let range = result.cost_range.as_ref().expect("CostRange should exist");
    assert!(
        range.low <= range.expected,
        "low ({}) should <= expected ({})",
        range.low,
        range.expected
    );
    assert!(
        range.expected <= range.high,
        "expected ({}) should <= high ({})",
        range.expected,
        range.high
    );
}

#[test]
fn confidence_interval_ordering_range_predicate() {
    let schema = schema_with_rich_stats();
    let steps = vec![table_scan("orders", &["id", "amount"], &["amount > 50000"])];
    let result = estimate_cost(&steps, &schema, SqlDialect::BigQuery);
    let range = result.cost_range.as_ref().expect("CostRange should exist");
    assert!(range.low <= range.expected);
    assert!(range.expected <= range.high);
}

#[test]
fn confidence_interval_ordering_is_null() {
    let schema = schema_with_rich_stats();
    let steps = vec![table_scan("orders", &["id", "notes"], &["notes IS NULL"])];
    let result = estimate_cost(&steps, &schema, SqlDialect::BigQuery);
    let range = result.cost_range.as_ref().expect("CostRange should exist");
    assert!(range.low <= range.expected);
    assert!(range.expected <= range.high);
}

#[test]
fn confidence_interval_ordering_in_list() {
    let schema = schema_with_rich_stats();
    let steps = vec![table_scan(
        "orders",
        &["id", "status"],
        &["status IN ('completed', 'pending')"],
    )];
    let result = estimate_cost(&steps, &schema, SqlDialect::BigQuery);
    let range = result.cost_range.as_ref().expect("CostRange should exist");
    assert!(range.low <= range.expected);
    assert!(range.expected <= range.high);
}

#[test]
fn confidence_interval_ordering_multiple_filters() {
    let schema = schema_with_rich_stats();
    let steps = vec![table_scan(
        "orders",
        &["id", "status", "amount"],
        &["status = 'completed'", "amount > 50000"],
    )];
    let result = estimate_cost(&steps, &schema, SqlDialect::BigQuery);
    let range = result.cost_range.as_ref().expect("CostRange should exist");
    assert!(
        range.low <= range.expected,
        "With multiple filters: low ({}) <= expected ({})",
        range.low,
        range.expected
    );
    assert!(
        range.expected <= range.high,
        "With multiple filters: expected ({}) <= high ({})",
        range.expected,
        range.high
    );
}

// ============================================================================
// Section 5: Correlation group handling
// ============================================================================

#[test]
fn correlated_columns_no_over_multiply() {
    // status and region are correlated in orders
    // Correlated: uses max() instead of multiply, so selectivity stays higher
    let schema = schema_with_rich_stats();
    let steps = vec![table_scan(
        "orders",
        &["id", "status", "region"],
        &["status = 'completed'", "region = 'US-East'"],
    )];
    let result = estimate_cost(&steps, &schema, SqlDialect::BigQuery);
    let sel = result.table_breakdown[0].selectivity;
    // Correlated: max(0.594, 0.245) = 0.594 (approximately)
    // Without correlation it would be ~0.594 * sqrt(0.245) = ~0.294
    assert!(
        sel > 0.50,
        "Correlated columns should not over-multiply: selectivity {sel} should be > 0.50"
    );
}

#[test]
fn uncorrelated_columns_do_multiply() {
    // status and amount are NOT correlated
    let schema = schema_with_rich_stats();
    let steps = vec![table_scan(
        "orders",
        &["id", "status", "amount"],
        &["status = 'completed'", "amount > 50000"],
    )];
    let result = estimate_cost(&steps, &schema, SqlDialect::BigQuery);
    let sel = result.table_breakdown[0].selectivity;
    // status='completed' -> 0.594, amount>50000 -> ~0.5
    // Uncorrelated: 0.594 * sqrt(0.5) ~= 0.42
    assert!(
        sel < 0.594,
        "Uncorrelated columns should reduce selectivity below 0.594, got {sel}"
    );
}

#[test]
fn correlated_columns_second_group() {
    // channel and region are both in correlation groups, but find_correlation_group
    // returns the FIRST matching group index. region is in group 0 (status-region)
    // and group 1 (channel-region). Since find_correlation_group returns group 0
    // for region, and channel is in group 1, they are treated as separate groups
    // and multiplied with sqrt dampening rather than max().
    let schema = schema_with_rich_stats();
    let steps = vec![table_scan(
        "orders",
        &["id", "channel", "region"],
        &["channel = 'web'", "region = 'US-East'"],
    )];
    let result = estimate_cost(&steps, &schema, SqlDialect::BigQuery);
    let sel = result.table_breakdown[0].selectivity;
    // channel='web' MCV -> 0.55 * 1.0 = 0.55
    // region='US-East' MCV -> 0.25 * 0.98 = 0.245
    // sqrt dampened: 0.55 * sqrt(0.245) ~= 0.272
    assert!(
        sel > 0.20 && sel < 0.40,
        "channel+region with sqrt dampening should be ~0.27, got {sel}"
    );
}

#[test]
fn uncorrelated_three_filters_sqrt_dampening() {
    // Three uncorrelated filters: each subsequent one uses sqrt dampening
    let schema = schema_with_rich_stats();
    let steps = vec![table_scan(
        "orders",
        &["id", "status", "amount", "user_id"],
        &["status = 'completed'", "amount > 50000", "user_id = 42"],
    )];
    let result = estimate_cost(&steps, &schema, SqlDialect::BigQuery);
    let sel = result.table_breakdown[0].selectivity;
    // Without dampening: 0.594 * 0.5 * tiny = very small
    // With sqrt dampening: larger than naive multiplication
    assert!(
        sel > 0.001,
        "Three filters with sqrt dampening should not be unrealistically small: got {sel}"
    );
}

// ============================================================================
// Section 6: Join selectivity (FK, n_distinct, default)
// ============================================================================

#[test]
fn fk_join_selectivity_is_one() {
    // orders.user_id -> customers.id is a FK join
    let schema = schema_with_rich_stats();
    let steps = vec![
        table_scan("orders", &["id", "user_id"], &[]),
        PlanStep {
            step: 2,
            operation: PlanOperation::TableScan {
                table: "customers".to_string(),
                columns_read: vec!["id".to_string(), "name".to_string()],
                filters: vec![],
            },
            notes: None,
        },
        join_step(
            3,
            JoinKind::Inner,
            "orders",
            "customers",
            "orders.user_id = customers.id",
        ),
    ];
    let result = estimate_cost(&steps, &schema, SqlDialect::BigQuery);
    // FK join = selectivity 1.0, so bytes shouldn't increase from join
    assert!(result.bytes_scanned > 0);
}

#[test]
fn fk_join_reverse_direction() {
    // Also check the reverse: customers.id referenced by orders.user_id
    let schema = schema_with_rich_stats();
    let steps = vec![
        PlanStep {
            step: 1,
            operation: PlanOperation::TableScan {
                table: "customers".to_string(),
                columns_read: vec!["id".to_string()],
                filters: vec![],
            },
            notes: None,
        },
        table_scan("orders", &["user_id"], &[]),
        join_step(
            3,
            JoinKind::Inner,
            "customers",
            "orders",
            "customers.id = orders.user_id",
        ),
    ];
    let result = estimate_cost(&steps, &schema, SqlDialect::BigQuery);
    // Should still detect FK relationship
    assert!(result.bytes_scanned > 0);
}

#[test]
fn default_join_selectivity_unknown_columns() {
    // When columns can't be resolved, default is 0.1
    let schema = schema_with_rich_stats();
    let steps = vec![
        table_scan("orders", &["id"], &[]),
        PlanStep {
            step: 2,
            operation: PlanOperation::TableScan {
                table: "customers".to_string(),
                columns_read: vec!["id".to_string()],
                filters: vec![],
            },
            notes: None,
        },
        join_step(
            3,
            JoinKind::Inner,
            "orders",
            "customers",
            "unknown_a = unknown_b",
        ),
    ];
    let result = estimate_cost(&steps, &schema, SqlDialect::BigQuery);
    // Should still produce a valid estimate
    assert!(result.bytes_scanned > 0);
}

#[test]
fn cross_join_does_not_use_selectivity() {
    let schema = schema_with_rich_stats();
    let steps = vec![
        table_scan("orders", &["id"], &[]),
        PlanStep {
            step: 2,
            operation: PlanOperation::TableScan {
                table: "customers".to_string(),
                columns_read: vec!["id".to_string()],
                filters: vec![],
            },
            notes: None,
        },
        join_step(3, JoinKind::Cross, "orders", "customers", ""),
    ];
    let result = estimate_cost(&steps, &schema, SqlDialect::BigQuery);
    assert!(
        result.warnings.iter().any(|w| w.contains("CROSS JOIN")),
        "Cross join should produce a warning"
    );
    // Cross join bytes should be very large (1M * 50K * bytes_per_row)
    assert!(
        result.bytes_scanned > 100_000_000,
        "Cross join should produce very large byte estimate: got {}",
        result.bytes_scanned
    );
}

// ============================================================================
// Section 7: CostRange presence/absence
// ============================================================================

#[test]
fn cost_range_present_with_stats_driven_filter() {
    let schema = schema_with_rich_stats();
    let steps = vec![table_scan(
        "orders",
        &["id", "status"],
        &["status = 'completed'"],
    )];
    let result = estimate_cost(&steps, &schema, SqlDialect::BigQuery);
    assert!(
        result.cost_range.is_some(),
        "CostRange should be present when column stats drive selectivity"
    );
}

#[test]
fn cost_range_absent_without_column_stats() {
    let schema = schema_without_column_stats();
    let steps = vec![table_scan(
        "orders",
        &["id", "status"],
        &["status = 'completed'"],
    )];
    let result = estimate_cost(&steps, &schema, SqlDialect::BigQuery);
    assert!(
        result.cost_range.is_none(),
        "CostRange should be absent when no column stats drive selectivity"
    );
}

#[test]
fn cost_range_absent_no_filters() {
    // Even with column stats, no filters means no stats-based selectivity
    let schema = schema_with_rich_stats();
    let steps = vec![table_scan("orders", &["id", "status"], &[])];
    let result = estimate_cost(&steps, &schema, SqlDialect::BigQuery);
    assert!(
        result.cost_range.is_none(),
        "CostRange should be absent when there are no filters (selectivity = 1.0)"
    );
}

#[test]
fn cost_range_absent_no_stats_at_all() {
    let schema = schema_no_stats();
    let steps = vec![table_scan("orders", &["id"], &["id = 42"])];
    let result = estimate_cost(&steps, &schema, SqlDialect::Generic);
    // Even without statistics, fallback heuristic produces a cost estimate
    assert_ne!(result.cost_tier, CostTier::Unknown);
    assert!(result.cost_range.is_none());
}

#[test]
fn cost_range_serialization_skip_when_none() {
    let cost = altimate_core::complexity::types::CostEstimate {
        bytes_scanned: 100,
        cost_usd: 0.001,
        cost_tier: CostTier::Cheap,
        table_breakdown: vec![],
        warnings: vec![],
        disclaimer: None,
        cost_range: None,
        recommendations: vec![],
        confidence: EstimationConfidence::High,
        confidence_factors: vec![],
        estimation_method: None,
        time_based_cost: None,
    };
    let json = serde_json::to_string(&cost).unwrap();
    assert!(
        !json.contains("cost_range"),
        "None cost_range should be skipped"
    );
}

#[test]
fn cost_range_serialization_present_when_some() {
    let cost = altimate_core::complexity::types::CostEstimate {
        bytes_scanned: 100,
        cost_usd: 0.001,
        cost_tier: CostTier::Cheap,
        table_breakdown: vec![],
        warnings: vec![],
        disclaimer: None,
        cost_range: Some(CostRange {
            low: 0.0001,
            expected: 0.001,
            high: 0.01,
        }),
        recommendations: vec![],
        confidence: EstimationConfidence::High,
        confidence_factors: vec![],
        estimation_method: None,
        time_based_cost: None,
    };
    let json = serde_json::to_string(&cost).unwrap();
    assert!(
        json.contains("cost_range"),
        "Some cost_range should be present"
    );
    assert!(json.contains("\"low\""));
    assert!(json.contains("\"high\""));
}

// ============================================================================
// Section 8: Full integration -- stats vs no-stats produce different costs
// ============================================================================

#[test]
fn stats_vs_heuristic_common_value_more_expensive() {
    // With column stats: "status = 'completed'" -> 60% selectivity (MCV)
    // Without column stats: equality = 10% heuristic
    // Stats-driven should estimate MORE bytes for a common value.
    // Use Generic dialect (non-columnar) so the columnar floor doesn't
    // mask the selectivity difference.
    let schema_stats = schema_with_rich_stats();
    let schema_no_stats = schema_without_column_stats();

    let steps = vec![table_scan(
        "orders",
        &["id", "status"],
        &["status = 'completed'"],
    )];

    let with_stats = estimate_cost(&steps, &schema_stats, SqlDialect::Generic);
    let without_stats = estimate_cost(&steps, &schema_no_stats, SqlDialect::Generic);

    assert!(
        with_stats.bytes_scanned > without_stats.bytes_scanned,
        "Stats-driven ({}) should scan more than heuristic ({}) for common value",
        with_stats.bytes_scanned,
        without_stats.bytes_scanned
    );
}

#[test]
fn stats_vs_heuristic_rare_value_cheaper() {
    // With column stats: id has n_distinct=-1.0 -> 1/1M = 0.000001
    // Without column stats: equality = 10%
    // Stats-driven for unique column should be MUCH cheaper.
    // Use Generic dialect (non-columnar) so the columnar floor doesn't
    // mask the selectivity difference.
    let schema_stats = schema_with_rich_stats();
    let schema_no_stats = schema_without_column_stats();

    let steps = vec![table_scan("orders", &["id"], &["id = 42"])];

    let with_stats = estimate_cost(&steps, &schema_stats, SqlDialect::Generic);
    let without_stats = estimate_cost(&steps, &schema_no_stats, SqlDialect::Generic);

    assert!(
        with_stats.bytes_scanned < without_stats.bytes_scanned,
        "Stats-driven ({}) should scan less than heuristic ({}) for unique column lookup",
        with_stats.bytes_scanned,
        without_stats.bytes_scanned
    );
}

#[test]
fn stats_vs_heuristic_range_filter_accuracy() {
    // With stats: amount > 90000 -> (99999.99 - 90000) / 99999.98 ~= 10%
    // Without stats: range heuristic = 30%
    // Stats-driven should be less for a selective range.
    // Use Generic dialect (non-columnar) so the columnar floor doesn't
    // mask the selectivity difference.
    let schema_stats = schema_with_rich_stats();
    let schema_no_stats = schema_without_column_stats();

    let steps = vec![table_scan("orders", &["id", "amount"], &["amount > 90000"])];

    let with_stats = estimate_cost(&steps, &schema_stats, SqlDialect::Generic);
    let without_stats = estimate_cost(&steps, &schema_no_stats, SqlDialect::Generic);

    assert!(
        with_stats.bytes_scanned < without_stats.bytes_scanned,
        "Stats-driven range ({}) should be less than heuristic range ({}) for selective range",
        with_stats.bytes_scanned,
        without_stats.bytes_scanned
    );
}

// ============================================================================
// Section 9: Edge cases
// ============================================================================

#[test]
fn edge_case_zero_n_distinct() {
    // n_distinct=0 should not cause division by zero
    let mut schema = schema_with_rich_stats();
    if let Some(table) = schema.tables.get_mut("orders") {
        // Set status n_distinct to 0 (edge case)
        for col in &mut table.columns {
            if col.name == "status" {
                if let Some(ref mut stats) = col.statistics {
                    stats.n_distinct = Some(0.0);
                }
            }
        }
    }
    let steps = vec![table_scan(
        "orders",
        &["id", "status"],
        &["status = 'test'"],
    )];
    // Should not panic or produce NaN
    let result = estimate_cost(&steps, &schema, SqlDialect::BigQuery);
    assert!(!result.cost_usd.is_nan(), "Cost should not be NaN");
    assert!(
        !result.cost_usd.is_infinite(),
        "Cost should not be infinite"
    );
}

#[test]
fn edge_case_hundred_percent_null_fraction() {
    // null_fraction=1.0 means all values are NULL
    let mut schema = schema_with_rich_stats();
    if let Some(table) = schema.tables.get_mut("orders") {
        for col in &mut table.columns {
            if col.name == "notes" {
                if let Some(ref mut stats) = col.statistics {
                    stats.null_fraction = Some(1.0);
                }
            }
        }
    }
    let steps = vec![table_scan("orders", &["id", "notes"], &["notes IS NULL"])];
    let result = estimate_cost(&steps, &schema, SqlDialect::BigQuery);
    let sel = result.table_breakdown[0].selectivity;
    assert!(
        (sel - 1.0).abs() < 0.01,
        "IS NULL with 100% null_fraction should have selectivity ~1.0, got {sel}"
    );
}

#[test]
fn edge_case_zero_null_fraction() {
    // null_fraction=0.0 means no NULLs
    let schema = schema_with_rich_stats();
    let steps = vec![table_scan("orders", &["id"], &["id IS NULL"])];
    let result = estimate_cost(&steps, &schema, SqlDialect::BigQuery);
    let sel = result.table_breakdown[0].selectivity;
    // id has null_fraction=0.0, so IS NULL should give ~0.0
    assert!(
        sel < 0.01,
        "IS NULL with 0% null_fraction should have very low selectivity, got {sel}"
    );
}

#[test]
fn edge_case_empty_mcv_list() {
    // Empty MCV list should fall through to n_distinct
    let mut schema = schema_with_rich_stats();
    if let Some(table) = schema.tables.get_mut("orders") {
        for col in &mut table.columns {
            if col.name == "status" {
                if let Some(ref mut stats) = col.statistics {
                    stats.most_common_values.clear();
                }
            }
        }
    }
    let steps = vec![table_scan(
        "orders",
        &["id", "status"],
        &["status = 'completed'"],
    )];
    let result = estimate_cost(&steps, &schema, SqlDialect::BigQuery);
    let sel = result.table_breakdown[0].selectivity;
    // Should use n_distinct=5: 1/5 * 0.99 = 0.198
    assert!(
        (sel - 0.198).abs() < 0.02,
        "Empty MCV should fall to n_distinct: expected ~0.198, got {sel}"
    );
}

#[test]
fn edge_case_no_min_max_range_falls_to_heuristic() {
    // When min/max not present, range predicates use heuristic (0.3)
    let mut schema = schema_with_rich_stats();
    if let Some(table) = schema.tables.get_mut("orders") {
        for col in &mut table.columns {
            if col.name == "amount" {
                if let Some(ref mut stats) = col.statistics {
                    stats.min_value = None;
                    stats.max_value = None;
                }
            }
        }
    }
    let steps = vec![table_scan("orders", &["id", "amount"], &["amount > 50000"])];
    let result = estimate_cost(&steps, &schema, SqlDialect::BigQuery);
    let sel = result.table_breakdown[0].selectivity;
    // Without min/max, range falls back to heuristic 0.3
    assert!(
        (sel - 0.3).abs() < 0.05,
        "Range without min/max should use heuristic 0.3, got {sel}"
    );
}

#[test]
fn edge_case_min_equals_max_no_range() {
    // When min == max, range selectivity returns None (falls to heuristic)
    let mut schema = schema_with_rich_stats();
    if let Some(table) = schema.tables.get_mut("orders") {
        for col in &mut table.columns {
            if col.name == "amount" {
                if let Some(ref mut stats) = col.statistics {
                    stats.min_value = Some("100".to_string());
                    stats.max_value = Some("100".to_string());
                }
            }
        }
    }
    let steps = vec![table_scan("orders", &["id", "amount"], &["amount > 50"])];
    let result = estimate_cost(&steps, &schema, SqlDialect::BigQuery);
    // Should not panic, should fall back to heuristic
    assert!(!result.cost_usd.is_nan());
}

#[test]
fn edge_case_negative_n_distinct_zero() {
    // n_distinct=-0.0 (edge): abs(-0.0) * 1M = 0 -> max(1.0) = 1 distinct
    let mut schema = schema_with_rich_stats();
    if let Some(table) = schema.tables.get_mut("orders") {
        for col in &mut table.columns {
            if col.name == "status" {
                if let Some(ref mut stats) = col.statistics {
                    stats.n_distinct = Some(-0.0);
                    stats.most_common_values.clear();
                }
            }
        }
    }
    let steps = vec![table_scan(
        "orders",
        &["id", "status"],
        &["status = 'test'"],
    )];
    let result = estimate_cost(&steps, &schema, SqlDialect::BigQuery);
    // Should not panic; -0.0 * 1M = 0 -> distinct = max(1.0) = 1 -> sel = 1/1 = 1.0
    assert!(!result.cost_usd.is_nan());
}

#[test]
fn edge_case_empty_plan_steps() {
    let schema = schema_with_rich_stats();
    let steps: Vec<PlanStep> = vec![];
    let result = estimate_cost(&steps, &schema, SqlDialect::BigQuery);
    assert_eq!(result.bytes_scanned, 0);
    assert_eq!(result.cost_tier, CostTier::Unknown);
    assert!(result.table_breakdown.is_empty());
}

#[test]
fn edge_case_table_not_in_schema() {
    let schema = schema_with_rich_stats();
    let steps = vec![table_scan("nonexistent_table", &["id"], &["id = 1"])];
    let result = estimate_cost(&steps, &schema, SqlDialect::BigQuery);
    // Falls through to no-stats path with fallback heuristics
    // Still produces a valid cost estimate with a real tier
    assert_ne!(result.cost_tier, CostTier::Unknown);
    assert!(!result.table_breakdown[0].has_statistics);
}

// ============================================================================
// Section 10: Gating tier uses pessimistic
// ============================================================================

#[test]
fn gating_tier_uses_pessimistic_cost_range() {
    let cost = altimate_core::complexity::types::CostEstimate {
        bytes_scanned: 1_000,
        cost_usd: 0.005, // Cheap
        cost_tier: CostTier::Cheap,
        table_breakdown: vec![],
        warnings: vec![],
        disclaimer: None,
        cost_range: Some(CostRange {
            low: 0.001,
            expected: 0.005,
            high: 200.0, // Dangerous
        }),
        recommendations: vec![],
        confidence: EstimationConfidence::High,
        confidence_factors: vec![],
        estimation_method: None,
        time_based_cost: None,
    };
    assert_eq!(
        cost.gating_tier(),
        CostTier::Dangerous,
        "Gating should use pessimistic high for safety"
    );
}

#[test]
fn gating_tier_falls_back_without_range() {
    let cost = altimate_core::complexity::types::CostEstimate {
        bytes_scanned: 1_000,
        cost_usd: 0.005,
        cost_tier: CostTier::Cheap,
        table_breakdown: vec![],
        warnings: vec![],
        disclaimer: None,
        cost_range: None,
        recommendations: vec![],
        confidence: EstimationConfidence::High,
        confidence_factors: vec![],
        estimation_method: None,
        time_based_cost: None,
    };
    assert_eq!(
        cost.gating_tier(),
        CostTier::Cheap,
        "Without range, gating should use cost_tier"
    );
}

// ============================================================================
// Section 11: SelectivityEstimate type tests
// ============================================================================

#[test]
fn selectivity_estimate_serialization_roundtrip() {
    let sel = SelectivityEstimate {
        expected: 0.1,
        pessimistic: 0.3,
        optimistic: 0.01,
        stats_based: true,
    };
    let json = serde_json::to_string(&sel).unwrap();
    let deserialized: SelectivityEstimate = serde_json::from_str(&json).unwrap();
    assert!((deserialized.expected - 0.1).abs() < f64::EPSILON);
    assert!((deserialized.pessimistic - 0.3).abs() < f64::EPSILON);
    assert!((deserialized.optimistic - 0.01).abs() < f64::EPSILON);
    assert!(deserialized.stats_based);
}

#[test]
fn selectivity_estimate_copy_semantics() {
    let sel = SelectivityEstimate {
        expected: 0.1,
        pessimistic: 0.3,
        optimistic: 0.01,
        stats_based: false,
    };
    let copied = sel; // Copy trait
    assert!((copied.expected - 0.1).abs() < f64::EPSILON);
    assert!(!copied.stats_based);
}

// ============================================================================
// Section 12: Benchmark warehouse schema integration
// ============================================================================

fn benchmark_schema() -> SchemaDefinition {
    let path = Path::new(env!("CARGO_MANIFEST_DIR"))
        .join("../../tests/fixtures/schemas/benchmark_warehouse.yaml");
    let content = std::fs::read_to_string(&path)
        .unwrap_or_else(|e| panic!("Failed to read benchmark schema: {e}"));
    serde_yaml::from_str(&content).expect("Failed to parse benchmark schema YAML")
}

#[test]
fn benchmark_schema_has_column_stats() {
    let schema = benchmark_schema();
    let orders = &schema.tables["fact_orders"];
    assert!(
        orders.column_stats("id").is_some(),
        "fact_orders.id should have column statistics"
    );
    assert!(
        orders.column_stats("status").is_some(),
        "fact_orders.status should have column statistics"
    );
    let status_stats = orders.column_stats("status").unwrap();
    assert!(!status_stats.most_common_values.is_empty());
}

#[test]
fn benchmark_schema_has_correlation_groups() {
    let schema = benchmark_schema();
    let orders_stats = schema.tables["fact_orders"].statistics.as_ref().unwrap();
    assert!(
        !orders_stats.correlated_columns.is_empty(),
        "fact_orders should have correlated_columns"
    );
}

#[test]
fn benchmark_filtered_scan_produces_cost_range() {
    let schema = benchmark_schema();
    let steps = vec![table_scan(
        "fact_orders",
        &["id", "status", "amount"],
        &["status = 'completed'"],
    )];
    let result = estimate_cost(&steps, &schema, SqlDialect::BigQuery);
    assert!(
        result.cost_range.is_some(),
        "Filtered benchmark query should produce CostRange"
    );
    let range = result.cost_range.unwrap();
    assert!(range.low <= range.expected);
    assert!(range.expected <= range.high);
}

#[test]
fn benchmark_full_scan_no_cost_range() {
    let schema = benchmark_schema();
    let steps = vec![table_scan("fact_orders", &["id"], &[])];
    let result = estimate_cost(&steps, &schema, SqlDialect::BigQuery);
    assert!(
        result.cost_range.is_none(),
        "Unfiltered scan should not produce CostRange (no stats-based selectivity)"
    );
}

#[test]
fn benchmark_mcv_hit_status_completed() {
    // fact_orders.status MCV: completed=0.60
    let schema = benchmark_schema();
    let steps = vec![table_scan(
        "fact_orders",
        &["id", "status"],
        &["status = 'completed'"],
    )];
    let result = estimate_cost(&steps, &schema, SqlDialect::BigQuery);
    assert!(result.cost_range.is_some());
    let sel = result.table_breakdown[0].selectivity;
    // 0.60 * (1 - 0.01) = 0.594
    assert!(
        (sel - 0.594).abs() < 0.02,
        "Expected MCV selectivity ~0.594, got {sel}"
    );
}

#[test]
fn benchmark_correlated_status_region() {
    // status and region are correlated in fact_orders
    let schema = benchmark_schema();
    let steps = vec![table_scan(
        "fact_orders",
        &["id", "status", "region"],
        &["status = 'completed'", "region = 'US-East'"],
    )];
    let result = estimate_cost(&steps, &schema, SqlDialect::BigQuery);
    let sel = result.table_breakdown[0].selectivity;
    // Correlated: should not be much smaller than the larger selectivity
    assert!(
        sel > 0.20,
        "Correlated status+region should not over-multiply: got {sel}"
    );
}

#[test]
fn benchmark_range_order_date() {
    // order_date: min=2023-01-01, max=2024-12-31
    // order_date >= '2024-07-01' should be a range filter
    let schema = benchmark_schema();
    let steps = vec![table_scan(
        "fact_orders",
        &["id", "order_date"],
        &["order_date >= '2024-07-01'"],
    )];
    let result = estimate_cost(&steps, &schema, SqlDialect::BigQuery);
    assert!(result.cost_range.is_some());
    // Partition pruning also applies, reducing cost further
    assert!(result.warnings.is_empty(), "Should match partition column");
}

#[test]
fn benchmark_dim_customers_country_mcv() {
    // dim_customers.country MCV: US=0.35
    let schema = benchmark_schema();
    let steps = vec![table_scan(
        "dim_customers",
        &["id", "country"],
        &["country = 'US'"],
    )];
    let result = estimate_cost(&steps, &schema, SqlDialect::BigQuery);
    assert!(result.cost_range.is_some());
    let sel = result.table_breakdown[0].selectivity;
    // MCV: 0.35 * (1 - 0.01) = 0.3465
    assert!(
        (sel - 0.3465).abs() < 0.02,
        "Expected country='US' MCV selectivity ~0.3465, got {sel}"
    );
}

// ============================================================================
// Section 13: CostRange roundtrip and type tests
// ============================================================================

#[test]
fn cost_range_roundtrip() {
    let range = CostRange {
        low: 0.001,
        expected: 0.005,
        high: 0.05,
    };
    let json = serde_json::to_string(&range).unwrap();
    let deserialized: CostRange = serde_json::from_str(&json).unwrap();
    assert!((deserialized.low - 0.001).abs() < f64::EPSILON);
    assert!((deserialized.expected - 0.005).abs() < f64::EPSILON);
    assert!((deserialized.high - 0.05).abs() < f64::EPSILON);
}

#[test]
fn cost_range_clone() {
    let range = CostRange {
        low: 1.0,
        expected: 5.0,
        high: 10.0,
    };
    let cloned = range.clone();
    assert!((cloned.low - 1.0).abs() < f64::EPSILON);
}

// ============================================================================
// Section 14: Cross-dialect cost comparison with stats
// ============================================================================

#[test]
fn stats_driven_bigquery_more_expensive_than_snowflake() {
    let schema = schema_with_rich_stats();
    let steps = vec![table_scan(
        "orders",
        &["id", "status"],
        &["status = 'completed'"],
    )];
    let bq = estimate_cost(&steps, &schema, SqlDialect::BigQuery);
    let sf = estimate_cost(&steps, &schema, SqlDialect::Snowflake);
    assert!(
        bq.cost_usd > sf.cost_usd,
        "BigQuery ({}) should be more expensive than Snowflake ({})",
        bq.cost_usd,
        sf.cost_usd
    );
}

#[test]
fn stats_driven_both_dialects_produce_cost_range() {
    let schema = schema_with_rich_stats();
    let steps = vec![table_scan(
        "orders",
        &["id", "status"],
        &["status = 'completed'"],
    )];
    let bq = estimate_cost(&steps, &schema, SqlDialect::BigQuery);
    let sf = estimate_cost(&steps, &schema, SqlDialect::Snowflake);
    assert!(bq.cost_range.is_some(), "BigQuery should have CostRange");
    assert!(sf.cost_range.is_some(), "Snowflake should have CostRange");
}

// ============================================================================
// Section 13: EstimationConfidence detection tests
// ============================================================================

#[test]
fn confidence_high_for_simple_equality() {
    let schema = schema_with_rich_stats();
    let steps = vec![table_scan(
        "orders",
        &["id", "status"],
        &["status = 'completed'"],
    )];
    let result = estimate_cost(&steps, &schema, SqlDialect::Snowflake);
    assert_eq!(
        result.confidence,
        EstimationConfidence::High,
        "Equality filter should produce high confidence"
    );
    assert!(
        result.confidence_factors.is_empty(),
        "No degradation factors for simple equality"
    );
}

#[test]
fn confidence_low_for_like_filter() {
    let schema = schema_with_rich_stats();
    let steps = vec![table_scan(
        "orders",
        &["id", "status"],
        &["status LIKE 'comp%'"],
    )];
    let result = estimate_cost(&steps, &schema, SqlDialect::Snowflake);
    assert_eq!(
        result.confidence,
        EstimationConfidence::Low,
        "LIKE filter should produce low confidence"
    );
    assert!(
        result.confidence_factors.iter().any(|f| f.contains("LIKE")),
        "Should mention LIKE in factors"
    );
}

#[test]
fn confidence_low_for_semi_join() {
    let schema = schema_with_rich_stats();
    let steps = vec![
        table_scan("orders", &["id", "status"], &[]),
        table_scan("orders", &["id"], &[]),
        join_step(
            3,
            JoinKind::LeftSemi,
            "orders",
            "orders",
            "orders.id = orders.id",
        ),
    ];
    let result = estimate_cost(&steps, &schema, SqlDialect::Snowflake);
    assert_eq!(
        result.confidence,
        EstimationConfidence::Low,
        "Semi join (EXISTS) should produce low confidence"
    );
    assert!(
        result.confidence_factors.iter().any(|f| f.contains("Semi")),
        "Should mention semi join in factors"
    );
}

#[test]
fn confidence_medium_for_three_joins() {
    let schema = schema_with_rich_stats();
    let steps = vec![
        table_scan("orders", &["id"], &[]),
        table_scan("orders", &["id"], &[]),
        table_scan("orders", &["id"], &[]),
        table_scan("orders", &["id"], &[]),
        join_step(
            5,
            JoinKind::Inner,
            "orders",
            "orders",
            "orders.id = orders.id",
        ),
        join_step(
            6,
            JoinKind::Inner,
            "orders",
            "orders",
            "orders.id = orders.id",
        ),
        join_step(
            7,
            JoinKind::Inner,
            "orders",
            "orders",
            "orders.id = orders.id",
        ),
    ];
    let result = estimate_cost(&steps, &schema, SqlDialect::Snowflake);
    assert!(
        result.confidence.severity() >= EstimationConfidence::Medium.severity(),
        "3+ joins should degrade confidence to at least Medium"
    );
}

#[test]
fn confidence_medium_for_no_stats() {
    // Build a schema with an unknown table (no definition at all)
    // so the estimator falls back entirely to heuristic, producing cost_tier == Unknown
    let schema = SchemaDefinition {
        version: "1".to_string(),
        dialect: SqlDialect::Snowflake,
        tables: std::collections::HashMap::new(), // empty - no tables
        database: None,
        schema_name: None,
        glossary: None,
    };
    let steps = vec![table_scan("unknown_table", &["id"], &[])];
    let result = estimate_cost(&steps, &schema, SqlDialect::Snowflake);
    // With no table definitions at all, bytes > 0 but any_stats is false,
    // so cost_tier is computed from cost_usd (not Unknown).
    // However, the table has no stats, so range filter rule would detect
    // heuristic-only estimation. For pure "no stats" degradation, we rely
    // on cost_tier == Unknown which requires 0 bytes and no stats.
    // Instead, test that range filter without stats degrades confidence:
    let steps_range = vec![table_scan("unknown_table", &["id"], &["id > 100"])];
    let result_range = estimate_cost(&steps_range, &schema, SqlDialect::Snowflake);
    assert!(
        result_range.confidence.severity() >= EstimationConfidence::Medium.severity(),
        "Range filter without column statistics should degrade confidence, got {:?}",
        result_range.confidence
    );
    assert!(
        result_range
            .confidence_factors
            .iter()
            .any(|f| f.contains("Range")),
        "Should mention range filter in factors, got {:?}",
        result_range.confidence_factors
    );
    // Verify simple unfiltered scan on unknown table still gets High
    // (no problematic patterns detected even though heuristic is used)
    assert_eq!(
        result.confidence,
        EstimationConfidence::High,
        "Simple unfiltered scan should still be high confidence"
    );
}

#[test]
fn confidence_minimum_wins() {
    // LIKE (Low) + 3 joins (Medium) -> should be Low (the worst)
    let schema = schema_with_rich_stats();
    let steps = vec![
        table_scan("orders", &["id", "status"], &["status LIKE 'comp%'"]),
        table_scan("orders", &["id"], &[]),
        table_scan("orders", &["id"], &[]),
        table_scan("orders", &["id"], &[]),
        join_step(
            5,
            JoinKind::Inner,
            "orders",
            "orders",
            "orders.id = orders.id",
        ),
        join_step(
            6,
            JoinKind::Inner,
            "orders",
            "orders",
            "orders.id = orders.id",
        ),
        join_step(
            7,
            JoinKind::Inner,
            "orders",
            "orders",
            "orders.id = orders.id",
        ),
    ];
    let result = estimate_cost(&steps, &schema, SqlDialect::Snowflake);
    assert_eq!(
        result.confidence,
        EstimationConfidence::Low,
        "Multiple degradations should take the worst (minimum) confidence"
    );
    assert!(
        result.confidence_factors.len() >= 2,
        "Should have at least 2 confidence factors"
    );
}

// ============================================================================
// Section: EXISTS / correlated / IN subquery byte accounting
// ============================================================================

#[test]
fn exists_subquery_counts_inner_table_bytes() {
    // Simulates: SELECT CASE WHEN EXISTS (SELECT 1 FROM orders WHERE amount > 99999) THEN 1 ELSE 0 END
    // The outer plan has no FROM (EmptyRelation). The EXISTS inner plan scans orders.
    // The inner TableScan should be counted via all_steps() flattening.
    let schema = schema_with_rich_stats();
    let steps = vec![
        PlanStep {
            step: 1,
            operation: PlanOperation::Subquery {
                alias: "exists_subquery".to_string(),
                inner_steps: vec![
                    table_scan("orders", &["amount"], &["amount > 99999"]),
                    PlanStep {
                        step: 2,
                        operation: PlanOperation::Aggregate {
                            group_by: vec![],
                            functions: vec!["COUNT(*)".to_string()],
                        },
                        notes: None,
                    },
                ],
            },
            notes: None,
        },
        PlanStep {
            step: 3,
            operation: PlanOperation::EmptyRelation,
            notes: None,
        },
        PlanStep {
            step: 4,
            operation: PlanOperation::Projection {
                columns: vec!["CASE WHEN exists_subquery > 0 THEN 1 ELSE 0 END".to_string()],
            },
            notes: None,
        },
    ];
    let result = estimate_cost(&steps, &schema, SqlDialect::BigQuery);
    assert!(
        result.bytes_scanned > 0,
        "EXISTS subquery should count inner table bytes, got 0"
    );
    assert!(
        !result.table_breakdown.is_empty(),
        "Should have at least one table in breakdown"
    );
    assert_eq!(
        result.table_breakdown[0].table, "orders",
        "Inner table should be orders"
    );
    assert!(
        result.table_breakdown[0].has_statistics,
        "orders should have statistics"
    );
    // Confidence is Low because the range filter (amount > 99999) on a
    // non-partition column in a columnar engine has unpredictable zone-map behavior
    assert_eq!(
        result.confidence,
        EstimationConfidence::Low,
        "Range filter on non-partition column should degrade confidence"
    );
}

#[test]
fn correlated_subquery_adds_inner_table() {
    // Simulates: SELECT id, (SELECT COUNT(*) FROM orders o WHERE o.user_id = c.id) FROM customers c
    // Both customers (outer scan) and orders (inner subquery scan) should be counted.
    let schema = schema_with_rich_stats();
    let steps = vec![
        table_scan("customers", &["id", "name"], &[]),
        PlanStep {
            step: 2,
            operation: PlanOperation::Subquery {
                alias: "scalar_subquery".to_string(),
                inner_steps: vec![
                    table_scan("orders", &["user_id"], &["orders.user_id = customers.id"]),
                    PlanStep {
                        step: 3,
                        operation: PlanOperation::Aggregate {
                            group_by: vec![],
                            functions: vec!["COUNT(*)".to_string()],
                        },
                        notes: None,
                    },
                ],
            },
            notes: None,
        },
        PlanStep {
            step: 4,
            operation: PlanOperation::Projection {
                columns: vec!["id".to_string(), "scalar_subquery".to_string()],
            },
            notes: None,
        },
    ];
    let result = estimate_cost(&steps, &schema, SqlDialect::BigQuery);
    // Both tables should appear in the breakdown
    let table_names: Vec<&str> = result
        .table_breakdown
        .iter()
        .map(|tb| tb.table.as_str())
        .collect();
    assert!(
        table_names.contains(&"customers"),
        "Outer table 'customers' should be in breakdown, got {:?}",
        table_names
    );
    assert!(
        table_names.contains(&"orders"),
        "Inner subquery table 'orders' should be in breakdown, got {:?}",
        table_names
    );
    // Both tables should contribute bytes
    let customers_bytes = result
        .table_breakdown
        .iter()
        .find(|tb| tb.table == "customers")
        .map(|tb| tb.bytes_scanned)
        .unwrap_or(0);
    let orders_bytes = result
        .table_breakdown
        .iter()
        .find(|tb| tb.table == "orders")
        .map(|tb| tb.bytes_scanned)
        .unwrap_or(0);
    assert!(
        customers_bytes > 0,
        "customers should have > 0 bytes, got {customers_bytes}"
    );
    assert!(
        orders_bytes > 0,
        "orders should have > 0 bytes, got {orders_bytes}"
    );
    assert!(
        result.bytes_scanned >= customers_bytes + orders_bytes,
        "Total bytes should include both tables"
    );
}

#[test]
fn in_subquery_counts_inner_table() {
    // Simulates: SELECT * FROM customers WHERE id IN (SELECT user_id FROM orders WHERE status = 'completed')
    // Both customers (outer) and orders (inner IN subquery) should be counted.
    let schema = schema_with_rich_stats();
    let steps = vec![
        table_scan("customers", &["id", "name", "segment"], &[]),
        PlanStep {
            step: 2,
            operation: PlanOperation::Subquery {
                alias: "in_subquery".to_string(),
                inner_steps: vec![table_scan(
                    "orders",
                    &["user_id", "status"],
                    &["status = 'completed'"],
                )],
            },
            notes: None,
        },
        join_step(
            3,
            JoinKind::LeftSemi,
            "customers",
            "in_subquery",
            "customers.id = orders.user_id",
        ),
        PlanStep {
            step: 4,
            operation: PlanOperation::Projection {
                columns: vec!["id".to_string(), "name".to_string(), "segment".to_string()],
            },
            notes: None,
        },
    ];
    let result = estimate_cost(&steps, &schema, SqlDialect::BigQuery);
    let table_names: Vec<&str> = result
        .table_breakdown
        .iter()
        .map(|tb| tb.table.as_str())
        .collect();
    assert!(
        table_names.contains(&"customers"),
        "Outer table 'customers' should be in breakdown, got {:?}",
        table_names
    );
    assert!(
        table_names.contains(&"orders"),
        "IN subquery table 'orders' should be in breakdown, got {:?}",
        table_names
    );
    let orders_bytes = result
        .table_breakdown
        .iter()
        .find(|tb| tb.table == "orders")
        .map(|tb| tb.bytes_scanned)
        .unwrap_or(0);
    assert!(
        orders_bytes > 0,
        "orders from IN subquery should have > 0 bytes, got {orders_bytes}"
    );
    // The LeftSemi join should trigger confidence degradation
    assert_eq!(
        result.confidence,
        EstimationConfidence::Low,
        "Semi join should degrade confidence to Low"
    );
}

// ============================================================================
// Section: Range filter selectivity improvements
// ============================================================================

#[test]
fn range_selectivity_uses_min_max_stats() {
    // orders.amount has min=0.01, max=99999.99
    // amount > 50000 should give ~50% selectivity, not the default 30%
    let schema = schema_with_rich_stats();
    let steps = vec![table_scan("orders", &["id", "amount"], &["amount > 50000"])];
    let result = estimate_cost(&steps, &schema, SqlDialect::BigQuery);
    let sel = result.table_breakdown[0].selectivity;
    // (99999.99 - 50000) / (99999.99 - 0.01) ≈ 0.5 * null_factor(1.0) = 0.5
    assert!(
        (sel - 0.5).abs() < 0.05,
        "Range selectivity with min/max stats should be ~0.5, got {sel}"
    );
    assert!(
        result.cost_range.is_some(),
        "Stats-based selectivity should produce CostRange"
    );
}

#[test]
fn range_selectivity_date_columns() {
    // orders.order_date: min=2023-01-01, max=2024-12-31
    // order_date >= '2024-07-01' should produce a selectivity based on date range
    let schema = schema_with_rich_stats();
    let steps = vec![table_scan(
        "orders",
        &["id", "order_date"],
        &["order_date >= '2024-07-01'"],
    )];
    let result = estimate_cost(&steps, &schema, SqlDialect::BigQuery);
    let sel = result.table_breakdown[0].selectivity;
    // Date range: 2023-01-01 to 2024-12-31 ≈ 730 days
    // 2024-07-01 to 2024-12-31 ≈ 183 days → ~0.25
    assert!(
        sel > 0.1 && sel < 0.5,
        "Date range selectivity should be between 0.1 and 0.5, got {sel}"
    );
    assert!(
        result.cost_range.is_some(),
        "Date range with stats should produce CostRange"
    );
}

#[test]
fn range_selectivity_no_stats_date_column() {
    // Test the no-stats path with a DATE column: should get 0.25 instead of 0.3
    // Build a custom schema with DATE column but no table-level statistics.
    let mut tables = HashMap::new();
    tables.insert(
        "events".to_string(),
        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,
                },
                ColumnDef {
                    name: "event_date".to_string(),
                    data_type: "DATE".to_string(),
                    nullable: false,
                    description: None,
                    tags: vec![],
                    synonyms: vec![],
                    statistics: None,
                },
                ColumnDef {
                    name: "value".to_string(),
                    data_type: "INT".to_string(),
                    nullable: true,
                    description: None,
                    tags: vec![],
                    synonyms: vec![],
                    statistics: None,
                },
            ],
            primary_key: None,
            foreign_keys: vec![],
            // No table statistics
            statistics: None,
            clustering_keys: vec![],
        },
    );
    let no_stats_schema = SchemaDefinition {
        version: "1".to_string(),
        dialect: SqlDialect::BigQuery,
        tables,
        database: None,
        schema_name: None,
        glossary: None,
    };

    // Range filter on DATE column should use 0.25 (temporal heuristic)
    let steps_date = vec![table_scan(
        "events",
        &["id", "event_date"],
        &["event_date >= '2024-01-01'"],
    )];
    let result_date = estimate_cost(&steps_date, &no_stats_schema, SqlDialect::BigQuery);
    let sel_date = result_date.table_breakdown[0].selectivity;
    assert!(
        (sel_date - 0.25).abs() < 0.01,
        "DATE column range filter should use 0.25 heuristic, got {sel_date}"
    );

    // Range filter on non-temporal column should still use 0.3
    let steps_int = vec![table_scan("events", &["id", "value"], &["value > 100"])];
    let result_int = estimate_cost(&steps_int, &no_stats_schema, SqlDialect::BigQuery);
    let sel_int = result_int.table_breakdown[0].selectivity;
    assert!(
        (sel_int - 0.3).abs() < 0.01,
        "INT column range filter should use 0.3 heuristic, got {sel_int}"
    );
}

#[test]
fn range_selectivity_timestamp_parsing() {
    // Verify that timestamp strings like '2024-01-15 00:00:00' are parsed correctly
    // for range selectivity computation using min/max stats.
    let mut tables = HashMap::new();
    tables.insert(
        "logs".to_string(),
        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,
                },
                ColumnDef {
                    name: "created_at".to_string(),
                    data_type: "TIMESTAMP".to_string(),
                    nullable: false,
                    description: None,
                    tags: vec![],
                    synonyms: vec![],
                    statistics: Some(ColumnStatistics {
                        n_distinct: Some(10000.0),
                        null_fraction: Some(0.0),
                        min_value: Some("2023-01-01 00:00:00".to_string()),
                        max_value: Some("2024-12-31 23:59:59".to_string()),
                        most_common_values: vec![],
                        avg_bytes: None,
                        compression_ratio: None,
                    }),
                },
            ],
            primary_key: None,
            foreign_keys: vec![],
            statistics: Some(TableStatistics {
                row_count: Some(500_000),
                avg_row_bytes: Some(128),
                partitioned: false,
                partition_columns: vec![],
                correlated_columns: vec![],
                ..Default::default()
            }),
            clustering_keys: vec![],
        },
    );
    let schema = SchemaDefinition {
        version: "1".to_string(),
        dialect: SqlDialect::BigQuery,
        tables,
        database: None,
        schema_name: None,
        glossary: None,
    };

    // created_at >= '2024-07-01 00:00:00' should produce valid range selectivity
    let steps = vec![table_scan(
        "logs",
        &["id", "created_at"],
        &["created_at >= '2024-07-01 00:00:00'"],
    )];
    let result = estimate_cost(&steps, &schema, SqlDialect::BigQuery);
    let sel = result.table_breakdown[0].selectivity;
    // Timestamp range: 2023-01-01 to 2024-12-31 ≈ 730 days
    // 2024-07-01 to 2024-12-31 ≈ 183 days → ~0.25
    assert!(
        sel > 0.1 && sel < 0.5,
        "Timestamp range selectivity should be between 0.1 and 0.5, got {sel}"
    );
    assert!(
        result.bytes_scanned > 0,
        "Timestamp range query should estimate > 0 bytes"
    );
    assert!(
        result.cost_range.is_some(),
        "Stats-based timestamp range should produce CostRange"
    );
}
