#!/usr/bin/env bash
# Tests for user — OOSH user and SSH identity management
# Tests: id, list, list.all, list.groups, get.current.identity, ssh.status, list.other.identities

level=$1
if [ -z "$level" ]; then
  level=1
fi
echo "starting: ${BASH_SOURCE[@]##*/} <LOG_LEVEL=$level>"

source this
source test.suite

log.level $level

# ============================================================================
# T1: user is callable
# ============================================================================
test.case $level "user is callable" \
  which user

if which user &>/dev/null; then
  expect.pass "user found on PATH"
else
  expect.fail "user should be on PATH"
fi

# ============================================================================
# T2: user.id returns current user id
# ============================================================================
test.case $level "user.id returns user id" \
  bash -c 'source this; source user; user.id -u -n'

ID_OUTPUT=$(bash -c 'source this; source user; user.id -u -n 2>/dev/null')
if [ "$ID_OUTPUT" = "$(id -u -n)" ]; then
  expect.pass "user.id -u -n returns $(id -u -n)"
else
  expect.fail "user.id -u -n should return $(id -u -n), got: $ID_OUTPUT"
fi

# ============================================================================
# T3: user.list returns users (filters _ prefixed)
# ============================================================================
test.case $level "user.list returns users" \
  bash -c 'source this; source user; user.list'

LIST_OUTPUT=$(bash -c 'source this; source user; user.list 2>/dev/null')
if [ -n "$LIST_OUTPUT" ]; then
  expect.pass "user.list returned users"
else
  expect.fail "user.list should return at least one user"
fi

# ============================================================================
# T4: user.list.all returns all users including system
# ============================================================================
test.case $level "user.list.all returns all users" \
  bash -c 'source this; source user; user.list.all'

ALL_OUTPUT=$(bash -c 'source this; source user; user.list.all 2>/dev/null')
if [ -n "$ALL_OUTPUT" ]; then
  # list.all should have >= list (includes _ prefixed)
  ALL_COUNT=$(echo "$ALL_OUTPUT" | wc -l | tr -d ' ')
  expect.pass "user.list.all returned $ALL_COUNT users"
else
  expect.fail "user.list.all should return users"
fi

# ============================================================================
# T5: user.list.groups returns groups
# ============================================================================
test.case $level "user.list.groups returns groups" \
  bash -c 'source this; source user; user.list.groups'

GROUPS_OUTPUT=$(bash -c 'source this; source user; user.list.groups 2>/dev/null')
if [ -n "$GROUPS_OUTPUT" ]; then
  expect.pass "user.list.groups returned groups"
else
  expect.fail "user.list.groups should return at least one group"
fi

# ============================================================================
# T6: user.get.current.identity returns SSH dir
# ============================================================================
test.case $level "user.get.current.identity returns SSH dir" \
  bash -c 'source this; source ossh; source user; user.get.current.identity'

IDENTITY_OUTPUT=$(bash -c 'source this; source ossh; source user; user.get.current.identity 2>/dev/null')
if [ -n "$IDENTITY_OUTPUT" ]; then
  if [ -d "$IDENTITY_OUTPUT" ]; then
    expect.pass "user.get.current.identity returned valid dir: $IDENTITY_OUTPUT"
  else
    expect.fail "user.get.current.identity returned non-existent dir: $IDENTITY_OUTPUT"
  fi
else
  expect.fail "user.get.current.identity should return a path"
fi

# ============================================================================
# T7: user.ssh.status returns 0 when SSH is configured
# ============================================================================
test.case $level "user.ssh.status returns 0 when SSH configured" \
  bash -c 'source this; source ossh; source user; user.ssh.status'

bash -c 'source this; source ossh; source user; user.ssh.status 2>/dev/null'
SSH_RC=$?
if [ -d "$HOME/.ssh" ]; then
  if [ $SSH_RC -eq 0 ]; then
    expect.pass "user.ssh.status returns 0 (SSH configured)"
  else
    expect.fail "user.ssh.status should return 0 when ~/.ssh exists (got $SSH_RC)"
  fi
else
  if [ $SSH_RC -ne 0 ]; then
    expect.pass "user.ssh.status returns non-zero (no ~/.ssh)"
  else
    expect.fail "user.ssh.status should return non-zero when ~/.ssh missing"
  fi
fi

# ============================================================================
# T8: user.id.completion returns valid options
# ============================================================================
test.case $level "user.id.completion returns options" \
  bash -c 'source this; source user; user.id.completion'

COMP_OUTPUT=$(bash -c 'source this; source user; user.id.completion 2>/dev/null')
if echo "$COMP_OUTPUT" | grep -q '\-u'; then
  expect.pass "user.id.completion includes -u flag"
else
  expect.fail "user.id.completion should include -u flag"
fi

# ============================================================================
# T9: user.list filters underscore-prefixed users
# ============================================================================
test.case $level "user.list filters _ prefixed system users" \
  bash -c 'source this; source user; user.list'

FILTERED=$(bash -c 'source this; source user; user.list 2>/dev/null' | grep '^_' || true)
if [ -z "$FILTERED" ]; then
  expect.pass "user.list correctly filters _ prefixed users"
else
  expect.fail "user.list should not contain _ prefixed users, found: $(echo "$FILTERED" | head -1)"
fi

echo ""
echo "test.user complete"

# ============================================================================
# Password-handling tests (user.create password keyword + user.password.set)
# Guarded: skipped if sudo -n is not available (no passwordless sudo).
# All created accounts are cleaned up with user.delete.linux (cross-platform).
# ============================================================================

if sudo -n true 2>/dev/null; then

  # Source the methods we're testing so we can invoke them directly in this shell
  source this
  source user
  os.check.env

  # Platform-specific helper: returns 0 if the given user has a password hash set
  user.test.hasPassword() {
    local u="$1"
    if [ "$OOSH_OS" = "darwin" ]; then
      sudo dscl . -read "/Users/$u" AuthenticationAuthority 2>/dev/null | grep -q ';ShadowHash;'
    else
      # GNU & BusyBox /etc/shadow: field 2 is the hash; '!' or '*' (or empty) means locked
      local field2
      field2=$(sudo grep -E "^$u:" /etc/shadow 2>/dev/null | cut -d: -f2)
      [ -n "$field2" ] && [ "$field2" != "!" ] && [ "$field2" != "*" ] && [ "$field2" != "!!" ]
    fi
  }

  # --------------------------------------------------------------------------
  # T-a: user.create with explicit password sets the password
  # --------------------------------------------------------------------------
  test.case $level "user.create ooshTestA password secretA sets password" \
    bash -c 'source this; source user; user.create ooshTestA password secretA >/dev/null 2>&1; id ooshTestA'

  user.delete.linux ooshTestA >/dev/null 2>&1  # ensure clean slate
  user.create ooshTestA password secretA >/dev/null 2>&1
  if id ooshTestA >/dev/null 2>&1 && user.test.hasPassword ooshTestA; then
    expect.pass "ooshTestA created and password is set"
  else
    expect.fail "ooshTestA should exist with a password hash"
  fi
  user.delete.linux ooshTestA >/dev/null 2>&1

  # --------------------------------------------------------------------------
  # T-b: user.create defaults password to username
  # --------------------------------------------------------------------------
  test.case $level "user.create ooshTestB defaults password to username" \
    bash -c 'source this; source user; user.create ooshTestB >/dev/null 2>&1; id ooshTestB'

  user.delete.linux ooshTestB >/dev/null 2>&1
  user.create ooshTestB >/dev/null 2>&1
  if id ooshTestB >/dev/null 2>&1 && user.test.hasPassword ooshTestB; then
    expect.pass "ooshTestB created and default password (username) is set"
  else
    expect.fail "ooshTestB should exist with a password hash"
  fi
  user.delete.linux ooshTestB >/dev/null 2>&1

  # --------------------------------------------------------------------------
  # T-c: explicit empty password is a no-op (account stays locked)
  # --------------------------------------------------------------------------
  test.case $level "user.create ooshTestC password '' leaves account locked" \
    bash -c 'source this; source user; user.create ooshTestC password "" >/dev/null 2>&1; id ooshTestC'

  user.delete.linux ooshTestC >/dev/null 2>&1
  user.create ooshTestC password "" >/dev/null 2>&1
  if id ooshTestC >/dev/null 2>&1 && ! user.test.hasPassword ooshTestC; then
    expect.pass "ooshTestC created with no password (locked) — back-compat preserved"
  else
    expect.fail "ooshTestC should exist with no password hash"
  fi
  user.delete.linux ooshTestC >/dev/null 2>&1

  # --------------------------------------------------------------------------
  # T-d: user.password.set resets an existing user's password
  # --------------------------------------------------------------------------
  test.case $level "user.password.set resets ooshTestD's password" \
    bash -c 'source this; source user; user.create ooshTestD password "" >/dev/null 2>&1; user.password.set ooshTestD freshPw >/dev/null 2>&1'

  user.delete.linux ooshTestD >/dev/null 2>&1
  user.create ooshTestD password "" >/dev/null 2>&1
  user.password.set ooshTestD freshPw >/dev/null 2>&1
  if user.test.hasPassword ooshTestD; then
    expect.pass "user.password.set populated ooshTestD's password hash"
  else
    expect.fail "user.password.set should populate the password hash"
  fi
  user.delete.linux ooshTestD >/dev/null 2>&1

  # --------------------------------------------------------------------------
  # T-e2: short username (≤3 chars) still produces a usable account
  # Exercises the darwin auto-pad branch; on Linux the short password passes
  # chpasswd directly. Either way the created user should have password metadata.
  # --------------------------------------------------------------------------
  test.case $level "user.create bo (short name) produces a user with password" \
    bash -c 'source this; source user; user.create bo >/dev/null 2>&1; id bo'

  user.delete.linux bo >/dev/null 2>&1
  user.create bo >/dev/null 2>&1
  if id bo >/dev/null 2>&1 && user.test.hasPassword bo; then
    expect.pass "short-username 'bo' created with a valid password"
  else
    expect.fail "short-username 'bo' should be creatable with the default-password auto-pad on darwin / chpasswd on Linux"
  fi
  user.delete.linux bo >/dev/null 2>&1

  unset -f user.test.hasPassword

else
  # Skipped path — keep the run green on envs without passwordless sudo
  test.case $level "password tests skipped (no passwordless sudo)" \
    true
  expect.pass "skipped — 'sudo -n' not available"
fi

# ============================================================================
# user.oosh.install — always-runnable negative paths (positive path is E2E)
# ============================================================================

source this
source user

# T-e: missing username → error + non-zero
test.case $level "user.oosh.install with no args returns non-zero" \
  bash -c 'source this; source user; user.oosh.install 2>/dev/null'

bash -c 'source this; source user; user.oosh.install 2>/dev/null'
if [ $? -ne 0 ]; then
  expect.pass "user.oosh.install returns non-zero when username missing"
else
  expect.fail "user.oosh.install should fail without a username"
fi

# T-f: nonexistent user → home-lookup fails → non-zero
test.case $level "user.oosh.install for unknown user returns non-zero" \
  bash -c 'source this; source user; user.oosh.install __ooshNoSuchUser__ dev 2>/dev/null'

bash -c 'source this; source user; user.oosh.install __ooshNoSuchUser__ dev 2>/dev/null'
if [ $? -ne 0 ]; then
  expect.pass "user.oosh.install fails on unknown user"
else
  expect.fail "user.oosh.install should fail for a non-existent user"
fi

# T-g: os.check dispatches the shell-change helper to a real function
test.case $level "os.check resolves user.oosh.install.shell to a defined function" \
  bash -c 'source this; source os; source user; os.check private.user.oosh.install.shell && type -t "$RESULT"'

RESOLVED=$(bash -c 'source this; source os; source user; if os.check private.user.oosh.install.shell; then echo "$RESULT"; fi' 2>/dev/null)
if [ "$RESOLVED" = "private.user.oosh.install.shell.darwin" ] || [ "$RESOLVED" = "private.user.oosh.install.shell.linux" ]; then
  expect.pass "os.check resolves to '$RESOLVED'"
else
  expect.fail "os.check should resolve to .darwin or .linux variant, got: '$RESOLVED'"
fi

# T-h: completion lists the current host's worktree branches (non-empty when OOSH_DIR is set)
test.case $level "user.oosh.install.completion.branch emits candidates" \
  bash -c 'source this; source user; user.oosh.install.completion.branch'

BRANCHES=$(bash -c 'source this; source user; user.oosh.install.completion.branch 2>/dev/null')
if [ -n "$BRANCHES" ]; then
  expect.pass "branch completion returned: $(echo "$BRANCHES" | head -1 | tr -d '\n')…"
else
  # Not a hard failure — on some envs OOSH_DIR may not sit in a worktree-style layout
  expect.pass "branch completion returned empty (acceptable on non-worktree layouts)"
fi

# ============================================================================
# T-USER-INIT-SELF-HEAL: private.user.init fills in missing OS_CMD_* vars
# when os.commands.env was saved incomplete (e.g. private.check.pm wrote it
# during install before USER_MOD / USER_DEL were computed). Reproduces the
# hetzsim bug where `user.group.add dev bob` silently degraded and bob
# never joined `dev`, breaking write access to log.live.out.
#
# CRITICAL: private.user.init transitively calls `config save`, which
# writes to $CONFIG (NOT just $CONFIG_PATH). If we only override
# CONFIG_PATH here, config save still hits the real user.env and can
# poison it with the temp path. Run the whole helper inside a bash -c
# subshell where BOTH CONFIG and CONFIG_PATH are redirected to the
# throw-away dir. Results are captured via files written to that dir,
# not via env vars that would leak back.
# ============================================================================
TUSR_SELF_HEAL=$(mktemp -d)
cat > "$TUSR_SELF_HEAL/os.commands.env" <<'EOF'
export declare OS_CMD_GROUP_ADD="groupadd -f"
export declare OS_CMD_USER_ADD="useradd -g dev"
EOF

# Run in a fully-isolated subshell: fresh CONFIG + CONFIG_PATH pointing at
# the temp dir, private.user.init fires, we capture the resulting
# OS_CMD_USER_MOD / OS_CMD_USER_DEL values into files we can grep from
# the outer shell. No exports leak back to the caller's env.
bash -c '
  export CONFIG_PATH="'"$TUSR_SELF_HEAL"'"
  export CONFIG="'"$TUSR_SELF_HEAL"'/user.env"
  : > "$CONFIG"
  unset OS_CMD_USER_MOD OS_CMD_USER_DEL OS_CMD_USER_ADD OS_CMD_GROUP_ADD
  source "'"$OOSH_DIR"'/user" >/dev/null 2>&1 || true
  private.user.init >/dev/null 2>&1 || true
  printf "%s" "$OS_CMD_USER_MOD" > "'"$TUSR_SELF_HEAL"'/.result.USER_MOD"
  printf "%s" "$OS_CMD_USER_DEL" > "'"$TUSR_SELF_HEAL"'/.result.USER_DEL"
' >/dev/null 2>&1 || true

_HEAL_USER_MOD=$(cat "$TUSR_SELF_HEAL/.result.USER_MOD" 2>/dev/null)
_HEAL_USER_DEL=$(cat "$TUSR_SELF_HEAL/.result.USER_DEL" 2>/dev/null)

test.case $level "T-USER-INIT-SELF-HEAL: fills missing OS_CMD_USER_MOD" \
  echo "OS_CMD_USER_MOD=$_HEAL_USER_MOD"
if [ -n "$_HEAL_USER_MOD" ]; then
  expect.pass "OS_CMD_USER_MOD filled in: $_HEAL_USER_MOD"
else
  expect.fail "OS_CMD_USER_MOD still empty after private.user.init (self-heal broken)"
fi

test.case $level "T-USER-INIT-SELF-HEAL: fills missing OS_CMD_USER_DEL" \
  echo "OS_CMD_USER_DEL=$_HEAL_USER_DEL"
if [ -n "$_HEAL_USER_DEL" ]; then
  expect.pass "OS_CMD_USER_DEL filled in: $_HEAL_USER_DEL"
else
  expect.fail "OS_CMD_USER_DEL still empty after private.user.init"
fi

test.case $level "T-USER-INIT-SELF-HEAL: re-saves os.commands.env with complete set" \
  echo "(grep re-saved file)"
if grep -q 'OS_CMD_USER_MOD' "$TUSR_SELF_HEAL/os.commands.env" \
   && grep -q 'OS_CMD_USER_DEL' "$TUSR_SELF_HEAL/os.commands.env"; then
  expect.pass "os.commands.env re-saved with all four OS_CMD_* vars"
else
  expect.fail "os.commands.env not re-saved with complete set — future sessions still broken"
fi

test.case $level "T-USER-INIT-SELF-HEAL-ISOLATION: outer CONFIG_PATH unchanged" \
  echo "CONFIG_PATH=$CONFIG_PATH"
if [ "$CONFIG_PATH" != "$TUSR_SELF_HEAL" ] \
   && ! grep -qE 'CONFIG_PATH="/tmp/tmp\.' "$CONFIG" 2>/dev/null; then
  expect.pass "subshell isolation worked — outer CONFIG and user.env untouched"
else
  expect.fail "test poisoned outer config — CONFIG_PATH or user.env now references the temp dir"
fi

rm -rf "$TUSR_SELF_HEAL"

# ============================================================================
# T-PRIVATE-AS-USER-DISPATCH: private.as.user must try runuser FIRST (works
# without sudo when caller is root), then sudo -H -u (most distros), then
# su -s /bin/bash - … -c (root + no sudo, e.g. minimal containers). The
# dispatch order matters: runuser is the only path that doesn't need sudo
# at all, so it must precede sudo. su is last-resort. Reversing this would
# silently break sudo-less containers like naked alpine before sudo install.
# ============================================================================
source "$OOSH_DIR/user" >/dev/null 2>&1 || true

test.case $level "T-PRIVATE-AS-USER-DISPATCH: runuser → sudo → su fallback chain in correct order" \
  echo "(grep)"
PAU_BODY=$(declare -f private.as.user 2>/dev/null)
if printf "%s" "$PAU_BODY" | grep -qE 'runuser.*-u' \
   && printf "%s" "$PAU_BODY" | grep -qE 'sudo.*-H.*-u' \
   && printf "%s" "$PAU_BODY" | grep -qE 'su.*-s.*/bin/bash'; then
  # Verify ORDER: runuser line must appear before sudo, sudo before su.
  # Use word boundaries (\b) so 'command -v su' doesn't match 'command -v sudo'.
  _runLn=$(printf "%s" "$PAU_BODY" | grep -nE 'command -v runuser\b' | head -1 | cut -d: -f1)
  _sudoLn=$(printf "%s" "$PAU_BODY" | grep -nE 'command -v sudo\b' | head -1 | cut -d: -f1)
  _suLn=$(printf "%s" "$PAU_BODY" | grep -nE 'command -v su\b' | head -1 | cut -d: -f1)
  if [ -n "$_runLn" ] && [ -n "$_sudoLn" ] && [ -n "$_suLn" ] \
     && [ "$_runLn" -lt "$_sudoLn" ] && [ "$_sudoLn" -lt "$_suLn" ]; then
    expect.pass "private.as.user dispatch chain order: runuser → sudo → su"
  else
    expect.fail "private.as.user dispatch order wrong — runuser must precede sudo, sudo must precede su"
  fi
else
  expect.fail "private.as.user missing one of runuser/sudo/su dispatches — sudo-less containers will break"
fi

test.suite.save.results
