//! ORM SQL injection vulnerability extractor.
//!
//! Detects patterns where ORM methods are used with string interpolation
//! instead of proper parameterized queries, which can lead to SQL injection.

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

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

/// Extractor for ORM-specific SQL injection vulnerabilities.
///
/// Detects patterns indicating unsafe query construction in popular ORMs:
/// - Django: .raw() and .extra() with string interpolation
/// - SQLAlchemy: text() and execute() with f-strings
/// - Sequelize: query() with template literals
/// - TypeORM: where() with template literals
/// - GORM: Raw() with fmt.Sprintf
/// - Prisma: $queryRawUnsafe with interpolation
pub struct OrmInjectionExtractor {
    // Django patterns
    django_raw: Regex,
    django_extra: Regex,

    // SQLAlchemy patterns
    sqlalchemy_text: Regex,
    sqlalchemy_exec: Regex,

    // Sequelize patterns
    sequelize_raw: Regex,

    // TypeORM patterns
    typeorm_where: Regex,

    // GORM patterns
    gorm_raw: Regex,

    // Prisma patterns
    prisma_raw: Regex,
}

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

impl OrmInjectionExtractor {
    /// Create a new ORM injection extractor with compiled regexes.
    ///
    /// # Panics
    /// Panics if any regex pattern is invalid (programmer error).
    #[allow(clippy::expect_used)]
    pub fn new() -> Self {
        Self {
            // Django: raw/extra with formatting
            django_raw: Regex::new(r#"\.raw\s*\(\s*(?:f["']|["'][^"']*\{[^}]*\}["']\.format)"#)
                .expect("valid regex"),
            django_extra: Regex::new(r#"\.extra\s*\([^)]*where\s*=\s*\[.*(?:f["']|%)"#)
                .expect("valid regex"),

            // SQLAlchemy: text with formatting
            sqlalchemy_text: Regex::new(r#"text\s*\(\s*(?:f["']|["'][^"']*%|["'][^"']*\.format)"#)
                .expect("valid regex"),
            sqlalchemy_exec: Regex::new(r#"\.execute\s*\(\s*(?:f["']|["'][^"']*\{)"#)
                .expect("valid regex"),

            // Sequelize: raw query interpolation
            sequelize_raw: Regex::new(r#"sequelize\.query\s*\(\s*`[^`]*\$\{"#)
                .expect("valid regex"),

            // TypeORM: where with interpolation
            typeorm_where: Regex::new(r#"\.(?:where|andWhere|orWhere)\s*\(\s*`[^`]*\$\{"#)
                .expect("valid regex"),

            // GORM: Raw with Sprintf
            gorm_raw: Regex::new(r#"\.Raw\s*\(\s*(?:fmt\.Sprintf|"[^"]*"\s*\+)"#)
                .expect("valid regex"),

            // Prisma: $queryRawUnsafe
            prisma_raw: Regex::new(r#"\$queryRawUnsafe\s*\(\s*`[^`]*\$\{"#).expect("valid regex"),
        }
    }

    fn make_claim(
        path_segments: &[String],
        file: &str,
        line: usize,
        matched: &str,
        orm: &str,
        description: &str,
    ) -> ExtractedClaim {
        build_claim(
            path_segments,
            &["db", "orm", "query"],
            "query_construction",
            ObjectValue::Text(format!("interpolated_{}", orm)),
            file,
            line,
            matched,
            0.9,
            description,
        )
    }
}

impl Extractor for OrmInjectionExtractor {
    fn name(&self) -> &str {
        "orm_injection"
    }

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

    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;

            match language {
                Language::Python => {
                    // Django raw queries
                    if let Some(m) = self.django_raw.find(line) {
                        claims.push(Self::make_claim(
                            path_segments,
                            file,
                            line_num,
                            m.as_str(),
                            "django",
                            "Django .raw() with string interpolation (SQL injection risk)",
                        ));
                    }
                    // Django extra queries
                    if let Some(m) = self.django_extra.find(line) {
                        claims.push(Self::make_claim(
                            path_segments,
                            file,
                            line_num,
                            m.as_str(),
                            "django",
                            "Django .extra() with interpolation (SQL injection risk)",
                        ));
                    }
                    // SQLAlchemy text
                    if let Some(m) = self.sqlalchemy_text.find(line) {
                        claims.push(Self::make_claim(
                            path_segments,
                            file,
                            line_num,
                            m.as_str(),
                            "sqlalchemy",
                            "SQLAlchemy text() with interpolation (SQL injection risk)",
                        ));
                    }
                    // SQLAlchemy execute
                    if let Some(m) = self.sqlalchemy_exec.find(line) {
                        claims.push(Self::make_claim(
                            path_segments,
                            file,
                            line_num,
                            m.as_str(),
                            "sqlalchemy",
                            "SQLAlchemy execute() with f-string (SQL injection risk)",
                        ));
                    }
                }
                Language::JavaScript | Language::TypeScript => {
                    // Sequelize raw query
                    if let Some(m) = self.sequelize_raw.find(line) {
                        claims.push(Self::make_claim(
                            path_segments,
                            file,
                            line_num,
                            m.as_str(),
                            "sequelize",
                            "Sequelize raw query with template literal (SQL injection risk)",
                        ));
                    }
                    // TypeORM where
                    if let Some(m) = self.typeorm_where.find(line) {
                        claims.push(Self::make_claim(
                            path_segments,
                            file,
                            line_num,
                            m.as_str(),
                            "typeorm",
                            "TypeORM where() with template literal (SQL injection risk)",
                        ));
                    }
                    // Prisma queryRawUnsafe
                    if let Some(m) = self.prisma_raw.find(line) {
                        claims.push(Self::make_claim(
                            path_segments,
                            file,
                            line_num,
                            m.as_str(),
                            "prisma",
                            "Prisma $queryRawUnsafe with interpolation (SQL injection risk)",
                        ));
                    }
                }
                Language::Go => {
                    // GORM Raw
                    if let Some(m) = self.gorm_raw.find(line) {
                        claims.push(Self::make_claim(
                            path_segments,
                            file,
                            line_num,
                            m.as_str(),
                            "gorm",
                            "GORM Raw() with fmt.Sprintf (SQL injection risk)",
                        ));
                    }
                }
                _ => {}
            }
        }

        claims
    }
}

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

    #[test]
    fn test_django_raw_fstring() {
        let extractor = OrmInjectionExtractor::new();
        let content = r#"
            users = User.objects.raw(f"SELECT * FROM users WHERE name = '{name}'")
        "#;

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

        assert_eq!(claims.len(), 1);
        assert!(claims[0].concept_path.contains("db/orm/query"));
    }

    #[test]
    fn test_django_raw_format() {
        let extractor = OrmInjectionExtractor::new();
        let content = r#"
            users = User.objects.raw("SELECT * FROM users WHERE id = {}".format(user_id))
        "#;

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

        // This matches the django_raw pattern (has {} and .format)
        assert_eq!(claims.len(), 1);
    }

    #[test]
    fn test_sqlalchemy_text() {
        let extractor = OrmInjectionExtractor::new();
        let content = r#"
            result = session.execute(text(f"SELECT * FROM users WHERE id = {user_id}"))
        "#;

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

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

    #[test]
    fn test_sqlalchemy_execute_fstring() {
        let extractor = OrmInjectionExtractor::new();
        let content = r#"
            conn.execute(f"UPDATE users SET name = '{name}' WHERE id = {id}")
        "#;

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

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

    #[test]
    fn test_sequelize_raw() {
        let extractor = OrmInjectionExtractor::new();
        let content = r#"
            const users = await sequelize.query(`SELECT * FROM users WHERE id = ${userId}`);
        "#;

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

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

    #[test]
    fn test_typeorm_where() {
        let extractor = OrmInjectionExtractor::new();
        let content = r#"
            const user = await userRepo.createQueryBuilder("user")
                .where(`user.id = ${userId}`)
                .getOne();
        "#;

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

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

    #[test]
    fn test_gorm_raw() {
        let extractor = OrmInjectionExtractor::new();
        let content = r#"
            db.Raw(fmt.Sprintf("SELECT * FROM users WHERE id = %d", userID)).Scan(&user)
        "#;

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

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

    #[test]
    fn test_prisma_query_raw_unsafe() {
        let extractor = OrmInjectionExtractor::new();
        let content = r#"
            const users = await prisma.$queryRawUnsafe(`SELECT * FROM users WHERE id = ${userId}`);
        "#;

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

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

    #[test]
    fn test_no_false_positives_parameterized() {
        let extractor = OrmInjectionExtractor::new();
        // Safe: parameterized query - no f-string, no .format(), just %s placeholder
        let content = r#"
            users = User.objects.raw("SELECT * FROM users WHERE id = ?", [user_id])
        "#;

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

        assert!(claims.is_empty());
    }

    #[test]
    fn test_no_false_positives_orm_filter() {
        let extractor = OrmInjectionExtractor::new();
        // Safe: using ORM filter methods
        let content = r#"
            users = User.objects.filter(id=user_id)
        "#;

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

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