#!/usr/bin/env bash

# The containers page shows rootless podman-compose stacks: what they are, what
# they expose, and what they cost.
#
# The rules:
#
#   1. A volume in use is never offered for deletion. MountCount is a runtime
#      lock counter, not a usage signal -- it reads zero for a volume a running
#      container has mounted this second. Trusting it offered to delete the live
#      Command Center database. This is the rule the whole file exists for.
#   2. Grouping is read from the compose labels, never invented. Where the
#      project name says nothing ("docker" is a directory), the repository does.
#   3. Acting on a stack uses plain `podman`, never `podman-compose down`, which
#      would REMOVE containers this tool did not create.
#   4. Exposure means published on every interface. A stopped container exposes
#      nothing yet, and is reported as such rather than as a live finding.
#   5. Pruning names what it removes, by id, from the state that was shown.
#   6. The compose edit adds an address and changes nothing else -- variables,
#      quoting, comments and layout all survive -- and is undone if the file
#      does not read back exactly as intended.
#   7. A refusal states its reason. "Something failed" has let a mutation
#      through three times on this project.
#
# Every mutation runs against a stubbed podman. Nothing here starts, stops or
# removes a real container, image or volume: the containers on this machine are
# a working development database.

set -uo pipefail

repo_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
helper="$repo_dir/config/dot/quickshell/scripts/panama-containers"

fail() {
    printf 'containers contract: %s\n' "$1" >&2
    exit 1
}

[[ -r "$helper" ]] || fail "missing $helper"
[[ -x "$helper" ]] || fail 'panama-containers is not executable'

work="$(mktemp -d)"
trap 'rm -rf "$work"' EXIT

calls="$work/calls.log"
: >"$calls"

# A repository laid out like the real one: the compose file sits well below the
# root, in a directory called "docker", which is exactly how podman-compose ends
# up naming the project after a directory that means nothing.
repo="$work/command-center"
compose_dir="$repo/packages/db/docker"
mkdir -p "$compose_dir"
git -C "$repo" init --quiet 2>/dev/null || fail 'could not create the fixture repository'
compose="$compose_dir/compose.yml"

# ── a podman that answers, and records what it was asked to do ───────────────
#
# State lives in files so a test can change it between calls. Mutating verbs are
# recorded and otherwise do nothing, which is the entire point.

cat >"$work/ps.json" <<PSJSON
[
  {"Id":"aaaaaaaaaaaa1","Names":["kcc-postgres"],"Image":"pgvector:pg17",
   "State":"running","Status":"Up 32 hours (healthy)","ExitCode":0,"StartedAt":1,"Restarts":0,
   "Ports":[{"host_ip":"","host_port":5432,"container_port":5432,"protocol":"tcp","range":1}],
   "Labels":{"com.docker.compose.project":"docker","com.docker.compose.service":"kcc-postgres",
             "com.docker.compose.project.config_files":"$compose",
             "com.docker.compose.project.working_dir":"$compose_dir"}},
  {"Id":"aaaaaaaaaaaa2","Names":["kcc-redis"],"Image":"redis:7-alpine",
   "State":"running","Status":"Up 32 hours (healthy)","ExitCode":0,"StartedAt":1,"Restarts":0,
   "Ports":[{"host_ip":"127.0.0.1","host_port":6379,"container_port":6379,"protocol":"tcp","range":1}],
   "Labels":{"com.docker.compose.project":"docker","com.docker.compose.service":"kcc-redis",
             "com.docker.compose.project.config_files":"$compose",
             "com.docker.compose.project.working_dir":"$compose_dir"}},
  {"Id":"bbbbbbbbbbbb1","Names":["planner_db"],"Image":"postgres:17",
   "State":"exited","Status":"Exited (137) 3 weeks ago","ExitCode":137,"StartedAt":1,"Restarts":0,
   "Ports":[{"host_ip":"","host_port":54322,"container_port":5432,"protocol":"tcp","range":1}],
   "Labels":{"com.docker.compose.project":"planner","com.docker.compose.service":"db"}}
]
PSJSON

cat >"$work/images.json" <<'IMGJSON'
[
  {"Id":"1111111111112222","Names":["docker.io/library/postgres:16"],"Size":458000000,"Containers":0},
  {"Id":"3333333333334444","Names":["pgvector:pg17"],"Size":450000000,"Containers":1}
]
IMGJSON

# Two volumes exist. docker_kcc-pg-data is mounted by the RUNNING postgres, so
# podman does not consider it dangling; the anonymous one it does.
cat >"$work/volumes-dangling.json" <<'VOLJSON'
[{"Name":"7a830b1a58f2anonymous"}]
VOLJSON

cat >"$work/df.json" <<'DFJSON'
[
  {"Type":"Images","Total":2,"Active":1,"RawSize":908000000,"RawReclaimable":458000000},
  {"Type":"Containers","Total":3,"Active":2,"RawSize":11886090,"RawReclaimable":10795752},
  {"Type":"Local Volumes","Total":2,"Active":1,"RawSize":274720148,"RawReclaimable":84280000}
]
DFJSON

cat >"$work/podman" <<STUB
#!/usr/bin/env bash
printf '%s\n' "\$*" >>"$calls"
case "\$1 \$2" in
    "ps -a")            cat "$work/ps.json" ;;
    "images --format")  cat "$work/images.json" ;;
    "system df")        cat "$work/df.json" ;;
    "volume ls")        cat "$work/volumes-dangling.json" ;;
    *) exit 0 ;;
esac
STUB
chmod +x "$work/podman"
export PANAMA_CONTAINERS_PODMAN="$work/podman"

# His actual compose style: flow sequence, an interpolation containing a colon,
# and comments that must survive.
cat >"$compose" <<'COMPOSE'
services:
  kcc-postgres:
    # pgvector is the official Postgres image with the extension preinstalled.
    image: docker.io/pgvector/pgvector:pg17
    ports: ["${POSTGRES_PORT:-5432}:5432"]
    restart: unless-stopped
  kcc-redis:
    image: redis:7-alpine
    ports: ["127.0.0.1:${REDIS_PORT:-6379}:6379"]
COMPOSE
cp "$compose" "$work/compose.original"

snap() { "$helper" snapshot; }
field() { python3 -c "import json,sys; print(json.load(sys.stdin)$1)"; }

state="$(snap)" || fail 'snapshot failed'

# ── 1. A volume in use is never offered for deletion ─────────────────────────

unused_volumes="$(printf '%s' "$state" | field "['disk']['unusedVolumes']")"
case "$unused_volumes" in
    *pg-data*) fail 'a volume mounted by a running container was offered for deletion' ;;
esac
[[ "$unused_volumes" == *anonymous* ]] || fail 'the genuinely unused volume was not found'

grep -q 'dangling=true' "$helper" \
    || fail 'unused volumes are not read from podman own dangling filter'
# Matched as code, not as prose: the comment explaining why MountCount is the
# wrong signal is the reason this rule is here, and must not trip it.
grep -q 'get("MountCount")' "$helper" \
    && fail 'MountCount is being used to decide whether a volume is in use'

# ── 2. Grouping comes from the labels, and says something ────────────────────

titles="$(printf '%s' "$state" | field "['projects']")"
[[ "$titles" == *"'name': 'docker'"* ]] || fail 'the compose project label was not read'
case "$titles" in
    *"'title': 'docker'"*) fail 'a project named after a directory was shown as "docker"' ;;
esac

running_first="$(printf '%s' "$state" | field "['projects'][0]['running']")"
[[ "$running_first" == "2" ]] || fail "the running project is not first (got $running_first)"

# ── 3. Stacks are acted on with podman, never compose down ───────────────────

: >"$calls"
"$helper" project-stop docker >/dev/null || fail 'project-stop failed'
grep -q '^stop kcc-postgres$' "$calls" || fail 'project-stop did not stop the containers'
grep -qi 'compose' "$calls" && fail 'project-stop invoked podman-compose'
grep -qE '^(rm|rmi) ' "$calls" && fail 'project-stop removed something'

# A verb with nothing to do refuses, and says why.
: >"$calls"
reason="$(printf '%s' "$("$helper" start kcc-postgres)" | field "['error']")"
[[ "$reason" == *"already running"* ]] \
    || fail "starting a running container did not say why it refused (got: $reason)"
[[ -s "$calls" ]] && grep -qE '^start ' "$calls" && fail 'it started an already-running container'

# ── 4. Exposure is published-on-every-interface, and honest about state ──────

exposed="$(printf '%s' "$state" | field "['exposed']")"
[[ "$exposed" == *kcc-postgres* ]] || fail 'a container published on every interface was not reported'
case "$exposed" in
    *kcc-redis*) fail 'a container bound to loopback was reported as exposed' ;;
esac
[[ "$exposed" == *"'container': 'planner_db'"*  ]] || fail 'a stopped publisher was dropped entirely'
printf '%s' "$state" | python3 -c "
import json,sys
for e in json.load(sys.stdin)['exposed']:
    if e['container'] == 'planner_db' and e['running']:
        raise SystemExit('a stopped container was reported as currently reachable')
" || fail 'a stopped container was reported as currently reachable'

# ── 5. Pruning names what it removes ─────────────────────────────────────────

: >"$calls"
"$helper" prune-images >/dev/null || fail 'prune-images failed'
grep -q '^rmi 111111111111$' "$calls" || fail 'prune-images did not remove the unused image by id'
grep -q '^rmi 333333333333$' "$calls" && fail 'prune-images removed an image still in use'
grep -q 'image prune' "$calls" && fail 'prune-images used a blanket prune instead of named ids'

# Nothing to do refuses with a reason.
cat >"$work/images.json" <<'ALLUSED'
[{"Id":"3333333333334444","Names":["pgvector:pg17"],"Size":450000000,"Containers":1}]
ALLUSED
: >"$calls"
reason="$(printf '%s' "$("$helper" prune-images)" | field "['error']")"
[[ "$reason" == *"in use"* ]] || fail "prune-images did not say why it refused (got: $reason)"
grep -qE '^rmi ' "$calls" && fail 'prune-images removed something while refusing'

# ── 6. The compose edit adds an address and nothing else ─────────────────────

result="$("$helper" bind-local docker kcc-postgres)" || fail 'bind-local failed'
reason="$(printf '%s' "$result" | field "['error']")"
[[ -z "$reason" ]] || fail "bind-local refused a valid edit: $reason"

grep -q 'ports: \["127.0.0.1:\${POSTGRES_PORT:-5432}:5432"\]' "$compose" \
    || fail 'the edit did not preserve the interpolation, the quoting, or the flow style'
grep -q '# pgvector is the official Postgres image' "$compose" \
    || fail 'the edit destroyed a comment'
grep -q 'restart: unless-stopped' "$compose" \
    || fail 'the edit disturbed the rest of the service'
[[ "$(grep -c '' "$compose")" == "$(grep -c '' "$work/compose.original")" ]] \
    || fail 'the edit changed the shape of the file'

# Twice is a refusal, not a second address.
reason="$(printf '%s' "$("$helper" bind-local docker kcc-postgres)" | field "['error']")"
[[ -n "$reason" ]] || fail 'binding an already-bound service was allowed'
grep -q '127.0.0.1:127.0.0.1' "$compose" && fail 'the address was prepended twice'

# A service whose ports it cannot read is refused, not guessed at.
cat >"$compose" <<'RANGE'
services:
  kcc-postgres:
    ports:
      - "5432:5432"
      - "5432:5432"
RANGE
reason="$(printf '%s' "$("$helper" bind-local docker kcc-postgres)" | field "['error']")"
[[ "$reason" == *"times"* || "$reason" == *"not clear"* ]] \
    || fail "an ambiguous mapping was not refused with a reason (got: $reason)"
grep -q '127.0.0.1' "$compose" && fail 'an ambiguous compose file was edited anyway'

# ── 7. The tests above must be capable of failing ────────────────────────────
#
# Rule 1 is the one that matters, so it is proven to fail: told to use
# MountCount, the helper must offer the live database for deletion and be
# caught. A guard nobody has seen fail is not a guard.

probe="$work/probe-helper"
sed 's/"volume", "ls", "--filter", "dangling=true", "--format", "json"/"volume", "ls", "--format", "json"/' \
    "$helper" >"$probe"
chmod +x "$probe"
cat >"$work/volumes-all.json" <<'ALLVOL'
[{"Name":"docker_kcc-pg-data","MountCount":0},{"Name":"7a830b1a58f2anonymous","MountCount":0}]
ALLVOL
sed -i "s|\"volume ls\")     cat \"$work/volumes-dangling.json\"|\"volume ls\")     cat \"$work/volumes-all.json\"|" "$work/podman"
cat >"$work/podman" <<STUB2
#!/usr/bin/env bash
printf '%s\n' "\$*" >>"$calls"
case "\$1 \$2" in
    "ps -a")            cat "$work/ps.json" ;;
    "images --format")  cat "$work/images.json" ;;
    "system df")        cat "$work/df.json" ;;
    "volume ls")        cat "$work/volumes-all.json" ;;
    *) exit 0 ;;
esac
STUB2
chmod +x "$work/podman"

leaked="$("$probe" snapshot | field "['disk']['unusedVolumes']")"
[[ "$leaked" == *pg-data* ]] \
    || fail 'the volume guard cannot be made to fail, so it proves nothing'

printf 'containers contract: ok\n'
