use crate::annotation::{Fuse, Status};
use chrono::NaiveDate;
use std::io::{self, Write};
pub fn select_armory_fuses(fuses: &[Fuse], today: NaiveDate, limit: usize) -> Vec<&Fuse> {
let mut selected: Vec<&Fuse> = fuses
.iter()
.filter(|fuse| matches!(fuse.status, Status::Detonated | Status::Ticking))
.collect();
selected.sort_unstable_by(|a, b| {
armory_status_order(&a.status)
.cmp(&armory_status_order(&b.status))
.then(a.days_from_today(today).cmp(&b.days_from_today(today)))
.then(a.file.cmp(&b.file))
.then(a.line.cmp(&b.line))
});
selected.truncate(limit);
selected
}
pub fn print_armory_to_writer<W: Write>(
fuses: &[&Fuse],
today: NaiveDate,
mut writer: W,
) -> io::Result<()> {
writeln!(writer, "Most volatile fuses")?;
if fuses.is_empty() {
writeln!(writer)?;
writeln!(writer, "Magazine is quiet.")?;
return Ok(());
}
for (idx, fuse) in fuses.iter().enumerate() {
let days = fuse.days_from_today(today);
let status = match fuse.status {
Status::Detonated => "DETONATED",
Status::Ticking => "TICKING",
Status::Inert => "INERT",
};
let delta = if days < 0 {
format!("{}d overdue", days.abs())
} else {
format!("{}d left", days)
};
writeln!(
writer,
"{}. {:<9} {:>11} {}:{}",
idx + 1,
status,
delta,
fuse.file.display(),
fuse.line
)?;
writeln!(writer, " {}", fuse.annotation_text())?;
if idx + 1 < fuses.len() {
writeln!(writer)?;
}
}
Ok(())
}
pub fn print_armory(fuses: &[&Fuse], today: NaiveDate) {
let stdout = io::stdout();
let mut handle = stdout.lock();
let _ = print_armory_to_writer(fuses, today, &mut handle);
}
fn armory_status_order(status: &Status) -> u8 {
match status {
Status::Detonated => 0,
Status::Ticking => 1,
Status::Inert => 2,
}
}
trait FuseAnnotationText {
fn annotation_text(&self) -> String;
}
impl FuseAnnotationText for Fuse {
fn annotation_text(&self) -> String {
match self.owner.as_deref() {
Some(owner) => format!(
"{}[{}][{}]: {}",
self.tag,
self.date_str(),
owner,
self.message
),
None => format!("{}[{}]: {}", self.tag, self.date_str(), self.message),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::path::PathBuf;
fn date(s: &str) -> NaiveDate {
NaiveDate::parse_from_str(s, "%Y-%m-%d").unwrap()
}
fn fuse(file: &str, line: usize, expiry: &str, status: Status, message: &str) -> Fuse {
Fuse {
file: PathBuf::from(file),
line,
tag: "TODO".to_string(),
date: date(expiry),
owner: None,
message: message.to_string(),
status,
blamed_owner: None,
}
}
#[test]
fn test_select_armory_fuses_ranks_detonated_then_ticking() {
let today = date("2026-04-18");
let fuses = vec![
fuse("soon.rs", 1, "2026-04-19", Status::Ticking, "one day"),
fuse("old.rs", 1, "2026-04-01", Status::Detonated, "old"),
fuse("older.rs", 1, "2026-03-01", Status::Detonated, "older"),
fuse("later.rs", 1, "2026-04-25", Status::Ticking, "later"),
fuse("safe.rs", 1, "2026-12-01", Status::Inert, "safe"),
];
let selected = select_armory_fuses(&fuses, today, 10);
assert_eq!(selected.len(), 4);
assert_eq!(selected[0].file, PathBuf::from("older.rs"));
assert_eq!(selected[1].file, PathBuf::from("old.rs"));
assert_eq!(selected[2].file, PathBuf::from("soon.rs"));
assert_eq!(selected[3].file, PathBuf::from("later.rs"));
}
#[test]
fn test_select_armory_fuses_honors_limit() {
let today = date("2026-04-18");
let fuses = vec![
fuse("a.rs", 1, "2026-04-01", Status::Detonated, "a"),
fuse("b.rs", 1, "2026-04-02", Status::Detonated, "b"),
fuse("c.rs", 1, "2026-04-03", Status::Detonated, "c"),
];
let selected = select_armory_fuses(&fuses, today, 2);
assert_eq!(selected.len(), 2);
}
#[test]
fn test_print_armory_to_writer_empty() {
let today = date("2026-04-18");
let mut out = Vec::new();
print_armory_to_writer(&[], today, &mut out).unwrap();
let text = String::from_utf8(out).unwrap();
assert!(text.contains("Most volatile fuses"));
assert!(text.contains("Magazine is quiet."));
}
#[test]
fn test_print_armory_to_writer_includes_annotation() {
let today = date("2026-04-18");
let mut item = fuse(
"src/auth.rs",
42,
"2026-04-01",
Status::Detonated,
"remove fallback",
);
item.owner = Some("alice".to_string());
let selected = vec![&item];
let mut out = Vec::new();
print_armory_to_writer(&selected, today, &mut out).unwrap();
let text = String::from_utf8(out).unwrap();
assert!(text.contains("DETONATED"));
assert!(text.contains("17d overdue"));
assert!(text.contains("src/auth.rs:42"));
assert!(text.contains("TODO[2026-04-01][alice]: remove fallback"));
}
}