//! Safety scanning engine for SQL injection detection.
//!
//! Provides `scan_sql`, `is_safe`, and `scan_batch` functions that analyze
//! SQL strings for injection patterns using regex-based detection rules.
//! No SQL execution occurs — this is purely static analysis.

use super::rules;
use super::types::{SafetyConfig, SafetyRule, SafetyScanResult, SafetySeverity, ThreatFinding};

/// Perform a full safety scan of a SQL query.
///
/// Runs all enabled rules from the config and returns detailed findings.
///
/// # Arguments
/// * `sql` - The SQL string to scan
/// * `config` - Configuration controlling which rules to run and severity threshold
///
/// # Returns
/// A `SafetyScanResult` with threat findings, risk score, and statement metadata.
pub fn scan_sql(sql: &str, config: &SafetyConfig) -> SafetyScanResult {
    let mut all_threats: Vec<ThreatFinding> = Vec::new();

    for rule in &config.rules {
        let findings = run_rule(*rule, sql);
        all_threats.extend(findings);
    }

    // Also check for Jinja injection (always, regardless of rules — it's a meta-rule)
    let jinja_findings = rules::check_jinja_injection(sql);
    all_threats.extend(jinja_findings);

    // Filter by severity threshold
    all_threats.retain(|t| t.severity >= config.min_severity);

    // Extract statement info
    let (statement_count, statement_types) = extract_statement_info(sql);

    // Check allowed_statements: if any statement type is not in the allowed list, add a finding
    if !config.allow_multi_statement && statement_count > 1 {
        // StackedQueries rule would have caught this, but ensure it's flagged
        let already_flagged = all_threats
            .iter()
            .any(|t| t.rule == SafetyRule::StackedQueries);
        if !already_flagged {
            all_threats.push(ThreatFinding {
                rule: SafetyRule::StackedQueries,
                severity: SafetySeverity::High,
                message: format!(
                    "Multiple statements detected ({statement_count}) but multi-statement is disabled"
                ),
                detail: "The safety config disallows multi-statement SQL."
                    .to_string(),
                location: None,
                matched_pattern: format!("{statement_count} statements"),
            });
        }
    }

    // Check for disallowed statement types
    for st in &statement_types {
        let upper = st.to_uppercase();
        if !config
            .allowed_statements
            .iter()
            .any(|a| a.to_uppercase() == upper)
        {
            all_threats.push(ThreatFinding {
                rule: SafetyRule::MultiStatement,
                severity: SafetySeverity::Critical,
                message: format!("Disallowed statement type: {upper}"),
                detail: format!(
                    "Statement type '{}' is not in the allowed list: {:?}",
                    upper, config.allowed_statements
                ),
                location: None,
                matched_pattern: upper,
            });
        }
    }

    let risk_score = compute_risk_score(&all_threats);
    let safe = all_threats.is_empty();

    SafetyScanResult {
        safe,
        threats: all_threats,
        risk_score,
        statement_count,
        statement_types,
    }
}

/// Quick safety check with strict defaults (all rules, minimum severity = Medium).
///
/// Returns `true` if the SQL passes all safety checks.
pub fn is_safe(sql: &str) -> bool {
    let config = SafetyConfig::default();
    let result = scan_sql(sql, &config);
    result.safe
}

/// Scan a batch of SQL queries and return results for each.
pub fn scan_batch(queries: &[&str], config: &SafetyConfig) -> Vec<SafetyScanResult> {
    queries.iter().map(|sql| scan_sql(sql, config)).collect()
}

/// Run a single detection rule against a SQL string.
fn run_rule(rule: SafetyRule, sql: &str) -> Vec<ThreatFinding> {
    match rule {
        SafetyRule::MultiStatement => rules::check_multi_statement(sql),
        SafetyRule::UnionInjection => rules::check_union_injection(sql),
        SafetyRule::CommentInjection => rules::check_comment_injection(sql),
        SafetyRule::TransactionEscape => rules::check_transaction_escape(sql),
        SafetyRule::StackedQueries => rules::check_stacked_queries(sql),
        SafetyRule::TautologyAttack => rules::check_tautology_attack(sql),
        SafetyRule::SleepAttack => rules::check_sleep_attack(sql),
        SafetyRule::InfoSchemaProbe => rules::check_info_schema_probe(sql),
        SafetyRule::StringConcatPredicate => rules::check_string_concat_predicate(sql),
        SafetyRule::EncodingBypass => rules::check_encoding_bypass(sql),
    }
}

/// Compute risk score from 0.0 to 1.0 based on threat findings.
fn compute_risk_score(threats: &[ThreatFinding]) -> f64 {
    if threats.is_empty() {
        return 0.0;
    }

    let max_severity = threats
        .iter()
        .map(|t| match t.severity {
            SafetySeverity::Low => 0.25,
            SafetySeverity::Medium => 0.5,
            SafetySeverity::High => 0.75,
            SafetySeverity::Critical => 1.0,
        })
        .fold(0.0_f64, f64::max);

    // Increase score with more findings, capped at 1.0
    let count_factor = (threats.len() as f64 * 0.1).min(0.25);

    (max_severity + count_factor).min(1.0)
}

/// Extract statement count and types from SQL.
fn extract_statement_info(sql: &str) -> (usize, Vec<String>) {
    let stripped = rules::strip_string_literals(sql);
    let parts: Vec<&str> = stripped
        .split(';')
        .map(|s| s.trim())
        .filter(|s| !s.is_empty())
        .collect();

    let count = parts.len();
    let types: Vec<String> = parts
        .iter()
        .filter_map(|s| {
            let first_word = s.split_whitespace().next()?;
            Some(first_word.to_uppercase())
        })
        .collect();

    (count, types)
}

#[cfg(test)]
mod tests {
    use super::*;

    // =========================================================================
    // scan_sql integration tests
    // =========================================================================

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

    #[test]
    fn test_scan_drop_injection() {
        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.threats.len() >= 2); // MultiStatement + StackedQueries + disallowed stmt
    }

    #[test]
    fn test_scan_union_injection() {
        let config = SafetyConfig::default();
        let result = scan_sql(
            "SELECT id FROM users WHERE name = 'x' UNION SELECT password FROM admin",
            &config,
        );
        assert!(!result.safe);
        assert!(
            result
                .threats
                .iter()
                .any(|t| t.rule == SafetyRule::UnionInjection)
        );
    }

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

    #[test]
    fn test_scan_sleep_injection() {
        let config = SafetyConfig::default();
        let result = scan_sql("SELECT * FROM users WHERE id = 1 AND SLEEP(5)", &config);
        assert!(!result.safe);
        assert!(
            result
                .threats
                .iter()
                .any(|t| t.rule == SafetyRule::SleepAttack)
        );
        assert_eq!(result.risk_score, 1.0); // Critical severity
    }

    #[test]
    fn test_scan_multiple_threats() {
        let config = SafetyConfig::default();
        let result = scan_sql(
            "SELECT * FROM users WHERE id = 1 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 test_scan_comment_truncation() {
        let config = SafetyConfig::default();
        let result = scan_sql("SELECT * FROM users WHERE name = 'admin' --", &config);
        assert!(!result.safe);
    }

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

    #[test]
    fn test_scan_allowed_non_select() {
        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);
        // INSERT is allowed, so no disallowed-statement finding
        let disallowed = result
            .threats
            .iter()
            .any(|t| t.rule == SafetyRule::MultiStatement && t.matched_pattern == "INSERT");
        assert!(!disallowed);
    }

    // =========================================================================
    // is_safe tests
    // =========================================================================

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

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

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

    #[test]
    fn test_is_safe_sleep() {
        assert!(!is_safe("SELECT SLEEP(5)"));
    }

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

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

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

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

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

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

    // =========================================================================
    // scan_batch tests
    // =========================================================================

    #[test]
    fn test_scan_batch_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 test_scan_batch_empty() {
        let config = SafetyConfig::default();
        let results = scan_batch(&[], &config);
        assert!(results.is_empty());
    }

    // =========================================================================
    // Risk score tests
    // =========================================================================

    #[test]
    fn test_risk_score_zero_for_clean() {
        let score = compute_risk_score(&[]);
        assert_eq!(score, 0.0);
    }

    #[test]
    fn test_risk_score_critical() {
        let threats = vec![ThreatFinding {
            rule: SafetyRule::MultiStatement,
            severity: SafetySeverity::Critical,
            message: "test".to_string(),
            detail: "test".to_string(),
            location: None,
            matched_pattern: "test".to_string(),
        }];
        let score = compute_risk_score(&threats);
        assert!(score >= 1.0);
    }

    #[test]
    fn test_risk_score_medium() {
        let threats = vec![ThreatFinding {
            rule: SafetyRule::EncodingBypass,
            severity: SafetySeverity::Medium,
            message: "test".to_string(),
            detail: "test".to_string(),
            location: None,
            matched_pattern: "test".to_string(),
        }];
        let score = compute_risk_score(&threats);
        assert!(score > 0.4 && score < 0.8);
    }

    // =========================================================================
    // Statement info extraction tests
    // =========================================================================

    #[test]
    fn test_extract_single_select() {
        let (count, types) = extract_statement_info("SELECT 1");
        assert_eq!(count, 1);
        assert_eq!(types, vec!["SELECT"]);
    }

    #[test]
    fn test_extract_multiple_statements() {
        let (count, types) = extract_statement_info("SELECT 1; DROP TABLE users");
        assert_eq!(count, 2);
        assert_eq!(types, vec!["SELECT", "DROP"]);
    }

    #[test]
    fn test_extract_with_trailing_semi() {
        let (count, types) = extract_statement_info("SELECT 1;");
        assert_eq!(count, 1);
        assert_eq!(types, vec!["SELECT"]);
    }

    #[test]
    fn test_extract_empty() {
        let (count, types) = extract_statement_info("");
        assert_eq!(count, 0);
        assert!(types.is_empty());
    }

    // =========================================================================
    // Config tests
    // =========================================================================

    #[test]
    fn test_default_config() {
        let config = SafetyConfig::default();
        assert_eq!(config.rules.len(), 10);
        assert_eq!(config.min_severity, SafetySeverity::Medium);
        assert!(!config.allow_multi_statement);
        assert_eq!(config.allowed_statements, vec!["SELECT", "WITH"]);
    }

    #[test]
    fn test_custom_config_only_select_rules() {
        let config = SafetyConfig {
            rules: vec![SafetyRule::TautologyAttack],
            min_severity: SafetySeverity::High,
            allow_multi_statement: true,
            allowed_statements: vec![
                "SELECT".to_string(),
                "INSERT".to_string(),
                "UPDATE".to_string(),
                "DELETE".to_string(),
            ],
        };
        // Only tautology rule active — multi-statement should not be flagged as injection
        let result = scan_sql("SELECT 1; SELECT 2", &config);
        // No tautology, multi-statement allowed, all statement types allowed
        assert!(result.safe);
    }

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

    // =========================================================================
    // Complex real-world injection payloads
    // =========================================================================

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

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

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

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

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

    #[test]
    fn test_error_based_with_info_schema() {
        assert!(!is_safe(
            "SELECT table_name FROM information_schema.tables WHERE table_schema = 'public'"
        ));
    }

    #[test]
    fn test_second_order_with_concat() {
        assert!(!is_safe(
            "SELECT * FROM users WHERE name = CONCAT('admin', CHAR(39))"
        ));
    }

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

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

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

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

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

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

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

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

    // =========================================================================
    // False positive resistance tests — clean SQL should NOT trigger
    // =========================================================================

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

    #[test]
    fn test_fp_select_with_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 test_fp_select_with_subquery() {
        assert!(is_safe(
            "SELECT * FROM users WHERE id IN (SELECT user_id FROM orders WHERE total > 100)"
        ));
    }

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

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

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

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

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

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

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

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

    #[test]
    fn test_fp_boolean_or_condition() {
        // Normal OR condition should NOT trigger tautology
        assert!(is_safe(
            "SELECT * FROM users WHERE role = 'admin' OR role = 'superadmin'"
        ));
    }

    #[test]
    fn test_fp_numeric_comparison() {
        // Normal numeric comparison should NOT trigger tautology
        assert!(is_safe("SELECT * FROM users WHERE age > 18 OR age = 0"));
    }
}
