//! Repo invariant: the only place allowed to issue a direct
//! `remove_file(... ONELINER_FILENAME)` (or the literal `"oneliner.json"`)
//! is `server/src/paths.rs`, which hosts the canonical wrapper
//! `delete_oneliner_unless_manual`. Every other call-site MUST go
//! through that wrapper so the sticky-Manual rule
//! (`OnelinerState::Manual { .. }` survives every server-driven wipe)
//! is enforced uniformly.
//!
//! Static, AST-free scan: a line in a non-whitelisted source file is a
//! violation iff it mentions `remove_file` together with either the
//! constant `ONELINER_FILENAME` or the literal `"oneliner.json"` —
//! pragmatic and good enough to catch the common regression pattern.
//! Comment lines (starting with `//` after trim) are ignored so this
//! file's own documentation does not self-trip.

use std::fs;
use std::path::{Path, PathBuf};

/// Files that may legitimately call `remove_file(... oneliner.json)`.
/// Paths are relative to `server/src/`.
const REMOVE_WHITELIST: &[&str] = &["paths.rs"];

#[test]
fn no_direct_oneliner_remove_outside_paths_rs() {
    let server_src = Path::new(env!("CARGO_MANIFEST_DIR")).join("src");
    let mut rs_files = Vec::new();
    collect_rs_files(&server_src, &mut rs_files);
    assert!(
        !rs_files.is_empty(),
        "expected to find at least one .rs file under {}",
        server_src.display()
    );

    let mut violations: Vec<String> = Vec::new();
    for path in &rs_files {
        let rel = path.strip_prefix(&server_src).unwrap();
        let rel_str = rel.to_string_lossy().replace('\\', "/");
        if REMOVE_WHITELIST.iter().any(|w| rel_str == *w) {
            continue;
        }
        let contents = match fs::read_to_string(path) {
            Ok(c) => c,
            Err(e) => panic!("failed to read {}: {e}", path.display()),
        };
        for (idx, line) in contents.lines().enumerate() {
            let trimmed = line.trim_start();
            if trimmed.starts_with("//") {
                continue;
            }
            let mentions_remove = trimmed.contains("remove_file");
            let mentions_target =
                trimmed.contains("ONELINER_FILENAME") || trimmed.contains("\"oneliner.json\"");
            if mentions_remove && mentions_target {
                violations.push(format!(
                    "{}:{}: direct remove_file on oneliner.json — \
                     route through paths::delete_oneliner_unless_manual to honour \
                     the sticky-Manual rule. Offending line: {}",
                    rel_str,
                    idx + 1,
                    line.trim()
                ));
            }
        }
    }

    assert!(
        violations.is_empty(),
        "Sticky-Manual oneliner invariant violated:\n  - {}\n\n\
         If this is a new legitimate exception, extend REMOVE_WHITELIST in \
         tests/oneliner_disk_invariants_test.rs (and document why).",
        violations.join("\n  - ")
    );
}

fn collect_rs_files(dir: &Path, out: &mut Vec<PathBuf>) {
    let entries = match fs::read_dir(dir) {
        Ok(e) => e,
        Err(e) => panic!("read_dir {}: {e}", dir.display()),
    };
    for entry in entries.flatten() {
        let path = entry.path();
        if path.is_dir() {
            collect_rs_files(&path, out);
        } else if path.extension().and_then(|s| s.to_str()) == Some("rs") {
            out.push(path);
        }
    }
}
