//! Benchmark tests for cost estimation across dialects.
//!
//! Uses a realistic warehouse schema with large table statistics
//! to verify cost estimates are in reasonable ranges for BigQuery,
//! Snowflake, and Databricks pricing models.

use altimate_core::complexity::cost_estimator::estimate_cost;
use altimate_core::complexity::types::CostTier;
use altimate_core::explainer::types::{JoinKind, PlanOperation, PlanStep};
use altimate_core::schema::types::{SchemaDefinition, SqlDialect};
use std::path::Path;

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 at {}: {e}", path.display()));
    serde_yaml::from_str(&content).expect("Failed to parse benchmark schema YAML")
}

// ---------- Plan step builders for each query pattern ----------

/// Simple lookup: SELECT * FROM dim_products WHERE id = 42
fn steps_simple_lookup() -> Vec<PlanStep> {
    vec![PlanStep {
        step: 1,
        operation: PlanOperation::TableScan {
            table: "dim_products".to_string(),
            columns_read: vec![
                "id".to_string(),
                "name".to_string(),
                "category".to_string(),
                "price".to_string(),
            ],
            filters: vec!["id = 42".to_string()],
        },
        notes: None,
    }]
}

/// Full table scan: SELECT * FROM fact_orders
fn steps_full_table_scan() -> Vec<PlanStep> {
    vec![PlanStep {
        step: 1,
        operation: PlanOperation::TableScan {
            table: "fact_orders".to_string(),
            columns_read: vec![
                "id".to_string(),
                "user_id".to_string(),
                "product_id".to_string(),
                "amount".to_string(),
                "order_date".to_string(),
                "status".to_string(),
                "region".to_string(),
                "channel".to_string(),
            ],
            filters: vec![],
        },
        notes: None,
    }]
}

/// Filtered scan with partition column: SELECT * FROM fact_orders WHERE order_date = '...'
fn steps_filtered_scan() -> Vec<PlanStep> {
    vec![PlanStep {
        step: 1,
        operation: PlanOperation::TableScan {
            table: "fact_orders".to_string(),
            columns_read: vec![
                "id".to_string(),
                "user_id".to_string(),
                "product_id".to_string(),
                "amount".to_string(),
                "order_date".to_string(),
                "status".to_string(),
                "region".to_string(),
                "channel".to_string(),
            ],
            filters: vec!["order_date = '2024-01-01'".to_string()],
        },
        notes: None,
    }]
}

/// Multi join: fact_orders JOIN dim_customers JOIN dim_products with partition filter
fn steps_multi_join() -> Vec<PlanStep> {
    vec![
        PlanStep {
            step: 1,
            operation: PlanOperation::TableScan {
                table: "fact_orders".to_string(),
                columns_read: vec![
                    "id".to_string(),
                    "user_id".to_string(),
                    "product_id".to_string(),
                ],
                filters: vec!["order_date = '2024-01-01'".to_string()],
            },
            notes: None,
        },
        PlanStep {
            step: 2,
            operation: PlanOperation::TableScan {
                table: "dim_customers".to_string(),
                columns_read: vec!["id".to_string(), "name".to_string()],
                filters: vec![],
            },
            notes: None,
        },
        PlanStep {
            step: 3,
            operation: PlanOperation::TableScan {
                table: "dim_products".to_string(),
                columns_read: vec!["id".to_string(), "name".to_string()],
                filters: vec![],
            },
            notes: None,
        },
        PlanStep {
            step: 4,
            operation: PlanOperation::Join {
                join_type: JoinKind::Inner,
                left: "fact_orders".to_string(),
                right: "dim_customers".to_string(),
                condition: "o.user_id = c.id".to_string(),
            },
            notes: None,
        },
        PlanStep {
            step: 5,
            operation: PlanOperation::Join {
                join_type: JoinKind::Inner,
                left: "result".to_string(),
                right: "dim_products".to_string(),
                condition: "o.product_id = p.id".to_string(),
            },
            notes: None,
        },
    ]
}

/// Cross join: dim_customers CROSS JOIN dim_regions
fn steps_cross_join() -> Vec<PlanStep> {
    vec![
        PlanStep {
            step: 1,
            operation: PlanOperation::TableScan {
                table: "dim_customers".to_string(),
                columns_read: vec![
                    "id".to_string(),
                    "email".to_string(),
                    "name".to_string(),
                    "country".to_string(),
                    "segment".to_string(),
                ],
                filters: vec![],
            },
            notes: None,
        },
        PlanStep {
            step: 2,
            operation: PlanOperation::TableScan {
                table: "dim_regions".to_string(),
                columns_read: vec![
                    "id".to_string(),
                    "name".to_string(),
                    "country".to_string(),
                    "timezone".to_string(),
                ],
                filters: vec![],
            },
            notes: None,
        },
        PlanStep {
            step: 3,
            operation: PlanOperation::Join {
                join_type: JoinKind::Cross,
                left: "dim_customers".to_string(),
                right: "dim_regions".to_string(),
                condition: String::new(),
            },
            notes: None,
        },
    ]
}

/// Correlated subquery: two scans of fact_orders
fn steps_subquery_correlated() -> Vec<PlanStep> {
    vec![
        PlanStep {
            step: 1,
            operation: PlanOperation::TableScan {
                table: "fact_orders".to_string(),
                columns_read: vec![
                    "id".to_string(),
                    "amount".to_string(),
                    "user_id".to_string(),
                ],
                filters: vec![],
            },
            notes: None,
        },
        PlanStep {
            step: 2,
            operation: PlanOperation::Subquery {
                alias: "subq".to_string(),
                inner_steps: vec![PlanStep {
                    step: 1,
                    operation: PlanOperation::TableScan {
                        table: "fact_orders".to_string(),
                        columns_read: vec!["amount".to_string(), "user_id".to_string()],
                        filters: vec!["user_id = o.user_id".to_string()],
                    },
                    notes: None,
                }],
            },
            notes: None,
        },
    ]
}

/// Window analytics: full scan of fact_orders with window functions
fn steps_window_analytics() -> Vec<PlanStep> {
    vec![PlanStep {
        step: 1,
        operation: PlanOperation::TableScan {
            table: "fact_orders".to_string(),
            columns_read: vec![
                "user_id".to_string(),
                "order_date".to_string(),
                "amount".to_string(),
            ],
            filters: vec![],
        },
        notes: None,
    }]
}

/// CTE heavy: scans fact_orders (filtered) + dim_customers
fn steps_cte_heavy() -> Vec<PlanStep> {
    vec![
        PlanStep {
            step: 1,
            operation: PlanOperation::TableScan {
                table: "fact_orders".to_string(),
                columns_read: vec![
                    "id".to_string(),
                    "user_id".to_string(),
                    "amount".to_string(),
                    "order_date".to_string(),
                ],
                filters: vec!["order_date >= '2024-01-01'".to_string()],
            },
            notes: None,
        },
        PlanStep {
            step: 2,
            operation: PlanOperation::TableScan {
                table: "dim_customers".to_string(),
                columns_read: vec![
                    "id".to_string(),
                    "email".to_string(),
                    "name".to_string(),
                    "country".to_string(),
                    "segment".to_string(),
                ],
                filters: vec![],
            },
            notes: None,
        },
        PlanStep {
            step: 3,
            operation: PlanOperation::Join {
                join_type: JoinKind::Inner,
                left: "top_customers".to_string(),
                right: "dim_customers".to_string(),
                condition: "c.id = tc.user_id".to_string(),
            },
            notes: None,
        },
    ]
}

/// Aggregation: full scan of fact_orders for GROUP BY
fn steps_aggregation_groupby() -> Vec<PlanStep> {
    vec![PlanStep {
        step: 1,
        operation: PlanOperation::TableScan {
            table: "fact_orders".to_string(),
            columns_read: vec![
                "region".to_string(),
                "status".to_string(),
                "amount".to_string(),
            ],
            filters: vec![],
        },
        notes: None,
    }]
}

/// Mixed complexity: multi-join with multiple filters
fn steps_mixed_complexity() -> Vec<PlanStep> {
    vec![
        PlanStep {
            step: 1,
            operation: PlanOperation::TableScan {
                table: "fact_orders".to_string(),
                columns_read: vec![
                    "id".to_string(),
                    "user_id".to_string(),
                    "product_id".to_string(),
                    "amount".to_string(),
                    "order_date".to_string(),
                ],
                filters: vec![
                    "order_date BETWEEN '2024-01-01' AND '2024-12-31'".to_string(),
                    "amount > 0".to_string(),
                ],
            },
            notes: None,
        },
        PlanStep {
            step: 2,
            operation: PlanOperation::TableScan {
                table: "dim_customers".to_string(),
                columns_read: vec![
                    "id".to_string(),
                    "segment".to_string(),
                    "country".to_string(),
                ],
                filters: vec!["country = 'US'".to_string()],
            },
            notes: None,
        },
        PlanStep {
            step: 3,
            operation: PlanOperation::TableScan {
                table: "dim_products".to_string(),
                columns_read: vec!["id".to_string(), "category".to_string()],
                filters: vec![],
            },
            notes: None,
        },
        PlanStep {
            step: 4,
            operation: PlanOperation::Join {
                join_type: JoinKind::Inner,
                left: "fact_orders".to_string(),
                right: "dim_customers".to_string(),
                condition: "o.user_id = c.id".to_string(),
            },
            notes: None,
        },
        PlanStep {
            step: 5,
            operation: PlanOperation::Join {
                join_type: JoinKind::Inner,
                left: "result".to_string(),
                right: "dim_products".to_string(),
                condition: "o.product_id = p.id".to_string(),
            },
            notes: None,
        },
    ]
}

// ---------- Cost range test macro ----------

macro_rules! cost_range_test {
    ($name:ident, $steps_fn:ident, $dialect:expr, $min:expr, $max:expr) => {
        #[test]
        fn $name() {
            let schema = benchmark_schema();
            let steps = $steps_fn();
            let result = estimate_cost(&steps, &schema, $dialect);
            assert!(
                result.cost_usd >= $min,
                "{}: cost ${:.6} below min ${}",
                stringify!($name),
                result.cost_usd,
                $min
            );
            assert!(
                result.cost_usd <= $max,
                "{}: cost ${:.6} above max ${}",
                stringify!($name),
                result.cost_usd,
                $max
            );
        }
    };
}

// ---------- BigQuery cost range tests ----------
// BigQuery: $6.25/TB

// simple_lookup: dim_products 1M rows * 128 bytes * 0.1 selectivity = 12.8MB ~ $0.00007
cost_range_test!(
    bench_simple_lookup_bigquery,
    steps_simple_lookup,
    SqlDialect::BigQuery,
    0.0,
    0.001
);

// full_table_scan: fact_orders 1B * 512 = 512GB, no partition filter warning
// 512GB = 0.5TB * $6.25 = $3.125
cost_range_test!(
    bench_full_table_scan_bigquery,
    steps_full_table_scan,
    SqlDialect::BigQuery,
    1.0,
    10.0
);

// filtered_scan: fact_orders with partition filter (order_date)
// With column stats: selectivity = 1/n_distinct(730) = 0.00137, partition factor = 1/730
// 1B * 512 * 0.00137 * 0.00137 = ~960KB ~ $0.000006
// Very small because partition pruning is now n_distinct-based, not flat 0.1
cost_range_test!(
    bench_filtered_scan_bigquery,
    steps_filtered_scan,
    SqlDialect::BigQuery,
    0.0,
    1.0
);

// multi_join: fact_orders (filtered+partition) + dim_customers (full) + dim_products (full)
cost_range_test!(
    bench_multi_join_bigquery,
    steps_multi_join,
    SqlDialect::BigQuery,
    0.01,
    100.0
);

// cross_join: dim_customers (50M * 256 = 12.8GB) CROSS JOIN dim_regions (500 * 64 = 32KB)
// Cross join bytes = 50M * 500 * max_avg_bytes = very large
cost_range_test!(
    bench_cross_join_bigquery,
    steps_cross_join,
    SqlDialect::BigQuery,
    0.01,
    100_000.0
);

// subquery_correlated: two scans of fact_orders (1 full + 1 filtered in subquery)
cost_range_test!(
    bench_subquery_correlated_bigquery,
    steps_subquery_correlated,
    SqlDialect::BigQuery,
    0.5,
    50.0
);

// window_analytics: fact_orders scanning 3 cols (user_id:4 + order_date:4 + amount:16 = 24)
// out of 8 cols (total ~182 bytes), col_ratio ~0.13 → ~67.5GB on BigQuery = ~$0.41
cost_range_test!(
    bench_window_analytics_bigquery,
    steps_window_analytics,
    SqlDialect::BigQuery,
    0.2,
    10.0
);

// cte_heavy: fact_orders filtered + dim_customers full
cost_range_test!(
    bench_cte_heavy_bigquery,
    steps_cte_heavy,
    SqlDialect::BigQuery,
    0.01,
    100.0
);

// aggregation: fact_orders 3 cols / 8 cols = 0.375 ratio, 1B * 512 * 0.375 = 192GB
cost_range_test!(
    bench_aggregation_bigquery,
    steps_aggregation_groupby,
    SqlDialect::BigQuery,
    0.5,
    10.0
);

// mixed_complexity: multi-join with filters
cost_range_test!(
    bench_mixed_complexity_bigquery,
    steps_mixed_complexity,
    SqlDialect::BigQuery,
    0.001,
    100.0
);

// ---------- Snowflake cost range tests ----------
// Snowflake: $1.80/TB (cheaper than BigQuery)

cost_range_test!(
    bench_simple_lookup_snowflake,
    steps_simple_lookup,
    SqlDialect::Snowflake,
    0.0,
    0.001
);
cost_range_test!(
    bench_full_table_scan_snowflake,
    steps_full_table_scan,
    SqlDialect::Snowflake,
    0.5,
    5.0
);
cost_range_test!(
    bench_filtered_scan_snowflake,
    steps_filtered_scan,
    SqlDialect::Snowflake,
    0.0,
    1.0
);
cost_range_test!(
    bench_multi_join_snowflake,
    steps_multi_join,
    SqlDialect::Snowflake,
    0.001,
    50.0
);
cost_range_test!(
    bench_cross_join_snowflake,
    steps_cross_join,
    SqlDialect::Snowflake,
    0.001,
    100_000.0
);
cost_range_test!(
    bench_subquery_correlated_snowflake,
    steps_subquery_correlated,
    SqlDialect::Snowflake,
    0.1,
    20.0
);
cost_range_test!(
    bench_window_analytics_snowflake,
    steps_window_analytics,
    SqlDialect::Snowflake,
    0.1,
    5.0
);
cost_range_test!(
    bench_cte_heavy_snowflake,
    steps_cte_heavy,
    SqlDialect::Snowflake,
    0.001,
    50.0
);
cost_range_test!(
    bench_aggregation_snowflake,
    steps_aggregation_groupby,
    SqlDialect::Snowflake,
    0.1,
    5.0
);
cost_range_test!(
    bench_mixed_complexity_snowflake,
    steps_mixed_complexity,
    SqlDialect::Snowflake,
    0.0001,
    50.0
);

// ---------- Databricks cost range tests ----------
// Databricks: $3.025/TB

cost_range_test!(
    bench_simple_lookup_databricks,
    steps_simple_lookup,
    SqlDialect::Databricks,
    0.0,
    0.001
);
cost_range_test!(
    bench_full_table_scan_databricks,
    steps_full_table_scan,
    SqlDialect::Databricks,
    0.5,
    10.0
);
cost_range_test!(
    bench_filtered_scan_databricks,
    steps_filtered_scan,
    SqlDialect::Databricks,
    0.0,
    1.0
);
cost_range_test!(
    bench_multi_join_databricks,
    steps_multi_join,
    SqlDialect::Databricks,
    0.001,
    100.0
);
cost_range_test!(
    bench_cross_join_databricks,
    steps_cross_join,
    SqlDialect::Databricks,
    0.001,
    100_000.0
);
cost_range_test!(
    bench_subquery_correlated_databricks,
    steps_subquery_correlated,
    SqlDialect::Databricks,
    0.1,
    50.0
);
cost_range_test!(
    bench_window_analytics_databricks,
    steps_window_analytics,
    SqlDialect::Databricks,
    0.1,
    10.0
);
cost_range_test!(
    bench_cte_heavy_databricks,
    steps_cte_heavy,
    SqlDialect::Databricks,
    0.001,
    100.0
);
cost_range_test!(
    bench_aggregation_databricks,
    steps_aggregation_groupby,
    SqlDialect::Databricks,
    0.1,
    10.0
);
cost_range_test!(
    bench_mixed_complexity_databricks,
    steps_mixed_complexity,
    SqlDialect::Databricks,
    0.0001,
    100.0
);

// ---------- Cross-dialect comparison tests ----------

#[test]
fn bench_bigquery_more_expensive_than_snowflake() {
    let schema = benchmark_schema();
    let steps = steps_full_table_scan();
    let bq = estimate_cost(&steps, &schema, SqlDialect::BigQuery);
    let sf = estimate_cost(&steps, &schema, SqlDialect::Snowflake);
    assert!(
        bq.cost_usd > sf.cost_usd,
        "BigQuery (${:.4}) should be more expensive than Snowflake (${:.4}) for same scan",
        bq.cost_usd,
        sf.cost_usd
    );
}

#[test]
fn bench_bigquery_more_expensive_than_databricks() {
    let schema = benchmark_schema();
    let steps = steps_full_table_scan();
    let bq = estimate_cost(&steps, &schema, SqlDialect::BigQuery);
    let db = estimate_cost(&steps, &schema, SqlDialect::Databricks);
    assert!(
        bq.cost_usd > db.cost_usd,
        "BigQuery (${:.4}) should be more expensive than Databricks (${:.4}) for same scan",
        bq.cost_usd,
        db.cost_usd
    );
}

#[test]
fn bench_databricks_more_expensive_than_snowflake() {
    let schema = benchmark_schema();
    let steps = steps_full_table_scan();
    let db = estimate_cost(&steps, &schema, SqlDialect::Databricks);
    let sf = estimate_cost(&steps, &schema, SqlDialect::Snowflake);
    assert!(
        db.cost_usd > sf.cost_usd,
        "Databricks (${:.4}) should be more expensive than Snowflake (${:.4}) for same scan",
        db.cost_usd,
        sf.cost_usd
    );
}

// ---------- Disclaimer tests ----------

#[test]
fn bench_snowflake_has_disclaimer() {
    let schema = benchmark_schema();
    let steps = steps_simple_lookup();
    let result = estimate_cost(&steps, &schema, SqlDialect::Snowflake);
    assert!(
        result.disclaimer.is_some(),
        "Snowflake estimates should include a credit-based disclaimer"
    );
    assert!(result.disclaimer.unwrap().contains("credit"));
}

#[test]
fn bench_databricks_has_disclaimer() {
    let schema = benchmark_schema();
    let steps = steps_simple_lookup();
    let result = estimate_cost(&steps, &schema, SqlDialect::Databricks);
    assert!(
        result.disclaimer.is_some(),
        "Databricks estimates should include a DBU-based disclaimer"
    );
    assert!(result.disclaimer.unwrap().contains("DBU"));
}

#[test]
fn bench_bigquery_no_disclaimer() {
    let schema = benchmark_schema();
    let steps = steps_simple_lookup();
    let result = estimate_cost(&steps, &schema, SqlDialect::BigQuery);
    assert!(
        result.disclaimer.is_none(),
        "BigQuery uses simple per-TB pricing and should have no disclaimer"
    );
}

// ---------- Partition pruning tests ----------

#[test]
fn bench_partition_filter_reduces_cost() {
    let schema = benchmark_schema();
    let full = estimate_cost(&steps_full_table_scan(), &schema, SqlDialect::BigQuery);
    let filtered = estimate_cost(&steps_filtered_scan(), &schema, SqlDialect::BigQuery);
    assert!(
        filtered.cost_usd < full.cost_usd * 0.5,
        "Partition-filtered scan (${:.4}) should be <50% of full scan (${:.4})",
        filtered.cost_usd,
        full.cost_usd
    );
}

#[test]
fn bench_full_scan_partition_warning() {
    let schema = benchmark_schema();
    let result = estimate_cost(&steps_full_table_scan(), &schema, SqlDialect::BigQuery);
    assert!(
        result.warnings.iter().any(|w| w.contains("partitioned")),
        "Full scan on partitioned table should produce partition warning"
    );
}

#[test]
fn bench_filtered_scan_no_partition_warning() {
    let schema = benchmark_schema();
    let result = estimate_cost(&steps_filtered_scan(), &schema, SqlDialect::BigQuery);
    assert!(
        !result.warnings.iter().any(|w| w.contains("partitioned")),
        "Scan with partition filter should not produce partition warning"
    );
}

// ---------- Cross join cost tests ----------

#[test]
fn bench_cross_join_produces_high_cost() {
    let schema = benchmark_schema();
    let result = estimate_cost(&steps_cross_join(), &schema, SqlDialect::BigQuery);
    // dim_customers (50M) x dim_regions (500) = 25B result rows
    // This should be much more expensive than a simple dim_customers scan
    let simple = estimate_cost(
        &[PlanStep {
            step: 1,
            operation: PlanOperation::TableScan {
                table: "dim_customers".to_string(),
                columns_read: vec!["id".to_string()],
                filters: vec![],
            },
            notes: None,
        }],
        &schema,
        SqlDialect::BigQuery,
    );
    assert!(
        result.cost_usd > simple.cost_usd,
        "Cross join (${:.4}) should be more expensive than simple scan (${:.4})",
        result.cost_usd,
        simple.cost_usd
    );
}

#[test]
fn bench_cross_join_warning_present() {
    let schema = benchmark_schema();
    let result = estimate_cost(&steps_cross_join(), &schema, SqlDialect::BigQuery);
    assert!(
        result.warnings.iter().any(|w| w.contains("CROSS JOIN")),
        "Cross join should produce a CROSS JOIN warning, got: {:?}",
        result.warnings
    );
}

// ---------- Edge case tests ----------

#[test]
fn bench_empty_schema_returns_fallback_tier() {
    use std::collections::HashMap;
    let schema = SchemaDefinition {
        version: "1".to_string(),
        dialect: SqlDialect::Generic,
        tables: HashMap::new(),
        database: None,
        schema_name: None,
        glossary: None,
    };
    let steps = steps_simple_lookup();
    let result = estimate_cost(&steps, &schema, SqlDialect::BigQuery);
    // Table not found in schema -- fallback heuristic still produces a cost estimate
    assert_ne!(result.cost_tier, CostTier::Unknown);
    assert!(result.bytes_scanned > 0);
}

#[test]
fn bench_table_breakdown_correct_count() {
    let schema = benchmark_schema();
    let result = estimate_cost(&steps_multi_join(), &schema, SqlDialect::BigQuery);
    assert_eq!(
        result.table_breakdown.len(),
        3,
        "Multi-join should have 3 table breakdowns (fact_orders, dim_customers, dim_products)"
    );
}

#[test]
fn bench_all_tables_have_statistics() {
    let schema = benchmark_schema();
    let result = estimate_cost(&steps_multi_join(), &schema, SqlDialect::BigQuery);
    for tb in &result.table_breakdown {
        assert!(
            tb.has_statistics,
            "Table '{}' should have statistics in benchmark schema",
            tb.table
        );
    }
}

// ---------- CostRange tests (research-driven) ----------

#[test]
fn bench_filtered_scan_produces_cost_range() {
    let schema = benchmark_schema();
    let result = estimate_cost(&steps_filtered_scan(), &schema, SqlDialect::BigQuery);
    assert!(
        result.cost_range.is_some(),
        "Filtered scan with column stats should produce a CostRange"
    );
    let range = result.cost_range.unwrap();
    assert!(
        range.low <= range.expected,
        "CostRange: low ({}) should be <= expected ({})",
        range.low,
        range.expected
    );
    assert!(
        range.expected <= range.high,
        "CostRange: expected ({}) should be <= high ({})",
        range.expected,
        range.high
    );
}

#[test]
fn bench_full_scan_no_cost_range() {
    let schema = benchmark_schema();
    let result = estimate_cost(&steps_full_table_scan(), &schema, SqlDialect::BigQuery);
    assert!(
        result.cost_range.is_none(),
        "Full scan (no filters) should not produce CostRange since no stats-based selectivity is used"
    );
}

#[test]
fn bench_mixed_complexity_produces_cost_range() {
    let schema = benchmark_schema();
    let result = estimate_cost(&steps_mixed_complexity(), &schema, SqlDialect::BigQuery);
    assert!(
        result.cost_range.is_some(),
        "Mixed complexity query with column-stats-driven filters should produce CostRange"
    );
    let range = result.cost_range.unwrap();
    assert!(range.low <= range.expected);
    assert!(range.expected <= range.high);
}

#[test]
fn bench_cte_heavy_produces_cost_range() {
    let schema = benchmark_schema();
    let result = estimate_cost(&steps_cte_heavy(), &schema, SqlDialect::BigQuery);
    assert!(
        result.cost_range.is_some(),
        "CTE query with date range filter (column stats) should produce CostRange"
    );
}

#[test]
fn bench_cost_range_ordering_across_dialects() {
    let schema = benchmark_schema();
    for dialect in [
        SqlDialect::BigQuery,
        SqlDialect::Snowflake,
        SqlDialect::Databricks,
    ] {
        let result = estimate_cost(&steps_filtered_scan(), &schema, dialect);
        if let Some(range) = &result.cost_range {
            assert!(
                range.low <= range.expected,
                "{dialect}: low ({}) <= expected ({})",
                range.low,
                range.expected
            );
            assert!(
                range.expected <= range.high,
                "{dialect}: expected ({}) <= high ({})",
                range.expected,
                range.high
            );
        }
    }
}

#[test]
fn bench_gating_tier_uses_pessimistic() {
    let schema = benchmark_schema();
    let result = estimate_cost(&steps_filtered_scan(), &schema, SqlDialect::BigQuery);
    if let Some(ref range) = result.cost_range {
        let gating = result.gating_tier();
        let pessimistic_tier = CostTier::from_cost(range.high);
        assert_eq!(
            gating, pessimistic_tier,
            "gating_tier() should use the pessimistic high cost for classification"
        );
    }
}

#[test]
fn bench_simple_lookup_cost_range_with_stats() {
    let schema = benchmark_schema();
    // dim_products.id has n_distinct=-1.0, so equality on id uses stats
    let result = estimate_cost(&steps_simple_lookup(), &schema, SqlDialect::BigQuery);
    assert!(
        result.cost_range.is_some(),
        "Point lookup on column with n_distinct stats should produce CostRange"
    );
}

#[test]
fn bench_mcv_status_selectivity() {
    // fact_orders.status MCV: completed=0.60
    let schema = benchmark_schema();
    let steps = vec![PlanStep {
        step: 1,
        operation: PlanOperation::TableScan {
            table: "fact_orders".to_string(),
            columns_read: vec!["id".to_string(), "status".to_string()],
            filters: vec!["status = 'completed'".to_string()],
        },
        notes: None,
    }];
    let result = estimate_cost(&steps, &schema, SqlDialect::BigQuery);
    assert!(result.cost_range.is_some());
    let sel = result.table_breakdown[0].selectivity;
    // MCV frequency 0.60 * (1.0 - 0.01 null_fraction) = 0.594
    assert!(
        (sel - 0.594).abs() < 0.02,
        "Expected MCV selectivity ~0.594 for 'completed', got {sel}"
    );
}

#[test]
fn bench_correlated_columns_selectivity() {
    // status and region are correlated in fact_orders
    let schema = benchmark_schema();
    let steps = vec![PlanStep {
        step: 1,
        operation: PlanOperation::TableScan {
            table: "fact_orders".to_string(),
            columns_read: vec!["id".to_string(), "status".to_string(), "region".to_string()],
            filters: vec![
                "status = 'completed'".to_string(),
                "region = 'US-East'".to_string(),
            ],
        },
        notes: None,
    }];
    let result = estimate_cost(&steps, &schema, SqlDialect::BigQuery);
    let sel = result.table_breakdown[0].selectivity;
    // Correlated: should use max() instead of multiplication
    // status=completed: 0.594, region=US-East: 0.25*0.98=0.245
    // max(0.594, 0.245) = 0.594
    assert!(
        sel > 0.50,
        "Correlated columns should not over-multiply: got {sel}"
    );
}

// ===========================================================================
// Snowflake Validation Regression Tests
//
// These tests encode findings from the Snowflake validation experiment
// (experiments/snowflake_validation/). They verify that key behaviors
// discovered during validation remain stable across code changes.
// ===========================================================================

#[test]
fn snowflake_validation_metadata_count_star_zero_bytes() {
    // Q2: SELECT COUNT(*) FROM fact_orders
    // Snowflake: 0 bytes (answered from metadata)
    // altimate-core should detect metadata-only aggregation and return 0 bytes.
    let schema = benchmark_schema();
    let steps = vec![
        PlanStep {
            step: 1,
            operation: PlanOperation::TableScan {
                table: "fact_orders".to_string(),
                columns_read: vec![],
                filters: vec![],
            },
            notes: None,
        },
        PlanStep {
            step: 2,
            operation: PlanOperation::Aggregate {
                group_by: vec![],
                functions: vec!["COUNT(*)".to_string()],
            },
            notes: None,
        },
    ];
    let result = estimate_cost(&steps, &schema, SqlDialect::Snowflake);
    assert_eq!(
        result.bytes_scanned, 0,
        "COUNT(*) without filters should detect metadata-only aggregation (0 bytes)"
    );
}

#[test]
fn snowflake_validation_window_function_no_multiplier() {
    // Snowflake validation (986 queries) showed:
    // - 78/93 window queries are accurate at 1x (no multiplier)
    // - 15/93 ranking+PARTITION BY queries scan 2x, but applying 2x broadly
    //   also breaks 14 queries → nearly zero net gain. Keep 1x default.
    let schema = benchmark_schema();
    let steps_no_window = vec![PlanStep {
        step: 1,
        operation: PlanOperation::TableScan {
            table: "fact_orders".to_string(),
            columns_read: vec![
                "id".to_string(),
                "status".to_string(),
                "order_date".to_string(),
            ],
            filters: vec![],
        },
        notes: None,
    }];
    let steps_with_window = vec![
        PlanStep {
            step: 1,
            operation: PlanOperation::TableScan {
                table: "fact_orders".to_string(),
                columns_read: vec![
                    "id".to_string(),
                    "status".to_string(),
                    "order_date".to_string(),
                ],
                filters: vec![],
            },
            notes: None,
        },
        PlanStep {
            step: 2,
            operation: PlanOperation::Window {
                function: "ROW_NUMBER()".to_string(),
                partition_by: vec!["status".to_string()],
                order_by: vec!["order_date".to_string()],
            },
            notes: None,
        },
    ];
    let base = estimate_cost(&steps_no_window, &schema, SqlDialect::Snowflake);
    let windowed = estimate_cost(&steps_with_window, &schema, SqlDialect::Snowflake);
    assert_eq!(
        windowed.bytes_scanned, base.bytes_scanned,
        "Window function should NOT double bytes scanned (1x default)"
    );
}

#[test]
fn snowflake_validation_unfiltered_scan_matches_table_size() {
    // Q20: SELECT * FROM fact_orders
    // Snowflake: 8.8 MB. altimate-core should estimate close to row_count * avg_row_bytes.
    let schema = benchmark_schema();
    let steps = vec![PlanStep {
        step: 1,
        operation: PlanOperation::TableScan {
            table: "fact_orders".to_string(),
            columns_read: vec![
                "id".to_string(),
                "user_id".to_string(),
                "product_id".to_string(),
                "amount".to_string(),
                "order_date".to_string(),
                "status".to_string(),
                "region".to_string(),
                "channel".to_string(),
            ],
            filters: vec![],
        },
        notes: None,
    }];
    let result = estimate_cost(&steps, &schema, SqlDialect::Snowflake);
    // fact_orders: 1B rows * 512 bytes/row = ~512 GB
    // With 8 columns all read = full table
    assert!(
        result.bytes_scanned > 400_000_000_000, // > 400 GB
        "Full scan should estimate at table scale: got {} bytes",
        result.bytes_scanned
    );
}

#[test]
fn snowflake_validation_partition_filter_reduces_bytes() {
    // Filtered scan on partition column should reduce bytes vs unfiltered.
    let schema = benchmark_schema();
    let steps_unfiltered = vec![PlanStep {
        step: 1,
        operation: PlanOperation::TableScan {
            table: "fact_orders".to_string(),
            columns_read: vec!["id".to_string(), "order_date".to_string()],
            filters: vec![],
        },
        notes: None,
    }];
    let steps_filtered = vec![PlanStep {
        step: 1,
        operation: PlanOperation::TableScan {
            table: "fact_orders".to_string(),
            columns_read: vec!["id".to_string(), "order_date".to_string()],
            filters: vec!["order_date = '2024-01-01'".to_string()],
        },
        notes: None,
    }];
    let unfiltered = estimate_cost(&steps_unfiltered, &schema, SqlDialect::Snowflake);
    let filtered = estimate_cost(&steps_filtered, &schema, SqlDialect::Snowflake);
    assert!(
        filtered.bytes_scanned < unfiltered.bytes_scanned / 10,
        "Partition filter should reduce scan by >10x: unfiltered={}, filtered={}",
        unfiltered.bytes_scanned,
        filtered.bytes_scanned
    );
}

#[test]
fn snowflake_validation_missing_partition_filter_warning() {
    // Scanning a partitioned table without filtering on partition column
    // should emit a warning.
    let schema = benchmark_schema();
    let steps = vec![PlanStep {
        step: 1,
        operation: PlanOperation::TableScan {
            table: "fact_orders".to_string(),
            columns_read: vec!["id".to_string()],
            filters: vec![],
        },
        notes: None,
    }];
    let result = estimate_cost(&steps, &schema, SqlDialect::Snowflake);
    assert!(
        result
            .warnings
            .iter()
            .any(|w| w.contains("no partition filter")),
        "Should warn about missing partition filter"
    );
}

#[test]
fn snowflake_validation_mcv_selectivity_accuracy() {
    // Q4: WHERE status = 'completed' → MCV frequency ~0.60
    // Snowflake scans 8.8 MB for small table; our selectivity should reflect MCV.
    let schema = benchmark_schema();
    let steps = vec![PlanStep {
        step: 1,
        operation: PlanOperation::TableScan {
            table: "fact_orders".to_string(),
            columns_read: vec!["id".to_string(), "status".to_string()],
            filters: vec!["status = 'completed'".to_string()],
        },
        notes: None,
    }];
    let result = estimate_cost(&steps, &schema, SqlDialect::Snowflake);
    let sel = result.table_breakdown[0].selectivity;
    // MCV: completed=0.60, null_fraction=0.01 → adjusted=0.594
    assert!(
        (0.55..=0.65).contains(&sel),
        "MCV selectivity for 'completed' should be ~0.594, got {sel}"
    );
}

// ===========================================================================
// Cost Recommendation Tests
// ===========================================================================

#[test]
fn recommendations_partition_filter_missing() {
    let schema = benchmark_schema();
    let steps = vec![PlanStep {
        step: 1,
        operation: PlanOperation::TableScan {
            table: "fact_orders".to_string(),
            columns_read: vec!["id".to_string()],
            filters: vec![],
        },
        notes: None,
    }];
    let result = estimate_cost(&steps, &schema, SqlDialect::Snowflake);
    assert!(
        result
            .recommendations
            .iter()
            .any(|r| r.action.contains("partition filter")),
        "Should recommend adding partition filter"
    );
    let rec = result
        .recommendations
        .iter()
        .find(|r| r.action.contains("partition"))
        .unwrap();
    assert!(
        rec.estimated_savings_pct > 0.9,
        "Partition filter savings should be >90%"
    );
}

#[test]
fn recommendations_select_star() {
    let schema = benchmark_schema();
    let all_cols = vec![
        "id",
        "user_id",
        "product_id",
        "amount",
        "order_date",
        "status",
        "region",
        "channel",
    ];
    let steps = vec![PlanStep {
        step: 1,
        operation: PlanOperation::TableScan {
            table: "fact_orders".to_string(),
            columns_read: all_cols.iter().map(|c| c.to_string()).collect(),
            filters: vec!["order_date = '2024-01-01'".to_string()],
        },
        notes: None,
    }];
    let result = estimate_cost(&steps, &schema, SqlDialect::Snowflake);
    assert!(
        result
            .recommendations
            .iter()
            .any(|r| r.action.contains("specific columns")),
        "Should recommend selecting specific columns instead of all"
    );
}

#[test]
fn recommendations_no_recs_for_optimized_query() {
    // A well-optimized query: partition filter + column pruning
    let schema = benchmark_schema();
    let steps = vec![PlanStep {
        step: 1,
        operation: PlanOperation::TableScan {
            table: "fact_orders".to_string(),
            columns_read: vec!["id".to_string(), "status".to_string()],
            filters: vec!["order_date = '2024-01-01'".to_string()],
        },
        notes: None,
    }];
    let result = estimate_cost(&steps, &schema, SqlDialect::Snowflake);
    // With partition filter and column pruning, no high-priority recs
    assert!(
        !result
            .recommendations
            .iter()
            .any(|r| r.action.contains("partition filter")),
        "Should not recommend partition filter when one exists"
    );
}

#[test]
fn recommendations_cross_join_highest_priority() {
    let schema = benchmark_schema();
    let steps = vec![
        PlanStep {
            step: 1,
            operation: PlanOperation::TableScan {
                table: "dim_products".to_string(),
                columns_read: vec!["id".to_string()],
                filters: vec![],
            },
            notes: None,
        },
        PlanStep {
            step: 2,
            operation: PlanOperation::TableScan {
                table: "dim_customers".to_string(),
                columns_read: vec!["id".to_string()],
                filters: vec![],
            },
            notes: None,
        },
        PlanStep {
            step: 3,
            operation: PlanOperation::Join {
                join_type: JoinKind::Cross,
                left: "dim_products".to_string(),
                right: "dim_customers".to_string(),
                condition: String::new(),
            },
            notes: None,
        },
    ];
    let result = estimate_cost(&steps, &schema, SqlDialect::Snowflake);
    assert!(
        result
            .recommendations
            .iter()
            .any(|r| r.action.contains("CROSS JOIN")),
        "Should recommend eliminating CROSS JOIN"
    );
}

#[test]
fn recommendations_sorted_by_priority_then_savings() {
    let schema = benchmark_schema();
    let all_cols: Vec<String> = vec![
        "id",
        "user_id",
        "product_id",
        "amount",
        "order_date",
        "status",
        "region",
        "channel",
    ]
    .iter()
    .map(|s| s.to_string())
    .collect();
    let steps = vec![PlanStep {
        step: 1,
        operation: PlanOperation::TableScan {
            table: "fact_orders".to_string(),
            columns_read: all_cols,
            filters: vec![],
        },
        notes: None,
    }];
    let result = estimate_cost(&steps, &schema, SqlDialect::Snowflake);
    assert!(
        result.recommendations.len() >= 2,
        "Should have at least 2 recs"
    );
    // First rec should be high priority (partition filter)
    assert_eq!(
        result.recommendations[0].action, "Add partition filter",
        "Highest priority recommendation should be partition filter"
    );
}

#[test]
fn recommendations_empty_in_serialization_when_none() {
    let schema = benchmark_schema();
    let steps = vec![PlanStep {
        step: 1,
        operation: PlanOperation::TableScan {
            table: "fact_orders".to_string(),
            columns_read: vec!["id".to_string()],
            filters: vec!["order_date = '2024-01-01'".to_string()],
        },
        notes: None,
    }];
    let result = estimate_cost(&steps, &schema, SqlDialect::Snowflake);
    let json = serde_json::to_string(&result).unwrap();
    // Empty recommendations should be omitted from JSON
    if result.recommendations.is_empty() {
        assert!(
            !json.contains("recommendations"),
            "Empty recommendations should be omitted from JSON"
        );
    }
}

// ===========================================================================
// Columnar Storage Floor Tests
//
// These tests verify that the columnar storage floor is applied correctly:
// bytes_scanned >= table_total_bytes for non-partition-filtered scans on
// columnar dialects (Snowflake, BigQuery, Databricks, etc.).
// ===========================================================================

#[test]
fn columnar_floor_point_lookup_bytes_ge_table_total() {
    // A point lookup on a non-partition column should scan at least
    // the full table's physical storage on columnar engines.
    let schema = benchmark_schema();
    let steps = vec![PlanStep {
        step: 1,
        operation: PlanOperation::TableScan {
            table: "dim_products".to_string(),
            columns_read: vec!["id".to_string()],
            filters: vec!["id = 42".to_string()],
        },
        notes: None,
    }];
    let result = estimate_cost(&steps, &schema, SqlDialect::Snowflake);
    // dim_products: 1M rows * 128 bytes/row = 128MB
    let table_total = 1_000_000u64 * 128;
    assert!(
        result.bytes_scanned >= table_total,
        "Columnar floor: point lookup bytes ({}) should >= table total ({table_total})",
        result.bytes_scanned
    );
}

#[test]
fn columnar_floor_full_scan_unchanged() {
    // A full scan should already be at or above the table total.
    let schema = benchmark_schema();
    let steps = vec![PlanStep {
        step: 1,
        operation: PlanOperation::TableScan {
            table: "dim_products".to_string(),
            columns_read: vec![
                "id".to_string(),
                "name".to_string(),
                "category".to_string(),
                "price".to_string(),
            ],
            filters: vec![],
        },
        notes: None,
    }];
    let result = estimate_cost(&steps, &schema, SqlDialect::Snowflake);
    let table_total = 1_000_000u64 * 128;
    assert!(
        result.bytes_scanned >= table_total,
        "Full scan bytes ({}) should >= table total ({table_total})",
        result.bytes_scanned
    );
}

#[test]
fn columnar_floor_join_sums_table_floors() {
    // Join of two non-partitioned tables: each gets its own floor.
    let schema = benchmark_schema();
    let steps = vec![
        PlanStep {
            step: 1,
            operation: PlanOperation::TableScan {
                table: "dim_products".to_string(),
                columns_read: vec!["id".to_string()],
                filters: vec!["id = 1".to_string()],
            },
            notes: None,
        },
        PlanStep {
            step: 2,
            operation: PlanOperation::TableScan {
                table: "dim_customers".to_string(),
                columns_read: vec!["id".to_string()],
                filters: vec!["id = 1".to_string()],
            },
            notes: None,
        },
        PlanStep {
            step: 3,
            operation: PlanOperation::Join {
                join_type: JoinKind::Inner,
                left: "dim_products".to_string(),
                right: "dim_customers".to_string(),
                condition: "dim_products.id = dim_customers.id".to_string(),
            },
            notes: None,
        },
    ];
    let result = estimate_cost(&steps, &schema, SqlDialect::Snowflake);
    // dim_products: small table (128MB < 128 MiB threshold) → full table floor
    let products_total = 1_000_000u64 * 128;
    // dim_customers: large table (12.8GB), reading ["id"] → column-pruned floor
    // id(INT=4) out of total col bytes (id:4 + email:50 + name:50 + country:50 + segment:50 = 204)
    let customers_total = 50_000_000u64 * 256;
    let customers_col_ratio = 4.0 / 204.0;
    let customers_floor = (customers_total as f64 * customers_col_ratio) as u64;
    assert!(
        result.bytes_scanned >= products_total + customers_floor,
        "Join bytes ({}) should >= sum of column-pruned floors ({})",
        result.bytes_scanned,
        products_total + customers_floor
    );
}

#[test]
fn columnar_floor_not_applied_with_partition_filter() {
    // When a partition filter is present, the floor should NOT apply
    // (partition pruning correctly reduces bytes).
    let schema = benchmark_schema();
    let steps_filtered = vec![PlanStep {
        step: 1,
        operation: PlanOperation::TableScan {
            table: "fact_orders".to_string(),
            columns_read: vec!["id".to_string(), "order_date".to_string()],
            filters: vec!["order_date = '2024-01-01'".to_string()],
        },
        notes: None,
    }];
    let steps_unfiltered = vec![PlanStep {
        step: 1,
        operation: PlanOperation::TableScan {
            table: "fact_orders".to_string(),
            columns_read: vec!["id".to_string(), "order_date".to_string()],
            filters: vec![],
        },
        notes: None,
    }];
    let filtered = estimate_cost(&steps_filtered, &schema, SqlDialect::Snowflake);
    let unfiltered = estimate_cost(&steps_unfiltered, &schema, SqlDialect::Snowflake);
    assert!(
        filtered.bytes_scanned < unfiltered.bytes_scanned,
        "Partition-filtered scan ({}) should be less than unfiltered ({})",
        filtered.bytes_scanned,
        unfiltered.bytes_scanned
    );
}

#[test]
fn columnar_floor_non_partition_filter_no_reduction() {
    // A non-partition filter on a columnar engine should NOT reduce bytes
    // below the table total for a non-partitioned table.
    let schema = benchmark_schema();
    let steps_filtered = vec![PlanStep {
        step: 1,
        operation: PlanOperation::TableScan {
            table: "dim_customers".to_string(),
            columns_read: vec!["id".to_string()],
            filters: vec!["id = 42".to_string()],
        },
        notes: None,
    }];
    let steps_unfiltered = vec![PlanStep {
        step: 1,
        operation: PlanOperation::TableScan {
            table: "dim_customers".to_string(),
            columns_read: vec!["id".to_string()],
            filters: vec![],
        },
        notes: None,
    }];
    let filtered = estimate_cost(&steps_filtered, &schema, SqlDialect::Snowflake);
    let unfiltered = estimate_cost(&steps_unfiltered, &schema, SqlDialect::Snowflake);
    // dim_customers is a large table (12.8GB > 128 MiB), reading ["id"] only.
    // Column-pruned floor: table_total * col_ratio where col_ratio = id_bytes / total_col_bytes
    // id(INT=4) / (id:4 + email:50 + name:50 + country:50 + segment:50 = 204) = 4/204
    let table_total = 50_000_000u64 * 256;
    let col_ratio = 4.0 / 204.0;
    let referenced_floor = (table_total as f64 * col_ratio) as u64;
    assert!(
        filtered.bytes_scanned >= referenced_floor,
        "Filtered columnar scan should be at column-pruned floor: {} >= {referenced_floor}",
        filtered.bytes_scanned
    );
    assert_eq!(
        filtered.bytes_scanned, unfiltered.bytes_scanned,
        "Columnar: filtered and unfiltered non-partitioned scans should be equal"
    );
}

#[test]
fn columnar_floor_not_applied_on_generic_dialect() {
    // Generic dialect (non-columnar) should NOT apply the floor.
    let schema = benchmark_schema();
    let steps = vec![PlanStep {
        step: 1,
        operation: PlanOperation::TableScan {
            table: "dim_products".to_string(),
            columns_read: vec!["id".to_string()],
            filters: vec!["id = 42".to_string()],
        },
        notes: None,
    }];
    let columnar = estimate_cost(&steps, &schema, SqlDialect::Snowflake);
    let generic = estimate_cost(&steps, &schema, SqlDialect::Generic);
    assert!(
        columnar.bytes_scanned > generic.bytes_scanned,
        "Columnar ({}) should scan more than generic ({}) due to floor",
        columnar.bytes_scanned,
        generic.bytes_scanned
    );
}

#[test]
fn columnar_floor_metadata_count_with_cross_join_zero_bytes() {
    // COUNT(*) with a cross join should still return 0 bytes when
    // detected as a metadata-only aggregation.
    let schema = benchmark_schema();
    let steps = vec![
        PlanStep {
            step: 1,
            operation: PlanOperation::TableScan {
                table: "dim_regions".to_string(),
                columns_read: vec![],
                filters: vec![],
            },
            notes: None,
        },
        PlanStep {
            step: 2,
            operation: PlanOperation::Aggregate {
                group_by: vec![],
                functions: vec!["COUNT(*)".to_string()],
            },
            notes: None,
        },
    ];
    let result = estimate_cost(&steps, &schema, SqlDialect::Snowflake);
    assert_eq!(
        result.bytes_scanned, 0,
        "COUNT(*) metadata aggregation should be 0 bytes even on columnar"
    );
}

// ----------------------------------------------------------------
// LIMIT-aware estimation tests
// ----------------------------------------------------------------

#[test]
fn limit_reduces_bytes_on_columnar() {
    let schema = benchmark_schema();
    // Full scan: SELECT * FROM fact_orders (Snowflake)
    let full_steps = vec![PlanStep {
        step: 1,
        operation: PlanOperation::TableScan {
            table: "fact_orders".to_string(),
            columns_read: vec!["id".to_string()],
            filters: vec![],
        },
        notes: None,
    }];
    let full_result = estimate_cost(&full_steps, &schema, SqlDialect::Snowflake);

    // With LIMIT 1000
    let limit_steps = vec![
        PlanStep {
            step: 1,
            operation: PlanOperation::TableScan {
                table: "fact_orders".to_string(),
                columns_read: vec!["id".to_string()],
                filters: vec![],
            },
            notes: None,
        },
        PlanStep {
            step: 2,
            operation: PlanOperation::Limit {
                limit: Some(1000),
                offset: None,
            },
            notes: None,
        },
    ];
    let limit_result = estimate_cost(&limit_steps, &schema, SqlDialect::Snowflake);

    assert!(
        limit_result.bytes_scanned < full_result.bytes_scanned,
        "LIMIT 1000 should reduce bytes: {} vs full {}",
        limit_result.bytes_scanned,
        full_result.bytes_scanned
    );
}

#[test]
fn limit_no_effect_on_generic_dialect() {
    let schema = benchmark_schema();
    // Generic dialect (non-columnar) should not apply LIMIT optimization
    let full_steps = vec![PlanStep {
        step: 1,
        operation: PlanOperation::TableScan {
            table: "fact_orders".to_string(),
            columns_read: vec!["id".to_string()],
            filters: vec![],
        },
        notes: None,
    }];
    let full_result = estimate_cost(&full_steps, &schema, SqlDialect::Generic);

    let limit_steps = vec![
        PlanStep {
            step: 1,
            operation: PlanOperation::TableScan {
                table: "fact_orders".to_string(),
                columns_read: vec!["id".to_string()],
                filters: vec![],
            },
            notes: None,
        },
        PlanStep {
            step: 2,
            operation: PlanOperation::Limit {
                limit: Some(1000),
                offset: None,
            },
            notes: None,
        },
    ];
    let limit_result = estimate_cost(&limit_steps, &schema, SqlDialect::Generic);

    assert_eq!(
        limit_result.bytes_scanned, full_result.bytes_scanned,
        "LIMIT on Generic dialect should not affect bytes"
    );
}

#[test]
fn limit_with_filter_on_columnar_uses_floor() {
    let schema = benchmark_schema();
    // SELECT * FROM fact_orders WHERE status = 'completed' LIMIT 1000 (Snowflake)
    // Snowflake validation: filtered+LIMIT queries still scan the full table
    // because zone maps can't always prune. LIMIT reduction only applies to
    // unfiltered scans on large tables (>= 16MB).
    let steps = vec![
        PlanStep {
            step: 1,
            operation: PlanOperation::TableScan {
                table: "fact_orders".to_string(),
                columns_read: vec!["id".to_string(), "status".to_string()],
                filters: vec!["status = 'completed'".to_string()],
            },
            notes: None,
        },
        PlanStep {
            step: 2,
            operation: PlanOperation::Limit {
                limit: Some(1000),
                offset: None,
            },
            notes: None,
        },
    ];
    let result = estimate_cost(&steps, &schema, SqlDialect::Snowflake);

    // With filter present, LIMIT does NOT reduce bytes on columnar engines.
    // The columnar floor applies: bytes >= table_total_bytes.
    let full_table_bytes = 10_000_000u64 * 100;
    assert!(
        result.bytes_scanned >= full_table_bytes,
        "Filtered + LIMIT should use columnar floor: {} vs full {}",
        result.bytes_scanned,
        full_table_bytes
    );
}

#[test]
fn limit_does_not_reduce_subquery_tables() {
    let schema = benchmark_schema();
    // SELECT id, (SELECT MAX(amount) FROM fact_orders o WHERE o.user_id = c.id) FROM dim_customers c LIMIT 1000
    // The LIMIT applies to dim_customers, not to fact_orders inside the subquery.
    // fact_orders has a correlated filter (o.user_id = c.id) so it's not metadata-only.
    let steps = vec![
        PlanStep {
            step: 1,
            operation: PlanOperation::TableScan {
                table: "dim_customers".to_string(),
                columns_read: vec!["id".to_string()],
                filters: vec![],
            },
            notes: None,
        },
        PlanStep {
            step: 2,
            operation: PlanOperation::Subquery {
                alias: "subq".to_string(),
                inner_steps: vec![
                    PlanStep {
                        step: 3,
                        operation: PlanOperation::TableScan {
                            table: "fact_orders".to_string(),
                            columns_read: vec!["user_id".to_string(), "amount".to_string()],
                            filters: vec!["user_id = 1".to_string()],
                        },
                        notes: None,
                    },
                    PlanStep {
                        step: 4,
                        operation: PlanOperation::Aggregate {
                            group_by: vec![],
                            functions: vec!["MAX(amount)".to_string()],
                        },
                        notes: None,
                    },
                ],
            },
            notes: None,
        },
        PlanStep {
            step: 5,
            operation: PlanOperation::Limit {
                limit: Some(1000),
                offset: None,
            },
            notes: None,
        },
    ];
    let result = estimate_cost(&steps, &schema, SqlDialect::Snowflake);

    // fact_orders is inside a subquery with a filter, so it gets full columnar floor.
    // dim_customers should be reduced by LIMIT but fact_orders should NOT.
    // Total should include fact_orders' full columnar scan.
    let fact_orders_full = 10_000_000u64 * 100; // row_count * avg_row_bytes
    assert!(
        result.bytes_scanned >= fact_orders_full,
        "Should include fact_orders full scan (subquery not LIMIT-reduced): {} vs expected >= {}",
        result.bytes_scanned,
        fact_orders_full
    );
}

// ----------------------------------------------------------------
// Set operations deduplication tests
// ----------------------------------------------------------------

#[test]
fn union_all_same_table_deduplicates_on_columnar() {
    let schema = benchmark_schema();
    // UNION ALL of same table: should count once, not twice
    let steps = vec![
        PlanStep {
            step: 1,
            operation: PlanOperation::TableScan {
                table: "fact_orders".to_string(),
                columns_read: vec!["id".to_string()],
                filters: vec![],
            },
            notes: None,
        },
        PlanStep {
            step: 2,
            operation: PlanOperation::TableScan {
                table: "fact_orders".to_string(),
                columns_read: vec!["id".to_string()],
                filters: vec![],
            },
            notes: None,
        },
        PlanStep {
            step: 3,
            operation: PlanOperation::Union {
                all: true,
                branches: 2,
            },
            notes: None,
        },
    ];

    let single_scan = vec![PlanStep {
        step: 1,
        operation: PlanOperation::TableScan {
            table: "fact_orders".to_string(),
            columns_read: vec!["id".to_string()],
            filters: vec![],
        },
        notes: None,
    }];

    let union_result = estimate_cost(&steps, &schema, SqlDialect::Snowflake);
    let single_result = estimate_cost(&single_scan, &schema, SqlDialect::Snowflake);

    assert_eq!(
        union_result.bytes_scanned, single_result.bytes_scanned,
        "UNION ALL same table on columnar should equal single scan: {} vs {}",
        union_result.bytes_scanned, single_result.bytes_scanned
    );
}

#[test]
fn union_all_different_tables_sums_on_columnar() {
    let schema = benchmark_schema();
    // UNION ALL of different tables: should sum both
    let steps = vec![
        PlanStep {
            step: 1,
            operation: PlanOperation::TableScan {
                table: "fact_orders".to_string(),
                columns_read: vec!["id".to_string()],
                filters: vec![],
            },
            notes: None,
        },
        PlanStep {
            step: 2,
            operation: PlanOperation::TableScan {
                table: "dim_customers".to_string(),
                columns_read: vec!["id".to_string()],
                filters: vec![],
            },
            notes: None,
        },
        PlanStep {
            step: 3,
            operation: PlanOperation::Union {
                all: true,
                branches: 2,
            },
            notes: None,
        },
    ];

    let orders_scan = vec![PlanStep {
        step: 1,
        operation: PlanOperation::TableScan {
            table: "fact_orders".to_string(),
            columns_read: vec!["id".to_string()],
            filters: vec![],
        },
        notes: None,
    }];
    let customers_scan = vec![PlanStep {
        step: 1,
        operation: PlanOperation::TableScan {
            table: "dim_customers".to_string(),
            columns_read: vec!["id".to_string()],
            filters: vec![],
        },
        notes: None,
    }];

    let union_result = estimate_cost(&steps, &schema, SqlDialect::Snowflake);
    let orders_result = estimate_cost(&orders_scan, &schema, SqlDialect::Snowflake);
    let customers_result = estimate_cost(&customers_scan, &schema, SqlDialect::Snowflake);

    assert_eq!(
        union_result.bytes_scanned,
        orders_result.bytes_scanned + customers_result.bytes_scanned,
        "UNION ALL different tables should sum: {} vs {} + {}",
        union_result.bytes_scanned,
        orders_result.bytes_scanned,
        customers_result.bytes_scanned
    );
}
