//! Integration tests for the SQL safety scanner module.
//!
//! Tests cover all 10 safety rules, dbt Jinja patterns,
//! Snowflake/BigQuery/Databricks-specific patterns,
//! false positive avoidance, and batch scanning.

use altimate_core::safety::{
    SafetyConfig, SafetyRule, SafetySeverity, is_safe, scan_batch, scan_sql,
};

// ═══════════════════════════════════════════════════════════════════════
// is_safe — clean SQL should pass
// ═══════════════════════════════════════════════════════════════════════

#[test]
fn safety_clean_simple_select() {
    assert!(is_safe("SELECT id, name FROM users WHERE id = 1"));
}

#[test]
fn safety_clean_join() {
    assert!(is_safe(
        "SELECT u.name, o.total FROM users u JOIN orders o ON u.id = o.user_id WHERE u.id = 5"
    ));
}

#[test]
fn safety_clean_subquery() {
    assert!(is_safe(
        "SELECT * FROM users WHERE id IN (SELECT user_id FROM orders WHERE total > 100)"
    ));
}

#[test]
fn safety_clean_aggregation() {
    assert!(is_safe(
        "SELECT department, COUNT(*) FROM employees GROUP BY department HAVING COUNT(*) > 5"
    ));
}

#[test]
fn safety_clean_cte() {
    assert!(is_safe(
        "WITH active AS (SELECT * FROM users WHERE active = 1) SELECT * FROM active"
    ));
}

#[test]
fn safety_clean_window_function() {
    assert!(is_safe(
        "SELECT id, ROW_NUMBER() OVER (PARTITION BY dept ORDER BY salary DESC) FROM employees"
    ));
}

#[test]
fn safety_clean_like_pattern() {
    assert!(is_safe("SELECT * FROM users WHERE name LIKE '%smith%'"));
}

#[test]
fn safety_clean_between() {
    assert!(is_safe(
        "SELECT * FROM orders WHERE created_at BETWEEN '2024-01-01' AND '2024-12-31'"
    ));
}

#[test]
fn safety_clean_case_when() {
    assert!(is_safe(
        "SELECT CASE WHEN status = 1 THEN 'active' ELSE 'inactive' END FROM users"
    ));
}

#[test]
fn safety_clean_exists_subquery() {
    assert!(is_safe(
        "SELECT * FROM users WHERE EXISTS (SELECT 1 FROM orders WHERE orders.user_id = users.id)"
    ));
}

#[test]
fn safety_clean_null_check() {
    assert!(is_safe("SELECT * FROM users WHERE deleted_at IS NULL"));
}

#[test]
fn safety_clean_boolean_or() {
    assert!(is_safe(
        "SELECT * FROM users WHERE role = 'admin' OR role = 'superadmin'"
    ));
}

#[test]
fn safety_clean_numeric_comparison() {
    assert!(is_safe("SELECT * FROM users WHERE age > 18 OR age = 0"));
}

// ═══════════════════════════════════════════════════════════════════════
// is_safe — injection patterns should be caught
// ═══════════════════════════════════════════════════════════════════════

#[test]
fn safety_catches_drop_table() {
    assert!(!is_safe("SELECT 1; DROP TABLE users"));
}

#[test]
fn safety_catches_tautology_1_eq_1() {
    assert!(!is_safe("SELECT * FROM users WHERE id = 1 OR 1=1"));
}

#[test]
fn safety_catches_union_injection() {
    assert!(!is_safe(
        "SELECT id FROM users UNION SELECT password FROM admin"
    ));
}

#[test]
fn safety_catches_sleep_attack() {
    assert!(!is_safe("SELECT * FROM users WHERE id = 1 AND SLEEP(5)"));
}

#[test]
fn safety_catches_comment_injection() {
    assert!(!is_safe("SELECT * FROM users WHERE name = 'admin' --"));
}

#[test]
fn safety_catches_info_schema() {
    assert!(!is_safe("SELECT * FROM information_schema.tables"));
}

#[test]
fn safety_catches_encoding_bypass() {
    assert!(!is_safe("SELECT CHAR(65)"));
}

#[test]
fn safety_catches_transaction_escape() {
    assert!(!is_safe("COMMIT"));
}

#[test]
fn safety_catches_delete() {
    assert!(!is_safe("DELETE FROM users"));
}

#[test]
fn safety_catches_update() {
    assert!(!is_safe("UPDATE users SET role = 'admin'"));
}

// ═══════════════════════════════════════════════════════════════════════
// scan_sql — detailed threat findings
// ═══════════════════════════════════════════════════════════════════════

#[test]
fn safety_scan_clean_sql_no_threats() {
    let config = SafetyConfig::default();
    let result = scan_sql("SELECT id FROM users WHERE id = 1", &config);
    assert!(result.safe);
    assert!(result.threats.is_empty());
    assert_eq!(result.risk_score, 0.0);
}

#[test]
fn safety_scan_drop_injection_details() {
    let config = SafetyConfig::default();
    let result = scan_sql("SELECT 1; DROP TABLE users", &config);
    assert!(!result.safe);
    assert!(result.risk_score > 0.5);
    assert!(result.statement_count >= 2);
}

#[test]
fn safety_scan_tautology_has_rule() {
    let config = SafetyConfig::default();
    let result = scan_sql("SELECT * FROM users WHERE id = 1 OR 1=1", &config);
    assert!(
        result
            .threats
            .iter()
            .any(|t| t.rule == SafetyRule::TautologyAttack)
    );
}

#[test]
fn safety_scan_multiple_threats() {
    let config = SafetyConfig::default();
    let result = scan_sql(
        "SELECT * FROM users WHERE 1=0 OR 1=1 UNION SELECT password FROM admin; DROP TABLE users",
        &config,
    );
    assert!(!result.safe);
    assert!(result.threats.len() >= 3);
    assert_eq!(result.risk_score, 1.0);
}

#[test]
fn safety_scan_disallowed_statement_type() {
    let config = SafetyConfig::default();
    let result = scan_sql("DELETE FROM users WHERE id = 1", &config);
    assert!(!result.safe);
    assert!(result.threats.iter().any(|t| t.matched_pattern == "DELETE"));
}

// ═══════════════════════════════════════════════════════════════════════
// scan_sql — config customization
// ═══════════════════════════════════════════════════════════════════════

#[test]
fn safety_config_allow_insert() {
    let config = SafetyConfig {
        allowed_statements: vec!["SELECT".to_string(), "INSERT".to_string()],
        ..Default::default()
    };
    let result = scan_sql("INSERT INTO logs(msg) VALUES ('hello')", &config);
    let disallowed = result
        .threats
        .iter()
        .any(|t| t.rule == SafetyRule::MultiStatement && t.matched_pattern == "INSERT");
    assert!(!disallowed);
}

#[test]
fn safety_config_only_tautology_rule() {
    let config = SafetyConfig {
        rules: vec![SafetyRule::TautologyAttack],
        allow_multi_statement: true,
        allowed_statements: vec!["SELECT".to_string(), "INSERT".to_string()],
        ..Default::default()
    };
    let result = scan_sql("SELECT 1; SELECT 2", &config);
    assert!(result.safe);
}

#[test]
fn safety_config_severity_filter() {
    let config = SafetyConfig {
        min_severity: SafetySeverity::Critical,
        ..Default::default()
    };
    let result = scan_sql("SELECT * FROM users WHERE name = 'a' || 'b'", &config);
    let concat_findings: Vec<_> = result
        .threats
        .iter()
        .filter(|t| t.rule == SafetyRule::StringConcatPredicate)
        .collect();
    assert!(concat_findings.is_empty());
}

// ═══════════════════════════════════════════════════════════════════════
// scan_batch — batch scanning
// ═══════════════════════════════════════════════════════════════════════

#[test]
fn safety_batch_scan_mixed() {
    let config = SafetyConfig::default();
    let results = scan_batch(
        &[
            "SELECT 1",
            "SELECT 1; DROP TABLE users",
            "SELECT * FROM users WHERE 1=1 OR 1=1",
        ],
        &config,
    );
    assert_eq!(results.len(), 3);
    assert!(results[0].safe);
    assert!(!results[1].safe);
    assert!(!results[2].safe);
}

#[test]
fn safety_batch_scan_empty() {
    let config = SafetyConfig::default();
    let results = scan_batch(&[], &config);
    assert!(results.is_empty());
}

#[test]
fn safety_batch_scan_all_clean() {
    let config = SafetyConfig::default();
    let results = scan_batch(
        &[
            "SELECT id FROM users",
            "SELECT name FROM users WHERE id = 1",
            "SELECT COUNT(*) FROM orders",
        ],
        &config,
    );
    assert!(results.iter().all(|r| r.safe));
}

// ═══════════════════════════════════════════════════════════════════════
// dbt Jinja patterns
// ═══════════════════════════════════════════════════════════════════════

#[test]
fn safety_jinja_ref_expression_detected() {
    assert!(!is_safe("SELECT * FROM {{ ref('stg_users') }}"));
}

#[test]
fn safety_jinja_source_expression_detected() {
    assert!(!is_safe("SELECT * FROM {{ source('raw', 'users') }}"));
}

#[test]
fn safety_jinja_config_expression_detected() {
    assert!(!is_safe(
        "SELECT * FROM {{ config.get('malicious_table') }}"
    ));
}

#[test]
fn safety_jinja_statement_detected() {
    assert!(!is_safe(
        "{% set table = 'users' %}SELECT * FROM {{ table }}"
    ));
}

#[test]
fn safety_jinja_if_block_detected() {
    assert!(!is_safe("{% if true %}SELECT 1{% endif %}"));
}

#[test]
fn safety_jinja_for_loop_detected() {
    assert!(!is_safe(
        "{% for i in range(5) %}SELECT {{ i }};{% endfor %}"
    ));
}

#[test]
fn safety_jinja_macro_call_detected() {
    assert!(!is_safe("SELECT * FROM {{ generate_schema_name() }}"));
}

#[test]
fn safety_jinja_statement_critical_severity() {
    let config = SafetyConfig::default();
    let result = scan_sql("{% set x = 1 %}SELECT {{ x }}", &config);
    let jinja_threats: Vec<_> = result
        .threats
        .iter()
        .filter(|t| t.message.contains("Jinja"))
        .collect();
    assert!(!jinja_threats.is_empty());
    assert!(
        jinja_threats
            .iter()
            .any(|t| t.severity == SafetySeverity::Critical)
    );
}

// ═══════════════════════════════════════════════════════════════════════
// Snowflake-specific patterns
// ═══════════════════════════════════════════════════════════════════════

#[test]
fn safety_snowflake_system_function_probe() {
    assert!(!is_safe("SELECT SYSTEM$TYPEOF(current_version())"));
}

#[test]
fn safety_snowflake_system_current_user() {
    assert!(!is_safe("SELECT SYSTEM$CURRENT_USER()"));
}

#[test]
fn safety_snowflake_copy_into_blocked_by_default() {
    // COPY INTO is not SELECT, should be blocked
    assert!(!is_safe("COPY INTO @my_stage FROM users"));
}

#[test]
fn safety_snowflake_create_stage_blocked() {
    assert!(!is_safe("CREATE STAGE my_stage"));
}

// ═══════════════════════════════════════════════════════════════════════
// BigQuery-specific patterns
// ═══════════════════════════════════════════════════════════════════════

#[test]
fn safety_bigquery_merge_blocked_by_default() {
    assert!(!is_safe(
        "MERGE INTO target USING source ON target.id = source.id WHEN MATCHED THEN UPDATE SET name = source.name"
    ));
}

// ═══════════════════════════════════════════════════════════════════════
// Databricks-specific patterns
// ═══════════════════════════════════════════════════════════════════════

#[test]
fn safety_databricks_show_tables_probe() {
    let config = SafetyConfig::default();
    let result = scan_sql("SHOW TABLES", &config);
    assert!(
        result
            .threats
            .iter()
            .any(|t| t.rule == SafetyRule::InfoSchemaProbe)
    );
}

#[test]
fn safety_databricks_show_databases_probe() {
    let config = SafetyConfig::default();
    let result = scan_sql("SHOW DATABASES", &config);
    assert!(
        result
            .threats
            .iter()
            .any(|t| t.rule == SafetyRule::InfoSchemaProbe)
    );
}

#[test]
fn safety_databricks_describe_table_probe() {
    let config = SafetyConfig::default();
    let result = scan_sql("DESCRIBE TABLE users", &config);
    assert!(
        result
            .threats
            .iter()
            .any(|t| t.rule == SafetyRule::InfoSchemaProbe)
    );
}

// ═══════════════════════════════════════════════════════════════════════
// Complex real-world injection payloads
// ═══════════════════════════════════════════════════════════════════════

#[test]
fn safety_classic_auth_bypass() {
    assert!(!is_safe(
        "SELECT * FROM users WHERE username = 'admin' OR 1=1 --"
    ));
}

#[test]
fn safety_classic_union_exfil() {
    assert!(!is_safe(
        "SELECT id FROM products WHERE id = 1 UNION SELECT password FROM users"
    ));
}

#[test]
fn safety_classic_stacked_drop() {
    assert!(!is_safe("SELECT * FROM users; DROP TABLE users; --"));
}

#[test]
fn safety_time_based_blind() {
    assert!(!is_safe(
        "SELECT * FROM users WHERE id = 1 AND (SELECT SLEEP(5) FROM dual)"
    ));
}

#[test]
fn safety_pg_sleep_blind() {
    assert!(!is_safe(
        "SELECT * FROM users WHERE id = 1 AND pg_sleep(10) IS NOT NULL"
    ));
}

#[test]
fn safety_grant_privilege_escalation() {
    assert!(!is_safe(
        "SELECT 1; GRANT ALL PRIVILEGES ON *.* TO 'attacker'@'%'"
    ));
}

#[test]
fn safety_hex_encoded_injection() {
    assert!(!is_safe("SELECT * FROM users WHERE id = 0x61646D696E"));
}

#[test]
fn safety_url_encoded_injection() {
    assert!(!is_safe("SELECT%20*%20FROM%20users%27"));
}

#[test]
fn safety_waitfor_blind() {
    assert!(!is_safe(
        "SELECT * FROM users WHERE id = 1; WAITFOR DELAY '00:00:10'"
    ));
}

// ═══════════════════════════════════════════════════════════════════════
// Serialization
// ═══════════════════════════════════════════════════════════════════════

#[test]
fn safety_scan_result_serializes() {
    let config = SafetyConfig::default();
    let result = scan_sql("SELECT 1; DROP TABLE users", &config);
    let json = serde_json::to_string(&result).expect("should serialize");
    assert!(json.contains("safe"));
    assert!(json.contains("threats"));
    assert!(json.contains("risk_score"));
}

#[test]
fn safety_config_serializes() {
    let config = SafetyConfig::default();
    let json = serde_json::to_string(&config).expect("should serialize");
    assert!(json.contains("rules"));
    assert!(json.contains("min_severity"));
}
