//! Comprehensive tests for the schema introspection module.

use altimate_core::introspect::engine::{generate_introspection_sql, parse_introspection_output};
use altimate_core::introspect::types::*;

// ─── SQL generation: Postgres ──────────────────────────────────────

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

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

#[test]
fn test_postgres_queries_use_information_schema() {
    let sql = generate_introspection_sql(DatabaseType::Postgres, "mydb", None);
    assert!(sql.columns_query.contains("information_schema.columns"));
    assert!(
        sql.primary_keys_query
            .contains("information_schema.table_constraints")
    );
    assert!(
        sql.foreign_keys_query
            .contains("information_schema.table_constraints")
    );
}

#[test]
fn test_postgres_columns_query_selects_required_fields() {
    let sql = generate_introspection_sql(DatabaseType::Postgres, "mydb", None);
    assert!(sql.columns_query.contains("table_name"));
    assert!(sql.columns_query.contains("column_name"));
    assert!(sql.columns_query.contains("data_type"));
    assert!(sql.columns_query.contains("is_nullable"));
    assert!(sql.columns_query.contains("ordinal_position"));
}

// ─── SQL generation: MySQL ────────────────────────────────────────

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

#[test]
fn test_mysql_primary_key_filter() {
    let sql = generate_introspection_sql(DatabaseType::MySQL, "db", None);
    assert!(
        sql.primary_keys_query
            .contains("CONSTRAINT_NAME = 'PRIMARY'")
    );
}

#[test]
fn test_mysql_foreign_key_uses_referenced_table() {
    let sql = generate_introspection_sql(DatabaseType::MySQL, "db", None);
    assert!(sql.foreign_keys_query.contains("REFERENCED_TABLE_NAME"));
}

// ─── SQL generation: SQLite ────────────────────────────────────────

#[test]
fn test_sqlite_uses_sqlite_master() {
    let sql = generate_introspection_sql(DatabaseType::SQLite, "", None);
    assert_eq!(sql.database_type, "sqlite");
    assert!(sql.columns_query.contains("sqlite_master"));
}

#[test]
fn test_sqlite_uses_pragma_for_columns() {
    let sql = generate_introspection_sql(DatabaseType::SQLite, "", None);
    assert!(sql.columns_query.contains("pragma_table_info"));
}

#[test]
fn test_sqlite_uses_pragma_for_foreign_keys() {
    let sql = generate_introspection_sql(DatabaseType::SQLite, "", None);
    assert!(sql.foreign_keys_query.contains("pragma_foreign_key_list"));
}

#[test]
fn test_sqlite_excludes_internal_tables() {
    let sql = generate_introspection_sql(DatabaseType::SQLite, "", None);
    assert!(sql.columns_query.contains("NOT LIKE 'sqlite_%'"));
}

#[test]
fn test_sqlite_primary_key_detection() {
    let sql = generate_introspection_sql(DatabaseType::SQLite, "", None);
    assert!(sql.primary_keys_query.contains("pk > 0"));
}

// ─── SQL generation: Snowflake ────────────────────────────────────

#[test]
fn test_snowflake_uses_database_prefix() {
    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'"));
}

#[test]
fn test_snowflake_default_schema_is_public() {
    let sql = generate_introspection_sql(DatabaseType::Snowflake, "DB", None);
    assert!(sql.columns_query.contains("TABLE_SCHEMA = 'PUBLIC'"));
}

#[test]
fn test_snowflake_uses_show_commands() {
    let sql = generate_introspection_sql(DatabaseType::Snowflake, "DB", Some("SCH"));
    assert!(sql.primary_keys_query.contains("SHOW PRIMARY KEYS"));
    assert!(sql.foreign_keys_query.contains("SHOW IMPORTED KEYS"));
}

// ─── SQL generation: BigQuery ─────────────────────────────────────

#[test]
fn test_bigquery_uses_dataset_prefix() {
    let sql = generate_introspection_sql(DatabaseType::BigQuery, "project.dataset", None);
    assert_eq!(sql.database_type, "bigquery");
    assert!(sql.columns_query.contains("project.dataset"));
}

#[test]
fn test_bigquery_with_schema_name() {
    let sql = generate_introspection_sql(DatabaseType::BigQuery, "my_project", Some("my_dataset"));
    assert!(sql.columns_query.contains("my_project.my_dataset"));
}

#[test]
fn test_bigquery_uses_information_schema() {
    let sql = generate_introspection_sql(DatabaseType::BigQuery, "proj", None);
    assert!(sql.columns_query.contains("INFORMATION_SCHEMA.COLUMNS"));
}

// ─── SQL generation: Redshift ─────────────────────────────────────

#[test]
fn test_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_redshift_default_schema_is_public() {
    let sql = generate_introspection_sql(DatabaseType::Redshift, "mydb", None);
    assert!(sql.columns_query.contains("table_schema = 'public'"));
}

// ─── SQL generation: DuckDB ───────────────────────────────────────

#[test]
fn test_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_duckdb_custom_schema() {
    let sql = generate_introspection_sql(DatabaseType::DuckDB, "mydb", Some("staging"));
    assert!(sql.columns_query.contains("table_schema = 'staging'"));
}

// ─── Parsing: empty output ────────────────────────────────────────

#[test]
fn test_parse_empty_columns_returns_error() {
    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"));
}

// ─── Parsing: single table ───────────────────────────────────────

#[test]
fn test_parse_single_table_basic() {
    let output = IntrospectionOutput {
        columns: vec![
            make_col("users", "id", "integer", false, 1),
            make_col("users", "name", "text", true, 2),
        ],
        primary_keys: vec![],
        foreign_keys: vec![],
    };
    let schema = parse_introspection_output(&output).expect("should parse");
    assert_eq!(schema.tables.len(), 1);
    assert!(schema.tables.contains_key("users"));
    assert_eq!(schema.tables["users"].columns.len(), 2);
}

#[test]
fn test_parse_primary_key() {
    let output = IntrospectionOutput {
        columns: vec![make_col("users", "id", "integer", false, 1)],
        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["users"].primary_key,
        Some(vec!["id".to_string()])
    );
}

#[test]
fn test_parse_composite_primary_key() {
    let output = IntrospectionOutput {
        columns: vec![
            make_col("order_items", "order_id", "integer", false, 1),
            make_col("order_items", "product_id", "integer", false, 2),
        ],
        primary_keys: vec![
            RawPrimaryKeyInfo {
                table_name: "order_items".to_string(),
                column_name: "order_id".to_string(),
            },
            RawPrimaryKeyInfo {
                table_name: "order_items".to_string(),
                column_name: "product_id".to_string(),
            },
        ],
        foreign_keys: vec![],
    };
    let schema = parse_introspection_output(&output).expect("should parse");
    assert_eq!(
        schema.tables["order_items"].primary_key,
        Some(vec!["order_id".to_string(), "product_id".to_string()])
    );
}

// ─── Parsing: foreign keys ───────────────────────────────────────

#[test]
fn test_parse_single_foreign_key() {
    let output = IntrospectionOutput {
        columns: vec![
            make_col("orders", "id", "integer", false, 1),
            make_col("orders", "user_id", "integer", false, 2),
        ],
        primary_keys: vec![],
        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 fks = &schema.tables["orders"].foreign_keys;
    assert_eq!(fks.len(), 1);
    assert_eq!(fks[0].columns, vec!["user_id"]);
    assert_eq!(fks[0].references.table, "users");
    assert_eq!(fks[0].references.columns, vec!["id"]);
}

#[test]
fn test_parse_multi_column_foreign_key() {
    let output = IntrospectionOutput {
        columns: vec![
            make_col("order_items", "order_id", "integer", false, 1),
            make_col("order_items", "product_id", "integer", false, 2),
        ],
        primary_keys: vec![],
        foreign_keys: vec![
            RawForeignKeyInfo {
                table_name: "order_items".to_string(),
                column_name: "order_id".to_string(),
                referenced_table: "orders".to_string(),
                referenced_column: "id".to_string(),
            },
            RawForeignKeyInfo {
                table_name: "order_items".to_string(),
                column_name: "product_id".to_string(),
                referenced_table: "products".to_string(),
                referenced_column: "id".to_string(),
            },
        ],
    };
    let schema = parse_introspection_output(&output).expect("should parse");
    let fks = &schema.tables["order_items"].foreign_keys;
    assert_eq!(fks.len(), 2);
}

// ─── Parsing: type normalization ─────────────────────────────────

#[test]
fn test_parse_integer_normalization() {
    let output = make_single_col_output("t", "c", "integer", false);
    let schema = parse_introspection_output(&output).expect("should parse");
    assert_eq!(schema.tables["t"].columns[0].data_type, "INT");
}

#[test]
fn test_parse_varchar_with_length() {
    let output = IntrospectionOutput {
        columns: vec![RawColumnInfo {
            table_name: "t".to_string(),
            column_name: "c".to_string(),
            data_type: "character varying".to_string(),
            is_nullable: true,
            ordinal_position: 1,
            column_default: None,
            character_maximum_length: Some(255),
            numeric_precision: None,
            numeric_scale: None,
        }],
        primary_keys: vec![],
        foreign_keys: vec![],
    };
    let schema = parse_introspection_output(&output).expect("should parse");
    assert_eq!(schema.tables["t"].columns[0].data_type, "VARCHAR(255)");
}

#[test]
fn test_parse_varchar_without_length() {
    let output = make_single_col_output("t", "c", "character varying", true);
    let schema = parse_introspection_output(&output).expect("should parse");
    assert_eq!(schema.tables["t"].columns[0].data_type, "VARCHAR");
}

#[test]
fn test_parse_decimal_with_precision_scale() {
    let output = IntrospectionOutput {
        columns: vec![RawColumnInfo {
            table_name: "t".to_string(),
            column_name: "c".to_string(),
            data_type: "numeric".to_string(),
            is_nullable: true,
            ordinal_position: 1,
            column_default: None,
            character_maximum_length: None,
            numeric_precision: Some(10),
            numeric_scale: Some(2),
        }],
        primary_keys: vec![],
        foreign_keys: vec![],
    };
    let schema = parse_introspection_output(&output).expect("should parse");
    assert_eq!(schema.tables["t"].columns[0].data_type, "DECIMAL(10,2)");
}

#[test]
fn test_parse_boolean_normalization() {
    let output = make_single_col_output("t", "c", "boolean", false);
    let schema = parse_introspection_output(&output).expect("should parse");
    assert_eq!(schema.tables["t"].columns[0].data_type, "BOOLEAN");
}

#[test]
fn test_parse_timestamp_normalization() {
    let output = make_single_col_output("t", "c", "timestamp without time zone", false);
    let schema = parse_introspection_output(&output).expect("should parse");
    assert_eq!(schema.tables["t"].columns[0].data_type, "TIMESTAMP");
}

#[test]
fn test_parse_timestamptz_normalization() {
    let output = make_single_col_output("t", "c", "timestamp with time zone", false);
    let schema = parse_introspection_output(&output).expect("should parse");
    assert_eq!(schema.tables["t"].columns[0].data_type, "TIMESTAMPTZ");
}

#[test]
fn test_parse_bigint_normalization() {
    let output = make_single_col_output("t", "c", "bigint", false);
    let schema = parse_introspection_output(&output).expect("should parse");
    assert_eq!(schema.tables["t"].columns[0].data_type, "BIGINT");
}

#[test]
fn test_parse_smallint_normalization() {
    let output = make_single_col_output("t", "c", "smallint", false);
    let schema = parse_introspection_output(&output).expect("should parse");
    assert_eq!(schema.tables["t"].columns[0].data_type, "SMALLINT");
}

#[test]
fn test_parse_float_normalization() {
    let output = make_single_col_output("t", "c", "real", false);
    let schema = parse_introspection_output(&output).expect("should parse");
    assert_eq!(schema.tables["t"].columns[0].data_type, "FLOAT");
}

#[test]
fn test_parse_double_normalization() {
    let output = make_single_col_output("t", "c", "double precision", false);
    let schema = parse_introspection_output(&output).expect("should parse");
    assert_eq!(schema.tables["t"].columns[0].data_type, "DOUBLE");
}

#[test]
fn test_parse_text_normalization() {
    let output = make_single_col_output("t", "c", "text", true);
    let schema = parse_introspection_output(&output).expect("should parse");
    assert_eq!(schema.tables["t"].columns[0].data_type, "TEXT");
}

#[test]
fn test_parse_jsonb_normalization() {
    let output = make_single_col_output("t", "c", "jsonb", true);
    let schema = parse_introspection_output(&output).expect("should parse");
    assert_eq!(schema.tables["t"].columns[0].data_type, "JSONB");
}

#[test]
fn test_parse_uuid_normalization() {
    let output = make_single_col_output("t", "c", "uuid", false);
    let schema = parse_introspection_output(&output).expect("should parse");
    assert_eq!(schema.tables["t"].columns[0].data_type, "UUID");
}

#[test]
fn test_parse_bytea_normalization() {
    let output = make_single_col_output("t", "c", "bytea", true);
    let schema = parse_introspection_output(&output).expect("should parse");
    assert_eq!(schema.tables["t"].columns[0].data_type, "BYTES");
}

#[test]
fn test_parse_custom_type_passthrough() {
    let output = make_single_col_output("t", "c", "GEOMETRY", false);
    let schema = parse_introspection_output(&output).expect("should parse");
    assert_eq!(schema.tables["t"].columns[0].data_type, "GEOMETRY");
}

// ─── Parsing: multiple tables ────────────────────────────────────

#[test]
fn test_parse_multiple_tables() {
    let output = IntrospectionOutput {
        columns: vec![
            make_col("users", "id", "integer", false, 1),
            make_col("users", "name", "text", true, 2),
            make_col("orders", "id", "integer", false, 1),
            make_col("orders", "user_id", "integer", false, 2),
        ],
        primary_keys: vec![
            RawPrimaryKeyInfo {
                table_name: "users".to_string(),
                column_name: "id".to_string(),
            },
            RawPrimaryKeyInfo {
                table_name: "orders".to_string(),
                column_name: "id".to_string(),
            },
        ],
        foreign_keys: vec![],
    };
    let schema = parse_introspection_output(&output).expect("should parse");
    assert_eq!(schema.tables.len(), 2);
    assert!(schema.tables.contains_key("users"));
    assert!(schema.tables.contains_key("orders"));
}

#[test]
fn test_parse_nullable_flags() {
    let output = IntrospectionOutput {
        columns: vec![
            make_col("t", "required_col", "integer", false, 1),
            make_col("t", "optional_col", "text", true, 2),
        ],
        primary_keys: vec![],
        foreign_keys: vec![],
    };
    let schema = parse_introspection_output(&output).expect("should parse");
    assert!(!schema.tables["t"].columns[0].nullable);
    assert!(schema.tables["t"].columns[1].nullable);
}

#[test]
fn test_parse_no_primary_key() {
    let output = IntrospectionOutput {
        columns: vec![make_col("logs", "message", "text", true, 1)],
        primary_keys: vec![],
        foreign_keys: vec![],
    };
    let schema = parse_introspection_output(&output).expect("should parse");
    assert_eq!(schema.tables["logs"].primary_key, None);
}

#[test]
fn test_parse_schema_version_is_one() {
    let output = IntrospectionOutput {
        columns: vec![make_col("t", "c", "text", true, 1)],
        primary_keys: vec![],
        foreign_keys: vec![],
    };
    let schema = parse_introspection_output(&output).expect("should parse");
    assert_eq!(schema.version, "1");
}

#[test]
fn test_parse_dialect_is_generic() {
    let output = IntrospectionOutput {
        columns: vec![make_col("t", "c", "text", true, 1)],
        primary_keys: vec![],
        foreign_keys: vec![],
    };
    let schema = parse_introspection_output(&output).expect("should parse");
    assert_eq!(
        schema.dialect,
        altimate_core::schema::types::SqlDialect::Generic
    );
}

// ─── Helpers ─────────────────────────────────────────────────────

fn make_col(
    table: &str,
    column: &str,
    data_type: &str,
    nullable: bool,
    ordinal: usize,
) -> RawColumnInfo {
    RawColumnInfo {
        table_name: table.to_string(),
        column_name: column.to_string(),
        data_type: data_type.to_string(),
        is_nullable: nullable,
        ordinal_position: ordinal,
        column_default: None,
        character_maximum_length: None,
        numeric_precision: None,
        numeric_scale: None,
    }
}

fn make_single_col_output(
    table: &str,
    column: &str,
    data_type: &str,
    nullable: bool,
) -> IntrospectionOutput {
    IntrospectionOutput {
        columns: vec![make_col(table, column, data_type, nullable, 1)],
        primary_keys: vec![],
        foreign_keys: vec![],
    }
}
