// tests/table_script/type_safety_comprehensive_tests.rs

use crate::common::setup_isolated_db;
use server::table_script::handlers::post_table_script::post_table_script;
use common::proto::komp_ac::table_script::PostTableScriptRequest;
use rstest::*;
use serde_json::json;
use sqlx::PgPool;

/// Test fixture for allowed mathematical types
#[fixture]
#[once]
pub fn allowed_math_types() -> Vec<(&'static str, &'static str)> {
    vec![
        ("basic_integer", "INTEGER"),
        ("small_numeric", "NUMERIC(5, 2)"),
        ("currency_numeric", "NUMERIC(14, 4)"),
        ("high_precision", "NUMERIC(28, 15)"),
        ("zero_scale", "NUMERIC(10, 0)"),
        ("percentage", "NUMERIC(5, 4)"),
        ("scientific", "NUMERIC(25, 15)"),
    ]
}

/// Test fixture for prohibited mathematical types
#[fixture]
#[once]
pub fn prohibited_math_types() -> Vec<(&'static str, &'static str)> {
    vec![
        ("text_field", "TEXT"),
        ("boolean_field", "BOOLEAN"),
        ("bigint_field", "BIGINT"),
        ("date_field", "DATE"),
        ("timestamp_field", "TIMESTAMPTZ"),
    ]
}

/// Test fixture for prohibited target types
#[fixture]
#[once]
pub fn prohibited_target_types() -> Vec<(&'static str, &'static str)> {
    vec![
        ("bigint_target", "BIGINT"),
        ("date_target", "DATE"),
        ("timestamp_target", "TIMESTAMPTZ"),
    ]
}

/// Test fixture for basic mathematical operations
#[fixture]
#[once]
pub fn basic_math_operations() -> Vec<(&'static str, &'static str)> {
    vec![
        ("+", "addition"),
        ("-", "subtraction"),
        ("*", "multiplication"),
        ("/", "division"),
    ]
}

/// Test fixture for advanced mathematical operations
#[fixture]
#[once]
pub fn advanced_math_operations() -> Vec<(&'static str, &'static str)> {
    vec![
        ("sqrt", "square root"),
        ("abs", "absolute value"),
        ("min", "minimum"),
        ("max", "maximum"),
        ("pow", "power"),
    ]
}

/// Helper function to create a test table with specified columns
async fn create_test_table(
    pool: &PgPool,
    schema_id: i64,
    table_name: &str,
    columns: Vec<(&str, &str)>,
) -> i64 {
    let column_definitions: Vec<String> = columns
        .iter()
        .map(|(name, type_def)| format!("\"{}\" {}", name, type_def))
        .collect();

    let columns_json = json!(column_definitions);
    let indexes_json = json!([]);

    sqlx::query_scalar!(
        r#"INSERT INTO table_definitions (schema_id, table_name, columns, indexes)
           VALUES ($1, $2, $3, $4) RETURNING id"#,
        schema_id,
        table_name,
        columns_json,
        indexes_json
    )
    .fetch_one(pool)
    .await
    .expect("Failed to create test table")
}

/// Helper function to get default schema ID
async fn get_default_schema_id(pool: &PgPool) -> i64 {
    sqlx::query_scalar!("SELECT id FROM schemas WHERE name = 'default'")
        .fetch_one(pool)
        .await
        .expect("Failed to get default schema ID")
}

#[rstest]
#[case::integer_addition("basic_integer", "+", "INTEGER addition should work")]
#[case::numeric_multiplication("currency_numeric", "*", "NUMERIC multiplication should work")]
#[case::high_precision_division("high_precision", "/", "High precision division should work")]
#[case::zero_scale_subtraction("zero_scale", "-", "Zero scale NUMERIC subtraction should work")]
#[tokio::test]
async fn test_allowed_types_in_math_operations(
    #[case] column_name: &str,
    #[case] operation: &str,
    #[case] description: &str,
    allowed_math_types: &Vec<(&'static str, &'static str)>,
) {
    let pool = setup_isolated_db().await;
    let schema_id = get_default_schema_id(&pool).await;

    // Create table with all allowed mathematical types plus a result column
    let mut columns = allowed_math_types.clone();
    columns.push(("result", "NUMERIC(30, 15)"));

    let table_id = create_test_table(&pool, schema_id, "math_test_table", columns).await;

    // Create script using the specified operation and column
    let script = match operation {
        "sqrt" | "abs" => {
            format!(
                r#"({} (steel_get_column "math_test_table" "{}"))"#,
                operation, column_name
            )
        }
        _ => {
            format!(
                r#"({} (steel_get_column "math_test_table" "{}") "10")"#,
                operation, column_name
            )
        }
    };

    let request = PostTableScriptRequest {
        table_definition_id: table_id,
        target_column: "result".to_string(),
        script,
        description: description.to_string(),
    };

    let result = post_table_script(&pool, request).await;
    assert!(result.is_ok(), "{}", description);
}

#[rstest]
#[case::text_addition("text_field", "+", "TEXT in addition should fail")]
#[case::boolean_multiplication("boolean_field", "*", "BOOLEAN in multiplication should fail")]
#[case::bigint_division("bigint_field", "/", "BIGINT in division should fail")]
#[case::date_subtraction("date_field", "-", "DATE in subtraction should fail")]
#[case::timestamp_sqrt("timestamp_field", "sqrt", "TIMESTAMPTZ in sqrt should fail")]
#[tokio::test]
async fn test_prohibited_types_in_math_operations(
    #[case] column_name: &str,
    #[case] operation: &str,
    #[case] description: &str,
    prohibited_math_types: &Vec<(&'static str, &'static str)>,
) {
    let pool = setup_isolated_db().await;
    let schema_id = get_default_schema_id(&pool).await;

    // Create table with prohibited types plus a valid result column
    let mut columns = prohibited_math_types.clone();
    columns.push(("result", "NUMERIC(15, 6)"));

    let table_id = create_test_table(&pool, schema_id, "prohibited_math_table", columns).await;

    // Create script using prohibited type in mathematical operation
    let script = match operation {
        "sqrt" | "abs" => {
            format!(
                r#"({} (steel_get_column "prohibited_math_table" "{}"))"#,
                operation, column_name
            )
        }
        _ => {
            format!(
                r#"({} (steel_get_column "prohibited_math_table" "{}") "10")"#,
                operation, column_name
            )
        }
    };

    let request = PostTableScriptRequest {
        table_definition_id: table_id,
        target_column: "result".to_string(),
        script,
        description: description.to_string(),
    };

    let result = post_table_script(&pool, request).await;
    assert!(result.is_err(), "{}", description);

    let error_message = result.unwrap_err().to_string();
    assert!(
        error_message.contains("mathematical operations") || error_message.contains("prohibited"),
        "Error should mention mathematical operations or prohibited types: {}",
        error_message
    );
}

#[rstest]
#[case::bigint_target("bigint_target", "BIGINT target should be rejected")]
#[case::date_target("date_target", "DATE target should be rejected")]
#[case::timestamp_target("timestamp_target", "TIMESTAMPTZ target should be rejected")]
#[tokio::test]
async fn test_prohibited_target_column_types(
    #[case] target_column: &str,
    #[case] description: &str,
    prohibited_target_types: &Vec<(&'static str, &'static str)>,
) {
    let pool = setup_isolated_db().await;
    let schema_id = get_default_schema_id(&pool).await;

    // Create table with prohibited target types plus some valid source columns
    let mut columns = prohibited_target_types.clone();
    columns.push(("amount", "NUMERIC(10, 2)"));

    let table_id = create_test_table(&pool, schema_id, "prohibited_target_table", columns).await;

    // Try to create script targeting prohibited column type
    let script = r#"(+ "10" "20")"#;

    let request = PostTableScriptRequest {
        table_definition_id: table_id,
        target_column: target_column.to_string(),
        script: script.to_string(),
        description: description.to_string(),
    };

    let result = post_table_script(&pool, request).await;
    assert!(result.is_err(), "{}", description);

    let error_message = result.unwrap_err().to_string();
    assert!(
        error_message.to_lowercase().contains("cannot create script") || 
        error_message.contains("prohibited type"),
        "Error should mention prohibited type: {}",
        error_message
    );
}

#[rstest]
#[case::system_id("id", "System column 'id' should be rejected")]
#[case::system_deleted("deleted", "System column 'deleted' should be rejected")]
#[case::system_created_at("created_at", "System column 'created_at' should be rejected")]
#[tokio::test]
async fn test_system_column_restrictions(#[case] target_column: &str, #[case] description: &str) {
    let pool = setup_isolated_db().await;
    let schema_id = get_default_schema_id(&pool).await;

    let columns = vec![("amount", "NUMERIC(10, 2)")];
    let table_id = create_test_table(&pool, schema_id, "system_test_table", columns).await;

    let script = r#"(+ "10" "20")"#;

    let request = PostTableScriptRequest {
        table_definition_id: table_id,
        target_column: target_column.to_string(),
        script: script.to_string(),
        description: description.to_string(),
    };

    let result = post_table_script(&pool, request).await;
    assert!(result.is_err(), "{}", description);

    let error_message = result.unwrap_err().to_string();
    assert!(
        error_message.contains("system column") || error_message.contains("cannot override"),
        "Error should mention system column: {}",
        error_message
    );
}

#[tokio::test]
async fn test_comprehensive_type_matrix() {
    let pool = setup_isolated_db().await;
    let schema_id = get_default_schema_id(&pool).await;

    // Create comprehensive table with all type categories
    let all_columns = vec![
        // Allowed math types
        ("integer_col", "INTEGER"),
        ("numeric_col", "NUMERIC(10, 2)"),
        ("high_precision", "NUMERIC(28, 15)"),
        
        // Prohibited math types
        ("text_col", "TEXT"),
        ("boolean_col", "BOOLEAN"),
        ("bigint_col", "BIGINT"),
        ("date_col", "DATE"),
        ("timestamp_col", "TIMESTAMPTZ"),
        
        // Result columns
        ("result_numeric", "NUMERIC(20, 8)"),
        ("result_text", "TEXT"),
    ];

    let table_id = create_test_table(&pool, schema_id, "comprehensive_table", all_columns).await;

    // Test matrix: [source_column, operation, target_column, should_succeed]
    let test_matrix = vec![
        // Valid combinations
        ("integer_col", "+", "result_numeric", true),
        ("numeric_col", "*", "result_numeric", true),
        ("high_precision", "/", "result_numeric", true),
        
        // Invalid source types in math
        ("text_col", "+", "result_numeric", false),
        ("boolean_col", "*", "result_numeric", false),
        ("bigint_col", "/", "result_numeric", false),
        ("date_col", "-", "result_numeric", false),
        ("timestamp_col", "+", "result_numeric", false),
    ];

    for (source_col, operation, target_col, should_succeed) in test_matrix {
        let script = format!(
            r#"({} (steel_get_column "comprehensive_table" "{}") "10")"#,
            operation, source_col
        );

        let request = PostTableScriptRequest {
            table_definition_id: table_id,
            target_column: target_col.to_string(),
            script,
            description: format!("Matrix test: {} {} -> {}", source_col, operation, target_col),
        };

        let result = post_table_script(&pool, request).await;

        if should_succeed {
            assert!(
                result.is_ok(),
                "Should succeed: {} {} -> {}",
                source_col, operation, target_col
            );
        } else {
            assert!(
                result.is_err(),
                "Should fail: {} {} -> {}",
                source_col, operation, target_col
            );
        }
    }
}

#[tokio::test]
async fn test_complex_mathematical_expressions() {
    let pool = setup_isolated_db().await;
    let schema_id = get_default_schema_id(&pool).await;

    let columns = vec![
        ("principal", "NUMERIC(16, 2)"),
        ("rate", "NUMERIC(6, 5)"),
        ("years", "INTEGER"),
        ("compound_result", "NUMERIC(20, 8)"),
    ];

    let table_id = create_test_table(&pool, schema_id, "financial_table", columns).await;

    // Complex compound interest calculation - all using allowed types
    let complex_script = r#"
        (* 
            (steel_get_column "financial_table" "principal")
            (pow 
                (+ "1" (steel_get_column "financial_table" "rate"))
                (steel_get_column "financial_table" "years")))
    "#;

    let request = PostTableScriptRequest {
        table_definition_id: table_id,
        target_column: "compound_result".to_string(),
        script: complex_script.to_string(),
        description: "Complex compound interest calculation".to_string(),
    };

    let result = post_table_script(&pool, request).await;
    assert!(result.is_ok(), "Complex mathematical expression with allowed types should succeed");
}

#[tokio::test]
async fn test_nonexistent_column_reference() {
    let pool = setup_isolated_db().await;
    let schema_id = get_default_schema_id(&pool).await;

    let columns = vec![
        ("amount", "NUMERIC(10, 2)"),
        ("result", "NUMERIC(10, 2)"),
    ];

    let table_id = create_test_table(&pool, schema_id, "simple_table", columns).await;

    let script = r#"(+ (steel_get_column "simple_table" "nonexistent_column") "10")"#;

    let request = PostTableScriptRequest {
        table_definition_id: table_id,
        target_column: "result".to_string(),
        script: script.to_string(),
        description: "Test nonexistent column".to_string(),
    };

    let result = post_table_script(&pool, request).await;
    assert!(result.is_err(), "Should reject script referencing nonexistent column");

    let error_message = result.unwrap_err().to_string();
    assert!(
        error_message.contains("does not exist") || error_message.contains("not found"),
        "Error should mention column not found: {}",
        error_message
    );
}

#[tokio::test]
async fn test_nonexistent_table_reference() {
    let pool = setup_isolated_db().await;
    let schema_id = get_default_schema_id(&pool).await;

    let columns = vec![
        ("amount", "NUMERIC(10, 2)"),
        ("result", "NUMERIC(10, 2)"),
    ];

    let table_id = create_test_table(&pool, schema_id, "existing_table", columns).await;

    let script = r#"(+ (steel_get_column "nonexistent_table" "amount") "10")"#;

    let request = PostTableScriptRequest {
        table_definition_id: table_id,
        target_column: "result".to_string(),
        script: script.to_string(),
        description: "Test nonexistent table".to_string(),
    };

    let result = post_table_script(&pool, request).await;
    assert!(result.is_err(), "Should reject script referencing nonexistent table");

    let error_message = result.unwrap_err().to_string();
    assert!(
        error_message.contains("does not exist") || error_message.contains("not found"),
        "Error should mention table not found: {}",
        error_message
    );
}
