//! Business glossary resolution engine.
//!
//! Provides fuzzy-matching of business terms to schema elements, batch
//! resolution, and reverse mapping from SQL elements to business terms.

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

/// Minimum similarity threshold for fuzzy matching.
const FUZZY_THRESHOLD: f64 = 0.75;

/// Resolve a single business term against the schema.
///
/// Searches the glossary (exact term, synonyms, descriptions) and then
/// falls back to fuzzy matching against column names and column-level synonyms.
/// Returns matches sorted by confidence (highest first).
///
/// # Example
///
/// ```rust,no_run
/// use altimate_core::glossary::resolve_term;
/// use altimate_core::SchemaDefinition;
///
/// let schema = SchemaDefinition::from_yaml_file("schema.yaml").unwrap();
/// let matches = resolve_term("revenue", &schema);
/// for m in &matches {
///     println!("{}: {:.2} ({})", m.term, m.confidence, m.source);
/// }
/// ```
pub fn resolve_term(term: &str, schema: &SchemaDefinition) -> Vec<GlossaryMatch> {
    let mut matches = Vec::new();
    let term_lower = term.to_lowercase();

    // Phase 1: Search the glossary if present
    if let Some(glossary) = &schema.glossary {
        for (key, gterm) in &glossary.terms {
            // Exact match on canonical term name
            if key.to_lowercase() == term_lower || gterm.term.to_lowercase() == term_lower {
                matches.push(GlossaryMatch {
                    term: gterm.term.clone(),
                    matched_column: gterm.column_ref.clone(),
                    matched_definition: Some(gterm.definition.clone()),
                    confidence: 1.0,
                    source: MatchSource::ExactTerm,
                });
                continue;
            }

            // Synonym match
            for syn in &gterm.synonyms {
                if syn.to_lowercase() == term_lower {
                    matches.push(GlossaryMatch {
                        term: gterm.term.clone(),
                        matched_column: gterm.column_ref.clone(),
                        matched_definition: Some(gterm.definition.clone()),
                        confidence: 0.95,
                        source: MatchSource::Synonym,
                    });
                    break;
                }
            }

            // Description match (contains)
            if let Some(desc) = &gterm.description
                && desc.to_lowercase().contains(&term_lower)
                && term_lower.len() >= 3
            {
                matches.push(GlossaryMatch {
                    term: gterm.term.clone(),
                    matched_column: gterm.column_ref.clone(),
                    matched_definition: Some(gterm.definition.clone()),
                    confidence: 0.7,
                    source: MatchSource::Description,
                });
            }

            // Fuzzy match on term name
            let sim = jaro_winkler(&term_lower, &gterm.term.to_lowercase());
            if (FUZZY_THRESHOLD..1.0).contains(&sim) {
                // Avoid duplicate if already matched above
                let already_matched = matches.iter().any(|m| m.term == gterm.term);
                if !already_matched {
                    matches.push(GlossaryMatch {
                        term: gterm.term.clone(),
                        matched_column: gterm.column_ref.clone(),
                        matched_definition: Some(gterm.definition.clone()),
                        confidence: sim,
                        source: MatchSource::FuzzyName,
                    });
                }
            }
        }
    }

    // Phase 2: Search column-level synonyms on ColumnDef
    for (table_name, table_def) in &schema.tables {
        for col in &table_def.columns {
            // Check column synonyms
            for syn in &col.synonyms {
                if syn.to_lowercase() == term_lower {
                    let col_ref = ColumnReference {
                        table: table_name.clone(),
                        column: col.name.clone(),
                    };
                    let already_matched = matches
                        .iter()
                        .any(|m| m.matched_column.as_ref() == Some(&col_ref));
                    if !already_matched {
                        matches.push(GlossaryMatch {
                            term: syn.clone(),
                            matched_column: Some(col_ref),
                            matched_definition: None,
                            confidence: 0.9,
                            source: MatchSource::ColumnSynonym,
                        });
                    }
                }
            }

            // Fuzzy match on column name itself
            let col_sim = jaro_winkler(&term_lower, &col.name.to_lowercase());
            if col_sim >= FUZZY_THRESHOLD {
                let col_ref = ColumnReference {
                    table: table_name.clone(),
                    column: col.name.clone(),
                };
                let already_matched = matches
                    .iter()
                    .any(|m| m.matched_column.as_ref() == Some(&col_ref));
                if !already_matched {
                    matches.push(GlossaryMatch {
                        term: col.name.clone(),
                        matched_column: Some(col_ref),
                        matched_definition: None,
                        confidence: col_sim * 0.85,
                        source: MatchSource::FuzzyName,
                    });
                }
            }
        }
    }

    // Sort by confidence descending
    matches.sort_by(|a, b| {
        b.confidence
            .partial_cmp(&a.confidence)
            .unwrap_or(std::cmp::Ordering::Equal)
    });
    matches
}

/// Resolve multiple business terms in batch.
///
/// Returns a [`TermResolutionResult`] containing all resolved matches and
/// a list of terms that could not be resolved.
pub fn resolve_all_terms(terms: &[&str], schema: &SchemaDefinition) -> TermResolutionResult {
    let mut resolved = Vec::new();
    let mut unresolved = Vec::new();

    for term in terms {
        let matches = resolve_term(term, schema);
        if matches.is_empty() {
            unresolved.push(term.to_string());
        } else {
            resolved.extend(matches);
        }
    }

    TermResolutionResult {
        resolved,
        unresolved,
    }
}

/// Given a SQL query, suggest business terms that relate to the schema elements used.
///
/// Extracts identifiers from the SQL (table and column names that appear in the schema)
/// and finds matching glossary terms for them.
pub fn suggest_terms(sql: &str, schema: &SchemaDefinition) -> Vec<TermSuggestion> {
    let mut suggestions = Vec::new();
    let sql_lower = sql.to_lowercase();

    // Extract identifiers from the SQL that match schema elements
    let mut sql_elements = Vec::new();

    // Collect table names found in the SQL
    for table_name in schema.tables.keys() {
        if sql_contains_identifier(&sql_lower, &table_name.to_lowercase()) {
            sql_elements.push(table_name.clone());
        }
    }

    // Collect column names found in the SQL
    for table_def in schema.tables.values() {
        for col in &table_def.columns {
            if sql_contains_identifier(&sql_lower, &col.name.to_lowercase()) {
                sql_elements.push(col.name.clone());
            }
        }
    }

    // Deduplicate
    sql_elements.sort();
    sql_elements.dedup();

    // For each SQL element, find glossary terms that map to it
    if let Some(glossary) = &schema.glossary {
        for element in &sql_elements {
            let element_lower = element.to_lowercase();
            for gterm in glossary.terms.values() {
                // Check if the glossary term's column_ref matches
                if let Some(ref col_ref) = gterm.column_ref
                    && (col_ref.column.to_lowercase() == element_lower
                        || col_ref.table.to_lowercase() == element_lower)
                {
                    suggestions.push(TermSuggestion {
                        sql_element: element.clone(),
                        suggested_term: gterm.term.clone(),
                        confidence: 0.95,
                    });
                    continue;
                }

                // Check if the definition mentions this element
                if gterm.definition.to_lowercase().contains(&element_lower)
                    && element_lower.len() >= 3
                {
                    suggestions.push(TermSuggestion {
                        sql_element: element.clone(),
                        suggested_term: gterm.term.clone(),
                        confidence: 0.8,
                    });
                    continue;
                }

                // Fuzzy match element to term name or synonyms
                let sim = jaro_winkler(&element_lower, &gterm.term.to_lowercase());
                if sim >= FUZZY_THRESHOLD {
                    suggestions.push(TermSuggestion {
                        sql_element: element.clone(),
                        suggested_term: gterm.term.clone(),
                        confidence: sim * 0.9,
                    });
                    continue;
                }

                for syn in &gterm.synonyms {
                    let syn_sim = jaro_winkler(&element_lower, &syn.to_lowercase());
                    if syn_sim >= FUZZY_THRESHOLD {
                        suggestions.push(TermSuggestion {
                            sql_element: element.clone(),
                            suggested_term: gterm.term.clone(),
                            confidence: syn_sim * 0.85,
                        });
                        break;
                    }
                }
            }
        }
    }

    // Also check column-level synonyms
    for (table_name, table_def) in &schema.tables {
        for col in &table_def.columns {
            if !col.synonyms.is_empty() {
                let col_name_lower = col.name.to_lowercase();
                for element in &sql_elements {
                    if element.to_lowercase() == col_name_lower {
                        for syn in &col.synonyms {
                            suggestions.push(TermSuggestion {
                                sql_element: element.clone(),
                                suggested_term: format!(
                                    "{}.{} (synonym: {})",
                                    table_name, col.name, syn
                                ),
                                confidence: 0.85,
                            });
                        }
                    }
                }
            }
        }
    }

    // Deduplicate by (sql_element, suggested_term), keeping highest confidence
    {
        let mut seen = std::collections::HashMap::<(String, String), usize>::new();
        for (i, s) in suggestions.iter().enumerate() {
            let key = (s.sql_element.clone(), s.suggested_term.clone());
            match seen.get(&key) {
                Some(&prev_idx) => {
                    if suggestions[i].confidence > suggestions[prev_idx].confidence {
                        seen.insert(key, i);
                    }
                }
                None => {
                    seen.insert(key, i);
                }
            }
        }
        let keep_indices: std::collections::HashSet<usize> = seen.values().copied().collect();
        let mut idx = 0;
        suggestions.retain(|_| {
            let keep = keep_indices.contains(&idx);
            idx += 1;
            keep
        });
    }

    // Sort by confidence descending
    suggestions.sort_by(|a, b| {
        b.confidence
            .partial_cmp(&a.confidence)
            .unwrap_or(std::cmp::Ordering::Equal)
    });
    suggestions
}

/// Check if a SQL string contains an identifier as a whole word.
/// This is a simple heuristic — not a full parser.
fn sql_contains_identifier(sql_lower: &str, ident_lower: &str) -> bool {
    // Find all occurrences and check word boundaries
    let mut start = 0;
    while let Some(pos) = sql_lower[start..].find(ident_lower) {
        let abs_pos = start + pos;
        let end_pos = abs_pos + ident_lower.len();

        let before_ok = abs_pos == 0
            || !sql_lower.as_bytes()[abs_pos - 1].is_ascii_alphanumeric()
                && sql_lower.as_bytes()[abs_pos - 1] != b'_';
        let after_ok = end_pos >= sql_lower.len()
            || !sql_lower.as_bytes()[end_pos].is_ascii_alphanumeric()
                && sql_lower.as_bytes()[end_pos] != b'_';

        if before_ok && after_ok {
            return true;
        }

        start = abs_pos + 1;
        if start >= sql_lower.len() {
            break;
        }
    }
    false
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::schema::types::{ColumnDef, SchemaDefinition, SqlDialect, TableDef};
    use std::collections::HashMap;

    fn make_schema() -> SchemaDefinition {
        let mut tables = HashMap::new();
        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: Some("Primary key".to_string()),
                        tags: vec![],
                        synonyms: vec![],

                        statistics: None,
                    },
                    ColumnDef {
                        name: "total_amount".to_string(),
                        data_type: "DECIMAL(10,2)".to_string(),
                        nullable: false,
                        description: Some("Total order amount".to_string()),
                        tags: vec!["fiscal".to_string()],
                        synonyms: vec!["revenue".to_string(), "sales".to_string()],
                        statistics: None,
                    },
                    ColumnDef {
                        name: "user_id".to_string(),
                        data_type: "INT".to_string(),
                        nullable: false,
                        description: Some("Foreign key to users".to_string()),
                        tags: vec![],
                        synonyms: vec![],

                        statistics: None,
                    },
                    ColumnDef {
                        name: "status".to_string(),
                        data_type: "VARCHAR".to_string(),
                        nullable: true,
                        description: Some("Order status".to_string()),
                        tags: vec![],
                        synonyms: vec!["order_status".to_string()],
                        statistics: None,
                    },
                ],
                primary_key: Some(vec!["id".to_string()]),
                foreign_keys: vec![],
                statistics: None,
                clustering_keys: vec![],
            },
        );
        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: "email".to_string(),
                        data_type: "VARCHAR".to_string(),
                        nullable: false,
                        description: Some("User email address".to_string()),
                        tags: vec!["pii".to_string()],
                        synonyms: vec!["email_address".to_string()],
                        statistics: None,
                    },
                    ColumnDef {
                        name: "name".to_string(),
                        data_type: "VARCHAR".to_string(),
                        nullable: true,
                        description: Some("Full name".to_string()),
                        tags: vec!["pii".to_string()],
                        synonyms: vec!["full_name".to_string(), "customer_name".to_string()],
                        statistics: None,
                    },
                ],
                primary_key: Some(vec!["id".to_string()]),
                foreign_keys: vec![],
                statistics: None,
                clustering_keys: vec![],
            },
        );
        SchemaDefinition {
            version: "1".to_string(),
            dialect: SqlDialect::Generic,
            tables,
            database: None,
            schema_name: None,
            glossary: Some(make_glossary()),
        }
    }

    fn make_glossary() -> Glossary {
        let mut terms = HashMap::new();
        terms.insert(
            "revenue".to_string(),
            GlossaryTerm {
                term: "revenue".to_string(),
                definition: "SUM(orders.total_amount)".to_string(),
                column_ref: Some(ColumnReference {
                    table: "orders".to_string(),
                    column: "total_amount".to_string(),
                }),
                synonyms: vec!["sales".to_string(), "income".to_string()],
                description: Some("Total revenue from all orders".to_string()),
            },
        );
        terms.insert(
            "active_customers".to_string(),
            GlossaryTerm {
                term: "active_customers".to_string(),
                definition: "COUNT(DISTINCT users.id) WHERE orders.status = 'completed'"
                    .to_string(),
                column_ref: None,
                synonyms: vec!["active_users".to_string()],
                description: Some(
                    "Number of customers with at least one completed order".to_string(),
                ),
            },
        );
        terms.insert(
            "order_value".to_string(),
            GlossaryTerm {
                term: "order_value".to_string(),
                definition: "orders.total_amount".to_string(),
                column_ref: Some(ColumnReference {
                    table: "orders".to_string(),
                    column: "total_amount".to_string(),
                }),
                synonyms: vec!["purchase_amount".to_string()],
                description: Some("The monetary value of a single order".to_string()),
            },
        );
        Glossary { terms }
    }

    fn make_schema_no_glossary() -> SchemaDefinition {
        let mut schema = make_schema();
        schema.glossary = None;
        schema
    }

    // === resolve_term tests ===

    #[test]
    fn test_resolve_exact_term() {
        let schema = make_schema();
        let matches = resolve_term("revenue", &schema);
        assert!(!matches.is_empty());
        let best = &matches[0];
        assert_eq!(best.term, "revenue");
        assert_eq!(best.confidence, 1.0);
        assert_eq!(best.source, MatchSource::ExactTerm);
    }

    #[test]
    fn test_resolve_exact_term_case_insensitive() {
        let schema = make_schema();
        let matches = resolve_term("Revenue", &schema);
        assert!(!matches.is_empty());
        assert_eq!(matches[0].source, MatchSource::ExactTerm);
    }

    #[test]
    fn test_resolve_synonym() {
        let schema = make_schema();
        let matches = resolve_term("sales", &schema);
        assert!(!matches.is_empty());
        let synonym_match = matches.iter().find(|m| m.source == MatchSource::Synonym);
        assert!(synonym_match.is_some());
        assert_eq!(synonym_match.unwrap().term, "revenue");
    }

    #[test]
    fn test_resolve_synonym_income() {
        let schema = make_schema();
        let matches = resolve_term("income", &schema);
        let synonym_match = matches.iter().find(|m| m.source == MatchSource::Synonym);
        assert!(synonym_match.is_some());
    }

    #[test]
    fn test_resolve_description_match() {
        let schema = make_schema();
        let matches = resolve_term("completed order", &schema);
        let desc_match = matches
            .iter()
            .find(|m| m.source == MatchSource::Description);
        assert!(desc_match.is_some());
        assert_eq!(desc_match.unwrap().term, "active_customers");
    }

    #[test]
    fn test_resolve_fuzzy_name() {
        let schema = make_schema();
        // "revenu" is close to "revenue" — matches via Description (since
        // "revenue" contains "revenu") and/or FuzzyName.
        let matches = resolve_term("revenu", &schema);
        assert!(!matches.is_empty());
        // Should find a match with decent confidence
        assert!(matches[0].confidence >= 0.7);
    }

    #[test]
    fn test_resolve_column_synonym() {
        let schema = make_schema();
        // "revenue" is a column synonym on orders.total_amount
        // It will also match the glossary term. Check that ColumnSynonym source is present.
        let matches = resolve_term("order_status", &schema);
        let col_syn = matches
            .iter()
            .find(|m| m.source == MatchSource::ColumnSynonym);
        assert!(col_syn.is_some());
        let col_ref = col_syn.unwrap().matched_column.as_ref().unwrap();
        assert_eq!(col_ref.table, "orders");
        assert_eq!(col_ref.column, "status");
    }

    #[test]
    fn test_resolve_no_match() {
        let schema = make_schema();
        let matches = resolve_term("zzz_nonexistent_term_zzz", &schema);
        assert!(matches.is_empty());
    }

    #[test]
    fn test_resolve_with_column_ref() {
        let schema = make_schema();
        let matches = resolve_term("revenue", &schema);
        let exact = matches
            .iter()
            .find(|m| m.source == MatchSource::ExactTerm)
            .unwrap();
        assert!(exact.matched_column.is_some());
        let col_ref = exact.matched_column.as_ref().unwrap();
        assert_eq!(col_ref.table, "orders");
        assert_eq!(col_ref.column, "total_amount");
    }

    #[test]
    fn test_resolve_with_definition() {
        let schema = make_schema();
        let matches = resolve_term("revenue", &schema);
        let exact = &matches[0];
        assert_eq!(
            exact.matched_definition.as_deref(),
            Some("SUM(orders.total_amount)")
        );
    }

    #[test]
    fn test_resolve_sorted_by_confidence() {
        let schema = make_schema();
        let matches = resolve_term("sales", &schema);
        for i in 1..matches.len() {
            assert!(matches[i - 1].confidence >= matches[i].confidence);
        }
    }

    #[test]
    fn test_resolve_no_glossary_falls_back_to_columns() {
        let schema = make_schema_no_glossary();
        let matches = resolve_term("email", &schema);
        assert!(!matches.is_empty());
        // Should find via fuzzy column name match
        let has_column_match = matches.iter().any(|m| {
            m.matched_column
                .as_ref()
                .map_or(false, |c| c.column == "email")
        });
        assert!(has_column_match);
    }

    #[test]
    fn test_resolve_column_synonym_without_glossary() {
        let schema = make_schema_no_glossary();
        let matches = resolve_term("email_address", &schema);
        let col_syn = matches
            .iter()
            .find(|m| m.source == MatchSource::ColumnSynonym);
        assert!(col_syn.is_some());
    }

    #[test]
    fn test_resolve_active_users_synonym() {
        let schema = make_schema();
        let matches = resolve_term("active_users", &schema);
        let syn = matches.iter().find(|m| m.source == MatchSource::Synonym);
        assert!(syn.is_some());
        assert_eq!(syn.unwrap().term, "active_customers");
    }

    #[test]
    fn test_resolve_purchase_amount() {
        let schema = make_schema();
        let matches = resolve_term("purchase_amount", &schema);
        let syn = matches.iter().find(|m| m.source == MatchSource::Synonym);
        assert!(syn.is_some());
        assert_eq!(syn.unwrap().term, "order_value");
    }

    #[test]
    fn test_resolve_short_term_no_description_match() {
        let schema = make_schema();
        // Terms shorter than 3 chars should not match via description
        let matches = resolve_term("or", &schema);
        let desc_match = matches
            .iter()
            .find(|m| m.source == MatchSource::Description);
        assert!(desc_match.is_none());
    }

    #[test]
    fn test_resolve_customer_name_synonym() {
        let schema = make_schema();
        let matches = resolve_term("customer_name", &schema);
        let col_syn = matches
            .iter()
            .find(|m| m.source == MatchSource::ColumnSynonym);
        assert!(col_syn.is_some());
        let col_ref = col_syn.unwrap().matched_column.as_ref().unwrap();
        assert_eq!(col_ref.column, "name");
    }

    // === resolve_all_terms tests ===

    #[test]
    fn test_resolve_all_terms_mixed() {
        let schema = make_schema();
        let result = resolve_all_terms(&["revenue", "nonexistent_xyz"], &schema);
        assert!(!result.resolved.is_empty());
        assert_eq!(result.unresolved, vec!["nonexistent_xyz"]);
    }

    #[test]
    fn test_resolve_all_terms_all_resolved() {
        let schema = make_schema();
        let result = resolve_all_terms(&["revenue", "sales", "active_users"], &schema);
        assert!(result.unresolved.is_empty());
        assert!(result.resolved.len() >= 3);
    }

    #[test]
    fn test_resolve_all_terms_all_unresolved() {
        let schema = make_schema();
        let result = resolve_all_terms(&["zzz_aaa", "zzz_bbb"], &schema);
        assert!(result.resolved.is_empty());
        assert_eq!(result.unresolved.len(), 2);
    }

    #[test]
    fn test_resolve_all_terms_empty_input() {
        let schema = make_schema();
        let result = resolve_all_terms(&[], &schema);
        assert!(result.resolved.is_empty());
        assert!(result.unresolved.is_empty());
    }

    #[test]
    fn test_resolve_all_terms_single() {
        let schema = make_schema();
        let result = resolve_all_terms(&["order_value"], &schema);
        assert!(result.unresolved.is_empty());
        assert!(!result.resolved.is_empty());
    }

    // === suggest_terms tests ===

    #[test]
    fn test_suggest_terms_finds_table() {
        let schema = make_schema();
        let suggestions = suggest_terms("SELECT * FROM orders", &schema);
        let has_orders = suggestions.iter().any(|s| s.sql_element == "orders");
        assert!(has_orders, "Should find 'orders' table in SQL");
    }

    #[test]
    fn test_suggest_terms_finds_column() {
        let schema = make_schema();
        let suggestions = suggest_terms("SELECT total_amount FROM orders", &schema);
        let has_total = suggestions.iter().any(|s| s.sql_element == "total_amount");
        assert!(has_total, "Should find 'total_amount' column in SQL");
    }

    #[test]
    fn test_suggest_terms_maps_to_glossary() {
        let schema = make_schema();
        let suggestions = suggest_terms("SELECT total_amount FROM orders", &schema);
        let mapped = suggestions
            .iter()
            .find(|s| s.sql_element == "total_amount" && s.suggested_term.contains("revenue"));
        assert!(mapped.is_some(), "Should map total_amount to revenue term");
    }

    #[test]
    fn test_suggest_terms_no_sql_elements() {
        let schema = make_schema();
        let suggestions = suggest_terms("SELECT 1", &schema);
        assert!(suggestions.is_empty());
    }

    #[test]
    fn test_suggest_terms_no_glossary() {
        let schema = make_schema_no_glossary();
        let suggestions = suggest_terms("SELECT total_amount FROM orders", &schema);
        // Without glossary, we may still find column synonym suggestions
        // but no glossary-term suggestions
        let glossary_suggestions = suggestions
            .iter()
            .filter(|s| !s.suggested_term.contains("synonym"))
            .count();
        // Without a glossary, there should be no non-synonym suggestions for glossary terms
        // (column synonyms are still found)
        assert!(
            glossary_suggestions == 0
                || suggestions
                    .iter()
                    .all(|s| s.suggested_term.contains("synonym"))
        );
    }

    #[test]
    fn test_suggest_terms_sorted_by_confidence() {
        let schema = make_schema();
        let suggestions = suggest_terms(
            "SELECT total_amount, user_id FROM orders JOIN users ON orders.user_id = users.id",
            &schema,
        );
        for i in 1..suggestions.len() {
            assert!(suggestions[i - 1].confidence >= suggestions[i].confidence);
        }
    }

    #[test]
    fn test_suggest_terms_with_join() {
        let schema = make_schema();
        let suggestions = suggest_terms(
            "SELECT u.email, o.total_amount FROM users u JOIN orders o ON u.id = o.user_id",
            &schema,
        );
        // Should find both tables and columns
        assert!(!suggestions.is_empty());
    }

    #[test]
    fn test_suggest_terms_column_synonym_suggestion() {
        let schema = make_schema();
        let suggestions = suggest_terms("SELECT status FROM orders", &schema);
        let syn_suggestion = suggestions
            .iter()
            .find(|s| s.suggested_term.contains("synonym"));
        assert!(
            syn_suggestion.is_some(),
            "Should suggest column synonym for status -> order_status"
        );
    }

    // === sql_contains_identifier tests ===

    #[test]
    fn test_sql_contains_identifier_basic() {
        assert!(sql_contains_identifier("select id from users", "users"));
        assert!(sql_contains_identifier("select id from users", "id"));
    }

    #[test]
    fn test_sql_contains_identifier_no_partial() {
        assert!(!sql_contains_identifier(
            "select user_id from orders",
            "user"
        ));
        assert!(!sql_contains_identifier(
            "select users_count from stats",
            "users"
        ));
    }

    #[test]
    fn test_sql_contains_identifier_start_of_string() {
        assert!(sql_contains_identifier("users WHERE id = 1", "users"));
    }

    #[test]
    fn test_sql_contains_identifier_end_of_string() {
        assert!(sql_contains_identifier("select * from users", "users"));
    }

    #[test]
    fn test_sql_contains_identifier_with_comma() {
        assert!(sql_contains_identifier(
            "select id, name from users",
            "name"
        ));
    }

    #[test]
    fn test_sql_contains_identifier_with_dot() {
        assert!(sql_contains_identifier(
            "select users.id from users",
            "users"
        ));
    }

    #[test]
    fn test_sql_contains_identifier_with_parens() {
        assert!(sql_contains_identifier("select count(id) from users", "id"));
    }

    #[test]
    fn test_sql_contains_identifier_empty() {
        assert!(!sql_contains_identifier("", "users"));
    }

    // === MatchSource Display ===

    #[test]
    fn test_match_source_display() {
        assert_eq!(MatchSource::ExactTerm.to_string(), "exact_term");
        assert_eq!(MatchSource::Synonym.to_string(), "synonym");
        assert_eq!(MatchSource::Description.to_string(), "description");
        assert_eq!(MatchSource::FuzzyName.to_string(), "fuzzy_name");
        assert_eq!(MatchSource::ColumnSynonym.to_string(), "column_synonym");
    }

    // === ColumnReference Display ===

    #[test]
    fn test_column_reference_display() {
        let cr = ColumnReference {
            table: "orders".to_string(),
            column: "total_amount".to_string(),
        };
        assert_eq!(cr.to_string(), "orders.total_amount");
    }

    // === Serialization tests ===

    #[test]
    fn test_glossary_match_serialize() {
        let m = GlossaryMatch {
            term: "revenue".to_string(),
            matched_column: Some(ColumnReference {
                table: "orders".to_string(),
                column: "total".to_string(),
            }),
            matched_definition: Some("SUM(total)".to_string()),
            confidence: 0.95,
            source: MatchSource::Synonym,
        };
        let json = serde_json::to_string(&m).expect("serialize");
        assert!(json.contains("revenue"));
        assert!(json.contains("Synonym"));
    }

    #[test]
    fn test_term_resolution_result_serialize() {
        let result = TermResolutionResult {
            resolved: vec![GlossaryMatch {
                term: "revenue".to_string(),
                matched_column: None,
                matched_definition: None,
                confidence: 1.0,
                source: MatchSource::ExactTerm,
            }],
            unresolved: vec!["foo".to_string()],
        };
        let json = serde_json::to_string(&result).expect("serialize");
        assert!(json.contains("resolved"));
        assert!(json.contains("unresolved"));
    }

    #[test]
    fn test_term_suggestion_serialize() {
        let s = TermSuggestion {
            sql_element: "total_amount".to_string(),
            suggested_term: "revenue".to_string(),
            confidence: 0.9,
        };
        let json = serde_json::to_string(&s).expect("serialize");
        assert!(json.contains("total_amount"));
    }

    #[test]
    fn test_glossary_deserialize() {
        let yaml = r#"
terms:
  revenue:
    term: revenue
    definition: "SUM(orders.total_amount)"
    column_ref:
      table: orders
      column: total_amount
    synonyms:
      - sales
      - income
    description: "Total revenue"
"#;
        let glossary: Glossary = serde_yaml::from_str(yaml).expect("deserialize glossary");
        assert_eq!(glossary.terms.len(), 1);
        let term = glossary.terms.get("revenue").unwrap();
        assert_eq!(term.synonyms.len(), 2);
    }

    #[test]
    fn test_glossary_default() {
        let g = Glossary::default();
        assert!(g.terms.is_empty());
    }

    // === Edge cases ===

    #[test]
    fn test_resolve_empty_string() {
        let schema = make_schema();
        let matches = resolve_term("", &schema);
        // Empty string should not match anything meaningfully
        // (jaro_winkler of empty vs any = 0)
        // but may match short description contains — this is fine
        assert!(matches.is_empty() || matches.iter().all(|m| m.confidence < 1.0));
    }

    #[test]
    fn test_resolve_exact_and_synonym_same_term() {
        // If searching for "revenue", we should get ExactTerm, not also a ColumnSynonym for "revenue"
        let schema = make_schema();
        let matches = resolve_term("revenue", &schema);
        // The ExactTerm should be first
        assert_eq!(matches[0].source, MatchSource::ExactTerm);
    }

    #[test]
    fn test_resolve_multiple_glossary_matches() {
        // "order" appears in descriptions of multiple terms
        let schema = make_schema();
        let matches = resolve_term("order", &schema);
        // Should find matches (description, fuzzy) — order_value is close to "order"
        assert!(!matches.is_empty());
    }

    #[test]
    fn test_suggest_terms_deduplication() {
        let schema = make_schema();
        let suggestions = suggest_terms(
            "SELECT total_amount FROM orders WHERE total_amount > 100",
            &schema,
        );
        // total_amount may appear in multiple suggestions (glossary term + column synonym)
        // but should not have exact duplicate (sql_element, suggested_term) pairs
        let total_suggestions: Vec<_> = suggestions
            .iter()
            .filter(|s| s.sql_element == "total_amount")
            .collect();
        // Check no exact duplicate pairs exist
        for i in 0..total_suggestions.len() {
            for j in (i + 1)..total_suggestions.len() {
                assert_ne!(
                    total_suggestions[i].suggested_term, total_suggestions[j].suggested_term,
                    "Found duplicate suggestion: {}",
                    total_suggestions[i].suggested_term
                );
            }
        }
    }
}
