#!/bin/bash
# Self-check for enhanced_cleanup.sh. No root needed; touches only a temp dir.
# Pulls the risky pieces out of the script with sed and runs them against fake
# input (sample log names, mock df, mock /proc scan). Usage: bash test_enhanced_cleanup.sh
set -u
export LC_ALL=C   # sort order and tool messages must not depend on the caller's locale
src="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/enhanced_cleanup.sh"
fail=0
check() {  # name expected actual
    if [[ "$2" == "$3" ]]; then echo "ok   $1"; else
        echo "FAIL $1"; echo "  expected: $(printf '%s' "$2" | tr '\n' '|')"
        echo "  actual:   $(printf '%s' "$3" | tr '\n' '|')"; fail=1
    fi
}
work=$(mktemp -d); pub=; MOUNTS=()
cleanup() {  # unmount test mounts first; never delete across a mount point
    local p
    for p in ${MOUNTS[@]+"${MOUNTS[@]}"}; do umount "$p" 2>/dev/null; done
    rm -rf --one-file-system "$work" ${pub:+"$pub"}
}
trap cleanup EXIT
fn() { sed -n "/^$1()/,/^}/p" "$src"; }   # one top-level function's text

# ── 1. Rotated-log regexes: which names count as general vs security ──────────
eval "$(sed -n '/^_re_z=/,/^_re_gen=/p' "$src")"
cd "$work" && mkdir -p log/apt log/audit log/nginx log/myapp
for f in syslog syslog.1 syslog.2.gz auth.log auth.log.1 auth.log-20240101.gz dpkg.log.1 kern.log.3.gz \
         apt/history.log apt/history.log.1.gz apt/term.log.2.gz audit/audit.log.1 \
         nginx/access.log nginx/access.log.1 nginx/error.log-20240101 messages-20240101 secure.4 \
         btmp.1 wtmp.1 Xorg.0.log.old debug debug.1 cron cron-20240101 foo.log.gz \
         myapp.log.2024-01-01 lastlog journal.gz myapp/history.log.1; do touch "log/$f"; done
gen=$(find log -regextype posix-extended -type f ! -path "log/audit/*" -regex "$_re_gen" ! -regex "$_re_sec" | sort)
sec=$(find log -regextype posix-extended -type f ! -path "log/audit/*" -regex "$_re_sec" | sort)
check "general logs matched (unrelated history.log stays general)" "log/apt/term.log.2.gz
log/cron-20240101
log/debug.1
log/foo.log.gz
log/myapp/history.log.1
log/nginx/access.log.1
log/nginx/error.log-20240101" "$gen"
check "security logs matched (apt history included)" "log/apt/history.log.1.gz
log/auth.log-20240101.gz
log/auth.log.1
log/dpkg.log.1
log/kern.log.3.gz
log/messages-20240101
log/secure.4
log/syslog.1
log/syslog.2.gz" "$sec"

# ── 2. /tmp in-use exclusion: open files spared, glob look-alikes not ─────────
warn() { WARNED+=("$*"); }; WARNED=()
eval "$(fn snapshot_tmp_in_use)"
mkdir -p "$work/t"
open=()
for pair in 'c[d]|cd' 'a*b|aXb' 'e?f|eGf'; do         # NTFS can't hold * or ?; skip those pairs there
    special=${pair%|*}; lookalike=${pair#*|}
    touch "$work/t/$special" 2>/dev/null && { touch "$work/t/$lookalike"; open+=("$work/t/$special"); }
done
touch "$work/t/plain"
find() { printf '%s\0' "${open[@]}"; }               # stands in for the /proc scan
snapshot_tmp_in_use
unset -f find
listed=$(find "$work/t" -type f ${TMP_IN_USE[@]+"${TMP_IN_USE[@]}"} | sort)
want=$(find "$work/t" -type f | sort)
for o in "${open[@]}"; do want=$(grep -vxF -- "$o" <<<"$want"); done
check "in-use files excluded, look-alikes kept" "$want" "$listed"

# ── 3. Filesystem list: same device once, separate tmpfs mounts both ──────────
mkdir -p "$work"/{root,home,tmp,log}
declare -A DF=(
    ["$work/root"]='100M 40M 60M /dev/vda1 /'
    ["$work/home"]='100M 40M 60M /dev/vda1 /home'
    ["$work/tmp"]='1500M 10M 1490M tmpfs /tmp'
    ["$work/log"]='500M 490M 10M tmpfs /var/log'
)
df() { echo "Size Used Avail Filesystem Mounted-on"; echo "${DF[${!#}]}"; }
CLEAN_DIRS=("$work/root" "$work/home" "$work/tmp" "$work/log")
MIN_FREE=5000
eval "$(fn cleaned_filesystems)"; eval "$(fn total_used_mb)"; eval "$(fn check_min_free)"
check "filesystems deduped by device, not by tmpfs name" "$work/root 100 40 60 /dev/vda1
$work/tmp 1500 10 1490 tmpfs
$work/log 500 490 10 tmpfs" "$(cleaned_filesystems)"
check "total used MB" "540" "$(total_used_mb)"
WARNED=(); check_min_free
check "only the nearly-full tmpfs warns" "Free space on tmpfs ($work/log): 10M is below safety threshold 250M!" "${WARNED[*]-}"

# ── 4. Cache cleanup ───────────────────────────────────────────────────────────
# ctime cannot be backdated, so no test file can pass the full age check. The
# delete path is therefore driven with direct predicates (4a, 4c), and
# clean_cache_dir is checked for what it must keep (4b).
DRY_RUN=false VERBOSE=false LOG_FILE= CACHE_DAYS=30
eval "$(fn find_and_act)"; eval "$(fn log_line)"; eval "$(fn clean_cache_dir)"
eval "$(grep '^find_files()' "$src")"
info() { INFOS+=("$*"); }; INFOS=()
declare -A ROOT_SKIP_SAID=(); MOUNTINFO=/proc/self/mountinfo   # real mount table here
eval "$(fn root_is_safe)"

# 4a. Delete mechanism: matching files go, the rest stay, ERRF is cleared.
c="$work/del"; mkdir -p "$c"
touch -d '40 days ago' "$c/oldfile"; touch "$c/newfile"
find_and_act "$c" -type f -mtime +30
check "delete: matched file removed, other kept, temp file cleared" "del|del/newfile|" \
    "$(cd "$work" && find del | sort | tr '\n' '|')${ERRF}"

# 4b. Fresh files with an old mtime (unpacked archive) are kept; old empty dirs
# still go; fresh links, valid or dangling, are kept.
c="$work/cache"; mkdir -p "$c"/{keep,emptyold/nested,emptynew,full}
touch -d '40 days ago' "$c/oldmtime" "$c/full/oldmtime"
touch "$c/keep/newfile" "$c/full/newfile"
ln -s newfile "$c/keep/valid"; ln -s gone "$c/keep/dangling"
touch -d '40 days ago' "$c/emptyold/nested" "$c/emptyold"
clean_cache_dir "$c"
check "cache: fresh files with old mtime kept, old empty dirs removed" "cache
cache/emptynew
cache/full
cache/full/newfile
cache/full/oldmtime
cache/keep
cache/keep/dangling
cache/keep/newfile
cache/keep/valid
cache/oldmtime" "$(cd "$work" && find cache | sort)"
grep -q '^    root_is_safe "\$1" nomount && find_and_act "\$1" -type f "\${old\[@\]}"$' "$src" \
    && grep -q '^    local old=( -mtime +"\$CACHE_DAYS" -ctime +"\$CACHE_DAYS" -atime +"\$CACHE_DAYS" )$' "$src" \
    && ok=yes || ok=no
check "cache: file rule requires mtime, ctime and atime" "yes" "$ok"

# 4c. Symlink rule (without the age part): dangling and looping links go; valid
# links stay; a link to a directory is not followed into its target.
l="$work/links"; mkdir -p "$l/in/realdir" "$l/outside"
touch "$l/in/file" "$l/in/realdir/f" "$l/outside/precious"
ln -s file "$l/in/tofile"; ln -s realdir "$l/in/todir"; ln -s ../outside "$l/in/tooutside"
ln -s gone "$l/in/dangling"; ln -s loopb "$l/in/loopa"; ln -s loopa "$l/in/loopb"
DRY_RUN=true
check "links: dry run lists only dangling and looping links" "$l/in/dangling
$l/in/loopa
$l/in/loopb" "$(find_and_act "$l/in" -type l -xtype l | sort)"
DRY_RUN=false WARNED=()
find_and_act "$l/in" -type l -xtype l
check "links: dangling/looping removed, valid kept, targets untouched" "links
links/in
links/in/file
links/in/realdir
links/in/realdir/f
links/in/todir
links/in/tofile
links/in/tooutside
links/outside
links/outside/precious" "$(cd "$work" && find links | sort)"
check "links: no warnings from the link pass" "" "${WARNED[*]-}"
grep -q '^    root_is_safe "\$1" nomount && find_and_act "\$1" -type l -xtype l "\${old\[@\]}"$' "$src" && ok=yes || ok=no
check "links: script applies the same rule plus the age checks" "yes" "$ok"

# 4d. Temp-file rule: default 10 days, all three timestamps required.
grep -qx 'TMP_DAYS=10' "$src" \
    && grep -q -- '-mtime +"\$TMP_DAYS" -ctime +"\$TMP_DAYS" -atime +"\$TMP_DAYS"' "$src" && ok=yes || ok=no
check "tmp: default 10 days; mtime, ctime and atime all required" "yes" "$ok"

# 4e. Abort handler removes the script's own temp file and exits 1.
ef="$work/errf"; touch "$ef"
( error() { :; }; eval "$(fn on_abort)"; ERRF="$ef"; on_abort ); rc=$?
check "abort: own temp file removed, exit code 1" "1 gone" "$rc $([[ -e "$ef" ]] && echo present || echo gone)"

# ── 5. Package-manager block must not leak the lock fd to children ────────────
exec 9>"$work/lock"
leak=$(bash -c '[ -e /dev/fd/9 ] && echo open || echo closed')
if [[ "$leak" == open ]]; then   # skip where /dev/fd isn't usable
    inblock=$(case x in x) bash -c '[ -e /dev/fd/9 ] && echo open || echo closed' ;; esac 9>&-)
    after=$([ -e /dev/fd/9 ] && echo open || echo closed)
    check "child in 9>&- block has no lock fd; shell keeps it" "closed open" "$inblock $after"
fi
exec 9>&-
grep -q '^    esac 9>&-$' "$src" && ok=yes || ok=no
check "script closes the lock fd around the package-manager block" "yes" "$ok"

# ── 6. npm cache: stub npm/du, fake root home, strict shell; no real cache touched ─
# ~root is rewritten to the fake home, and the stub npm only records its calls, so
# nothing is deleted. The real npm is run in one case only, as a dry run in a fake home.
nsec=$( { grep '^find_files()' "$src"; fn root_npm; fn clean_npm_cache; } | sed 's/~root/"$FAKE_HOME"/g')
[[ "$nsec" == *'cd "$FAKE_HOME"'* && "$nsec" == *'HOME="$FAKE_HOME"'* ]] && ok=yes || ok=no
check "npm tests are confined to the fake root home" "yes" "$ok"
if [[ "$ok" != yes ]]; then echo "skip npm tests: unsafe to run"; exit 1; fi

n="$work/npm"; mkdir -p "$n/stub" "$n/du" "$n/tools"
cat > "$n/stub/npm" <<'EOF'
#!/bin/bash
# env -i leaves only PATH and HOME, so behavior is driven by files in the fake home.
[ -e /dev/fd/9 ] && fd=open || fd=closed
printf '%s|%s|%s|%s|%s\n' "$PWD" "${npm_config_cache-}" "${NPM_CONFIG_CACHE-}" "$fd" "$*" >> "$HOME/calls"
case "$*" in
    *"config get cache")
        [[ -e "$HOME/fail_config" ]] && exit 7
        if [[ -e "$HOME/cache_path" ]]; then cat "$HOME/cache_path"; else echo "$HOME/.npm"; fi ;;
    *"cache clean --force")
        [[ -e "$HOME/fail_clean" ]] && exit 9 ;;
esac
exit 0
EOF
cat > "$n/du/du" <<'EOF'
#!/bin/bash
# Fixed sizes make boundaries exact on any filesystem; unset simulates du failing.
[[ -n "${NPM_TEST_DU_KB-}" ]] || exit 1
printf '%s\t%s\n' "$NPM_TEST_DU_KB" "${!#}"
EOF
chmod +x "$n/stub/npm" "$n/du/du"
for t in find du cut; do ln -s "$(command -v "$t")" "$n/tools/$t"; done
P_STUB="$n/du:$n/stub:$PATH"

_nh=0
new_home() {  # sets h to a fresh fake home; with an argument, also fills its cache
    _nh=$((_nh + 1)); h="$n/h$_nh"; mkdir -p "$h"
    [[ $# -eq 0 ]] || { mkdir -p "$h/.npm/_cacache/x"; echo blob > "$h/.npm/_cacache/x/blob"; }
}
npm_run() {  # fake_home dry_run threshold path: section output, ends in SURVIVED unless set -e killed it
    FAKE_HOME="$1" DRY_RUN="$2" NPM_CACHE_THRESHOLD_MB="$3" TEST_PATH="$4" NSEC="$nsec" bash -c '
        set -euo pipefail
        WARNINGS=false
        PATH=$TEST_PATH
        info() { echo "INFO $*"; }
        warn() { WARNINGS=true; echo "WARN $*"; }
        eval "$NSEC"
        clean_npm_cache
        echo "SURVIVED warnings=$WARNINGS"
    ' 2>&1
}
npm_check() {  # name fragment clean_calls warnings dry_run path [du_kb]; uses h
    local out cleans found=found
    out=$(NPM_TEST_DU_KB="${7-}" npm_run "$h" "$5" 500 "$6")
    cleans=$(grep -c 'cache clean --force' "$h/calls" 2>/dev/null) || cleans=0
    [[ "$out" == *"$2"* ]] || found="no '$2' in: $out"
    check "$1" "found clean=$3 SURVIVED warnings=$4" "$found clean=$cleans $(grep -o 'SURVIVED.*' <<<"$out")"
}

grep -qx 'NPM_CACHE_THRESHOLD_MB=500' "$src" && ok=yes || ok=no
check "npm: default threshold is 500" "yes" "$ok"

new_home full; npm_check "npm: absent, no warning" "npm not installed" 0 false false "$n/tools"
new_home;      npm_check "npm: _cacache absent, no warning" "absent or empty" 0 false false "$P_STUB"
new_home; mkdir -p "$h/.npm/_cacache/x"
               npm_check "npm: _cacache without files, no warning" "absent or empty" 0 false false "$P_STUB"
new_home full; npm_check "npm: below threshold skips (no round-up)" "499M (below 500M" 0 false false "$P_STUB" 511999
new_home full; npm_check "npm: at threshold cleans" "500M (threshold 500M)" 1 false false "$P_STUB" 512000
new_home full; npm_check "npm: above threshold cleans" "1024M" 1 false false "$P_STUB" 1048576
new_home full; npm_check "npm: dry run above threshold only reports" "[DRY RUN] Would run" 0 false true "$P_STUB" 512000
new_home full; npm_check "npm: dry run below threshold skips" "below 500M" 0 false true "$P_STUB" 1024
new_home full; touch "$h/fail_clean"
               npm_check "npm: clean failure warns, run continues" "WARN npm cache clean failed" 1 true false "$P_STUB" 512000
new_home full; touch "$h/fail_config"
               npm_check "npm: config failure warns, run continues" "WARN Could not determine" 0 true false "$P_STUB" 512000
new_home full; echo undefined > "$h/cache_path"
               npm_check "npm: non-path config value warns, no clean" "WARN Could not determine" 0 true false "$P_STUB" 512000
new_home full; npm_check "npm: du failure warns, no clean" "WARN Could not measure" 0 true false "$P_STUB"
new_home full; npm_check "npm: real du output parses" "0M (below 500M" 0 false false "$n/stub:$PATH"

# A root-owned custom location named by root's own config is legitimate.
new_home; mkdir -p "$n/custom/_cacache/x"; echo blob > "$n/custom/_cacache/x/blob"; echo "$n/custom" > "$h/cache_path"
npm_check "npm: custom cache owned by root cleans" "500M" 1 false false "$P_STUB" 512000

# Environment and cwd must not reach npm: another user's HOME, config vars, project .npmrc.
new_home full; mkdir -p "$n/victim" "$n/proj"
( cd "$n/proj" && HOME="$n/victim" npm_config_cache="$n/victim" NPM_CONFIG_CACHE="$n/victim" \
    NPM_TEST_DU_KB=512000 npm_run "$h" false 500 "$P_STUB" >/dev/null )
check "npm: env and cwd isolated, logging flags passed" \
    "$h|||closed|--logs-max=0 --no-update-notifier config get cache" "$(head -n1 "$h/calls")"
[[ -e "$n/victim/calls" ]] && ok=no || ok=yes
check "npm: never ran with the caller's HOME" "yes" "$ok"

# Lock fd: an npm child must not inherit it.
new_home full; exec 9>"$work/lock"
NPM_TEST_DU_KB=1 npm_run "$h" false 500 "$P_STUB" >/dev/null
exec 9>&-
check "npm: lock fd closed for npm" "closed" "$(head -n1 "$h/calls" | cut -d'|' -f4)"

# Ownership: needs root to create a directory owned by someone else.
new_home full
chown 65534 "$h/.npm" 2>/dev/null
if [[ -O "$h/.npm" ]]; then
    echo "skip npm ownership tests (needs root and a filesystem with ownership)"
else
    npm_check "npm: other user's cache dir refused" "not owned by root" 0 true false "$P_STUB" 512000
    new_home full; chown 65534 "$h/.npm/_cacache"
    npm_check "npm: other user's _cacache refused" "not owned by root" 0 true false "$P_STUB" 512000
    new_home full; chown 65534 "$h/.npm"; ln -s "$h/.npm" "$h/link"; echo "$h/link" > "$h/cache_path"
    npm_check "npm: symlink to other user's cache refused" "not owned by root" 0 true false "$P_STUB" 512000
    new_home full; chown 65534 "$h/.npm"
    npm_check "npm: dry run refuses other user's cache too" "not owned by root" 0 true true "$P_STUB" 512000
fi

# Real npm where installed: a dry run must not write anything, logs included.
if command -v npm >/dev/null 2>&1; then
    new_home full
    before=$(find "$h" -printf '%p %s %T@\n' | sort)
    out=$(npm_run "$h" true 500 "$PATH")
    check "npm: real npm dry run writes nothing" "$before" "$(find "$h" -printf '%p %s %T@\n' | sort)"
    [[ "$out" == *SURVIVED* && "$out" != *"Could not determine"* ]] && ok=yes || ok="no: $out"
    check "npm: real npm resolves cache dir and run continues" "yes" "$ok"
else
    echo "skip real npm test (npm not installed)"
fi

# ── 7. Validation messages: config variable named as such, flags unchanged ───────
vloop=$(sed -n '/^for _v in /,/^unset _v _flag/p' "$src")
v_run() {  # VAR=value overrides, then the loop runs
    env LOG_DAYS=1 SECURITY_LOG_DAYS=1 CACHE_DAYS=1 TMP_DAYS=1 MIN_FREE=1 NPM_CACHE_THRESHOLD_MB=1 "$@" \
        bash -c "$vloop"$'\n''echo "ok $NPM_CACHE_THRESHOLD_MB"' 2>&1
}
check "threshold: non-numeric names the variable" \
    "Error: NPM_CACHE_THRESHOLD_MB must be a positive integer (got: 'abc')" "$(v_run NPM_CACHE_THRESHOLD_MB=abc)"
check "threshold: zero rejected" \
    "Error: NPM_CACHE_THRESHOLD_MB must be a positive integer (got: '0')" "$(v_run NPM_CACHE_THRESHOLD_MB=0)"
check "threshold: leading zeros normalized" "ok 500" "$(v_run NPM_CACHE_THRESHOLD_MB=0500)"
check "flag settings keep their flag name" \
    "Error: --min-free must be a positive integer (got: 'x')" "$(v_run MIN_FREE=x)"
check "multi-word flag names keep hyphens" \
    "Error: --security-log-days must be a positive integer (got: 'x')" "$(v_run SECURITY_LOG_DAYS=x)"

# ── 8. Root check runs before the log file is created; --help needs no root ───
# As root, the script is fed on stdin to an unprivileged bash (needs setpriv).
pub=$(mktemp -d); chmod 777 "$pub"
as_user() {
    if [[ $EUID -ne 0 ]]; then bash "$src" "$@"
    else setpriv --reuid=65534 --regid=65534 --clear-groups bash -s -- "$@" < "$src"; fi
}
if [[ $EUID -ne 0 ]] || command -v setpriv >/dev/null 2>&1; then
    out=$(as_user --log-file "$pub/x.log" 2>&1); rc=$?
    check "non-root: refused before the log file is created" \
        "1|Error: Must be run as root (use sudo).|absent" \
        "$rc|$out|$([[ -e "$pub/x.log" ]] && echo created || echo absent)"
    out=$(as_user --help 2>&1); rc=$?
    [[ "$out" == Usage:* ]] && ok=usage || ok="no usage: $out"
    check "non-root: --help still works" "0 usage" "$rc $ok"
    out=$(as_user --log-days x 2>&1); rc=$?
    check "non-root: argument errors still reported" \
        "1 Error: --log-days must be a positive integer (got: 'x')" "$rc $out"
else
    echo "skip root-check tests (running as root without setpriv)"
fi

# ── 9. Mount safety, SIMULATED: fixture mount tables, nothing is mounted ──────
verdict() {  # root [mode] -> yes | no:info | no:warn | no:silent
    ROOT_SKIP_SAID=() INFOS=() WARNED=()
    if root_is_safe "$@"; then echo yes
    elif [[ ${#WARNED[@]} -gt 0 ]]; then echo no:warn
    elif [[ ${#INFOS[@]} -gt 0 ]]; then echo no:info
    else echo no:silent; fi
}
mi() {  # write a fixture mount table: / and /proc plus the given (already escaped) mount points
    local p; : > "$work/mi"
    for p in / /proc "$@"; do
        printf '21 1 8:1 / %s rw,relatime shared:1 - ext4 /dev/sda1 rw\n' "$p" >> "$work/mi"
    done
}
r="$work/mt"; mkdir -p "$r/root/sub" "$r/rootfoo" "$r/sp ace/in" "$r/g[l]ob" "$r/glob"; ln -s root "$r/link"
MOUNTINFO="$work/mi"
mi;                 check "sim: no mount inside -> cleaned" "yes" "$(verdict "$r/root")"
mi "$r/root/sub";   check "sim: nested mount -> root skipped, not a warning" "no:info" "$(verdict "$r/root")"
mi "$r/rootfoo";    check "sim: sibling with the same name prefix is not 'inside'" "yes" "$(verdict "$r/root")"
mi "$r/root";       check "sim: separately mounted root is allowed (tmp, log)" "yes" "$(verdict "$r/root")"
                    check "sim: mount directly on a cache dir -> skipped" "no:info" "$(verdict "$r/root" nomount)"
mi "$r"'/sp\040ace/in'
                    check "sim: escaped space in mount path is decoded" "no:info" "$(verdict "$r/sp ace")"
mi "$r"'/sp\040acex'
                    check "sim: decoded path still respects the directory boundary" "yes" "$(verdict "$r/sp ace")"
mi "$r/glob/x";     check "sim: glob characters in the root are literal (no false match)" "yes" "$(verdict "$r/g[l]ob")"
mi "$r/g[l]ob/x";   check "sim: glob characters in the root are literal (real match)" "no:info" "$(verdict "$r/g[l]ob")"
mi "$r"'/root/t\011ab\012nl\134bs'
ROOT_SKIP_SAID=() INFOS=(); root_is_safe "$r/root"; root_is_safe "$r/root"
[[ ${#INFOS[@]} -eq 1 && "${INFOS[0]}" == *"mounted inside"* && "${INFOS[0]}" != *$'\n'* && "${INFOS[0]}" != *$'\t'* ]] \
    && ok=yes || ok="no: ${INFOS[*]-}"
check "sim: tab/newline/backslash path matched, reported once, on one line" "yes" "$ok"
mi;                 check "sim: symlinked root is not followed" "no:info" "$(verdict "$r/link")"
                    check "sim: root below a symlinked directory is not followed" "no:info" "$(verdict "$r/link/sub")"
                    check "sim: missing root is skipped silently" "no:silent" "$(verdict "$r/nope")"
MOUNTINFO="$work/absent"
                    check "sim: mount table unavailable -> skipped with a warning" "no:warn" "$(verdict "$r/root")"
MOUNTINFO="$work/mi"; : > "$work/mi"
                    check "sim: empty mount table -> skipped with a warning" "no:warn" "$(verdict "$r/root")"
printf '21 1 8:1\n' > "$work/mi"
                    check "sim: truncated mount table line -> skipped with a warning" "no:warn" "$(verdict "$r/root")"
printf '21 1 8:1 / relative/path rw - ext4 /dev/sda1 rw\n' > "$work/mi"
                    check "sim: non-absolute mount point -> skipped with a warning" "no:warn" "$(verdict "$r/root")"
[[ "$(fn root_is_safe)" != *DRY_RUN* ]] && ok=yes || ok=no
check "mount check is the same in dry-run and live (does not read DRY_RUN)" "yes" "$ok"
[[ "$(fn find_and_act)" == *'find_files "$1" -xdev "${@:2}"'* && "$(fn find_and_act)" == *'find "$1" -xdev "${@:2}" -delete'* ]] \
    && ok=yes || ok=no
check "dry-run and live traversals take the same arguments, both with -xdev" "yes" "$ok"
n=$(sed '/^find_and_act()/,/^}/d' "$src" | grep -c '^ *find_and_act ')
check "every deletion call in the script is guarded by root_is_safe" "0" "$n"

# ── 10. Mount safety, REAL mounts (needs root and permission to mount) ────────
m="$work/real"; mkdir -p "$m/nest/n est" "$m/bind/in" "$m/src" "$m/sep" "$m/x/n"
MOUNTINFO=/proc/self/mountinfo
if [[ $EUID -eq 0 ]] && mount -t tmpfs none "$m/nest/n est" 2>/dev/null; then
    MOUNTS+=("$m/nest/n est")
    mount --bind "$m/src" "$m/bind/in" && MOUNTS+=("$m/bind/in")
    mount -t tmpfs none "$m/sep"       && MOUNTS+=("$m/sep")
    mount -t tmpfs none "$m/x/n"       && MOUNTS+=("$m/x/n")
    check "real: all four test mounts in place" "4" "${#MOUNTS[@]}"
    touch "$m/src/file"
    check "real: nested filesystem (space in its path) -> root skipped" "no:info" "$(verdict "$m/nest")"
    check "real: same-filesystem bind mount -> root skipped" "no:info" "$(verdict "$m/bind")"
    check "real: -xdev alone walks into a same-filesystem bind mount (hence the skip)" \
        "$m/bind/in/file" "$(find "$m/bind" -xdev -type f)"
    check "real: separately mounted root is allowed" "yes" "$(verdict "$m/sep")"
    check "real: mount directly on a cache dir -> skipped" "no:info" "$(verdict "$m/sep" nomount)"
    touch -d '40 days ago' "$m/sep/old" "$m/x/old" "$m/x/n/old"; touch "$m/sep/new"
    DRY_RUN=false WARNED=()
    find_and_act "$m/sep" -type f -mtime +30
    check "real: separately mounted root is cleaned" "new" "$(ls "$m/sep")"
    # Called without root_is_safe on purpose: -xdev alone must hold for another filesystem.
    DRY_RUN=true
    check "real: dry run does not list files on a nested filesystem" "$m/x/old" "$(find_and_act "$m/x" -type f -mtime +30)"
    DRY_RUN=false
    find_and_act "$m/x" -type f -mtime +30
    check "real: live run deletes the same set, nested filesystem untouched" "$m/x/n|$m/x/n/old|" \
        "$(find "$m/x" -mindepth 1 | sort | tr '\n' '|')${WARNED[*]-}"
    for p in "${MOUNTS[@]}"; do umount "$p"; done; MOUNTS=()
else
    echo "skip real mount tests (needs root and permission to mount); simulated tests above still ran"
fi

# ── 11. Proxmox VE detection and profile, SIMULATED (stubbed dpkg-query etc.) ─
eval "$(sed -n '/^PVE_DIR=/,/^PVE_FOUND=/p' "$src")"
eval "$(fn detect_pve)"; eval "$(fn apply_profile)"; eval "$(fn report_profile)"
command() {  # controls what "is installed"; the real machine's packages play no part
    if   [[ "${1-}" == -v && "${2-}" == dpkg-query ]]; then [[ "$T_DPKG" != absent ]]
    elif [[ "${1-}" == -v && "${2-}" == pveversion ]]; then [[ "$T_PVEVER" == yes ]]
    else builtin command "$@"; fi
}
dpkg-query() { return "$T_DPKG"; }
mkdir "$work/pve-yes"
pve_case() {  # dpkg(absent|exit status) pve_dir(yes|no) pveversion(yes|no) -> "host indicator_count"
    ( T_DPKG=$1 T_PVEVER=$3 PVE_DIR="$work/pve-$2"; declare -A GIVEN=()
      LOG_DAYS=7 SECURITY_LOG_DAYS=30 AUTOREMOVE=false
      apply_profile; echo "$PVE_HOST ${#PVE_FOUND[@]}" )
}
check "pve: plain Debian, VM guest or LXC guest (no indicators) -> standard" "false 0" "$(pve_case 1 no no)"
check "pve: non-apt system without indicators -> standard"                 "false 0" "$(pve_case absent no no)"
check "pve: full host (package, /etc/pve, pveversion)"                     "true 3"  "$(pve_case 0 yes yes)"
check "pve: package alone is enough"                                       "true 1"  "$(pve_case 0 no no)"
check "pve: /etc/pve alone is enough"                                      "true 1"  "$(pve_case 1 yes no)"
check "pve: pveversion alone is enough"                                    "true 1"  "$(pve_case 1 no yes)"
check "pve: dpkg-query failure is ambiguous -> conservative profile"       "true 1"  "$(pve_case 2 no no)"
grep -Eq 'uname|systemd-detect-virt|/proc/version' "$src" && ok=used || ok=unused
check "pve: kernel version and virtualization detection are not used" "unused" "$ok"

prof() {  # pve_dir(yes|no) [explicit options...] -> settings line, then the report lines
    ( T_DPKG=1 T_PVEVER=no PVE_DIR="$work/pve-$1"; shift
      declare -A GIVEN=(); for k; do GIVEN[$k]=1; done
      LOG_DAYS=7; [[ -n "${GIVEN[log-days]+x}" ]] && LOG_DAYS=3
      SECURITY_LOG_DAYS=30 AUTOREMOVE=true TMP_DAYS=10 CACHE_DAYS=30 NPM_CACHE_THRESHOLD_MB=500
      CLEAN_DIRS=(/a /b) INFOS=() WARNED=()
      apply_profile; report_profile
      echo "host=$PVE_HOST log=$LOG_DAYS journal=$JOURNAL_DAYS autoremove=$AUTOREMOVE warnings=${#WARNED[@]}"
      printf '%s\n' "${INFOS[@]}" )
}
out=$(prof yes autoremove tmp-days cache-days)
check "pve profile: logs default to 30d, autoremove forced off, no warnings" \
    "host=true log=30 journal=30 autoremove=false warnings=0" "$(head -n1 <<<"$out")"
check "pve profile: report names environment, profile, settings, skips, ignored options" \
"Environment: Proxmox VE host ($work/pve-yes exists)
Profile: proxmox-conservative
Effective settings: package cache clean on; autoremove prohibited; general logs 30d; security logs 30d; journal 30d
Cleanup targets: package cache, journal, rotated logs in /var/log
Skipped by profile: package autoremove, /tmp cleanup, user and root cache cleanup, npm cache cleanup
Options ignored by profile: --autoremove (prohibited on Proxmox VE hosts); --tmp-days (/tmp cleanup is off); --cache-days (cache cleanup is off)
Free space monitored on (not all of these are cleaned): /a /b" "$(tail -n +2 <<<"$out")"
out=$(prof yes log-days)
check "pve profile: explicit --log-days is honored" \
    "host=true log=3 journal=30 autoremove=false warnings=0" "$(head -n1 <<<"$out")"
grep -q 'Options ignored' <<<"$out" && ok=listed || ok=none
check "pve profile: no 'ignored' line when nothing was ignored" "none" "$ok"
out=$(prof no autoremove)
check "standard profile: settings untouched" \
    "host=false log=7 journal=30 autoremove=true warnings=0" "$(head -n1 <<<"$out")"
check "standard profile: report" \
"Environment: standard Linux (no Proxmox VE indicators)
Profile: standard
Effective settings: package cache clean on; autoremove on; general logs 7d; security logs 30d; journal 30d; /tmp files 10d; cache files 30d; npm cache at 500M
Cleanup targets: package cache, journal, rotated logs in /var/log, /tmp, user and root caches, root npm cache
Free space monitored on (not all of these are cleaned): /a /b" "$(tail -n +2 <<<"$out")"
unset -f command dpkg-query

# Section gating: the script's own /tmp and cache sections are run with every
# deleting or scanning helper replaced by a recorder, so nothing real is touched.
sect() { sed -n "/^# ── $1/,/^# ── $2/p" "$src"; }
sect_calls() {  # pve_host from_header to_header -> recorded helper calls
    ( PVE_HOST=$1 DRY_RUN=true TMP_DAYS=10 CACHE_DAYS=30 CALLS=(); : > "$work/ff"
      find_and_act() { CALLS+=("find_and_act $*"); }
      find_files()   { echo "find_files $*" >> "$work/ff"; }   # runs in a subshell: record to a file
      root_is_safe() { return 0; }
      snapshot_tmp_in_use() { TMP_IN_USE=(); }
      eval "$(sect "$2" "$3")"
      cat "$work/ff"; printf '%s\n' "${CALLS[@]-}" )
}
check "pve profile: /tmp section does nothing" "" "$(sect_calls true 'Temp file cleanup' 'User cache cleanup')"
check "standard profile: /tmp section runs its one guarded traversal" \
    "find_and_act /tmp -type f ! -path /tmp/systemd-private-* ! -name *.pid ! -name *.lock -mtime +10 -ctime +10 -atime +10" \
    "$(sect_calls false 'Temp file cleanup' 'User cache cleanup')"
check "pve profile: cache section does nothing" "" "$(sect_calls true 'User cache cleanup' 'npm cache cleanup')"
check "standard profile: cache section enumerates user caches" \
    "find_files /home -mindepth 2 -maxdepth 2 -type d -name .cache" \
    "$(sect_calls false 'User cache cleanup' 'npm cache cleanup' | head -n1)"
grep -qx '\[\[ "\$PVE_HOST" = true \]\] || clean_npm_cache' "$src" && ok=yes || ok=no
check "pve profile: npm cache step is gated" "yes" "$ok"

exit "$fail"
