//! Command injection vulnerability extractor.
//!
//! Detects patterns where OS commands are constructed using untrusted input
//! or executed through shell interpreters, which leads to command injection vulnerabilities.

use regex::Regex;
use stemedb_core::types::ObjectValue;

use super::Extractor;
use crate::types::{ExtractedClaim, Language};

/// Extractor for command injection vulnerabilities.
///
/// Detects patterns indicating unsafe command execution:
/// - Shell execution with string interpolation
/// - subprocess with shell=True in Python
/// - Command::new with format! in Rust
/// - exec/system calls with user input
pub struct CommandInjectionExtractor {
    // Rust patterns
    rust_command_format: Regex,
    rust_shell_cmd: Regex,

    // Go patterns
    go_exec_command: Regex,
    go_shell_exec: Regex,

    // Python patterns
    python_shell_true: Regex,
    python_os_system: Regex,
    python_os_popen: Regex,

    // JavaScript/Node patterns
    js_exec: Regex,
    js_exec_sync: Regex,
    js_spawn_shell: Regex,
}

impl Default for CommandInjectionExtractor {
    fn default() -> Self {
        Self::new()
    }
}

impl CommandInjectionExtractor {
    /// Create a new command injection extractor with compiled regexes.
    ///
    /// # Panics
    /// Panics if any regex pattern is invalid (programmer error).
    #[allow(clippy::expect_used)]
    pub fn new() -> Self {
        Self {
            // Rust: Command::new with format!
            rust_command_format: Regex::new(r#"Command::new\s*\(\s*format!\s*\("#)
                .expect("valid regex"),
            // Rust: shell command execution
            rust_shell_cmd: Regex::new(r#"Command::new\s*\(\s*["'](?:sh|bash|cmd|powershell)"#)
                .expect("valid regex"),

            // Go: exec.Command with shell
            go_exec_command: Regex::new(r#"exec\.Command\s*\(\s*["'](?:sh|bash|cmd)"#)
                .expect("valid regex"),
            // Go: shell execution with -c flag
            go_shell_exec: Regex::new(r#"exec\.Command\s*\([^)]*,\s*["']-c["']"#)
                .expect("valid regex"),

            // Python: subprocess with shell=True
            python_shell_true: Regex::new(r"subprocess\.\w+\s*\([^)]*shell\s*=\s*True")
                .expect("valid regex"),
            // Python: os.system (always uses shell)
            python_os_system: Regex::new(r"os\.system\s*\(").expect("valid regex"),
            // Python: os.popen (uses shell)
            python_os_popen: Regex::new(r"os\.popen\s*\(").expect("valid regex"),

            // JavaScript: child_process.exec (uses shell)
            // Match either:
            // - child_process.exec(...) - explicit module reference
            // - exec(...) preceded by non-word, non-dot char (destructured import)
            // This avoids matching RegExp.exec(), obj.exec(), etc.
            js_exec: Regex::new(r"(?:child_process\.exec|(?:^|[^\w.])exec)\s*\(")
                .expect("valid regex"),
            js_exec_sync: Regex::new(r"(?:child_process\.execSync|(?:^|[^\w.])execSync)\s*\(")
                .expect("valid regex"),
            // JavaScript: spawn with shell option
            js_spawn_shell: Regex::new(r"spawn\s*\([^)]*shell\s*:\s*true").expect("valid regex"),
        }
    }

    fn check_pattern_command(
        &self,
        content: &str,
        pattern: &Regex,
        path_segments: &[String],
        file: &str,
        description: &str,
    ) -> Vec<ExtractedClaim> {
        let mut claims = Vec::new();

        for (line_idx, line) in content.lines().enumerate() {
            if let Some(matched) = pattern.find(line) {
                let mut concept_path = path_segments.to_vec();
                concept_path.push("os".to_string());
                concept_path.push("command".to_string());
                concept_path.push("input".to_string());

                claims.push(ExtractedClaim {
                    concept_path: format!("code://{}", concept_path.join("/")),
                    predicate: "input_source".to_string(),
                    value: ObjectValue::Text("untrusted".to_string()),
                    file: file.to_string(),
                    line: line_idx + 1,
                    matched_text: matched.as_str().to_string(),
                    confidence: 0.85, // High but allow for safe usage patterns
                    description: description.to_string(),
                });
            }
        }

        claims
    }

    fn check_pattern_shell(
        &self,
        content: &str,
        pattern: &Regex,
        path_segments: &[String],
        file: &str,
        description: &str,
    ) -> Vec<ExtractedClaim> {
        let mut claims = Vec::new();

        for (line_idx, line) in content.lines().enumerate() {
            if let Some(matched) = pattern.find(line) {
                let mut concept_path = path_segments.to_vec();
                concept_path.push("os".to_string());
                concept_path.push("shell_mode".to_string());

                claims.push(ExtractedClaim {
                    concept_path: format!("code://{}", concept_path.join("/")),
                    predicate: "enabled".to_string(),
                    value: ObjectValue::Boolean(true),
                    file: file.to_string(),
                    line: line_idx + 1,
                    matched_text: matched.as_str().to_string(),
                    confidence: 0.9,
                    description: description.to_string(),
                });
            }
        }

        claims
    }
}

impl Extractor for CommandInjectionExtractor {
    fn name(&self) -> &str {
        "command_injection"
    }

    fn languages(&self) -> &[Language] {
        &[
            Language::Rust,
            Language::Go,
            Language::Python,
            Language::TypeScript,
            Language::JavaScript,
        ]
    }

    fn extract(
        &self,
        path_segments: &[String],
        content: &str,
        language: Language,
        file: &str,
    ) -> Vec<ExtractedClaim> {
        let mut claims = Vec::new();

        match language {
            Language::Rust => {
                claims.extend(self.check_pattern_command(
                    content,
                    &self.rust_command_format,
                    path_segments,
                    file,
                    "Command::new with format! may allow command injection",
                ));
                claims.extend(self.check_pattern_shell(
                    content,
                    &self.rust_shell_cmd,
                    path_segments,
                    file,
                    "Shell execution through sh/bash allows command injection",
                ));
            }
            Language::Go => {
                claims.extend(self.check_pattern_shell(
                    content,
                    &self.go_exec_command,
                    path_segments,
                    file,
                    "exec.Command with shell binary allows command injection",
                ));
                claims.extend(self.check_pattern_shell(
                    content,
                    &self.go_shell_exec,
                    path_segments,
                    file,
                    "Shell execution with -c flag allows command injection",
                ));
            }
            Language::Python => {
                claims.extend(self.check_pattern_shell(
                    content,
                    &self.python_shell_true,
                    path_segments,
                    file,
                    "subprocess with shell=True allows command injection",
                ));
                claims.extend(self.check_pattern_shell(
                    content,
                    &self.python_os_system,
                    path_segments,
                    file,
                    "os.system() executes through shell - command injection risk",
                ));
                claims.extend(self.check_pattern_shell(
                    content,
                    &self.python_os_popen,
                    path_segments,
                    file,
                    "os.popen() executes through shell - command injection risk",
                ));
            }
            Language::TypeScript | Language::JavaScript => {
                claims.extend(self.check_pattern_shell(
                    content,
                    &self.js_exec,
                    path_segments,
                    file,
                    "child_process.exec() uses shell - command injection risk",
                ));
                claims.extend(self.check_pattern_shell(
                    content,
                    &self.js_exec_sync,
                    path_segments,
                    file,
                    "child_process.execSync() uses shell - command injection risk",
                ));
                claims.extend(self.check_pattern_shell(
                    content,
                    &self.js_spawn_shell,
                    path_segments,
                    file,
                    "spawn with shell:true allows command injection",
                ));
            }
            _ => {}
        }

        claims
    }
}

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

    #[test]
    fn test_python_shell_true() {
        let extractor = CommandInjectionExtractor::new();
        let content = r#"
            import subprocess
            subprocess.run(user_input, shell=True)
        "#;

        let claims =
            extractor.extract(&["python".to_string()], content, Language::Python, "run.py");

        assert_eq!(claims.len(), 1);
        assert_eq!(claims[0].value, ObjectValue::Boolean(true));
        assert!(claims[0].concept_path.contains("shell_mode"));
    }

    #[test]
    fn test_python_os_system() {
        let extractor = CommandInjectionExtractor::new();
        let content = r#"
            import os
            os.system(f"ls {user_input}")
        "#;

        let claims =
            extractor.extract(&["python".to_string()], content, Language::Python, "run.py");

        assert_eq!(claims.len(), 1);
    }

    #[test]
    fn test_js_exec() {
        let extractor = CommandInjectionExtractor::new();
        let content = r#"
            const { exec } = require('child_process');
            exec(`ls ${userInput}`, callback);
        "#;

        let claims =
            extractor.extract(&["js".to_string()], content, Language::JavaScript, "run.js");

        assert_eq!(claims.len(), 1);
    }

    #[test]
    fn test_rust_command_with_shell() {
        let extractor = CommandInjectionExtractor::new();
        let content = r#"
            use std::process::Command;
            Command::new("sh")
                .arg("-c")
                .arg(user_input)
                .output()?;
        "#;

        let claims = extractor.extract(&["rust".to_string()], content, Language::Rust, "run.rs");

        assert_eq!(claims.len(), 1);
    }

    #[test]
    fn test_go_exec_with_shell() {
        let extractor = CommandInjectionExtractor::new();
        let content = r#"
            cmd := exec.Command("bash", "-c", userInput)
        "#;

        let claims = extractor.extract(&["go".to_string()], content, Language::Go, "run.go");

        assert_eq!(claims.len(), 2); // Matches both patterns
    }

    #[test]
    fn test_no_false_positives_safe_spawn() {
        let extractor = CommandInjectionExtractor::new();
        // Safe spawn without shell
        let content = r#"
            const { spawn } = require('child_process');
            spawn('ls', ['-la', directory]);
        "#;

        let claims =
            extractor.extract(&["js".to_string()], content, Language::JavaScript, "run.js");

        // Should not flag safe spawn patterns
        assert!(claims.is_empty());
    }

    #[test]
    fn test_no_false_positives_subprocess_list() {
        let extractor = CommandInjectionExtractor::new();
        // Safe subprocess.run without shell
        let content = r#"
            import subprocess
            subprocess.run(['ls', '-la', directory], check=True)
        "#;

        let claims =
            extractor.extract(&["python".to_string()], content, Language::Python, "run.py");

        // Should not flag when shell=True is not present
        assert!(claims.is_empty());
    }

    #[test]
    fn test_no_false_positives_regexp_exec() {
        let extractor = CommandInjectionExtractor::new();
        // RegExp.exec() is not command execution - should not trigger
        let content = r#"
            const pattern = /\d+/g;
            const result = pattern.exec(input);

            // Also test with explicit new RegExp
            const regex = new RegExp("\\d+", "g");
            const match = regex.exec(str);
        "#;

        let claims =
            extractor.extract(&["js".to_string()], content, Language::JavaScript, "regex.js");

        // Should not flag RegExp.exec() calls
        assert!(
            claims.is_empty(),
            "RegExp.exec() should not trigger command injection detection, but got {} claims",
            claims.len()
        );
    }

    #[test]
    fn test_detects_standalone_exec() {
        let extractor = CommandInjectionExtractor::new();
        // Bare exec() and child_process.exec() should still be detected
        let content = r#"
            const { exec, execSync } = require('child_process');
            exec(command, callback);
            execSync(command);
            child_process.exec(command);
            child_process.execSync(command);
        "#;

        let claims =
            extractor.extract(&["js".to_string()], content, Language::JavaScript, "run.js");

        // Should detect: exec(), execSync(), child_process.exec(), child_process.execSync()
        assert_eq!(
            claims.len(),
            4,
            "Should detect standalone exec/execSync and child_process.exec/execSync"
        );
    }
}
