//! Type definitions for complexity analysis and cost estimation.

use crate::complexity::cost_profiles::SnowflakeWarehouseSize;
use serde::{Deserialize, Serialize};

/// Estimated cost of running a query on a cloud data warehouse.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CostEstimate {
    /// Estimated bytes scanned
    pub bytes_scanned: u64,

    /// Estimated cost in USD
    pub cost_usd: f64,

    /// Cost classification tier
    pub cost_tier: CostTier,

    /// Per-table breakdown of bytes scanned
    pub table_breakdown: Vec<TableCostBreakdown>,

    /// Cost-related warnings
    pub warnings: Vec<String>,

    /// Optional disclaimer (e.g., for credit/DBU pricing models)
    #[serde(skip_serializing_if = "Option::is_none")]
    pub disclaimer: Option<String>,

    /// Cost confidence interval (optimistic / expected / pessimistic).
    /// Inspired by SafeBound/LpBound pessimistic cardinality bounds (SIGMOD 2023/2025).
    #[serde(skip_serializing_if = "Option::is_none")]
    pub cost_range: Option<CostRange>,

    /// Actionable recommendations for reducing query cost, sorted by impact.
    #[serde(skip_serializing_if = "Vec::is_empty")]
    #[serde(default)]
    pub recommendations: Vec<CostRecommendation>,

    /// Confidence in the accuracy of this estimate.
    ///
    /// Based on detectable AST patterns known to cause estimation errors.
    /// High-confidence queries achieve ~95% within ±10% in validation.
    #[serde(default = "default_confidence")]
    pub confidence: EstimationConfidence,

    /// Factors that degraded confidence from High.
    #[serde(skip_serializing_if = "Vec::is_empty")]
    #[serde(default)]
    pub confidence_factors: Vec<String>,

    /// Estimation method used (v4 only, None for v3).
    #[serde(skip_serializing_if = "Option::is_none")]
    #[serde(default)]
    pub estimation_method: Option<EstimationMethod>,

    /// Time-based cost for Snowflake (v4 only, None when warehouse_size not specified).
    #[serde(skip_serializing_if = "Option::is_none")]
    #[serde(default)]
    pub time_based_cost: Option<TimeCostEstimate>,
}

/// An actionable recommendation for reducing query cost.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CostRecommendation {
    /// Short action label (e.g., "Add partition filter")
    pub action: String,

    /// Detailed explanation with specific column/table names
    pub detail: String,

    /// Estimated reduction factor (e.g., 0.9 means ~90% reduction)
    pub estimated_savings_pct: f64,

    /// Priority: high, medium, low
    pub priority: RecommendationPriority,
}

/// Priority level for cost recommendations.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum RecommendationPriority {
    /// Critical: likely to reduce cost by >50%
    High,
    /// Moderate: likely to reduce cost by 10-50%
    Medium,
    /// Minor: may reduce cost by <10%
    Low,
}

/// Confidence level for a cost estimate.
///
/// Indicates how much trust should be placed in the estimate based on
/// detectable AST patterns known to cause estimation inaccuracy.
/// High-confidence queries achieve ~95% within ±10% in Snowflake validation.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum EstimationConfidence {
    /// No known problematic patterns detected. Expect ±10% accuracy.
    High,
    /// Some uncertain patterns detected (3+ joins, cross joins, no stats).
    Medium,
    /// Patterns known to cause large estimation errors (LIKE, EXISTS, range on non-partition col).
    Low,
}

impl EstimationConfidence {
    /// Numeric severity: High=0, Medium=1, Low=2.
    pub fn severity(self) -> u8 {
        match self {
            EstimationConfidence::High => 0,
            EstimationConfidence::Medium => 1,
            EstimationConfidence::Low => 2,
        }
    }

    /// Return the lower-confidence of two levels.
    pub fn min_confidence(self, other: Self) -> Self {
        if self.severity() >= other.severity() {
            self
        } else {
            other
        }
    }
}

impl std::fmt::Display for EstimationConfidence {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            EstimationConfidence::High => write!(f, "high"),
            EstimationConfidence::Medium => write!(f, "medium"),
            EstimationConfidence::Low => write!(f, "low"),
        }
    }
}

/// Default confidence for backward-compatible deserialization.
fn default_confidence() -> EstimationConfidence {
    EstimationConfidence::High
}

/// Confidence interval for cost estimation.
///
/// Provides optimistic, expected, and pessimistic cost bounds.
/// Inspired by SafeBound (SIGMOD 2023) and LpBound (SIGMOD 2025 Best Paper):
/// pessimistic upper bounds prevent catastrophic underestimates that lead
/// the optimizer (or CI/CD gate) to approve queries that turn out expensive.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CostRange {
    /// Optimistic estimate (best case: all optimizations apply, indexes hit)
    pub low: f64,
    /// Expected estimate (most likely scenario)
    pub expected: f64,
    /// Pessimistic upper bound (worst case: no optimization, full scans)
    pub high: f64,
}

/// A selectivity estimate with confidence bounds.
///
/// Used by the selectivity engine to return bounded estimates rather than
/// single-point guesses. The `pessimistic` value (higher selectivity = more rows)
/// should be used for cost gating decisions.
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
pub struct SelectivityEstimate {
    /// Most likely selectivity (0.0 - 1.0)
    pub expected: f64,
    /// Pessimistic selectivity (higher = more rows)
    pub pessimistic: f64,
    /// Optimistic selectivity (lower = fewer rows)
    pub optimistic: f64,
    /// Whether this estimate used column statistics (true) or heuristics (false)
    pub stats_based: bool,
}

impl CostEstimate {
    /// Get the cost tier for gating decisions.
    ///
    /// Uses the pessimistic estimate (`cost_range.high`) if available,
    /// otherwise falls back to `cost_tier`. This follows the SafeBound principle:
    /// underestimates are far worse than overestimates for gating decisions.
    pub fn gating_tier(&self) -> CostTier {
        if let Some(ref range) = self.cost_range {
            CostTier::from_cost(range.high)
        } else {
            self.cost_tier
        }
    }
}

/// Per-table cost breakdown.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TableCostBreakdown {
    /// Table name
    pub table: String,

    /// Estimated bytes scanned from this table
    pub bytes_scanned: u64,

    /// Estimated row count after filters
    pub estimated_rows: u64,

    /// Selectivity factor applied (0.0 - 1.0)
    pub selectivity: f64,

    /// Whether statistics were available for this table
    pub has_statistics: bool,
}

/// Cost classification tiers.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum CostTier {
    /// Very cheap: < $0.01
    Cheap,
    /// Moderate cost: $0.01 - $1.00
    Moderate,
    /// Expensive: $1.00 - $100.00
    Expensive,
    /// Dangerously expensive: > $100.00
    Dangerous,
    /// Unable to estimate (no statistics)
    Unknown,
}

impl std::fmt::Display for CostTier {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            CostTier::Cheap => write!(f, "cheap (<$0.01)"),
            CostTier::Moderate => write!(f, "moderate ($0.01-$1)"),
            CostTier::Expensive => write!(f, "expensive ($1-$100)"),
            CostTier::Dangerous => write!(f, "DANGEROUS (>$100)"),
            CostTier::Unknown => write!(f, "unknown"),
        }
    }
}

impl CostTier {
    /// Classify a USD cost into a tier.
    pub fn from_cost(cost_usd: f64) -> Self {
        if cost_usd.is_nan() || cost_usd.is_infinite() {
            return CostTier::Unknown;
        }
        if cost_usd < 0.0 {
            CostTier::Unknown
        } else if cost_usd < 0.01 {
            CostTier::Cheap
        } else if cost_usd < 1.0 {
            CostTier::Moderate
        } else if cost_usd < 100.0 {
            CostTier::Expensive
        } else {
            CostTier::Dangerous
        }
    }
}

/// Multi-dimensional complexity score (0-100).
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ComplexityScoreV2 {
    /// Total score (0-100)
    pub total: u32,

    /// Structural dimension (0-40): joins, subqueries, unions, CTEs
    pub structural: DimensionScore,

    /// Semantic dimension (0-30): anti-pattern findings weighted by severity
    pub semantic: DimensionScore,

    /// Cost dimension (0-30): from cost estimate or heuristic fallback
    pub cost: DimensionScore,

    /// Complexity tier classification
    pub tier: ComplexityTier,

    /// Individual complexity factors that contributed to the score
    pub factors: Vec<ComplexityFactor>,
}

/// A score for a single complexity dimension.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DimensionScore {
    /// Raw score for this dimension
    pub score: u32,

    /// Maximum possible score for this dimension
    pub max: u32,

    /// Contributing factors
    pub factors: Vec<String>,
}

/// Complexity tier classification (0-100 scale).
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ComplexityTier {
    /// Simple query (0-20)
    Simple,
    /// Moderate complexity (21-40)
    Moderate,
    /// Complex query (41-60)
    Complex,
    /// Very complex query (61-80)
    VeryComplex,
    /// Extreme complexity (81-100)
    Extreme,
}

impl std::fmt::Display for ComplexityTier {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            ComplexityTier::Simple => write!(f, "simple"),
            ComplexityTier::Moderate => write!(f, "moderate"),
            ComplexityTier::Complex => write!(f, "complex"),
            ComplexityTier::VeryComplex => write!(f, "very_complex"),
            ComplexityTier::Extreme => write!(f, "extreme"),
        }
    }
}

impl ComplexityTier {
    /// Classify a score (0-100) into a tier.
    pub fn from_score(score: u32) -> Self {
        match score {
            0..=20 => ComplexityTier::Simple,
            21..=40 => ComplexityTier::Moderate,
            41..=60 => ComplexityTier::Complex,
            61..=80 => ComplexityTier::VeryComplex,
            _ => ComplexityTier::Extreme,
        }
    }
}

/// A single factor contributing to the complexity score.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ComplexityFactor {
    /// Factor name (e.g., "joins", "subquery_nesting")
    pub name: String,

    /// Points contributed
    pub points: u32,

    /// Human-readable description
    pub description: String,
}

/// Result of a complexity gate check.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GateResult {
    /// Whether all queries passed the threshold
    pub passed: bool,

    /// Threshold that was applied
    pub threshold: u32,

    /// Per-query results
    pub results: Vec<QueryGateResult>,

    /// Summary statistics
    pub summary: GateSummary,
}

/// Per-query result in a gate check.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct QueryGateResult {
    /// File path or identifier
    pub file: String,

    /// SQL query (truncated)
    pub sql_preview: String,

    /// Complexity score
    pub score: u32,

    /// Complexity tier
    pub tier: ComplexityTier,

    /// Whether this query passed the threshold
    pub passed: bool,

    /// Cost estimate if available
    #[serde(skip_serializing_if = "Option::is_none")]
    pub cost_estimate: Option<CostEstimate>,

    /// Complexity score v2 breakdown
    pub complexity: ComplexityScoreV2,
}

/// Summary statistics for a gate check.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GateSummary {
    /// Total queries checked
    pub total: usize,

    /// Queries that passed
    pub passed: usize,

    /// Queries that failed
    pub failed: usize,

    /// Average complexity score
    pub avg_score: f64,

    /// Maximum complexity score
    pub max_score: u32,
}

/// Cross-dialect complexity comparison result.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DialectComparisonResult {
    /// The SQL query compared
    pub sql: String,

    /// Source dialect
    pub from_dialect: String,

    /// Target dialect
    pub to_dialect: String,

    /// Source dialect complexity
    pub from_complexity: ComplexityScoreV2,

    /// Target dialect complexity
    pub to_complexity: ComplexityScoreV2,

    /// Source dialect cost estimate
    #[serde(skip_serializing_if = "Option::is_none")]
    pub from_cost: Option<CostEstimate>,

    /// Target dialect cost estimate
    #[serde(skip_serializing_if = "Option::is_none")]
    pub to_cost: Option<CostEstimate>,

    /// Dialect-specific insights
    pub insights: Vec<String>,
}

/// dbt project complexity report.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DbtComplexityReport {
    /// Project name
    pub project_name: String,

    /// Per-model complexity results, sorted by score descending
    pub models: Vec<DbtModelComplexity>,

    /// Project-wide summary
    pub summary: DbtComplexitySummary,

    /// Recommendations for the most complex models
    pub recommendations: Vec<String>,
}

/// Per-model complexity in a dbt project.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DbtModelComplexity {
    /// Model name
    pub name: String,

    /// Model file path
    pub path: String,

    /// Complexity score (0-100)
    pub score: u32,

    /// Complexity tier
    pub tier: ComplexityTier,

    /// Number of downstream dependents
    pub downstream_count: usize,

    /// Lint findings count
    pub lint_findings: usize,

    /// Cost estimate if available
    #[serde(skip_serializing_if = "Option::is_none")]
    pub cost_estimate: Option<CostEstimate>,

    /// Full complexity breakdown
    pub complexity: ComplexityScoreV2,
}

/// Summary for a dbt complexity report.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DbtComplexitySummary {
    /// Total models analyzed
    pub total_models: usize,

    /// Average complexity score
    pub avg_score: f64,

    /// Count of models per tier
    pub tier_distribution: Vec<(String, usize)>,

    /// Total estimated cost (if stats available)
    #[serde(skip_serializing_if = "Option::is_none")]
    pub total_estimated_cost: Option<f64>,
}

/// An execution feedback entry for calibrating future cost estimates.
///
/// Inspired by Presto's History-Based Optimization (HBO): after a query
/// executes, the actual bytes scanned can be recorded to improve future
/// estimates for similar queries. The calibration ratio (actual / estimated)
/// is used as a correction factor.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FeedbackEntry {
    /// SQL fingerprint (normalized query with literal values replaced).
    /// Used as the lookup key for matching future queries.
    pub sql_fingerprint: String,

    /// Actual bytes scanned (from query history / INFORMATION_SCHEMA)
    pub actual_bytes_scanned: u64,

    /// Estimated bytes at time of recording
    pub estimated_bytes_scanned: u64,

    /// Calibration ratio: actual / estimated.
    /// Values > 1.0 mean the model under-estimates, < 1.0 means over-estimates.
    pub calibration_ratio: f64,

    /// ISO 8601 timestamp of when the feedback was recorded
    pub recorded_at: String,
}

/// Options for v4 cost estimation.
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct CostEstimationOptions {
    /// Snowflake warehouse size for time-based cost estimation.
    #[serde(default)]
    pub warehouse_size: Option<SnowflakeWarehouseSize>,
    /// Override credit price (default: $3.00/credit).
    #[serde(default)]
    pub credit_price: Option<f64>,
    /// SQL text for query type detection (optional).
    #[serde(default)]
    pub sql_text: Option<String>,
}

/// Which estimation tier produced the bytes_scanned result.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum EstimationMethod {
    /// Tier 1: sum of historical per-table median bytes_scanned (best accuracy).
    HistoricalMedian,
    /// Tier 2: EXPLAIN-corrected (future, placeholder).
    ExplainCorrected,
    /// Tier 3: static analysis from schema statistics.
    StaticAnalysis,
    /// Mixed: some tables used historical median, others used static analysis.
    Mixed,
}

/// Time-based cost estimate for Snowflake.
/// Reflects how Snowflake actually bills: by warehouse-time, not bytes scanned.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TimeCostEstimate {
    /// Estimated execution time in seconds (bytes / throughput).
    pub estimated_execution_secs: f64,
    /// Billable time in seconds (max of execution_time, 60s minimum billing).
    pub billable_secs: f64,
    /// Warehouse size used for estimation.
    pub warehouse_size: String,
    /// Credits consumed (billable_time/3600 * credits_per_hour * query_type_multiplier).
    pub credits: f64,
    /// Cost in USD (credits * credit_price).
    pub cost_usd: f64,
    /// Detected query type.
    pub query_type: String,
    /// Query type cost multiplier applied.
    pub query_type_multiplier: f64,
}

/// Apply execution feedback to calibrate a cost estimate.
///
/// When historical feedback is available for matching queries, blends the
/// model's estimate with the observed actual bytes using exponential smoothing.
/// The `alpha` parameter controls how much weight to give to the model (1.0)
/// vs. the historical feedback (0.0). Default is 0.7 (30% feedback influence).
///
/// Returns the calibrated bytes_scanned. Does not modify the original estimate.
pub fn apply_feedback_calibration(
    estimate: &CostEstimate,
    feedback: &[FeedbackEntry],
    sql_fingerprint: &str,
    alpha: f64,
) -> u64 {
    let alpha = alpha.clamp(0.0, 1.0);

    // Find matching feedback entries (most recent first)
    let matching: Vec<&FeedbackEntry> = feedback
        .iter()
        .filter(|f| f.sql_fingerprint == sql_fingerprint)
        .collect();

    if matching.is_empty() {
        return estimate.bytes_scanned;
    }

    // Compute average calibration ratio from matching entries
    let avg_ratio: f64 =
        matching.iter().map(|f| f.calibration_ratio).sum::<f64>() / matching.len() as f64;

    // Blend: calibrated = alpha * model_estimate + (1 - alpha) * (model_estimate * avg_ratio)
    // Simplifies to: model_estimate * (alpha + (1 - alpha) * avg_ratio)
    let blended = estimate.bytes_scanned as f64 * (alpha + (1.0 - alpha) * avg_ratio);

    // Clamp to reasonable bounds
    if blended.is_nan() || blended.is_infinite() || blended < 0.0 {
        estimate.bytes_scanned
    } else {
        blended as u64
    }
}

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

    fn base_cost_estimate() -> CostEstimate {
        CostEstimate {
            bytes_scanned: 1_000_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,
        }
    }

    // --- CostRange tests ---

    #[test]
    fn test_cost_range_serialization() {
        let range = CostRange {
            low: 0.001,
            expected: 0.005,
            high: 0.05,
        };
        let json = serde_json::to_string(&range).unwrap();
        assert!(json.contains("\"low\""));
        assert!(json.contains("\"expected\""));
        assert!(json.contains("\"high\""));

        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 test_cost_range_low_le_expected_le_high() {
        let range = CostRange {
            low: 0.001,
            expected: 0.005,
            high: 0.05,
        };
        assert!(range.low <= range.expected);
        assert!(range.expected <= range.high);
    }

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

    // --- SelectivityEstimate tests ---

    #[test]
    fn test_selectivity_estimate_construction() {
        let sel = SelectivityEstimate {
            expected: 0.1,
            pessimistic: 0.3,
            optimistic: 0.01,
            stats_based: true,
        };
        assert!((sel.expected - 0.1).abs() < f64::EPSILON);
        assert!((sel.pessimistic - 0.3).abs() < f64::EPSILON);
        assert!((sel.optimistic - 0.01).abs() < f64::EPSILON);
        assert!(sel.stats_based);
    }

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

    #[test]
    fn test_selectivity_estimate_serialization() {
        let sel = SelectivityEstimate {
            expected: 0.1,
            pessimistic: 0.3,
            optimistic: 0.01,
            stats_based: true,
        };
        let json = serde_json::to_string(&sel).unwrap();
        assert!(json.contains("\"expected\""));
        assert!(json.contains("\"pessimistic\""));
        assert!(json.contains("\"optimistic\""));
        assert!(json.contains("\"stats_based\""));

        let deserialized: SelectivityEstimate = serde_json::from_str(&json).unwrap();
        assert!((deserialized.expected - 0.1).abs() < f64::EPSILON);
        assert!(deserialized.stats_based);
    }

    // --- CostEstimate with cost_range tests ---

    #[test]
    fn test_cost_estimate_without_range_omits_field() {
        let cost = base_cost_estimate();
        let json = serde_json::to_string(&cost).unwrap();
        assert!(
            !json.contains("cost_range"),
            "cost_range=None should be omitted from JSON"
        );
    }

    #[test]
    fn test_cost_estimate_with_range_includes_field() {
        let mut cost = base_cost_estimate();
        cost.cost_range = Some(CostRange {
            low: 0.001,
            expected: 0.005,
            high: 0.05,
        });
        let json = serde_json::to_string(&cost).unwrap();
        assert!(
            json.contains("cost_range"),
            "cost_range=Some should be present in JSON"
        );
        assert!(json.contains("\"low\""));
        assert!(json.contains("\"high\""));
    }

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

    #[test]
    fn test_cost_estimate_roundtrip_without_range() {
        let cost = base_cost_estimate();
        let json = serde_json::to_string(&cost).unwrap();
        let deserialized: CostEstimate = serde_json::from_str(&json).unwrap();
        assert!(deserialized.cost_range.is_none());
    }

    // --- gating_tier tests ---

    #[test]
    fn test_gating_tier_uses_pessimistic_when_available() {
        let mut cost = base_cost_estimate();
        // Base cost_tier is Cheap ($0.005)
        // But pessimistic high is $200 -> Dangerous
        cost.cost_range = Some(CostRange {
            low: 0.001,
            expected: 0.005,
            high: 200.0,
        });
        assert_eq!(cost.gating_tier(), CostTier::Dangerous);
    }

    #[test]
    fn test_gating_tier_falls_back_to_cost_tier_when_none() {
        let cost = base_cost_estimate();
        assert!(cost.cost_range.is_none());
        assert_eq!(cost.gating_tier(), CostTier::Cheap);
    }

    #[test]
    fn test_gating_tier_expensive_range() {
        let mut cost = base_cost_estimate();
        cost.cost_range = Some(CostRange {
            low: 0.001,
            expected: 0.5,
            high: 50.0,
        });
        assert_eq!(cost.gating_tier(), CostTier::Expensive);
    }

    #[test]
    fn test_gating_tier_moderate_range() {
        let mut cost = base_cost_estimate();
        cost.cost_range = Some(CostRange {
            low: 0.001,
            expected: 0.01,
            high: 0.5,
        });
        assert_eq!(cost.gating_tier(), CostTier::Moderate);
    }

    #[test]
    fn test_gating_tier_cheap_range() {
        let mut cost = base_cost_estimate();
        cost.cost_range = Some(CostRange {
            low: 0.0001,
            expected: 0.001,
            high: 0.005,
        });
        assert_eq!(cost.gating_tier(), CostTier::Cheap);
    }

    // --- EstimationConfidence tests ---

    #[test]
    fn test_confidence_serde_roundtrip() {
        for (variant, expected_str) in [
            (EstimationConfidence::High, "\"high\""),
            (EstimationConfidence::Medium, "\"medium\""),
            (EstimationConfidence::Low, "\"low\""),
        ] {
            let json = serde_json::to_string(&variant).unwrap();
            assert_eq!(json, expected_str);
            let deserialized: EstimationConfidence = serde_json::from_str(&json).unwrap();
            assert_eq!(deserialized, variant);
        }
    }

    #[test]
    fn test_confidence_min_confidence() {
        assert_eq!(
            EstimationConfidence::High.min_confidence(EstimationConfidence::High),
            EstimationConfidence::High
        );
        assert_eq!(
            EstimationConfidence::High.min_confidence(EstimationConfidence::Medium),
            EstimationConfidence::Medium
        );
        assert_eq!(
            EstimationConfidence::High.min_confidence(EstimationConfidence::Low),
            EstimationConfidence::Low
        );
        assert_eq!(
            EstimationConfidence::Medium.min_confidence(EstimationConfidence::Low),
            EstimationConfidence::Low
        );
        assert_eq!(
            EstimationConfidence::Low.min_confidence(EstimationConfidence::High),
            EstimationConfidence::Low
        );
    }

    #[test]
    fn test_confidence_severity() {
        assert_eq!(EstimationConfidence::High.severity(), 0);
        assert_eq!(EstimationConfidence::Medium.severity(), 1);
        assert_eq!(EstimationConfidence::Low.severity(), 2);
    }

    #[test]
    fn test_confidence_display() {
        assert_eq!(EstimationConfidence::High.to_string(), "high");
        assert_eq!(EstimationConfidence::Medium.to_string(), "medium");
        assert_eq!(EstimationConfidence::Low.to_string(), "low");
    }

    #[test]
    fn test_backward_compatible_deserialization_without_confidence() {
        // JSON from an older version that doesn't include confidence fields
        let json = r#"{
            "bytes_scanned": 1000,
            "cost_usd": 0.005,
            "cost_tier": "cheap",
            "table_breakdown": [],
            "warnings": []
        }"#;
        let cost: CostEstimate = serde_json::from_str(json).unwrap();
        assert_eq!(cost.confidence, EstimationConfidence::High);
        assert!(cost.confidence_factors.is_empty());
    }

    #[test]
    fn test_confidence_fields_in_serialization() {
        let mut cost = base_cost_estimate();
        cost.confidence = EstimationConfidence::Low;
        cost.confidence_factors = vec!["LIKE filter detected".to_string()];
        let json = serde_json::to_string(&cost).unwrap();
        assert!(json.contains("\"confidence\":\"low\""));
        assert!(json.contains("confidence_factors"));
        assert!(json.contains("LIKE filter detected"));
    }

    #[test]
    fn test_empty_confidence_factors_skipped() {
        let cost = base_cost_estimate();
        let json = serde_json::to_string(&cost).unwrap();
        assert!(
            !json.contains("confidence_factors"),
            "Empty confidence_factors should be skipped"
        );
    }
}
