use std::fs;
use std::path::{Path, PathBuf};
use chrono::{Duration as ChronoDuration, Utc};
use grex_core::manifest::append::read_all;
use grex_core::manifest::event::Event;
use grex_core::tree::quarantine::{
prune_quarantine, restore_quarantine, QuarantineConfig, RetentionConfig,
};
use tempfile::tempdir;
fn setup_meta(tmp: &Path) -> (PathBuf, QuarantineConfig) {
let meta = tmp.join("meta");
fs::create_dir_all(meta.join(".grex")).unwrap();
let cfg = QuarantineConfig {
trash_root: meta.join(".grex").join("trash"),
audit_log: meta.join(".grex").join("events.jsonl"),
};
(meta, cfg)
}
fn ts_n_days_ago(days: i64) -> String {
(Utc::now() - ChronoDuration::days(days)).format("%Y-%m-%dT%H-%M-%S%.3fZ").to_string()
}
fn populate_trash_entry(trash_root: &Path, ts: &str, basename: &str) -> PathBuf {
let snapshot = trash_root.join(ts).join(basename);
fs::create_dir_all(&snapshot).unwrap();
fs::write(snapshot.join("payload.txt"), b"quarantined-bytes").unwrap();
snapshot
}
#[test]
fn sync_retains_recent_quarantine() {
let tmp = tempdir().unwrap();
let (meta, cfg) = setup_meta(tmp.path());
let ts_fresh = ts_n_days_ago(1);
let ts_borderline = ts_n_days_ago(29);
let ts_stale = ts_n_days_ago(35);
let snapshot_fresh = populate_trash_entry(&cfg.trash_root, &ts_fresh, "fresh");
let snapshot_borderline = populate_trash_entry(&cfg.trash_root, &ts_borderline, "borderline");
let snapshot_stale = populate_trash_entry(&cfg.trash_root, &ts_stale, "stale");
assert!(snapshot_fresh.exists());
assert!(snapshot_borderline.exists());
assert!(snapshot_stale.exists());
let report = prune_quarantine(&meta, RetentionConfig { retain_days: 30 }, Some(&cfg.audit_log))
.expect("prune_quarantine succeeds");
assert!(cfg.trash_root.join(&ts_fresh).exists(), "fresh entry must survive");
assert!(cfg.trash_root.join(&ts_borderline).exists(), "borderline entry must survive");
assert!(!cfg.trash_root.join(&ts_stale).exists(), "stale entry must be deleted");
assert_eq!(report.pruned.len(), 1, "exactly one entry pruned: {report:?}");
assert_eq!(report.retained.len(), 2, "exactly two entries retained: {report:?}");
let events = read_all(&cfg.audit_log).unwrap_or_default();
let gc_events: Vec<_> =
events.iter().filter(|e| matches!(e, Event::QuarantineGcSwept { .. })).collect();
assert_eq!(gc_events.len(), 1, "exactly one QuarantineGCSwept event recorded: {events:?}",);
}
#[test]
fn sync_retention_zero_keeps_all() {
let tmp = tempdir().unwrap();
let (meta, cfg) = setup_meta(tmp.path());
let ts_fresh = ts_n_days_ago(1);
let ts_borderline = ts_n_days_ago(29);
let ts_stale = ts_n_days_ago(35);
let _ = populate_trash_entry(&cfg.trash_root, &ts_fresh, "fresh");
let _ = populate_trash_entry(&cfg.trash_root, &ts_borderline, "borderline");
let _ = populate_trash_entry(&cfg.trash_root, &ts_stale, "stale");
let report = prune_quarantine(&meta, RetentionConfig { retain_days: 0 }, Some(&cfg.audit_log))
.expect("prune with retain=0 is a no-op success");
assert!(cfg.trash_root.join(&ts_fresh).exists(), "fresh survives under retain=0");
assert!(cfg.trash_root.join(&ts_borderline).exists(), "borderline survives under retain=0");
assert!(cfg.trash_root.join(&ts_stale).exists(), "stale survives under retain=0");
assert_eq!(report.pruned.len(), 0, "no entries pruned under retain=0: {report:?}");
}
#[test]
fn quarantine_restore_round_trip() {
let tmp = tempdir().unwrap();
let (meta, cfg) = setup_meta(tmp.path());
let ts = ts_n_days_ago(2);
let basename = "restored";
let snapshot = populate_trash_entry(&cfg.trash_root, &ts, basename);
assert!(snapshot.exists(), "fixture: snapshot must exist before restore");
let dest = meta.join(basename);
assert!(!dest.exists(), "precondition: dest must NOT exist before restore");
let _report = restore_quarantine(&meta, &ts, Some(basename), false, Some(&cfg.audit_log))
.expect("restore_quarantine succeeds for unambiguous, non-existing dest");
assert!(dest.exists(), "dest must materialise after restore");
assert_eq!(
fs::read(dest.join("payload.txt")).expect("payload readable in restored dest"),
b"quarantined-bytes",
);
assert!(!snapshot.exists(), "trash snapshot must be moved out (rename semantics)");
let events = read_all(&cfg.audit_log).expect("audit log readable post-restore");
let restored_events: Vec<_> =
events.iter().filter(|e| matches!(e, Event::QuarantineRestored { .. })).collect();
assert_eq!(
restored_events.len(),
1,
"exactly one QuarantineRestored event recorded: {events:?}",
);
}