//! Integration tests for the business glossary module.
//!
//! Tests cover term resolution, batch resolution, SQL-to-term suggestion,
//! and interactions with the ecommerce schema fixture.

use altimate_core::glossary::engine::{resolve_all_terms, resolve_term, suggest_terms};
use altimate_core::glossary::types::{ColumnReference, Glossary, GlossaryTerm, MatchSource};
use altimate_core::schema::types::{ColumnDef, SchemaDefinition, SqlDialect, TableDef};
use std::collections::HashMap;

// ── Helpers ──

fn ecommerce_schema() -> SchemaDefinition {
    SchemaDefinition::from_yaml(include_str!(
        "../../../tests/fixtures/schemas/ecommerce.yaml"
    ))
    .expect("Failed to load ecommerce schema")
}

fn schema_with_glossary() -> SchemaDefinition {
    let mut schema = ecommerce_schema();
    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 customer orders".to_string()),
        },
    );
    terms.insert(
        "active_users".to_string(),
        GlossaryTerm {
            term: "active_users".to_string(),
            definition: "COUNT(DISTINCT users.id) WHERE users.status = 'active'".to_string(),
            column_ref: Some(ColumnReference {
                table: "users".to_string(),
                column: "id".to_string(),
            }),
            synonyms: vec!["active_customers".to_string(), "engaged_users".to_string()],
            description: Some("Users with active account status".to_string()),
        },
    );
    terms.insert(
        "average_order_value".to_string(),
        GlossaryTerm {
            term: "average_order_value".to_string(),
            definition: "AVG(orders.total_amount)".to_string(),
            column_ref: Some(ColumnReference {
                table: "orders".to_string(),
                column: "total_amount".to_string(),
            }),
            synonyms: vec!["aov".to_string()],
            description: Some("Average monetary value per order".to_string()),
        },
    );
    terms.insert(
        "order_count".to_string(),
        GlossaryTerm {
            term: "order_count".to_string(),
            definition: "COUNT(orders.id)".to_string(),
            column_ref: Some(ColumnReference {
                table: "orders".to_string(),
                column: "id".to_string(),
            }),
            synonyms: vec!["num_orders".to_string(), "total_orders".to_string()],
            description: Some("Total number of orders placed".to_string()),
        },
    );
    schema.glossary = Some(Glossary { terms });
    schema
}

fn schema_with_synonyms() -> 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: None,
                    tags: vec![],
                    synonyms: vec!["order_id".to_string()],

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

                    statistics: None,
                },
                ColumnDef {
                    name: "status".to_string(),
                    data_type: "VARCHAR".to_string(),
                    nullable: false,
                    description: None,
                    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".to_string()),
                    tags: vec!["pii".to_string()],
                    synonyms: vec!["email_address".to_string(), "contact_email".to_string()],

                    statistics: None,
                },
                ColumnDef {
                    name: "name".to_string(),
                    data_type: "VARCHAR".to_string(),
                    nullable: true,
                    description: Some("Full name".to_string()),
                    tags: vec![],
                    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: None,
    }
}

// ═══════════════════════════════════════════════════════════════════════
// resolve_term — exact match
// ═══════════════════════════════════════════════════════════════════════

#[test]
fn glossary_resolve_exact_term_revenue() {
    let schema = schema_with_glossary();
    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 glossary_resolve_exact_term_case_insensitive() {
    let schema = schema_with_glossary();
    let matches = resolve_term("Revenue", &schema);
    assert!(!matches.is_empty());
    assert_eq!(matches[0].source, MatchSource::ExactTerm);
}

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

#[test]
fn glossary_resolve_exact_term_has_column_ref() {
    let schema = schema_with_glossary();
    let matches = resolve_term("revenue", &schema);
    let exact = matches
        .iter()
        .find(|m| m.source == MatchSource::ExactTerm)
        .expect("should have exact match");
    let col_ref = exact.matched_column.as_ref().expect("should have col ref");
    assert_eq!(col_ref.table, "orders");
    assert_eq!(col_ref.column, "total_amount");
}

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

// ═══════════════════════════════════════════════════════════════════════
// resolve_term — synonym match
// ═══════════════════════════════════════════════════════════════════════

#[test]
fn glossary_resolve_synonym_sales() {
    let schema = schema_with_glossary();
    let matches = resolve_term("sales", &schema);
    let syn = matches
        .iter()
        .find(|m| m.source == MatchSource::Synonym)
        .expect("should find synonym");
    assert_eq!(syn.term, "revenue");
    assert!(syn.confidence >= 0.9);
}

#[test]
fn glossary_resolve_synonym_income() {
    let schema = schema_with_glossary();
    let matches = resolve_term("income", &schema);
    assert!(matches.iter().any(|m| m.source == MatchSource::Synonym));
}

#[test]
fn glossary_resolve_synonym_active_customers() {
    let schema = schema_with_glossary();
    let matches = resolve_term("active_customers", &schema);
    let syn = matches
        .iter()
        .find(|m| m.source == MatchSource::Synonym)
        .expect("should find synonym");
    assert_eq!(syn.term, "active_users");
}

#[test]
fn glossary_resolve_synonym_aov() {
    let schema = schema_with_glossary();
    let matches = resolve_term("aov", &schema);
    let syn = matches
        .iter()
        .find(|m| m.source == MatchSource::Synonym)
        .expect("should find synonym");
    assert_eq!(syn.term, "average_order_value");
}

#[test]
fn glossary_resolve_synonym_total_orders() {
    let schema = schema_with_glossary();
    let matches = resolve_term("total_orders", &schema);
    let syn = matches
        .iter()
        .find(|m| m.source == MatchSource::Synonym)
        .expect("should find synonym");
    assert_eq!(syn.term, "order_count");
}

// ═══════════════════════════════════════════════════════════════════════
// resolve_term — description match
// ═══════════════════════════════════════════════════════════════════════

#[test]
fn glossary_resolve_description_match() {
    let schema = schema_with_glossary();
    let matches = resolve_term("customer orders", &schema);
    let desc = matches
        .iter()
        .find(|m| m.source == MatchSource::Description);
    assert!(desc.is_some(), "should match via description");
}

#[test]
fn glossary_resolve_short_term_no_description_match() {
    let schema = schema_with_glossary();
    let matches = resolve_term("or", &schema);
    let desc = matches
        .iter()
        .find(|m| m.source == MatchSource::Description);
    assert!(desc.is_none(), "short terms should not match descriptions");
}

// ═══════════════════════════════════════════════════════════════════════
// resolve_term — column synonym match (without glossary)
// ═══════════════════════════════════════════════════════════════════════

#[test]
fn glossary_resolve_column_synonym_order_status() {
    let schema = schema_with_synonyms();
    let matches = resolve_term("order_status", &schema);
    let col_syn = matches
        .iter()
        .find(|m| m.source == MatchSource::ColumnSynonym)
        .expect("should find column synonym");
    let col_ref = col_syn.matched_column.as_ref().unwrap();
    assert_eq!(col_ref.table, "orders");
    assert_eq!(col_ref.column, "status");
}

#[test]
fn glossary_resolve_column_synonym_email_address() {
    let schema = schema_with_synonyms();
    let matches = resolve_term("email_address", &schema);
    let col_syn = matches
        .iter()
        .find(|m| m.source == MatchSource::ColumnSynonym)
        .expect("should find column synonym");
    assert_eq!(col_syn.matched_column.as_ref().unwrap().column, "email");
}

#[test]
fn glossary_resolve_column_synonym_customer_name() {
    let schema = schema_with_synonyms();
    let matches = resolve_term("customer_name", &schema);
    let col_syn = matches
        .iter()
        .find(|m| m.source == MatchSource::ColumnSynonym)
        .expect("should find column synonym");
    assert_eq!(col_syn.matched_column.as_ref().unwrap().column, "name");
}

#[test]
fn glossary_resolve_column_synonym_order_total() {
    let schema = schema_with_synonyms();
    let matches = resolve_term("order_total", &schema);
    let col_syn = matches
        .iter()
        .find(|m| m.source == MatchSource::ColumnSynonym)
        .expect("should find column synonym");
    assert_eq!(
        col_syn.matched_column.as_ref().unwrap().column,
        "total_amount"
    );
}

// ═══════════════════════════════════════════════════════════════════════
// resolve_term — fuzzy match
// ═══════════════════════════════════════════════════════════════════════

#[test]
fn glossary_resolve_fuzzy_revenu() {
    let schema = schema_with_glossary();
    let matches = resolve_term("revenu", &schema);
    assert!(!matches.is_empty());
    assert!(matches[0].confidence >= 0.7);
}

#[test]
fn glossary_resolve_no_match_garbage() {
    let schema = schema_with_glossary();
    let matches = resolve_term("zzz_xyzzy_nonexistent", &schema);
    assert!(matches.is_empty());
}

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

// ═══════════════════════════════════════════════════════════════════════
// resolve_all_terms — batch resolution
// ═══════════════════════════════════════════════════════════════════════

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

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

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

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

#[test]
fn glossary_batch_resolve_five_terms() {
    let schema = schema_with_glossary();
    let result = resolve_all_terms(
        &["revenue", "sales", "aov", "active_customers", "num_orders"],
        &schema,
    );
    assert!(result.unresolved.is_empty());
}

// ═══════════════════════════════════════════════════════════════════════
// suggest_terms — SQL to glossary mapping
// ═══════════════════════════════════════════════════════════════════════

#[test]
fn glossary_suggest_terms_simple_select() {
    let schema = schema_with_glossary();
    let suggestions = suggest_terms("SELECT total_amount FROM orders", &schema);
    assert!(!suggestions.is_empty());
    let has_revenue = suggestions
        .iter()
        .any(|s| s.suggested_term.contains("revenue"));
    assert!(has_revenue, "should map total_amount to revenue");
}

#[test]
fn glossary_suggest_terms_join_query() {
    let schema = schema_with_glossary();
    let suggestions = suggest_terms(
        "SELECT u.id, o.total_amount FROM users u JOIN orders o ON u.id = o.user_id",
        &schema,
    );
    assert!(!suggestions.is_empty());
}

#[test]
fn glossary_suggest_terms_aggregation() {
    let schema = schema_with_glossary();
    let suggestions = suggest_terms(
        "SELECT SUM(total_amount) FROM orders WHERE status = 'completed'",
        &schema,
    );
    let has_revenue = suggestions
        .iter()
        .any(|s| s.suggested_term.contains("revenue"));
    assert!(has_revenue);
}

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

#[test]
fn glossary_suggest_terms_sorted_by_confidence() {
    let schema = schema_with_glossary();
    let suggestions = suggest_terms(
        "SELECT total_amount, status 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 glossary_suggest_terms_deduplication() {
    let schema = schema_with_glossary();
    let suggestions = suggest_terms(
        "SELECT total_amount FROM orders WHERE total_amount > 100",
        &schema,
    );
    // Check no exact duplicate (sql_element, suggested_term) pairs
    for i in 0..suggestions.len() {
        for j in (i + 1)..suggestions.len() {
            let same_pair = suggestions[i].sql_element == suggestions[j].sql_element
                && suggestions[i].suggested_term == suggestions[j].suggested_term;
            assert!(!same_pair, "duplicate suggestion found");
        }
    }
}

// ═══════════════════════════════════════════════════════════════════════
// suggest_terms — column synonym suggestions
// ═══════════════════════════════════════════════════════════════════════

#[test]
fn glossary_suggest_terms_column_synonyms() {
    let schema = schema_with_synonyms();
    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());
}

// ═══════════════════════════════════════════════════════════════════════
// Serialization tests
// ═══════════════════════════════════════════════════════════════════════

#[test]
fn glossary_match_serializes_to_json() {
    let schema = schema_with_glossary();
    let matches = resolve_term("revenue", &schema);
    let json = serde_json::to_string(&matches).expect("should serialize");
    assert!(json.contains("revenue"));
    assert!(json.contains("ExactTerm"));
}

#[test]
fn glossary_resolution_result_serializes() {
    let schema = schema_with_glossary();
    let result = resolve_all_terms(&["revenue", "zzz_missing"], &schema);
    let json = serde_json::to_string(&result).expect("should serialize");
    assert!(json.contains("resolved"));
    assert!(json.contains("unresolved"));
}

#[test]
fn glossary_term_suggestion_serializes() {
    let schema = schema_with_glossary();
    let suggestions = suggest_terms("SELECT total_amount FROM orders", &schema);
    let json = serde_json::to_string(&suggestions).expect("should serialize");
    assert!(json.contains("sql_element"));
}

// ═══════════════════════════════════════════════════════════════════════
// Edge cases
// ═══════════════════════════════════════════════════════════════════════

#[test]
fn glossary_resolve_empty_string() {
    let schema = schema_with_glossary();
    let matches = resolve_term("", &schema);
    assert!(matches.is_empty() || matches.iter().all(|m| m.confidence < 1.0));
}

#[test]
fn glossary_resolve_no_glossary_falls_back_to_columns() {
    let schema = schema_with_synonyms();
    let matches = resolve_term("email", &schema);
    assert!(!matches.is_empty());
    let has_col = matches.iter().any(|m| {
        m.matched_column
            .as_ref()
            .is_some_and(|c| c.column == "email")
    });
    assert!(has_col);
}
