//! JWT configuration extractor.
//!
//! Detects patterns where JWT validation is misconfigured,
//! violating RFC 7519 security requirements.

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

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

/// Extractor for JWT validation configuration.
pub struct JwtConfigExtractor {
    /// Audience validation disabled
    aud_disabled: Regex,
    /// Algorithm none allowed
    alg_none: Regex,
    /// Signature verification skipped
    sig_skip: Regex,
    /// Expiry validation disabled
    exp_disabled: Regex,
    /// Go jwt.Parse without algorithm check (heuristic)
    go_parse_insecure: Regex,
}

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

impl JwtConfigExtractor {
    /// Create a new JWT config extractor with compiled regexes.
    ///
    /// # Panics
    /// Panics if any regex pattern is invalid (programmer error).
    #[allow(clippy::expect_used)]
    pub fn new() -> Self {
        Self {
            // Match JWT audience validation disabled patterns:
            // - set_audience([]) or set_audience(vec![]) - empty audience list
            // - validate_aud = false or validate_audience = false
            // - aud = None or audience = None (direct assignment)
            // - ValidateAudience = false (.NET style)
            // NOTE: \baud\b ensures we don't match "audit" in "audit_log_path"
            aud_disabled: Regex::new(
                r"(?i)(set_audience\s*\(\s*(?:vec!)?\s*\[\s*\]\s*\)|validate_aud\w*\s*[:=]\s*false|\baud(?:ience)?\s*[:=]\s*None|ValidateAudience\s*[:=]\s*false)",
            )
            .expect("valid regex"),
            // Match JWT algorithm none patterns (signature bypass vulnerability):
            // - Algorithm::None (Rust enum variant)
            // - alg: none, alg = none, alg: "none" (config assignment)
            // - allow_none = true or allow_none_algorithm = true
            // - SigningMethodNone (Go jwt library)
            // - YAML list item: "- none" or '- "none"' in algorithms list
            // - WithValidMethods([]string{"none"...}) in Go
            // NOTE: Post-processing filters out documentation like "(no alg: none)"
            alg_none: Regex::new(
                r#"(?i)(Algorithm::None|\balg(?:orithm)?s?\b\s*[:=]\s*['"]?none|allow_none\w*\s*[:=]\s*true|SigningMethodNone|-\s*['"]?none['"]?\s*(?:#|$)|WithValidMethods.*none)"#,
            )
            .expect("valid regex"),
            // Match signature verification disabled patterns:
            // - dangerous_insecure_* functions
            // - skip_signature settings
            // - verify_signature = false, signature_verify = false
            // - RequireSignedTokens = false
            // NOTE: Excludes patterns like "skip_verify = false" (that's secure!)
            // and UI state like "isVerifying = false"
            sig_skip: Regex::new(
                r"(?i)(dangerous_insecure|skip_signature|(?:verify_signature|signature_verify)\s*[:=]\s*false|RequireSignedTokens\s*[:=]\s*false)",
            )
            .expect("valid regex"),
            exp_disabled: Regex::new(
                r"(?i)(validate_exp.*false|RequireExpirationTime.*false|IgnoreExpiration)",
            )
            .expect("valid regex"),
            go_parse_insecure: Regex::new(
                r"jwt\.Parse\([^,]+,\s*func\s*\([^)]*\*jwt\.Token\)\s*\([^)]*,\s*error\)\s*\{[^}]*return\s+[^,]+,\s*nil",
            )
            .expect("valid regex"),
        }
    }

    #[allow(clippy::too_many_arguments)]
    fn extract_claim(
        &self,
        path_segments: &[String],
        file: &str,
        line: usize,
        matched_text: &str,
        leaf: &str,
        predicate: &str,
        value: ObjectValue,
        description: &str,
        confidence: f32,
    ) -> ExtractedClaim {
        let mut concept_path = path_segments.to_vec();
        concept_path.push("jwt".to_string());
        concept_path.push(leaf.to_string());

        ExtractedClaim {
            concept_path: format!("code://{}", concept_path.join("/")),
            predicate: predicate.to_string(),
            value,
            file: file.to_string(),
            line,
            matched_text: matched_text.to_string(),
            confidence,
            description: description.to_string(),
        }
    }
}

impl Extractor for JwtConfigExtractor {
    fn name(&self) -> &str {
        "jwt_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();

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

            // Audience validation disabled
            if let Some(matched) = self.aud_disabled.find(line) {
                claims.push(self.extract_claim(
                    path_segments,
                    file,
                    line_num,
                    matched.as_str(),
                    "audience_validation",
                    "enabled",
                    ObjectValue::Boolean(false),
                    "JWT audience validation is disabled",
                    1.0,
                ));
            }

            // Algorithm none allowed
            if let Some(matched) = self.alg_none.find(line) {
                // Filter out documentation that describes PREVENTING alg:none
                // e.g., "(no alg: none)" or "don't allow alg none"
                let lower_line = line.to_lowercase();
                let is_prevention_doc = lower_line.contains("no alg")
                    || lower_line.contains("no `alg")
                    || lower_line.contains("don't allow")
                    || lower_line.contains("do not allow")
                    || lower_line.contains("whitelist")
                    || lower_line.contains("reject")
                    || (lower_line.contains("algorithm") && lower_line.contains("none"))
                        && (lower_line.contains("checksum") || lower_line.contains("crc"));

                if is_prevention_doc {
                    continue;
                }

                claims.push(self.extract_claim(
                    path_segments,
                    file,
                    line_num,
                    matched.as_str(),
                    "algorithm_restriction",
                    "config_value",
                    ObjectValue::Text("none_allowed".to_string()),
                    "JWT allows 'none' algorithm (signature bypass)",
                    1.0,
                ));
            }

            // Signature verification skipped
            if let Some(matched) = self.sig_skip.find(line) {
                claims.push(self.extract_claim(
                    path_segments,
                    file,
                    line_num,
                    matched.as_str(),
                    "signature_verification",
                    "enabled",
                    ObjectValue::Boolean(false),
                    "JWT signature verification is disabled",
                    1.0,
                ));
            }

            // Expiry validation disabled
            if let Some(matched) = self.exp_disabled.find(line) {
                claims.push(self.extract_claim(
                    path_segments,
                    file,
                    line_num,
                    matched.as_str(),
                    "expiry_validation",
                    "enabled",
                    ObjectValue::Boolean(false),
                    "JWT expiry validation is disabled",
                    1.0,
                ));
            }
        }

        // Check for Go insecure parse pattern (multi-line, lower confidence)
        if let Some(matched) = self.go_parse_insecure.find(content) {
            // Find line number for start of match
            let line_num = content[..matched.start()].lines().count() + 1;
            claims.push(self.extract_claim(
                path_segments,
                file,
                line_num,
                &matched.as_str()[..matched.as_str().len().min(50)],
                "signature_verification",
                "enabled",
                ObjectValue::Boolean(false),
                "JWT parsed without algorithm verification (heuristic)",
                0.7, // Lower confidence - heuristic match
            ));
        }

        claims
    }
}

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

    #[test]
    fn test_audience_disabled() {
        let extractor = JwtConfigExtractor::new();
        let content = r#"
            let validation = Validation::new(Algorithm::HS256);
            validation.validate_aud = false;
        "#;

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

        assert_eq!(claims.len(), 1);
        assert!(claims[0].concept_path.contains("audience_validation"));
        assert_eq!(claims[0].value, ObjectValue::Boolean(false));
    }

    #[test]
    fn test_algorithm_none() {
        let extractor = JwtConfigExtractor::new();
        let content = r#"
            // Dangerous: allows unsigned tokens
            Algorithm::None
        "#;

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

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

    #[test]
    fn test_signature_skip() {
        let extractor = JwtConfigExtractor::new();
        let content = r#"
            let claims = dangerous_insecure_decode(&token)?;
        "#;

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

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

    #[test]
    fn test_multiple_issues() {
        let extractor = JwtConfigExtractor::new();
        let content = r#"
            validation.validate_aud = false;
            validation.validate_exp = false;
        "#;

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

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

    // Regression tests for false positives

    #[test]
    fn test_skip_verify_false_not_flagged() {
        // "insecure_skip_verify = false" means verification IS enabled (secure)
        let extractor = JwtConfigExtractor::new();
        let content = r#"
            insecure_skip_verify = false
        "#;

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

        assert!(claims.is_empty(), "skip_verify = false should NOT be flagged (it's secure)");
    }

    #[test]
    fn test_ui_state_not_flagged() {
        // React state "isVerifying = false" is UI state, not security
        let extractor = JwtConfigExtractor::new();
        let content = r#"
            const [isVerifying, setIsVerifying] = useState(false);
            setIsVerifying(false);
        "#;

        let claims = extractor.extract(
            &["typescript".to_string()],
            content,
            Language::TypeScript,
            "page.tsx",
        );

        assert!(claims.is_empty(), "UI state isVerifying should NOT be flagged");
    }

    #[test]
    fn test_verify_release_not_flagged() {
        // verify_release(version, false) is a release verification function, not JWT
        let extractor = JwtConfigExtractor::new();
        let content = r#"
            verify_release(version, false);
        "#;

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

        assert!(claims.is_empty(), "verify_release function should NOT be flagged as JWT");
    }

    #[test]
    fn test_actual_signature_verify_disabled() {
        // verify_signature = false should be flagged
        let extractor = JwtConfigExtractor::new();
        let content = r#"
            verify_signature = false
        "#;

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

        assert_eq!(claims.len(), 1, "verify_signature = false should be flagged");
        assert!(claims[0].concept_path.contains("signature_verification"));
    }

    #[test]
    fn test_yaml_algorithms_list_none() {
        let extractor = JwtConfigExtractor::new();
        let content = r#"
jwt:
  algorithms:
    - HS256
    - none
"#;

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

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

    #[test]
    fn test_go_with_valid_methods_none() {
        let extractor = JwtConfigExtractor::new();
        let content = r#"
            parser := jwt.NewParser(jwt.WithValidMethods([]string{"none", "HS256"}))
        "#;

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

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