//! Tenant-aware execution feedback calibration system.
//!
//! Records actual query execution metrics (bytes scanned) per tenant and uses
//! them to calibrate future cost estimates via exponential smoothing. Provides
//! tenant isolation, regression detection with automatic rollback, and
//! incremental feedback ingestion.
//!
//! Inspired by Presto's History-Based Optimization (HBO).

use crate::complexity::cost_profiles::CostProfile;
use crate::complexity::types::*;
use crate::schema::types::SqlDialect;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::hash::{DefaultHasher, Hash, Hasher};
use std::path::PathBuf;

/// Tenant configuration — represents a customer's warehouse environment.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TenantConfig {
    /// Unique tenant identifier.
    pub tenant_id: String,

    /// Human-readable tenant name.
    pub name: String,

    /// SQL dialect used by this tenant's warehouse.
    pub dialect: SqlDialect,

    /// ISO 8601 timestamp of when the tenant was created.
    pub created_at: String,

    /// Exponential smoothing alpha (0.0-1.0). Higher = more weight on model estimate.
    /// Default: 0.7 (30% feedback influence).
    #[serde(default = "default_alpha")]
    pub feedback_alpha: f64,

    /// Maximum feedback entries to retain per tenant.
    #[serde(default = "default_max_entries")]
    pub max_entries: usize,

    /// Maximum accuracy regression allowed before rolling back (0.0-1.0).
    /// Default: 0.10 (10% regression threshold).
    #[serde(default = "default_validation_threshold")]
    pub validation_threshold: f64,
}

fn default_alpha() -> f64 {
    0.7
}

fn default_max_entries() -> usize {
    10_000
}

fn default_validation_threshold() -> f64 {
    0.10
}

/// Feedback store for a tenant — holds all execution feedback entries.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FeedbackStore {
    /// Tenant configuration.
    pub tenant: TenantConfig,

    /// Recorded feedback entries.
    pub entries: Vec<FeedbackEntry>,

    /// Accuracy baseline recorded when feedback was first ingested.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub baseline_accuracy: Option<FeedbackBaseline>,

    /// Current accuracy after the most recent ingestion.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub current_accuracy: Option<FeedbackBaseline>,
}

/// Accuracy metrics snapshot for validation.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FeedbackBaseline {
    /// Total number of feedback entries evaluated.
    pub total_queries: usize,

    /// Entries with actual/estimated ratio within +/-10%.
    pub within_10_pct: usize,

    /// Entries with actual/estimated ratio within +/-25%.
    pub within_25_pct: usize,

    /// Entries with actual/estimated ratio outside 2x.
    pub outliers: usize,

    /// Median of actual/estimated ratios.
    pub median_ratio: f64,

    /// ISO 8601 timestamp when this baseline was recorded.
    pub recorded_at: String,
}

/// Result of calibrating an estimate with tenant feedback.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CalibratedEstimate {
    /// Original model estimate (bytes).
    pub original_bytes_scanned: u64,

    /// Calibrated estimate after feedback blending (bytes).
    pub calibrated_bytes_scanned: u64,

    /// Original USD cost from model.
    pub original_cost_usd: f64,

    /// Calibrated USD cost after feedback blending.
    pub calibrated_cost_usd: f64,

    /// How the calibration was determined.
    pub calibration_source: CalibrationSource,

    /// Number of feedback entries that matched the query fingerprint.
    pub feedback_entries_matched: usize,
}

/// How a calibrated estimate was derived.
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum CalibrationSource {
    /// No matching feedback entries found; original estimate returned.
    NoFeedback,
    /// Exact fingerprint match found in feedback store.
    ExactMatch,
    /// Similar (structural) match found (future extension).
    SimilarMatch,
}

/// Result of an incremental feedback ingestion.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FeedbackValidation {
    /// Number of new entries added.
    pub new_entries_added: usize,

    /// Total entries in the store after ingestion.
    pub total_entries: usize,

    /// Accuracy before this ingestion.
    pub before_accuracy: Option<FeedbackBaseline>,

    /// Accuracy after this ingestion.
    pub after_accuracy: Option<FeedbackBaseline>,

    /// Whether a regression was detected.
    pub regression_detected: bool,

    /// Whether the ingestion was rolled back due to regression.
    pub rolled_back: bool,

    /// Human-readable details about the ingestion result.
    pub details: String,
}

/// Tenant feedback status summary.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TenantStatus {
    /// Tenant identifier.
    pub tenant_id: String,

    /// Tenant name.
    pub name: String,

    /// Dialect name.
    pub dialect: String,

    /// Total feedback entries.
    pub total_entries: usize,

    /// Number of unique SQL fingerprints.
    pub unique_fingerprints: usize,

    /// Baseline accuracy (if set).
    pub baseline_accuracy: Option<FeedbackBaseline>,

    /// Current accuracy (if set).
    pub current_accuracy: Option<FeedbackBaseline>,
}

// ---------------------------------------------------------------------------
// Public API
// ---------------------------------------------------------------------------

/// Create a new tenant with an empty feedback store.
pub fn create_tenant(config: TenantConfig) -> FeedbackStore {
    FeedbackStore {
        tenant: config,
        entries: Vec::new(),
        baseline_accuracy: None,
        current_accuracy: None,
    }
}

/// Normalize SQL into a deterministic fingerprint for matching.
///
/// Steps:
/// 1. Replace single-quoted string literals with `?`
/// 2. Replace standalone numeric literals with `?`
/// 3. Normalize whitespace to single spaces
/// 4. Lowercase
/// 5. Hash with `DefaultHasher` and format as hex
pub fn sql_fingerprint(sql: &str) -> String {
    let normalized = normalize_sql(sql);
    let mut hasher = DefaultHasher::new();
    normalized.hash(&mut hasher);
    format!("{:016x}", hasher.finish())
}

/// Calibrate a cost estimate using tenant feedback.
///
/// Looks up matching feedback entries by SQL fingerprint, then blends the
/// model estimate with historical actuals using exponential smoothing.
pub fn calibrate_estimate(
    estimate: &CostEstimate,
    store: &FeedbackStore,
    sql: &str,
    dialect: SqlDialect,
) -> CalibratedEstimate {
    let fingerprint = sql_fingerprint(sql);
    let alpha = store.tenant.feedback_alpha.clamp(0.0, 1.0);

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

    if matching.is_empty() {
        return CalibratedEstimate {
            original_bytes_scanned: estimate.bytes_scanned,
            calibrated_bytes_scanned: estimate.bytes_scanned,
            original_cost_usd: estimate.cost_usd,
            calibrated_cost_usd: estimate.cost_usd,
            calibration_source: CalibrationSource::NoFeedback,
            feedback_entries_matched: 0,
        };
    }

    // Use the existing apply_feedback_calibration from types.rs
    let calibrated_bytes =
        apply_feedback_calibration(estimate, &store.entries, &fingerprint, alpha);

    // Recompute USD cost using dialect profile
    let profile = CostProfile::for_dialect(dialect);
    let calibrated_cost = profile.calculate_cost(calibrated_bytes);

    CalibratedEstimate {
        original_bytes_scanned: estimate.bytes_scanned,
        calibrated_bytes_scanned: calibrated_bytes,
        original_cost_usd: estimate.cost_usd,
        calibrated_cost_usd: calibrated_cost,
        calibration_source: CalibrationSource::ExactMatch,
        feedback_entries_matched: matching.len(),
    }
}

/// Ingest a batch of feedback entries with regression validation.
///
/// If accuracy regresses beyond the tenant's configured threshold,
/// the ingestion is rolled back and the function returns an error
/// in the `FeedbackValidation` result.
pub fn ingest_feedback(
    store: &mut FeedbackStore,
    new_entries: Vec<FeedbackEntry>,
) -> FeedbackValidation {
    let new_count = new_entries.len();

    // Snapshot for potential rollback
    let snapshot = store.entries.clone();
    let snapshot_baseline = store.baseline_accuracy.clone();
    let snapshot_current = store.current_accuracy.clone();

    // Compute baseline before ingestion
    let before_accuracy = if store.entries.is_empty() {
        None
    } else {
        Some(compute_accuracy(&store.entries))
    };

    // Add new entries
    store.entries.extend(new_entries);

    // Deduplicate by fingerprint: keep latest per fingerprint
    deduplicate_entries(&mut store.entries);

    // Enforce max_entries (remove oldest first)
    let max = store.tenant.max_entries;
    if store.entries.len() > max {
        let excess = store.entries.len() - max;
        store.entries.drain(0..excess);
    }

    // Compute accuracy after ingestion
    let after_accuracy = compute_accuracy(&store.entries);

    // Check for regression
    let regression_detected = if let Some(ref before) = before_accuracy {
        if before.total_queries > 0 {
            let threshold = store.tenant.validation_threshold;
            let before_pct = before.within_10_pct as f64 / before.total_queries as f64;
            let after_pct =
                after_accuracy.within_10_pct as f64 / after_accuracy.total_queries as f64;
            (before_pct - after_pct) > threshold
        } else {
            false
        }
    } else {
        false
    };

    if regression_detected {
        // Rollback
        store.entries = snapshot;
        store.baseline_accuracy = snapshot_baseline;
        store.current_accuracy = snapshot_current;

        return FeedbackValidation {
            new_entries_added: 0,
            total_entries: store.entries.len(),
            before_accuracy,
            after_accuracy: Some(after_accuracy),
            regression_detected: true,
            rolled_back: true,
            details: format!(
                "Regression detected: accuracy dropped beyond {:.0}% threshold. \
                 Ingestion of {} entries rolled back.",
                store.tenant.validation_threshold * 100.0,
                new_count
            ),
        };
    }

    // Update accuracy snapshots
    if store.baseline_accuracy.is_none() {
        store.baseline_accuracy = Some(after_accuracy.clone());
    }
    store.current_accuracy = Some(after_accuracy.clone());

    FeedbackValidation {
        new_entries_added: new_count,
        total_entries: store.entries.len(),
        before_accuracy,
        after_accuracy: Some(after_accuracy),
        regression_detected: false,
        rolled_back: false,
        details: format!(
            "Successfully ingested {} entries. Total: {}.",
            new_count,
            store.entries.len()
        ),
    }
}

/// Get tenant status summary.
pub fn tenant_status(store: &FeedbackStore) -> TenantStatus {
    let unique_fingerprints = {
        let mut fps: Vec<&str> = store
            .entries
            .iter()
            .map(|e| e.sql_fingerprint.as_str())
            .collect();
        fps.sort();
        fps.dedup();
        fps.len()
    };

    TenantStatus {
        tenant_id: store.tenant.tenant_id.clone(),
        name: store.tenant.name.clone(),
        dialect: format!("{:?}", store.tenant.dialect),
        total_entries: store.entries.len(),
        unique_fingerprints,
        baseline_accuracy: store.baseline_accuracy.clone(),
        current_accuracy: store.current_accuracy.clone(),
    }
}

/// Compute accuracy baseline from feedback entries.
fn compute_accuracy(entries: &[FeedbackEntry]) -> FeedbackBaseline {
    if entries.is_empty() {
        return FeedbackBaseline {
            total_queries: 0,
            within_10_pct: 0,
            within_25_pct: 0,
            outliers: 0,
            median_ratio: 1.0,
            recorded_at: now_iso8601(),
        };
    }

    let mut ratios: Vec<f64> = entries
        .iter()
        .map(|e| e.calibration_ratio)
        .filter(|r| r.is_finite() && *r > 0.0)
        .collect();

    ratios.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));

    let total = ratios.len();
    let within_10 = ratios.iter().filter(|&&r| (0.9..=1.1).contains(&r)).count();
    let within_25 = ratios
        .iter()
        .filter(|&&r| (0.75..=1.25).contains(&r))
        .count();
    let outliers = ratios
        .iter()
        .filter(|&&r| !(0.5..=2.0).contains(&r))
        .count();

    let median = if total == 0 {
        1.0
    } else if total.is_multiple_of(2) {
        (ratios[total / 2 - 1] + ratios[total / 2]) / 2.0
    } else {
        ratios[total / 2]
    };

    FeedbackBaseline {
        total_queries: total,
        within_10_pct: within_10,
        within_25_pct: within_25,
        outliers,
        median_ratio: median,
        recorded_at: now_iso8601(),
    }
}

/// Load a feedback store from disk.
///
/// Reads tenant config and feedback entries from `~/.altimate/tenants/{tenant_id}/`.
pub fn load_feedback_store(tenant_id: &str) -> Result<FeedbackStore, String> {
    let dir = tenant_path(tenant_id);
    let config_path = dir.join("config.json");
    let feedback_path = dir.join("feedback.json");

    if !config_path.exists() {
        return Err(format!(
            "Tenant '{}' not found at {}",
            tenant_id,
            config_path.display()
        ));
    }

    let config_data =
        std::fs::read_to_string(&config_path).map_err(|e| format!("Failed to read config: {e}"))?;
    let tenant: TenantConfig =
        serde_json::from_str(&config_data).map_err(|e| format!("Failed to parse config: {e}"))?;

    let entries: Vec<FeedbackEntry> = if feedback_path.exists() {
        let data = std::fs::read_to_string(&feedback_path)
            .map_err(|e| format!("Failed to read feedback: {e}"))?;
        serde_json::from_str(&data).map_err(|e| format!("Failed to parse feedback: {e}"))?
    } else {
        Vec::new()
    };

    // Load baseline if it exists
    let baseline_path = dir.join("baseline.json");
    let baseline: Option<FeedbackBaseline> = if baseline_path.exists() {
        let data = std::fs::read_to_string(&baseline_path)
            .map_err(|e| format!("Failed to read baseline: {e}"))?;
        Some(serde_json::from_str(&data).map_err(|e| format!("Failed to parse baseline: {e}"))?)
    } else {
        None
    };

    // Load current accuracy if it exists
    let current_path = dir.join("current.json");
    let current: Option<FeedbackBaseline> = if current_path.exists() {
        let data = std::fs::read_to_string(&current_path)
            .map_err(|e| format!("Failed to read current accuracy: {e}"))?;
        Some(serde_json::from_str(&data).map_err(|e| format!("Failed to parse current: {e}"))?)
    } else {
        None
    };

    Ok(FeedbackStore {
        tenant,
        entries,
        baseline_accuracy: baseline,
        current_accuracy: current,
    })
}

/// Save a feedback store to disk.
///
/// Writes tenant config, feedback entries, and accuracy baselines to
/// `~/.altimate/tenants/{tenant_id}/`.
pub fn save_feedback_store(store: &FeedbackStore) -> Result<(), String> {
    let dir = tenant_path(&store.tenant.tenant_id);
    std::fs::create_dir_all(&dir).map_err(|e| format!("Failed to create tenant directory: {e}"))?;

    // Write config
    let config_json = serde_json::to_string_pretty(&store.tenant)
        .map_err(|e| format!("Failed to serialize config: {e}"))?;
    std::fs::write(dir.join("config.json"), config_json)
        .map_err(|e| format!("Failed to write config: {e}"))?;

    // Write entries
    let entries_json = serde_json::to_string_pretty(&store.entries)
        .map_err(|e| format!("Failed to serialize entries: {e}"))?;
    std::fs::write(dir.join("feedback.json"), entries_json)
        .map_err(|e| format!("Failed to write feedback: {e}"))?;

    // Write baseline if present
    if let Some(ref baseline) = store.baseline_accuracy {
        let baseline_json = serde_json::to_string_pretty(baseline)
            .map_err(|e| format!("Failed to serialize baseline: {e}"))?;
        std::fs::write(dir.join("baseline.json"), baseline_json)
            .map_err(|e| format!("Failed to write baseline: {e}"))?;
    }

    // Write current accuracy if present
    if let Some(ref current) = store.current_accuracy {
        let current_json = serde_json::to_string_pretty(current)
            .map_err(|e| format!("Failed to serialize current: {e}"))?;
        std::fs::write(dir.join("current.json"), current_json)
            .map_err(|e| format!("Failed to write current: {e}"))?;
    }

    Ok(())
}

// ---------------------------------------------------------------------------
// Internal helpers
// ---------------------------------------------------------------------------

/// Get the storage path for a tenant.
fn tenant_path(tenant_id: &str) -> PathBuf {
    let home = std::env::var("HOME")
        .or_else(|_| std::env::var("USERPROFILE"))
        .unwrap_or_else(|_| "/tmp".to_string());
    PathBuf::from(home)
        .join(".altimate")
        .join("tenants")
        .join(tenant_id)
}

/// Normalize SQL for fingerprinting.
///
/// Replaces string literals and numeric literals with `?`, normalizes
/// whitespace, and lowercases the result.
fn normalize_sql(sql: &str) -> String {
    let mut result = String::with_capacity(sql.len());
    let chars: Vec<char> = sql.chars().collect();
    let len = chars.len();
    let mut i = 0;

    while i < len {
        if chars[i] == '\'' {
            // Skip string literal, replace with ?
            result.push('?');
            i += 1;
            while i < len && chars[i] != '\'' {
                if chars[i] == '\\' {
                    i += 1; // skip escaped char
                }
                i += 1;
            }
            if i < len {
                i += 1; // skip closing quote
            }
        } else if chars[i].is_ascii_digit()
            && (i == 0 || !chars[i - 1].is_ascii_alphanumeric() && chars[i - 1] != '_')
        {
            // Replace numeric literal with ?
            result.push('?');
            while i < len && (chars[i].is_ascii_digit() || chars[i] == '.') {
                i += 1;
            }
        } else if chars[i].is_ascii_whitespace() {
            // Collapse whitespace
            result.push(' ');
            while i < len && chars[i].is_ascii_whitespace() {
                i += 1;
            }
        } else {
            result.push(chars[i].to_ascii_lowercase());
            i += 1;
        }
    }

    result.trim().to_string()
}

/// Deduplicate entries by fingerprint, keeping the latest (last) entry per fingerprint.
fn deduplicate_entries(entries: &mut Vec<FeedbackEntry>) {
    let mut latest: HashMap<String, FeedbackEntry> = HashMap::new();
    // Preserve insertion order: iterate forward, always overwrite
    for entry in entries.drain(..) {
        latest.insert(entry.sql_fingerprint.clone(), entry);
    }
    // Rebuild entries from the map, sorted by recorded_at for deterministic ordering
    let mut deduped: Vec<FeedbackEntry> = latest.into_values().collect();
    deduped.sort_by(|a, b| a.recorded_at.cmp(&b.recorded_at));
    *entries = deduped;
}

/// Get current time as ISO 8601 string.
///
/// Uses a simple UTC approximation without external dependencies.
fn now_iso8601() -> String {
    use std::time::{SystemTime, UNIX_EPOCH};
    let duration = SystemTime::now()
        .duration_since(UNIX_EPOCH)
        .unwrap_or_default();
    let secs = duration.as_secs();
    // Simple UTC timestamp format
    let days = secs / 86400;
    let time_secs = secs % 86400;
    let hours = time_secs / 3600;
    let minutes = (time_secs % 3600) / 60;
    let seconds = time_secs % 60;

    // Days since 1970-01-01
    let (year, month, day) = days_to_date(days);
    format!("{year:04}-{month:02}-{day:02}T{hours:02}:{minutes:02}:{seconds:02}Z")
}

/// Convert days since epoch to (year, month, day).
fn days_to_date(days: u64) -> (u64, u64, u64) {
    // Algorithm from http://howardhinnant.github.io/date_algorithms.html
    let z = days + 719468;
    let era = z / 146097;
    let doe = z - era * 146097;
    let yoe = (doe - doe / 1460 + doe / 36524 - doe / 146096) / 365;
    let y = yoe + era * 400;
    let doy = doe - (365 * yoe + yoe / 4 - yoe / 100);
    let mp = (5 * doy + 2) / 153;
    let d = doy - (153 * mp + 2) / 5 + 1;
    let m = if mp < 10 { mp + 3 } else { mp - 9 };
    let y = if m <= 2 { y + 1 } else { y };
    (y, m, d)
}

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

    fn test_config() -> TenantConfig {
        TenantConfig {
            tenant_id: "test-tenant".to_string(),
            name: "Test Tenant".to_string(),
            dialect: SqlDialect::Snowflake,
            created_at: "2026-01-01T00:00:00Z".to_string(),
            feedback_alpha: 0.7,
            max_entries: 100,
            validation_threshold: 0.10,
        }
    }

    fn make_entry(fingerprint: &str, actual: u64, estimated: u64, ts: &str) -> FeedbackEntry {
        FeedbackEntry {
            sql_fingerprint: fingerprint.to_string(),
            actual_bytes_scanned: actual,
            estimated_bytes_scanned: estimated,
            calibration_ratio: actual as f64 / estimated as f64,
            recorded_at: ts.to_string(),
        }
    }

    fn make_cost_estimate(bytes: u64) -> CostEstimate {
        let profile = CostProfile::for_dialect(SqlDialect::Snowflake);
        CostEstimate {
            bytes_scanned: bytes,
            cost_usd: profile.calculate_cost(bytes),
            cost_tier: CostTier::from_cost(profile.calculate_cost(bytes)),
            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,
        }
    }

    #[test]
    fn test_create_tenant_empty_store() {
        let store = create_tenant(test_config());
        assert!(store.entries.is_empty());
        assert!(store.baseline_accuracy.is_none());
        assert!(store.current_accuracy.is_none());
        assert_eq!(store.tenant.tenant_id, "test-tenant");
    }

    #[test]
    fn test_sql_fingerprint_normalizes_literals() {
        let fp1 = sql_fingerprint("SELECT * FROM users WHERE id = 123");
        let fp2 = sql_fingerprint("SELECT * FROM users WHERE id = 456");
        assert_eq!(
            fp1, fp2,
            "Different numeric literals should produce same fingerprint"
        );

        let fp3 = sql_fingerprint("SELECT * FROM users WHERE name = 'alice'");
        let fp4 = sql_fingerprint("SELECT * FROM users WHERE name = 'bob'");
        assert_eq!(
            fp3, fp4,
            "Different string literals should produce same fingerprint"
        );
    }

    #[test]
    fn test_sql_fingerprint_different_tables_differ() {
        let fp1 = sql_fingerprint("SELECT * FROM users WHERE id = 1");
        let fp2 = sql_fingerprint("SELECT * FROM orders WHERE id = 1");
        assert_ne!(
            fp1, fp2,
            "Different tables should produce different fingerprints"
        );
    }

    #[test]
    fn test_sql_fingerprint_whitespace_normalization() {
        let fp1 = sql_fingerprint("SELECT  *  FROM  users");
        let fp2 = sql_fingerprint("SELECT * FROM users");
        assert_eq!(
            fp1, fp2,
            "Different whitespace should produce same fingerprint"
        );

        let fp3 = sql_fingerprint("SELECT *\n  FROM\n  users");
        assert_eq!(fp1, fp3, "Newlines should produce same fingerprint");
    }

    #[test]
    fn test_sql_fingerprint_case_insensitive() {
        let fp1 = sql_fingerprint("SELECT * FROM Users");
        let fp2 = sql_fingerprint("select * from users");
        assert_eq!(fp1, fp2, "Case should not affect fingerprint");
    }

    #[test]
    fn test_calibrate_no_feedback_returns_original() {
        let store = create_tenant(test_config());
        let estimate = make_cost_estimate(1_000_000);

        let result = calibrate_estimate(
            &estimate,
            &store,
            "SELECT * FROM users",
            SqlDialect::Snowflake,
        );

        assert_eq!(result.original_bytes_scanned, 1_000_000);
        assert_eq!(result.calibrated_bytes_scanned, 1_000_000);
        assert_eq!(result.calibration_source, CalibrationSource::NoFeedback);
        assert_eq!(result.feedback_entries_matched, 0);
    }

    #[test]
    fn test_calibrate_with_exact_match() {
        let mut store = create_tenant(test_config());
        let sql = "SELECT * FROM users WHERE id = 1";
        let fp = sql_fingerprint(sql);

        // Add feedback: actual was 2x the estimate
        store.entries.push(make_entry(
            &fp,
            2_000_000,
            1_000_000,
            "2026-01-01T00:00:00Z",
        ));

        let estimate = make_cost_estimate(1_000_000);
        let result = calibrate_estimate(&estimate, &store, sql, SqlDialect::Snowflake);

        assert_eq!(result.calibration_source, CalibrationSource::ExactMatch);
        assert_eq!(result.feedback_entries_matched, 1);
        // With alpha=0.7, calibrated = 1M * (0.7 + 0.3 * 2.0) = 1M * 1.3 = 1.3M
        assert_eq!(result.calibrated_bytes_scanned, 1_300_000);
    }

    #[test]
    fn test_calibrate_blends_with_alpha() {
        let mut config = test_config();
        config.feedback_alpha = 0.5; // 50/50 blend
        let mut store = create_tenant(config);
        let sql = "SELECT * FROM users WHERE id = 1";
        let fp = sql_fingerprint(sql);

        // Actual was 3x the estimate
        store.entries.push(make_entry(
            &fp,
            3_000_000,
            1_000_000,
            "2026-01-01T00:00:00Z",
        ));

        let estimate = make_cost_estimate(1_000_000);
        let result = calibrate_estimate(&estimate, &store, sql, SqlDialect::Snowflake);

        // With alpha=0.5, calibrated = 1M * (0.5 + 0.5 * 3.0) = 1M * 2.0 = 2M
        assert_eq!(result.calibrated_bytes_scanned, 2_000_000);
    }

    #[test]
    fn test_ingest_single_entry() {
        let mut store = create_tenant(test_config());
        let entry = make_entry("fp1", 1_000_000, 1_000_000, "2026-01-01T00:00:00Z");

        let result = ingest_feedback(&mut store, vec![entry]);

        assert_eq!(result.new_entries_added, 1);
        assert_eq!(result.total_entries, 1);
        assert!(!result.regression_detected);
        assert!(!result.rolled_back);
    }

    #[test]
    fn test_ingest_batch_deduplicates() {
        let mut store = create_tenant(test_config());

        let entries = vec![
            make_entry("fp1", 1_000_000, 1_000_000, "2026-01-01T00:00:00Z"),
            make_entry("fp1", 1_200_000, 1_000_000, "2026-01-02T00:00:00Z"), // same fingerprint, later
        ];

        let result = ingest_feedback(&mut store, entries);

        assert_eq!(result.new_entries_added, 2);
        // After dedup, only 1 unique fingerprint
        assert_eq!(store.entries.len(), 1);
        // The latest entry should be kept (1.2M actual)
        assert_eq!(store.entries[0].actual_bytes_scanned, 1_200_000);
    }

    #[test]
    fn test_ingest_respects_max_entries() {
        let mut config = test_config();
        config.max_entries = 3;
        let mut store = create_tenant(config);

        let entries: Vec<FeedbackEntry> = (0..5)
            .map(|i| {
                make_entry(
                    &format!("fp{i}"),
                    1_000_000,
                    1_000_000,
                    &format!("2026-01-0{}T00:00:00Z", i + 1),
                )
            })
            .collect();

        let result = ingest_feedback(&mut store, entries);

        assert_eq!(result.total_entries, 3);
        // Oldest entries should be removed
        assert_eq!(store.entries.len(), 3);
    }

    #[test]
    fn test_regression_detection_rolls_back() {
        let mut store = create_tenant(test_config());

        // First ingestion: good accuracy (ratio ~1.0)
        let good_entries: Vec<FeedbackEntry> = (0..10)
            .map(|i| {
                make_entry(
                    &format!("good{i}"),
                    1_000_000,
                    1_000_000,
                    &format!("2026-01-{:02}T00:00:00Z", i + 1),
                )
            })
            .collect();
        ingest_feedback(&mut store, good_entries);
        assert_eq!(store.entries.len(), 10);

        // Second ingestion: terrible accuracy (ratio = 10.0 — way off)
        let bad_entries: Vec<FeedbackEntry> = (0..20)
            .map(|i| {
                make_entry(
                    &format!("bad{i}"),
                    10_000_000,
                    1_000_000,
                    &format!("2026-02-{:02}T00:00:00Z", i + 1),
                )
            })
            .collect();

        let result = ingest_feedback(&mut store, bad_entries);

        assert!(result.regression_detected);
        assert!(result.rolled_back);
        // Store should be unchanged
        assert_eq!(store.entries.len(), 10);
    }

    #[test]
    fn test_regression_within_threshold_accepted() {
        let mut store = create_tenant(test_config());

        // First ingestion: 100% accuracy
        let good_entries: Vec<FeedbackEntry> = (0..10)
            .map(|i| {
                make_entry(
                    &format!("good{i}"),
                    1_000_000,
                    1_000_000,
                    &format!("2026-01-{:02}T00:00:00Z", i + 1),
                )
            })
            .collect();
        ingest_feedback(&mut store, good_entries);

        // Second ingestion: slightly off (ratio 1.05 — within 10%)
        let ok_entries: Vec<FeedbackEntry> = (0..5)
            .map(|i| {
                make_entry(
                    &format!("ok{i}"),
                    1_050_000,
                    1_000_000,
                    &format!("2026-02-{:02}T00:00:00Z", i + 1),
                )
            })
            .collect();

        let result = ingest_feedback(&mut store, ok_entries);

        assert!(!result.regression_detected);
        assert!(!result.rolled_back);
        assert_eq!(store.entries.len(), 15);
    }

    #[test]
    fn test_incremental_ingestion_accumulates() {
        let mut store = create_tenant(test_config());

        let batch1 = vec![make_entry(
            "fp1",
            1_000_000,
            1_000_000,
            "2026-01-01T00:00:00Z",
        )];
        ingest_feedback(&mut store, batch1);
        assert_eq!(store.entries.len(), 1);

        let batch2 = vec![make_entry(
            "fp2",
            2_000_000,
            2_000_000,
            "2026-01-02T00:00:00Z",
        )];
        ingest_feedback(&mut store, batch2);
        assert_eq!(store.entries.len(), 2);

        let batch3 = vec![make_entry(
            "fp3",
            3_000_000,
            3_000_000,
            "2026-01-03T00:00:00Z",
        )];
        ingest_feedback(&mut store, batch3);
        assert_eq!(store.entries.len(), 3);
    }

    #[test]
    fn test_feedback_store_save_load_roundtrip() {
        // Use a unique tenant ID to avoid collisions with parallel tests
        let tenant_id = format!("roundtrip-test-{}", std::process::id());

        let mut config = test_config();
        config.tenant_id = tenant_id.clone();
        let mut store = create_tenant(config);

        store.entries.push(make_entry(
            "fp1",
            1_000_000,
            900_000,
            "2026-01-01T00:00:00Z",
        ));
        store.entries.push(make_entry(
            "fp2",
            2_000_000,
            2_100_000,
            "2026-01-02T00:00:00Z",
        ));
        store.baseline_accuracy = Some(compute_accuracy(&store.entries));
        store.current_accuracy = store.baseline_accuracy.clone();

        // Save
        let save_result = save_feedback_store(&store);
        assert!(save_result.is_ok(), "Save failed: {:?}", save_result.err());

        // Load
        let loaded = load_feedback_store(&tenant_id);
        assert!(loaded.is_ok(), "Load failed: {:?}", loaded.err());
        let loaded = loaded.unwrap();

        assert_eq!(loaded.tenant.tenant_id, tenant_id);
        assert_eq!(loaded.entries.len(), 2);
        assert!(loaded.baseline_accuracy.is_some());
        assert!(loaded.current_accuracy.is_some());
        assert_eq!(loaded.entries[0].sql_fingerprint, "fp1");
        assert_eq!(loaded.entries[1].sql_fingerprint, "fp2");

        // Cleanup
        let dir = tenant_path(&tenant_id);
        let _ = std::fs::remove_dir_all(&dir);
    }

    #[test]
    fn test_tenant_status_reports_correctly() {
        let mut store = create_tenant(test_config());

        store.entries.push(make_entry(
            "fp1",
            1_000_000,
            1_000_000,
            "2026-01-01T00:00:00Z",
        ));
        store.entries.push(make_entry(
            "fp2",
            2_000_000,
            2_000_000,
            "2026-01-02T00:00:00Z",
        ));
        store.entries.push(make_entry(
            "fp1",
            1_100_000,
            1_000_000,
            "2026-01-03T00:00:00Z",
        )); // duplicate fp

        let status = tenant_status(&store);

        assert_eq!(status.tenant_id, "test-tenant");
        assert_eq!(status.name, "Test Tenant");
        assert_eq!(status.total_entries, 3);
        assert_eq!(status.unique_fingerprints, 2);
    }

    #[test]
    fn test_normalize_sql_basic() {
        let result = normalize_sql("SELECT * FROM users WHERE id = 42");
        assert_eq!(result, "select * from users where id = ?");
    }

    #[test]
    fn test_normalize_sql_string_literals() {
        let result = normalize_sql("SELECT * FROM users WHERE name = 'alice'");
        assert_eq!(result, "select * from users where name = ?");
    }

    #[test]
    fn test_normalize_sql_multiple_literals() {
        let result = normalize_sql("SELECT * FROM t WHERE a = 1 AND b = 'x' AND c = 99.5");
        assert_eq!(result, "select * from t where a = ? and b = ? and c = ?");
    }

    #[test]
    fn test_compute_accuracy_empty() {
        let acc = compute_accuracy(&[]);
        assert_eq!(acc.total_queries, 0);
        assert_eq!(acc.within_10_pct, 0);
    }

    #[test]
    fn test_compute_accuracy_perfect() {
        let entries = vec![
            make_entry("fp1", 1_000_000, 1_000_000, "2026-01-01T00:00:00Z"),
            make_entry("fp2", 2_000_000, 2_000_000, "2026-01-02T00:00:00Z"),
        ];
        let acc = compute_accuracy(&entries);
        assert_eq!(acc.total_queries, 2);
        assert_eq!(acc.within_10_pct, 2);
        assert_eq!(acc.within_25_pct, 2);
        assert_eq!(acc.outliers, 0);
        assert!((acc.median_ratio - 1.0).abs() < f64::EPSILON);
    }

    #[test]
    fn test_deduplicate_entries_keeps_latest() {
        let mut entries = vec![
            make_entry("fp1", 1_000_000, 1_000_000, "2026-01-01T00:00:00Z"),
            make_entry("fp1", 1_500_000, 1_000_000, "2026-01-02T00:00:00Z"),
            make_entry("fp2", 2_000_000, 2_000_000, "2026-01-01T00:00:00Z"),
        ];
        deduplicate_entries(&mut entries);
        assert_eq!(entries.len(), 2);
        // fp1's latest has actual=1.5M
        let fp1 = entries.iter().find(|e| e.sql_fingerprint == "fp1").unwrap();
        assert_eq!(fp1.actual_bytes_scanned, 1_500_000);
    }
}
