use crate::common::Drip;
use rusqlite::Connection;
use serde_json::Value;
use std::fs;
#[test]
fn meter_json_has_expected_schema() {
let drip = Drip::new();
let dir = tempfile::tempdir().unwrap();
let f = dir.path().join("g.txt");
fs::write(&f, "abc\n".repeat(100)).unwrap();
drip.read_stdout(&f);
fs::write(&f, "abc\n".repeat(99) + "xyz\n").unwrap();
drip.read_stdout(&f);
let o = drip.cmd().arg("meter").arg("--json").output().unwrap();
assert!(o.status.success());
let v: Value = serde_json::from_slice(&o.stdout).expect("valid JSON");
for key in &[
"session_id",
"started_at",
"elapsed_secs",
"files_tracked",
"total_reads",
"tokens_full",
"tokens_sent",
"tokens_saved",
"reduction_pct",
"top",
"history",
] {
assert!(v.get(*key).is_some(), "missing field {key} in JSON: {v}");
}
assert!(v["reduction_pct"].is_number());
assert!(v["top"].is_array());
assert!(v["top"]
.as_array()
.unwrap()
.iter()
.any(|t| t.get("file").is_some() && t.get("reduction_pct").is_some()));
}
#[test]
fn meter_warns_when_lifetime_polluted_by_ghost_files() {
let drip = Drip::new();
let live = tempfile::tempdir().unwrap();
let live_file = live.path().join("real.txt");
fs::write(&live_file, "abc\n".repeat(50)).unwrap();
drip.read_stdout(&live_file);
fs::write(&live_file, "abc\n".repeat(49) + "xyz\n").unwrap();
drip.read_stdout(&live_file);
let ghost = tempfile::tempdir().unwrap();
let ghost_file = ghost.path().join("ghost.txt");
fs::write(&ghost_file, "abc\n".repeat(5_000)).unwrap();
drip.read_stdout(&ghost_file);
drop(ghost);
let o = drip
.cmd()
.arg("meter")
.env("NO_COLOR", "1")
.output()
.unwrap();
let s = String::from_utf8_lossy(&o.stdout);
assert!(s.contains("ghost file"), "expected ghost-file hint: {s}");
assert!(
s.contains("drip meter --prune"),
"hint should mention --prune remediation: {s}"
);
let o = drip.cmd().arg("meter").arg("--json").output().unwrap();
let v: Value = serde_json::from_slice(&o.stdout).expect("valid JSON");
assert!(
v.get("ghost_pollution").is_some(),
"ghost_pollution missing from JSON when ghosts present: {v}"
);
let g = &v["ghost_pollution"];
assert!(g["ghost_files"].as_i64().unwrap() >= 1);
assert!(g["ghost_pct"].as_u64().unwrap() >= 50);
drip.cmd().arg("meter").arg("--prune").output().unwrap();
let o = drip.cmd().arg("meter").arg("--json").output().unwrap();
let v: Value = serde_json::from_slice(&o.stdout).expect("valid JSON");
assert!(
v.get("ghost_pollution").is_none(),
"ghost_pollution should be absent after prune: {v}"
);
}
#[test]
fn meter_no_ghost_hint_on_clean_install() {
let drip = Drip::new();
let dir = tempfile::tempdir().unwrap();
let f = dir.path().join("g.txt");
fs::write(&f, "x\n".repeat(100)).unwrap();
drip.read_stdout(&f);
let o = drip.cmd().arg("meter").arg("--json").output().unwrap();
let v: Value = serde_json::from_slice(&o.stdout).expect("valid JSON");
assert!(
v.get("ghost_pollution").is_none(),
"ghost_pollution must not appear when nothing is polluted: {v}"
);
let o = drip
.cmd()
.arg("meter")
.env("NO_COLOR", "1")
.output()
.unwrap();
let s = String::from_utf8_lossy(&o.stdout);
assert!(!s.contains("ghost file"), "false-positive hint: {s}");
}
#[test]
fn lifetime_stats_self_heal_on_drift() {
let drip = Drip::new();
let dir = tempfile::tempdir().unwrap();
let f = dir.path().join("g.txt");
fs::write(&f, "abc\n".repeat(50)).unwrap();
drip.read_stdout(&f);
fs::write(&f, "abc\n".repeat(49) + "xyz\n").unwrap();
drip.read_stdout(&f);
let db_path = drip.data_dir.path().join("sessions.db");
let conn = Connection::open(&db_path).unwrap();
let real_full: i64 = conn
.query_row(
"SELECT COALESCE(SUM(tokens_full), 0) FROM lifetime_per_file",
[],
|r| r.get(0),
)
.unwrap();
assert!(real_full > 0, "fixture did not populate lifetime_per_file");
let drifted = real_full * 1000;
conn.execute(
"UPDATE lifetime_stats SET tokens_full = ?1 WHERE id = 1",
rusqlite::params![drifted],
)
.unwrap();
drop(conn);
let conn = Connection::open(&db_path).unwrap();
let drifted_now: i64 = conn
.query_row("SELECT tokens_full FROM lifetime_stats", [], |r| r.get(0))
.unwrap();
assert_eq!(drifted_now, drifted);
drop(conn);
let o = drip.cmd().arg("meter").arg("--json").output().unwrap();
assert!(o.status.success(), "drip meter failed: {:?}", o);
let v: Value = serde_json::from_slice(&o.stdout).expect("valid JSON");
let headline_full = v["tokens_full"].as_i64().unwrap();
assert_eq!(
headline_full, real_full,
"headline tokens_full should be re-synced to SUM(lifetime_per_file)"
);
let conn = Connection::open(&db_path).unwrap();
let healed: i64 = conn
.query_row("SELECT tokens_full FROM lifetime_stats", [], |r| r.get(0))
.unwrap();
assert_eq!(healed, real_full);
}
#[test]
fn meter_human_output_is_terse_when_no_color() {
let drip = Drip::new();
let dir = tempfile::tempdir().unwrap();
let f = dir.path().join("g.txt");
fs::write(&f, "x\n").unwrap();
drip.read_stdout(&f);
let o = drip
.cmd()
.arg("meter")
.env("NO_COLOR", "1")
.output()
.unwrap();
let s = String::from_utf8_lossy(&o.stdout);
assert!(!s.contains('\x1b'), "ANSI leaked when NO_COLOR set: {s:?}");
assert!(s.contains("DRIP"), "expected DRIP banner: {s:?}");
assert!(
s.to_lowercase().contains("tokens saved"),
"expected tokens-saved headline: {s:?}"
);
}