//! Test generation engine.
//!
//! Analyzes SQL queries against a schema to generate test cases
//! covering boundary values, NULL handling, empty tables, type edge
//! cases, WHERE clause variations, and JOIN coverage.

use super::types::*;
use crate::schema::types::SchemaDefinition;
use sqlparser::ast::{
    Expr, FunctionArg, FunctionArgExpr, FunctionArguments, ObjectNamePart, SelectItem, SetExpr,
    Statement, TableFactor,
};
use sqlparser::dialect::GenericDialect;
use sqlparser::parser::Parser;

/// Generate test cases for a SQL query based on its schema.
pub fn generate_tests(sql: &str, schema: &SchemaDefinition) -> TestGenResult {
    let dialect = GenericDialect {};
    let statements = match Parser::parse_sql(&dialect, sql) {
        Ok(stmts) => stmts,
        Err(_) => {
            return TestGenResult {
                sql: sql.to_string(),
                test_cases: vec![],
                columns_analyzed: vec![],
                tables_referenced: vec![],
            };
        }
    };

    let mut tables_referenced = Vec::new();
    let mut columns_analyzed = Vec::new();
    let mut test_cases = Vec::new();

    for stmt in &statements {
        if let Statement::Query(query) = stmt
            && let SetExpr::Select(ref select) = *query.body
        {
            // Collect table names
            for twj in &select.from {
                if let Some(name) = extract_table_name(&twj.relation) {
                    tables_referenced.push(name);
                }
                for join in &twj.joins {
                    if let Some(name) = extract_table_name(&join.relation) {
                        tables_referenced.push(name);
                    }
                }
            }

            // Collect columns from projection
            for item in &select.projection {
                match item {
                    SelectItem::UnnamedExpr(expr) => {
                        collect_columns_from_expr(expr, &mut columns_analyzed);
                    }
                    SelectItem::ExprWithAlias { expr, .. } => {
                        collect_columns_from_expr(expr, &mut columns_analyzed);
                    }
                    SelectItem::Wildcard(_) => {
                        // Expand * using schema
                        for table in &tables_referenced {
                            if let Some(cols) = schema.column_names(table) {
                                for col in cols {
                                    if !columns_analyzed.contains(&col.to_string()) {
                                        columns_analyzed.push(col.to_string());
                                    }
                                }
                            }
                        }
                    }
                    _ => {}
                }
            }

            // Collect columns from WHERE clause
            if let Some(ref selection) = select.selection {
                collect_columns_from_expr(selection, &mut columns_analyzed);
                // Generate WHERE clause test cases
                generate_where_tests(selection, schema, &tables_referenced, &mut test_cases);
            }

            // Generate boundary value tests for each column
            generate_boundary_tests(
                &columns_analyzed,
                schema,
                &tables_referenced,
                &mut test_cases,
            );

            // Generate NULL handling tests
            generate_null_tests(
                &columns_analyzed,
                schema,
                &tables_referenced,
                &mut test_cases,
            );

            // Generate empty table tests
            generate_empty_table_tests(&tables_referenced, &mut test_cases);

            // Generate type edge case tests
            generate_type_edge_tests(
                &columns_analyzed,
                schema,
                &tables_referenced,
                &mut test_cases,
            );

            // Generate JOIN coverage tests
            if !select.from.is_empty() && select.from.iter().any(|t| !t.joins.is_empty()) {
                generate_join_tests(&tables_referenced, &mut test_cases);
            }
        }
    }

    // Deduplicate
    columns_analyzed.sort();
    columns_analyzed.dedup();
    tables_referenced.sort();
    tables_referenced.dedup();

    TestGenResult {
        sql: sql.to_string(),
        test_cases,
        columns_analyzed,
        tables_referenced,
    }
}

fn extract_table_name(factor: &TableFactor) -> Option<String> {
    if let TableFactor::Table { name, .. } = factor {
        name.0.last().map(|part| match part {
            ObjectNamePart::Identifier(ident) => ident.value.clone(),
        })
    } else {
        None
    }
}

fn collect_columns_from_expr(expr: &Expr, columns: &mut Vec<String>) {
    match expr {
        Expr::Identifier(ident) => {
            let name = ident.value.clone();
            if !columns.contains(&name) {
                columns.push(name);
            }
        }
        Expr::CompoundIdentifier(idents) => {
            if let Some(last) = idents.last() {
                let name = last.value.clone();
                if !columns.contains(&name) {
                    columns.push(name);
                }
            }
        }
        Expr::BinaryOp { left, right, .. } => {
            collect_columns_from_expr(left, columns);
            collect_columns_from_expr(right, columns);
        }
        Expr::Function(func) => {
            if let FunctionArguments::List(arg_list) = &func.args {
                for arg in &arg_list.args {
                    if let FunctionArg::Unnamed(FunctionArgExpr::Expr(e)) = arg {
                        collect_columns_from_expr(e, columns);
                    }
                }
            }
        }
        Expr::IsNull(e) | Expr::IsNotNull(e) => {
            collect_columns_from_expr(e, columns);
        }
        Expr::Between {
            expr, low, high, ..
        } => {
            collect_columns_from_expr(expr, columns);
            collect_columns_from_expr(low, columns);
            collect_columns_from_expr(high, columns);
        }
        Expr::InList { expr, list, .. } => {
            collect_columns_from_expr(expr, columns);
            for item in list {
                collect_columns_from_expr(item, columns);
            }
        }
        Expr::Nested(inner) => {
            collect_columns_from_expr(inner, columns);
        }
        _ => {}
    }
}

/// Find column data type from schema.
fn find_column_type<'a>(
    col: &str,
    schema: &'a SchemaDefinition,
    tables: &[String],
) -> Option<&'a str> {
    for table in tables {
        if let Some(table_def) = schema.tables.get(table) {
            for col_def in &table_def.columns {
                if col_def.name.eq_ignore_ascii_case(col) {
                    return Some(&col_def.data_type);
                }
            }
        }
    }
    None
}

/// Check if a column is nullable in the schema.
fn is_column_nullable(col: &str, schema: &SchemaDefinition, tables: &[String]) -> bool {
    for table in tables {
        if let Some(table_def) = schema.tables.get(table) {
            for col_def in &table_def.columns {
                if col_def.name.eq_ignore_ascii_case(col) {
                    return col_def.nullable;
                }
            }
        }
    }
    true // default to nullable if not found
}

fn is_numeric_type(data_type: &str) -> bool {
    let upper = data_type.to_uppercase();
    upper.contains("INT")
        || upper.contains("FLOAT")
        || upper.contains("DOUBLE")
        || upper.contains("DECIMAL")
        || upper.contains("NUMERIC")
        || upper.contains("REAL")
        || upper.contains("BIGINT")
        || upper.contains("SMALLINT")
        || upper.contains("TINYINT")
}

fn is_string_type(data_type: &str) -> bool {
    let upper = data_type.to_uppercase();
    upper.contains("VARCHAR")
        || upper.contains("TEXT")
        || upper.contains("CHAR")
        || upper.contains("STRING")
        || upper == "UTF8"
}

fn is_timestamp_type(data_type: &str) -> bool {
    let upper = data_type.to_uppercase();
    upper.contains("TIMESTAMP") || upper.contains("DATETIME") || upper.contains("DATE")
}

fn is_decimal_type(data_type: &str) -> bool {
    let upper = data_type.to_uppercase();
    upper.contains("DECIMAL") || upper.contains("NUMERIC")
}

fn is_boolean_type(data_type: &str) -> bool {
    let upper = data_type.to_uppercase();
    upper.contains("BOOL")
}

/// Generate boundary value test cases for columns.
fn generate_boundary_tests(
    columns: &[String],
    schema: &SchemaDefinition,
    tables: &[String],
    test_cases: &mut Vec<TestCase>,
) {
    for col in columns {
        let data_type = match find_column_type(col, schema, tables) {
            Some(dt) => dt.to_string(),
            None => continue,
        };

        if is_numeric_type(&data_type) {
            let values = if data_type.to_uppercase().contains("BIGINT") {
                vec![
                    ("0", "zero value"),
                    ("1", "minimum positive"),
                    ("-1", "minimum negative"),
                    ("9223372036854775807", "BIGINT MAX"),
                    ("-9223372036854775808", "BIGINT MIN"),
                ]
            } else {
                vec![
                    ("0", "zero value"),
                    ("1", "minimum positive"),
                    ("-1", "minimum negative"),
                    ("2147483647", "INT MAX"),
                    ("-2147483648", "INT MIN"),
                ]
            };

            for (val, desc) in values {
                test_cases.push(TestCase {
                    name: format!("boundary_{col}_{}", val.replace('-', "neg")),
                    category: "boundary".to_string(),
                    description: format!("Test {col} with {desc} ({val})"),
                    inputs: vec![TestInput {
                        column: col.clone(),
                        value: val.to_string(),
                        data_type: data_type.clone(),
                    }],
                    expected: TestExpectation::ReturnsRows,
                });
            }
        }

        if is_string_type(&data_type) {
            test_cases.push(TestCase {
                name: format!("boundary_{col}_empty"),
                category: "boundary".to_string(),
                description: format!("Test {col} with empty string"),
                inputs: vec![TestInput {
                    column: col.clone(),
                    value: "''".to_string(),
                    data_type: data_type.clone(),
                }],
                expected: TestExpectation::ReturnsRows,
            });

            test_cases.push(TestCase {
                name: format!("boundary_{col}_long"),
                category: "boundary".to_string(),
                description: format!("Test {col} with very long string (1000 chars)"),
                inputs: vec![TestInput {
                    column: col.clone(),
                    value: format!("'{}'", "a".repeat(1000)),
                    data_type: data_type.clone(),
                }],
                expected: TestExpectation::ReturnsRows,
            });
        }

        if is_boolean_type(&data_type) {
            for val in &["TRUE", "FALSE"] {
                test_cases.push(TestCase {
                    name: format!("boundary_{col}_{}", val.to_lowercase()),
                    category: "boundary".to_string(),
                    description: format!("Test {col} with {val}"),
                    inputs: vec![TestInput {
                        column: col.clone(),
                        value: val.to_string(),
                        data_type: data_type.clone(),
                    }],
                    expected: TestExpectation::ReturnsRows,
                });
            }
        }
    }
}

/// Generate NULL handling test cases.
fn generate_null_tests(
    columns: &[String],
    schema: &SchemaDefinition,
    tables: &[String],
    test_cases: &mut Vec<TestCase>,
) {
    for col in columns {
        if !is_column_nullable(col, schema, tables) {
            continue;
        }

        let data_type = find_column_type(col, schema, tables)
            .unwrap_or("UNKNOWN")
            .to_string();

        test_cases.push(TestCase {
            name: format!("null_{col}"),
            category: "null".to_string(),
            description: format!("Test query when {col} is NULL"),
            inputs: vec![TestInput {
                column: col.clone(),
                value: "NULL".to_string(),
                data_type,
            }],
            expected: TestExpectation::ReturnsRows,
        });
    }
}

/// Generate empty table test cases.
fn generate_empty_table_tests(tables: &[String], test_cases: &mut Vec<TestCase>) {
    for table in tables {
        test_cases.push(TestCase {
            name: format!("empty_table_{table}"),
            category: "empty".to_string(),
            description: format!("Test query when {table} is empty (no rows)"),
            inputs: vec![],
            expected: TestExpectation::ReturnsEmpty,
        });
    }
}

/// Generate type edge case test cases.
fn generate_type_edge_tests(
    columns: &[String],
    schema: &SchemaDefinition,
    tables: &[String],
    test_cases: &mut Vec<TestCase>,
) {
    for col in columns {
        let data_type = match find_column_type(col, schema, tables) {
            Some(dt) => dt.to_string(),
            None => continue,
        };

        if is_decimal_type(&data_type) {
            let values = vec![
                ("0.00", "zero decimal"),
                ("99999999.99", "large decimal"),
                ("-0.01", "small negative decimal"),
                ("0.001", "precision boundary"),
            ];
            for (val, desc) in values {
                test_cases.push(TestCase {
                    name: format!("type_edge_{col}_{}", desc.replace(' ', "_")),
                    category: "type_edge".to_string(),
                    description: format!("Test {col} with {desc} ({val})"),
                    inputs: vec![TestInput {
                        column: col.clone(),
                        value: val.to_string(),
                        data_type: data_type.clone(),
                    }],
                    expected: TestExpectation::ReturnsRows,
                });
            }
        }

        if is_timestamp_type(&data_type) {
            let values = vec![
                ("'1970-01-01 00:00:00'", "epoch"),
                ("'2099-12-31 23:59:59'", "far future"),
                ("'2024-03-10 02:30:00'", "DST boundary (US spring forward)"),
                ("'2024-11-03 01:30:00'", "DST boundary (US fall back)"),
            ];
            for (val, desc) in values {
                test_cases.push(TestCase {
                    name: format!(
                        "type_edge_{col}_{}",
                        desc.split_whitespace()
                            .take(2)
                            .collect::<Vec<_>>()
                            .join("_")
                            .to_lowercase()
                    ),
                    category: "type_edge".to_string(),
                    description: format!("Test {col} with {desc}"),
                    inputs: vec![TestInput {
                        column: col.clone(),
                        value: val.to_string(),
                        data_type: data_type.clone(),
                    }],
                    expected: TestExpectation::ReturnsRows,
                });
            }
        }
    }
}

/// Generate WHERE clause test cases from conditions.
fn generate_where_tests(
    expr: &Expr,
    schema: &SchemaDefinition,
    tables: &[String],
    test_cases: &mut Vec<TestCase>,
) {
    match expr {
        Expr::BinaryOp { left, op, right } => {
            use sqlparser::ast::BinaryOperator;
            match op {
                BinaryOperator::Eq
                | BinaryOperator::NotEq
                | BinaryOperator::Gt
                | BinaryOperator::GtEq
                | BinaryOperator::Lt
                | BinaryOperator::LtEq => {
                    if let Some(col) = expr_to_column_name(left) {
                        let data_type = find_column_type(&col, schema, tables)
                            .unwrap_or("UNKNOWN")
                            .to_string();
                        let val_str = right.to_string();

                        // Test: matching value
                        test_cases.push(TestCase {
                            name: format!("where_{col}_match"),
                            category: "where_clause".to_string(),
                            description: format!(
                                "Test WHERE {col} {op} {val_str} with matching value"
                            ),
                            inputs: vec![TestInput {
                                column: col.clone(),
                                value: val_str.clone(),
                                data_type: data_type.clone(),
                            }],
                            expected: TestExpectation::ReturnsRows,
                        });

                        // Test: non-matching value
                        let non_match = generate_non_matching_value(&data_type, &val_str);
                        test_cases.push(TestCase {
                            name: format!("where_{col}_no_match"),
                            category: "where_clause".to_string(),
                            description: format!("Test WHERE {col} {op} with non-matching value"),
                            inputs: vec![TestInput {
                                column: col.clone(),
                                value: non_match,
                                data_type: data_type.clone(),
                            }],
                            expected: TestExpectation::ReturnsEmpty,
                        });

                        // Test: boundary value for comparisons
                        if matches!(
                            op,
                            BinaryOperator::Gt
                                | BinaryOperator::GtEq
                                | BinaryOperator::Lt
                                | BinaryOperator::LtEq
                        ) && is_numeric_type(&data_type)
                        {
                            test_cases.push(TestCase {
                                name: format!("where_{col}_boundary"),
                                category: "where_clause".to_string(),
                                description: format!(
                                    "Test WHERE {col} {op} with exact boundary value"
                                ),
                                inputs: vec![TestInput {
                                    column: col.clone(),
                                    value: val_str,
                                    data_type,
                                }],
                                expected: TestExpectation::ReturnsRows,
                            });
                        }
                    }
                }
                BinaryOperator::And | BinaryOperator::Or => {
                    generate_where_tests(left, schema, tables, test_cases);
                    generate_where_tests(right, schema, tables, test_cases);
                }
                _ => {}
            }
        }
        Expr::IsNull(inner) | Expr::IsNotNull(inner) => {
            if let Some(col) = expr_to_column_name(inner) {
                let data_type = find_column_type(&col, schema, tables)
                    .unwrap_or("UNKNOWN")
                    .to_string();
                test_cases.push(TestCase {
                    name: format!("where_{col}_null_check"),
                    category: "where_clause".to_string(),
                    description: format!("Test NULL check on {col}"),
                    inputs: vec![TestInput {
                        column: col,
                        value: "NULL".to_string(),
                        data_type,
                    }],
                    expected: TestExpectation::ReturnsRows,
                });
            }
        }
        Expr::Between {
            expr,
            low,
            high,
            negated,
            ..
        } => {
            if let Some(col) = expr_to_column_name(expr) {
                let data_type = find_column_type(&col, schema, tables)
                    .unwrap_or("UNKNOWN")
                    .to_string();
                let neg_str = if *negated { "NOT " } else { "" };
                test_cases.push(TestCase {
                    name: format!("where_{col}_between_low"),
                    category: "where_clause".to_string(),
                    description: format!("Test {neg_str}BETWEEN at low boundary for {col}"),
                    inputs: vec![TestInput {
                        column: col.clone(),
                        value: low.to_string(),
                        data_type: data_type.clone(),
                    }],
                    expected: TestExpectation::ReturnsRows,
                });
                test_cases.push(TestCase {
                    name: format!("where_{col}_between_high"),
                    category: "where_clause".to_string(),
                    description: format!("Test {neg_str}BETWEEN at high boundary for {col}"),
                    inputs: vec![TestInput {
                        column: col,
                        value: high.to_string(),
                        data_type,
                    }],
                    expected: TestExpectation::ReturnsRows,
                });
            }
        }
        Expr::Nested(inner) => {
            generate_where_tests(inner, schema, tables, test_cases);
        }
        _ => {}
    }
}

/// Generate JOIN coverage tests.
fn generate_join_tests(tables: &[String], test_cases: &mut Vec<TestCase>) {
    if tables.len() < 2 {
        return;
    }

    // Test: matching rows in join
    test_cases.push(TestCase {
        name: "join_matching_rows".to_string(),
        category: "join".to_string(),
        description: "Test JOIN with matching rows in both tables".to_string(),
        inputs: vec![],
        expected: TestExpectation::ReturnsRows,
    });

    // Test: no matching rows in join
    test_cases.push(TestCase {
        name: "join_no_matching_rows".to_string(),
        category: "join".to_string(),
        description: "Test JOIN with no matching rows (empty result for INNER JOIN)".to_string(),
        inputs: vec![],
        expected: TestExpectation::ReturnsEmpty,
    });

    // Test: multiple matches
    test_cases.push(TestCase {
        name: "join_multiple_matches".to_string(),
        category: "join".to_string(),
        description: "Test JOIN with multiple matching rows (one-to-many relationship)".to_string(),
        inputs: vec![],
        expected: TestExpectation::ReturnsRows,
    });
}

fn expr_to_column_name(expr: &Expr) -> Option<String> {
    match expr {
        Expr::Identifier(ident) => Some(ident.value.clone()),
        Expr::CompoundIdentifier(idents) => idents.last().map(|i| i.value.clone()),
        _ => None,
    }
}

fn generate_non_matching_value(data_type: &str, current_value: &str) -> String {
    if is_numeric_type(data_type) {
        // Try to produce a different numeric value
        if let Ok(n) = current_value.parse::<i64>() {
            return (n + 999999).to_string();
        }
        "999999".to_string()
    } else if is_string_type(data_type) {
        "'__nonexistent__'".to_string()
    } else if is_timestamp_type(data_type) {
        "'1900-01-01 00:00:00'".to_string()
    } else {
        "NULL".to_string()
    }
}
