//! Path traversal vulnerability extractor.
//!
//! Detects patterns where file system operations use user-controlled input
//! without proper validation, which can lead to directory traversal attacks.

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

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

/// Extractor for path traversal vulnerabilities.
///
/// Detects patterns indicating unsafe file path handling:
/// - File operations with user-controlled input
/// - Path.join/filepath.Join with request parameters
/// - sendFile/res.download with user input
/// - Direct traversal literals (../ or %2e%2e)
pub struct PathTraversalExtractor {
    // Python patterns
    python_open_user: Regex,
    python_path_join: Regex,

    // JavaScript/TypeScript patterns
    js_fs_user: Regex,
    js_path_join: Regex,
    js_sendfile: Regex,

    // Go patterns
    go_filepath: Regex,

    // Rust patterns
    rust_path_user: Regex,

    // Universal patterns
    traversal_literal: Regex,
}

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

impl PathTraversalExtractor {
    /// Create a new path traversal extractor with compiled regexes.
    ///
    /// # Panics
    /// Panics if any regex pattern is invalid (programmer error).
    #[allow(clippy::expect_used)]
    pub fn new() -> Self {
        Self {
            // Python: file ops with user input
            python_open_user: Regex::new(
                r#"(?:open|read|write)\s*\([^)]*(?:request\.|params\[|input|user)"#,
            )
            .expect("valid regex"),
            python_path_join: Regex::new(
                r#"os\.path\.join\s*\([^)]*(?:request\.|params\[|input|user)"#,
            )
            .expect("valid regex"),

            // JavaScript: fs/path with user input
            js_fs_user: Regex::new(
                r#"fs\.(?:readFile|writeFile|createReadStream|readFileSync|writeFileSync)\s*\([^)]*(?:req\.|params\.|query\.)"#,
            )
            .expect("valid regex"),
            js_path_join: Regex::new(
                r#"path\.(?:join|resolve)\s*\([^)]*(?:req\.|params\.|query\.)"#,
            )
            .expect("valid regex"),
            js_sendfile: Regex::new(r#"res\.(?:sendFile|download)\s*\([^)]*(?:req\.|params\.)"#)
                .expect("valid regex"),

            // Go: filepath with user input
            go_filepath: Regex::new(
                r#"(?:filepath\.Join|os\.Open|os\.ReadFile|ioutil\.ReadFile)\s*\([^)]*(?:r\.|req\.|c\.)"#,
            )
            .expect("valid regex"),

            // Rust: path operations with user input
            rust_path_user: Regex::new(
                r#"(?:Path::new|PathBuf::from|std::fs::read|std::fs::write)\s*\([^)]*(?:request|params|query|user)"#,
            )
            .expect("valid regex"),

            // Universal: direct traversal literals
            traversal_literal: Regex::new(r#"\.\.[\\/]|%2e%2e|%2E%2E"#).expect("valid regex"),
        }
    }

    fn make_claim(
        path_segments: &[String],
        file: &str,
        line: usize,
        matched: &str,
        category: &str,
        description: &str,
    ) -> ExtractedClaim {
        build_claim(
            path_segments,
            &["filesystem", "path", category],
            "user_controlled_path",
            ObjectValue::Boolean(true),
            file,
            line,
            matched,
            0.85,
            description,
        )
    }
}

impl Extractor for PathTraversalExtractor {
    fn name(&self) -> &str {
        "path_traversal"
    }

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

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

        for (line_idx, line) in content.lines().enumerate() {
            let line_num = line_idx + 1;

            // Check for traversal literals in any language
            if let Some(m) = self.traversal_literal.find(line) {
                claims.push(Self::make_claim(
                    path_segments,
                    file,
                    line_num,
                    m.as_str(),
                    "traversal",
                    "Path contains directory traversal sequence (../)",
                ));
            }

            // Language-specific patterns
            match language {
                Language::Python => {
                    if let Some(m) = self.python_open_user.find(line) {
                        claims.push(Self::make_claim(
                            path_segments,
                            file,
                            line_num,
                            m.as_str(),
                            "file_operation",
                            "File operation with user-controlled path (path traversal risk)",
                        ));
                    }
                    if let Some(m) = self.python_path_join.find(line) {
                        claims.push(Self::make_claim(
                            path_segments,
                            file,
                            line_num,
                            m.as_str(),
                            "path_construction",
                            "os.path.join with user input (path traversal risk)",
                        ));
                    }
                }
                Language::JavaScript | Language::TypeScript => {
                    if let Some(m) = self.js_fs_user.find(line) {
                        claims.push(Self::make_claim(
                            path_segments,
                            file,
                            line_num,
                            m.as_str(),
                            "file_operation",
                            "fs operation with user-controlled path (path traversal risk)",
                        ));
                    }
                    if let Some(m) = self.js_path_join.find(line) {
                        claims.push(Self::make_claim(
                            path_segments,
                            file,
                            line_num,
                            m.as_str(),
                            "path_construction",
                            "path.join/resolve with user input (path traversal risk)",
                        ));
                    }
                    if let Some(m) = self.js_sendfile.find(line) {
                        claims.push(Self::make_claim(
                            path_segments,
                            file,
                            line_num,
                            m.as_str(),
                            "file_serving",
                            "res.sendFile with user input (path traversal risk)",
                        ));
                    }
                }
                Language::Go => {
                    if let Some(m) = self.go_filepath.find(line) {
                        claims.push(Self::make_claim(
                            path_segments,
                            file,
                            line_num,
                            m.as_str(),
                            "file_operation",
                            "Filepath operation with user input (path traversal risk)",
                        ));
                    }
                }
                Language::Rust => {
                    if let Some(m) = self.rust_path_user.find(line) {
                        claims.push(Self::make_claim(
                            path_segments,
                            file,
                            line_num,
                            m.as_str(),
                            "file_operation",
                            "Path operation with user input (path traversal risk)",
                        ));
                    }
                }
                _ => {}
            }
        }

        claims
    }
}

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

    #[test]
    fn test_python_open_user_input() {
        let extractor = PathTraversalExtractor::new();
        let content = r#"
            file = open(request.GET['filename'], 'r')
        "#;

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

        assert_eq!(claims.len(), 1);
        assert!(claims[0].concept_path.contains("filesystem/path"));
    }

    #[test]
    fn test_python_path_join() {
        let extractor = PathTraversalExtractor::new();
        let content = r#"
            path = os.path.join(base_dir, request.args['file'])
        "#;

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

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

    #[test]
    fn test_js_fs_read() {
        let extractor = PathTraversalExtractor::new();
        let content = r#"
            fs.readFileSync(req.query.file);
        "#;

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

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

    #[test]
    fn test_js_sendfile() {
        let extractor = PathTraversalExtractor::new();
        let content = r#"
            res.sendFile(req.params.filename);
        "#;

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

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

    #[test]
    fn test_go_filepath() {
        let extractor = PathTraversalExtractor::new();
        let content = r#"
            path := filepath.Join(baseDir, r.URL.Query().Get("file"))
        "#;

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

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

    #[test]
    fn test_traversal_literal() {
        let extractor = PathTraversalExtractor::new();
        let content = r#"
            path := "../../../etc/passwd"
        "#;

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

        assert_eq!(claims.len(), 1);
        assert!(claims[0].description.contains("traversal sequence"));
    }

    #[test]
    fn test_encoded_traversal() {
        let extractor = PathTraversalExtractor::new();
        let content = r#"
            url := "%2e%2e%2fconfig"
        "#;

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

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

    #[test]
    fn test_no_false_positives_safe_path() {
        let extractor = PathTraversalExtractor::new();
        // Safe: no user input
        let content = r#"
            fs.readFileSync('./config.json');
        "#;

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

        assert!(claims.is_empty());
    }
}
