//! Unvalidated redirects vulnerability extractor.
//!
//! Detects patterns where HTTP redirects use user-controlled input
//! without proper validation, which can lead to open redirect attacks.

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

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

/// Extractor for unvalidated redirect vulnerabilities.
///
/// Detects patterns indicating unsafe redirect handling:
/// - redirect() with user-controlled URLs
/// - window.location assignment with user input
/// - URL parameters named redirect/return/next/goto
pub struct UnvalidatedRedirectsExtractor {
    // Python patterns
    python_redirect: Regex,
    python_flask_redirect: Regex,

    // JavaScript/TypeScript patterns
    js_redirect: Regex,
    js_location: Regex,

    // Go patterns
    go_redirect: Regex,

    // Universal URL parameter patterns
    url_param: Regex,
}

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

impl UnvalidatedRedirectsExtractor {
    /// Create a new unvalidated redirects extractor with compiled regexes.
    ///
    /// # Panics
    /// Panics if any regex pattern is invalid (programmer error).
    #[allow(clippy::expect_used)]
    pub fn new() -> Self {
        Self {
            // Python: redirect with user input
            python_redirect: Regex::new(
                r#"(?:redirect|HttpResponseRedirect)\s*\(\s*(?:request\.(?:GET|POST|args)|url|next|return_url)"#,
            )
            .expect("valid regex"),
            python_flask_redirect: Regex::new(
                r#"redirect\s*\(\s*request\.(?:args|form)\.get\s*\("#,
            )
            .expect("valid regex"),

            // JavaScript: redirect with user input
            js_redirect: Regex::new(
                r#"res\.redirect\s*\(\s*(?:req\.(?:query|params|body)\.|url|next)"#,
            )
            .expect("valid regex"),
            js_location: Regex::new(
                r#"(?:window\.)?location(?:\.href)?\s*=\s*(?:url|params|query|req\.)"#,
            )
            .expect("valid regex"),

            // Go: http.Redirect with user input
            go_redirect: Regex::new(
                r#"http\.Redirect\s*\([^,]*,\s*[^,]*,\s*(?:r\.|req\.|c\.)"#,
            )
            .expect("valid regex"),

            // Universal: dangerous URL parameter patterns
            url_param: Regex::new(
                r#"(?i)(?:redirect|return|next|goto|url|continue)(?:_url|Uri|_to)?\s*[:=]\s*(?:req\.|request\.|params\.)"#,
            )
            .expect("valid regex"),
        }
    }

    fn make_claim(
        path_segments: &[String],
        file: &str,
        line: usize,
        matched: &str,
        category: &str,
        description: &str,
    ) -> ExtractedClaim {
        build_claim(
            path_segments,
            &["http", "redirect", category],
            "redirect_source",
            ObjectValue::Text("user_controlled".to_string()),
            file,
            line,
            matched,
            0.85,
            description,
        )
    }
}

impl Extractor for UnvalidatedRedirectsExtractor {
    fn name(&self) -> &str {
        "unvalidated_redirects"
    }

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

    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 dangerous URL parameter patterns (all languages)
            if let Some(m) = self.url_param.find(line) {
                claims.push(Self::make_claim(
                    path_segments,
                    file,
                    line_num,
                    m.as_str(),
                    "url_parameter",
                    "Redirect URL from user-controlled parameter (open redirect risk)",
                ));
            }

            // Language-specific patterns
            match language {
                Language::Python => {
                    if let Some(m) = self.python_redirect.find(line) {
                        claims.push(Self::make_claim(
                            path_segments,
                            file,
                            line_num,
                            m.as_str(),
                            "response",
                            "Redirect with user-controlled URL (open redirect risk)",
                        ));
                    }
                    if let Some(m) = self.python_flask_redirect.find(line) {
                        claims.push(Self::make_claim(
                            path_segments,
                            file,
                            line_num,
                            m.as_str(),
                            "response",
                            "Flask redirect with request parameter (open redirect risk)",
                        ));
                    }
                }
                Language::JavaScript | Language::TypeScript => {
                    if let Some(m) = self.js_redirect.find(line) {
                        claims.push(Self::make_claim(
                            path_segments,
                            file,
                            line_num,
                            m.as_str(),
                            "response",
                            "res.redirect with user input (open redirect risk)",
                        ));
                    }
                    if let Some(m) = self.js_location.find(line) {
                        claims.push(Self::make_claim(
                            path_segments,
                            file,
                            line_num,
                            m.as_str(),
                            "client_side",
                            "window.location assignment with user input (open redirect risk)",
                        ));
                    }
                }
                Language::Go => {
                    if let Some(m) = self.go_redirect.find(line) {
                        claims.push(Self::make_claim(
                            path_segments,
                            file,
                            line_num,
                            m.as_str(),
                            "response",
                            "http.Redirect with user input (open redirect risk)",
                        ));
                    }
                }
                _ => {}
            }
        }

        claims
    }
}

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

    #[test]
    fn test_python_redirect() {
        let extractor = UnvalidatedRedirectsExtractor::new();
        let content = r#"
            return redirect(request.GET['next'])
        "#;

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

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

    #[test]
    fn test_python_flask_redirect() {
        let extractor = UnvalidatedRedirectsExtractor::new();
        let content = r#"
            return redirect(request.form.get('destination'))
        "#;

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

        // Should match the flask redirect pattern (redirect + request.form.get)
        assert_eq!(claims.len(), 1);
    }

    #[test]
    fn test_js_redirect() {
        let extractor = UnvalidatedRedirectsExtractor::new();
        let content = r#"
            res.redirect(req.query.next);
        "#;

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

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

    #[test]
    fn test_js_location() {
        let extractor = UnvalidatedRedirectsExtractor::new();
        let content = r#"
            window.location = url;
        "#;

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

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

    #[test]
    fn test_go_redirect() {
        let extractor = UnvalidatedRedirectsExtractor::new();
        let content = r#"
            http.Redirect(w, r, r.URL.Query().Get("next"), http.StatusFound)
        "#;

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

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

    #[test]
    fn test_url_param_pattern() {
        let extractor = UnvalidatedRedirectsExtractor::new();
        let content = r#"
            redirect_url = request.args.get("redirect")
        "#;

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

        // Should match the url_param pattern (redirect_url = request.)
        assert_eq!(claims.len(), 1);
    }

    #[test]
    fn test_no_false_positives_static_redirect() {
        let extractor = UnvalidatedRedirectsExtractor::new();
        // Safe: static redirect URL
        let content = r#"
            res.redirect('/login');
        "#;

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

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