//! Adversarial Security Hardening Tests
//!
//! Covers every known SQL injection, encoding attack, value type confusion,
//! and operator abuse vector against the Qail AST transpiler.
//!
//! Attack classifications:
//!   1. Classic SQL Injection (string, identifier, comment)
//!   2. LIKE/Pattern Injection (wildcard, backslash)
//!   3. Encoding Attacks (null byte, unicode, BOM, RTL)
//!   4. Value Type Confusion (NaN, infinity, overflow, empty)
//!   5. Operator Abuse (BETWEEN, EXISTS, IN, raw SQL hatch)
//!   6. Cross-Site via SQL (XSS, HTML entities)
//!   7. Combined / Multi-Vector attacks

use qail_core::ast::*;
use qail_core::transpiler::{ToSql, ToSqlParameterized};

// ============================================================================
// Helpers — same pattern as transpiler_correctness.rs
// ============================================================================

fn filter_cage(conditions: Vec<Condition>) -> Cage {
    Cage {
        kind: CageKind::Filter,
        conditions,
        logical_op: LogicalOp::And,
    }
}

fn payload_cage(conditions: Vec<Condition>) -> Cage {
    Cage {
        kind: CageKind::Payload,
        conditions,
        logical_op: LogicalOp::And,
    }
}

fn cond(col: &str, op: Operator, val: Value) -> Condition {
    Condition {
        left: Expr::Named(col.to_string()),
        op,
        value: val,
        is_array_unnest: false,
    }
}

fn select_where(table: &str, col: &str, op: Operator, val: Value) -> Qail {
    Qail {
        action: Action::Get,
        table: table.to_string(),
        cages: vec![filter_cage(vec![cond(col, op, val)])],
        ..Default::default()
    }
}

fn select_from(table: &str) -> Qail {
    Qail {
        action: Action::Get,
        table: table.to_string(),
        ..Default::default()
    }
}

// ============================================================================
// 1. CLASSIC SQL INJECTION
// ============================================================================

#[test]
fn injection_single_quote_in_string_value() {
    let cmd = select_where(
        "users",
        "name",
        Operator::Eq,
        Value::String("O'Brien".into()),
    );
    let sql = cmd.to_sql();
    assert!(
        sql.contains("O''Brien"),
        "Single quote not escaped: {}",
        sql
    );
    assert!(
        !sql.contains("O'Brien'"),
        "Unescaped quote breaks syntax: {}",
        sql
    );
}

#[test]
fn injection_double_single_quote_in_string() {
    let cmd = select_where(
        "users",
        "bio",
        Operator::Eq,
        Value::String("it''s fine".into()),
    );
    let sql = cmd.to_sql();
    assert!(
        sql.contains("it''''s fine"),
        "Double-escaped quotes: {}",
        sql
    );
}

#[test]
fn injection_semicolon_in_string_value() {
    let cmd = select_where(
        "users",
        "name",
        Operator::Eq,
        Value::String("alice'; DROP TABLE users; --".into()),
    );
    let sql = cmd.to_sql();
    assert!(
        sql.contains("alice''; DROP TABLE users; --"),
        "Semicolon injection not neutralized: {}",
        sql
    );
}

#[test]
fn injection_comment_in_string_value() {
    let cmd = select_where(
        "users",
        "name",
        Operator::Eq,
        Value::String("admin'--".into()),
    );
    let sql = cmd.to_sql();
    assert!(
        sql.contains("admin''--"),
        "Comment injection not escaped: {}",
        sql
    );
}

#[test]
fn injection_block_comment_in_string_value() {
    let cmd = select_where(
        "users",
        "name",
        Operator::Eq,
        Value::String("admin'/**/OR 1=1".into()),
    );
    let sql = cmd.to_sql();
    assert!(
        sql.contains("admin''/**/OR 1=1"),
        "Block comment injection: {}",
        sql
    );
}

#[test]
fn injection_union_select_in_string_value() {
    let cmd = select_where(
        "users",
        "name",
        Operator::Eq,
        Value::String("' UNION SELECT password FROM users --".into()),
    );
    let sql = cmd.to_sql();
    assert!(
        sql.contains("'' UNION SELECT"),
        "Union injection not escaped: {}",
        sql
    );
}

#[test]
fn injection_stacked_query_in_identifier() {
    let cmd = select_from("users; DROP TABLE orders");
    let sql = cmd.to_sql();
    assert!(
        sql.contains("\"users; DROP TABLE orders\""),
        "Semicolon in table not quoted: {}",
        sql
    );
}

#[test]
fn injection_double_quote_in_identifier() {
    let cmd = select_from("users\"inject");
    let sql = cmd.to_sql();
    assert!(
        sql.contains("\"users\"\"inject\""),
        "Double quote in identifier not escaped: {}",
        sql
    );
}

#[test]
fn injection_backslash_in_string_value() {
    let cmd = select_where(
        "users",
        "path",
        Operator::Eq,
        Value::String("C:\\Users\\admin".into()),
    );
    let sql = cmd.to_sql();
    assert!(
        sql.contains("C:\\Users\\admin"),
        "Backslash modified: {}",
        sql
    );
}

#[test]
fn injection_newline_in_string_value() {
    let cmd = select_where(
        "users",
        "bio",
        Operator::Eq,
        Value::String("line1\nline2".into()),
    );
    let sql = cmd.to_sql();
    assert!(sql.contains("line1\nline2"), "Newline modified: {}", sql);
}

#[test]
fn injection_crlf_injection() {
    let cmd = select_where(
        "users",
        "name",
        Operator::Eq,
        Value::String("alice\r\n' OR 1=1 --".into()),
    );
    let sql = cmd.to_sql();
    assert!(
        sql.contains("'' OR 1=1"),
        "CRLF injection not escaped: {}",
        sql
    );
}

#[test]
fn injection_empty_string_value() {
    let cmd = select_where("users", "name", Operator::Eq, Value::String("".into()));
    let sql = cmd.to_sql();
    assert!(
        sql.contains("''"),
        "Empty string not properly represented: {}",
        sql
    );
}

#[test]
fn injection_string_of_only_quotes() {
    let cmd = select_where("users", "name", Operator::Eq, Value::String("'''".into()));
    let sql = cmd.to_sql();
    assert!(
        sql.contains("''''''"),
        "Quote-only string not escaped: {}",
        sql
    );
}

#[test]
fn injection_dollar_quoting_attempt() {
    let cmd = select_where(
        "users",
        "name",
        Operator::Eq,
        Value::String("$$; DROP TABLE users; $$".into()),
    );
    let sql = cmd.to_sql();
    assert!(
        sql.contains("'$$; DROP TABLE users; $$'"),
        "Dollar quoting not contained: {}",
        sql
    );
}

#[test]
fn injection_repeated_escaping_idempotent() {
    let cmd = select_where("users", "name", Operator::Eq, Value::String("test".into()));
    let sql = cmd.to_sql();
    assert!(sql.contains("'test'"), "Simple string broken: {}", sql);
    assert!(!sql.contains("''test''"), "Over-escaped: {}", sql);
}

// ============================================================================
// 2. LIKE / PATTERN INJECTION
// ============================================================================

#[test]
fn like_wildcard_percent_in_value() {
    let cmd = select_where(
        "users",
        "name",
        Operator::Fuzzy,
        Value::String("100% complete".into()),
    );
    let sql = cmd.to_sql();
    assert!(
        sql.contains("ILIKE") || sql.contains("LIKE"),
        "Must use LIKE: {}",
        sql
    );
    assert!(sql.contains("100% complete"), "Value present: {}", sql);
}

#[test]
fn like_underscore_in_value() {
    let cmd = select_where(
        "users",
        "name",
        Operator::Fuzzy,
        Value::String("user_name".into()),
    );
    let sql = cmd.to_sql();
    assert!(sql.contains("user_name"), "Underscore preserved: {}", sql);
}

#[test]
fn like_quote_injection_in_fuzzy() {
    let cmd = select_where(
        "users",
        "name",
        Operator::Fuzzy,
        Value::String("admin' OR '1'='1".into()),
    );
    let sql = cmd.to_sql();
    assert!(
        sql.contains("admin'' OR ''1''=''1"),
        "ILIKE quote injection not escaped: {}",
        sql
    );
}

#[test]
fn like_backslash_escape_attempt() {
    let cmd = select_where(
        "users",
        "name",
        Operator::Fuzzy,
        Value::String("test\\' OR 1=1 --".into()),
    );
    let sql = cmd.to_sql();
    assert!(
        sql.contains("test\\'' OR 1=1 --"),
        "Backslash escape attack: {}",
        sql
    );
}

#[test]
fn like_very_long_pattern() {
    let long_pattern = "a".repeat(10_000);
    let cmd = select_where(
        "users",
        "name",
        Operator::Fuzzy,
        Value::String(long_pattern),
    );
    let sql = cmd.to_sql();
    assert!(
        sql.len() > 10_000,
        "Long pattern truncated: len={}",
        sql.len()
    );
}

// ============================================================================
// 3. ENCODING ATTACKS
// ============================================================================

#[test]
fn encoding_null_byte_in_identifier_preserved_for_downstream_rejection() {
    let cmd = select_from("users\0injected");
    let sql = cmd.to_sql();
    assert!(
        sql.as_bytes().contains(&0u8),
        "Null bytes must reach protocol/FFI rejection boundary"
    );
    assert!(
        !sql.contains("usersinjected"),
        "Name must not silently collide after stripping: {}",
        sql
    );
}

#[test]
fn encoding_null_byte_in_column_name() {
    let cmd = Qail {
        action: Action::Get,
        table: "users".to_string(),
        columns: vec![Expr::Named("id\0; DROP TABLE x".to_string())],
        ..Default::default()
    };
    let sql = cmd.to_sql();
    assert!(
        sql.as_bytes().contains(&0u8),
        "Null bytes must reach protocol/FFI rejection boundary"
    );
}

#[test]
fn encoding_utf8_bom_in_identifier() {
    let cmd = select_from("\u{FEFF}users");
    let sql = cmd.to_sql();
    assert!(
        sql.contains("users"),
        "Table name contains 'users': {}",
        sql
    );
}

#[test]
fn encoding_rtl_override_in_identifier() {
    let cmd = select_from("users\u{202E}admin");
    let sql = cmd.to_sql();
    assert!(
        sql.contains("\""),
        "RTL override must trigger quoting: {}",
        sql
    );
}

#[test]
fn encoding_zero_width_space_in_identifier() {
    let cmd = select_from("users\u{200B}");
    let sql = cmd.to_sql();
    assert!(
        sql.contains("\""),
        "Zero-width space must trigger quoting: {}",
        sql
    );
}

#[test]
fn encoding_homoglyph_in_identifier() {
    let cmd = select_from("us\u{0435}rs"); // Cyrillic 'е'
    let sql = cmd.to_sql();
    assert!(sql.contains("us\u{0435}rs"), "Homoglyph preserved: {}", sql);
}

#[test]
fn encoding_null_byte_in_string_value() {
    let cmd = select_where(
        "users",
        "name",
        Operator::Eq,
        Value::String("hello\0world".into()),
    );
    let sql = cmd.to_sql();
    assert!(sql.contains("hello"), "Value contains hello: {}", sql);
}

#[test]
fn encoding_mixed_unicode_scripts() {
    let cmd = select_where(
        "users",
        "name",
        Operator::Eq,
        Value::String("Hello 世界 🌍 مرحبا".into()),
    );
    let sql = cmd.to_sql();
    assert!(
        sql.contains("Hello 世界 🌍 مرحبا"),
        "Unicode preserved: {}",
        sql
    );
}

// ============================================================================
// 4. VALUE TYPE CONFUSION
// ============================================================================

#[test]
fn value_float_nan() {
    let cmd = select_where("stats", "val", Operator::Eq, Value::Float(f64::NAN));
    let sql = cmd.to_sql();
    assert!(sql.contains("NaN"), "NaN representable: {}", sql);
}

#[test]
fn value_float_infinity() {
    let cmd = select_where("stats", "val", Operator::Eq, Value::Float(f64::INFINITY));
    let sql = cmd.to_sql();
    assert!(sql.contains("inf"), "Infinity representable: {}", sql);
}

#[test]
fn value_float_neg_infinity() {
    let cmd = select_where(
        "stats",
        "val",
        Operator::Eq,
        Value::Float(f64::NEG_INFINITY),
    );
    let sql = cmd.to_sql();
    assert!(sql.contains("-inf"), "Negative infinity: {}", sql);
}

#[test]
fn value_float_negative_zero() {
    let cmd = select_where("stats", "val", Operator::Eq, Value::Float(-0.0));
    let sql = cmd.to_sql();
    assert!(sql.contains("0"), "Negative zero: {}", sql);
}

#[test]
fn value_int_max() {
    let cmd = select_where("users", "id", Operator::Eq, Value::Int(i64::MAX));
    let sql = cmd.to_sql();
    assert!(sql.contains("9223372036854775807"), "i64::MAX: {}", sql);
}

#[test]
fn value_int_min() {
    let cmd = select_where("users", "id", Operator::Eq, Value::Int(i64::MIN));
    let sql = cmd.to_sql();
    assert!(sql.contains("-9223372036854775808"), "i64::MIN: {}", sql);
}

#[test]
fn value_empty_string() {
    let cmd = select_where("users", "name", Operator::Eq, Value::String("".into()));
    let sql = cmd.to_sql();
    assert!(sql.contains("''"), "Empty string: {}", sql);
}

#[test]
fn value_whitespace_only_string() {
    let cmd = select_where("users", "name", Operator::Eq, Value::String("   ".into()));
    let sql = cmd.to_sql();
    assert!(sql.contains("'   '"), "Whitespace preserved: {}", sql);
}

// ============================================================================
// 5. OPERATOR ABUSE
// ============================================================================

#[test]
fn operator_between_with_injection() {
    let cmd = Qail {
        action: Action::Get,
        table: "orders".to_string(),
        cages: vec![filter_cage(vec![Condition {
            left: Expr::Named("price".to_string()),
            op: Operator::Between,
            value: Value::Array(vec![
                Value::String("0'; DROP TABLE orders; --".into()),
                Value::String("100".into()),
            ]),
            is_array_unnest: false,
        }])],
        ..Default::default()
    };
    let sql = cmd.to_sql();
    assert!(sql.contains("BETWEEN"), "Must contain BETWEEN: {}", sql);
    assert!(
        sql.contains("0''; DROP TABLE orders; --"),
        "BETWEEN bound injection not escaped: {}",
        sql
    );
}

#[test]
fn operator_between_rejects_wrong_arity() {
    let cmd = select_where(
        "orders",
        "price",
        Operator::Between,
        Value::Array(vec![Value::Int(100)]),
    );

    let sql = cmd.to_sql();

    assert!(
        sql.contains("FALSE /* ERROR: BETWEEN condition requires exactly two array values */"),
        "invalid BETWEEN must fail closed: {}",
        sql
    );
}

#[test]
fn operator_exists_with_subquery() {
    let subquery = Box::new(Qail {
        action: Action::Get,
        table: "orders".to_string(),
        columns: vec![Expr::Literal(Value::Int(1))],
        cages: vec![filter_cage(vec![cond(
            "status",
            Operator::Eq,
            Value::String("active".into()),
        )])],
        ..Default::default()
    });
    let cmd = Qail {
        action: Action::Get,
        table: "users".to_string(),
        cages: vec![filter_cage(vec![Condition {
            left: Expr::Named("id".to_string()),
            op: Operator::Exists,
            value: Value::Subquery(subquery),
            is_array_unnest: false,
        }])],
        ..Default::default()
    };
    let sql = cmd.to_sql();
    assert!(sql.contains("EXISTS"), "Must contain EXISTS: {}", sql);
    assert!(sql.contains("SELECT"), "Subquery must expand: {}", sql);
}

#[test]
fn operator_exists_rejects_non_subquery_value() {
    let cmd = select_where(
        "users",
        "id",
        Operator::Exists,
        Value::Function("SELECT 1); DROP TABLE users; --".into()),
    );

    let sql = cmd.to_sql();

    assert!(
        sql.contains("FALSE /* ERROR: EXISTS condition requires subquery value */"),
        "invalid EXISTS must fail closed: {}",
        sql
    );
    assert!(
        !sql.contains("DROP TABLE"),
        "invalid EXISTS value leaked into SQL: {}",
        sql
    );
}

#[test]
fn operator_in_rejects_mutating_subquery_value() {
    let mut subquery = Qail::del("audit_log");
    subquery = subquery.filter("tenant_id", Operator::Eq, Value::String("tenant_a".into()));

    let cmd = Qail {
        action: Action::Get,
        table: "users".to_string(),
        cages: vec![filter_cage(vec![Condition {
            left: Expr::Named("id".to_string()),
            op: Operator::In,
            value: Value::Subquery(Box::new(subquery)),
            is_array_unnest: false,
        }])],
        ..Default::default()
    };

    let sql = cmd.to_sql();

    assert!(
        sql.contains("/* ERROR: subquery must be read-only SELECT, got DEL */"),
        "mutating subquery must fail closed: {}",
        sql
    );
    assert!(
        !sql.contains("DELETE FROM"),
        "mutating subquery SQL leaked into parent query: {}",
        sql
    );
}

#[test]
fn operator_exists_rejects_mutating_subquery_parameterized() {
    let subquery = Qail::set("users").set_value("role", Value::String("admin".into()));
    let cmd = Qail {
        action: Action::Get,
        table: "sessions".to_string(),
        cages: vec![filter_cage(vec![Condition {
            left: Expr::Named("ignored".to_string()),
            op: Operator::Exists,
            value: Value::Subquery(Box::new(subquery)),
            is_array_unnest: false,
        }])],
        ..Default::default()
    };

    let result = cmd.to_sql_parameterized();

    assert!(
        result
            .sql
            .contains("/* ERROR: subquery must be read-only SELECT, got SET */"),
        "mutating parameterized subquery must fail closed: {}",
        result.sql
    );
    assert!(
        !result.sql.contains("UPDATE users"),
        "mutating parameterized subquery SQL leaked: {}",
        result.sql
    );
}

#[test]
fn condition_left_subquery_rejects_mutating_query() {
    let cmd = Qail {
        action: Action::Get,
        table: "users".to_string(),
        cages: vec![filter_cage(vec![Condition {
            left: Expr::Subquery {
                query: Box::new(Qail::del("sessions")),
                alias: None,
            },
            op: Operator::Eq,
            value: Value::Int(1),
            is_array_unnest: false,
        }])],
        ..Default::default()
    };

    let sql = cmd.to_sql();

    assert!(
        sql.contains("(/* ERROR: subquery must be read-only SELECT, got DEL */) = 1"),
        "mutating condition-left subquery must fail closed: {}",
        sql
    );
    assert!(
        !sql.contains("DELETE FROM"),
        "mutating condition-left subquery SQL leaked: {}",
        sql
    );
}

#[test]
fn operator_not_exists_rejects_non_subquery_value_parameterized() {
    let cmd = select_where(
        "users",
        "id",
        Operator::NotExists,
        Value::NamedParam("unsafe_subquery".into()),
    );

    let result = cmd.to_sql_parameterized();

    assert!(
        result
            .sql
            .contains("FALSE /* ERROR: EXISTS condition requires subquery value */"),
        "invalid NOT EXISTS must fail closed: {}",
        result.sql
    );
    assert!(
        result.params.is_empty() && result.named_params.is_empty(),
        "invalid NOT EXISTS must not bind a fake subquery"
    );
}

#[test]
fn operator_in_with_injection_values() {
    let cmd = Qail {
        action: Action::Get,
        table: "users".to_string(),
        cages: vec![filter_cage(vec![Condition {
            left: Expr::Named("role".to_string()),
            op: Operator::In,
            value: Value::Array(vec![
                Value::String("admin".into()),
                Value::String("user'; DROP TABLE users; --".into()),
            ]),
            is_array_unnest: false,
        }])],
        ..Default::default()
    };
    let sql = cmd.to_sql();
    assert!(
        sql.contains("user''; DROP TABLE users; --"),
        "IN value injection not escaped: {}",
        sql
    );
}

#[test]
fn operator_in_rejects_scalar_value() {
    let cmd = select_where(
        "users",
        "role",
        Operator::In,
        Value::String("admin".to_string()),
    );
    let sql = cmd.to_sql();

    assert!(
        sql.contains(
            "FALSE /* ERROR: IN condition requires a non-empty array, subquery, or array parameter */"
        ),
        "scalar IN must fail closed: {}",
        sql
    );
}

#[test]
fn select_cast_rejects_unsafe_target_type() {
    let cmd = Qail {
        action: Action::Get,
        table: "users".to_string(),
        columns: vec![Expr::Cast {
            expr: Box::new(Expr::Named("name".to_string())),
            target_type: "text; DROP TABLE users; --".to_string(),
            alias: None,
        }],
        ..Default::default()
    };
    let sql = cmd.to_sql();

    assert!(
        sql.contains("/* ERROR: Invalid cast target type */"),
        "unsafe cast target must fail closed: {}",
        sql
    );
    assert!(
        !sql.contains("DROP TABLE"),
        "unsafe cast target leaked into SQL: {}",
        sql
    );
}

#[test]
fn select_collate_and_field_access_quote_identifier_fragments() {
    let cmd = Qail {
        action: Action::Get,
        table: "users".to_string(),
        columns: vec![
            Expr::Collate {
                expr: Box::new(Expr::Named("name".to_string())),
                collation: "C\"; DROP TABLE users; --".to_string(),
                alias: Some("sorted".to_string()),
            },
            Expr::FieldAccess {
                expr: Box::new(Expr::Named("profile".to_string())),
                field: "field; DROP TABLE users; --".to_string(),
                alias: Some("field_value".to_string()),
            },
        ],
        ..Default::default()
    };
    let sql = cmd.to_sql();

    assert!(
        sql.contains("COLLATE \"C\"\"; DROP TABLE users; --\""),
        "collation identifier was not escaped: {}",
        sql
    );
    assert!(
        !sql.contains("COLLATE \"C\"; DROP"),
        "collation escaped identifier broke out of quotes: {}",
        sql
    );
    assert!(
        sql.contains(").\"field; DROP TABLE users; --\""),
        "field access identifier was not quoted: {}",
        sql
    );
    assert!(
        !sql.contains(").field; DROP"),
        "field access identifier broke out unquoted: {}",
        sql
    );
}

#[test]
fn insert_column_list_quotes_identifier_fragments() {
    let cmd = Qail {
        action: Action::Add,
        table: "users".to_string(),
        columns: vec![Expr::Named("name); DROP TABLE users; --".to_string())],
        cages: vec![payload_cage(vec![cond(
            "name",
            Operator::Eq,
            Value::String("Ada".to_string()),
        )])],
        returning: Some(vec![]),
        ..Default::default()
    };
    let sql = cmd.to_sql();

    assert!(
        sql.contains("\"name); DROP TABLE users; --\""),
        "insert column identifier was not quoted: {}",
        sql
    );
    assert!(
        !sql.contains("(name); DROP"),
        "insert column identifier broke out unquoted: {}",
        sql
    );
}

#[test]
fn insert_returning_rejects_unsafe_cast_target() {
    let cmd = Qail {
        action: Action::Add,
        table: "users".to_string(),
        columns: vec![Expr::Named("name".to_string())],
        cages: vec![payload_cage(vec![cond(
            "name",
            Operator::Eq,
            Value::String("Ada".to_string()),
        )])],
        returning: Some(vec![Expr::Cast {
            expr: Box::new(Expr::Named("name".to_string())),
            target_type: "text; DROP TABLE users; --".to_string(),
            alias: None,
        }]),
        ..Default::default()
    };
    let sql = cmd.to_sql();

    assert!(
        sql.contains("/* ERROR: Invalid cast target type */"),
        "unsafe INSERT RETURNING cast target must fail closed: {}",
        sql
    );
    assert!(
        !sql.contains("DROP TABLE"),
        "unsafe INSERT RETURNING cast target leaked into SQL: {}",
        sql
    );
}

#[test]
fn insert_conflict_assignment_escapes_collation_fragment() {
    let cmd = Qail::add("users")
        .set_value("id", 1)
        .set_value("name", "Ada")
        .on_conflict_update(
            &["id"],
            &[(
                "name",
                Expr::Collate {
                    expr: Box::new(Expr::Named("EXCLUDED.name".to_string())),
                    collation: "C\"; DROP TABLE users; --".to_string(),
                    alias: None,
                },
            )],
        );
    let sql = cmd.to_sql();

    assert!(
        sql.contains("COLLATE \"C\"\"; DROP TABLE users; --\""),
        "ON CONFLICT collation identifier was not escaped: {}",
        sql
    );
    assert!(
        !sql.contains("COLLATE \"C\"; DROP"),
        "ON CONFLICT collation identifier broke out of quotes: {}",
        sql
    );
}

#[test]
fn update_set_rejects_non_named_column_expression() {
    let cmd = Qail {
        action: Action::Set,
        table: "users".to_string(),
        cages: vec![payload_cage(vec![Condition {
            left: Expr::FieldAccess {
                expr: Box::new(Expr::Named("profile".to_string())),
                field: "field; DROP TABLE users; --".to_string(),
                alias: None,
            },
            op: Operator::Eq,
            value: Value::String("Ada".to_string()),
            is_array_unnest: false,
        }])],
        ..Default::default()
    };
    let sql = cmd.to_sql();

    assert!(
        sql.contains("/* ERROR: Invalid update column */"),
        "non-column UPDATE target must fail closed: {}",
        sql
    );
    assert!(
        !sql.contains(").field; DROP"),
        "non-column UPDATE target leaked raw expression: {}",
        sql
    );
}

#[test]
fn update_returning_rejects_unsafe_cast_target() {
    let cmd = Qail {
        action: Action::Set,
        table: "users".to_string(),
        cages: vec![payload_cage(vec![cond(
            "name",
            Operator::Eq,
            Value::String("Ada".to_string()),
        )])],
        returning: Some(vec![Expr::Cast {
            expr: Box::new(Expr::Named("name".to_string())),
            target_type: "text; DROP TABLE users; --".to_string(),
            alias: None,
        }]),
        ..Default::default()
    };
    let sql = cmd.to_sql();

    assert!(
        sql.contains("/* ERROR: Invalid cast target type */"),
        "unsafe UPDATE RETURNING cast target must fail closed: {}",
        sql
    );
    assert!(
        !sql.contains("DROP TABLE"),
        "unsafe UPDATE RETURNING cast target leaked into SQL: {}",
        sql
    );
}

#[test]
fn select_function_name_rejects_raw_sql_fragment() {
    let cmd = Qail {
        action: Action::Get,
        table: "users".to_string(),
        columns: vec![Expr::FunctionCall {
            name: "lower); DROP TABLE users; --".to_string(),
            args: vec![Expr::Named("name".to_string())],
            alias: None,
        }],
        ..Default::default()
    };
    let sql = cmd.to_sql();

    assert!(
        sql.contains("/* ERROR: Invalid function name */"),
        "unsafe SELECT function name must fail closed: {}",
        sql
    );
    assert!(
        !sql.contains("DROP TABLE"),
        "unsafe SELECT function name leaked into SQL: {}",
        sql
    );
}

#[test]
fn select_aliased_projection_escapes_alias_fragment() {
    let cmd = Qail {
        action: Action::Get,
        table: "users".to_string(),
        columns: vec![Expr::Aliased {
            name: "name".to_string(),
            alias: "display\"; DROP TABLE users; --".to_string(),
        }],
        ..Default::default()
    };
    let sql = cmd.to_sql();

    assert!(
        sql.contains("name AS \"display\"\"; DROP TABLE users; --\""),
        "SELECT alias identifier was not escaped: {}",
        sql
    );
    assert!(
        !sql.contains("AS \"display\"; DROP"),
        "SELECT alias identifier broke out of quotes: {}",
        sql
    );
}

#[test]
fn select_aggregate_filter_uses_structured_condition_renderer() {
    let cmd = Qail {
        action: Action::Get,
        table: "users".to_string(),
        columns: vec![Expr::Aggregate {
            col: "*".to_string(),
            func: AggregateFunc::Count,
            distinct: false,
            filter: Some(vec![Condition {
                left: Expr::FunctionCall {
                    name: "lower); DROP TABLE users; --".to_string(),
                    args: vec![Expr::Named("name".to_string())],
                    alias: None,
                },
                op: Operator::Eq,
                value: Value::String("ada".to_string()),
                is_array_unnest: false,
            }]),
            alias: Some("total".to_string()),
        }],
        ..Default::default()
    };
    let sql = cmd.to_sql();

    assert!(
        sql.contains("FILTER (WHERE /* ERROR: Invalid function name */ = 'ada')"),
        "aggregate filter must use the structured condition renderer: {}",
        sql
    );
    assert!(
        !sql.contains("DROP TABLE"),
        "unsafe aggregate filter expression leaked into SQL: {}",
        sql
    );
}

#[test]
fn select_projection_rejects_mutating_subquery() {
    let cmd = Qail {
        action: Action::Get,
        table: "users".to_string(),
        columns: vec![Expr::Subquery {
            query: Box::new(Qail::del("audit_log")),
            alias: Some("deleted_count".to_string()),
        }],
        ..Default::default()
    };
    let sql = cmd.to_sql();

    assert!(
        sql.contains("(/* ERROR: subquery must be read-only SELECT, got DEL */) AS deleted_count"),
        "mutating SELECT projection subquery must fail closed: {}",
        sql
    );
    assert!(
        !sql.contains("DELETE FROM"),
        "mutating SELECT projection subquery SQL leaked: {}",
        sql
    );
}

#[test]
fn select_order_by_rejects_mutating_subquery() {
    let cmd = Qail {
        action: Action::Get,
        table: "users".to_string(),
        cages: vec![Cage {
            kind: CageKind::Sort(SortOrder::Asc),
            conditions: vec![Condition {
                left: Expr::Subquery {
                    query: Box::new(Qail::set("audit_log").set_value("seen", Value::Bool(true))),
                    alias: None,
                },
                op: Operator::Eq,
                value: Value::Null,
                is_array_unnest: false,
            }],
            logical_op: LogicalOp::And,
        }],
        ..Default::default()
    };
    let sql = cmd.to_sql();

    assert!(
        sql.contains("ORDER BY (/* ERROR: subquery must be read-only SELECT, got SET */) ASC"),
        "mutating ORDER BY subquery must fail closed: {}",
        sql
    );
    assert!(
        !sql.contains("UPDATE audit_log"),
        "mutating ORDER BY subquery SQL leaked: {}",
        sql
    );
}

#[test]
fn select_order_by_special_function_name_rejects_raw_sql_fragment() {
    let cmd = Qail {
        action: Action::Get,
        table: "users".to_string(),
        cages: vec![Cage {
            kind: CageKind::Sort(SortOrder::Asc),
            conditions: vec![Condition {
                left: Expr::SpecialFunction {
                    name: "EXTRACT); DROP TABLE users; --".to_string(),
                    args: vec![(None, Box::new(Expr::Named("created_at".to_string())))],
                    alias: None,
                },
                op: Operator::Eq,
                value: Value::Null,
                is_array_unnest: false,
            }],
            logical_op: LogicalOp::And,
        }],
        ..Default::default()
    };
    let sql = cmd.to_sql();

    assert!(
        sql.contains("ORDER BY /* ERROR: Invalid function name */ ASC"),
        "unsafe ORDER BY special function name must fail closed: {}",
        sql
    );
    assert!(
        !sql.contains("DROP TABLE"),
        "unsafe ORDER BY special function name leaked into SQL: {}",
        sql
    );
}

#[test]
fn window_function_name_rejects_raw_sql_fragment() {
    let cmd = Qail {
        action: Action::Over,
        table: "users".to_string(),
        columns: vec![Expr::Window {
            name: "total".to_string(),
            func: "sum); DROP TABLE users; --".to_string(),
            params: vec![Expr::Named("amount".to_string())],
            partition: vec![],
            order: vec![],
            frame: None,
        }],
        ..Default::default()
    };
    let sql = cmd.to_sql();

    assert!(
        sql.contains("/* ERROR: Invalid window function name */"),
        "unsafe standalone window function name must fail closed: {}",
        sql
    );
    assert!(
        !sql.contains("DROP TABLE"),
        "unsafe standalone window function name leaked into SQL: {}",
        sql
    );
}

#[test]
fn window_param_rejects_unsafe_cast_target() {
    let cmd = Qail {
        action: Action::Over,
        table: "users".to_string(),
        columns: vec![Expr::Window {
            name: "total".to_string(),
            func: "sum".to_string(),
            params: vec![Expr::Cast {
                expr: Box::new(Expr::Named("amount".to_string())),
                target_type: "numeric); DROP TABLE users; --".to_string(),
                alias: None,
            }],
            partition: vec![],
            order: vec![],
            frame: None,
        }],
        ..Default::default()
    };
    let sql = cmd.to_sql();

    assert!(
        sql.contains("/* ERROR: Invalid cast target type */"),
        "unsafe standalone window parameter cast target must fail closed: {}",
        sql
    );
    assert!(
        !sql.contains("DROP TABLE"),
        "unsafe standalone window parameter cast target leaked into SQL: {}",
        sql
    );
}

#[test]
fn select_window_order_escapes_collation_fragment() {
    let cmd = Qail {
        action: Action::Get,
        table: "users".to_string(),
        columns: vec![Expr::Window {
            name: "rn".to_string(),
            func: "row_number".to_string(),
            params: vec![],
            partition: vec![],
            order: vec![Cage {
                kind: CageKind::Sort(SortOrder::Asc),
                conditions: vec![Condition {
                    left: Expr::Collate {
                        expr: Box::new(Expr::Named("name".to_string())),
                        collation: "C\"; DROP TABLE users; --".to_string(),
                        alias: None,
                    },
                    op: Operator::Eq,
                    value: Value::Null,
                    is_array_unnest: false,
                }],
                logical_op: LogicalOp::And,
            }],
            frame: None,
        }],
        ..Default::default()
    };
    let sql = cmd.to_sql();

    assert!(
        sql.contains("COLLATE \"C\"\"; DROP TABLE users; --\""),
        "window ORDER BY collation identifier was not escaped: {}",
        sql
    );
    assert!(
        !sql.contains("COLLATE \"C\"; DROP"),
        "window ORDER BY collation identifier broke out of quotes: {}",
        sql
    );
}

#[test]
fn condition_left_cast_rejects_unsafe_target_type() {
    let cmd = Qail {
        action: Action::Get,
        table: "users".to_string(),
        cages: vec![filter_cage(vec![Condition {
            left: Expr::Cast {
                expr: Box::new(Expr::Named("name".to_string())),
                target_type: "text); DROP TABLE users; --".to_string(),
                alias: None,
            },
            op: Operator::Eq,
            value: Value::String("Ada".to_string()),
            is_array_unnest: false,
        }])],
        ..Default::default()
    };
    let sql = cmd.to_sql();

    assert!(
        sql.contains("/* ERROR: Invalid cast target type */"),
        "unsafe condition cast target must fail closed: {}",
        sql
    );
    assert!(
        !sql.contains("DROP TABLE"),
        "unsafe condition cast target leaked into SQL: {}",
        sql
    );
}

#[test]
fn condition_left_function_name_rejects_raw_sql_fragment() {
    let cmd = Qail {
        action: Action::Get,
        table: "users".to_string(),
        cages: vec![filter_cage(vec![Condition {
            left: Expr::FunctionCall {
                name: "lower); DROP TABLE users; --".to_string(),
                args: vec![Expr::Named("name".to_string())],
                alias: None,
            },
            op: Operator::Eq,
            value: Value::String("ada".to_string()),
            is_array_unnest: false,
        }])],
        ..Default::default()
    };
    let sql = cmd.to_sql();

    assert!(
        sql.contains("/* ERROR: Invalid function name */"),
        "unsafe condition function name must fail closed: {}",
        sql
    );
    assert!(
        !sql.contains("DROP TABLE"),
        "unsafe condition function name leaked into SQL: {}",
        sql
    );
}

#[test]
fn condition_left_collate_escapes_identifier_fragment_parameterized() {
    let cmd = Qail {
        action: Action::Get,
        table: "users".to_string(),
        cages: vec![filter_cage(vec![Condition {
            left: Expr::Collate {
                expr: Box::new(Expr::Named("name".to_string())),
                collation: "C\"; DROP TABLE users; --".to_string(),
                alias: None,
            },
            op: Operator::Eq,
            value: Value::String("Ada".to_string()),
            is_array_unnest: false,
        }])],
        ..Default::default()
    };
    let result = cmd.to_sql_parameterized();

    assert!(
        result
            .sql
            .contains("COLLATE \"C\"\"; DROP TABLE users; --\""),
        "condition collation identifier was not escaped: {}",
        result.sql
    );
    assert!(
        !result.sql.contains("COLLATE \"C\"; DROP"),
        "condition collation identifier broke out of quotes: {}",
        result.sql
    );
}

#[test]
fn condition_value_expr_rejects_unsafe_cast_target() {
    let cmd = Qail {
        action: Action::Get,
        table: "users".to_string(),
        cages: vec![filter_cage(vec![Condition {
            left: Expr::Named("name".to_string()),
            op: Operator::Eq,
            value: Value::Expr(Box::new(Expr::Cast {
                expr: Box::new(Expr::Named("display_name".to_string())),
                target_type: "text); DROP TABLE users; --".to_string(),
                alias: None,
            })),
            is_array_unnest: false,
        }])],
        ..Default::default()
    };
    let sql = cmd.to_sql();

    assert!(
        sql.contains("/* ERROR: Invalid cast target type */"),
        "unsafe condition value expression cast target must fail closed: {}",
        sql
    );
    assert!(
        !sql.contains("DROP TABLE"),
        "unsafe condition value expression cast target leaked into SQL: {}",
        sql
    );
}

#[test]
fn operator_is_null_no_value_leak() {
    let cmd = select_where("users", "deleted_at", Operator::IsNull, Value::Null);
    let sql = cmd.to_sql();
    assert!(sql.contains("IS NULL"), "Must contain IS NULL: {}", sql);
    assert!(
        !sql.contains("IS NULL NULL"),
        "Value leaked into IS NULL: {}",
        sql
    );
}

#[test]
fn operator_not_in_with_empty_array() {
    let cmd = Qail {
        action: Action::Get,
        table: "users".to_string(),
        cages: vec![filter_cage(vec![Condition {
            left: Expr::Named("role".to_string()),
            op: Operator::NotIn,
            value: Value::Array(vec![]),
            is_array_unnest: false,
        }])],
        ..Default::default()
    };
    let sql = cmd.to_sql();
    assert!(
        sql.contains(
            "FALSE /* ERROR: IN condition requires a non-empty array, subquery, or array parameter */"
        ),
        "empty NOT IN must fail closed: {}",
        sql
    );
}

#[test]
fn raw_sql_escape_hatch_documented() {
    let cmd = Qail {
        action: Action::Get,
        table: "users".to_string(),
        cages: vec![filter_cage(vec![Condition {
            left: Expr::Named("{age > 18 AND verified = true}".to_string()),
            op: Operator::Eq,
            value: Value::Null,
            is_array_unnest: false,
        }])],
        ..Default::default()
    };
    let sql = cmd.to_sql();
    assert!(sql.contains("age > 18"), "Raw SQL included: {}", sql);
}

// ============================================================================
// 6. CROSS-SITE VIA SQL
// ============================================================================

#[test]
fn xss_script_tag_in_value() {
    let cmd = select_where(
        "users",
        "name",
        Operator::Eq,
        Value::String("<script>alert('xss')</script>".into()),
    );
    let sql = cmd.to_sql();
    assert!(
        sql.contains("alert(''xss'')"),
        "XSS quotes not escaped: {}",
        sql
    );
}

#[test]
fn xss_html_entities_in_identifier() {
    let cmd = select_from("users&amp;admin");
    let sql = cmd.to_sql();
    assert!(
        sql.contains("users&amp;admin") || sql.contains("\"users&amp;admin\""),
        "HTML entities preserved: {}",
        sql
    );
}

#[test]
fn xss_css_injection_in_value() {
    let cmd = select_where(
        "users",
        "style",
        Operator::Eq,
        Value::String("background:url(javascript:alert(1))".into()),
    );
    let sql = cmd.to_sql();
    assert!(
        sql.contains("background:url(javascript:alert(1))"),
        "CSS injection stored: {}",
        sql
    );
}

// ============================================================================
// 7. COMBINED / MULTI-VECTOR ATTACKS
// ============================================================================

#[test]
fn combined_identifier_and_value_injection() {
    let cmd = select_where(
        "users\"; DROP TABLE x; --",
        "name",
        Operator::Eq,
        Value::String("'; DELETE FROM users; --".into()),
    );
    let sql = cmd.to_sql();
    assert!(
        sql.contains("\"users\"\"; DROP TABLE x; --\""),
        "Table injection not escaped: {}",
        sql
    );
    assert!(
        sql.contains("''; DELETE FROM users; --"),
        "Value injection not escaped: {}",
        sql
    );
}

#[test]
fn combined_join_injection() {
    let cmd = Qail {
        action: Action::Get,
        table: "orders".to_string(),
        joins: vec![Join {
            table: "users; DROP TABLE orders".to_string(),
            kind: JoinKind::Inner,
            on: Some(vec![cond(
                "orders.user_id",
                Operator::Eq,
                Value::Column("users; DROP TABLE orders.id".to_string()),
            )]),
            on_true: false,
        }],
        ..Default::default()
    };
    let sql = cmd.to_sql();
    assert!(
        sql.contains("\"users; DROP TABLE orders\""),
        "JOIN table injection not quoted: {}",
        sql
    );
}

#[test]
fn combined_update_payload_injection() {
    let cmd = Qail {
        action: Action::Set,
        table: "users".to_string(),
        cages: vec![
            payload_cage(vec![cond(
                "name",
                Operator::Eq,
                Value::String("admin'; UPDATE users SET role='superadmin' WHERE '1'='1".into()),
            )]),
            filter_cage(vec![cond("id", Operator::Eq, Value::Int(1))]),
        ],
        ..Default::default()
    };
    let sql = cmd.to_sql();
    assert!(sql.starts_with("UPDATE"), "Must be UPDATE: {}", sql);
    assert!(
        sql.contains("admin''; UPDATE users SET role=''superadmin'' WHERE ''1''=''1"),
        "UPDATE payload injection not escaped: {}",
        sql
    );
}

#[test]
fn combined_delete_where_injection() {
    let cmd = Qail {
        action: Action::Del,
        table: "orders".to_string(),
        cages: vec![filter_cage(vec![cond(
            "id",
            Operator::Eq,
            Value::String("1' OR '1'='1".into()),
        )])],
        ..Default::default()
    };
    let sql = cmd.to_sql();
    assert!(sql.starts_with("DELETE"), "Must be DELETE: {}", sql);
    assert!(
        sql.contains("1'' OR ''1''=''1"),
        "DELETE WHERE injection not escaped: {}",
        sql
    );
}

#[test]
fn combined_insert_values_injection() {
    let cmd = Qail {
        action: Action::Add,
        table: "users".to_string(),
        cages: vec![payload_cage(vec![cond(
            "name",
            Operator::Eq,
            Value::String("alice'), ('evil_user".into()),
        )])],
        ..Default::default()
    };
    let sql = cmd.to_sql();
    assert!(sql.contains("INSERT"), "Must be INSERT: {}", sql);
    assert!(
        sql.contains("alice''), (''evil_user"),
        "INSERT values injection not escaped: {}",
        sql
    );
}
