kimun 0.20.0

Code metrics tool — health score, complexity, duplication, hotspots, ownership
use std::io::Write;
use std::path::PathBuf;

use tempfile::tempdir;

use super::analyzer::SmellKind;
use crate::walk::{ExcludeFilter, WalkConfig};

#[test]
fn finds_long_function_with_correct_count() {
    let dir = tempdir().unwrap();
    let file = dir.path().join("example.rs");

    let mut f = std::fs::File::create(&file).unwrap();
    writeln!(f, "fn long_func() {{").unwrap();
    // 55 body lines > default 50
    for i in 0..55 {
        writeln!(f, "    let x{i} = {i};").unwrap();
    }
    writeln!(f, "}}").unwrap();

    let spec = crate::loc::language::detect(&file).unwrap();
    let result = super::analyze_file(&file, spec, 50, 4).unwrap().unwrap();

    assert!(
        result.total >= 1,
        "expected at least 1 smell, got {}",
        result.total
    );
    let long_fns: Vec<_> = result
        .smells
        .smells
        .iter()
        .filter(|s| s.kind == SmellKind::LongFunction)
        .collect();
    assert_eq!(long_fns.len(), 1);
    assert!(long_fns[0].detail.contains("long_func"));
}

#[test]
fn finds_todo_debt_in_comments() {
    let dir = tempdir().unwrap();
    let file = dir.path().join("debt.rs");

    let mut f = std::fs::File::create(&file).unwrap();
    writeln!(f, "fn main() {{").unwrap();
    writeln!(f, "    // TODO: fix this").unwrap();
    writeln!(f, "    // FIXME: also broken").unwrap();
    writeln!(f, "    let x = 1;").unwrap();
    writeln!(f, "}}").unwrap();

    let spec = crate::loc::language::detect(&file).unwrap();
    let result = super::analyze_file(&file, spec, 50, 4).unwrap().unwrap();

    let todos: Vec<_> = result
        .smells
        .smells
        .iter()
        .filter(|s| s.kind == SmellKind::TodoDebt)
        .collect();
    assert_eq!(todos.len(), 2);
    assert!(todos[0].detail.contains("TODO"));
    assert!(todos[1].detail.contains("FIXME"));
}

#[test]
fn finds_long_params_single_line() {
    let dir = tempdir().unwrap();
    let file = dir.path().join("params.rs");

    let mut f = std::fs::File::create(&file).unwrap();
    writeln!(f, "fn many(a: i32, b: i32, c: i32, d: i32, e: i32) {{").unwrap();
    writeln!(f, "    let x = a;").unwrap();
    writeln!(f, "}}").unwrap();

    let spec = crate::loc::language::detect(&file).unwrap();
    let result = super::analyze_file(&file, spec, 50, 4).unwrap().unwrap();

    let params: Vec<_> = result
        .smells
        .smells
        .iter()
        .filter(|s| s.kind == SmellKind::LongParameterList)
        .collect();
    assert_eq!(params.len(), 1);
    assert!(params[0].detail.contains("5 params"));
}

#[test]
fn finds_long_params_multiline() {
    let dir = tempdir().unwrap();
    let file = dir.path().join("multiline.rs");

    let mut f = std::fs::File::create(&file).unwrap();
    writeln!(f, "fn process(").unwrap();
    writeln!(f, "    a: i32,").unwrap();
    writeln!(f, "    b: i32,").unwrap();
    writeln!(f, "    c: i32,").unwrap();
    writeln!(f, "    d: i32,").unwrap();
    writeln!(f, "    e: i32,").unwrap();
    writeln!(f, ") {{").unwrap();
    writeln!(f, "    let x = a;").unwrap();
    writeln!(f, "}}").unwrap();

    let spec = crate::loc::language::detect(&file).unwrap();
    let result = super::analyze_file(&file, spec, 50, 4).unwrap().unwrap();

    let params: Vec<_> = result
        .smells
        .smells
        .iter()
        .filter(|s| s.kind == SmellKind::LongParameterList)
        .collect();
    assert_eq!(params.len(), 1, "multi-line signature should be detected");
    assert!(params[0].detail.contains("5 params"));
}

#[test]
fn no_smells_for_clean_file() {
    let dir = tempdir().unwrap();
    let file = dir.path().join("clean.rs");

    let mut f = std::fs::File::create(&file).unwrap();
    writeln!(f, "fn add(a: i32, b: i32) -> i32 {{").unwrap();
    writeln!(f, "    a + b").unwrap();
    writeln!(f, "}}").unwrap();

    let spec = crate::loc::language::detect(&file).unwrap();
    let result = super::analyze_file(&file, spec, 50, 4).unwrap();
    assert!(result.is_none(), "clean file should have no smells");
}

#[test]
fn json_output_runs_without_error() {
    let dir = tempdir().unwrap();
    let file = dir.path().join("test.rs");

    let mut f = std::fs::File::create(&file).unwrap();
    writeln!(f, "fn small() {{}}").unwrap();

    let filter = crate::walk::ExcludeFilter::default();
    let cfg = crate::walk::WalkConfig::new(dir.path(), true, &filter);
    let result = super::run(&cfg, true, 20, 50, 4, None);
    assert!(result.is_ok());
}

#[test]
fn doc_comments_with_use_not_false_positive() {
    let dir = tempdir().unwrap();
    let file = dir.path().join("doc.rs");

    let mut f = std::fs::File::create(&file).unwrap();
    writeln!(f, "/// use std::io::Read;").unwrap();
    writeln!(f, "/// use tokio::runtime::Runtime;").unwrap();
    writeln!(f, "fn main() {{}}").unwrap();

    let spec = crate::loc::language::detect(&file).unwrap();
    let result = super::analyze_file(&file, spec, 50, 4).unwrap();
    // Should not detect commented-out code for doc comments with `use`
    if let Some(ref metrics) = result {
        let commented: Vec<_> = metrics
            .smells
            .smells
            .iter()
            .filter(|s| s.kind == SmellKind::CommentedOutCode)
            .collect();
        assert!(
            commented.is_empty(),
            "doc comments with `use` should not trigger commented-out code detection"
        );
    }
}

#[test]
fn run_on_files_with_smell() {
    let dir = tempdir().unwrap();
    let file = dir.path().join("big.rs");
    let mut f = std::fs::File::create(&file).unwrap();
    writeln!(f, "fn big() {{").unwrap();
    for i in 0..55 {
        writeln!(f, "    let x{i} = {i};").unwrap();
    }
    writeln!(f, "}}").unwrap();

    let result = super::run_on_files(&[file], false, 20, 50, 4, None);
    assert!(result.is_ok());
}

#[test]
fn run_on_files_empty_list() {
    let result = super::run_on_files(&[], false, 20, 50, 4, None);
    assert!(result.is_ok());
}

#[test]
fn run_on_files_skips_unknown_extension() {
    let dir = tempdir().unwrap();
    let file = dir.path().join("data.xyz");
    std::fs::write(&file, "hello world").unwrap();

    let result = super::run_on_files(&[file], false, 20, 50, 4, None);
    assert!(result.is_ok());
}

#[test]
fn run_on_files_json_empty() {
    let result = super::run_on_files(&[], true, 20, 50, 4, None);
    assert!(result.is_ok());
}

#[test]
fn run_on_files_nonexistent_path_skips() {
    let paths = vec![PathBuf::from("/nonexistent/path/fake.rs")];
    let result = super::run_on_files(&paths, false, 20, 50, 4, None);
    assert!(
        result.is_ok(),
        "nonexistent files should be skipped, not panic"
    );
}

#[test]
fn run_on_files_github_format_with_smells() {
    let dir = tempdir().unwrap();
    let file = dir.path().join("big.rs");
    let mut f = std::fs::File::create(&file).unwrap();
    writeln!(f, "fn big() {{").unwrap();
    for i in 0..55 {
        writeln!(f, "    let x{i} = {i};").unwrap();
    }
    writeln!(f, "}}").unwrap();

    let result = super::run_on_files(&[file], false, 20, 50, 4, Some("github"));
    assert!(result.is_ok(), "github format should succeed");
}

#[test]
fn run_on_files_json_format_via_format_param() {
    let dir = tempdir().unwrap();
    let file = dir.path().join("big2.rs");
    let mut f = std::fs::File::create(&file).unwrap();
    writeln!(f, "fn big() {{").unwrap();
    for i in 0..55 {
        writeln!(f, "    let x{i} = {i};").unwrap();
    }
    writeln!(f, "}}").unwrap();

    let result = super::run_on_files(&[file], false, 20, 50, 4, Some("json"));
    assert!(result.is_ok(), "json format param should succeed");
}

#[test]
fn run_on_files_empty_json_format() {
    // json=false but format=Some("json") with empty list → print_json([])
    let result = super::run_on_files(&[], false, 20, 50, 4, Some("json"));
    assert!(result.is_ok());
}

#[test]
fn run_github_format_with_smells() {
    let dir = tempdir().unwrap();
    let file = dir.path().join("big3.rs");
    let mut f = std::fs::File::create(&file).unwrap();
    writeln!(f, "fn big() {{").unwrap();
    for i in 0..55 {
        writeln!(f, "    let x{i} = {i};").unwrap();
    }
    writeln!(f, "}}").unwrap();

    let filter = ExcludeFilter::default();
    let cfg = WalkConfig::new(dir.path(), true, &filter);
    let result = super::run(&cfg, false, 20, 50, 4, Some("github"));
    assert!(result.is_ok(), "run github format should succeed");
}

#[test]
fn analyze_file_returns_none_for_unsupported_language() {
    let dir = tempdir().unwrap();
    let file = dir.path().join("data.json");
    std::fs::write(&file, "{\"key\": \"value\"}\n").unwrap();
    let spec = crate::loc::language::detect(&file).unwrap();
    // JSON has no function detection markers → returns None
    let result = super::analyze_file(&file, spec, 50, 4).unwrap();
    assert!(result.is_none(), "JSON should have no markers, return None");
}