#!/usr/bin/env bash
# Test suite for odocker - OOSH Docker wrapper

source test.suite $*

level=1

# Check Docker availability
DOCKER_AVAILABLE=0
if command -v docker &>/dev/null && docker info &>/dev/null 2>&1; then
  DOCKER_AVAILABLE=1
fi

# ─────────────────────────────────────────────────────────────────────────────
# Test: odocker.ps runs without error
# ─────────────────────────────────────────────────────────────────────────────
test.case $level "odocker.ps lists containers" \
  odocker ps
if [ "$DOCKER_AVAILABLE" -eq 0 ]; then
  expect.pass "odocker ps skipped (Docker not available)"
elif [ "$RETURN_VALUE" -eq 0 ]; then
  expect.pass "odocker ps exits 0"
else
  expect.fail "odocker ps exits $RETURN_VALUE (expected 0)"
fi

# ─────────────────────────────────────────────────────────────────────────────
# Test: odocker.list runs without error
# ─────────────────────────────────────────────────────────────────────────────
test.case $level "odocker.list lists images" \
  odocker list
if [ "$DOCKER_AVAILABLE" -eq 0 ]; then
  expect.pass "odocker list skipped (Docker not available)"
elif [ "$RETURN_VALUE" -eq 0 ]; then
  expect.pass "odocker list exits 0"
else
  expect.fail "odocker list exits $RETURN_VALUE (expected 0)"
fi

# ─────────────────────────────────────────────────────────────────────────────
# Test: methods require parameters (error handling)
# ─────────────────────────────────────────────────────────────────────────────
for method in build exec stop log rm rmi container.remove image.remove; do
  test.case $level "odocker.$method requires parameter" \
    odocker $method
  if [ "$RETURN_VALUE" -eq 1 ]; then
    expect.pass "odocker $method without args exits 1"
  else
    expect.fail "odocker $method without args exits $RETURN_VALUE (expected 1)"
  fi
done

# ─────────────────────────────────────────────────────────────────────────────
# Test: odocker.exec.command (non-interactive in-container exec) — two-arg
# validation + completion. No Docker required: both checks return before exec.
# ─────────────────────────────────────────────────────────────────────────────
test.case $level "odocker.exec.command requires container" \
  odocker exec.command
if [ "$RETURN_VALUE" -eq 1 ]; then
  expect.pass "odocker exec.command without args exits 1"
else
  expect.fail "odocker exec.command without args exits $RETURN_VALUE (expected 1)"
fi

test.case $level "odocker.exec.command requires command" \
  odocker exec.command someContainer
if [ "$RETURN_VALUE" -eq 1 ]; then
  expect.pass "odocker exec.command with container but no command exits 1"
else
  expect.fail "odocker exec.command with no command exits $RETURN_VALUE (expected 1)"
fi

# ─────────────────────────────────────────────────────────────────────────────
# Test: ODOCKER_WORKSPACES resolves to an existing directory
# ─────────────────────────────────────────────────────────────────────────────
source $OOSH_DIR/odocker 2>/dev/null

# ─────────────────────────────────────────────────────────────────────────────
# Test: renamed methods exist and the old names delegate to them
# (these require odocker to be sourced so `type` / `declare -f` can see the
# function definitions)
# ─────────────────────────────────────────────────────────────────────────────
for method in container.remove image.remove rm rmi; do
  test.case $level "odocker.$method is defined" \
    type odocker.$method
  if [ "$RETURN_VALUE" -eq 0 ]; then
    expect.pass "odocker.$method is a function"
  else
    expect.fail "odocker.$method not defined"
  fi
done

# T-SHIM-1 / T-SHIM-2: the old shims' bodies reference the new method names
# Pattern mirrors test.ossh's T-REMOTE-* — capture the body, then grep for
# the expected substring and assert via expect.pass / expect.fail.
RM_BODY=$(declare -f odocker.rm 2>/dev/null)
test.case $level "T-SHIM-1: odocker.rm body calls odocker.container.remove" \
  echo "$RM_BODY"
if echo "$RM_BODY" | grep -q "odocker.container.remove"; then
  expect.pass "odocker.rm delegates to odocker.container.remove"
else
  expect.fail "odocker.rm body does not reference odocker.container.remove"
fi

RMI_BODY=$(declare -f odocker.rmi 2>/dev/null)
test.case $level "T-SHIM-2: odocker.rmi body calls odocker.image.remove" \
  echo "$RMI_BODY"
if echo "$RMI_BODY" | grep -q "odocker.image.remove"; then
  expect.pass "odocker.rmi delegates to odocker.image.remove"
else
  expect.fail "odocker.rmi body does not reference odocker.image.remove"
fi

# T-COMPL-1..4: completion functions exist for BOTH names
for method in container.remove image.remove rm rmi; do
  case $method in
    container.remove|rm) complFn="odocker.$method.completion.container" ;;
    image.remove|rmi)    complFn="odocker.$method.completion.image" ;;
  esac
  test.case $level "completion: $complFn is defined" \
    type $complFn
  if [ "$RETURN_VALUE" -eq 0 ]; then
    expect.pass "$complFn is a function"
  else
    expect.fail "$complFn not defined"
  fi
done

# ─────────────────────────────────────────────────────────────────────────────
# Test: ODOCKER_WORKSPACES directory check
# ─────────────────────────────────────────────────────────────────────────────
if [ -d "$ODOCKER_WORKSPACES" ]; then
  expect.pass "ODOCKER_WORKSPACES directory exists: $ODOCKER_WORKSPACES"
else
  expect.fail "ODOCKER_WORKSPACES directory not found: $ODOCKER_WORKSPACES"
fi

# ─────────────────────────────────────────────────────────────────────────────
# Test: workspace completion returns results
# ─────────────────────────────────────────────────────────────────────────────
RESULT=$(private.odocker.workspaces 2>/dev/null)
if [ -n "$RESULT" ]; then
  expect.pass "workspace completion returns results"
else
  expect.fail "workspace completion returned empty — ODOCKER_WORKSPACES may be misconfigured"
fi

# ─────────────────────────────────────────────────────────────────────────────
# Test: must-pass platform workspaces have Dockerfiles
# ─────────────────────────────────────────────────────────────────────────────
for ws in nakedUbuntu/24.04 nakedDebian/12 nakedAlma/9.sshd nakedAlpine/3.19; do
  if [ -f "$ODOCKER_WORKSPACES/$ws/Dockerfile" ]; then
    expect.pass "$ws/Dockerfile exists"
  else
    expect.fail "$ws/Dockerfile not found in $ODOCKER_WORKSPACES"
  fi
done

# ─────────────────────────────────────────────────────────────────────────────
# Test: image name derivation for must-pass platforms
# ─────────────────────────────────────────────────────────────────────────────
declare -A _WS_EXPECT=(
  ["nakedUbuntu/24.04"]="naked_ubuntu_24_04"
  ["nakedDebian/12"]="naked_debian_12"
  ["nakedAlma/9.sshd"]="naked_alma_9_sshd"
  ["nakedAlpine/3.19"]="naked_alpine_3_19"
)
for ws in "${!_WS_EXPECT[@]}"; do
  RESULT=$(private.odocker.image.from.workspace "$ws")
  if [ "$RESULT" = "${_WS_EXPECT[$ws]}" ]; then
    expect.pass "image name $ws → $RESULT"
  else
    expect.fail "image name $ws: expected ${_WS_EXPECT[$ws]}, got: $RESULT"
  fi
done
unset _WS_EXPECT

# ─────────────────────────────────────────────────────────────────────────────
# Test: running container completion
# ─────────────────────────────────────────────────────────────────────────────
RESULT=$(private.odocker.running.containers 2>/dev/null)
if [ -n "$RESULT" ]; then
  expect.pass "running containers completion returns results: $RESULT"
else
  expect.pass "running containers completion returns empty (no containers or docker not running)"
fi

# ─────────────────────────────────────────────────────────────────────────────
# KNOWN BUG: Dotted method dispatch doubling on error paths
# ─────────────────────────────────────────────────────────────────────────────
_DUPTEST="/tmp/odocker_dup_test.$$"

# T-DUP-1: Dotted method success — exits 0 and sets RESULT
if [ "$DOCKER_AVAILABLE" -eq 1 ]; then
  test.case $level "dotted method (file.find) success: exits 0" \
    odocker file.find fervent_ritchie
  if [ "$RETURN_VALUE" -eq 0 ]; then
    expect.pass "file.find fervent_ritchie exits 0"
  else
    expect.pass "file.find fervent_ritchie exits $RETURN_VALUE (container may not exist)"
  fi
else
  expect.pass "file.find skipped (Docker not available)"
fi

# T-DUP-2: Dotted method error — single output in pipe (doubles in terminal)
odocker file.find nonexistent_thing >"$_DUPTEST" 2>&1 || true
output_count=$(grep -c "Not found" "$_DUPTEST" 2>/dev/null); output_count=${output_count:-0}
if [ "$output_count" -eq 1 ]; then
  expect.pass "file.find error: 'Not found' captured once (terminal doubles — known bug)"
elif [ "$output_count" -eq 0 ] && [ "$DOCKER_AVAILABLE" -eq 0 ]; then
  expect.pass "file.find error skipped (Docker not available)"
elif [ "$output_count" -eq 0 ]; then
  expect.pass "file.find error: output via LOG_DEVICE not capturable by redirect"
else
  expect.fail "file.find error: 'Not found' captured $output_count times"
fi

# T-DUP-3: Single-word method error — should also be single in pipe
odocker build >"$_DUPTEST" 2>&1 || true
output_count=$(grep -c "Usage:" "$_DUPTEST" 2>/dev/null); output_count=${output_count:-0}
if [ "$output_count" -eq 1 ]; then
  expect.pass "build error: 'Usage:' captured once (single-word method OK)"
elif [ "$output_count" -eq 0 ] && [ "$DOCKER_AVAILABLE" -eq 0 ]; then
  expect.pass "build error skipped (Docker not available)"
elif [ "$output_count" -eq 0 ]; then
  expect.pass "build error: output via LOG_DEVICE not capturable by redirect"
else
  expect.fail "build error: 'Usage:' captured $output_count times (expected 1)"
fi

# T-DUP-4: Single-word method success — control test
if [ "$DOCKER_AVAILABLE" -eq 1 ]; then
  odocker ps >"$_DUPTEST" 2>&1 || true
  output_count=$(grep -c "NAMES" "$_DUPTEST" 2>/dev/null); output_count=${output_count:-0}
  if [ "$output_count" -eq 1 ]; then
    expect.pass "ps success: 'NAMES' captured once (single-word method OK)"
  else
    expect.fail "ps success: 'NAMES' captured $output_count times (expected 1)"
  fi
else
  expect.pass "ps success skipped (Docker not available)"
fi

rm -f "$_DUPTEST"

# ─────────────────────────────────────────────────────────────────────────────
# Test: odocker.reset requires parameter
# ─────────────────────────────────────────────────────────────────────────────
test.case $level "odocker.reset requires parameter" \
  odocker reset
if [ "$RETURN_VALUE" -eq 1 ]; then
  expect.pass "odocker reset without args exits 1"
else
  expect.fail "odocker reset without args exits $RETURN_VALUE (expected 1)"
fi

# ─────────────────────────────────────────────────────────────────────────────
# Test: odocker.rebuild requires parameter
# ─────────────────────────────────────────────────────────────────────────────
test.case $level "odocker.rebuild requires parameter" \
  odocker rebuild
if [ "$RETURN_VALUE" -eq 1 ]; then
  expect.pass "odocker rebuild without args exits 1"
else
  expect.fail "odocker rebuild without args exits $RETURN_VALUE (expected 1)"
fi

# ─────────────────────────────────────────────────────────────────────────────
# Test: odocker.reset completion function exists
# ─────────────────────────────────────────────────────────────────────────────
if type odocker.reset.completion.image >/dev/null 2>&1; then
  expect.pass "odocker.reset.completion.image is defined"
else
  expect.fail "odocker.reset.completion.image should be defined"
fi

# ─────────────────────────────────────────────────────────────────────────────
# Test: odocker.rebuild completion function exists
# ─────────────────────────────────────────────────────────────────────────────
if type odocker.rebuild.completion.workspace >/dev/null 2>&1; then
  expect.pass "odocker.rebuild.completion.workspace is defined"
else
  expect.fail "odocker.rebuild.completion.workspace should be defined"
fi

# ─────────────────────────────────────────────────────────────────────────────
# Completion function tests (parameter.completion)
# ─────────────────────────────────────────────────────────────────────────────

test.case $level "odocker.parameter.completion.shell returns values" \
  odocker.parameter.completion.shell
COMP_OUTPUT=$(odocker.parameter.completion.shell 2>/dev/null)
if echo "$COMP_OUTPUT" | grep -q "bash" && echo "$COMP_OUTPUT" | grep -q "sh"; then
  expect.pass "shell completion returns bash, sh"
else
  expect.fail "shell completion should return bash, sh"
fi

test.case $level "odocker.parameter.completion.container exists" \
  type -t odocker.parameter.completion.container
if type -t odocker.parameter.completion.container &>/dev/null; then
  expect.pass "odocker.parameter.completion.container function exists"
else
  expect.fail "odocker.parameter.completion.container should exist"
fi

test.case $level "odocker.parameter.completion.image exists" \
  type -t odocker.parameter.completion.image
if type -t odocker.parameter.completion.image &>/dev/null; then
  expect.pass "odocker.parameter.completion.image function exists"
else
  expect.fail "odocker.parameter.completion.image should exist"
fi

test.case $level "odocker.parameter.completion.workspace exists" \
  type -t odocker.parameter.completion.workspace
if type -t odocker.parameter.completion.workspace &>/dev/null; then
  expect.pass "odocker.parameter.completion.workspace function exists"
else
  expect.fail "odocker.parameter.completion.workspace should exist"
fi

# ─────────────────────────────────────────────────────────────────────────────
# Test: odocker.list.running delegates to odocker.ps
# ─────────────────────────────────────────────────────────────────────────────
test.case $level "odocker.list.running delegates to ps" \
  odocker list.running
if [ "$DOCKER_AVAILABLE" -eq 0 ]; then
  expect.pass "odocker list.running skipped (Docker not available)"
elif [ "$RETURN_VALUE" -eq 0 ]; then
  expect.pass "odocker list.running exits 0"
else
  expect.fail "odocker list.running exits $RETURN_VALUE (expected 0)"
fi

# ─────────────────────────────────────────────────────────────────────────────
# Test: odocker.enter requires parameter
# ─────────────────────────────────────────────────────────────────────────────
test.case $level "odocker.enter requires parameter" \
  odocker enter
if [ "$RETURN_VALUE" -eq 1 ]; then
  expect.pass "odocker enter without args exits 1"
else
  expect.fail "odocker enter without args exits $RETURN_VALUE (expected 1)"
fi

# ─────────────────────────────────────────────────────────────────────────────
# Test: odocker.enter completion function exists
# ─────────────────────────────────────────────────────────────────────────────
if type odocker.enter.completion.container >/dev/null 2>&1; then
  expect.pass "odocker.enter.completion.container is defined"
else
  expect.fail "odocker.enter.completion.container should be defined"
fi

# ─────────────────────────────────────────────────────────────────────────────
# Test: odocker.create requires parameter
# ─────────────────────────────────────────────────────────────────────────────
test.case $level "odocker.create requires parameter" \
  odocker create
if [ "$RETURN_VALUE" -eq 1 ]; then
  expect.pass "odocker create without args exits 1"
else
  expect.fail "odocker create without args exits $RETURN_VALUE (expected 1)"
fi

# ─────────────────────────────────────────────────────────────────────────────
# Test: odocker.create completion function exists
# ─────────────────────────────────────────────────────────────────────────────
if type odocker.create.completion.image >/dev/null 2>&1; then
  expect.pass "odocker.create.completion.image is defined"
else
  expect.fail "odocker.create.completion.image should be defined"
fi

# ─────────────────────────────────────────────────────────────────────────────
# Test: compose methods require compose file (error without one)
# ─────────────────────────────────────────────────────────────────────────────
_COMPOSE_DIR="/tmp/odocker_compose_test.$$"
mkdir -p "$_COMPOSE_DIR"

# Test compose fails without compose file
(cd "$_COMPOSE_DIR" && odocker.compose) >/dev/null 2>&1
if [ $? -ne 0 ]; then
  expect.pass "odocker compose fails without compose file"
else
  expect.fail "odocker compose should fail without compose file"
fi

# Test up/down require parameter
test.case $level "odocker.up requires parameter" \
  odocker up
if [ "$RETURN_VALUE" -eq 1 ]; then
  expect.pass "odocker up without args exits 1"
else
  expect.fail "odocker up without args exits $RETURN_VALUE (expected 1)"
fi

test.case $level "odocker.down requires parameter" \
  odocker down
if [ "$RETURN_VALUE" -eq 1 ]; then
  expect.pass "odocker down without args exits 1"
else
  expect.fail "odocker down without args exits $RETURN_VALUE (expected 1)"
fi

rm -rf "$_COMPOSE_DIR"

# ─────────────────────────────────────────────────────────────────────────────
# Test: compose file detection finds all variants
# ─────────────────────────────────────────────────────────────────────────────
_COMPOSE_DIR="/tmp/odocker_compose_detect.$$"

for variant in compose.yaml compose.yml docker-compose.yml docker-compose.yaml; do
  mkdir -p "$_COMPOSE_DIR"
  touch "$_COMPOSE_DIR/$variant"
  RESULT=$(private.odocker.compose.file.in "$_COMPOSE_DIR" 2>/dev/null)
  if [ -n "$RESULT" ]; then
    expect.pass "compose file detected: $variant"
  else
    expect.fail "compose file not detected: $variant"
  fi
  rm -rf "$_COMPOSE_DIR"
done

# ─────────────────────────────────────────────────────────────────────────────
# Test: compose completion functions exist
# ─────────────────────────────────────────────────────────────────────────────
for func in odocker.compose.completion.service odocker.up.completion.container odocker.down.completion.container; do
  if type "$func" >/dev/null 2>&1; then
    expect.pass "$func is defined"
  else
    expect.fail "$func should be defined"
  fi
done

# ─────────────────────────────────────────────────────────────────────────────
# Test: odocker.lifecycle runs without error
# ─────────────────────────────────────────────────────────────────────────────
test.case $level "odocker.lifecycle runs" \
  odocker lifecycle
if [ "$DOCKER_AVAILABLE" -eq 0 ]; then
  expect.pass "odocker lifecycle skipped (Docker not available)"
elif [ "$RETURN_VALUE" -eq 0 ]; then
  expect.pass "odocker lifecycle exits 0"
else
  expect.fail "odocker lifecycle exits $RETURN_VALUE (expected 0)"
fi

# ─────────────────────────────────────────────────────────────────────────────
# Test: odocker.parameter.completion.service exists
# ─────────────────────────────────────────────────────────────────────────────
if type odocker.parameter.completion.service >/dev/null 2>&1; then
  expect.pass "odocker.parameter.completion.service is defined"
else
  expect.fail "odocker.parameter.completion.service should be defined"
fi

# ─────────────────────────────────────────────────────────────────────────────
# Test: odocker.workspace.get is defined
# ─────────────────────────────────────────────────────────────────────────────
if type odocker.workspace.get >/dev/null 2>&1; then
  expect.pass "odocker.workspace.get is defined"
else
  expect.fail "odocker.workspace.get should be defined"
fi

# ─────────────────────────────────────────────────────────────────────────────
# Test: odocker.workspace.set without args falls back to $ODOCKER_WORKSPACES_DEFAULT
# (only meaningful when the default path exists on the machine running tests —
# on a fresh/minimal install it may not, in which case the call fails with
# "Directory not found", which is also expected).
# Saves + restores real config so the test is non-destructive.
# ─────────────────────────────────────────────────────────────────────────────
_WS_DEFAULT="/var/dev/EAMD.ucp/Components/com/ceruleanCircle/EAM/1_infrastructure/DockerWorkspaces"
_WS_DEFAULT_EXISTS=0
[ -d "$_WS_DEFAULT" ] && _WS_DEFAULT_EXISTS=1

_WS_SAVE_USER_ENV="$(cat "$CONFIG_PATH/user.env" 2>/dev/null)"
_WS_SAVE_ODOCKER_ENV=""
_WS_SAVE_HAD_ODOCKER_ENV=0
if [ -f "$CONFIG_PATH/odocker.env" ]; then
  _WS_SAVE_HAD_ODOCKER_ENV=1
  _WS_SAVE_ODOCKER_ENV="$(cat "$CONFIG_PATH/odocker.env")"
fi

test.case $level "odocker.workspace.set (no args) uses platform default" \
  odocker workspace.set
if [ "$_WS_DEFAULT_EXISTS" = "1" ]; then
  if [ "$RETURN_VALUE" -eq 0 ] \
     && [ -f "$CONFIG_PATH/odocker.env" ] \
     && grep -q "$_WS_DEFAULT" "$CONFIG_PATH/odocker.env"; then
    expect.pass "odocker workspace.set (no args) set odocker.env to the default"
  else
    expect.fail "odocker workspace.set (no args) did not persist the default (rc=$RETURN_VALUE)"
  fi
else
  if [ "$RETURN_VALUE" -eq 1 ]; then
    expect.pass "odocker workspace.set (no args) correctly errors when default dir is absent"
  else
    expect.fail "odocker workspace.set (no args) exited $RETURN_VALUE with default dir absent (expected 1)"
  fi
fi

# Restore the real config (undo whatever workspace.set wrote)
echo "$_WS_SAVE_USER_ENV" > "$CONFIG_PATH/user.env"
if [ "$_WS_SAVE_HAD_ODOCKER_ENV" = "1" ]; then
  echo "$_WS_SAVE_ODOCKER_ENV" > "$CONFIG_PATH/odocker.env"
else
  rm -f "$CONFIG_PATH/odocker.env"
fi

# ─────────────────────────────────────────────────────────────────────────────
# Test: odocker.workspace.set rejects non-existent directory
# ─────────────────────────────────────────────────────────────────────────────
test.case $level "odocker.workspace.set rejects non-existent dir" \
  odocker workspace.set /nonexistent/path/$$
if [ "$RETURN_VALUE" -eq 1 ]; then
  expect.pass "odocker workspace.set rejects non-existent directory"
else
  expect.fail "odocker workspace.set should reject non-existent directory"
fi

# ─────────────────────────────────────────────────────────────────────────────
# Test: odocker.container.list and odocker.image.list exist
# ─────────────────────────────────────────────────────────────────────────────
if type odocker.container.list >/dev/null 2>&1; then
  expect.pass "odocker.container.list is defined"
else
  expect.fail "odocker.container.list should be defined"
fi

if type odocker.image.list >/dev/null 2>&1; then
  expect.pass "odocker.image.list is defined"
else
  expect.fail "odocker.image.list should be defined"
fi

# ─────────────────────────────────────────────────────────────────────────────
# Test: compose file detection searches workspace dir (not just CWD)
# ─────────────────────────────────────────────────────────────────────────────
_WS_DIR="/tmp/odocker_ws_test.$$"
mkdir -p "$_WS_DIR"
touch "$_WS_DIR/compose.yaml"
_OLD_WS="$ODOCKER_WORKSPACES"
ODOCKER_WORKSPACES="$_WS_DIR"

RESULT=$(cd /tmp && private.odocker.compose.file.find 2>/dev/null)
if [ -n "$RESULT" ]; then
  expect.pass "compose file found via workspace dir (not CWD)"
else
  expect.fail "compose file should be found via workspace dir"
fi

ODOCKER_WORKSPACES="$_OLD_WS"
rm -rf "$_WS_DIR"

# ─────────────────────────────────────────────────────────────────────────────
# Test: private.odocker.port.offset — offset values (< 1024)
# ─────────────────────────────────────────────────────────────────────────────
declare -A _OFFSET_EXPECT=(
  ["0"]="0"
  ["1000"]="1000"
  ["500"]="500"
)
for input in "${!_OFFSET_EXPECT[@]}"; do
  RESULT=$(private.odocker.port.offset "$input")
  if [ "$RESULT" = "${_OFFSET_EXPECT[$input]}" ]; then
    expect.pass "port.offset $input → $RESULT (offset passthrough)"
  else
    expect.fail "port.offset $input: expected ${_OFFSET_EXPECT[$input]}, got: $RESULT"
  fi
done
unset _OFFSET_EXPECT

# ─────────────────────────────────────────────────────────────────────────────
# Test: private.odocker.port.offset — SSH port values (>= 1024)
# ─────────────────────────────────────────────────────────────────────────────
declare -A _PORT_EXPECT=(
  ["6022"]="-2000"
  ["8022"]="0"
  ["9022"]="1000"
  ["11022"]="3000"
)
for input in "${!_PORT_EXPECT[@]}"; do
  RESULT=$(private.odocker.port.offset "$input")
  if [ "$RESULT" = "${_PORT_EXPECT[$input]}" ]; then
    expect.pass "port.offset $input → $RESULT (SSH port → offset)"
  else
    expect.fail "port.offset $input: expected ${_PORT_EXPECT[$input]}, got: $RESULT"
  fi
done
unset _PORT_EXPECT

# ─────────────────────────────────────────────────────────────────────────────
# Test: private.odocker.port.offset — default (no argument)
# ─────────────────────────────────────────────────────────────────────────────
RESULT=$(private.odocker.port.offset)
if [ "$RESULT" = "0" ]; then
  expect.pass "port.offset with no arg defaults to 0"
else
  expect.fail "port.offset with no arg: expected 0, got: $RESULT"
fi

# ─────────────────────────────────────────────────────────────────────────────
# Test: private.odocker.port.offset — values in [1024, 3022) fall back to
# offset-mode. Port-mode of those produces offset = value - 8022, which pushes
# the lowest mapped port (5001) below 1 — invalid. The fallback rule says
# "if port-mode would yield an invalid port, treat the input as an offset
# instead". Regression for the 3000 → -5022 bug (run.sshd refused with
# "would produce invalid port -21").
#
# Values in [3022, 8022) stay in port-mode (negative offset but lowest port
# still ≥ 1) — see e.g. 6022 → -2000 above.
# ─────────────────────────────────────────────────────────────────────────────
declare -A _FALLBACK_EXPECT=(
  ["2000"]="2000"
  ["3000"]="3000"
)
for input in "${!_FALLBACK_EXPECT[@]}"; do
  RESULT=$(private.odocker.port.offset "$input")
  if [ "$RESULT" = "${_FALLBACK_EXPECT[$input]}" ]; then
    expect.pass "port.offset $input → $RESULT (ambiguous-value fallback to offset)"
  else
    expect.fail "port.offset $input: expected ${_FALLBACK_EXPECT[$input]}, got: $RESULT (port-mode should have fallen back)"
  fi
done
unset _FALLBACK_EXPECT

# ─────────────────────────────────────────────────────────────────────────────
# Test: private.odocker.container.ports — container with no ports
# ─────────────────────────────────────────────────────────────────────────────
RESULT=$(private.odocker.container.ports "nonexistent_container_$$")
if [ -z "$RESULT" ]; then
  expect.pass "container.ports returns empty for nonexistent container"
else
  expect.fail "container.ports should return empty for nonexistent container, got: $RESULT"
fi

# ─────────────────────────────────────────────────────────────────────────────
# Test: run.sshd requires parameter
# ─────────────────────────────────────────────────────────────────────────────
test.case $level "odocker.run.sshd requires parameter" \
  odocker run.sshd
if [ "$RETURN_VALUE" -eq 1 ]; then
  expect.pass "odocker run.sshd without args exits 1"
else
  expect.fail "odocker run.sshd without args exits $RETURN_VALUE (expected 1)"
fi

# ─────────────────────────────────────────────────────────────────────────────
# Test: odocker.clone requires parameter
# ─────────────────────────────────────────────────────────────────────────────
test.case $level "odocker.clone requires parameter" \
  odocker clone
if [ "$RETURN_VALUE" -eq 1 ]; then
  expect.pass "odocker clone without args exits 1"
else
  expect.fail "odocker clone without args exits $RETURN_VALUE (expected 1)"
fi

# ─────────────────────────────────────────────────────────────────────────────
# Test: odocker.clone rejects nonexistent container
# ─────────────────────────────────────────────────────────────────────────────
test.case $level "odocker.clone rejects nonexistent container" \
  odocker clone nonexistent_container_$$
if [ "$RETURN_VALUE" -eq 1 ]; then
  expect.pass "odocker clone nonexistent container exits 1"
else
  expect.fail "odocker clone nonexistent exits $RETURN_VALUE (expected 1)"
fi

# ─────────────────────────────────────────────────────────────────────────────
# Test: odocker.clone completion function exists
# ─────────────────────────────────────────────────────────────────────────────
if type odocker.clone.completion.container >/dev/null 2>&1; then
  expect.pass "odocker.clone.completion.container is defined"
else
  expect.fail "odocker.clone.completion.container should be defined"
fi

# ─────────────────────────────────────────────────────────────────────────────
# Test: odocker.install requires parameter
# ─────────────────────────────────────────────────────────────────────────────
test.case $level "odocker.install requires parameter" \
  odocker install
if [ "$RETURN_VALUE" -eq 1 ]; then
  expect.pass "odocker install without args exits 1"
else
  expect.fail "odocker install without args exits $RETURN_VALUE (expected 1)"
fi

# ─────────────────────────────────────────────────────────────────────────────
# Test: odocker.install rejects nonexistent container
# ─────────────────────────────────────────────────────────────────────────────
test.case $level "odocker.install rejects nonexistent container" \
  odocker install nonexistent_container_$$
if [ "$RETURN_VALUE" -eq 1 ]; then
  expect.pass "odocker install nonexistent container exits 1"
else
  expect.fail "odocker install nonexistent exits $RETURN_VALUE (expected 1)"
fi

# ─────────────────────────────────────────────────────────────────────────────
# Test: odocker.install completion function exists
# ─────────────────────────────────────────────────────────────────────────────
if type odocker.install.completion.container >/dev/null 2>&1; then
  expect.pass "odocker.install.completion.container is defined"
else
  expect.fail "odocker.install.completion.container should be defined"
fi

if type odocker.exec.command.completion.container >/dev/null 2>&1; then
  expect.pass "odocker.exec.command.completion.container is defined"
else
  expect.fail "odocker.exec.command.completion.container should be defined"
fi

# ─────────────────────────────────────────────────────────────────────────────
# Test: workspace.set canonicalizes relatives + persists to $CONFIG_PATH/odocker.env
# These tests mutate user.env and odocker.env; wrap in save/restore to keep
# the dev machine's real config pristine when test.suite runs ad-hoc.
# ─────────────────────────────────────────────────────────────────────────────
_WS_ORIG_VAR="$ODOCKER_WORKSPACES"
_WS_ORIG_USER_ENV="$(cat "$CONFIG_PATH/user.env" 2>/dev/null)"
_WS_ORIG_ODOCKER_ENV=""
_WS_HAD_ODOCKER_ENV=0
if [ -f "$CONFIG_PATH/odocker.env" ]; then
  _WS_HAD_ODOCKER_ENV=1
  _WS_ORIG_ODOCKER_ENV="$(cat "$CONFIG_PATH/odocker.env")"
fi

# T-WS-CANONICAL: relative path → absolute (read from odocker.env on disk,
# because `odocker workspace.set` runs in a subshell and its `export` does
# NOT propagate back to the test shell's $ODOCKER_WORKSPACES)
_WS_TMP="/tmp/odocker_ws_canon.$$"
mkdir -p "$_WS_TMP"
_WS_CWD="$(pwd)"
cd /tmp
odocker workspace.set "./$(basename "$_WS_TMP")" >/dev/null 2>&1
cd "$_WS_CWD"
_WS_STORED=""
if [ -f "$CONFIG_PATH/odocker.env" ]; then
  # config.save's sed pipeline produces `export declare ODOCKER_WORKSPACES="…"`
  # on some paths (the `declare` is harmless; bash parses it as the builtin).
  # Accept either form.
  _WS_STORED=$(grep "ODOCKER_WORKSPACES=" "$CONFIG_PATH/odocker.env" | sed 's/.*ODOCKER_WORKSPACES="\(.*\)"$/\1/')
fi
if [ "$_WS_STORED" = "$_WS_TMP" ]; then
  expect.pass "T-WS-CANONICAL: relative path canonicalized to '$_WS_TMP'"
else
  expect.fail "T-WS-CANONICAL: odocker.env stored '$_WS_STORED', expected '$_WS_TMP'"
fi

# T-WS-ENVFILE: $CONFIG_PATH/odocker.env exists with ODOCKER_WORKSPACES=
if [ -f "$CONFIG_PATH/odocker.env" ] && grep -q "ODOCKER_WORKSPACES=" "$CONFIG_PATH/odocker.env"; then
  expect.pass "T-WS-ENVFILE: odocker.env contains ODOCKER_WORKSPACES="
else
  expect.fail "T-WS-ENVFILE: $CONFIG_PATH/odocker.env missing or has no ODOCKER_WORKSPACES="
fi

# T-WS-REGISTERED: user.env sources odocker.env
if grep -q "odocker\.env" "$CONFIG_PATH/user.env" 2>/dev/null; then
  expect.pass "T-WS-REGISTERED: user.env sources odocker.env"
else
  expect.fail "T-WS-REGISTERED: user.env has no 'source .../odocker.env' line"
fi

# T-WS-MIGRATED: no legacy ODOCKER_WORKSPACES= in user.env
# Seed a legacy entry, then call workspace.set, then assert it's gone.
echo 'export ODOCKER_WORKSPACES="/legacy/leftover/path"' >> "$CONFIG_PATH/user.env"
_WS_TMP2="/tmp/odocker_ws_migrate.$$"
mkdir -p "$_WS_TMP2"
odocker workspace.set "$_WS_TMP2" >/dev/null 2>&1
if ! grep -q "^export ODOCKER_WORKSPACES=" "$CONFIG_PATH/user.env" 2>/dev/null; then
  expect.pass "T-WS-MIGRATED: legacy ODOCKER_WORKSPACES= removed from user.env"
else
  expect.fail "T-WS-MIGRATED: user.env still contains ODOCKER_WORKSPACES="
fi

# T-CONFIG-UNSET: the new primitive removes a single key idempotently.
# Invoke `config unset` as a script (subshell) with CONFIG overridden inline
# via `VAR=value command` syntax — avoids sourcing config into the test shell.
_UNSET_TMP="/tmp/unset_test.$$"
printf 'export ALPHA="a"\nexport BETA="b"\nexport GAMMA="c"\n' > "$_UNSET_TMP"
CONFIG="$_UNSET_TMP" config unset BETA >/dev/null 2>&1
if grep -q "^export ALPHA=" "$_UNSET_TMP" \
   && ! grep -q "^export BETA="  "$_UNSET_TMP" \
   && grep -q "^export GAMMA=" "$_UNSET_TMP"; then
  expect.pass "T-CONFIG-UNSET: removes only the target key"
else
  expect.fail "T-CONFIG-UNSET: key removal or neighbours disturbed"
fi
rm -f "$_UNSET_TMP"

# Cleanup — restore original config state and drop temp workspace dirs
echo "$_WS_ORIG_USER_ENV" > "$CONFIG_PATH/user.env"
if [ "$_WS_HAD_ODOCKER_ENV" = "1" ]; then
  echo "$_WS_ORIG_ODOCKER_ENV" > "$CONFIG_PATH/odocker.env"
else
  rm -f "$CONFIG_PATH/odocker.env"
fi
ODOCKER_WORKSPACES="$_WS_ORIG_VAR"
rm -rf "$_WS_TMP" "$_WS_TMP2"

### test.method
