barad-dur 0.18.0

The all-seeing repository analyzer
Documentation
use super::*;
use crate::snapshot::*;
use chrono::{Duration, Utc};
use std::path::PathBuf;

#[test]
fn compute_team_small_team_metrics_unscored() {
    // Fewer than MIN_TEAM_SIZE authors → all metrics N/A, category scores 100
    let mut snapshot = RepoSnapshot::new(
        PathBuf::from("/tmp"),
        "test".into(),
        "main".into(),
        TimeWindow::default(),
    );
    for i in 0..3 {
        snapshot.authors.push(Author {
            id: i,
            name: format!("Author {i}"),
            email: format!("a{i}@t.com"),
        });
    }
    let result = compute_team(&snapshot, &crate::config::TeamThresholds::default());
    // Category keeps 100 (gates must not punish N/A), but the individual
    // metrics carry no score — renderers show a dash, not a fake 100.
    assert_eq!(result.score, 100);
    assert!(result.metrics.iter().all(|m| m.score.is_none()));
    assert!(result
        .metrics
        .iter()
        .all(|m| m.description.contains("not applicable")));
}

fn make_solo_snapshot() -> RepoSnapshot {
    let mut snapshot = RepoSnapshot::new(
        PathBuf::from("/tmp"),
        "test".into(),
        "main".into(),
        TimeWindow::default(),
    );
    snapshot.authors = vec![Author {
        id: 0,
        name: "Alice".into(),
        email: "a@t.com".into(),
    }];
    snapshot
}

#[test]
fn knowledge_distribution_solo_project_has_no_score() {
    let snapshot = make_solo_snapshot();
    let result = knowledge_distribution(&snapshot, &crate::config::TeamThresholds::default());
    assert_eq!(result.score, None);
    assert!(result.description.contains("Solo project"));
}

#[test]
fn ownership_clarity_solo_project_has_no_score() {
    let snapshot = make_solo_snapshot();
    let result = ownership_clarity(&snapshot, &crate::config::TeamThresholds::default());
    assert_eq!(result.score, None);
    assert!(result.description.contains("Solo project"));
}

#[test]
fn collaboration_patterns_solo_project_has_no_score() {
    let snapshot = make_solo_snapshot();
    let result = collaboration_patterns(&snapshot, &crate::config::TeamThresholds::default());
    assert_eq!(result.score, None);
    assert!(result.description.contains("Solo project"));
}

#[test]
fn knowledge_distribution_detects_concentration() {
    let mut snapshot = RepoSnapshot::new(
        PathBuf::from("/tmp"),
        "test".into(),
        "main".into(),
        TimeWindow::default(),
    );

    snapshot.authors = vec![
        Author {
            id: 0,
            name: "Alice".into(),
            email: "a@t.com".into(),
        },
        Author {
            id: 1,
            name: "Bob".into(),
            email: "b@t.com".into(),
        },
        Author {
            id: 2,
            name: "Carol".into(),
            email: "c@t.com".into(),
        },
    ];

    let now = Utc::now();
    // Alice owns 95 lines, Bob 4, Carol 1 → very high Gini
    let mut blame = Vec::new();
    for _ in 0..95 {
        blame.push(BlameLine::new(0, now));
    }
    for _ in 0..4 {
        blame.push(BlameLine::new(1, now));
    }
    for _ in 0..1 {
        blame.push(BlameLine::new(2, now));
    }
    snapshot.blame_map.insert(PathBuf::from("file.rs"), blame);

    let result = knowledge_distribution(&snapshot, &crate::config::TeamThresholds::default());
    match result.raw_value {
        RawValue::Float(gini) => assert!(gini >= 0.5, "Expected Gini >= 0.5, got {}", gini),
        _ => panic!("Expected Float"),
    }
    assert!(result.score.unwrap() <= 50);
}

#[test]
fn contributor_activity_counts_active() {
    let mut snapshot = RepoSnapshot::new(
        PathBuf::from("/tmp"),
        "test".into(),
        "main".into(),
        TimeWindow::default(),
    );

    snapshot.authors = vec![
        Author {
            id: 0,
            name: "Alice".into(),
            email: "a@t.com".into(),
        },
        Author {
            id: 1,
            name: "Bob".into(),
            email: "b@t.com".into(),
        },
        Author {
            id: 2,
            name: "Carol".into(),
            email: "c@t.com".into(),
        },
        Author {
            id: 3,
            name: "Dave".into(),
            email: "d@t.com".into(),
        },
        Author {
            id: 4,
            name: "Eve".into(),
            email: "e@t.com".into(),
        },
    ];

    let now = Utc::now();
    // Only 3 authors have recent commits
    for i in 0..3 {
        snapshot.commits.push(Commit {
            id: CommitId(i as u32),
            author: i,
            timestamp: now - Duration::days(10),
            message: "msg".into(),
            files_changed: vec![],
            is_merge: false,
            parent_count: 1,
        });
    }
    snapshot.build_indexes();

    let result = contributor_activity(&snapshot, &crate::config::TeamThresholds::default());
    match result.raw_value {
        RawValue::Percentage(p) => assert!((p - 60.0).abs() < 1.0, "Expected ~60%, got {}", p),
        _ => panic!("Expected Percentage"),
    }
}

#[test]
fn ownership_clarity_detects_owners() {
    let mut snapshot = RepoSnapshot::new(
        PathBuf::from("/tmp"),
        "test".into(),
        "main".into(),
        TimeWindow::default(),
    );
    snapshot.authors = vec![
        Author {
            id: 0,
            name: "Alice".into(),
            email: "a@t.com".into(),
        },
        Author {
            id: 1,
            name: "Bob".into(),
            email: "b@t.com".into(),
        },
    ];

    let now = Utc::now();
    // File 1: Alice 80%, Bob 20% → clear owner
    let mut blame1 = Vec::new();
    for _ in 0..80 {
        blame1.push(BlameLine::new(0, now));
    }
    for _ in 0..20 {
        blame1.push(BlameLine::new(1, now));
    }
    snapshot.blame_map.insert(PathBuf::from("f1.rs"), blame1);

    // File 2: 50/50 → no clear owner
    let mut blame2 = Vec::new();
    for _ in 0..50 {
        blame2.push(BlameLine::new(0, now));
    }
    for _ in 0..50 {
        blame2.push(BlameLine::new(1, now));
    }
    snapshot.blame_map.insert(PathBuf::from("f2.rs"), blame2);

    let result = ownership_clarity(&snapshot, &crate::config::TeamThresholds::default());
    // 1 out of 2 files has clear owner = 50%
    match result.raw_value {
        RawValue::Percentage(p) => assert!((p - 50.0).abs() < 1.0),
        _ => panic!("Expected Percentage"),
    }
}

#[test]
fn collaboration_detects_silos() {
    let mut snapshot = RepoSnapshot::new(
        PathBuf::from("/tmp"),
        "test".into(),
        "main".into(),
        TimeWindow::default(),
    );
    snapshot.authors = vec![
        Author {
            id: 0,
            name: "Alice".into(),
            email: "a@t.com".into(),
        },
        Author {
            id: 1,
            name: "Bob".into(),
            email: "b@t.com".into(),
        },
    ];

    let now = Utc::now();
    // "auth" directory: 100% Alice → silo
    let mut blame_auth = Vec::new();
    for _ in 0..100 {
        blame_auth.push(BlameLine::new(0, now));
    }
    snapshot
        .blame_map
        .insert(PathBuf::from("auth/login.rs"), blame_auth);

    // "api" directory: 60/40 split → NOT a silo
    let mut blame_api = Vec::new();
    for _ in 0..60 {
        blame_api.push(BlameLine::new(0, now));
    }
    for _ in 0..40 {
        blame_api.push(BlameLine::new(1, now));
    }
    snapshot
        .blame_map
        .insert(PathBuf::from("api/routes.rs"), blame_api);

    let result = collaboration_patterns(&snapshot, &crate::config::TeamThresholds::default());
    match result.raw_value {
        RawValue::Count(c) => assert_eq!(c, 1, "Should detect 1 silo (auth)"),
        _ => panic!("Expected Count"),
    }
}

#[test]
fn merge_patterns_counts_merges() {
    let mut snapshot = RepoSnapshot::new(
        PathBuf::from("/tmp"),
        "test".into(),
        "main".into(),
        TimeWindow::default(),
    );

    let now = Utc::now();
    for i in 0..20 {
        snapshot.commits.push(Commit {
            id: CommitId(i as u32),
            author: 0,
            timestamp: now - Duration::hours(i * 24),
            message: "msg".into(),
            files_changed: vec![],
            is_merge: i < 5, // First 5 are merges
            parent_count: if i < 5 { 2 } else { 1 },
        });
    }

    let result = merge_patterns(&snapshot, &crate::config::TeamThresholds::default());
    match result.raw_value {
        RawValue::Count(c) => assert_eq!(c, 5),
        _ => panic!("Expected Count"),
    }
}