#!/usr/bin/env bash
# =============================================================================
# The governor's managed block in .env (persist_memory_env / migration).
# Run:  bash tests/redamon_env_block_test.sh
#
# WHY THIS MATTERS: the allocation used to be `export`ed into a shell that then
# exited, so it lived exactly as long as one redamon.sh process. A later bare
# `docker compose up -d` -- what most people run locally, and what had been run
# on the server this feature came from -- silently fell back to the compose
# defaults: a fixed ~12.6 GB budget with no relation to the machine. Persisting
# it is what makes the allocation hold however the stack is started.
#
# Every scenario runs in a FRESH bash process: within one process the allocator
# remembers its own exports, so reusing a shell would not model a real `up`.
# =============================================================================
set -uo pipefail

REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
PASS=0; FAIL=0
ok()  { PASS=$((PASS+1)); printf '  ok   %s\n' "$1"; }
bad() { FAIL=$((FAIL+1)); printf '  FAIL %s (got: %s want: %s)\n' "$1" "$2" "$3"; }
eq()  { if [[ "$2" == "$3" ]]; then ok "$1"; else bad "$1" "$2" "$3"; fi; }
has()    { if grep -qE "$2" "$1"; then ok "$3"; else bad "$3" "absent" "$2"; fi; }
hasnt()  { if grep -qE "$2" "$1"; then bad "$3" "present" "no $2"; else ok "$3"; fi; }
# The markers contain regex metacharacters ((auto), >>>), so they must be
# matched as FIXED strings, never as an ERE.
hasF()   { if grep -qF "$2" "$1"; then ok "$3"; else bad "$3" "absent" "$2"; fi; }
hasntF() { if grep -qF "$2" "$1"; then bad "$3" "present" "no $2"; else ok "$3"; fi; }

BEGIN_MARK='# >>> redamon memory governor (auto) >>>'
END_MARK='# <<< redamon memory governor <<<'

# run_alloc <envdir> <MemTotalMB>  -- one full export_resource_caps in a fresh shell.
run_alloc() {
    local dir="$1" mem="$2"
    ( cd "$REPO_ROOT" && MEMSTUB="$mem" ENVDIR="$dir" bash -c '
        set -uo pipefail
        source ./redamon.sh
        set +e
        info(){ :; }; warn(){ :; }; error(){ :; }; success(){ :; }
        detect_build_resources(){ BUILD_MEM_MB="$MEMSTUB"; BUILD_NCPU=4; BUILD_RES_SOURCE=stub; }
        export_cpu_caps(){ :; }
        is_gvm_enabled(){ return 1; }; is_kbase_enabled(){ return 1; }
        SCRIPT_DIR="$ENVDIR"
        export BURST_FACTOR=1.75
        export_resource_caps
    ' >/dev/null 2>&1 )
}

newdir() { mktemp -d; }

echo "== the block is created, replaced, and never duplicated =="
D="$(newdir)"
run_alloc "$D" 16384
hasF "$D/.env" "$BEGIN_MARK" "block created when .env has none"
eq "exactly one begin marker" "$(grep -cF "$BEGIN_MARK" "$D/.env")" "1"
eq "exactly one end marker"   "$(grep -cF "$END_MARK"   "$D/.env")" "1"
run_alloc "$D" 16384
eq "still one begin marker after a second run" "$(grep -cF "$BEGIN_MARK" "$D/.env")" "1"
eq "WEBAPP_MEM assigned exactly once"          "$(grep -c '^WEBAPP_MEM=' "$D/.env")" "1"
rm -rf "$D"

echo "== idempotence: same host, byte-identical file =="
D="$(newdir)"
run_alloc "$D" 16384; a="$(md5sum < "$D/.env")"
run_alloc "$D" 16384; b="$(md5sum < "$D/.env")"
run_alloc "$D" 16384; c="$(md5sum < "$D/.env")"
eq "run 2 identical to run 1" "$b" "$a"
eq "run 3 identical to run 1" "$c" "$a"
eq "file does not grow"       "$(wc -l < "$D/.env")" "$(wc -l < "$D/.env")"
rm -rf "$D"

echo "== unrelated content and file mode are preserved =="
D="$(newdir)"
cat > "$D/.env" <<'ENV'
POSTGRES_PASSWORD=supersecret
NEO4J_PASSWORD=alsosecret
AUTH_SECRET=deadbeef
# a comment the operator wrote
RECON_MAX_CONCURRENT_PER_USER=7
ENV
chmod 600 "$D/.env"
run_alloc "$D" 16384
has "$D/.env" '^POSTGRES_PASSWORD=supersecret$' "secrets preserved verbatim"
has "$D/.env" '^AUTH_SECRET=deadbeef$'          "AUTH_SECRET preserved"
has "$D/.env" '^# a comment the operator wrote$' "comments preserved"
has "$D/.env" '^RECON_MAX_CONCURRENT_PER_USER=7$' "unrelated knobs preserved"
eq  "mode stays 600" "$(stat -c %a "$D/.env" 2>/dev/null || stat -f %Lp "$D/.env")" "600"
rm -rf "$D"

echo "== a changed host size re-tunes the block =="
D="$(newdir)"
run_alloc "$D" 16384; small="$(grep '^WEBAPP_MEM=' "$D/.env")"
run_alloc "$D" 65536; big="$(grep '^WEBAPP_MEM=' "$D/.env")"
if [[ "$small" != "$big" ]]; then ok "resizing the host rewrites the values ($small -> $big)"
else bad "resizing the host rewrites the values" "$small" "different"; fi
has "$D/.env" '^# Generated by redamon.sh from MemTotal=65536MB' "block records the MemTotal it used"
rm -rf "$D"

echo "== migration: a hand-pinned OOM firefight is folded in and reported =="
# Exactly the production case: an operator pinned 4g/6g by hand to stop a webapp
# OOM, then the governor landed. First run (no block yet) adopts the computed
# values so the host actually benefits.
D="$(newdir)"
cat > "$D/.env" <<'ENV'
POSTGRES_PASSWORD=keepme
# Neo4j tuning for large graphs (300K+ nodes)
NEO4J_MEM=6g
NEO4J_HEAP=3g
NEO4J_PAGECACHE=2g
WEBAPP_MEM=4g
ENV
out="$( cd "$REPO_ROOT" && MEMSTUB=16384 ENVDIR="$D" bash -c '
    set -uo pipefail; source ./redamon.sh; set +e
    warn(){ :; }; error(){ :; }; success(){ :; }
    detect_build_resources(){ BUILD_MEM_MB="$MEMSTUB"; BUILD_NCPU=4; }
    export_cpu_caps(){ :; }; is_gvm_enabled(){ return 1; }; is_kbase_enabled(){ return 1; }
    SCRIPT_DIR="$ENVDIR"; export BURST_FACTOR=1.75
    export_resource_caps' 2>&1 )"
case "$out" in *"folding hand-pinned limits"*) ok "migration is announced" ;;
               *) bad "migration is announced" "$out" "a notice" ;; esac
case "$out" in *"4g -> computed"*) ok "the old WEBAPP_MEM value is reported" ;;
               *) bad "the old WEBAPP_MEM value is reported" "$out" "4g -> computed" ;; esac
eq "WEBAPP_MEM assigned exactly once after migration" "$(grep -c '^WEBAPP_MEM=' "$D/.env")" "1"
hasnt "$D/.env" '^WEBAPP_MEM=4g$' "the stale 4g pin is gone"
hasnt "$D/.env" '^NEO4J_MEM=6g$'  "the stale 6g pin is gone"
has  "$D/.env" '^POSTGRES_PASSWORD=keepme$' "migration does not touch secrets"
rm -rf "$D"

echo "== an operator pin outside the block always wins =="
D="$(newdir)"
run_alloc "$D" 16384                       # establishes the block
printf 'WEBAPP_MEM=9g\n' >> "$D/.env"      # operator pins, as the block instructs
run_alloc "$D" 16384
eq "the pin survives the regen"        "$(grep -c '^WEBAPP_MEM=9g$' "$D/.env")" "1"
eq "the governor no longer writes its own WEBAPP_MEM" "$(grep -c '^WEBAPP_MEM=' "$D/.env")" "1"
has "$D/.env" '^AGENT_MEM='            "unpinned services are still managed"
# .env precedence is last-assignment-wins, which would matter if the block and a
# pin both assigned the same var. They never do: the block omits anything pinned,
# so there is exactly ONE assignment and position is irrelevant.
eq "no competing assignment exists for a pinned var" "$(grep -c '^WEBAPP_MEM=' "$D/.env")" "1"
if grep -A200 -F "$BEGIN_MARK" "$D/.env" | grep -q '^WEBAPP_MEM='; then
    bad "the block omits a pinned var" "block emits WEBAPP_MEM" "omitted"
else
    ok "the block omits a pinned var entirely"
fi
rm -rf "$D"

echo "== a .env copied from .env.example still works =="
# .env.example lists every knob as a bare `VAR=` placeholder. If that were read
# as an operator pin the allocator would skip the export, and compose would
# interpolate an EMPTY mem_limit -- which refuses to start the whole stack.
# The behaviour that matters is what COMPOSE finally resolves, so assert that.
if docker compose version >/dev/null 2>&1; then
    D="$(mktemp -d "$REPO_ROOT/.envblock-test.XXXXXX")"
    cp "$REPO_ROOT/.env.example" "$D/.env"
    run_alloc "$D" 16384
    {
      printf 'services:\n'
      for svc in webapp neo4j agent postgres kali; do
        case "$svc" in
          webapp) v=WEBAPP_MEM ;; neo4j) v=NEO4J_MEM ;; agent) v=AGENT_MEM ;;
          postgres) v=POSTGRES_MEM ;; kali) v=KALI_MEM ;;
        esac
        printf '  %s:\n    image: busybox\n    mem_limit: ${%s:-1g}\n' "$svc" "$v"
      done
    } > "$D/docker-compose.yml"
    cfg="$(docker compose --project-directory "$D" -f "$D/docker-compose.yml" config 2>&1)"
    if printf '%s' "$cfg" | grep -qiE "error|invalid"; then
        bad "compose accepts a .env built from .env.example" "$(printf '%s' "$cfg" | head -2)" "valid config"
    else
        ok "compose accepts a .env built from .env.example"
    fi
    # Every limit must resolve to a real size, and NOT to the 1g compose default
    # (which would mean the governor's value never reached compose).
    n_default="$(printf '%s' "$cfg" | grep -c '"1073741824"')"
    eq "no service falls back to the 1g compose default" "$n_default" "0"
    n_empty="$(printf '%s' "$cfg" | grep -cE 'mem_limit: *("")?$')"
    eq "no service resolves to an empty mem_limit" "$n_empty" "0"
    rm -rf "$D"
else
    echo "  SKIP  docker compose unavailable"
fi

echo "== a truncated block is repaired, not duplicated =="
D="$(newdir)"
{ printf 'POSTGRES_PASSWORD=keepme\n'; printf '%s\n' "$BEGIN_MARK"; printf 'WEBAPP_MEM=1m\n'; } > "$D/.env"
run_alloc "$D" 16384
eq "one begin marker after repair" "$(grep -cF "$BEGIN_MARK" "$D/.env")" "1"
eq "one end marker after repair"   "$(grep -cF "$END_MARK"   "$D/.env")" "1"
hasnt "$D/.env" '^WEBAPP_MEM=1m$'  "the truncated block's stale value is gone"
has  "$D/.env" '^POSTGRES_PASSWORD=keepme$' "content before a truncated block survives"
rm -rf "$D"

echo "== an undetectable host leaves .env alone =="
D="$(newdir)"
printf 'POSTGRES_PASSWORD=keepme\n' > "$D/.env"
run_alloc "$D" 0
hasntF "$D/.env" "$BEGIN_MARK" "no block written when RAM cannot be read (fail open)"
has  "$D/.env" '^POSTGRES_PASSWORD=keepme$' "existing content untouched"
rm -rf "$D"

echo "== docker compose really reads the block =="
# The precedence check that proved the original bug: compose must pick the
# governor's value up from .env with no redamon.sh process involved.
if docker compose version >/dev/null 2>&1; then
    D="$(mktemp -d "$REPO_ROOT/.envblock-test.XXXXXX")"
    run_alloc "$D" 16384
    printf 'services:\n  w:\n    image: busybox\n    mem_limit: ${WEBAPP_MEM:-1g}\n' > "$D/docker-compose.yml"
    want="$(grep '^WEBAPP_MEM=' "$D/.env" | cut -d= -f2)"
    got="$(docker compose --project-directory "$D" -f "$D/docker-compose.yml" config 2>/dev/null \
           | grep -E 'mem_limit' | tr -dc '0-9')"
    want_bytes=$(( ${want%m} * 1048576 ))
    eq "bare 'docker compose' picks up the governor's WEBAPP_MEM" "$got" "$want_bytes"
    rm -rf "$D"
else
    echo "  SKIP  docker compose unavailable"
fi

echo
echo "RESULT: $PASS passed, $FAIL failed"
[[ "$FAIL" -eq 0 ]]
