//! CORS configuration extractor.
//!
//! Detects overly permissive CORS settings that could expose
//! the application to cross-origin attacks.

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

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

/// Extractor for CORS configuration issues.
pub struct CorsConfigExtractor {
    /// Wildcard allow-origin patterns
    allow_all_origins: Regex,
    /// Credentials enabled pattern
    allow_credentials: Regex,
}

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

impl CorsConfigExtractor {
    /// Create a new CORS config extractor.
    ///
    /// # Panics
    /// Panics if any regex pattern is invalid (programmer error).
    #[allow(clippy::expect_used)]
    pub fn new() -> Self {
        Self {
            // Expanded to include plural forms (allowed_origins) and YAML list syntax
            // Also handles cors_origins
            allow_all_origins: Regex::new(
                r#"(?i)(allow(?:ed)?_origins?\s*[:=\(]\s*["']\*["']|Access-Control-Allow-Origin.*\*|AllowAllOrigins.*true|cors.*origin.*\*|cors_origins?\s*[:=]\s*["']\*["']|-\s*["']\*["'])"#,
            )
            .expect("valid regex"),
            allow_credentials: Regex::new(
                r"(?i)(allow_credentials|AllowCredentials|credentials)\s*[:=]\s*true",
            )
            .expect("valid regex"),
        }
    }
}

impl Extractor for CorsConfigExtractor {
    fn name(&self) -> &str {
        "cors_config"
    }

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

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

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

            // Wildcard allow-origin detection
            if let Some(matched) = self.allow_all_origins.find(line) {
                found_wildcard_origin = true;
                wildcard_line = line_num;
                wildcard_text = matched.as_str().to_string();

                let mut concept_path = path_segments.to_vec();
                concept_path.push("cors".to_string());
                concept_path.push("allow_origin".to_string());

                claims.push(ExtractedClaim {
                    concept_path: format!("code://{}", concept_path.join("/")),
                    predicate: "config_value".to_string(),
                    value: ObjectValue::Text("*".to_string()),
                    file: file.to_string(),
                    line: line_num,
                    matched_text: matched.as_str().to_string(),
                    confidence: 1.0,
                    description: "CORS allows all origins".to_string(),
                });
            }
        }

        // Check for credentials with wildcard (dangerous combination)
        // Look within a reasonable proximity (same file suggests related config)
        if found_wildcard_origin && self.allow_credentials.is_match(content) {
            let mut concept_path = path_segments.to_vec();
            concept_path.push("cors".to_string());
            concept_path.push("credentials_with_wildcard".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: wildcard_line,
                matched_text: wildcard_text,
                confidence: 0.9, // Slightly lower - we're inferring the combination
                description: "CORS allows credentials with wildcard origin (security risk)"
                    .to_string(),
            });
        }

        claims
    }
}

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

    #[test]
    fn test_wildcard_origin() {
        let extractor = CorsConfigExtractor::new();
        let content = r#"
            cors = tower_http::cors::CorsLayer::permissive()
                .allow_origin("*")
        "#;

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

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

    #[test]
    fn test_access_control_header() {
        let extractor = CorsConfigExtractor::new();
        let content = r#"
            res.setHeader("Access-Control-Allow-Origin", "*");
        "#;

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

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

    #[test]
    fn test_credentials_with_wildcard() {
        let extractor = CorsConfigExtractor::new();
        let content = r#"
            cors:
              allow_origin: "*"
              allow_credentials: true
        "#;

        let claims =
            extractor.extract(&["config".to_string()], content, Language::Yaml, "config/cors.yaml");

        assert_eq!(claims.len(), 2);
        assert!(claims.iter().any(|c| c.concept_path.contains("credentials_with_wildcard")));
    }

    #[test]
    fn test_go_allow_all_origins() {
        let extractor = CorsConfigExtractor::new();
        let content = r#"
            c := cors.New(cors.Config{
                AllowAllOrigins: true,
            })
        "#;

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

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

    #[test]
    fn test_yaml_allowed_origins_plural() {
        let extractor = CorsConfigExtractor::new();
        let content = r#"
cors:
  allowed_origins: "*"
"#;

        let claims = extractor.extract(
            &["config".to_string()],
            content,
            Language::Yaml,
            "config/production.yaml",
        );

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

    #[test]
    fn test_yaml_list_wildcard() {
        let extractor = CorsConfigExtractor::new();
        let content = r#"
cors:
  allowed_origins:
    - "*"
"#;

        let claims = extractor.extract(
            &["config".to_string()],
            content,
            Language::Yaml,
            "config/production.yaml",
        );

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