//! Weak password requirements extractor.
//!
//! Detects patterns where password policies are too weak,
//! such as minimum length < 8, bcrypt cost < 10, or missing complexity requirements.

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

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

/// Extractor for weak password requirement configurations.
///
/// Detects patterns indicating insufficient password policies:
/// - Minimum password length < 8 characters
/// - Bcrypt cost/rounds < 10
/// - Complexity requirements disabled
pub struct WeakPasswordExtractor {
    // Minimum length too short (< 8)
    min_length_weak: Regex,
    min_length_config: Regex,

    // Bcrypt cost too low (< 10)
    bcrypt_weak: Regex,

    // Simple length check in code
    simple_check: Regex,

    // Special chars not required
    no_special: Regex,
    no_uppercase: Regex,
    no_number: Regex,
}

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

impl WeakPasswordExtractor {
    /// Create a new weak password extractor with compiled regexes.
    ///
    /// # Panics
    /// Panics if any regex pattern is invalid (programmer error).
    #[allow(clippy::expect_used)]
    pub fn new() -> Self {
        Self {
            // Minimum length too short (< 8) in various config formats
            min_length_weak: Regex::new(
                r#"(?i)(?:min[_-]?(?:password[_-]?)?length|password[_-]?min(?:[_-]?length)?)\s*[:=]\s*[1-7](?:\D|$)"#,
            )
            .expect("valid regex"),
            min_length_config: Regex::new(
                r#"(?i)["']?(?:minLength|min_length|minimumLength)["']?\s*[:=]\s*[1-7](?:\D|$)"#,
            )
            .expect("valid regex"),

            // Bcrypt cost too low (< 10)
            bcrypt_weak: Regex::new(
                r#"(?i)(?:bcrypt|hash|argon2?|scrypt).*(?:cost|rounds|iterations)\s*[:=]\s*[1-9](?:\D|$)"#,
            )
            .expect("valid regex"),

            // Simple length check in code (checking for < 8)
            simple_check: Regex::new(
                r#"len\s*\(\s*password\s*\)\s*(?:>=?|>)\s*[1-7](?:\D|$)"#,
            )
            .expect("valid regex"),

            // Complexity requirements disabled
            no_special: Regex::new(
                r#"(?i)require[_-]?(?:special|symbol)[_-]?(?:char)?s?\s*[:=]\s*(?:false|no|0)"#,
            )
            .expect("valid regex"),
            no_uppercase: Regex::new(
                r#"(?i)require[_-]?(?:upper|uppercase)[_-]?(?:case)?\s*[:=]\s*(?:false|no|0)"#,
            )
            .expect("valid regex"),
            no_number: Regex::new(
                r#"(?i)require[_-]?(?:number|digit)s?\s*[:=]\s*(?:false|no|0)"#,
            )
            .expect("valid regex"),
        }
    }

    fn make_claim(
        path_segments: &[String],
        file: &str,
        line: usize,
        matched: &str,
        category: &str,
        value: ObjectValue,
        description: &str,
    ) -> ExtractedClaim {
        build_claim(
            path_segments,
            &["auth", "password", "policy", category],
            "requirement_strength",
            value,
            file,
            line,
            matched,
            0.9,
            description,
        )
    }
}

impl Extractor for WeakPasswordExtractor {
    fn name(&self) -> &str {
        "weak_password"
    }

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

    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 weak minimum length
            if let Some(m) = self.min_length_weak.find(line) {
                claims.push(Self::make_claim(
                    path_segments,
                    file,
                    line_num,
                    m.as_str(),
                    "min_length",
                    ObjectValue::Text("weak".to_string()),
                    "Minimum password length < 8 characters (should be at least 8)",
                ));
            }
            if let Some(m) = self.min_length_config.find(line) {
                claims.push(Self::make_claim(
                    path_segments,
                    file,
                    line_num,
                    m.as_str(),
                    "min_length",
                    ObjectValue::Text("weak".to_string()),
                    "Minimum password length < 8 characters (should be at least 8)",
                ));
            }

            // Check for weak bcrypt cost
            if let Some(m) = self.bcrypt_weak.find(line) {
                claims.push(Self::make_claim(
                    path_segments,
                    file,
                    line_num,
                    m.as_str(),
                    "hash_cost",
                    ObjectValue::Text("weak".to_string()),
                    "Password hashing cost/rounds < 10 (should be at least 10 for bcrypt)",
                ));
            }

            // Check for simple length validation
            if let Some(m) = self.simple_check.find(line) {
                claims.push(Self::make_claim(
                    path_segments,
                    file,
                    line_num,
                    m.as_str(),
                    "validation",
                    ObjectValue::Text("weak".to_string()),
                    "Password validation allows < 8 characters",
                ));
            }

            // Check for disabled complexity requirements
            if let Some(m) = self.no_special.find(line) {
                claims.push(Self::make_claim(
                    path_segments,
                    file,
                    line_num,
                    m.as_str(),
                    "complexity",
                    ObjectValue::Boolean(false),
                    "Special character requirement disabled",
                ));
            }
            if let Some(m) = self.no_uppercase.find(line) {
                claims.push(Self::make_claim(
                    path_segments,
                    file,
                    line_num,
                    m.as_str(),
                    "complexity",
                    ObjectValue::Boolean(false),
                    "Uppercase requirement disabled",
                ));
            }
            if let Some(m) = self.no_number.find(line) {
                claims.push(Self::make_claim(
                    path_segments,
                    file,
                    line_num,
                    m.as_str(),
                    "complexity",
                    ObjectValue::Boolean(false),
                    "Number/digit requirement disabled",
                ));
            }
        }

        claims
    }
}

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

    #[test]
    fn test_weak_min_length_yaml() {
        let extractor = WeakPasswordExtractor::new();
        let content = r#"
            password_min: 6
        "#;

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

        // Should match min_length_weak pattern
        assert_eq!(claims.len(), 1);
        assert!(claims[0].concept_path.contains("auth/password/policy"));
    }

    #[test]
    fn test_weak_min_length_json() {
        let extractor = WeakPasswordExtractor::new();
        let content = r#"
            "minLength": 4
        "#;

        let claims =
            extractor.extract(&["config".to_string()], content, Language::Json, "config.json");

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

    #[test]
    fn test_weak_bcrypt_cost() {
        let extractor = WeakPasswordExtractor::new();
        let content = r#"
            bcrypt_cost = 8
        "#;

        let claims =
            extractor.extract(&["config".to_string()], content, Language::Toml, "config.toml");

        assert_eq!(claims.len(), 1);
        assert!(claims[0].description.contains("cost/rounds"));
    }

    #[test]
    fn test_simple_length_check() {
        let extractor = WeakPasswordExtractor::new();
        let content = r#"
            if len(password) >= 6:
        "#;

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

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

    #[test]
    fn test_no_special_chars() {
        let extractor = WeakPasswordExtractor::new();
        let content = r#"
            require_special_chars: false
        "#;

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

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

    #[test]
    fn test_no_uppercase() {
        let extractor = WeakPasswordExtractor::new();
        let content = r#"
            require_uppercase = false
        "#;

        let claims =
            extractor.extract(&["config".to_string()], content, Language::Toml, "config.toml");

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

    #[test]
    fn test_no_false_positives_strong() {
        let extractor = WeakPasswordExtractor::new();
        // Strong: min length >= 8
        let content = r#"
            password_min_length: 12
            bcrypt_cost: 12
        "#;

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

        assert!(claims.is_empty());
    }

    #[test]
    fn test_boundary_value_8() {
        let extractor = WeakPasswordExtractor::new();
        // Boundary: exactly 8 should be OK
        let content = r#"
            password_min_length: 8
        "#;

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

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