//! Schema introspection engine.
//!
//! Generates INFORMATION_SCHEMA queries for various database types and parses
//! raw introspection output into altimate-core's `SchemaDefinition` format.

use std::collections::HashMap;

use super::types::*;
use crate::schema::types::{
    ColumnDef, ForeignKeyDef, ForeignKeyRef, SchemaDefinition, SqlDialect, TableDef,
};

/// Generate dialect-specific INFORMATION_SCHEMA SQL queries.
///
/// Returns separate queries for columns, primary keys, and foreign keys.
/// The caller is responsible for executing these queries against the target database
/// and feeding the results back through `parse_introspection_output`.
///
/// # Arguments
/// * `db_type` - The type of database to generate queries for
/// * `database` - The database/catalog name (used by MySQL and BigQuery)
/// * `schema_name` - The schema name (defaults to "public" for Postgres-like databases)
pub fn generate_introspection_sql(
    db_type: DatabaseType,
    database: &str,
    schema_name: Option<&str>,
) -> IntrospectionSql {
    match db_type {
        DatabaseType::Postgres | DatabaseType::Redshift | DatabaseType::DuckDB => {
            generate_postgres_sql(db_type, schema_name.unwrap_or("public"))
        }
        DatabaseType::MySQL => generate_mysql_sql(database),
        DatabaseType::SQLite => generate_sqlite_sql(),
        DatabaseType::Snowflake => {
            generate_snowflake_sql(database, schema_name.unwrap_or("PUBLIC"))
        }
        DatabaseType::BigQuery => generate_bigquery_sql(database, schema_name),
    }
}

/// Generate Postgres-compatible INFORMATION_SCHEMA queries.
/// Also used for Redshift and DuckDB which share the same schema.
fn generate_postgres_sql(db_type: DatabaseType, schema_name: &str) -> IntrospectionSql {
    let db_label = match db_type {
        DatabaseType::Redshift => "redshift",
        DatabaseType::DuckDB => "duckdb",
        _ => "postgres",
    };

    let columns_query = format!(
        "SELECT \
            table_name, \
            column_name, \
            data_type, \
            CASE WHEN is_nullable = 'YES' THEN true ELSE false END AS is_nullable, \
            ordinal_position, \
            column_default, \
            character_maximum_length, \
            numeric_precision, \
            numeric_scale \
        FROM information_schema.columns \
        WHERE table_schema = '{schema_name}' \
        ORDER BY table_name, ordinal_position"
    );

    let primary_keys_query = format!(
        "SELECT \
            kcu.table_name, \
            kcu.column_name \
        FROM information_schema.table_constraints tc \
        JOIN information_schema.key_column_usage kcu \
            ON tc.constraint_name = kcu.constraint_name \
            AND tc.table_schema = kcu.table_schema \
        WHERE tc.constraint_type = 'PRIMARY KEY' \
            AND tc.table_schema = '{schema_name}' \
        ORDER BY kcu.table_name, kcu.ordinal_position"
    );

    let foreign_keys_query = format!(
        "SELECT \
            kcu.table_name, \
            kcu.column_name, \
            ccu.table_name AS referenced_table, \
            ccu.column_name AS referenced_column \
        FROM information_schema.table_constraints tc \
        JOIN information_schema.key_column_usage kcu \
            ON tc.constraint_name = kcu.constraint_name \
            AND tc.table_schema = kcu.table_schema \
        JOIN information_schema.constraint_column_usage ccu \
            ON tc.constraint_name = ccu.constraint_name \
            AND tc.table_schema = ccu.table_schema \
        WHERE tc.constraint_type = 'FOREIGN KEY' \
            AND tc.table_schema = '{schema_name}' \
        ORDER BY kcu.table_name, kcu.ordinal_position"
    );

    IntrospectionSql {
        database_type: db_label.to_string(),
        columns_query,
        primary_keys_query,
        foreign_keys_query,
    }
}

/// Generate MySQL INFORMATION_SCHEMA queries.
fn generate_mysql_sql(database: &str) -> IntrospectionSql {
    let columns_query = format!(
        "SELECT \
            TABLE_NAME AS table_name, \
            COLUMN_NAME AS column_name, \
            DATA_TYPE AS data_type, \
            CASE WHEN IS_NULLABLE = 'YES' THEN true ELSE false END AS is_nullable, \
            ORDINAL_POSITION AS ordinal_position, \
            COLUMN_DEFAULT AS column_default, \
            CHARACTER_MAXIMUM_LENGTH AS character_maximum_length, \
            NUMERIC_PRECISION AS numeric_precision, \
            NUMERIC_SCALE AS numeric_scale \
        FROM information_schema.COLUMNS \
        WHERE TABLE_SCHEMA = '{database}' \
        ORDER BY TABLE_NAME, ORDINAL_POSITION"
    );

    let primary_keys_query = format!(
        "SELECT \
            TABLE_NAME AS table_name, \
            COLUMN_NAME AS column_name \
        FROM information_schema.KEY_COLUMN_USAGE \
        WHERE CONSTRAINT_NAME = 'PRIMARY' \
            AND TABLE_SCHEMA = '{database}' \
        ORDER BY TABLE_NAME, ORDINAL_POSITION"
    );

    let foreign_keys_query = format!(
        "SELECT \
            TABLE_NAME AS table_name, \
            COLUMN_NAME AS column_name, \
            REFERENCED_TABLE_NAME AS referenced_table, \
            REFERENCED_COLUMN_NAME AS referenced_column \
        FROM information_schema.KEY_COLUMN_USAGE \
        WHERE REFERENCED_TABLE_NAME IS NOT NULL \
            AND TABLE_SCHEMA = '{database}' \
        ORDER BY TABLE_NAME, ORDINAL_POSITION"
    );

    IntrospectionSql {
        database_type: "mysql".to_string(),
        columns_query,
        primary_keys_query,
        foreign_keys_query,
    }
}

/// Generate SQLite introspection queries.
///
/// SQLite does not have INFORMATION_SCHEMA. Instead, we query sqlite_master
/// for table names and use PRAGMA table_info for column details.
/// Foreign keys use PRAGMA foreign_key_list.
fn generate_sqlite_sql() -> IntrospectionSql {
    let columns_query = "\
        SELECT \
            m.name AS table_name, \
            p.name AS column_name, \
            p.type AS data_type, \
            CASE WHEN p.\"notnull\" = 0 THEN true ELSE false END AS is_nullable, \
            p.cid AS ordinal_position, \
            p.dflt_value AS column_default \
        FROM sqlite_master m \
        JOIN pragma_table_info(m.name) p \
        WHERE m.type = 'table' \
            AND m.name NOT LIKE 'sqlite_%' \
        ORDER BY m.name, p.cid"
        .to_string();

    let primary_keys_query = "\
        SELECT \
            m.name AS table_name, \
            p.name AS column_name \
        FROM sqlite_master m \
        JOIN pragma_table_info(m.name) p \
        WHERE m.type = 'table' \
            AND m.name NOT LIKE 'sqlite_%' \
            AND p.pk > 0 \
        ORDER BY m.name, p.pk"
        .to_string();

    let foreign_keys_query = "\
        SELECT \
            m.name AS table_name, \
            f.\"from\" AS column_name, \
            f.\"table\" AS referenced_table, \
            f.\"to\" AS referenced_column \
        FROM sqlite_master m \
        JOIN pragma_foreign_key_list(m.name) f \
        WHERE m.type = 'table' \
            AND m.name NOT LIKE 'sqlite_%' \
        ORDER BY m.name, f.seq"
        .to_string();

    IntrospectionSql {
        database_type: "sqlite".to_string(),
        columns_query,
        primary_keys_query,
        foreign_keys_query,
    }
}

/// Generate Snowflake INFORMATION_SCHEMA queries.
fn generate_snowflake_sql(database: &str, schema_name: &str) -> IntrospectionSql {
    let columns_query = format!(
        "SELECT \
            TABLE_NAME AS table_name, \
            COLUMN_NAME AS column_name, \
            DATA_TYPE AS data_type, \
            CASE WHEN IS_NULLABLE = 'YES' THEN true ELSE false END AS is_nullable, \
            ORDINAL_POSITION AS ordinal_position, \
            COLUMN_DEFAULT AS column_default, \
            CHARACTER_MAXIMUM_LENGTH AS character_maximum_length, \
            NUMERIC_PRECISION AS numeric_precision, \
            NUMERIC_SCALE AS numeric_scale \
        FROM {database}.information_schema.columns \
        WHERE TABLE_SCHEMA = '{schema_name}' \
        ORDER BY TABLE_NAME, ORDINAL_POSITION"
    );

    let primary_keys_query = format!("SHOW PRIMARY KEYS IN SCHEMA {database}.{schema_name}");

    let foreign_keys_query = format!("SHOW IMPORTED KEYS IN SCHEMA {database}.{schema_name}");

    IntrospectionSql {
        database_type: "snowflake".to_string(),
        columns_query,
        primary_keys_query,
        foreign_keys_query,
    }
}

/// Generate BigQuery INFORMATION_SCHEMA queries.
fn generate_bigquery_sql(dataset: &str, schema_name: Option<&str>) -> IntrospectionSql {
    let qualified = match schema_name {
        Some(s) => format!("{dataset}.{s}"),
        None => dataset.to_string(),
    };

    let columns_query = format!(
        "SELECT \
            table_name, \
            column_name, \
            data_type, \
            CASE WHEN is_nullable = 'YES' THEN true ELSE false END AS is_nullable, \
            ordinal_position, \
            CAST(NULL AS STRING) AS column_default, \
            CAST(NULL AS INT64) AS character_maximum_length, \
            CAST(NULL AS INT64) AS numeric_precision, \
            CAST(NULL AS INT64) AS numeric_scale \
        FROM `{qualified}`.INFORMATION_SCHEMA.COLUMNS \
        ORDER BY table_name, ordinal_position"
    );

    let primary_keys_query = format!(
        "SELECT \
            ccu.table_name, \
            ccu.column_name \
        FROM `{qualified}`.INFORMATION_SCHEMA.TABLE_CONSTRAINTS tc \
        JOIN `{qualified}`.INFORMATION_SCHEMA.CONSTRAINT_COLUMN_USAGE ccu \
            ON tc.constraint_name = ccu.constraint_name \
        WHERE tc.constraint_type = 'PRIMARY KEY' \
        ORDER BY ccu.table_name, ccu.ordinal_position"
    );

    // BigQuery does not have native foreign key constraints in all cases
    let foreign_keys_query = format!(
        "SELECT \
            ccu.table_name, \
            ccu.column_name, \
            rc.table_name AS referenced_table, \
            rc.column_name AS referenced_column \
        FROM `{qualified}`.INFORMATION_SCHEMA.TABLE_CONSTRAINTS tc \
        JOIN `{qualified}`.INFORMATION_SCHEMA.CONSTRAINT_COLUMN_USAGE ccu \
            ON tc.constraint_name = ccu.constraint_name \
        JOIN `{qualified}`.INFORMATION_SCHEMA.KEY_COLUMN_USAGE rc \
            ON tc.constraint_name = rc.constraint_name \
        WHERE tc.constraint_type = 'FOREIGN KEY' \
        ORDER BY ccu.table_name"
    );

    IntrospectionSql {
        database_type: "bigquery".to_string(),
        columns_query,
        primary_keys_query,
        foreign_keys_query,
    }
}

/// Parse raw INFORMATION_SCHEMA output into a altimate-core `SchemaDefinition`.
///
/// Groups columns by table, maps data type strings to altimate-core types,
/// applies primary key info, and builds foreign key constraints.
///
/// # Arguments
/// * `output` - Raw introspection output containing columns, primary keys, and foreign keys
///
/// # Returns
/// A `SchemaDefinition` ready for use with altimate-core's validation engine.
pub fn parse_introspection_output(
    output: &IntrospectionOutput,
) -> Result<SchemaDefinition, String> {
    if output.columns.is_empty() {
        return Err("introspection output contains no columns".to_string());
    }

    // Group columns by table
    let mut table_columns: HashMap<String, Vec<ColumnDef>> = HashMap::new();
    for raw_col in &output.columns {
        let col = ColumnDef {
            name: raw_col.column_name.clone(),
            data_type: normalize_data_type(
                &raw_col.data_type,
                raw_col.character_maximum_length,
                raw_col.numeric_precision,
                raw_col.numeric_scale,
            ),
            nullable: raw_col.is_nullable,
            description: None,
            tags: Vec::new(),
            synonyms: Vec::new(),
            statistics: None,
        };
        table_columns
            .entry(raw_col.table_name.clone())
            .or_default()
            .push(col);
    }

    // Sort columns within each table by ordinal position (they arrive sorted,
    // but let's be safe since we're grouping via HashMap)
    // The ordering is already ensured by our generated SQL's ORDER BY.

    // Group primary keys by table
    let mut table_pks: HashMap<String, Vec<String>> = HashMap::new();
    for pk in &output.primary_keys {
        table_pks
            .entry(pk.table_name.clone())
            .or_default()
            .push(pk.column_name.clone());
    }

    // Group foreign keys by table and column set
    let mut table_fks: HashMap<String, Vec<ForeignKeyDef>> = HashMap::new();
    for fk in &output.foreign_keys {
        let fk_list = table_fks.entry(fk.table_name.clone()).or_default();

        // Check if there's already a FK targeting the same referenced table
        // (multi-column FKs come as multiple rows)
        let existing = fk_list
            .iter_mut()
            .find(|existing_fk| existing_fk.references.table == fk.referenced_table);

        if let Some(existing_fk) = existing {
            if !existing_fk.columns.contains(&fk.column_name) {
                existing_fk.columns.push(fk.column_name.clone());
                existing_fk
                    .references
                    .columns
                    .push(fk.referenced_column.clone());
            }
        } else {
            fk_list.push(ForeignKeyDef {
                columns: vec![fk.column_name.clone()],
                references: ForeignKeyRef {
                    table: fk.referenced_table.clone(),
                    columns: vec![fk.referenced_column.clone()],
                },
            });
        }
    }

    // Build table definitions
    let mut tables: HashMap<String, TableDef> = HashMap::new();
    for (table_name, columns) in table_columns {
        let primary_key = table_pks.get(&table_name).cloned();
        let foreign_keys = table_fks.remove(&table_name).unwrap_or_default();

        tables.insert(
            table_name,
            TableDef {
                description: None,
                database: None,
                schema: None,
                columns,
                primary_key,
                foreign_keys,
                statistics: None,
                clustering_keys: vec![],
            },
        );
    }

    Ok(SchemaDefinition {
        version: "1".to_string(),
        dialect: SqlDialect::Generic,
        database: None,
        schema_name: None,
        tables,
        glossary: None,
    })
}

/// Normalize a raw data type string into altimate-core's canonical format.
///
/// Handles common variations across databases:
/// - `character varying` -> `VARCHAR(n)` or `VARCHAR`
/// - `integer` -> `INT`
/// - `timestamp without time zone` -> `TIMESTAMP`
/// - `numeric(p,s)` -> `DECIMAL(p,s)`
/// - etc.
fn normalize_data_type(
    raw_type: &str,
    char_max_len: Option<i64>,
    numeric_precision: Option<i64>,
    numeric_scale: Option<i64>,
) -> String {
    let lower = raw_type.to_lowercase();
    let lower = lower.trim();

    match lower {
        // Integer types
        "integer" | "int4" | "int" => "INT".to_string(),
        "smallint" | "int2" => "SMALLINT".to_string(),
        "bigint" | "int8" => "BIGINT".to_string(),
        "tinyint" => "TINYINT".to_string(),

        // Floating point
        "real" | "float4" => "FLOAT".to_string(),
        "double precision" | "float8" | "double" => "DOUBLE".to_string(),

        // Boolean
        "boolean" | "bool" => "BOOLEAN".to_string(),

        // Text types
        "text" | "mediumtext" | "longtext" | "string" => "TEXT".to_string(),
        "character varying" | "varchar" => match char_max_len {
            Some(len) => format!("VARCHAR({len})"),
            None => "VARCHAR".to_string(),
        },
        "character" | "char" | "bpchar" => match char_max_len {
            Some(len) => format!("CHAR({len})"),
            None => "CHAR".to_string(),
        },

        // Numeric/Decimal
        "numeric" | "decimal" => match (numeric_precision, numeric_scale) {
            (Some(p), Some(s)) if s > 0 => format!("DECIMAL({p},{s})"),
            (Some(p), _) => format!("DECIMAL({p})"),
            _ => "DECIMAL".to_string(),
        },

        // Date/time types
        "date" => "DATE".to_string(),
        "time" | "time without time zone" => "TIME".to_string(),
        "time with time zone" | "timetz" => "TIMETZ".to_string(),
        "timestamp" | "timestamp without time zone" | "datetime" => "TIMESTAMP".to_string(),
        "timestamp with time zone" | "timestamptz" => "TIMESTAMPTZ".to_string(),
        "interval" => "INTERVAL".to_string(),

        // Binary
        "bytea" | "blob" | "binary" | "varbinary" | "bytes" => "BYTES".to_string(),

        // JSON
        "json" => "JSON".to_string(),
        "jsonb" => "JSONB".to_string(),

        // UUID
        "uuid" => "UUID".to_string(),

        // Arrays (Postgres-style)
        s if s.starts_with("array") || s.ends_with("[]") => {
            format!("ARRAY<{}>", raw_type.trim_end_matches("[]"))
        }

        // Pass through anything else as-is (uppercased)
        _ => raw_type.to_uppercase(),
    }
}

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

    #[test]
    fn test_generate_postgres_sql() {
        let sql = generate_introspection_sql(DatabaseType::Postgres, "mydb", None);
        assert_eq!(sql.database_type, "postgres");
        assert!(sql.columns_query.contains("information_schema.columns"));
        assert!(sql.columns_query.contains("table_schema = 'public'"));
        assert!(sql.primary_keys_query.contains("PRIMARY KEY"));
        assert!(sql.foreign_keys_query.contains("FOREIGN KEY"));
    }

    #[test]
    fn test_generate_postgres_custom_schema() {
        let sql = generate_introspection_sql(DatabaseType::Postgres, "mydb", Some("analytics"));
        assert!(sql.columns_query.contains("table_schema = 'analytics'"));
    }

    #[test]
    fn test_generate_mysql_sql() {
        let sql = generate_introspection_sql(DatabaseType::MySQL, "mydb", None);
        assert_eq!(sql.database_type, "mysql");
        assert!(sql.columns_query.contains("TABLE_SCHEMA = 'mydb'"));
        assert!(
            sql.primary_keys_query
                .contains("CONSTRAINT_NAME = 'PRIMARY'")
        );
    }

    #[test]
    fn test_generate_sqlite_sql() {
        let sql = generate_introspection_sql(DatabaseType::SQLite, "", None);
        assert_eq!(sql.database_type, "sqlite");
        assert!(sql.columns_query.contains("sqlite_master"));
        assert!(sql.columns_query.contains("pragma_table_info"));
        assert!(sql.primary_keys_query.contains("pk > 0"));
        assert!(sql.foreign_keys_query.contains("pragma_foreign_key_list"));
    }

    #[test]
    fn test_generate_snowflake_sql() {
        let sql = generate_introspection_sql(DatabaseType::Snowflake, "MY_DB", Some("MY_SCHEMA"));
        assert_eq!(sql.database_type, "snowflake");
        assert!(
            sql.columns_query
                .contains("MY_DB.information_schema.columns")
        );
        assert!(sql.columns_query.contains("TABLE_SCHEMA = 'MY_SCHEMA'"));
        assert!(sql.primary_keys_query.contains("SHOW PRIMARY KEYS"));
    }

    #[test]
    fn test_generate_bigquery_sql() {
        let sql = generate_introspection_sql(DatabaseType::BigQuery, "my_project.my_dataset", None);
        assert_eq!(sql.database_type, "bigquery");
        assert!(sql.columns_query.contains("INFORMATION_SCHEMA.COLUMNS"));
    }

    #[test]
    fn test_generate_redshift_uses_postgres_style() {
        let sql = generate_introspection_sql(DatabaseType::Redshift, "mydb", None);
        assert_eq!(sql.database_type, "redshift");
        assert!(sql.columns_query.contains("information_schema.columns"));
    }

    #[test]
    fn test_generate_duckdb_uses_postgres_style() {
        let sql = generate_introspection_sql(DatabaseType::DuckDB, "mydb", None);
        assert_eq!(sql.database_type, "duckdb");
        assert!(sql.columns_query.contains("information_schema.columns"));
    }

    #[test]
    fn test_parse_introspection_empty() {
        let output = IntrospectionOutput {
            columns: vec![],
            primary_keys: vec![],
            foreign_keys: vec![],
        };
        let result = parse_introspection_output(&output);
        assert!(result.is_err());
        assert!(result.unwrap_err().contains("no columns"));
    }

    #[test]
    fn test_parse_introspection_basic() {
        let output = IntrospectionOutput {
            columns: vec![
                RawColumnInfo {
                    table_name: "users".to_string(),
                    column_name: "id".to_string(),
                    data_type: "integer".to_string(),
                    is_nullable: false,
                    ordinal_position: 1,
                    column_default: None,
                    character_maximum_length: None,
                    numeric_precision: None,
                    numeric_scale: None,
                },
                RawColumnInfo {
                    table_name: "users".to_string(),
                    column_name: "name".to_string(),
                    data_type: "character varying".to_string(),
                    is_nullable: true,
                    ordinal_position: 2,
                    column_default: None,
                    character_maximum_length: Some(255),
                    numeric_precision: None,
                    numeric_scale: None,
                },
                RawColumnInfo {
                    table_name: "users".to_string(),
                    column_name: "balance".to_string(),
                    data_type: "numeric".to_string(),
                    is_nullable: true,
                    ordinal_position: 3,
                    column_default: None,
                    character_maximum_length: None,
                    numeric_precision: Some(10),
                    numeric_scale: Some(2),
                },
            ],
            primary_keys: vec![RawPrimaryKeyInfo {
                table_name: "users".to_string(),
                column_name: "id".to_string(),
            }],
            foreign_keys: vec![],
        };

        let schema = parse_introspection_output(&output).expect("should parse");
        assert_eq!(schema.tables.len(), 1);

        let users = schema.tables.get("users").expect("users table");
        assert_eq!(users.columns.len(), 3);
        assert_eq!(users.primary_key, Some(vec!["id".to_string()]));
        assert!(users.foreign_keys.is_empty());

        // Check type normalization
        let id_col = users.columns.iter().find(|c| c.name == "id").unwrap();
        assert_eq!(id_col.data_type, "INT");
        assert!(!id_col.nullable);

        let name_col = users.columns.iter().find(|c| c.name == "name").unwrap();
        assert_eq!(name_col.data_type, "VARCHAR(255)");
        assert!(name_col.nullable);

        let balance_col = users.columns.iter().find(|c| c.name == "balance").unwrap();
        assert_eq!(balance_col.data_type, "DECIMAL(10,2)");
    }

    #[test]
    fn test_parse_introspection_foreign_keys() {
        let output = IntrospectionOutput {
            columns: vec![
                RawColumnInfo {
                    table_name: "orders".to_string(),
                    column_name: "id".to_string(),
                    data_type: "integer".to_string(),
                    is_nullable: false,
                    ordinal_position: 1,
                    column_default: None,
                    character_maximum_length: None,
                    numeric_precision: None,
                    numeric_scale: None,
                },
                RawColumnInfo {
                    table_name: "orders".to_string(),
                    column_name: "user_id".to_string(),
                    data_type: "integer".to_string(),
                    is_nullable: false,
                    ordinal_position: 2,
                    column_default: None,
                    character_maximum_length: None,
                    numeric_precision: None,
                    numeric_scale: None,
                },
            ],
            primary_keys: vec![RawPrimaryKeyInfo {
                table_name: "orders".to_string(),
                column_name: "id".to_string(),
            }],
            foreign_keys: vec![RawForeignKeyInfo {
                table_name: "orders".to_string(),
                column_name: "user_id".to_string(),
                referenced_table: "users".to_string(),
                referenced_column: "id".to_string(),
            }],
        };

        let schema = parse_introspection_output(&output).expect("should parse");
        let orders = schema.tables.get("orders").expect("orders table");
        assert_eq!(orders.foreign_keys.len(), 1);
        assert_eq!(orders.foreign_keys[0].columns, vec!["user_id"]);
        assert_eq!(orders.foreign_keys[0].references.table, "users");
        assert_eq!(orders.foreign_keys[0].references.columns, vec!["id"]);
    }

    #[test]
    fn test_normalize_data_type_variants() {
        assert_eq!(normalize_data_type("integer", None, None, None), "INT");
        assert_eq!(normalize_data_type("int4", None, None, None), "INT");
        assert_eq!(normalize_data_type("bigint", None, None, None), "BIGINT");
        assert_eq!(normalize_data_type("boolean", None, None, None), "BOOLEAN");
        assert_eq!(normalize_data_type("text", None, None, None), "TEXT");
        assert_eq!(
            normalize_data_type("character varying", Some(100), None, None),
            "VARCHAR(100)"
        );
        assert_eq!(
            normalize_data_type("character varying", None, None, None),
            "VARCHAR"
        );
        assert_eq!(
            normalize_data_type("numeric", None, Some(10), Some(2)),
            "DECIMAL(10,2)"
        );
        assert_eq!(
            normalize_data_type("timestamp without time zone", None, None, None),
            "TIMESTAMP"
        );
        assert_eq!(
            normalize_data_type("timestamp with time zone", None, None, None),
            "TIMESTAMPTZ"
        );
        assert_eq!(normalize_data_type("jsonb", None, None, None), "JSONB");
        assert_eq!(normalize_data_type("uuid", None, None, None), "UUID");
        assert_eq!(normalize_data_type("bytea", None, None, None), "BYTES");
        assert_eq!(
            normalize_data_type("SOME_CUSTOM_TYPE", None, None, None),
            "SOME_CUSTOM_TYPE"
        );
    }
}
