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_ruff(repo_path: &Path, repo_id: &str) -> Result<Vec<Finding>, CoreError> {
    let child = Command::new("ruff")
        .args(["check", ".", "--output-format", "json", "--exit-zero"])
        .current_dir(repo_path)
        .stdout(std::process::Stdio::piped())
        .stderr(std::process::Stdio::piped())
        .spawn()
        .map_err(|e| CoreError::Scanner {
            scanner: "ruff".to_string(),
            source: Box::new(e),
        })?;

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

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

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

    let findings = results
        .into_iter()
        .map(|r| {
            let severity = if r.code.starts_with('E') || r.code.starts_with('F') {
                Severity::Medium
            } else {
                Severity::Low
            };

            let fingerprint = dedup::compute_fingerprint(&[
                repo_id,
                "ruff",
                &r.code,
                &r.filename,
                &r.location.row.to_string(),
            ]);

            let mut finding = Finding::new(
                repo_id.to_string(),
                fingerprint,
                "ruff".to_string(),
                ScanType::Lint,
                format!("[ruff] {}: {}", r.code, r.message),
                r.message,
                severity,
            );
            finding.rule_id = Some(r.code);
            finding.file_path = Some(r.filename);
            finding.line_number = Some(r.location.row);
            finding
        })
        .collect();

    Ok(findings)
}

#[derive(serde::Deserialize)]
struct RuffResult {
    code: String,
    message: String,
    filename: String,
    location: RuffLocation,
}

#[derive(serde::Deserialize)]
struct RuffLocation {
    row: u32,
}

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

    #[test]
    fn deserialize_ruff_output() {
        let json = r#"[
            {
                "code": "E501",
                "message": "Line too long (120 > 79 characters)",
                "filename": "src/main.py",
                "location": {"row": 42}
            },
            {
                "code": "F401",
                "message": "`os` imported but unused",
                "filename": "src/utils.py",
                "location": {"row": 1}
            }
        ]"#;
        let results: Vec<RuffResult> = serde_json::from_str(json).unwrap();
        assert_eq!(results.len(), 2);

        assert_eq!(results[0].code, "E501");
        assert_eq!(results[0].filename, "src/main.py");
        assert_eq!(results[0].location.row, 42);

        assert_eq!(results[1].code, "F401");
        assert_eq!(results[1].location.row, 1);
    }

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

    #[test]
    fn ruff_severity_e_and_f_are_medium() {
        for code in &["E501", "E302", "F401", "F811"] {
            let is_medium = code.starts_with('E') || code.starts_with('F');
            assert!(is_medium, "Expected {code} to be Medium severity");
        }
    }

    #[test]
    fn ruff_severity_others_are_low() {
        for code in &["W291", "I001", "D100", "C901", "N801"] {
            let is_medium = code.starts_with('E') || code.starts_with('F');
            assert!(!is_medium, "Expected {code} to be Low severity");
        }
    }

    #[test]
    fn deserialize_ruff_with_extra_fields() {
        // Ruff output may contain additional fields we don't use
        let json = r#"[{
            "code": "W291",
            "message": "Trailing whitespace",
            "filename": "app.py",
            "location": {"row": 3, "column": 10},
            "end_location": {"row": 3, "column": 11},
            "fix": null,
            "noqa_row": 3
        }]"#;
        let results: Vec<RuffResult> = serde_json::from_str(json).unwrap();
        assert_eq!(results.len(), 1);
        assert_eq!(results[0].code, "W291");
    }
}
