#!/usr/bin/env bash
# Tests for this — OOSH kernel
# Tests: method dispatch, parameter parsing, constructor, single-word dispatch
TEST_CATEGORY=core

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: this.start dispatches dotted method correctly
# ============================================================================
test.case $level "this.start dispatches dotted method (config.list)" \
  config list

if [ "$RETURN_VALUE" -eq 0 ]; then
  expect.pass "config list dispatched successfully"
else
  expect.fail "config list should dispatch to config.list()"
fi

# ============================================================================
# T2: this.absolutePath resolves relative paths
# ============================================================================
test.case $level "this.absolutePath resolves relative paths" \
  this.absolutePath "."

if [ -n "$RESULT" ] && [[ "$RESULT" == /* ]]; then
  expect.pass "this.absolutePath returned absolute path: $RESULT"
else
  expect.fail "this.absolutePath should return absolute path, got: $RESULT"
fi

# ============================================================================
# T3: this.isNumber validates numbers
# ============================================================================
test.case $level "this.isNumber validates numbers" \
  this.isNumber 42

if [ "$?" -eq 0 ]; then
  expect.pass "this.isNumber recognizes 42 as a number"
else
  expect.fail "this.isNumber should recognize 42 as a number"
fi

# ============================================================================
# T4: this.isNumber rejects non-numbers
# ============================================================================
test.case $level "this.isNumber rejects non-numbers" \
  this.isNumber "abc"

# Note: test.case sets RETURN_VALUE, not $?
if [ "$RETURN_VALUE" -ne 0 ]; then
  expect.pass "this.isNumber rejects 'abc'"
else
  # BUG: this.isNumber may accept non-numbers (rc=0 for 'abc')
  expect.fail "this.isNumber should reject 'abc' (RETURN_VALUE=$RETURN_VALUE)"
fi

# ============================================================================
# T5: this.help exists and runs
# ============================================================================
test.case $level "this.help function exists" \
  type -t this.help

if type -t this.help &>/dev/null; then
  expect.pass "this.help function exists"
else
  expect.fail "this.help should be a function"
fi

# ============================================================================
# BUG REPRODUCTION: Single-word method dispatch
# Known issue: single-word methods (e.g., hiveMind dashboard) may conflict
# with OOSH dispatch when scripts have sub-methods starting with the same word
# ============================================================================
test.case $level "BUG: single-word dispatch (config list vs config.list)" \
  echo "testing dispatch"

# config list should dispatch to config.list(), not try to run "list" as a command
CONFIG_OUTPUT=$($OOSH_DIR/config list 2>&1)
CONFIG_RC=$?
# config.list outputs to LOG_DEVICE, so check exit code only
if [ $CONFIG_RC -eq 0 ]; then
  expect.pass "single-word 'config list' dispatches correctly (rc=0)"
else
  expect.fail "single-word 'config list' dispatch failed (rc=$CONFIG_RC)"
fi

# ============================================================================
# FIX VERIFICATION: Dashed parameter names
# this.start should reject dashes with error, not hang or crash
# (dashes are not valid OOSH convention — use dots: peer.compact)
# ============================================================================
test.case $level "dashed names rejected gracefully by this.start" \
  this.start peer-compact

# this.start should return non-zero for dashed names
if [ "$RETURN_VALUE" -ne 0 ]; then
  expect.pass "dashed name 'peer-compact' rejected gracefully (rc=$RETURN_VALUE)"
else
  expect.fail "dashed name 'peer-compact' should be rejected (RETURN_VALUE=$RETURN_VALUE)"
fi

# ============================================================================
# T6: Unknown method returns 127 with error message
# ============================================================================
test.case.expect.error 127 "unknown method returns 127" \
  this.call nonExistentMethod_xyzzy
if [ "$RETURN_VALUE" -eq 127 ]; then
  expect.pass "unknown method 'nonExistentMethod_xyzzy' returns 127"
else
  expect.fail "unknown method should return 127, got $RETURN_VALUE"
fi

# ============================================================================
# T7: Unknown method on wrapper script shows suggestions
# ============================================================================
# Capture stderr+stdout from otmux with a bogus method
UNKNOWN_OUTPUT=$($OOSH_DIR/otmux logg 2>&1)
UNKNOWN_RC=$?
if [ $UNKNOWN_RC -eq 127 ] || [ $UNKNOWN_RC -eq 1 ]; then
  expect.pass "otmux logg returns non-zero exit code ($UNKNOWN_RC)"
else
  expect.fail "otmux logg should return non-zero, got $UNKNOWN_RC"
fi

# ============================================================================
# T8: Stage 3 dynamic loading still works (regression)
# ============================================================================
test.case $level "this.call still loads scripts dynamically (mycmd feature3)" \
  this.call mycmd feature3 hello world

if [ "$RETURN_VALUE" -eq 0 ]; then
  expect.pass "this.call mycmd feature3 still works via this.load"
else
  expect.fail "this.call mycmd feature3 should still work (rc=$RETURN_VALUE)"
fi

# ============================================================================
# T-FN-PARTIAL-* : this.function.partial defines a closured delegate.
# Replaces ad-hoc `source /dev/stdin <<HEREDOC` patterns so promote's
# per-platform check stub generation can be OOSH-idiomatic.
# ============================================================================

# T-FN-PARTIAL-1: missing args → non-zero return
test.case $level "T-FN-PARTIAL-1: missing args returns non-zero" \
  echo "(this.function.partial with no args)"
this.function.partial >/dev/null 2>&1
if [ "$RETURN_VALUE" != "0" ]; then
  expect.pass "missing args → RETURN_VALUE=$RETURN_VALUE"
else
  expect.fail "expected non-zero RETURN_VALUE, got 0"
fi

# T-FN-PARTIAL-2: defined wrapper invokes target with bound first arg + extra
_FN_PARTIAL_TARGET_CAPTURE=""
_fn_partial_target() { _FN_PARTIAL_TARGET_CAPTURE="$*"; }
this.function.partial test.fn.partial.wrap1 _fn_partial_target FIRST >/dev/null 2>&1
test.case $level "T-FN-PARTIAL-2: wrapper passes bound first arg + extras to target" \
  echo "(call test.fn.partial.wrap1 extra1 extra2)"
test.fn.partial.wrap1 extra1 extra2
if [ "$_FN_PARTIAL_TARGET_CAPTURE" = "FIRST extra1 extra2" ]; then
  expect.pass "target received: '$_FN_PARTIAL_TARGET_CAPTURE'"
else
  expect.fail "target received: '$_FN_PARTIAL_TARGET_CAPTURE' (expected 'FIRST extra1 extra2')"
fi

# T-FN-PARTIAL-3: bound first arg is closured by value, not reference
_FN_PARTIAL_TARGET_CAPTURE=""
_FN_PARTIAL_FIRST=ORIGINAL
this.function.partial test.fn.partial.wrap2 _fn_partial_target "$_FN_PARTIAL_FIRST" >/dev/null 2>&1
_FN_PARTIAL_FIRST=REBOUND   # mutate after definition — must NOT affect wrap2
test.case $level "T-FN-PARTIAL-3: bound first arg is closured by value" \
  echo "(rebind first-arg variable, then call wrap2)"
test.fn.partial.wrap2 tail
if [ "$_FN_PARTIAL_TARGET_CAPTURE" = "ORIGINAL tail" ]; then
  expect.pass "closure held ORIGINAL: '$_FN_PARTIAL_TARGET_CAPTURE'"
else
  expect.fail "closure leaked: '$_FN_PARTIAL_TARGET_CAPTURE' (expected 'ORIGINAL tail')"
fi

unset -f _fn_partial_target test.fn.partial.wrap1 test.fn.partial.wrap2
unset _FN_PARTIAL_TARGET_CAPTURE _FN_PARTIAL_FIRST

# ============================================================================
# T-GIT-COUNT-* : this.git.commits.count returns `git rev-list --count` via
# $RESULT. Establishes the this.git.* namespace (shared with
# this.git.branch.short) and absorbs promote's last raw rev-list call sites.
# ============================================================================
TGC_REPO=$(mktemp -d -t oosh-this-git-count.XXXXXX)
git -C "$TGC_REPO" init -q
git -C "$TGC_REPO" config user.email "test@example.com"
git -C "$TGC_REPO" config user.name "test"
echo "seed" > "$TGC_REPO/file"
git -C "$TGC_REPO" add file
git -C "$TGC_REPO" -c commit.gpgsign=false commit -q -m "seed"
git -C "$TGC_REPO" branch -f dev
git -C "$TGC_REPO" branch -f testing
git -C "$TGC_REPO" checkout -q dev

# T-GIT-COUNT-1: identical refs → 0
test.case $level "T-GIT-COUNT-1: identical refs return 0" \
  echo "(this.git.commits.count <repo> dev testing on identical refs)"
this.git.commits.count "$TGC_REPO" dev testing
if [ "$RESULT" = "0" ]; then
  expect.pass "identical refs → RESULT=0"
else
  expect.fail "expected RESULT=0, got '$RESULT'"
fi

# T-GIT-COUNT-2: one extra commit on dev only, counted from testing's POV
echo "dev-only" >> "$TGC_REPO/file"
git -C "$TGC_REPO" -c commit.gpgsign=false commit -aq -m "dev-only"
test.case $level "T-GIT-COUNT-2: dev ahead of testing by 1 commit" \
  echo "(this.git.commits.count <repo> testing dev)"
this.git.commits.count "$TGC_REPO" testing dev
if [ "$RESULT" = "1" ]; then
  expect.pass "RESULT=1 (one commit ahead)"
else
  expect.fail "expected RESULT=1, got '$RESULT'"
fi

# T-GIT-COUNT-3: reverse direction returns 0 (testing has nothing dev lacks)
test.case $level "T-GIT-COUNT-3: reverse direction returns 0" \
  echo "(this.git.commits.count <repo> dev testing)"
this.git.commits.count "$TGC_REPO" dev testing
if [ "$RESULT" = "0" ]; then
  expect.pass "RESULT=0 (testing has nothing dev lacks)"
else
  expect.fail "expected RESULT=0, got '$RESULT'"
fi

# T-GIT-COUNT-4: missing args → non-zero
test.case $level "T-GIT-COUNT-4: missing args returns non-zero" \
  echo "(this.git.commits.count with no args)"
this.git.commits.count >/dev/null 2>&1
if [ "$RETURN_VALUE" != "0" ]; then
  expect.pass "missing args → RETURN_VALUE=$RETURN_VALUE"
else
  expect.fail "expected non-zero RETURN_VALUE, got 0"
fi

# T-GIT-COUNT-5: nonexistent ref → 0 (defensive: refs absent on test rigs)
test.case $level "T-GIT-COUNT-5: nonexistent ref returns 0" \
  echo "(this.git.commits.count <repo> dev nosuch)"
this.git.commits.count "$TGC_REPO" dev nosuch
if [ "$RESULT" = "0" ]; then
  expect.pass "nonexistent ref → RESULT=0"
else
  expect.fail "expected RESULT=0, got '$RESULT'"
fi

rm -rf "$TGC_REPO"
unset TGC_REPO

# ============================================================================
# T-HEREDOC-LOCKDOWN: `source /dev/stdin` lives only inside
# this.function.partial. Regression net for the OOSH-conformance audit —
# any new HEREDOC code-synthesis site outside the documented OOSH helper
# is an anti-pattern.
# ============================================================================
test.case $level "T-HEREDOC-LOCKDOWN: source /dev/stdin only in dev/this (this.function.partial)" \
  echo "(grep across dev/ shell scripts)"
# `cd` into $OOSH_DIR so paths are relative (start with `./`), then anchor
# the directory filter to the start of the relative path. Two earlier
# attempts had portability issues:
#   - `grep -v '/test/'` (substring): false-matched `/home/test/` when the
#     test ran as user `test` inside `os platform.test`.
#   - `grep --exclude-dir=test`: GNU extension, busybox grep (Alpine
#     default) doesn't support it and silently let the test/ directory
#     through the filter.
# Anchoring with `^\./test/` works on both GNU and busybox grep.
_HEREDOC_HITS=$( cd "$OOSH_DIR" && \
  grep -rln 'source /dev/stdin' . 2>/dev/null \
  | grep -vE '^\./(\.git|docs|templates|test|sessions|restore)/' \
  | sort -u )
_HEREDOC_EXPECTED="./this"
if [ "$_HEREDOC_HITS" = "$_HEREDOC_EXPECTED" ]; then
  expect.pass "source /dev/stdin confined to dev/this (this.function.partial)"
else
  # Diagnostic — emit environment so future failures in cross-user platform
  # tests can be diagnosed from the log alone. (Quiet on the passing path.)
  echo "  DIAG: OOSH_DIR=$OOSH_DIR"
  echo "  DIAG: id=$(id 2>/dev/null)"
  echo "  DIAG: ls -laL \$OOSH_DIR | head:"
  ls -laL "$OOSH_DIR" 2>&1 | head -5 | sed 's/^/    /'
  echo "  DIAG: ls -l \$OOSH_DIR/this:"
  ls -l "$OOSH_DIR/this" 2>&1 | sed 's/^/    /'
  echo "  DIAG: grep -c 'source /dev/stdin' \$OOSH_DIR/this = $(grep -c 'source /dev/stdin' "$OOSH_DIR/this" 2>&1)"
  echo "  DIAG: raw grep -rln (no filter):"
  grep -rln 'source /dev/stdin' "$OOSH_DIR" 2>&1 | head -10 | sed 's/^/    /'
  expect.fail "source /dev/stdin found outside this.function.partial: $_HEREDOC_HITS"
fi
unset _HEREDOC_HITS _HEREDOC_EXPECTED

# ============================================================================
# T-SYMLINK-WITH-BACKUP-*: private.this.symlink.with.backup primitive
# ----------------------------------------------------------------------------
# Single canonical "make <linkPath> a symlink to <target>; preserve any
# pre-existing real entry as <linkPath>.orig.<ts>" primitive. Both
# `private.oo.user.shared.symlinks.ensure` (oo, state 31) and
# `config.init.user` (config) delegate to this — see the primitive's
# docstring at this:171 for the full contract.
#
# Four cases × one arg-validation:
#   ABSENT   → link created, no .orig.*
#   CORRECT  → no-op, no .orig.*, return 0
#   STALE    → re-pointed, no .orig.*
#   REAL-DIR → backed up to .orig.<ts>, link created
#   BAD-ARGS → return non-zero, no filesystem change
# ============================================================================
SLB_FIXTURE="/tmp/test.this.symlink.with.backup.$$"
mkdir -p "$SLB_FIXTURE/target.a" "$SLB_FIXTURE/target.b" "$SLB_FIXTURE/home"

test.symlink.with.backup.absent() {
  local link="$SLB_FIXTURE/home/absent"
  rm -rf "$link" "$link.orig."*
  private.this.symlink.with.backup "$link" "$SLB_FIXTURE/target.a" "20260101-000000" >/dev/null 2>&1
  local rc=$?
  if [ "$rc" -eq 0 ] && [ -L "$link" ] \
     && [ "$(readlink "$link")" = "$SLB_FIXTURE/target.a" ] \
     && ! ls "$link.orig."* >/dev/null 2>&1; then
    create.result 0 "absent → linked, no .orig.*"
  else
    create.result 1 "rc=$rc link=$([ -L "$link" ] && echo L || echo X) target=$(readlink "$link" 2>/dev/null)"
  fi
}
test.case $level "T-SYMLINK-WITH-BACKUP-ABSENT: absent path → symlink created" \
  test.symlink.with.backup.absent
expect 0 "*" "primitive creates symlink when no entry exists"

test.symlink.with.backup.correct() {
  local link="$SLB_FIXTURE/home/correct"
  rm -rf "$link" "$link.orig."*
  ln -s "$SLB_FIXTURE/target.a" "$link"
  private.this.symlink.with.backup "$link" "$SLB_FIXTURE/target.a" "20260101-000000" >/dev/null 2>&1
  local rc=$?
  if [ "$rc" -eq 0 ] && [ -L "$link" ] \
     && [ "$(readlink "$link")" = "$SLB_FIXTURE/target.a" ] \
     && ! ls "$link.orig."* >/dev/null 2>&1; then
    create.result 0 "already-correct → no-op, no .orig.*"
  else
    create.result 1 "rc=$rc link=$([ -L "$link" ] && echo L || echo X) target=$(readlink "$link" 2>/dev/null) orig=$(ls "$link.orig."* 2>/dev/null)"
  fi
}
test.case $level "T-SYMLINK-WITH-BACKUP-CORRECT: already-correct → idempotent no-op" \
  test.symlink.with.backup.correct
expect 0 "*" "primitive is idempotent on correct state"

test.symlink.with.backup.stale() {
  local link="$SLB_FIXTURE/home/stale"
  rm -rf "$link" "$link.orig."*
  ln -s "$SLB_FIXTURE/target.b" "$link"
  private.this.symlink.with.backup "$link" "$SLB_FIXTURE/target.a" "20260101-000000" >/dev/null 2>&1
  local rc=$?
  if [ "$rc" -eq 0 ] && [ -L "$link" ] \
     && [ "$(readlink "$link")" = "$SLB_FIXTURE/target.a" ] \
     && ! ls "$link.orig."* >/dev/null 2>&1; then
    create.result 0 "stale → re-pointed, no .orig.*"
  else
    create.result 1 "rc=$rc link=$([ -L "$link" ] && echo L || echo X) target=$(readlink "$link" 2>/dev/null) orig=$(ls "$link.orig."* 2>/dev/null)"
  fi
}
test.case $level "T-SYMLINK-WITH-BACKUP-STALE: stale symlink → re-pointed" \
  test.symlink.with.backup.stale
expect 0 "*" "primitive removes stale symlink and re-creates pointing at <target>"

test.symlink.with.backup.realdir() {
  local link="$SLB_FIXTURE/home/realdir"
  rm -rf "$link" "$link.orig."*
  mkdir -p "$link"
  touch "$link/preserve-me"
  private.this.symlink.with.backup "$link" "$SLB_FIXTURE/target.a" "20260101-000000" >/dev/null 2>&1
  local rc=$?
  if [ "$rc" -eq 0 ] && [ -L "$link" ] \
     && [ "$(readlink "$link")" = "$SLB_FIXTURE/target.a" ] \
     && [ -d "$link.orig.20260101-000000" ] \
     && [ -f "$link.orig.20260101-000000/preserve-me" ]; then
    create.result 0 "real-dir → backed up as .orig.<ts>, link created, contents preserved"
  else
    create.result 1 "rc=$rc link=$([ -L "$link" ] && echo L || echo X) target=$(readlink "$link" 2>/dev/null) backup=$([ -d "$link.orig.20260101-000000" ] && echo Y || echo N) contents=$([ -f "$link.orig.20260101-000000/preserve-me" ] && echo Y || echo N)"
  fi
}
test.case $level "T-SYMLINK-WITH-BACKUP-REALDIR: real-dir → backup + link" \
  test.symlink.with.backup.realdir
expect 0 "*" "primitive preserves real entry as .orig.<ts> with contents intact"

test.symlink.with.backup.badargs() {
  private.this.symlink.with.backup "" "/some/target" "20260101-000000" >/dev/null 2>&1
  local rc=$?
  if [ "$rc" -ne 0 ]; then
    create.result 0 "missing linkPath rejected (rc=$rc)"
  else
    create.result 1 "missing linkPath was accepted — should fail with non-zero"
  fi
}
test.case $level "T-SYMLINK-WITH-BACKUP-BADARGS: missing arg → non-zero, no fs change" \
  test.symlink.with.backup.badargs
expect 0 "*" "primitive validates required args"

rm -rf "$SLB_FIXTURE"
unset SLB_FIXTURE

# ============================================================================
# Test Summary
# ============================================================================

test.suite.save.results
