//! Express.js security extractor.
//!
//! Detects security misconfigurations in Express.js applications:
//! - CORS with wildcard origin and credentials
//! - Insecure session/cookie settings
//! - Missing security headers
//! - Weak session secrets
//! - Trust proxy misconfiguration

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

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

/// Extractor for Express.js security misconfigurations.
#[allow(dead_code)]
pub struct ExpressSecurityExtractor {
    // CORS patterns
    cors_wildcard_credentials: Regex,
    cors_origin_true_credentials: Regex,

    // Cookie/session patterns
    cookie_secure_false: Regex,
    cookie_httponly_false: Regex,
    cookie_samesite_none: Regex,
    weak_session_secret: Regex,
    session_secure_false: Regex,

    // Trust proxy
    trust_proxy_true: Regex,

    // Security headers
    x_frame_options_disabled: Regex,
    xss_protection_disabled: Regex,
    unsafe_csp: Regex,

    // Powered by header
    powered_by_enabled: Regex,
}

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

impl ExpressSecurityExtractor {
    /// Create a new Express.js security extractor.
    ///
    /// # Panics
    /// Panics if any regex pattern is invalid (programmer error).
    #[allow(clippy::expect_used)]
    pub fn new() -> Self {
        Self {
            // CORS dangerous combinations (multiline-aware)
            cors_wildcard_credentials: Regex::new(
                r#"cors\s*\(\s*\{[^}]*origin\s*:\s*['"]?\*['"]?[^}]*credentials\s*:\s*true"#,
            )
            .expect("valid regex"),
            cors_origin_true_credentials: Regex::new(
                r#"cors\s*\(\s*\{[^}]*origin\s*:\s*true[^}]*credentials\s*:\s*true"#,
            )
            .expect("valid regex"),

            // Cookie security (line-by-line patterns)
            cookie_secure_false: Regex::new(r"secure\s*:\s*false").expect("valid regex"),
            cookie_httponly_false: Regex::new(r"httpOnly\s*:\s*false").expect("valid regex"),
            cookie_samesite_none: Regex::new(r#"sameSite\s*:\s*['"]none['"]"#)
                .expect("valid regex"),
            weak_session_secret: Regex::new(
                r#"session\s*\(\s*\{[^}]*secret\s*:\s*['"][^'"]{1,20}['"]"#,
            )
            .expect("valid regex"),
            session_secure_false: Regex::new(r"session\s*\(\s*\{[^}]*secure\s*:\s*false")
                .expect("valid regex"),

            // Trust proxy
            trust_proxy_true: Regex::new(
                r#"(?:set\s*\(\s*['"]trust proxy['"]\s*,\s*true|enable\s*\(\s*['"]trust proxy['"])"#,
            )
            .expect("valid regex"),

            // Security headers
            x_frame_options_disabled: Regex::new(
                r#"(?i)setHeader\s*\(\s*['"]X-Frame-Options['"]\s*,\s*['"]ALLOWALL['"]"#,
            )
            .expect("valid regex"),
            xss_protection_disabled: Regex::new(
                r#"(?i)setHeader\s*\(\s*['"]X-XSS-Protection['"]\s*,\s*['"]0['"]"#,
            )
            .expect("valid regex"),
            unsafe_csp: Regex::new(
                r#"(?i)Content-Security-Policy['"]\s*,\s*['"][^'"]*(?:unsafe-inline|unsafe-eval)"#,
            )
            .expect("valid regex"),

            // Powered by
            powered_by_enabled: Regex::new(r"x-powered-by").expect("valid regex"),
        }
    }
}

impl Extractor for ExpressSecurityExtractor {
    fn name(&self) -> &str {
        "express_security"
    }

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

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

        // Check if this looks like an Express.js file
        let is_express = content.contains("express()")
            || content.contains("require('express')")
            || content.contains("require(\"express\")")
            || content.contains("from 'express'")
            || content.contains("from \"express\"");

        if !is_express {
            return claims;
        }

        // For multi-line patterns, we search the whole content
        // CORS wildcard with credentials
        if let Some(m) = self.cors_wildcard_credentials.find(content) {
            let line_num = content[..m.start()].lines().count() + 1;
            claims.push(build_claim(
                path_segments,
                &["express", "cors", "wildcard_credentials"],
                "vulnerable",
                ObjectValue::Boolean(true),
                file,
                line_num,
                &m.as_str()[..m.as_str().len().min(80)],
                1.0,
                "Express CORS allows all origins with credentials - security vulnerability",
            ));
        }

        // CORS origin: true with credentials (reflects any origin)
        if let Some(m) = self.cors_origin_true_credentials.find(content) {
            let line_num = content[..m.start()].lines().count() + 1;
            claims.push(build_claim(
                path_segments,
                &["express", "cors", "reflected_credentials"],
                "vulnerable",
                ObjectValue::Boolean(true),
                file,
                line_num,
                &m.as_str()[..m.as_str().len().min(80)],
                1.0,
                "Express CORS reflects origin with credentials - security vulnerability",
            ));
        }

        // Weak session secret
        if let Some(m) = self.weak_session_secret.find(content) {
            let line_num = content[..m.start()].lines().count() + 1;
            claims.push(build_claim(
                path_segments,
                &["express", "session", "weak_secret"],
                "vulnerable",
                ObjectValue::Boolean(true),
                file,
                line_num,
                &m.as_str()[..m.as_str().len().min(60)],
                0.9,
                "Express session secret is weak (too short) - use a strong secret",
            ));
        }

        // Line-by-line patterns
        for (line_idx, line) in content.lines().enumerate() {
            let line_num = line_idx + 1;

            // Cookie secure: false
            if let Some(m) = self.cookie_secure_false.find(line) {
                claims.push(build_claim(
                    path_segments,
                    &["express", "cookie", "secure"],
                    "enabled",
                    ObjectValue::Boolean(false),
                    file,
                    line_num,
                    m.as_str(),
                    1.0,
                    "Express cookie not marked secure - sent over HTTP",
                ));
            }

            // Cookie httpOnly: false
            if let Some(m) = self.cookie_httponly_false.find(line) {
                claims.push(build_claim(
                    path_segments,
                    &["express", "cookie", "httponly"],
                    "enabled",
                    ObjectValue::Boolean(false),
                    file,
                    line_num,
                    m.as_str(),
                    1.0,
                    "Express cookie accessible to JavaScript - XSS risk",
                ));
            }

            // Cookie sameSite: 'none'
            if let Some(m) = self.cookie_samesite_none.find(line) {
                claims.push(build_claim(
                    path_segments,
                    &["express", "cookie", "samesite"],
                    "config_value",
                    ObjectValue::Text("none".to_string()),
                    file,
                    line_num,
                    m.as_str(),
                    0.9,
                    "Express cookie sameSite=none - cross-site requests allowed",
                ));
            }

            // Trust proxy true
            if let Some(m) = self.trust_proxy_true.find(line) {
                claims.push(build_claim(
                    path_segments,
                    &["express", "trust_proxy"],
                    "enabled",
                    ObjectValue::Boolean(true),
                    file,
                    line_num,
                    m.as_str(),
                    0.7,
                    "Express trust proxy enabled globally - should be more specific",
                ));
            }

            // X-Frame-Options disabled
            if let Some(m) = self.x_frame_options_disabled.find(line) {
                claims.push(build_claim(
                    path_segments,
                    &["express", "x_frame_options"],
                    "enabled",
                    ObjectValue::Boolean(false),
                    file,
                    line_num,
                    m.as_str(),
                    1.0,
                    "Express X-Frame-Options set to ALLOWALL - clickjacking vulnerability",
                ));
            }

            // XSS protection disabled
            if let Some(m) = self.xss_protection_disabled.find(line) {
                claims.push(build_claim(
                    path_segments,
                    &["express", "xss_protection"],
                    "enabled",
                    ObjectValue::Boolean(false),
                    file,
                    line_num,
                    m.as_str(),
                    0.8,
                    "Express XSS protection header disabled",
                ));
            }

            // Unsafe CSP
            if let Some(m) = self.unsafe_csp.find(line) {
                claims.push(build_claim(
                    path_segments,
                    &["express", "csp", "unsafe"],
                    "vulnerable",
                    ObjectValue::Boolean(true),
                    file,
                    line_num,
                    m.as_str(),
                    0.9,
                    "Express CSP contains unsafe-inline or unsafe-eval",
                ));
            }
        }

        claims
    }
}

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

    #[test]
    fn test_cors_wildcard_credentials() {
        let extractor = ExpressSecurityExtractor::new();
        let content = r#"
const express = require('express');
const cors = require('cors');
const app = express();

app.use(cors({
    origin: '*',
    credentials: true
}));
"#;

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

        assert!(claims.iter().any(|c| c.concept_path.contains("wildcard_credentials")));
    }

    #[test]
    fn test_weak_session_secret() {
        let extractor = ExpressSecurityExtractor::new();
        let content = r#"
const express = require('express');
const session = require('express-session');
const app = express();

app.use(session({
    secret: 'keyboard cat',
    resave: false
}));
"#;

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

        assert!(claims.iter().any(|c| c.concept_path.contains("weak_secret")));
    }

    #[test]
    fn test_insecure_cookie() {
        let extractor = ExpressSecurityExtractor::new();
        let content = r#"
const express = require('express');
const app = express();

res.cookie('session', value, {
    secure: false,
    httpOnly: false
});
"#;

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

        assert!(claims.iter().any(|c| c.concept_path.contains("cookie/secure")));
        assert!(claims.iter().any(|c| c.concept_path.contains("cookie/httponly")));
    }

    #[test]
    fn test_x_frame_options_disabled() {
        let extractor = ExpressSecurityExtractor::new();
        let content = r#"
const express = require('express');
const app = express();

app.use((req, res, next) => {
    res.setHeader('X-Frame-Options', 'ALLOWALL');
    next();
});
"#;

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

        assert!(claims.iter().any(|c| c.concept_path.contains("x_frame_options")));
    }

    #[test]
    fn test_non_express_file_skipped() {
        let extractor = ExpressSecurityExtractor::new();
        let content = r#"
const app = createApp();
app.use(cors({ origin: '*', credentials: true }));
"#;

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

        // Should not detect since file doesn't look like Express
        assert!(claims.is_empty());
    }
}
