//! SQL completion engine.
//!
//! Provides context-aware SQL completions based on cursor position and schema.

use regex::Regex;
use strsim::jaro_winkler;

use super::types::*;
use crate::schema::types::SchemaDefinition;

/// Common SQL keywords for completion.
const SQL_KEYWORDS: &[&str] = &[
    "SELECT",
    "FROM",
    "WHERE",
    "JOIN",
    "INNER JOIN",
    "LEFT JOIN",
    "RIGHT JOIN",
    "FULL OUTER JOIN",
    "CROSS JOIN",
    "ON",
    "AND",
    "OR",
    "NOT",
    "IN",
    "EXISTS",
    "BETWEEN",
    "LIKE",
    "IS NULL",
    "IS NOT NULL",
    "ORDER BY",
    "GROUP BY",
    "HAVING",
    "LIMIT",
    "OFFSET",
    "UNION",
    "UNION ALL",
    "EXCEPT",
    "INTERSECT",
    "INSERT INTO",
    "VALUES",
    "UPDATE",
    "SET",
    "DELETE FROM",
    "CREATE TABLE",
    "ALTER TABLE",
    "DROP TABLE",
    "AS",
    "DISTINCT",
    "COUNT",
    "SUM",
    "AVG",
    "MIN",
    "MAX",
    "CASE",
    "WHEN",
    "THEN",
    "ELSE",
    "END",
    "ASC",
    "DESC",
    "NULLS FIRST",
    "NULLS LAST",
    "WITH",
    "RECURSIVE",
    "OVER",
    "PARTITION BY",
    "ROW_NUMBER",
    "RANK",
    "DENSE_RANK",
    "LAG",
    "LEAD",
    "FIRST_VALUE",
    "LAST_VALUE",
    "COALESCE",
    "CAST",
    "EXTRACT",
    "TRIM",
    "SUBSTRING",
    "CONCAT",
    "LOWER",
    "UPPER",
    "LENGTH",
    "REPLACE",
    "NOW",
    "CURRENT_TIMESTAMP",
    "DATE",
    "INTERVAL",
    "BOOLEAN",
    "TRUE",
    "FALSE",
];

/// Common SQL aggregate and scalar functions.
const SQL_FUNCTIONS: &[&str] = &[
    "COUNT",
    "SUM",
    "AVG",
    "MIN",
    "MAX",
    "COALESCE",
    "NULLIF",
    "CAST",
    "CASE",
    "TRIM",
    "LOWER",
    "UPPER",
    "LENGTH",
    "SUBSTRING",
    "REPLACE",
    "CONCAT",
    "ABS",
    "CEIL",
    "FLOOR",
    "ROUND",
    "EXTRACT",
    "DATE_TRUNC",
    "NOW",
    "CURRENT_TIMESTAMP",
    "CURRENT_DATE",
    "ROW_NUMBER",
    "RANK",
    "DENSE_RANK",
    "LAG",
    "LEAD",
    "FIRST_VALUE",
    "LAST_VALUE",
    "NTH_VALUE",
    "NTILE",
    "STRING_AGG",
    "ARRAY_AGG",
    "JSON_AGG",
    "BOOL_AND",
    "BOOL_OR",
    "STDDEV",
    "VARIANCE",
    "PERCENTILE_CONT",
    "PERCENTILE_DISC",
];

/// Minimum fuzzy match score to include a suggestion.
const FUZZY_THRESHOLD: f64 = 0.6;

/// Context detected from cursor position analysis.
#[derive(Debug, Clone, PartialEq)]
enum CompletionContext {
    /// After FROM or JOIN — suggest tables
    Table,
    /// After SELECT, WHERE, ON, GROUP BY, ORDER BY, HAVING — suggest columns
    Column,
    /// After `alias.` — suggest columns for the resolved table
    QualifiedColumn { qualifier: String },
    /// Start of statement or after certain keywords — suggest keywords
    Keyword,
    /// Inside a function call — suggest functions
    Function,
    /// After ON in a JOIN — suggest join conditions
    JoinCondition {
        left_table: Option<String>,
        right_table: Option<String>,
    },
}

/// Generate SQL completion suggestions at the given cursor position.
pub fn complete(sql: &str, cursor_offset: usize, schema: &SchemaDefinition) -> CompletionResult {
    let offset = cursor_offset.min(sql.len());
    let before_cursor = &sql[..offset];

    let context = detect_context(before_cursor, sql);
    let prefix = extract_prefix(before_cursor);

    let items = match &context {
        CompletionContext::Table => suggest_tables(schema, &prefix),
        CompletionContext::Column => suggest_columns_from_context(schema, sql, &prefix),
        CompletionContext::QualifiedColumn { qualifier } => {
            suggest_qualified_columns(schema, sql, qualifier)
        }
        CompletionContext::Keyword => suggest_keywords(&prefix),
        CompletionContext::Function => suggest_functions(&prefix),
        CompletionContext::JoinCondition {
            left_table,
            right_table,
        } => suggest_join_conditions(schema, left_table.as_deref(), right_table.as_deref()),
    };

    let context_label = match &context {
        CompletionContext::Table => "table".to_string(),
        CompletionContext::Column => "column".to_string(),
        CompletionContext::QualifiedColumn { qualifier } => {
            format!("column (qualified: {qualifier})")
        }
        CompletionContext::Keyword => "keyword".to_string(),
        CompletionContext::Function => "function".to_string(),
        CompletionContext::JoinCondition { .. } => "join_condition".to_string(),
    };

    CompletionResult {
        cursor_offset: offset,
        context: context_label,
        items,
    }
}

/// Detect what kind of completion context we're in based on the text before the cursor.
fn detect_context(before_cursor: &str, _full_sql: &str) -> CompletionContext {
    let trimmed = before_cursor.trim_end();
    let upper = trimmed.to_uppercase();

    // Check for qualified column reference: `alias.` or `table.`
    if before_cursor.ends_with('.') {
        let qualifier = extract_word_before_dot(before_cursor);
        if !qualifier.is_empty() {
            return CompletionContext::QualifiedColumn {
                qualifier: qualifier.to_string(),
            };
        }
    }

    // Check what's immediately before (last significant token/keyword)
    let last_tokens = last_significant_tokens(&upper, 3);

    // After ON in a JOIN context — suggest join conditions
    if last_tokens.last().map(|s| s.as_str()) == Some("ON") && contains_join_keyword(&upper) {
        let (left, right) = extract_join_tables(trimmed);
        return CompletionContext::JoinCondition {
            left_table: left,
            right_table: right,
        };
    }

    // After FROM or JOIN keywords — suggest tables
    // Check both last token and second-to-last (user might be typing a partial table name)
    let table_keywords = ["FROM", "JOIN", "INTO", "UPDATE", "TABLE"];
    if last_tokens
        .last()
        .map(|s| table_keywords.contains(&s.as_str()))
        .unwrap_or(false)
    {
        return CompletionContext::Table;
    }

    // If the second-to-last token is a table keyword, user is typing a partial table name
    if last_tokens.len() >= 2
        && table_keywords.contains(&last_tokens[last_tokens.len() - 2].as_str())
    {
        return CompletionContext::Table;
    }

    // After comma following FROM/JOIN — still in table context
    if last_tokens.last().map(|s| s.as_str()) == Some(",") && is_in_from_clause(&upper) {
        return CompletionContext::Table;
    }

    // Currently typing in a FROM clause (no other clause keyword after FROM)
    if is_in_from_clause(&upper) {
        return CompletionContext::Table;
    }

    // Inside function call parentheses
    if is_inside_function_call(before_cursor) {
        return CompletionContext::Function;
    }

    // After SELECT, WHERE, HAVING, SET, GROUP BY, ORDER BY — suggest columns
    if is_in_column_context(&upper) {
        return CompletionContext::Column;
    }

    // Default: keyword context
    CompletionContext::Keyword
}

/// Extract the word being typed (prefix for fuzzy matching).
fn extract_prefix(before_cursor: &str) -> String {
    let bytes = before_cursor.as_bytes();
    let mut end = bytes.len();
    while end > 0 {
        let ch = bytes[end - 1] as char;
        if ch.is_alphanumeric() || ch == '_' {
            end -= 1;
        } else {
            break;
        }
    }
    before_cursor[end..].to_string()
}

/// Extract the word before a trailing dot.
fn extract_word_before_dot(before_cursor: &str) -> &str {
    let without_dot = &before_cursor[..before_cursor.len() - 1];
    let trimmed = without_dot.trim_end();
    let start = trimmed
        .rfind(|c: char| !c.is_alphanumeric() && c != '_')
        .map(|i| i + 1)
        .unwrap_or(0);
    &trimmed[start..]
}

/// Get the last N significant tokens (words/punctuation) from upper-cased SQL.
fn last_significant_tokens(upper: &str, n: usize) -> Vec<String> {
    let re = Regex::new(r"[A-Z_]+|[,;.()\*]").expect("valid regex");
    let tokens: Vec<String> = re
        .find_iter(upper)
        .map(|m| m.as_str().to_string())
        .collect();
    let start = tokens.len().saturating_sub(n);
    tokens[start..].to_vec()
}

/// Check if the text contains a JOIN keyword.
fn contains_join_keyword(upper: &str) -> bool {
    upper.contains("JOIN")
}

/// Extract left and right table names from a JOIN clause.
fn extract_join_tables(sql: &str) -> (Option<String>, Option<String>) {
    let upper = sql.to_uppercase();

    // Find the last JOIN keyword position
    let join_pos = upper.rfind("JOIN");
    if join_pos.is_none() {
        return (None, None);
    }
    let join_pos = join_pos.expect("checked above");

    // Right table: word after JOIN
    let after_join = sql[join_pos..].trim_start();
    let after_keyword = after_join
        .strip_prefix("JOIN")
        .or_else(|| after_join.strip_prefix("join"));
    let right_table = after_keyword
        .map(|s| s.trim_start())
        .and_then(|s| s.split_whitespace().next())
        .map(|s| s.to_lowercase());

    // Left table: look in FROM clause
    let from_pos = upper.find("FROM");
    let left_table = from_pos.and_then(|pos| {
        let after_from = sql[pos + 4..].trim_start();
        after_from
            .split_whitespace()
            .next()
            .map(|s| s.to_lowercase())
    });

    (left_table, right_table)
}

/// Check if we're inside a FROM clause (after FROM, before WHERE/GROUP/ORDER/HAVING/LIMIT).
fn is_in_from_clause(upper: &str) -> bool {
    if let Some(from_pos) = upper.rfind("FROM") {
        let after_from = &upper[from_pos..];
        // If there's no WHERE/GROUP/ORDER/HAVING/LIMIT after FROM, we're still in FROM
        !after_from.contains("WHERE")
            && !after_from.contains("GROUP")
            && !after_from.contains("ORDER")
            && !after_from.contains("HAVING")
            && !after_from.contains("LIMIT")
            && !after_from.contains("JOIN")
    } else {
        false
    }
}

/// Check if we're inside a function call (unmatched open parenthesis after a word).
fn is_inside_function_call(before_cursor: &str) -> bool {
    let mut paren_depth = 0i32;
    for ch in before_cursor.chars().rev() {
        match ch {
            ')' => paren_depth += 1,
            '(' => {
                paren_depth -= 1;
                if paren_depth < 0 {
                    // We found an unmatched open paren — check if preceded by a function name
                    let pos = before_cursor.len()
                        - before_cursor
                            .chars()
                            .rev()
                            .position(|c| c == '(')
                            .unwrap_or(0);
                    if pos > 0 {
                        let before_paren = before_cursor[..pos - 1].trim_end();
                        let last_word = before_paren.split_whitespace().next_back().unwrap_or("");
                        let upper = last_word.to_uppercase();
                        return SQL_FUNCTIONS.iter().any(|f| *f == upper)
                            || upper.chars().all(|c| c.is_alphanumeric() || c == '_');
                    }
                    return false;
                }
            }
            _ => {}
        }
    }
    false
}

/// Check if cursor is in a column context.
fn is_in_column_context(upper: &str) -> bool {
    // Find the last major keyword
    let keywords_with_columns = ["SELECT", "WHERE", "HAVING", "SET", "ON"];
    let clause_keywords = [
        "SELECT", "FROM", "WHERE", "GROUP BY", "ORDER BY", "HAVING", "LIMIT", "OFFSET", "JOIN",
        "ON", "SET", "VALUES",
    ];

    // Find position of last clause keyword
    let mut last_col_kw_pos = None;
    let mut last_col_kw = "";
    for kw in &keywords_with_columns {
        if let Some(pos) = upper.rfind(kw)
            && (last_col_kw_pos.is_none() || pos > last_col_kw_pos.unwrap_or(0))
        {
            last_col_kw_pos = Some(pos);
            last_col_kw = kw;
        }
    }

    // Also check GROUP BY and ORDER BY
    for kw in &["GROUP BY", "ORDER BY"] {
        if let Some(pos) = upper.rfind(kw)
            && (last_col_kw_pos.is_none() || pos > last_col_kw_pos.unwrap_or(0))
        {
            last_col_kw_pos = Some(pos);
            last_col_kw = kw;
        }
    }

    if last_col_kw_pos.is_none() {
        return false;
    }
    let kw_pos = last_col_kw_pos.expect("checked above");

    // Make sure no other clause keyword appears after this one
    let after = &upper[kw_pos + last_col_kw.len()..];
    for ck in &clause_keywords {
        if *ck != last_col_kw && after.contains(ck) {
            return false;
        }
    }

    true
}

/// Extract table names and their aliases from the FROM clause of the SQL before cursor.
fn extract_from_tables(before_cursor: &str) -> Vec<(String, Option<String>)> {
    let upper = before_cursor.to_uppercase();
    let from_pos = match upper.find("FROM") {
        Some(pos) => pos,
        None => return vec![],
    };

    let after_from = &before_cursor[from_pos + 4..];

    // Find where FROM clause ends
    let end_keywords = [
        "WHERE",
        "GROUP",
        "ORDER",
        "HAVING",
        "LIMIT",
        "UNION",
        "EXCEPT",
        "INTERSECT",
    ];
    let after_upper = after_from.to_uppercase();
    let end_pos = end_keywords
        .iter()
        .filter_map(|kw| after_upper.find(kw))
        .min()
        .unwrap_or(after_from.len());

    let from_clause = &after_from[..end_pos];

    // Split on JOIN keywords to handle joins
    let re = Regex::new(r"(?i)\b(?:INNER\s+JOIN|LEFT\s+(?:OUTER\s+)?JOIN|RIGHT\s+(?:OUTER\s+)?JOIN|FULL\s+(?:OUTER\s+)?JOIN|CROSS\s+JOIN|JOIN)\b")
        .expect("valid regex");

    let mut tables = Vec::new();
    let mut parts: Vec<&str> = vec![];

    let mut last_end = 0;
    for m in re.find_iter(from_clause) {
        parts.push(&from_clause[last_end..m.start()]);
        last_end = m.end();
    }
    parts.push(&from_clause[last_end..]);

    for part in parts {
        // Each part might have ON condition — strip it
        let part_upper = part.to_uppercase();
        let on_pos = part_upper.find(" ON ");
        let table_part = if let Some(pos) = on_pos {
            &part[..pos]
        } else {
            part
        };

        // Split on commas for multi-table FROM
        for segment in table_part.split(',') {
            let segment = segment.trim();
            if segment.is_empty() {
                continue;
            }

            let words: Vec<&str> = segment.split_whitespace().collect();
            if words.is_empty() {
                continue;
            }

            let table_name = words[0].to_lowercase();
            let alias = if words.len() >= 3 && words[1].to_uppercase() == "AS" {
                Some(words[2].to_lowercase())
            } else if words.len() >= 2 && words[1].to_uppercase() != "ON" {
                Some(words[1].to_lowercase())
            } else {
                None
            };

            tables.push((table_name, alias));
        }
    }

    tables
}

/// Resolve a qualifier (alias or table name) to the actual table name.
fn resolve_qualifier<'a>(
    qualifier: &str,
    from_tables: &'a [(String, Option<String>)],
) -> Option<&'a str> {
    let lower = qualifier.to_lowercase();
    for (table, alias) in from_tables {
        if let Some(a) = alias
            && a == &lower
        {
            return Some(table.as_str());
        }
        if table == &lower {
            return Some(table.as_str());
        }
    }
    None
}

/// Score a candidate against a prefix.
fn score_match(candidate: &str, prefix: &str) -> f64 {
    if prefix.is_empty() {
        return 0.8; // Good default score when no prefix
    }

    let c_lower = candidate.to_lowercase();
    let p_lower = prefix.to_lowercase();

    if c_lower == p_lower {
        return 1.0; // Exact match
    }
    if c_lower.starts_with(&p_lower) {
        return 0.95; // Prefix match
    }

    jaro_winkler(&c_lower, &p_lower)
}

/// Suggest table names from the schema.
fn suggest_tables(schema: &SchemaDefinition, prefix: &str) -> Vec<CompletionItem> {
    let mut items: Vec<CompletionItem> = schema
        .tables
        .iter()
        .filter_map(|(name, def)| {
            let score = score_match(name, prefix);
            if score < FUZZY_THRESHOLD {
                return None;
            }

            let col_count = def.columns.len();
            let detail = format!("Table ({col_count} columns)");
            let documentation = def.description.clone();

            Some(CompletionItem {
                label: name.clone(),
                kind: CompletionKind::Table,
                detail,
                documentation,
                score,
            })
        })
        .collect();

    items.sort_by(|a, b| {
        b.score
            .partial_cmp(&a.score)
            .unwrap_or(std::cmp::Ordering::Equal)
    });
    items.truncate(20);
    items
}

/// Suggest columns from tables referenced in the FROM clause.
fn suggest_columns_from_context(
    schema: &SchemaDefinition,
    before_cursor: &str,
    prefix: &str,
) -> Vec<CompletionItem> {
    let from_tables = extract_from_tables(before_cursor);

    let mut items: Vec<CompletionItem> = Vec::new();

    if from_tables.is_empty() {
        // No FROM clause yet — suggest all columns from all tables
        for (table_name, table_def) in &schema.tables {
            for col in &table_def.columns {
                let score = score_match(&col.name, prefix);
                if score < FUZZY_THRESHOLD {
                    continue;
                }
                items.push(CompletionItem {
                    label: col.name.clone(),
                    kind: CompletionKind::Column,
                    detail: format!("{} ({})", col.data_type, table_name),
                    documentation: col.description.clone(),
                    score,
                });
            }
        }
    } else {
        for (table_name, _alias) in &from_tables {
            if let Some(table_def) = schema.tables.get(table_name) {
                for col in &table_def.columns {
                    let score = score_match(&col.name, prefix);
                    if score < FUZZY_THRESHOLD {
                        continue;
                    }
                    items.push(CompletionItem {
                        label: col.name.clone(),
                        kind: CompletionKind::Column,
                        detail: format!("{} ({})", col.data_type, table_name),
                        documentation: col.description.clone(),
                        score,
                    });
                }
            }
        }
    }

    // Deduplicate by label (keep highest score)
    items.sort_by(|a, b| {
        b.score
            .partial_cmp(&a.score)
            .unwrap_or(std::cmp::Ordering::Equal)
    });
    let mut seen = std::collections::HashSet::new();
    items.retain(|item| seen.insert(item.label.clone()));
    items.truncate(30);
    items
}

/// Suggest columns for a qualified reference (e.g., `u.` -> columns of users table).
fn suggest_qualified_columns(
    schema: &SchemaDefinition,
    before_cursor: &str,
    qualifier: &str,
) -> Vec<CompletionItem> {
    let from_tables = extract_from_tables(before_cursor);
    let resolved_table = resolve_qualifier(qualifier, &from_tables);

    let table_name = match resolved_table {
        Some(name) => name.to_string(),
        None => qualifier.to_lowercase(),
    };

    let table_def = match schema.tables.get(&table_name) {
        Some(def) => def,
        None => return vec![],
    };

    let mut items: Vec<CompletionItem> = table_def
        .columns
        .iter()
        .map(|col| CompletionItem {
            label: col.name.clone(),
            kind: CompletionKind::Column,
            detail: format!("{} ({})", col.data_type, table_name),
            documentation: col.description.clone(),
            score: 0.9,
        })
        .collect();

    items.sort_by(|a, b| a.label.cmp(&b.label));
    items
}

/// Suggest SQL keywords.
fn suggest_keywords(prefix: &str) -> Vec<CompletionItem> {
    let mut items: Vec<CompletionItem> = SQL_KEYWORDS
        .iter()
        .filter_map(|kw| {
            let score = score_match(kw, prefix);
            if score < FUZZY_THRESHOLD {
                return None;
            }
            Some(CompletionItem {
                label: kw.to_string(),
                kind: CompletionKind::Keyword,
                detail: "SQL keyword".to_string(),
                documentation: None,
                score,
            })
        })
        .collect();

    items.sort_by(|a, b| {
        b.score
            .partial_cmp(&a.score)
            .unwrap_or(std::cmp::Ordering::Equal)
    });
    items.truncate(15);
    items
}

/// Suggest SQL functions.
fn suggest_functions(prefix: &str) -> Vec<CompletionItem> {
    let mut items: Vec<CompletionItem> = SQL_FUNCTIONS
        .iter()
        .filter_map(|func| {
            let score = score_match(func, prefix);
            if score < FUZZY_THRESHOLD {
                return None;
            }
            Some(CompletionItem {
                label: func.to_string(),
                kind: CompletionKind::Function,
                detail: "SQL function".to_string(),
                documentation: None,
                score,
            })
        })
        .collect();

    items.sort_by(|a, b| {
        b.score
            .partial_cmp(&a.score)
            .unwrap_or(std::cmp::Ordering::Equal)
    });
    items.truncate(15);
    items
}

/// Suggest join conditions based on foreign keys.
fn suggest_join_conditions(
    schema: &SchemaDefinition,
    left_table: Option<&str>,
    right_table: Option<&str>,
) -> Vec<CompletionItem> {
    let mut items = Vec::new();

    // Try to find foreign key relationships between the tables
    if let (Some(left), Some(right)) = (left_table, right_table) {
        // Check FK from right to left
        if let Some(right_def) = schema.tables.get(right) {
            for fk in &right_def.foreign_keys {
                if fk.references.table == left {
                    for (fk_col, ref_col) in fk.columns.iter().zip(fk.references.columns.iter()) {
                        let condition = format!("{right}.{fk_col} = {left}.{ref_col}");
                        items.push(CompletionItem {
                            label: condition,
                            kind: CompletionKind::JoinCondition,
                            detail: format!("FK: {right}.{fk_col} -> {left}.{ref_col}"),
                            documentation: None,
                            score: 1.0,
                        });
                    }
                }
            }
        }

        // Check FK from left to right
        if let Some(left_def) = schema.tables.get(left) {
            for fk in &left_def.foreign_keys {
                if fk.references.table == right {
                    for (fk_col, ref_col) in fk.columns.iter().zip(fk.references.columns.iter()) {
                        let condition = format!("{left}.{fk_col} = {right}.{ref_col}");
                        items.push(CompletionItem {
                            label: condition,
                            kind: CompletionKind::JoinCondition,
                            detail: format!("FK: {left}.{fk_col} -> {right}.{ref_col}"),
                            documentation: None,
                            score: 1.0,
                        });
                    }
                }
            }
        }

        // If no FK found, suggest matching column names
        if items.is_empty()
            && let (Some(left_def), Some(right_def)) =
                (schema.tables.get(left), schema.tables.get(right))
        {
            for left_col in &left_def.columns {
                for right_col in &right_def.columns {
                    if left_col.name == right_col.name {
                        let condition =
                            format!("{left}.{} = {right}.{}", left_col.name, right_col.name);
                        items.push(CompletionItem {
                            label: condition,
                            kind: CompletionKind::JoinCondition,
                            detail: format!("Matching column: {}", left_col.name),
                            documentation: None,
                            score: 0.8,
                        });
                    }
                }
            }
        }
    }

    items.sort_by(|a, b| {
        b.score
            .partial_cmp(&a.score)
            .unwrap_or(std::cmp::Ordering::Equal)
    });
    items
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::schema::types::*;
    use std::collections::HashMap;

    fn test_schema() -> SchemaDefinition {
        let mut tables = HashMap::new();
        tables.insert(
            "users".to_string(),
            TableDef {
                description: Some("User accounts".to_string()),
                database: None,
                schema: None,
                columns: vec![
                    ColumnDef {
                        name: "id".to_string(),
                        data_type: "INT".to_string(),
                        nullable: false,
                        description: None,
                        tags: vec![],
                        synonyms: vec![],

                        statistics: None,
                    },
                    ColumnDef {
                        name: "name".to_string(),
                        data_type: "VARCHAR".to_string(),
                        nullable: false,
                        description: None,
                        tags: vec![],
                        synonyms: vec![],

                        statistics: None,
                    },
                    ColumnDef {
                        name: "email".to_string(),
                        data_type: "VARCHAR".to_string(),
                        nullable: true,
                        description: None,
                        tags: vec![],
                        synonyms: vec![],

                        statistics: None,
                    },
                ],
                primary_key: Some(vec!["id".to_string()]),
                foreign_keys: vec![],
                statistics: None,

                clustering_keys: vec![],
            },
        );
        tables.insert(
            "orders".to_string(),
            TableDef {
                description: Some("Customer orders".to_string()),
                database: None,
                schema: None,
                columns: vec![
                    ColumnDef {
                        name: "id".to_string(),
                        data_type: "INT".to_string(),
                        nullable: false,
                        description: None,
                        tags: vec![],
                        synonyms: vec![],

                        statistics: None,
                    },
                    ColumnDef {
                        name: "user_id".to_string(),
                        data_type: "INT".to_string(),
                        nullable: false,
                        description: None,
                        tags: vec![],
                        synonyms: vec![],

                        statistics: None,
                    },
                    ColumnDef {
                        name: "total".to_string(),
                        data_type: "DECIMAL(10,2)".to_string(),
                        nullable: false,
                        description: None,
                        tags: vec![],
                        synonyms: vec![],

                        statistics: None,
                    },
                ],
                primary_key: Some(vec!["id".to_string()]),
                foreign_keys: vec![ForeignKeyDef {
                    columns: vec!["user_id".to_string()],
                    references: ForeignKeyRef {
                        table: "users".to_string(),
                        columns: vec!["id".to_string()],
                    },
                }],
                statistics: None,

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

    #[test]
    fn test_table_completion_after_from() {
        let schema = test_schema();
        let result = complete("SELECT * FROM ", 14, &schema);
        assert_eq!(result.context, "table");
        assert!(!result.items.is_empty());
        let labels: Vec<&str> = result.items.iter().map(|i| i.label.as_str()).collect();
        assert!(labels.contains(&"users"));
        assert!(labels.contains(&"orders"));
    }

    #[test]
    fn test_column_completion_after_select() {
        let schema = test_schema();
        let result = complete("SELECT  FROM users", 7, &schema);
        assert_eq!(result.context, "column");
    }

    #[test]
    fn test_qualified_column_completion() {
        let schema = test_schema();
        let result = complete("SELECT u. FROM users u", 9, &schema);
        assert_eq!(result.context, "column (qualified: u)");
        let labels: Vec<&str> = result.items.iter().map(|i| i.label.as_str()).collect();
        assert!(labels.contains(&"id"));
        assert!(labels.contains(&"name"));
        assert!(labels.contains(&"email"));
    }

    #[test]
    fn test_keyword_completion() {
        let schema = test_schema();
        let result = complete("SEL", 3, &schema);
        assert_eq!(result.context, "keyword");
        let labels: Vec<&str> = result.items.iter().map(|i| i.label.as_str()).collect();
        assert!(labels.contains(&"SELECT"));
    }

    #[test]
    fn test_join_condition_completion() {
        let schema = test_schema();
        let result = complete("SELECT * FROM users JOIN orders ON ", 34, &schema);
        assert_eq!(result.context, "join_condition");
        assert!(!result.items.is_empty());
        // Should suggest FK-based condition
        let labels: Vec<&str> = result.items.iter().map(|i| i.label.as_str()).collect();
        assert!(
            labels
                .iter()
                .any(|l| l.contains("user_id") && l.contains("id"))
        );
    }

    #[test]
    fn test_empty_sql() {
        let schema = test_schema();
        let result = complete("", 0, &schema);
        assert_eq!(result.context, "keyword");
    }

    #[test]
    fn test_fuzzy_table_matching() {
        let schema = test_schema();
        let result = complete("SELECT * FROM usr", 17, &schema);
        assert_eq!(result.context, "table");
        // "usr" should fuzzy-match "users"
        let labels: Vec<&str> = result.items.iter().map(|i| i.label.as_str()).collect();
        assert!(labels.contains(&"users"));
    }
}
