use std::fs;
use tempfile::TempDir;
use super::test_support::{self, bind_script, landing_with, op_log, task};
use super::{parse, render, run};
use crate::config::EffectiveConfig;
use crate::edge::Edge;
use crate::log::Level;
use crate::task::Task;
use crate::verb::Verb;
fn edge_with(tmp: &TempDir, tasks: &[(&str, Task)]) -> Edge {
let edge = test_support::edge(tmp.path(), 0);
let store = edge.xdg.clone_dir(&edge.invocation_path).store();
for (id, t) in tasks {
crate::taskfile::write_task(&store, id, t).unwrap();
}
edge
}
#[test]
fn run_dispatches_each_read_verb() {
let tmp = TempDir::new().unwrap();
let edge = edge_with(&tmp, &[]);
let gs = test_support::git_store_at(&edge.xdg.clone_dir(&edge.invocation_path).store());
gs.create("bl-1", &task("One", 1), 1);
run(&edge, Verb::List, &[]).unwrap();
run(&edge, Verb::List, &["--status".into(), "ready".into()]).unwrap();
run(&edge, Verb::Show, &["bl-1".into()]).unwrap();
}
#[test]
fn run_errors_on_a_missing_ball_and_a_non_read_verb() {
let tmp = TempDir::new().unwrap();
let edge = edge_with(&tmp, &[]);
assert!(run(&edge, Verb::Show, &["bl-x".into()]).is_err());
assert!(run(&edge, Verb::Create, &[]).is_err()); }
#[test]
fn run_lists_and_shows_dead_balls_from_history() {
let tmp = TempDir::new().unwrap();
let edge = edge_with(&tmp, &[]);
let store = edge.xdg.clone_dir(&edge.invocation_path).store();
let gs = test_support::git_store_at(&store);
gs.create("bl-dead", &task("Gone", 1), 1).retire("bl-dead", "close", 9);
run(&edge, Verb::List, &["--status".into(), "closed".into()]).unwrap();
run(&edge, Verb::List, &["--all".into()]).unwrap();
run(&edge, Verb::Show, &["bl-dead".into()]).unwrap();
}
#[test]
fn a_read_narrates_begin_and_done_at_debug_only() {
let tmp = TempDir::new().unwrap();
let mut edge = edge_with(&tmp, &[("bl-1", task("One", 1))]);
run(&edge, Verb::List, &[]).unwrap();
assert_eq!(op_log(&edge), "");
edge.log_level = Some("debug".into());
run(&edge, Verb::List, &[]).unwrap();
let log = op_log(&edge);
assert!(log.contains(r#""lvl":"debug","src":"core","op":"list","msg":"begin""#));
assert!(log.contains(r#""lvl":"debug","src":"core","op":"list","msg":"done""#));
}
#[test]
fn a_failing_read_narrates_abort_at_debug() {
let tmp = TempDir::new().unwrap();
let mut edge = edge_with(&tmp, &[]);
let gs = test_support::git_store_at(&edge.xdg.clone_dir(&edge.invocation_path).store());
gs.create("bl-other", &task("Other", 1), 1); edge.log_level = Some("debug".into());
assert!(run(&edge, Verb::Show, &["bl-x".into()]).is_err());
let log = op_log(&edge);
assert!(log.contains(r#""msg":"begin""#));
assert!(log.contains(r#""lvl":"debug","src":"core","op":"show","msg":"abort no such ball: bl-x""#));
}
#[test]
fn run_fails_loud_on_a_bad_threshold_source() {
let tmp = TempDir::new().unwrap();
let mut edge = edge_with(&tmp, &[]);
edge.log_level = Some("noisy".into());
assert!(run(&edge, Verb::List, &[]).is_err());
edge.log_level = None;
let landing = landing_with(&edge, "[hooks]\n");
fs::write(landing.join("config").join("balls.toml"), "not = toml = at all").unwrap();
assert!(run(&edge, Verb::List, &[]).is_err());
}
fn wire_listing_hooks(tmp: &TempDir, edge: &Edge) {
let landing = landing_with(edge, "[hooks]\n\"list\" = [\"fake\"]\n");
let script = format!(
"#!/bin/sh\npayload=$(cat)\ncase \"$payload\" in *bl-id*) exit 1;; esac\necho \"$1 $2\" >> {}/hits\n",
tmp.path().display()
);
bind_script(tmp.path(), &landing, "fake", &script);
}
#[test]
fn a_wired_list_hook_actually_dispatches() {
let tmp = TempDir::new().unwrap();
let edge = edge_with(&tmp, &[("bl-1", task("One", 1))]);
wire_listing_hooks(&tmp, &edge);
run(&edge, Verb::List, &[]).unwrap();
assert_eq!(fs::read_to_string(tmp.path().join("hits")).unwrap(), "list read\n");
}
#[test]
fn json_reads_never_dispatch() {
let tmp = TempDir::new().unwrap();
let edge = edge_with(&tmp, &[("bl-1", task("One", 1))]);
wire_listing_hooks(&tmp, &edge);
run(&edge, Verb::List, &["--json".into()]).unwrap();
assert!(!tmp.path().join("hits").exists());
}
#[test]
fn the_folded_line_lands_in_the_human_render() {
let tmp = TempDir::new().unwrap();
let edge = edge_with(&tmp, &[("bl-1", task("One", 1))]);
let landing = landing_with(&edge, "[hooks]\n\"list\" = [\"fake\"]\n");
bind_script(tmp.path(), &landing, "fake", "#!/bin/sh\ncat > /dev/null\necho folded-line\n");
let store = edge.xdg.clone_dir(&edge.invocation_path).store();
let (cfg, log) = (EffectiveConfig::default(), test_support::log_at(tmp.path(), Level::Debug, Verb::List));
let flags = parse(Verb::List, &[]).unwrap();
let out = render(&edge, Verb::List, &flags, &store, &cfg, &log).unwrap();
assert!(out.contains("bl-1") && out.ends_with("folded-line\n"), "{out}");
}
#[test]
fn legacy_reads_project_the_old_store_through_the_ordinary_render() {
let tmp = TempDir::new().unwrap();
let edge = edge_with(&tmp, &[("bl-new", task("Greenfield", 1))]);
test_support::legacy_store_at(
&tmp.path().join("proj"),
&[(
"bl-old.json",
r#"{"id":"bl-old","title":"Old one","status":"open",
"created_at":"2026-01-01T00:00:00Z","updated_at":"2026-01-01T00:00:00Z"}"#,
)],
);
let store = edge.xdg.clone_dir(&edge.invocation_path).store();
let (cfg, log) = (EffectiveConfig::default(), test_support::log_at(tmp.path(), Level::Info, Verb::List));
let flags = parse(Verb::List, &["--legacy".into(), "--plain".into()]).unwrap();
let out = render(&edge, Verb::List, &flags, &store, &cfg, &log).unwrap();
assert!(out.contains("bl-old") && !out.contains("bl-new"), "{out}");
let flags = parse(Verb::Show, &["bl-old".into(), "--legacy".into(), "--plain".into()]).unwrap();
let out = render(&edge, Verb::Show, &flags, &store, &cfg, &log).unwrap();
assert!(out.contains("Old one"), "{out}");
let flags = parse(Verb::Show, &["bl-new".into(), "--legacy".into()]).unwrap();
let err = render(&edge, Verb::Show, &flags, &store, &cfg, &log).unwrap_err();
assert!(err.to_string().contains("no such legacy ball: bl-new"), "{err}");
}