#!/usr/bin/env bash
#clear
#export PS4='\e[90m+${LINENO} in ${#BASH_SOURCE[@]}>${FUNCNAME[0]}:${BASH_SOURCE[@]##*/} \e[0m'
#set -x

TEST_CATEGORY=core

level=$1
if [ -z "$level" ]; then
  level=1
else
  # remove the level parameter
  shift
fi
info.log "starting: ${BASH_SOURCE[@]##*/} <LOG_LEVEL=$level>"

#echo "sourcing init"
source this
source test.suite

log.level $level

completionArray=(once config list file ite)
source oo

source $OOSH_DIR/promote

# Save existing PROMOTE machine (if any) so we can restore it after tests
_PROMOTE_BACKUP=""
_PROMOTE_ENV_BACKUP=""
_CURRENT_MACHINE_BACKUP=""
if [ -f "$CONFIG_PATH/stateMachines/PROMOTE.states.env" ]; then
  _PROMOTE_BACKUP=$(cat "$CONFIG_PATH/stateMachines/PROMOTE.states.env")
fi
if [ -f "$CONFIG_PATH/stateMachines/PROMOTE.promote.env" ]; then
  _PROMOTE_ENV_BACKUP=$(cat "$CONFIG_PATH/stateMachines/PROMOTE.promote.env")
fi
if [ -f "$CONFIG_PATH/current.state.machine.env" ]; then
  _CURRENT_MACHINE_BACKUP=$(cat "$CONFIG_PATH/current.state.machine.env")
fi

# ============================================================================
# T-PROMOTE-1: promote.dev.to.testing function exists
# ============================================================================
test.case $level "promote.dev.to.testing function is defined" \
  type promote.dev.to.testing
if type promote.dev.to.testing >/dev/null 2>&1; then
  expect.pass "promote.dev.to.testing is defined"
else
  expect.fail "promote.dev.to.testing should be defined"
fi

# ============================================================================
# T-PROMOTE-2: promote.testing.to.prod function exists
# ============================================================================
test.case $level "promote.testing.to.prod function is defined" \
  type promote.testing.to.prod
if type promote.testing.to.prod >/dev/null 2>&1; then
  expect.pass "promote.testing.to.prod is defined"
else
  expect.fail "promote.testing.to.prod should be defined"
fi

# ============================================================================
# T-PROMOTE-3: promote.status function exists
# ============================================================================
test.case $level "promote.status function is defined" \
  type promote.status
if type promote.status >/dev/null 2>&1; then
  expect.pass "promote.status is defined"
else
  expect.fail "promote.status should be defined"
fi

# ============================================================================
# T-PROMOTE-4: promote.report function exists
# ============================================================================
test.case $level "promote.report function is defined" \
  type promote.report
if type promote.report >/dev/null 2>&1; then
  expect.pass "promote.report is defined"
else
  expect.fail "promote.report should be defined"
fi

# ============================================================================
# T-PROMOTE-5: private.promote.config.save / load round-trip
# ============================================================================
PROMOTE_TARGET=testing
PROMOTE_FORCE=yes
private.promote.config.save

PROMOTE_TARGET=""
PROMOTE_FORCE=""
private.promote.config.load

test.case $level "config round-trip preserves PROMOTE_TARGET" \
  echo "$PROMOTE_TARGET"
if [ "$PROMOTE_TARGET" = "testing" ]; then
  expect.pass "PROMOTE_TARGET=testing preserved"
else
  expect.fail "expected PROMOTE_TARGET=testing, got: $PROMOTE_TARGET"
fi

test.case $level "config round-trip preserves PROMOTE_FORCE" \
  echo "$PROMOTE_FORCE"
if [ "$PROMOTE_FORCE" = "yes" ]; then
  expect.pass "PROMOTE_FORCE=yes preserved"
else
  expect.fail "expected PROMOTE_FORCE=yes, got: $PROMOTE_FORCE"
fi

# Clean up config
PROMOTE_TARGET=""
PROMOTE_FORCE=""
private.promote.config.save

# ============================================================================
# T-PROMOTE-6: private.check.initialization.checked fails without PROMOTE_TARGET
# ============================================================================
PROMOTE_TARGET=""
private.promote.config.save

test.case $level "check.initialization.checked fails without target" \
  private.check.initialization.checked promote initialization.checked 11
if [ "$RETURN_VALUE" -ne 0 ]; then
  expect.pass "fails when PROMOTE_TARGET empty"
else
  expect.fail "should fail when PROMOTE_TARGET empty"
fi

# ============================================================================
# T-PROMOTE-7: private.check.target.checked returns 13 for testing
# ============================================================================
PROMOTE_TARGET=testing
private.promote.config.save

test.case $level "check.target.checked returns 13 for testing" \
  private.check.target.checked promote target.checked 12
if [ "$RESULT" = "13" ]; then
  expect.pass "returns 13 (testing path)"
else
  expect.fail "expected RESULT=13, got: $RESULT"
fi

# ============================================================================
# T-PROMOTE-8: private.check.target.checked returns 20 for prod
# ============================================================================
PROMOTE_TARGET=prod
private.promote.config.save

test.case $level "check.target.checked returns 20 for prod" \
  private.check.target.checked promote target.checked 12
if [ "$RESULT" = "20" ]; then
  expect.pass "returns 20 (prod path — pass-through to platform tests)"
else
  expect.fail "expected RESULT=20, got: $RESULT"
fi

# ============================================================================
# T-PROMOTE-9: private.check.uncommitted.checked passes on clean tree
# ============================================================================
test.case $level "check.uncommitted.checked passes on clean tree" \
  private.check.uncommitted.checked promote uncommitted.checked 13
# This may pass or fail depending on working tree state
if [ "$RETURN_VALUE" -eq 0 ]; then
  expect.pass "clean tree detected"
else
  expect.pass "dirty tree detected (expected in dev)"
fi

# ============================================================================
# T-PROMOTE-10: All required private.check.* promote functions are defined
# (Includes per-platform check functions generated dynamically by
#  private.promote.state.machine.init for each must-pass platform.)
# ============================================================================
# Pre-clean any existing PROMOTE machine so init's state.add calls aren't
# blocked by private.check.setup (which errors when state > 2). Safe — the
# backup at top of file is restored at end of suite. state.machine.delete
# also clears any stale current.state.machine.env reference itself
# (state:1031-1048) — no manual rm needed.
source state
state.machine.exists PROMOTE 2>/dev/null && state.machine.delete PROMOTE >/dev/null 2>&1

# Init the machine so the dynamic per-platform check functions exist.
private.promote.state.machine.init >/dev/null 2>&1

_allChecksDefined=true
_baseChecks=(
  initialization.checked target.checked uncommitted.checked
  test.suite.passed confirmation.received
  merged.to.testing testing.tagged testing.pushed
  uncommitted.checked.prod test.suite.passed.prod
  platform.test.dispatch
  confirmation.received.prod merged.to.prod
  prod.tagged prod.pushed finished
)
for _checkName in "${_baseChecks[@]}"; do
  if ! type "private.check.$_checkName" >/dev/null 2>&1; then
    expect.fail "private.check.$_checkName not defined"
    _allChecksDefined=false
  fi
done

# Per-platform stubs — must-pass platforms only
source $OOSH_DIR/os 2>/dev/null
private.os.platform.load 2>/dev/null
_mustPassPlatforms=()
for _p in $(private.os.platform.names); do
  private.os.platform.parse "$_p"
  [ "$PLATFORM_TIER" = "must-pass" ] && _mustPassPlatforms+=("$_p")
done
for _p in "${_mustPassPlatforms[@]}"; do
  if ! type "private.check.platform.test.$_p" >/dev/null 2>&1; then
    expect.fail "private.check.platform.test.$_p not defined (must-pass platform)"
    _allChecksDefined=false
  fi
done

if [ "$_allChecksDefined" = true ]; then
  expect.pass "all required private.check.* functions defined (${#_baseChecks[@]} base + ${#_mustPassPlatforms[@]} per-platform)"
fi
unset _allChecksDefined _checkName _baseChecks _mustPassPlatforms _p

# ============================================================================
# T-PROMOTE-11: Completion functions produce output
# ============================================================================
_testingCompletion=$(promote.dev.to.testing.completion.reset 2>/dev/null)
test.case $level "testing completion produces output" \
  echo "$_testingCompletion"
if echo "$_testingCompletion" | grep -q "reset"; then
  expect.pass "testing completion includes reset"
else
  expect.fail "testing completion should include reset"
fi
unset _testingCompletion

_prodCompletion=$(promote.testing.to.prod.completion.reset 2>/dev/null)
test.case $level "prod completion produces output" \
  echo "$_prodCompletion"
if echo "$_prodCompletion" | grep -q "reset"; then
  expect.pass "prod completion includes reset"
else
  expect.fail "prod completion should include reset"
fi
unset _prodCompletion

# ============================================================================
# T-PROMOTE-12: private.promote.state.machine.init creates machine
# ============================================================================
source state
# Clean up any existing machine from previous runs. state.machine.delete
# also clears stale current.state.machine.env refs itself (state:1031-1048).
state.machine.exists PROMOTE 2>/dev/null && state.machine.delete PROMOTE >/dev/null 2>&1

private.promote.state.machine.init

test.case $level "PROMOTE state machine created" \
  state.machine.exists PROMOTE
if state.machine.exists PROMOTE 2>/dev/null; then
  expect.pass "PROMOTE machine exists"
else
  expect.fail "PROMOTE machine should exist after init"
fi

# ============================================================================
# T-PROMOTE-13: promote.status runs without error
# ============================================================================
test.case $level "promote.status runs without crash" \
  promote.status
expect 0 "*" "promote.status returned 0"

# ============================================================================
# T-PROMOTE-14: promote.report runs without error
# ============================================================================
test.case $level "promote.report runs without crash" \
  promote.report
expect 0 "*" "promote.report returned 0"

# ============================================================================
# T-PROMOTE-15: Failed check keeps state unchanged
# ============================================================================
# Reuse PROMOTE machine from T-PROMOTE-12
# Set state to [11] and verify a failing check does NOT advance state
state of PROMOTE
state.set - 11
source $CONFIG_PATH/current.state.machine.env

PROMOTE_TARGET="invalid"
private.promote.config.save

# Call check function directly — should fail with invalid target
test.case $level "failed check keeps state at [11]" \
  private.check.target.checked promote target.checked 12

# Check function failed — state.set was NOT called, so state stays at [11]
state of PROMOTE
source $CONFIG_PATH/current.state.machine.env
if [ "$RETURN_VALUE" -ne 0 ] && [ "$state" -eq 11 ]; then
  expect.pass "check failed (rc=$RETURN_VALUE), state unchanged at [11]"
else
  expect.fail "expected check failure + state=11, got: rc=$RETURN_VALUE state=$state"
fi

# ============================================================================
# T-PROMOTE-16: Re-run after fix advances state
# ============================================================================
# State still at [11] — fix the target and call the same check again
PROMOTE_TARGET=testing
private.promote.config.save

test.case $level "re-run after fix passes and returns branch state" \
  private.check.target.checked promote target.checked 12

if [ "$RETURN_VALUE" -eq 0 ] && [ "$RESULT" = "13" ]; then
  # Simulate what state.check does on success: set state to RESULT
  state.set - "$RESULT"
  source $CONFIG_PATH/current.state.machine.env
  expect.pass "check passed (RESULT=13), state advanced to [$state]"
else
  expect.fail "expected rc=0 RESULT=13, got: rc=$RETURN_VALUE RESULT=$RESULT"
fi

# ============================================================================
# T-PROMOTE-17: Skip advances past pending check
# ============================================================================
# state.set bypasses the check function entirely
state of PROMOTE
state.set - 13
source $CONFIG_PATH/current.state.machine.env

# Skip: set state directly to [14] without running check
state.set - 14
source $CONFIG_PATH/current.state.machine.env

test.case $level "skip advances past [14] without running check" \
  echo "state=$state"
if [ "$state" -eq 14 ]; then
  expect.pass "state at [14] via direct set (skip)"
else
  expect.fail "expected state=14, got: $state"
fi

# ============================================================================
# T-PROMOTE-18: Skip with transition state follows through
# ============================================================================
state of PROMOTE
state.set - 18
source $CONFIG_PATH/current.state.machine.env

# Set state to [19] which is transition state (19=99)
state.set - 19
source $CONFIG_PATH/current.state.machine.env

test.case $level "skip to transition state [19] follows to [99]" \
  echo "state=$state"
if [ "$state" -eq 99 ]; then
  expect.pass "state followed transition to [99]"
else
  expect.fail "expected state=99, got: $state"
fi

# Clean up test state machine
if state.machine.exists PROMOTE 2>/dev/null; then
  state.machine.delete PROMOTE 2>/dev/null
fi
# Clean up config and stale env references
rm -f "$CONFIG_PATH/stateMachines/PROMOTE.promote.env" 2>/dev/null
if grep -q "machine=PROMOTE" "$CONFIG_PATH/current.state.machine.env" 2>/dev/null; then
  rm -f "$CONFIG_PATH/current.state.machine.env" 2>/dev/null
fi

# ============================================================================
# T-PROMOTE-19: Full init creates valid machine with all 17 custom states
# ============================================================================
source state
# Clean up any existing machine
if state.machine.exists PROMOTE 2>/dev/null; then
  state.machine.delete PROMOTE 2>/dev/null
fi
if grep -q "machine=PROMOTE" "$CONFIG_PATH/current.state.machine.env" 2>/dev/null; then
  rm -f "$CONFIG_PATH/current.state.machine.env" 2>/dev/null
fi

private.promote.state.machine.init

test.case $level "Full init creates all custom states" \
  state of PROMOTE list all
source $CONFIG_PATH/current.state.machine.env

# Build the expected state map. Testing path is fixed [11]-[19]. Prod path is
# dynamic — driven by must-pass platforms from defaults/platforms.env, so the
# assertions below mirror the same enumeration the init function uses.
_allStatesOk=true
declare -A _expectedStates=(
  [11]="initialization.checked"
  [12]="target.checked"
  [13]="uncommitted.checked"
  [14]="test.suite.passed"
  [15]="confirmation.received"
  [16]="merged.to.testing"
  [17]="testing.tagged"
  [18]="testing.pushed"
  [19]="99"
  [20]="prod.path.started"
  [21]="uncommitted.checked.prod"
  [22]="test.suite.passed.prod"
  [99]="finished"
)

# Append per-must-pass-platform states, then the closing prod-path states.
source $OOSH_DIR/os 2>/dev/null
private.os.platform.load 2>/dev/null
_nextId=23
for _p in $(private.os.platform.names); do
  private.os.platform.parse "$_p"
  [ "$PLATFORM_TIER" = "must-pass" ] || continue
  _expectedStates[$_nextId]="platform.test.$_p"
  _nextId=$((_nextId + 1))
done
_expectedStates[$_nextId]="confirmation.received.prod";  _nextId=$((_nextId + 1))
_expectedStates[$_nextId]="merged.to.prod";              _nextId=$((_nextId + 1))
_expectedStates[$_nextId]="prod.tagged";                 _nextId=$((_nextId + 1))
_expectedStates[$_nextId]="prod.pushed";                 _nextId=$((_nextId + 1))
_expectedStates[$_nextId]="99"

for _id in "${!_expectedStates[@]}"; do
  _sName="PROMOTE_STATES[${_id}]"
  _sValue="${!_sName}"
  if [ "$_sValue" != "${_expectedStates[$_id]}" ]; then
    expect.fail "State [$_id]: expected '${_expectedStates[$_id]}', got '$_sValue'"
    _allStatesOk=false
  fi
done

if [ "$_allStatesOk" = true ]; then
  expect.pass "All ${#_expectedStates[@]} states verified (data-driven prod path)"
fi
unset _allStatesOk _expectedStates _id _sName _sValue _nextId _p

# Clean up test machine
if state.machine.exists PROMOTE 2>/dev/null; then
  state.machine.delete PROMOTE 2>/dev/null
fi
rm -f "$CONFIG_PATH/stateMachines/PROMOTE.promote.env" 2>/dev/null
if grep -q "machine=PROMOTE" "$CONFIG_PATH/current.state.machine.env" 2>/dev/null; then
  rm -f "$CONFIG_PATH/current.state.machine.env" 2>/dev/null
fi

# Restore saved PROMOTE machine
if [ -n "$_PROMOTE_BACKUP" ]; then
  echo "$_PROMOTE_BACKUP" > "$CONFIG_PATH/stateMachines/PROMOTE.states.env"
fi
if [ -n "$_PROMOTE_ENV_BACKUP" ]; then
  echo "$_PROMOTE_ENV_BACKUP" > "$CONFIG_PATH/stateMachines/PROMOTE.promote.env"
fi
# Restore the current machine cache to what it was before promote tests
if [ -n "$_CURRENT_MACHINE_BACKUP" ]; then
  echo "$_CURRENT_MACHINE_BACKUP" > "$CONFIG_PATH/current.state.machine.env"
fi

### test.method

# ─────────────────────────────────────────────────────────────────────────────
# T-PROMOTE-SELF-BRANCH-* : private.promote.rewrite.self.branch rewrites the
# OOSH_SELF_BRANCH default in init/oosh to match the checked-out branch and
# commits the change. Branch-agnostic — seed a temp repo on branch "testing"
# and assert the rewrite picks that up automatically.
# ─────────────────────────────────────────────────────────────────────────────
source $OOSH_DIR/promote 2>/dev/null
PSB_TMP=$(mktemp -d)
git -C "$PSB_TMP" init -q -b testing
mkdir -p "$PSB_TMP/init"
printf '#!/bin/sh\nOOSH_SELF_BRANCH="${OOSH_SELF_BRANCH:-dev}"\n' > "$PSB_TMP/init/oosh"
printf '#!/usr/bin/env bash\nOOSH_SELF_BRANCH="${OOSH_SELF_BRANCH:-dev}"\n' > "$PSB_TMP/Install oosh.command"
git -C "$PSB_TMP" add init/oosh "Install oosh.command"
git -C "$PSB_TMP" -c user.email=t@t -c user.name=t commit -q -m seed

test.case $level "T-PROMOTE-SELF-BRANCH-REWRITE: rewrites init/oosh default to current branch" \
  echo "(grep below)"
private.promote.rewrite.self.branch "$PSB_TMP" 2>/dev/null
if grep -q 'OOSH_SELF_BRANCH:-testing' "$PSB_TMP/init/oosh"; then
  expect.pass "init/oosh default rewritten to testing (current branch)"
else
  expect.fail "init/oosh default not rewritten — got: $(grep OOSH_SELF_BRANCH "$PSB_TMP/init/oosh")"
fi

test.case $level "T-PROMOTE-SELF-BRANCH-REWRITE-COMMAND: rewrites Install oosh.command default too" \
  echo "(grep below)"
if grep -q 'OOSH_SELF_BRANCH:-testing' "$PSB_TMP/Install oosh.command"; then
  expect.pass "Install oosh.command default rewritten to testing"
else
  expect.fail "Install oosh.command default not rewritten — got: $(grep OOSH_SELF_BRANCH "$PSB_TMP/Install oosh.command")"
fi

test.case $level "T-PROMOTE-SELF-BRANCH-COMMIT: creates a follow-up commit" \
  echo "(grep below)"
if git -C "$PSB_TMP" log --oneline -2 | grep -q 'chore(promote): set OOSH_SELF_BRANCH default to testing'; then
  expect.pass "commit created with expected message"
else
  expect.fail "no promote-rewrite commit found"
fi

test.case $level "T-PROMOTE-SELF-BRANCH-IDEMPOTENT: second call is a no-op" \
  echo "(grep below)"
_shaBefore=$(git -C "$PSB_TMP" rev-parse HEAD)
private.promote.rewrite.self.branch "$PSB_TMP" 2>/dev/null
_shaAfter=$(git -C "$PSB_TMP" rev-parse HEAD)
if [ "$_shaBefore" = "$_shaAfter" ]; then
  expect.pass "no new commit when default already matches"
else
  expect.fail "extra commit created despite no change"
fi

rm -rf "$PSB_TMP"

# ─────────────────────────────────────────────────────────────────────────────
# T-PROMOTE-SOURCE-PUSH-* : private.promote.push.source.branch pushes the
# source branch to origin BEFORE the merge, so origin/<source> contains every
# commit that the subsequent origin/<destination> push will reference. Fails
# fast on push rejection (e.g. non-fast-forward) before destructive merge
# work begins.
# ─────────────────────────────────────────────────────────────────────────────

# T-PROMOTE-SOURCE-PUSH-1: no 'origin' remote → returns 0 (test rigs)
PSP_NO_REMOTE=$(mktemp -d)
git -C "$PSP_NO_REMOTE" init -q -b dev
printf 'seed\n' > "$PSP_NO_REMOTE/file"
git -C "$PSP_NO_REMOTE" add file
git -C "$PSP_NO_REMOTE" -c user.email=t@t -c user.name=t commit -q -m seed

test.case $level "T-PROMOTE-SOURCE-PUSH-1: returns 0 when no 'origin' remote configured" \
  echo "(call helper)"
private.promote.push.source.branch "$PSP_NO_REMOTE" "dev"
_rc=$?
if [ "$_rc" -eq 0 ]; then
  expect.pass "helper returned 0 (no-remote skip)"
else
  expect.fail "helper returned $_rc (expected 0 for missing remote)"
fi
rm -rf "$PSP_NO_REMOTE"

# T-PROMOTE-SOURCE-PUSH-2: pushes source branch to bare-repo origin
PSP_BARE=$(mktemp -d)/origin.git
PSP_WORK=$(mktemp -d)
git init -q --bare -b dev "$PSP_BARE"
git -C "$PSP_WORK" init -q -b dev
git -C "$PSP_WORK" remote add origin "$PSP_BARE"
printf 'seed\n' > "$PSP_WORK/file"
git -C "$PSP_WORK" add file
git -C "$PSP_WORK" -c user.email=t@t -c user.name=t commit -q -m seed
git -C "$PSP_WORK" push -q origin dev
printf 'new\n' >> "$PSP_WORK/file"
git -C "$PSP_WORK" add file
git -C "$PSP_WORK" -c user.email=t@t -c user.name=t commit -q -m "new commit"

test.case $level "T-PROMOTE-SOURCE-PUSH-2: pushes new commit to origin" \
  echo "(call helper)"
private.promote.push.source.branch "$PSP_WORK" "dev"
_rc=$?
_localSha=$(git -C "$PSP_WORK" rev-parse dev)
_originSha=$(git -C "$PSP_BARE" rev-parse dev 2>/dev/null)
if [ "$_rc" -eq 0 ] && [ "$_localSha" = "$_originSha" ]; then
  expect.pass "push succeeded; origin/dev matches local dev"
else
  expect.fail "push failed (rc=$_rc) or origin/dev != local (local=$_localSha origin=$_originSha)"
fi
rm -rf "$PSP_WORK" "$(dirname "$PSP_BARE")"

# T-PROMOTE-SOURCE-PUSH-3: returns non-zero on rejected non-fast-forward push
PSP_BARE2=$(mktemp -d)/origin.git
PSP_WORK2=$(mktemp -d)
git init -q --bare -b dev "$PSP_BARE2"
git -C "$PSP_WORK2" init -q -b dev
git -C "$PSP_WORK2" remote add origin "$PSP_BARE2"
printf 'seed\n' > "$PSP_WORK2/file"
git -C "$PSP_WORK2" add file
git -C "$PSP_WORK2" -c user.email=t@t -c user.name=t commit -q -m seed
git -C "$PSP_WORK2" push -q origin dev
# Origin advances independently (simulates someone else pushing)
PSP_WORK2_OTHER=$(mktemp -d)
git -C "$PSP_WORK2_OTHER" clone -q "$PSP_BARE2" .
printf 'their work\n' >> "$PSP_WORK2_OTHER/file"
git -C "$PSP_WORK2_OTHER" add file
git -C "$PSP_WORK2_OTHER" -c user.email=t@t -c user.name=t commit -q -m "remote ahead"
git -C "$PSP_WORK2_OTHER" push -q origin dev
rm -rf "$PSP_WORK2_OTHER"
# Local makes a divergent commit
printf 'mine\n' >> "$PSP_WORK2/file"
git -C "$PSP_WORK2" add file
git -C "$PSP_WORK2" -c user.email=t@t -c user.name=t commit -q -m "local diverges"

test.case $level "T-PROMOTE-SOURCE-PUSH-3: returns non-zero on non-fast-forward push" \
  echo "(call helper)"
private.promote.push.source.branch "$PSP_WORK2" "dev"
_rc=$?
if [ "$_rc" -ne 0 ]; then
  expect.pass "helper returned $_rc (rejected as expected)"
else
  expect.fail "helper returned 0 — should have rejected non-fast-forward"
fi
unset _rc _localSha _originSha
rm -rf "$PSP_WORK2" "$(dirname "$PSP_BARE2")"

# ─────────────────────────────────────────────────────────────────────────────
# T-PROMOTE-AUTO-RESOLVE-* : private.promote.try.resolve.self.branch.conflict
# auto-resolves dev→testing (and testing→prod) merge conflicts whose only
# conflicted paths are init/oosh and "Install oosh.command". Background: every
# successful promote commits an OOSH_SELF_BRANCH rewrite onto the destination
# branch, creating a permanent 1-line divergence that becomes a merge conflict
# the moment source-branch refactors touch lines near it.
# ─────────────────────────────────────────────────────────────────────────────

# Helper: seed a temp repo whose dev and testing branches both modify the
# OOSH_SELF_BRANCH default line in init/oosh + Install oosh.command, so that
# `git merge dev` from testing produces a conflict in exactly those two paths.
# Optionally adds a foreign divergent file when $1 is "with-foreign".
_seed_ar_repo() {
  local mode="$1"
  local d
  d=$(mktemp -d)
  git -C "$d" init -q -b testing
  git -C "$d" -c user.email=t@t -c user.name=t config commit.gpgsign false
  mkdir -p "$d/init"
  # Ancestor has a neutral OOSH_SELF_BRANCH value so both branches diverge from it.
  printf '#!/bin/sh\n# branch default\nOOSH_SELF_BRANCH="${OOSH_SELF_BRANCH:-main}"\n' > "$d/init/oosh"
  printf '#!/usr/bin/env bash\n# branch default\nOOSH_SELF_BRANCH="${OOSH_SELF_BRANCH:-main}"\n' > "$d/Install oosh.command"
  if [ "$mode" = "with-foreign" ]; then
    printf 'common base\n' > "$d/OTHER.md"
    git -C "$d" add init/oosh "Install oosh.command" OTHER.md
  else
    git -C "$d" add init/oosh "Install oosh.command"
  fi
  git -C "$d" -c user.email=t@t -c user.name=t commit -q -m "ancestor"
  git -C "$d" branch dev

  # testing side: rewrite OOSH_SELF_BRANCH to "testing" (simulates a prior promote rewrite commit)
  printf '#!/bin/sh\n# branch default\nOOSH_SELF_BRANCH="${OOSH_SELF_BRANCH:-testing}"\n' > "$d/init/oosh"
  printf '#!/usr/bin/env bash\n# branch default\nOOSH_SELF_BRANCH="${OOSH_SELF_BRANCH:-testing}"\n' > "$d/Install oosh.command"
  if [ "$mode" = "with-foreign" ]; then
    printf 'testing side\n' > "$d/OTHER.md"
    git -C "$d" add init/oosh "Install oosh.command" OTHER.md
  else
    git -C "$d" add init/oosh "Install oosh.command"
  fi
  git -C "$d" -c user.email=t@t -c user.name=t commit -q -m "testing: prior promote rewrite"

  # dev side: keep OOSH_SELF_BRANCH at "dev" but reshape the same line (simulates a refactor that touches the same hunk)
  git -C "$d" checkout -q dev
  printf '#!/bin/sh\n# branch default — overhauled\nOOSH_SELF_BRANCH="${OOSH_SELF_BRANCH:-dev}"\n' > "$d/init/oosh"
  printf '#!/usr/bin/env bash\n# branch default — overhauled\nOOSH_SELF_BRANCH="${OOSH_SELF_BRANCH:-dev}"\n' > "$d/Install oosh.command"
  if [ "$mode" = "with-foreign" ]; then
    printf 'dev side\n' > "$d/OTHER.md"
    git -C "$d" add init/oosh "Install oosh.command" OTHER.md
  else
    git -C "$d" add init/oosh "Install oosh.command"
  fi
  git -C "$d" -c user.email=t@t -c user.name=t commit -q -m "dev: refactor"

  # Back to testing for the merge attempt
  git -C "$d" checkout -q testing

  echo "$d"
}

# T-PROMOTE-AUTO-RESOLVE-1: known-files conflict gets auto-resolved
PSB_AR=$(_seed_ar_repo)
# -c user.email/name: matches the commit invocations in _seed_ar_repo. Without
# inline creds, `git merge` aborts before producing the conflict in
# environments lacking global git user config (alma containers, fresh CIs).
git -C "$PSB_AR" -c user.email=t@t -c user.name=t merge dev --no-edit >/dev/null 2>&1
_mergeRc=$?

test.case $level "T-PROMOTE-AUTO-RESOLVE-1: setup produces a real merge conflict" \
  echo "(merge rc: $_mergeRc)"
if [ "$_mergeRc" -ne 0 ]; then
  expect.pass "git merge dev failed as expected (conflict)"
else
  expect.fail "expected merge conflict but merge succeeded"
fi

test.case $level "T-PROMOTE-AUTO-RESOLVE-2: helper resolves known-files conflict and returns 0" \
  echo "(call helper)"
private.promote.try.resolve.self.branch.conflict "$PSB_AR" "dev"
_helperRc=$?
if [ "$_helperRc" -eq 0 ]; then
  expect.pass "helper returned 0 (resolved)"
else
  expect.fail "helper returned $_helperRc (expected 0)"
fi

test.case $level "T-PROMOTE-AUTO-RESOLVE-3: working tree is clean after auto-resolve" \
  echo "(status check)"
if [ -z "$(git -C "$PSB_AR" status --porcelain 2>/dev/null)" ]; then
  expect.pass "working tree clean"
else
  expect.fail "working tree dirty: $(git -C "$PSB_AR" status --porcelain 2>/dev/null)"
fi

test.case $level "T-PROMOTE-AUTO-RESOLVE-4: init/oosh takes dev's value (theirs)" \
  echo "(content check)"
if grep -q 'OOSH_SELF_BRANCH:-dev' "$PSB_AR/init/oosh"; then
  expect.pass "init/oosh got dev's OOSH_SELF_BRANCH value"
else
  expect.fail "init/oosh did not take theirs — content: $(grep OOSH_SELF_BRANCH "$PSB_AR/init/oosh")"
fi

test.case $level "T-PROMOTE-AUTO-RESOLVE-5: Install oosh.command takes dev's value (theirs)" \
  echo "(content check)"
if grep -q 'OOSH_SELF_BRANCH:-dev' "$PSB_AR/Install oosh.command"; then
  expect.pass "Install oosh.command got dev's OOSH_SELF_BRANCH value"
else
  expect.fail "Install oosh.command did not take theirs — content: $(grep OOSH_SELF_BRANCH "$PSB_AR/Install oosh.command")"
fi

test.case $level "T-PROMOTE-AUTO-RESOLVE-6: auto-resolve merge commit exists with expected subject" \
  echo "(log check)"
if git -C "$PSB_AR" log --oneline -1 | grep -q "auto-resolved OOSH_SELF_BRANCH drift"; then
  expect.pass "merge commit has auto-resolve marker"
else
  expect.fail "no auto-resolve commit found — last commit: $(git -C "$PSB_AR" log --oneline -1)"
fi

test.case $level "T-PROMOTE-AUTO-RESOLVE-7: rewrite.self.branch restores destination value after auto-resolve" \
  private.promote.rewrite.self.branch "$PSB_AR"
if grep -q 'OOSH_SELF_BRANCH:-testing' "$PSB_AR/init/oosh"; then
  expect.pass "init/oosh value restored to 'testing' after rewrite"
else
  expect.fail "rewrite did not restore destination value — content: $(grep OOSH_SELF_BRANCH "$PSB_AR/init/oosh")"
fi

rm -rf "$PSB_AR"

# T-PROMOTE-AUTO-RESOLVE-FOREIGN: foreign-file conflict refuses to auto-resolve
PSB_AR_F=$(_seed_ar_repo with-foreign)
# See merge-creds note on PSB_AR above — same rationale.
git -C "$PSB_AR_F" -c user.email=t@t -c user.name=t merge dev --no-edit >/dev/null 2>&1
_mergeRc=$?

test.case $level "T-PROMOTE-AUTO-RESOLVE-FOREIGN-1: setup produces multi-file merge conflict" \
  echo "(merge rc: $_mergeRc)"
if [ "$_mergeRc" -ne 0 ] && [ -n "$(git -C "$PSB_AR_F" diff --name-only --diff-filter=U 2>/dev/null | grep OTHER.md)" ]; then
  expect.pass "merge conflict includes foreign file"
else
  expect.fail "expected conflict including OTHER.md"
fi

test.case $level "T-PROMOTE-AUTO-RESOLVE-FOREIGN-2: helper refuses (returns non-zero) when foreign file conflicts" \
  echo "(call helper)"
private.promote.try.resolve.self.branch.conflict "$PSB_AR_F" "dev"
_helperRc=$?
if [ "$_helperRc" -ne 0 ]; then
  expect.pass "helper returned $_helperRc (refused)"
else
  expect.fail "helper auto-resolved a foreign-file conflict (should have refused)"
fi

test.case $level "T-PROMOTE-AUTO-RESOLVE-FOREIGN-3: helper did not create a commit on refusal" \
  echo "(log check)"
if ! git -C "$PSB_AR_F" log --oneline -1 2>/dev/null | grep -q "auto-resolved"; then
  expect.pass "no auto-resolve commit created"
else
  expect.fail "unexpected auto-resolve commit despite foreign conflict"
fi

git -C "$PSB_AR_F" merge --abort 2>/dev/null
rm -rf "$PSB_AR_F"
unset _mergeRc _helperRc PSB_AR PSB_AR_F

# ─────────────────────────────────────────────────────────────────────────────
# T-PROMOTE-PLATFORM-* : per-platform dispatch returns 0/1 cleanly so the
# state machine stops on a failing platform and advances on a passing one.
# We mock the `os` function so test runs are fast and deterministic.
# ─────────────────────────────────────────────────────────────────────────────

# Save the real os definition (it's a script on PATH, not a function — but
# defining a local function shadows the script lookup inside this shell).
_PROMOTE_PLATFORM_TEST_OUTCOME=pass
os() {
  if [ "$1" = "platform.test" ]; then
    [ "$_PROMOTE_PLATFORM_TEST_OUTCOME" = "pass" ] && return 0 || return 1
  fi
  command os "$@"
}

# T-PROMOTE-PLATFORM-1: dispatch returns 0 on pass
_PROMOTE_PLATFORM_TEST_OUTCOME=pass
test.case $level "T-PROMOTE-PLATFORM-1: dispatch returns 0 when os platform.test passes" \
  echo "(call dispatch)"
private.check.platform.test.dispatch ubuntu_24_04 promote platform.test.ubuntu_24_04 0 >/dev/null 2>&1
_rc=$?
if [ "$_rc" -eq 0 ]; then
  expect.pass "dispatch returned 0 on pass"
else
  expect.fail "dispatch returned $_rc on pass (expected 0)"
fi

# T-PROMOTE-PLATFORM-2: dispatch returns non-zero on fail
_PROMOTE_PLATFORM_TEST_OUTCOME=fail
test.case $level "T-PROMOTE-PLATFORM-2: dispatch returns non-zero when os platform.test fails" \
  echo "(call dispatch)"
private.check.platform.test.dispatch ubuntu_24_04 promote platform.test.ubuntu_24_04 0 >/dev/null 2>&1
_rc=$?
if [ "$_rc" -ne 0 ]; then
  expect.pass "dispatch returned $_rc on fail (>0 as expected)"
else
  expect.fail "dispatch returned 0 on fail (expected non-zero so state machine stops)"
fi

# T-PROMOTE-PLATFORM-3: per-platform stub functions exist and delegate to dispatch
test.case $level "T-PROMOTE-PLATFORM-3: per-platform stubs delegate to dispatch" \
  echo "(call platform stub)"
_PROMOTE_PLATFORM_TEST_OUTCOME=pass
_mustPass=$(private.os.platform.names | while read _p; do private.os.platform.parse "$_p"; [ "$PLATFORM_TIER" = "must-pass" ] && echo "$_p"; done | head -1)
if [ -n "$_mustPass" ]; then
  "private.check.platform.test.$_mustPass" promote "platform.test.$_mustPass" 0 >/dev/null 2>&1
  _rc=$?
  if [ "$_rc" -eq 0 ]; then
    expect.pass "platform.test.$_mustPass stub returned 0 via dispatch"
  else
    expect.fail "platform.test.$_mustPass stub returned $_rc (expected 0)"
  fi
else
  expect.fail "no must-pass platforms found — defaults/platforms.env may be empty"
fi

# Restore real os (unset the shadow function)
unset -f os
unset _PROMOTE_PLATFORM_TEST_OUTCOME _rc _mustPass

# ─────────────────────────────────────────────────────────────────────────────
# T-PROMOTE-RESUME-* : structural invariants that make per-platform resume work
#
# When `oo stage testing` runs platform tests and one fails, the user should
# be able to fix the platform and re-run, and the state machine should resume
# from the failing platform (not re-test passed platforms, not skip the
# failed one). Three structural prerequisites make that behaviour possible:
#
#   1. Each must-pass platform has its own STATE in PROMOTE (one state per
#      platform, not one batch state for all). Per-platform granularity is
#      what allows the machine to halt at a single failed platform.
#   2. Each platform has its own check FUNCTION (private.check.platform.test.<name>)
#      generated by private.promote.state.machine.init / platform.stubs.source.
#   3. PROMOTE_PROD_LAST_STATE points to the last platform state, so
#      promote.testing.to.prod's advancement loop has a correct upper bound
#      for "resume is still in the prod-platform range, don't re-init."
#
# Combined with T-PROMOTE-15 ("failed check keeps state unchanged" — generic
# state machine guarantee), T-PROMOTE-PLATFORM-1/2/3 (dispatch returns 0/1
# from `os platform.test`), and the resume guard at promote:113-125, these
# invariants imply the user-visible resume cycle. End-to-end mocking is
# avoided here because state.check at state:288 re-sources `promote` on
# every state advancement (via $stateScript=promote), which would wipe any
# in-shell override of `private.check.platform.test.dispatch`.
# ─────────────────────────────────────────────────────────────────────────────

# Fresh PROMOTE machine on the prod path. state.machine.delete handles the
# stale current.state.machine.env cleanup itself (state:1031-1048); the
# backup at top of file restores the developer's real machine at end of
# suite. Use OOSH idioms throughout: state.machine.exists / .delete,
# private.promote.config.save / .state.machine.init / .platform.stubs.source,
# state.of for loading PROMOTE_STATES + setting current machine context,
# private.promote.config.load for PROMOTE_PROD_LAST_STATE.
source state
state.machine.exists PROMOTE 2>/dev/null && state.machine.delete PROMOTE >/dev/null 2>&1
PROMOTE_TARGET=prod
private.promote.config.save
private.promote.state.machine.init >/dev/null 2>&1
private.promote.platform.stubs.source
state of PROMOTE >/dev/null
private.promote.config.load

# Discover must-pass platforms via the documented OOSH iteration idiom
# (private.os.platform.names + private.os.platform.parse — same pattern as
# T-PROMOTE-PLATFORM-3 and private.promote.platform.stubs.source).
_resumeMustPass=()
for _p in $(private.os.platform.names); do
  private.os.platform.parse "$_p"
  [ "$PLATFORM_TIER" = "must-pass" ] && _resumeMustPass+=("$_p")
done
_resumeCount=${#_resumeMustPass[@]}

# T-PROMOTE-RESUME-1: PROMOTE has one state per must-pass platform.
# Use state.find (the documented OOSH lookup, state:542) — it returns 0 and
# echoes the state id when the name exists, returns 1 otherwise.
test.case $level "T-PROMOTE-RESUME-1: one state per must-pass platform (per-platform granularity)" \
  echo "(state.find PROMOTE platform.test.<name> id, per platform)"
_platformStateCount=0
_missingStates=""
for _p in "${_resumeMustPass[@]}"; do
  if state.find PROMOTE "platform.test.$_p" id >/dev/null 2>&1; then
    _platformStateCount=$((_platformStateCount + 1))
  else
    _missingStates="$_missingStates $_p"
  fi
done
if [ "$_platformStateCount" -eq "$_resumeCount" ] && [ -z "$_missingStates" ]; then
  expect.pass "all $_resumeCount must-pass platforms have their own state (via state.find)"
else
  expect.fail "expected $_resumeCount platform states, found $_platformStateCount; missing:$_missingStates"
fi

# T-PROMOTE-RESUME-2: each platform has its own check stub.
# Use this.functionExists (the documented OOSH function-presence check —
# oosh-architecture.md:356, this:366) instead of `type ... >/dev/null`.
test.case $level "T-PROMOTE-RESUME-2: each platform has its own check function" \
  echo "(this.functionExists private.check.platform.test.<name>, per platform)"
_missingStubs=""
for _p in "${_resumeMustPass[@]}"; do
  if ! this.functionExists "private.check.platform.test.$_p"; then
    _missingStubs="$_missingStubs $_p"
  fi
done
if [ -z "$_missingStubs" ]; then
  expect.pass "all $_resumeCount platforms have private.check.platform.test.<name> defined"
else
  expect.fail "missing per-platform check stubs:$_missingStubs"
fi

# T-PROMOTE-RESUME-3: every platform state is within the resume-guard range
# at promote:119:  [ $state -ge 20 ] && [ $state -le $((PROMOTE_PROD_LAST_STATE + 1)) ]
# Use state.find (capturing the id via subshell) instead of walking
# PROMOTE_STATES directly.
test.case $level "T-PROMOTE-RESUME-3: every platform state is within the resume-guard range" \
  echo "(state.find for each platform; assert highest id ≤ PROMOTE_PROD_LAST_STATE+1)"
_highestPlatformIdx=0
for _p in "${_resumeMustPass[@]}"; do
  _id=$(state.find PROMOTE "platform.test.$_p" id 2>/dev/null)
  if [ -n "$_id" ] && [ "$_id" -gt "$_highestPlatformIdx" ]; then
    _highestPlatformIdx=$_id
  fi
done
if [ "$_highestPlatformIdx" -ge 20 ] && \
   [ "$_highestPlatformIdx" -le $((PROMOTE_PROD_LAST_STATE + 1)) ]; then
  expect.pass "highest platform state [$_highestPlatformIdx] is within resume-guard range [20, $((PROMOTE_PROD_LAST_STATE + 1))]"
else
  expect.fail "highest platform state [$_highestPlatformIdx] outside resume-guard range [20, $((PROMOTE_PROD_LAST_STATE + 1))] — being stuck at the last platform would trigger a re-init"
fi

unset _resumeMustPass _resumeCount _platformStateCount _missingStates _missingStubs
unset _highestPlatformIdx _id _p

# Restore the developer's PROMOTE machine again. The original restore at
# the top of file (lines 442-460) ran *before* the RESUME tests above,
# which delete-and-recreate PROMOTE. Without this second restore, the
# suite leaves PROMOTE in a freshly-initialised state (state ≤ 4 +
# PROMOTE_PROD_LAST_STATE unset in PROMOTE.promote.env). A subsequent
# `oo stage testing` then hits the resume guard at promote:119 with state
# out of [20, _prodMax+1] range and re-initialises — silently losing any
# in-progress prod-path promotion. Same backup vars (set at file top).
state.machine.exists PROMOTE 2>/dev/null && state.machine.delete PROMOTE >/dev/null 2>&1
if [ -n "$_PROMOTE_BACKUP" ]; then
  echo "$_PROMOTE_BACKUP" > "$CONFIG_PATH/stateMachines/PROMOTE.states.env"
fi
if [ -n "$_PROMOTE_ENV_BACKUP" ]; then
  echo "$_PROMOTE_ENV_BACKUP" > "$CONFIG_PATH/stateMachines/PROMOTE.promote.env"
fi
if [ -n "$_CURRENT_MACHINE_BACKUP" ]; then
  echo "$_CURRENT_MACHINE_BACKUP" > "$CONFIG_PATH/current.state.machine.env"
fi

# ============================================================================
# T-PROMOTE-BRANCH-ALIGN: promote.branch.alignment symmetric verdict
# ----------------------------------------------------------------------------
# Fixes the bug where `promote status` reported "up to date with dev" even
# though testing was strictly *ahead* of dev — the prior one-sided rev-list
# could not see commits the comparison branch had that the reference branch
# lacked. The new OOSH method must report all four states: equal, ahead,
# behind, diverged.
# ============================================================================

# Build a tiny throwaway repo with dev/testing/prod refs we control.
PBA_REPO=$(mktemp -d -t oosh-promote-align.XXXXXX)
git -C "$PBA_REPO" init -q
git -C "$PBA_REPO" config user.email "test@example.com"
git -C "$PBA_REPO" config user.name "test"
echo "seed" > "$PBA_REPO/file"
git -C "$PBA_REPO" add file
git -C "$PBA_REPO" -c commit.gpgsign=false commit -q -m "seed"
git -C "$PBA_REPO" branch -f dev
git -C "$PBA_REPO" branch -f testing
git -C "$PBA_REPO" branch -f prod

# Save / override OOSH_DIR so promote.branch.alignment operates on the fixture.
_PBA_OOSH_DIR_SAVE="$OOSH_DIR"
OOSH_DIR="$PBA_REPO"

# Case 1: identical SHAs → up to date
test.case $level "T-PROMOTE-BRANCH-ALIGN-1: identical refs report 'up to date'" \
  echo "(promote.branch.alignment dev testing on identical refs)"
promote.branch.alignment dev testing
if [ "$RESULT" = "up to date with dev" ]; then
  expect.pass "identical refs → '$RESULT'"
else
  expect.fail "expected 'up to date with dev', got '$RESULT'"
fi

# Case 2: testing strictly behind dev (one extra commit on dev only)
git -C "$PBA_REPO" checkout -q dev
echo "dev-only" >> "$PBA_REPO/file"
git -C "$PBA_REPO" -c commit.gpgsign=false commit -aq -m "dev-only"
test.case $level "T-PROMOTE-BRANCH-ALIGN-2: testing behind dev reports 'behind'" \
  echo "(promote.branch.alignment dev testing after dev advanced)"
promote.branch.alignment dev testing
if [[ "$RESULT" == *"behind dev"* ]] && [[ "$RESULT" != *"diverged"* ]]; then
  expect.pass "behind → '$RESULT'"
else
  expect.fail "expected behind-only, got '$RESULT'"
fi

# Case 3: testing strictly ahead of dev — i.e. <from>=dev is fully merged
# into <to>=testing (rewind dev one commit, advance testing).
# Detach HEAD first — `branch -f dev` is refused while dev is checked out.
git -C "$PBA_REPO" checkout -q --detach
git -C "$PBA_REPO" branch -f dev HEAD~1
git -C "$PBA_REPO" checkout -q testing
echo "testing-only" >> "$PBA_REPO/file"
git -C "$PBA_REPO" -c commit.gpgsign=false commit -aq -m "testing-only"
test.case $level "T-PROMOTE-BRANCH-ALIGN-3: testing fully contains dev reports 'merged in'" \
  echo "(promote.branch.alignment dev testing after testing advanced past dev)"
promote.branch.alignment dev testing
if [ "$RESULT" = "dev merged in" ]; then
  expect.pass "merged in → '$RESULT'"
else
  expect.fail "expected 'dev merged in', got '$RESULT'"
fi

# Case 4: diverged — dev gets its own commit on top of the pre-divergence base
git -C "$PBA_REPO" checkout -q dev
echo "dev-second" >> "$PBA_REPO/file"
git -C "$PBA_REPO" -c commit.gpgsign=false commit -aq -m "dev-second"
test.case $level "T-PROMOTE-BRANCH-ALIGN-4: divergent histories report 'diverged'" \
  echo "(promote.branch.alignment dev testing on diverged histories)"
promote.branch.alignment dev testing
if [[ "$RESULT" == diverged:* ]]; then
  expect.pass "diverged → '$RESULT'"
else
  expect.fail "expected 'diverged:*', got '$RESULT'"
fi

# Case 4b: diverged-label correctness. After T-4 testing is 1 behind and 1
# ahead of dev. Add ONE more dev-side commit so we have asymmetric counts
# (would distinguish a behind/ahead swap from a coincidence). Verdict
# carries only the behind-count (the actionable number — what `oo stage
# dev` will catch up). The ahead-count is bookkeeping and intentionally
# omitted from the user-facing verdict.
echo "dev-third" >> "$PBA_REPO/file"
git -C "$PBA_REPO" -c commit.gpgsign=false commit -aq -m "dev-third"
test.case $level "T-PROMOTE-BRANCH-ALIGN-4b: diverged carries the behind-count, not ahead-count" \
  echo "(verifying diverged verdict format)"
promote.branch.alignment dev testing
if [[ "$RESULT" == "diverged: 2 behind dev" ]]; then
  expect.pass "diverged → '$RESULT'"
else
  expect.fail "expected 'diverged: 2 behind dev', got '$RESULT'"
fi

# Case 5: missing args → non-zero return value
test.case $level "T-PROMOTE-BRANCH-ALIGN-5: missing args returns non-zero" \
  echo "(promote.branch.alignment with no arguments)"
promote.branch.alignment >/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

# Restore OOSH_DIR + clean fixture.
OOSH_DIR="$_PBA_OOSH_DIR_SAVE"
rm -rf "$PBA_REPO"
unset PBA_REPO _PBA_OOSH_DIR_SAVE

# ============================================================================
# T-PROMOTE-STATUS-CLEAN: promote.status output is free of stray state ids
# ----------------------------------------------------------------------------
# Regression: `oo promote.status` used to leak the current state index (a bare
# integer) onto its first stdout line because state.of (no-method) echoed.
# ============================================================================
test.case $level "T-PROMOTE-STATUS-CLEAN: first non-empty stdout line is not a bare integer" \
  echo "(capturing first non-empty stdout line of promote.status)"
_PS_FIRST=$(promote.status 2>/dev/null | awk 'NF{print; exit}')
if [[ "$_PS_FIRST" =~ ^[0-9]+$ ]]; then
  expect.fail "promote.status leaked bare integer as first line: '$_PS_FIRST'"
else
  expect.pass "promote.status first line OK: '$_PS_FIRST'"
fi
unset _PS_FIRST

# ============================================================================
# T-PROMOTE-STATUS-VISIBLE-AT-LL1: status produces visible stdout at LOG_LEVEL=1
# ----------------------------------------------------------------------------
# Regression: earlier OOSH-conformance refactor (6915877) replaced bare echo
# in promote.status with console.log, which is gated at LOG_LEVEL >= 3 →
# `oo promote.status` produced ZERO visible output at the user's working
# LOG_LEVEL=1. User-invoked .status commands are command-return values, not
# log events; they must use stdout regardless of log level.
# ============================================================================
_OLD_LL=$LOG_LEVEL
log.level 1 >/dev/null 2>&1
test.case 1 "T-PROMOTE-STATUS-VISIBLE-AT-LL1: status emits visible 'dev …' line at LOG_LEVEL=1" \
  echo "(capture promote.status stdout at LOG_LEVEL=1)"
_PS_LL1=$(promote.status 2>/dev/null)
log.level $_OLD_LL >/dev/null 2>&1
if [ -n "$_PS_LL1" ] && echo "$_PS_LL1" | grep -q '^dev    '; then
  expect.pass "promote.status emitted visible 'dev …' line at LL=1"
else
  expect.fail "promote.status produced no visible 'dev …' line at LL=1: '$_PS_LL1'"
fi
unset _OLD_LL _PS_LL1

# ============================================================================
# T-PROMOTE-STUCK-VISIBLE: state-check failures are surfaced to the user
# ----------------------------------------------------------------------------
# Regression for `oo stage testing` emitting only a bare `21` on stdout when
# state 21's check failed (uncommitted change). The check's `$RESULT`
# message went via `error.log` → `$LOG_DEVICE`, which the user didn't see
# in their context. Fix lives in both promote.dev.to.testing and
# promote.testing.to.prod advancement loops: when state can't advance,
# echo a 4-line "Cannot promote …" block via plain `echo` (NOT a log
# primitive) so it reaches stdout regardless of LOG_LEVEL / LOG_DEVICE.
#
# Structural tests (per [state-check-resources-script] — mocking
# state.next + private.check.* end-to-end is brittle; lock the contract
# in by asserting the body shape).
# ============================================================================
test.case $level "T-PROMOTE-STUCK-VISIBLE-1: promote.dev.to.testing emits 'Cannot promote' block" \
  bash -c "type promote.dev.to.testing | grep -qE 'Cannot promote to'"
if type promote.dev.to.testing 2>/dev/null | grep -qE 'Cannot promote to'; then
  expect.pass "promote.dev.to.testing body contains user-visible 'Cannot promote to' failure block"
else
  expect.fail "promote.dev.to.testing must echo a 'Cannot promote to …' block when state can't advance — error.log alone is invisible at the wrong LOG_LEVEL / LOG_DEVICE"
fi

test.case $level "T-PROMOTE-STUCK-VISIBLE-2: promote.testing.to.prod emits 'Cannot promote' block" \
  bash -c "type promote.testing.to.prod | grep -qE 'Cannot promote to'"
if type promote.testing.to.prod 2>/dev/null | grep -qE 'Cannot promote to'; then
  expect.pass "promote.testing.to.prod body contains user-visible 'Cannot promote to' failure block"
else
  expect.fail "promote.testing.to.prod must echo a 'Cannot promote to …' block when state can't advance"
fi

# Doctrine guard: ensure the failure block uses plain `echo`, not a log
# primitive. Guards against a future "uniform log routing" refactor that
# would silently re-introduce the bug.
test.case $level "T-PROMOTE-STUCK-VISIBLE-3: failure block uses plain echo (not console/important/error.log)" \
  bash -c "type promote.testing.to.prod | grep 'Cannot promote to' | grep -qE '^[[:space:]]*echo '"
if type promote.testing.to.prod 2>/dev/null | grep 'Cannot promote to' | grep -qE '^[[:space:]]*echo '; then
  expect.pass "stuck-state failure line uses bare \`echo\` — survives every LOG_LEVEL / LOG_DEVICE"
else
  expect.fail "'Cannot promote to' line must use bare \`echo\` (not console.log / important.log / error.log) per [status-command-output-idiom]"
fi

# ============================================================================
# T-PROMOTE-FORCE-PER-INVOCATION: a no-arg run must NOT inherit persisted force
# ----------------------------------------------------------------------------
# Regression: `promote dev.to.testing yes` writes PROMOTE_FORCE=yes to
# PROMOTE.promote.env. Because the no-arg branch only re-saved the loaded
# config, that flag stuck — every later `oo stage dev` then skipped the
# "Proceed with merge? [y/N]" confirmation. PROMOTE_FORCE / PROMOTE_SKIP are
# per-invocation (enabled only by `yes` / `skip`), so the reset and no-arg
# branches must clear them. Body-assertion guard — the full promote pipeline
# (core tests + real merge/push) can't be driven safely from a unit test, so
# we assert the source resets the flag (the buggy version contained no
# `PROMOTE_FORCE=""`; the `yes`/`skip` branches use `=yes`).
# ============================================================================
test.case $level "T-PROMOTE-FORCE-PER-INVOCATION-1: dev.to.testing clears persisted PROMOTE_FORCE" \
  bash -c "type promote.dev.to.testing | grep -qE 'PROMOTE_FORCE=\"\"'"
if type promote.dev.to.testing 2>/dev/null | grep -qE 'PROMOTE_FORCE=""'; then
  expect.pass "promote.dev.to.testing resets PROMOTE_FORCE (per-invocation force)"
else
  expect.fail "promote.dev.to.testing must reset PROMOTE_FORCE=\"\" on a no-arg/reset run — else a prior 'yes' suppresses the [y/N] prompt forever"
fi

test.case $level "T-PROMOTE-FORCE-PER-INVOCATION-2: testing.to.prod clears persisted PROMOTE_FORCE" \
  bash -c "type promote.testing.to.prod | grep -qE 'PROMOTE_FORCE=\"\"'"
if type promote.testing.to.prod 2>/dev/null | grep -qE 'PROMOTE_FORCE=""'; then
  expect.pass "promote.testing.to.prod resets PROMOTE_FORCE (per-invocation force)"
else
  expect.fail "promote.testing.to.prod must reset PROMOTE_FORCE=\"\" on a no-arg/reset run"
fi

test.suite.save.results
