use std::path::Path;

use compliance_core::models::{Finding, ScanType, Severity};
use compliance_core::CoreError;
use tokio::process::Command;

use crate::pipeline::dedup;

use super::run_with_timeout;

pub(super) async fn run_eslint(repo_path: &Path, repo_id: &str) -> Result<Vec<Finding>, CoreError> {
    // Use the project-local eslint binary directly, not npx (which can hang downloading)
    let eslint_bin = repo_path.join("node_modules/.bin/eslint");
    let child = Command::new(eslint_bin)
        .args([".", "--format", "json", "--no-error-on-unmatched-pattern"])
        .current_dir(repo_path)
        .stdout(std::process::Stdio::piped())
        .stderr(std::process::Stdio::piped())
        .spawn()
        .map_err(|e| CoreError::Scanner {
            scanner: "eslint".to_string(),
            source: Box::new(e),
        })?;

    let output = run_with_timeout(child, "eslint").await?;

    if output.stdout.is_empty() {
        return Ok(Vec::new());
    }

    let results: Vec<EslintFileResult> = serde_json::from_slice(&output.stdout).unwrap_or_default();

    let mut findings = Vec::new();
    for file_result in results {
        for msg in file_result.messages {
            let severity = match msg.severity {
                2 => Severity::Medium,
                _ => Severity::Low,
            };

            let rule_id = msg.rule_id.unwrap_or_default();
            let fingerprint = dedup::compute_fingerprint(&[
                repo_id,
                "eslint",
                &rule_id,
                &file_result.file_path,
                &msg.line.to_string(),
            ]);

            let mut finding = Finding::new(
                repo_id.to_string(),
                fingerprint,
                "eslint".to_string(),
                ScanType::Lint,
                format!("[eslint] {}", msg.message),
                msg.message,
                severity,
            );
            finding.rule_id = Some(rule_id);
            finding.file_path = Some(file_result.file_path.clone());
            finding.line_number = Some(msg.line);
            findings.push(finding);
        }
    }

    Ok(findings)
}

#[derive(serde::Deserialize)]
struct EslintFileResult {
    #[serde(rename = "filePath")]
    file_path: String,
    messages: Vec<EslintMessage>,
}

#[derive(serde::Deserialize)]
struct EslintMessage {
    #[serde(rename = "ruleId")]
    rule_id: Option<String>,
    severity: u8,
    message: String,
    line: u32,
}

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

    #[test]
    fn deserialize_eslint_output() {
        let json = r#"[
            {
                "filePath": "/home/user/project/src/app.js",
                "messages": [
                    {
                        "ruleId": "no-unused-vars",
                        "severity": 2,
                        "message": "'x' is defined but never used.",
                        "line": 10
                    },
                    {
                        "ruleId": "semi",
                        "severity": 1,
                        "message": "Missing semicolon.",
                        "line": 15
                    }
                ]
            }
        ]"#;
        let results: Vec<EslintFileResult> = serde_json::from_str(json).unwrap();
        assert_eq!(results.len(), 1);
        assert_eq!(results[0].file_path, "/home/user/project/src/app.js");
        assert_eq!(results[0].messages.len(), 2);

        assert_eq!(
            results[0].messages[0].rule_id,
            Some("no-unused-vars".to_string())
        );
        assert_eq!(results[0].messages[0].severity, 2);
        assert_eq!(results[0].messages[0].line, 10);

        assert_eq!(results[0].messages[1].severity, 1);
    }

    #[test]
    fn deserialize_eslint_null_rule_id() {
        let json = r#"[
            {
                "filePath": "src/index.js",
                "messages": [
                    {
                        "ruleId": null,
                        "severity": 2,
                        "message": "Parsing error: Unexpected token",
                        "line": 1
                    }
                ]
            }
        ]"#;
        let results: Vec<EslintFileResult> = serde_json::from_str(json).unwrap();
        assert_eq!(results[0].messages[0].rule_id, None);
    }

    #[test]
    fn deserialize_eslint_empty_messages() {
        let json = r#"[{"filePath": "src/clean.js", "messages": []}]"#;
        let results: Vec<EslintFileResult> = serde_json::from_str(json).unwrap();
        assert_eq!(results[0].messages.len(), 0);
    }

    #[test]
    fn deserialize_eslint_empty_array() {
        let json = "[]";
        let results: Vec<EslintFileResult> = serde_json::from_str(json).unwrap();
        assert!(results.is_empty());
    }

    #[test]
    fn eslint_severity_mapping() {
        // severity 2 = error -> Medium, anything else -> Low
        assert_eq!(
            match 2u8 {
                2 => "Medium",
                _ => "Low",
            },
            "Medium"
        );
        assert_eq!(
            match 1u8 {
                2 => "Medium",
                _ => "Low",
            },
            "Low"
        );
        assert_eq!(
            match 0u8 {
                2 => "Medium",
                _ => "Low",
            },
            "Low"
        );
    }
}
