//! NestJS security extractor.
//!
//! Detects security misconfigurations in NestJS applications:
//! - CORS with wildcard origin and credentials
//! - Auth bypass decorators (@Public, @SkipAuth)
//! - SQL injection in raw queries
//! - Weak JWT configuration
//! - Missing security middleware

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

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

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

    // Auth bypass patterns
    public_decorator: Regex,
    skip_auth_decorator: Regex,
    set_metadata_public: Regex,

    // SQL injection
    query_template_literal: Regex,
    query_concatenation: Regex,

    // JWT patterns
    weak_jwt_secret: Regex,
    jwt_long_expiry: Regex,

    // Missing security (heuristics)
    no_helmet_import: Regex,
}

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

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

            // Auth bypass decorators
            public_decorator: Regex::new(r"@Public\s*\(\s*\)").expect("valid regex"),
            skip_auth_decorator: Regex::new(r"@SkipAuth\s*\(\s*\)").expect("valid regex"),
            set_metadata_public: Regex::new(
                r#"@SetMetadata\s*\(\s*['"]isPublic['"]\s*,\s*true\s*\)"#,
            )
            .expect("valid regex"),

            // SQL injection in TypeORM
            query_template_literal: Regex::new(r"\.query\s*\(\s*`[^`]*\$\{[^}]*\}`")
                .expect("valid regex"),
            query_concatenation: Regex::new(r"\.query\s*\([^)]*\+[^)]*\)").expect("valid regex"),

            // Weak JWT
            weak_jwt_secret: Regex::new(
                r#"JwtModule\.register\s*\(\s*\{[^}]*secret\s*:\s*['"][^'"]{1,30}['"]"#,
            )
            .expect("valid regex"),
            jwt_long_expiry: Regex::new(
                r#"expiresIn\s*:\s*['"](?:365d|[3-9][0-9]+d|[1-9][0-9]{2,}d)['"]"#,
            )
            .expect("valid regex"),

            // Missing helmet
            no_helmet_import: Regex::new(r"import.*NestFactory").expect("valid regex"),
        }
    }
}

impl Extractor for NestJsSecurityExtractor {
    fn name(&self) -> &str {
        "nestjs_security"
    }

    fn languages(&self) -> &[Language] {
        &[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 a NestJS file
        let is_nestjs = content.contains("@nestjs")
            || content.contains("NestFactory")
            || content.contains("@Controller")
            || content.contains("@Injectable")
            || content.contains("@Module");

        if !is_nestjs {
            return claims;
        }

        // Multi-line patterns: CORS issues
        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,
                &["nestjs", "cors", "wildcard_credentials"],
                "vulnerable",
                ObjectValue::Boolean(true),
                file,
                line_num,
                &m.as_str()[..m.as_str().len().min(80)],
                1.0,
                "NestJS CORS allows all origins with credentials - security vulnerability",
            ));
        }

        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,
                &["nestjs", "cors", "reflected_credentials"],
                "vulnerable",
                ObjectValue::Boolean(true),
                file,
                line_num,
                &m.as_str()[..m.as_str().len().min(80)],
                1.0,
                "NestJS CORS reflects origin with credentials - security vulnerability",
            ));
        }

        // Multi-line: Weak JWT
        if let Some(m) = self.weak_jwt_secret.find(content) {
            let line_num = content[..m.start()].lines().count() + 1;
            claims.push(build_claim(
                path_segments,
                &["nestjs", "jwt", "weak_secret"],
                "vulnerable",
                ObjectValue::Boolean(true),
                file,
                line_num,
                &m.as_str()[..m.as_str().len().min(60)],
                0.9,
                "NestJS JWT secret appears weak or hardcoded",
            ));
        }

        // Multi-line: SQL injection via template literal
        if let Some(m) = self.query_template_literal.find(content) {
            let line_num = content[..m.start()].lines().count() + 1;
            claims.push(build_claim(
                path_segments,
                &["nestjs", "sql_injection"],
                "vulnerable",
                ObjectValue::Boolean(true),
                file,
                line_num,
                &m.as_str()[..m.as_str().len().min(80)],
                0.95,
                "NestJS raw query with template literal - SQL injection risk",
            ));
        }

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

            // @Public() decorator
            if let Some(m) = self.public_decorator.find(line) {
                claims.push(build_claim(
                    path_segments,
                    &["nestjs", "auth", "public_decorator"],
                    "used",
                    ObjectValue::Boolean(true),
                    file,
                    line_num,
                    m.as_str(),
                    0.7,
                    "NestJS @Public() decorator - route bypasses authentication",
                ));
            }

            // @SkipAuth() decorator
            if let Some(m) = self.skip_auth_decorator.find(line) {
                claims.push(build_claim(
                    path_segments,
                    &["nestjs", "auth", "skip_auth"],
                    "used",
                    ObjectValue::Boolean(true),
                    file,
                    line_num,
                    m.as_str(),
                    0.8,
                    "NestJS @SkipAuth() decorator - route bypasses authentication",
                ));
            }

            // SetMetadata('isPublic', true)
            if let Some(m) = self.set_metadata_public.find(line) {
                claims.push(build_claim(
                    path_segments,
                    &["nestjs", "auth", "metadata_public"],
                    "used",
                    ObjectValue::Boolean(true),
                    file,
                    line_num,
                    m.as_str(),
                    0.7,
                    "NestJS isPublic metadata - route may bypass authentication",
                ));
            }

            // SQL injection via concatenation
            if let Some(m) = self.query_concatenation.find(line) {
                claims.push(build_claim(
                    path_segments,
                    &["nestjs", "sql_injection"],
                    "vulnerable",
                    ObjectValue::Boolean(true),
                    file,
                    line_num,
                    m.as_str(),
                    0.9,
                    "NestJS raw query with string concatenation - SQL injection risk",
                ));
            }

            // JWT long expiry
            if let Some(m) = self.jwt_long_expiry.find(line) {
                claims.push(build_claim(
                    path_segments,
                    &["nestjs", "jwt", "long_expiry"],
                    "config_value",
                    ObjectValue::Text(m.as_str().to_string()),
                    file,
                    line_num,
                    m.as_str(),
                    0.8,
                    "NestJS JWT token expiry is very long - security risk",
                ));
            }
        }

        claims
    }
}

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

    #[test]
    fn test_cors_wildcard_credentials() {
        let extractor = NestJsSecurityExtractor::new();
        let content = r#"
import { NestFactory } from '@nestjs/core';

async function bootstrap() {
    const app = await NestFactory.create(AppModule);
    app.enableCors({
        origin: '*',
        credentials: true,
    });
}
"#;

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

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

    #[test]
    fn test_public_decorator() {
        let extractor = NestJsSecurityExtractor::new();
        let content = r#"
import { Controller, Get } from '@nestjs/common';

@Controller('users')
export class UsersController {
    @Public()
    @Get()
    findAll() {
        return [];
    }
}
"#;

        let claims = extractor.extract(
            &["ts".to_string()],
            content,
            Language::TypeScript,
            "users.controller.ts",
        );

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

    #[test]
    fn test_skip_auth_decorator() {
        let extractor = NestJsSecurityExtractor::new();
        let content = r#"
import { Controller, Get } from '@nestjs/common';

@Controller('health')
export class HealthController {
    @SkipAuth()
    @Get()
    check() {
        return { status: 'ok' };
    }
}
"#;

        let claims = extractor.extract(
            &["ts".to_string()],
            content,
            Language::TypeScript,
            "health.controller.ts",
        );

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

    #[test]
    fn test_sql_injection_template_literal() {
        let extractor = NestJsSecurityExtractor::new();
        let content = r#"
import { Injectable } from '@nestjs/common';

@Injectable()
export class UsersService {
    async findOne(id: string) {
        return this.entityManager.query(`SELECT * FROM users WHERE id = ${id}`);
    }
}
"#;

        let claims = extractor.extract(
            &["ts".to_string()],
            content,
            Language::TypeScript,
            "users.service.ts",
        );

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

    #[test]
    fn test_weak_jwt_secret() {
        let extractor = NestJsSecurityExtractor::new();
        let content = r#"
import { Module } from '@nestjs/common';
import { JwtModule } from '@nestjs/jwt';

@Module({
    imports: [
        JwtModule.register({
            secret: 'weak-secret',
            signOptions: { expiresIn: '60s' },
        }),
    ],
})
export class AuthModule {}
"#;

        let claims =
            extractor.extract(&["ts".to_string()], content, Language::TypeScript, "auth.module.ts");

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

    #[test]
    fn test_non_nestjs_file_skipped() {
        let extractor = NestJsSecurityExtractor::new();
        let content = r#"
const app = express();
app.enableCors({ origin: '*', credentials: true });
"#;

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

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