#!/opt/local/bin/bash
# check_os.sh

# shellcheck disable=SC2034
_BASHUNIT_OS="Unknown"
_BASHUNIT_DISTRO="Unknown"

function bashunit::check_os::init() {
  _BASHUNIT_UNAME="$(uname)"
  if bashunit::check_os::is_linux; then
    _BASHUNIT_OS="Linux"
    if bashunit::check_os::is_ubuntu; then
      _BASHUNIT_DISTRO="Ubuntu"
    elif bashunit::check_os::is_alpine; then
      _BASHUNIT_DISTRO="Alpine"
    elif bashunit::check_os::is_nixos; then
      _BASHUNIT_DISTRO="NixOS"
    else
      _BASHUNIT_DISTRO="Other"
    fi
  elif bashunit::check_os::is_macos; then
    _BASHUNIT_OS="OSX"
  elif bashunit::check_os::is_windows; then
    _BASHUNIT_OS="Windows"
  else
    _BASHUNIT_OS="Unknown"
    _BASHUNIT_DISTRO="Unknown"
  fi
}

function bashunit::check_os::is_ubuntu() {
  command -v apt >/dev/null 2>&1
}

function bashunit::check_os::is_alpine() {
  command -v apk >/dev/null 2>&1
}

function bashunit::check_os::is_nixos() {
  [ -f /etc/NIXOS ] && return 0
  grep -q '^ID=nixos' /etc/os-release 2>/dev/null
}

function bashunit::check_os::is_linux() {
  [ "$_BASHUNIT_UNAME" = "Linux" ]
}

function bashunit::check_os::is_macos() {
  [ "$_BASHUNIT_UNAME" = "Darwin" ]
}

function bashunit::check_os::is_windows() {
  case "$_BASHUNIT_UNAME" in
  *MINGW* | *MSYS* | *CYGWIN*)
    return 0
    ;;
  *)
    return 1
    ;;
  esac
}

##
# Detects the number of online CPU cores, portably across Linux/macOS/BSD.
# Tries nproc, then sysctl, then getconf; falls back to 4 when none report a
# usable positive integer. Takes the first whitespace-delimited token so a
# stray flag or trailing text never poisons the arithmetic guard.
# Returns: prints the core count (>= 1) to stdout.
##
function bashunit::check_os::nproc() {
  local cores=""
  cores="$(nproc 2>/dev/null)" || cores=""
  if [ -z "$cores" ]; then
    cores="$(sysctl -n hw.ncpu 2>/dev/null)" || cores=""
  fi
  if [ -z "$cores" ]; then
    cores="$(getconf _NPROCESSORS_ONLN 2>/dev/null)" || cores=""
  fi
  cores="${cores%% *}"
  case "$cores" in
  '' | *[!0-9]*) cores=4 ;;
  esac
  [ "$cores" -lt 1 ] && cores=4
  echo "$cores"
}

function bashunit::check_os::is_busybox() {

  case "$_BASHUNIT_DISTRO" in

  "Alpine")
    return 0
    ;;
  *)
    return 1
    ;;
  esac
}

bashunit::check_os::init

export _BASHUNIT_OS
export _BASHUNIT_DISTRO
export -f bashunit::check_os::nproc
export -f bashunit::check_os::is_alpine
export -f bashunit::check_os::is_busybox
export -f bashunit::check_os::is_ubuntu
export -f bashunit::check_os::is_nixos

# str.sh

_BASHUNIT_STR_STRIPPED_OUT=""

# Strip ANSI escape codes and control characters, writing the result into the
# global slot _BASHUNIT_STR_STRIPPED_OUT (no fork on the plain-text fast path).
# Callers on hot paths (assert_equals/assert_not_equals) use this to avoid the
# per-call command-substitution fork. See bash-style.md (return-slot pattern).
function bashunit::str::strip_ansi_to_slot() {
  local input="$1"
  # Fast path: plain text with no backslash (echo -e no-op) and no control
  # bytes (nothing for sed to strip) passes through unchanged, zero forks.
  case "$input" in
  *\\* | *[[:cntrl:]]*) ;;
  *)
    _BASHUNIT_STR_STRIPPED_OUT=$input
    return
    ;;
  esac
  # Pure-bash path for short strings without backslashes: display lines (e.g.
  # the per-test "✓ Passed … 3ms" alignment on systems whose clock is fork-free)
  # land here, so a colored line does not cost a sed fork per test. Strip
  # CSI sequences segment-wise, then sweep remaining control bytes; the size
  # guard avoids bash's quadratic pattern-substitution on large captures and
  # `*\\*` still defers to `echo -e` semantics below.
  case "$input" in
  *\\*) ;;
  *)
    if [ "${#input}" -le 1024 ]; then
      local out="" rest="$input" params
      while :; do
        case "$rest" in
        *$'\x1b'\[*)
          out="$out${rest%%$'\x1b'\[*}"
          rest="${rest#*$'\x1b'\[}"
          params=""
          while :; do
            case "$rest" in
            [0-9\;]*)
              params="$params${rest%"${rest#?}"}"
              rest="${rest#?}"
              ;;
            *) break ;;
            esac
          done
          case "$rest" in
          # Same finals sed strips; anything else keeps its printable residue
          # (the ESC itself falls to the control-byte sweep, exactly like sed).
          m* | K*) rest="${rest#?}" ;;
          *) out="${out}[${params}" ;;
          esac
          ;;
        *)
          out="$out$rest"
          break
          ;;
        esac
      done
      _BASHUNIT_STR_STRIPPED_OUT=${out//[[:cntrl:]]/}
      return
    fi
    ;;
  esac
  _BASHUNIT_STR_STRIPPED_OUT=$(echo -e "$input" | sed -E 's/\x1B\[[0-9;]*[mK]//g; s/[[:cntrl:]]//g')
}

# Strip ANSI escape codes and control characters, echoing the result.
# Thin wrapper over the return-slot variant for callers that want stdout.
function bashunit::str::strip_ansi() {
  bashunit::str::strip_ansi_to_slot "$1"
  echo "$_BASHUNIT_STR_STRIPPED_OUT"
}

function bashunit::str::rpad() {
  local left_text="$1"
  local right_word="$2"
  local width_padding="${3:-$TERMINAL_WIDTH}"
  # Subtract 1 more to account for the extra space
  local padding=$((width_padding - ${#right_word} - 1))
  if ((padding < 0)); then
    padding=0
  fi

  # Remove ANSI escape sequences (non-visible characters) for length calculation
  bashunit::str::strip_ansi_to_slot "$left_text"
  local clean_left_text=$_BASHUNIT_STR_STRIPPED_OUT

  local is_truncated=false
  # If the visible left text exceeds the padding, truncate it and add "..."
  if [ ${#clean_left_text} -gt $padding ]; then
    local truncation_length=$((padding < 3 ? 0 : padding - 3))
    clean_left_text="${clean_left_text:0:$truncation_length}"
    is_truncated=true
  fi

  local result_left_text
  local remaining_space
  if $is_truncated; then
    # Rebuild char-by-char with ANSI codes intact, applying the truncation.
    result_left_text=""
    local i=0
    local j=0
    while [ $i -lt ${#clean_left_text} ] && [ $j -lt ${#left_text} ]; do
      local char="${clean_left_text:$i:1}"
      local original_char="${left_text:$j:1}"

      # If the current character is part of an ANSI sequence, skip it and copy it
      if [ "$original_char" = $'\x1b' ]; then
        while [ "${left_text:$j:1}" != "m" ] && [ $j -lt ${#left_text} ]; do
          result_left_text="$result_left_text${left_text:$j:1}"
          ((++j))
        done
        result_left_text="$result_left_text${left_text:$j:1}" # Append the final 'm'
        ((++j))
      elif [ "$char" = "$original_char" ]; then
        # Match the actual character
        result_left_text="$result_left_text$char"
        ((++i))
        ((++j))
      else
        ((++j))
      fi
    done
    result_left_text="$result_left_text..."
    # 1: due to a blank space
    # 3: due to the appended ...
    remaining_space=$((width_padding - ${#clean_left_text} - ${#right_word} - 1 - 3))
  else
    # Not truncated: the visible text fits, so the original (ANSI intact) is
    # already correct — skip the per-character rebuild entirely.
    result_left_text="$left_text"
    remaining_space=$((width_padding - ${#clean_left_text} - ${#right_word} - 1))
  fi

  # Ensure the right word is placed exactly at the far right of the screen
  # filling the remaining space with padding
  if [ $remaining_space -lt 0 ]; then
    remaining_space=0
  fi

  printf "%s%${remaining_space}s %s\n" "$result_left_text" "" "$right_word"
}

# globals.sh
set -euo pipefail

# This file provides a set of global functions to developers.

function bashunit::current_dir() {
  dirname "${BASH_SOURCE[1]}"
}

function bashunit::current_filename() {
  basename "${BASH_SOURCE[1]}"
}

function bashunit::caller_filename() {
  dirname "${BASH_SOURCE[2]}"
}

function bashunit::caller_line() {
  echo "${BASH_LINENO[1]}"
}

function bashunit::current_timestamp() {
  date +"%Y-%m-%d %H:%M:%S"
}

function bashunit::is_command_available() {
  command -v "$1" >/dev/null 2>&1
}

function bashunit::random_str() {
  local length=${1:-6}
  local chars='abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'
  local str=''
  local i
  for ((i = 0; i < length; i++)); do
    str="$str${chars:RANDOM%${#chars}:1}"
  done
  echo "$str"
}

function bashunit::temp_file() {
  local prefix=${1:-bashunit}
  local test_prefix=""
  if [ -n "${BASHUNIT_CURRENT_TEST_ID:-}" ]; then
    # We're inside a test function - use test ID
    test_prefix="${BASHUNIT_CURRENT_TEST_ID}_"
  elif [ -n "${BASHUNIT_CURRENT_SCRIPT_ID:-}" ]; then
    # We're at script level (e.g., in set_up_before_script) - use script ID
    test_prefix="${BASHUNIT_CURRENT_SCRIPT_ID}_"
  fi
  "$MKTEMP" "$BASHUNIT_TEMP_DIR/${test_prefix}${prefix}.XXXXXXX"
}

function bashunit::temp_dir() {
  local prefix=${1:-bashunit}
  local test_prefix=""
  if [ -n "${BASHUNIT_CURRENT_TEST_ID:-}" ]; then
    # We're inside a test function - use test ID
    test_prefix="${BASHUNIT_CURRENT_TEST_ID}_"
  elif [ -n "${BASHUNIT_CURRENT_SCRIPT_ID:-}" ]; then
    # We're at script level (e.g., in set_up_before_script) - use script ID
    test_prefix="${BASHUNIT_CURRENT_SCRIPT_ID}_"
  fi
  "$MKTEMP" -d "$BASHUNIT_TEMP_DIR/${test_prefix}${prefix}.XXXXXXX"
}

function bashunit::cleanup_testcase_temp_files() {
  bashunit::internal_log "cleanup_testcase_temp_files"
  if [ -n "${BASHUNIT_CURRENT_TEST_ID:-}" ]; then
    # Probe the glob in pure bash first: most tests create no temp file, so
    # skipping the rm avoids a fork per test (#764). A non-matching glob either
    # stays literal (nullglob off) or yields an empty array (nullglob on);
    # ${matches[0]:-} handles both under set -u, and [ -e ] is false in each
    # case. temp_file runs in a $(...) subshell, so a global flag could not
    # reach this trap; checking the filesystem is the only reliable signal.
    # Declare and assign separately: bash 3.0 does not expand a compound array
    # assignment attached to `local`, it stores the literal "(glob)" instead.
    local matches
    matches=("$BASHUNIT_TEMP_DIR/${BASHUNIT_CURRENT_TEST_ID}"_*)
    # if-form, not `[ ] && rm`: as the function's last statement the skip path
    # would return 1, which is a death sentence for callers under set -e (#836)
    if [ -e "${matches[0]:-}" ]; then
      rm -rf "${matches[@]}"
    fi
  fi
}

function bashunit::cleanup_script_temp_files() {
  bashunit::internal_log "cleanup_script_temp_files"
  if [ -n "${BASHUNIT_CURRENT_SCRIPT_ID:-}" ]; then
    rm -rf "$BASHUNIT_TEMP_DIR/${BASHUNIT_CURRENT_SCRIPT_ID}"_*
  fi
}

# shellcheck disable=SC2145
function bashunit::log() {
  if ! bashunit::env::is_dev_mode_enabled; then
    return
  fi

  local level="$1"
  shift

  case "$level" in
  info | INFO) level="INFO" ;;
  debug | DEBUG) level="DEBUG" ;;
  warning | WARNING) level="WARNING" ;;
  critical | CRITICAL) level="CRITICAL" ;;
  error | ERROR) level="ERROR" ;;
  *)
    set -- "$level $@"
    level="INFO"
    ;;
  esac

  echo "$(bashunit::current_timestamp) [$level]: $* #${BASH_SOURCE[1]}:${BASH_LINENO[0]}" >>"$BASHUNIT_DEV_LOG"
}

function bashunit::internal_log() {
  if ! bashunit::env::is_dev_mode_enabled || ! bashunit::env::is_internal_log_enabled; then
    return
  fi

  echo "$(bashunit::current_timestamp) [INTERNAL]: $* #${BASH_SOURCE[1]}:${BASH_LINENO[0]}" >>"$BASHUNIT_DEV_LOG"
}

function bashunit::print_line() {
  local length="${1:-70}" # Default to 70 if not passed
  local char="${2:--}"    # Default to '-' if not passed
  printf '%*s\n' "$length" '' | tr ' ' "$char"
}

function bashunit::data_set() {
  local arg
  local first=true

  for arg in "$@"; do
    if [ "$first" = true ]; then
      # Bash 3.0 compatible: printf '%q' "" produces nothing in Bash 3.0
      if [ -z "$arg" ]; then
        printf "''"
      else
        printf '%q' "$arg"
      fi
      first=false
    else
      if [ -z "$arg" ]; then
        printf " ''"
      else
        printf ' %q' "$arg"
      fi
    fi
  done
  # Sentinel empty string at end
  printf " ''\n"
}

# dependencies.sh
set -euo pipefail

function bashunit::dependencies::has_perl() {
  command -v perl >/dev/null 2>&1
}

function bashunit::dependencies::has_powershell() {
  command -v powershell >/dev/null 2>&1
}

function bashunit::dependencies::has_adjtimex() {
  command -v adjtimex >/dev/null 2>&1
}

function bashunit::dependencies::has_bc() {
  command -v bc >/dev/null 2>&1
}

function bashunit::dependencies::has_awk() {
  command -v awk >/dev/null 2>&1
}

function bashunit::dependencies::has_git() {
  command -v git >/dev/null 2>&1
}

function bashunit::dependencies::has_curl() {
  command -v curl >/dev/null 2>&1
}

function bashunit::dependencies::has_wget() {
  command -v wget >/dev/null 2>&1
}

function bashunit::dependencies::has_python() {
  command -v python >/dev/null 2>&1
}

function bashunit::dependencies::has_node() {
  command -v node >/dev/null 2>&1
}

function bashunit::dependencies::has_tput() {
  command -v tput >/dev/null 2>&1
}

# io.sh

##
# Clear the terminal screen and move the cursor to the home position.
# Uses `tput clear` when available (queries terminfo for the right sequence)
# and falls back to the ANSI sequence \033[2J\033[H otherwise.
##
function bashunit::io::clear_screen() {
  if bashunit::dependencies::has_tput; then
    local out
    out=$(tput clear 2>/dev/null)
    if [ -n "$out" ]; then
      printf '%s' "$out"
      return
    fi
  fi
  printf '\033[2J\033[H'
}

function bashunit::io::download_to() {
  local url="$1"
  local output="$2"
  if bashunit::dependencies::has_curl; then
    curl -fsSL -o "$output" "$url"
  elif bashunit::dependencies::has_wget; then
    wget -q -O "$output" "$url"
  else
    echo "no curl or wget available" >&2
    return 1
  fi
}

# math.sh

function bashunit::math::calculate() {
  local expr="$*"

  if bashunit::dependencies::has_bc; then
    echo "$expr" | bc
    return
  fi

  case "$expr" in
  *.*)
    if bashunit::dependencies::has_awk; then
      awk "BEGIN { print ($expr) }"
      return
    fi
    # Downgrade to integer math by stripping decimals
    expr=$(echo "$expr" | sed -E 's/([0-9]+)\.[0-9]+/\1/g')
    ;;
  esac

  # Remove leading zeros from integers so $((...)) does not read them as octal.
  # Only fork sed when a leading zero is actually present — the common callers
  # (clock durations) never produce one, so the no-bc path stays fork-free.
  case "$expr" in
  0[0-9]* | *[!0-9.]0[0-9]*)
    expr=$(echo "$expr" | sed -E 's/\b0*([1-9][0-9]*)/\1/g')
    ;;
  esac

  local result=$((expr))
  echo "$result"
}

##
# Deterministically shuffles stdin lines (one item per line) with a Fisher-Yates
# driven by a seeded LCG (glibc constants). Same seed + same input always yields
# the same permutation, so a randomized run can be replayed via its seed.
# Self-contained (seeds a local state), so it is safe inside subshells/pipes and
# in --parallel where each test file shuffles in its own forked shell.
# Arguments: $1 - integer seed (non-numeric treated as 0)
##
function bashunit::math::shuffle() {
  local seed=$1
  case "$seed" in '' | *[!0-9]*) seed=0 ;; esac
  local state=$((seed & 2147483647))

  local -a items=()
  local n=0
  local line
  # `|| [ -n "$line" ]` keeps the final item when stdin has no trailing newline.
  while IFS= read -r line || [ -n "$line" ]; do
    items[n]=$line
    n=$((n + 1))
  done

  local i j tmp
  i=$((n - 1))
  while [ "$i" -gt 0 ]; do
    state=$(((1103515245 * state + 12345) & 2147483647))
    j=$((state % (i + 1)))
    tmp=${items[i]}
    items[i]=${items[j]}
    items[j]=$tmp
    i=$((i - 1))
  done

  local k=0
  while [ "$k" -lt "$n" ]; do
    printf '%s\n' "${items[k]}"
    k=$((k + 1))
  done
}

# parallel.sh

function bashunit::parallel::aggregate_test_results() {
  local temp_dir_parallel_test_suite=$1
  local IFS=$' \t\n'

  bashunit::internal_log "aggregate_test_results" "dir:$temp_dir_parallel_test_suite"

  local total_failed=0
  local total_passed=0
  local total_skipped=0
  local total_incomplete=0
  local total_snapshot=0

  local script_dir=""
  for script_dir in "$temp_dir_parallel_test_suite"/*; do
    shopt -s nullglob
    # Bash 3.0 compatible: separate declaration and assignment for arrays
    local result_files
    result_files=("$script_dir"/*.result)
    shopt -u nullglob

    if [ ${#result_files[@]} -eq 0 ]; then
      printf "%sNo tests found%s" "$_BASHUNIT_COLOR_SKIPPED" "$_BASHUNIT_COLOR_DEFAULT"
      continue
    fi

    local result_file=""
    for result_file in "${result_files[@]+"${result_files[@]}"}"; do
      local result_line
      result_line=$(<"$result_file")
      result_line="${result_line##*$'\n'}"

      local failed="${result_line##*##ASSERTIONS_FAILED=}"
      failed="${failed%%##*}"
      failed=${failed:-0}

      local passed="${result_line##*##ASSERTIONS_PASSED=}"
      passed="${passed%%##*}"
      passed=${passed:-0}

      local skipped="${result_line##*##ASSERTIONS_SKIPPED=}"
      skipped="${skipped%%##*}"
      skipped=${skipped:-0}

      local incomplete="${result_line##*##ASSERTIONS_INCOMPLETE=}"
      incomplete="${incomplete%%##*}"
      incomplete=${incomplete:-0}

      local snapshot="${result_line##*##ASSERTIONS_SNAPSHOT=}"
      snapshot="${snapshot%%##*}"
      snapshot=${snapshot:-0}

      local exit_code="${result_line##*##TEST_EXIT_CODE=}"
      exit_code="${exit_code%%##*}"
      exit_code=${exit_code:-0}

      # Add to the total counts
      total_failed=$((total_failed + failed))
      total_passed=$((total_passed + passed))
      total_skipped=$((total_skipped + skipped))
      total_incomplete=$((total_incomplete + incomplete))
      total_snapshot=$((total_snapshot + snapshot))

      if [ "${failed:-0}" -gt 0 ]; then
        bashunit::state::add_tests_failed
        continue
      fi

      if [ "${exit_code:-0}" -ne 0 ]; then
        bashunit::state::add_tests_failed
        continue
      fi

      if [ "${snapshot:-0}" -gt 0 ]; then
        bashunit::state::add_tests_snapshot
        continue
      fi

      if [ "${incomplete:-0}" -gt 0 ]; then
        bashunit::state::add_tests_incomplete
        continue
      fi

      if [ "${skipped:-0}" -gt 0 ]; then
        bashunit::state::add_tests_skipped
        continue
      fi

      # Check for risky test (zero assertions, no error)
      local total_for_test=$((failed + passed + skipped + incomplete + snapshot))
      if [ "$total_for_test" -eq 0 ] && [ "${exit_code:-0}" -eq 0 ]; then
        if bashunit::env::is_fail_on_risky_enabled; then
          bashunit::state::add_tests_failed
        else
          bashunit::state::add_tests_risky
        fi
        continue
      fi

      bashunit::state::add_tests_passed
    done
  done

  export _BASHUNIT_ASSERTIONS_FAILED=$total_failed
  export _BASHUNIT_ASSERTIONS_PASSED=$total_passed
  export _BASHUNIT_ASSERTIONS_SKIPPED=$total_skipped
  export _BASHUNIT_ASSERTIONS_INCOMPLETE=$total_incomplete
  export _BASHUNIT_ASSERTIONS_SNAPSHOT=$total_snapshot

  bashunit::internal_log "aggregate_totals" \
    "failed:$total_failed" \
    "passed:$total_passed" \
    "skipped:$total_skipped" \
    "incomplete:$total_incomplete" \
    "snapshot:$total_snapshot"
}

function bashunit::parallel::mark_stop_on_failure() {
  touch "$TEMP_FILE_PARALLEL_STOP_ON_FAILURE"
}

function bashunit::parallel::must_stop_on_failure() {
  [ -f "$TEMP_FILE_PARALLEL_STOP_ON_FAILURE" ]
}

function bashunit::parallel::cleanup() {
  # shellcheck disable=SC2153
  local target="$TEMP_DIR_PARALLEL_TEST_SUITE"
  case "$target" in
  */bashunit/parallel/*)
    rm -rf "$target"
    ;;
  *)
    bashunit::internal_log "parallel::cleanup" "refused unsafe path:$target"
    return 1
    ;;
  esac
}

function bashunit::parallel::init() {
  bashunit::parallel::cleanup
  mkdir -p "$TEMP_DIR_PARALLEL_TEST_SUITE"
}

# Cached result of resolve_enabled ("true"/"false"); empty until resolved.
_BASHUNIT_PARALLEL_ENABLED=""

# Pure predicate: parallel requested AND running on a supported OS. No caching
# or logging, so it is safe to call fresh (e.g. from unit tests).
function bashunit::parallel::_compute_enabled() {
  bashunit::env::is_parallel_run_enabled &&
    (bashunit::check_os::is_macos || bashunit::check_os::is_ubuntu ||
      bashunit::check_os::is_alpine || bashunit::check_os::is_windows)
}

# Resolve parallel mode once (after arg parsing) into _BASHUNIT_PARALLEL_ENABLED
# so is_enabled becomes a pure global read on the per-test hot path. The env +
# OS checks are constant for the whole run.
function bashunit::parallel::resolve_enabled() {
  if bashunit::parallel::_compute_enabled; then
    _BASHUNIT_PARALLEL_ENABLED=true
  else
    _BASHUNIT_PARALLEL_ENABLED=false
  fi
  bashunit::internal_log "bashunit::parallel::resolve_enabled" \
    "requested:$BASHUNIT_PARALLEL_RUN" "os:${_BASHUNIT_OS:-Unknown}" \
    "enabled:$_BASHUNIT_PARALLEL_ENABLED"
}

function bashunit::parallel::is_enabled() {
  case "$_BASHUNIT_PARALLEL_ENABLED" in
  true) return 0 ;;
  false) return 1 ;;
  esac
  # Not resolved yet (e.g. unit tests call is_enabled directly): compute fresh
  # without caching so per-test env/OS changes are still honoured.
  bashunit::parallel::_compute_enabled
}

# env.sh

# shellcheck disable=SC2034

##
# Loads a project config file of `KEY=value` lines (comments with `#` and blank
# lines are ignored). Each key is only applied when not already set in the
# environment, so real env vars and CLI flags keep precedence over the file.
# Surrounding single/double quotes and an optional `export ` prefix are stripped.
# Arguments: $1 path to the config file
##
function bashunit::env::load_config_file() {
  local file=$1
  [ -f "$file" ] || return 0

  local line key val
  while IFS= read -r line || [ -n "$line" ]; do
    # Trim leading whitespace
    line=${line#"${line%%[![:space:]]*}"}
    case "$line" in
    '' | '#'*) continue ;;
    esac
    case "$line" in export\ *) line=${line#export } ;; esac
    case "$line" in
    *=*) ;;
    *) continue ;;
    esac

    key=${line%%=*}
    val=${line#*=}

    # Only accept valid shell identifiers (defends the eval below)
    case "$key" in
    '' | *[!A-Za-z0-9_]* | [0-9]*) continue ;;
    esac

    # Strip surrounding matching quotes
    case "$val" in
    \"*\") val=${val#\"} val=${val%\"} ;;
    \'*\') val=${val#\'} val=${val%\'} ;;
    esac

    # Apply only when unset: env var / CLI flag > config file
    eval "export $key=\"\${$key:-\$val}\""
  done <"$file"
}

##
# Echoes $1 when it is a positive integer, otherwise echoes the default $2.
# Arguments: $1 candidate value, $2 fallback default
##
function bashunit::env::positive_int_or_default() {
  local value="$1"
  local default="$2"
  case "$value" in
  '' | *[!0-9]* | 0) echo "$default" ;;
  *) echo "$value" ;;
  esac
}

# Load project config (lower precedence than env vars, .env and CLI flags).
# Load .env file (skip if --skip-env-file is used to keep shell environment intact)
if [ "${BASHUNIT_SKIP_ENV_FILE:-false}" != "true" ]; then
  bashunit::env::load_config_file ".bashunitrc"
  set -o allexport
  # shellcheck source=/dev/null
  [ -f ".env" ] && source .env
  set +o allexport
fi

_BASHUNIT_DEFAULT_DEFAULT_PATH="tests"
_BASHUNIT_DEFAULT_BOOTSTRAP="tests/bootstrap.sh"
_BASHUNIT_DEFAULT_DEV_LOG=""
_BASHUNIT_DEFAULT_LOG_JUNIT=""
_BASHUNIT_DEFAULT_LOG_GHA=""
_BASHUNIT_DEFAULT_REPORT_HTML=""
_BASHUNIT_DEFAULT_REPORT_TAP=""
_BASHUNIT_DEFAULT_REPORT_JSON=""

# Coverage defaults (following kcov, bashcov, SimpleCov conventions)
_BASHUNIT_DEFAULT_COVERAGE="false"
_BASHUNIT_DEFAULT_COVERAGE_PATHS=""
_BASHUNIT_DEFAULT_COVERAGE_EXCLUDE="tests/*,vendor/*,*_test.sh,*Test.sh"
_BASHUNIT_DEFAULT_COVERAGE_REPORT="coverage/lcov.info"
_BASHUNIT_DEFAULT_COVERAGE_REPORT_HTML=""
_BASHUNIT_DEFAULT_COVERAGE_MIN=""
_BASHUNIT_DEFAULT_COVERAGE_THRESHOLD_LOW="50"
_BASHUNIT_DEFAULT_COVERAGE_THRESHOLD_HIGH="80"

: "${BASHUNIT_DEFAULT_PATH:=${DEFAULT_PATH:=$_BASHUNIT_DEFAULT_DEFAULT_PATH}}"
: "${BASHUNIT_DEV_LOG:=${DEV_LOG:=$_BASHUNIT_DEFAULT_DEV_LOG}}"
: "${BASHUNIT_BOOTSTRAP:=${BOOTSTRAP:=$_BASHUNIT_DEFAULT_BOOTSTRAP}}"
: "${BASHUNIT_BOOTSTRAP_ARGS:=${BOOTSTRAP_ARGS:=}}"
: "${BASHUNIT_LOG_JUNIT:=${LOG_JUNIT:=$_BASHUNIT_DEFAULT_LOG_JUNIT}}"
: "${BASHUNIT_LOG_GHA:=${LOG_GHA:=$_BASHUNIT_DEFAULT_LOG_GHA}}"
: "${BASHUNIT_REPORT_HTML:=${REPORT_HTML:=$_BASHUNIT_DEFAULT_REPORT_HTML}}"
: "${BASHUNIT_REPORT_TAP:=${REPORT_TAP:=$_BASHUNIT_DEFAULT_REPORT_TAP}}"
: "${BASHUNIT_REPORT_JSON:=${REPORT_JSON:=$_BASHUNIT_DEFAULT_REPORT_JSON}}"

# Watch mode polling interval (seconds) used by the pure-shell fallback
_BASHUNIT_DEFAULT_WATCH_INTERVAL="2"
: "${BASHUNIT_WATCH_INTERVAL:=${WATCH_INTERVAL:=$_BASHUNIT_DEFAULT_WATCH_INTERVAL}}"
BASHUNIT_WATCH_INTERVAL=$(bashunit::env::positive_int_or_default \
  "$BASHUNIT_WATCH_INTERVAL" "$_BASHUNIT_DEFAULT_WATCH_INTERVAL")

# Coverage
: "${BASHUNIT_COVERAGE:=${COVERAGE:=$_BASHUNIT_DEFAULT_COVERAGE}}"
: "${BASHUNIT_COVERAGE_PATHS:=${COVERAGE_PATHS:=$_BASHUNIT_DEFAULT_COVERAGE_PATHS}}"
: "${BASHUNIT_COVERAGE_EXCLUDE:=${COVERAGE_EXCLUDE:=$_BASHUNIT_DEFAULT_COVERAGE_EXCLUDE}}"
: "${BASHUNIT_COVERAGE_REPORT:=${COVERAGE_REPORT:=$_BASHUNIT_DEFAULT_COVERAGE_REPORT}}"
: "${BASHUNIT_COVERAGE_REPORT_HTML:=${COVERAGE_REPORT_HTML:=$_BASHUNIT_DEFAULT_COVERAGE_REPORT_HTML}}"
: "${BASHUNIT_COVERAGE_MIN:=${COVERAGE_MIN:=$_BASHUNIT_DEFAULT_COVERAGE_MIN}}"
: "${BASHUNIT_COVERAGE_THRESHOLD_LOW:=${COVERAGE_THRESHOLD_LOW:=$_BASHUNIT_DEFAULT_COVERAGE_THRESHOLD_LOW}}"
: "${BASHUNIT_COVERAGE_THRESHOLD_HIGH:=${COVERAGE_THRESHOLD_HIGH:=$_BASHUNIT_DEFAULT_COVERAGE_THRESHOLD_HIGH}}"

# Booleans
_BASHUNIT_DEFAULT_PARALLEL_RUN="false"
_BASHUNIT_DEFAULT_SHOW_HEADER="true"
_BASHUNIT_DEFAULT_HEADER_ASCII_ART="false"
_BASHUNIT_DEFAULT_SIMPLE_OUTPUT="false"
_BASHUNIT_DEFAULT_STOP_ON_FAILURE="false"
# "auto" shows per-test times only when the clock is fork-free (#765).
_BASHUNIT_DEFAULT_SHOW_EXECUTION_TIME="auto"
_BASHUNIT_DEFAULT_VERBOSE="false"
_BASHUNIT_DEFAULT_BENCH_MODE="false"
_BASHUNIT_DEFAULT_NO_OUTPUT="false"
_BASHUNIT_DEFAULT_INTERNAL_LOG="false"
_BASHUNIT_DEFAULT_SHOW_SKIPPED="false"
_BASHUNIT_DEFAULT_SHOW_INCOMPLETE="false"
_BASHUNIT_DEFAULT_STRICT_MODE="false"
_BASHUNIT_DEFAULT_STOP_ON_ASSERTION_FAILURE="true"
_BASHUNIT_DEFAULT_SKIP_ENV_FILE="false"
_BASHUNIT_DEFAULT_LOGIN_SHELL="false"
_BASHUNIT_DEFAULT_FAILURES_ONLY="false"
_BASHUNIT_DEFAULT_NO_COLOR="false"
_BASHUNIT_DEFAULT_NO_DIFF="false"
_BASHUNIT_DEFAULT_SHOW_OUTPUT_ON_FAILURE="true"
_BASHUNIT_DEFAULT_NO_PROGRESS="false"
_BASHUNIT_DEFAULT_OUTPUT_FORMAT=""
_BASHUNIT_DEFAULT_FAIL_ON_RISKY="false"
_BASHUNIT_DEFAULT_PROFILE="false"
_BASHUNIT_DEFAULT_PROFILE_COUNT="10"
# Per-test timeout in seconds (0 = disabled)
_BASHUNIT_DEFAULT_TEST_TIMEOUT="0"
# Extra attempts for a failed test (0 = no retry)
_BASHUNIT_DEFAULT_RETRY="0"
# Randomize test execution order to surface inter-test coupling
_BASHUNIT_DEFAULT_RANDOM_ORDER="false"
# Seed for --random-order (empty = generate one and print it)
_BASHUNIT_DEFAULT_SEED=""
# Shard <index>/<total> to split the suite across runners (empty = disabled)
_BASHUNIT_DEFAULT_SHARD_INDEX=""
_BASHUNIT_DEFAULT_SHARD_TOTAL=""

: "${BASHUNIT_PARALLEL_RUN:=${PARALLEL_RUN:=$_BASHUNIT_DEFAULT_PARALLEL_RUN}}"
: "${BASHUNIT_PARALLEL_JOBS:=0}"
: "${BASHUNIT_SHOW_HEADER:=${SHOW_HEADER:=$_BASHUNIT_DEFAULT_SHOW_HEADER}}"
: "${BASHUNIT_HEADER_ASCII_ART:=${HEADER_ASCII_ART:=$_BASHUNIT_DEFAULT_HEADER_ASCII_ART}}"
: "${BASHUNIT_SIMPLE_OUTPUT:=${SIMPLE_OUTPUT:=$_BASHUNIT_DEFAULT_SIMPLE_OUTPUT}}"
: "${BASHUNIT_STOP_ON_FAILURE:=${STOP_ON_FAILURE:=$_BASHUNIT_DEFAULT_STOP_ON_FAILURE}}"
: "${BASHUNIT_SHOW_EXECUTION_TIME:=${SHOW_EXECUTION_TIME:=$_BASHUNIT_DEFAULT_SHOW_EXECUTION_TIME}}"
: "${BASHUNIT_VERBOSE:=${VERBOSE:=$_BASHUNIT_DEFAULT_VERBOSE}}"
: "${BASHUNIT_BENCH_MODE:=${BENCH_MODE:=$_BASHUNIT_DEFAULT_BENCH_MODE}}"
: "${BASHUNIT_NO_OUTPUT:=${NO_OUTPUT:=$_BASHUNIT_DEFAULT_NO_OUTPUT}}"
: "${BASHUNIT_INTERNAL_LOG:=${INTERNAL_LOG:=$_BASHUNIT_DEFAULT_INTERNAL_LOG}}"
: "${BASHUNIT_SHOW_SKIPPED:=${SHOW_SKIPPED:=$_BASHUNIT_DEFAULT_SHOW_SKIPPED}}"
: "${BASHUNIT_SHOW_INCOMPLETE:=${SHOW_INCOMPLETE:=$_BASHUNIT_DEFAULT_SHOW_INCOMPLETE}}"
: "${BASHUNIT_STRICT_MODE:=${STRICT_MODE:=$_BASHUNIT_DEFAULT_STRICT_MODE}}"
: "${BASHUNIT_STOP_ON_ASSERTION_FAILURE:=${STOP_ON_ASSERTION_FAILURE:=$_BASHUNIT_DEFAULT_STOP_ON_ASSERTION_FAILURE}}"
: "${BASHUNIT_SKIP_ENV_FILE:=${SKIP_ENV_FILE:=$_BASHUNIT_DEFAULT_SKIP_ENV_FILE}}"
: "${BASHUNIT_LOGIN_SHELL:=${LOGIN_SHELL:=$_BASHUNIT_DEFAULT_LOGIN_SHELL}}"
: "${BASHUNIT_FAILURES_ONLY:=${FAILURES_ONLY:=$_BASHUNIT_DEFAULT_FAILURES_ONLY}}"
: "${BASHUNIT_SHOW_OUTPUT_ON_FAILURE:=${SHOW_OUTPUT_ON_FAILURE:=$_BASHUNIT_DEFAULT_SHOW_OUTPUT_ON_FAILURE}}"
: "${BASHUNIT_NO_DIFF:=${NO_DIFF:=$_BASHUNIT_DEFAULT_NO_DIFF}}"
: "${BASHUNIT_NO_PROGRESS:=${NO_PROGRESS:=$_BASHUNIT_DEFAULT_NO_PROGRESS}}"
: "${BASHUNIT_OUTPUT_FORMAT:=${OUTPUT_FORMAT:=$_BASHUNIT_DEFAULT_OUTPUT_FORMAT}}"
: "${BASHUNIT_FAIL_ON_RISKY:=${FAIL_ON_RISKY:=$_BASHUNIT_DEFAULT_FAIL_ON_RISKY}}"
: "${BASHUNIT_PROFILE:=${PROFILE:=$_BASHUNIT_DEFAULT_PROFILE}}"
: "${BASHUNIT_PROFILE_COUNT:=${PROFILE_COUNT:=$_BASHUNIT_DEFAULT_PROFILE_COUNT}}"
: "${BASHUNIT_TEST_TIMEOUT:=${TEST_TIMEOUT:=$_BASHUNIT_DEFAULT_TEST_TIMEOUT}}"
# No bare RETRY alias on purpose: it is too generic and would pick up unrelated
# environment values. Only BASHUNIT_RETRY configures retries.
: "${BASHUNIT_RETRY:=$_BASHUNIT_DEFAULT_RETRY}"
# Single alias on purpose: bare RANDOM_ORDER/SEED are too generic and would pick
# up unrelated environment values.
: "${BASHUNIT_RANDOM_ORDER:=$_BASHUNIT_DEFAULT_RANDOM_ORDER}"
: "${BASHUNIT_SEED:=$_BASHUNIT_DEFAULT_SEED}"
: "${BASHUNIT_SHARD_INDEX:=$_BASHUNIT_DEFAULT_SHARD_INDEX}"
: "${BASHUNIT_SHARD_TOTAL:=$_BASHUNIT_DEFAULT_SHARD_TOTAL}"
# Support NO_COLOR standard (https://no-color.org)
if [ -n "${NO_COLOR:-}" ]; then
  BASHUNIT_NO_COLOR="true"
else
  : "${BASHUNIT_NO_COLOR:=$_BASHUNIT_DEFAULT_NO_COLOR}"
fi

function bashunit::env::is_parallel_run_enabled() {
  [ "$BASHUNIT_PARALLEL_RUN" = "true" ]
}

##
# Whether a per-test timeout is configured (a positive integer number of seconds).
# Returns: 0 when enabled, 1 otherwise.
##
function bashunit::env::is_test_timeout_enabled() {
  case "${BASHUNIT_TEST_TIMEOUT:-0}" in
  '' | *[!0-9]*) return 1 ;;
  esac
  [ "${BASHUNIT_TEST_TIMEOUT:-0}" -gt 0 ]
}

##
# Prints the configured per-test timeout in seconds (0 when disabled).
##
function bashunit::env::test_timeout_secs() {
  printf '%s' "${BASHUNIT_TEST_TIMEOUT:-0}"
}

##
# Prints the number of extra attempts for a failed test (0 = no retry).
# A non-numeric value is treated as 0.
##
# Validates BASHUNIT_RETRY into the integer global _BASHUNIT_RETRY_VALIDATED.
# In-shell (no fork) so the per-test hot path can read the global instead of
# capturing retry_count in a $(...) subshell every test (#764).
_BASHUNIT_RETRY_VALIDATED=0
function bashunit::env::resolve_retry_count() {
  case "${BASHUNIT_RETRY:-0}" in
  '' | *[!0-9]*) _BASHUNIT_RETRY_VALIDATED=0 ;;
  *) _BASHUNIT_RETRY_VALIDATED="${BASHUNIT_RETRY:-0}" ;;
  esac
}

function bashunit::env::retry_count() {
  bashunit::env::resolve_retry_count
  printf '%s' "$_BASHUNIT_RETRY_VALIDATED"
}

function bashunit::env::is_random_order_enabled() {
  [ "$BASHUNIT_RANDOM_ORDER" = "true" ]
}

##
# Prints the configured random-order seed (empty when none set yet).
##
function bashunit::env::seed() {
  printf '%s' "${BASHUNIT_SEED:-}"
}

function bashunit::env::is_shard_enabled() {
  [ -n "${BASHUNIT_SHARD_INDEX:-}" ] && [ -n "${BASHUNIT_SHARD_TOTAL:-}" ]
}

function bashunit::env::shard_index() {
  printf '%s' "${BASHUNIT_SHARD_INDEX:-}"
}

function bashunit::env::shard_total() {
  printf '%s' "${BASHUNIT_SHARD_TOTAL:-}"
}

function bashunit::env::is_show_header_enabled() {
  [ "$BASHUNIT_SHOW_HEADER" = "true" ]
}

function bashunit::env::is_header_ascii_art_enabled() {
  [ "$BASHUNIT_HEADER_ASCII_ART" = "true" ]
}

function bashunit::env::is_simple_output_enabled() {
  [ "$BASHUNIT_SIMPLE_OUTPUT" = "true" ]
}

function bashunit::env::is_stop_on_failure_enabled() {
  [ "$BASHUNIT_STOP_ON_FAILURE" = "true" ]
}

function bashunit::env::is_show_execution_time_enabled() {
  case "$BASHUNIT_SHOW_EXECUTION_TIME" in
  true) return 0 ;;
  auto) ! bashunit::clock::is_expensive ;;
  *) return 1 ;;
  esac
}

# The total "Time taken" footer costs two clock reads per run (negligible), so it
# stays visible in "auto" mode even when per-test timing is skipped; only an
# explicit "false" hides it (#765).
function bashunit::env::is_total_execution_time_enabled() {
  [ "$BASHUNIT_SHOW_EXECUTION_TIME" != "false" ]
}

function bashunit::env::is_dev_mode_enabled() {
  [ -n "$BASHUNIT_DEV_LOG" ]
}

function bashunit::env::is_internal_log_enabled() {
  [ "$BASHUNIT_INTERNAL_LOG" = "true" ]
}

function bashunit::env::is_verbose_enabled() {
  [ "$BASHUNIT_VERBOSE" = "true" ]
}

function bashunit::env::is_bench_mode_enabled() {
  [ "$BASHUNIT_BENCH_MODE" = "true" ]
}

function bashunit::env::is_no_output_enabled() {
  [ "$BASHUNIT_NO_OUTPUT" = "true" ]
}

function bashunit::env::is_show_skipped_enabled() {
  [ "$BASHUNIT_SHOW_SKIPPED" = "true" ]
}

function bashunit::env::is_show_incomplete_enabled() {
  [ "$BASHUNIT_SHOW_INCOMPLETE" = "true" ]
}

function bashunit::env::is_strict_mode_enabled() {
  [ "$BASHUNIT_STRICT_MODE" = "true" ]
}

function bashunit::env::is_stop_on_assertion_failure_enabled() {
  [ "$BASHUNIT_STOP_ON_ASSERTION_FAILURE" = "true" ]
}

function bashunit::env::is_skip_env_file_enabled() {
  [ "$BASHUNIT_SKIP_ENV_FILE" = "true" ]
}

function bashunit::env::is_login_shell_enabled() {
  [ "$BASHUNIT_LOGIN_SHELL" = "true" ]
}

function bashunit::env::is_failures_only_enabled() {
  [ "$BASHUNIT_FAILURES_ONLY" = "true" ]
}

function bashunit::env::is_show_output_on_failure_enabled() {
  [ "$BASHUNIT_SHOW_OUTPUT_ON_FAILURE" = "true" ]
}

function bashunit::env::is_no_progress_enabled() {
  [ "$BASHUNIT_NO_PROGRESS" = "true" ]
}

function bashunit::env::is_no_color_enabled() {
  [ "$BASHUNIT_NO_COLOR" = "true" ]
}

function bashunit::env::is_diff_enabled() {
  # :- guard: a user (or test) may have unset BASHUNIT_NO_DIFF; a bare read
  # dies under set -u (--strict) (#836)
  [ "${BASHUNIT_NO_DIFF:-}" != "true" ]
}

##
# Whether the current terminal can render ANSI color sequences.
# Returns 1 when TERM=dumb or when `tput colors` reports fewer than 8.
# Returns 0 when tput is missing (assume colors work, preserving prior behavior).
##
function bashunit::env::supports_color() {
  if [ "${TERM:-}" = "dumb" ]; then
    return 1
  fi

  if ! bashunit::dependencies::has_tput; then
    return 0
  fi

  local n
  n=$(tput colors 2>/dev/null)
  case "$n" in
  '' | *[!0-9]*)
    return 0
    ;;
  *)
    [ "$n" -ge 8 ]
    ;;
  esac
}

function bashunit::env::is_coverage_enabled() {
  [ "$BASHUNIT_COVERAGE" = "true" ]
}

function bashunit::env::is_tap_output_enabled() {
  [ "$BASHUNIT_OUTPUT_FORMAT" = "tap" ]
}

function bashunit::env::is_fail_on_risky_enabled() {
  [ "$BASHUNIT_FAIL_ON_RISKY" = "true" ]
}

function bashunit::env::is_profile_enabled() {
  [ "$BASHUNIT_PROFILE" = "true" ]
}

function bashunit::env::active_internet_connection() {
  if [ "${BASHUNIT_NO_NETWORK:-}" = "true" ]; then
    return 1
  fi

  if command -v curl >/dev/null 2>&1; then
    curl -sfI https://github.com >/dev/null 2>&1 && return 0
  elif command -v wget >/dev/null 2>&1; then
    wget -q --spider https://github.com && return 0
  fi

  if ping -c 1 -W 3 google.com &>/dev/null; then
    return 0
  fi

  return 1
}

function bashunit::env::find_terminal_width() {
  local cols=""

  if [ -z "$cols" ] && command -v tput >/dev/null; then
    cols=$(tput cols 2>/dev/null)
  fi

  if [ -z "$cols" ] && command -v stty >/dev/null; then
    cols=$(stty size 2>/dev/null | cut -d' ' -f2)
  fi

  # Directly echo the value with fallback
  echo "${cols:-100}"
}

function bashunit::env::print_verbose() {
  bashunit::internal_log "Printing verbose environment variables"
  local IFS=$' \t\n'
  # Bash 3.0 compatible: separate declaration and assignment for arrays
  local keys
  keys=(
    "BASHUNIT_DEFAULT_PATH"
    "BASHUNIT_DEV_LOG"
    "BASHUNIT_BOOTSTRAP"
    "BASHUNIT_BOOTSTRAP_ARGS"
    "BASHUNIT_LOG_JUNIT"
    "BASHUNIT_LOG_GHA"
    "BASHUNIT_REPORT_HTML"
    "BASHUNIT_REPORT_TAP"
    "BASHUNIT_PARALLEL_RUN"
    "BASHUNIT_SHOW_HEADER"
    "BASHUNIT_HEADER_ASCII_ART"
    "BASHUNIT_SIMPLE_OUTPUT"
    "BASHUNIT_STOP_ON_FAILURE"
    "BASHUNIT_SHOW_EXECUTION_TIME"
    "BASHUNIT_VERBOSE"
    "BASHUNIT_STRICT_MODE"
    "BASHUNIT_STOP_ON_ASSERTION_FAILURE"
    "BASHUNIT_SKIP_ENV_FILE"
    "BASHUNIT_LOGIN_SHELL"
    "BASHUNIT_COVERAGE"
    "BASHUNIT_COVERAGE_PATHS"
    "BASHUNIT_COVERAGE_EXCLUDE"
    "BASHUNIT_COVERAGE_REPORT"
    "BASHUNIT_COVERAGE_REPORT_HTML"
    "BASHUNIT_COVERAGE_MIN"
  )

  local max_length=0

  local key
  for key in "${keys[@]+"${keys[@]}"}"; do
    if ((${#key} > max_length)); then
      max_length=${#key}
    fi
  done

  for key in "${keys[@]+"${keys[@]}"}"; do
    bashunit::internal_log "$key=${!key}"
    printf "%s:%*s%s\n" "$key" $((max_length - ${#key} + 1)) "" "${!key}"
  done
}

EXIT_CODE_STOP_ON_FAILURE=4
# Use a unique directory per run to avoid conflicts when bashunit is invoked
# recursively or multiple instances are executed in parallel.
TEMP_DIR_PARALLEL_TEST_SUITE="${TMPDIR:-/tmp}/bashunit/parallel/${_BASHUNIT_OS:-Unknown}/$(bashunit::random_str 8)"
TEMP_FILE_PARALLEL_STOP_ON_FAILURE="$TEMP_DIR_PARALLEL_TEST_SUITE/.stop-on-failure"
TERMINAL_WIDTH="$(bashunit::env::find_terminal_width)"
CAT="$(command -v cat)"
GREP="$(command -v grep)"
MKTEMP="$(command -v mktemp)"
# Deferred-output scratch files. Each used to be its own `mktemp` fork; at ~258
# nested cold starts in the acceptance suite that is ~1.5k forks and dominates
# cold-start cost (#798). Derive them from one run-unique directory instead:
# `bashunit::random_str` is fork-free and every consumer appends with `>>` (which
# creates the file lazily) or guards reads with `[ -s ... ]`, so the files need
# not be pre-created. The random suffix keeps the directory unique across
# recursive and parallel invocations, matching TEMP_DIR_PARALLEL_TEST_SUITE.
_BASHUNIT_RUN_OUTPUT_DIR="${TMPDIR:-/tmp}/bashunit/run/${_BASHUNIT_OS:-Unknown}/$(bashunit::random_str 8)"
FAILURES_OUTPUT_PATH="$_BASHUNIT_RUN_OUTPUT_DIR/failures"
SKIPPED_OUTPUT_PATH="$_BASHUNIT_RUN_OUTPUT_DIR/skipped"
INCOMPLETE_OUTPUT_PATH="$_BASHUNIT_RUN_OUTPUT_DIR/incomplete"
RISKY_OUTPUT_PATH="$_BASHUNIT_RUN_OUTPUT_DIR/risky"
PROFILE_OUTPUT_PATH="$_BASHUNIT_RUN_OUTPUT_DIR/profile"
# Collects "<test_file>:<function_name>" for every failing test in a run so the
# next --rerun-failed can replay just those. Shared across parallel subshells.
RERUN_FAILED_OUTPUT_PATH="$_BASHUNIT_RUN_OUTPUT_DIR/rerun-failed"

# Shared temp directory, initialized once at startup for performance.
BASHUNIT_TEMP_DIR="${TMPDIR:-/tmp}/bashunit/tmp"

# Create both scratch directories in a single `mkdir -p` fork.
mkdir -p "$_BASHUNIT_RUN_OUTPUT_DIR" "$BASHUNIT_TEMP_DIR" 2>/dev/null || true

# Removes this run's scratch directory (guarded like parallel::cleanup so a
# broken variable can never turn the rm loose elsewhere). Called at the end of
# a run and on SIGINT; without it every invocation leaks one directory.
function bashunit::env::cleanup_run_output_dir() {
  local target="$_BASHUNIT_RUN_OUTPUT_DIR"
  case "$target" in
  */bashunit/run/*)
    rm -rf "$target"
    ;;
  *)
    bashunit::internal_log "env::cleanup_run_output_dir" "refused unsafe path:$target"
    return 1
    ;;
  esac
}

# Cover early-exit paths (--version, --help, doc, init, ...). The test-run path
# replaces this trap in main.sh and calls the cleanup explicitly instead; child
# subshells never inherit EXIT traps, so a parallel worker cannot remove the
# directory mid-run.
trap 'bashunit::env::cleanup_run_output_dir' EXIT

if bashunit::env::is_dev_mode_enabled; then
  bashunit::internal_log "info" "Dev log enabled" "file:$BASHUNIT_DEV_LOG"
fi

# coverage.sh
# shellcheck disable=SC2094

# Coverage data storage
# Use :- to preserve inherited values from parent bashunit processes
_BASHUNIT_COVERAGE_DATA_FILE="${_BASHUNIT_COVERAGE_DATA_FILE:-}"
_BASHUNIT_COVERAGE_TRACKED_FILES="${_BASHUNIT_COVERAGE_TRACKED_FILES:-}"

# Simple file-based cache for tracked files (Bash 3.0 compatible)
# The tracked cache file stores files that have already been processed
_BASHUNIT_COVERAGE_TRACKED_CACHE_FILE="${_BASHUNIT_COVERAGE_TRACKED_CACHE_FILE:-}"

# File to store which tests hit each line (for detailed coverage tooltips)
_BASHUNIT_COVERAGE_TEST_HITS_FILE="${_BASHUNIT_COVERAGE_TEST_HITS_FILE:-}"

# In-memory buffer for coverage data (reduces file I/O)
_BASHUNIT_COVERAGE_BUFFER=""
_BASHUNIT_COVERAGE_BUFFER_COUNT=0
_BASHUNIT_COVERAGE_BUFFER_LIMIT=100
_BASHUNIT_COVERAGE_HITS_BUFFER=""

# In-memory caches for hot-path lookups (avoids grep + subshells)
_BASHUNIT_COVERAGE_TRACK_CACHE=""
_BASHUNIT_COVERAGE_PATH_CACHE=""
_BASHUNIT_COVERAGE_IS_PARALLEL=""

# Auto-discover coverage paths from test file names
# When no explicit coverage paths are set, find source files matching test file base names
# Example: tests/unit/assert_test.sh -> finds src/assert.sh, src/assert_*.sh
function bashunit::coverage::auto_discover_paths() {
  local project_root
  project_root="$(pwd)"
  local -a discovered_paths=()
  local discovered_paths_count=0
  local test_file

  for test_file in "$@"; do
    # Extract base name: tests/unit/assert_test.sh -> assert_test.sh
    local file_basename
    file_basename=$(basename "$test_file")

    # Remove test suffixes to get source name: assert_test.sh -> assert
    local source_name="${file_basename%_test.sh}"
    [ "$source_name" = "$file_basename" ] && source_name="${file_basename%Test.sh}"
    [ "$source_name" = "$file_basename" ] && continue # Not a test file pattern

    # Find matching source files recursively
    local found_file
    while IFS= read -r -d '' found_file; do
      # Skip test files and vendor directories
      case "$found_file" in
      *test* | *Test* | *vendor* | *node_modules*) continue ;;
      esac
      discovered_paths[discovered_paths_count]="$found_file"
      discovered_paths_count=$((discovered_paths_count + 1))
    done < <(find "$project_root" -name "${source_name}*.sh" -type f -print0 2>/dev/null)
  done

  # Return unique paths, comma-separated
  if [ "$discovered_paths_count" -gt 0 ]; then
    printf '%s\n' "${discovered_paths[@]}" | sort -u | tr '\n' ',' | sed 's/,$//'
  fi
}

function bashunit::coverage::init() {
  if ! bashunit::env::is_coverage_enabled; then
    return 0
  fi

  # Skip coverage init if we're a subprocess of another coverage-enabled bashunit
  # This prevents nested bashunit calls (e.g., in acceptance tests) from
  # interfering with the parent's coverage tracking
  if [ -n "${_BASHUNIT_COVERAGE_DATA_FILE:-}" ]; then
    export BASHUNIT_COVERAGE=false
    return 0
  fi

  # Create coverage data directory with unique name via mktemp -d
  # (avoids $$-$RANDOM collisions and symlink races in shared temp dirs)
  local coverage_dir
  coverage_dir=$("${MKTEMP:-mktemp}" -d "${BASHUNIT_TEMP_DIR:-${TMPDIR:-/tmp}}/bashunit-coverage.XXXXXXXX")

  _BASHUNIT_COVERAGE_DATA_FILE="${coverage_dir}/hits.dat"
  _BASHUNIT_COVERAGE_TRACKED_FILES="${coverage_dir}/files.dat"
  _BASHUNIT_COVERAGE_TRACKED_CACHE_FILE="${coverage_dir}/cache.dat"
  _BASHUNIT_COVERAGE_TEST_HITS_FILE="${coverage_dir}/test_hits.dat"

  # Initialize empty files
  : >"$_BASHUNIT_COVERAGE_DATA_FILE"
  : >"$_BASHUNIT_COVERAGE_TRACKED_FILES"
  : >"$_BASHUNIT_COVERAGE_TRACKED_CACHE_FILE"
  : >"$_BASHUNIT_COVERAGE_TEST_HITS_FILE"

  # Reset in-memory caches and buffers
  _BASHUNIT_COVERAGE_BUFFER=""
  _BASHUNIT_COVERAGE_BUFFER_COUNT=0
  _BASHUNIT_COVERAGE_HITS_BUFFER=""
  _BASHUNIT_COVERAGE_TRACK_CACHE=""
  _BASHUNIT_COVERAGE_PATH_CACHE=""
  _BASHUNIT_COVERAGE_IS_PARALLEL=""
  _BASHUNIT_COVERAGE_STATS_FILES=()
  _BASHUNIT_COVERAGE_STATS_EXEC=()
  _BASHUNIT_COVERAGE_STATS_HIT=()
  _BASHUNIT_COVERAGE_STATS_PCT=()
  _BASHUNIT_COVERAGE_STATS_CLASS=()
  _BASHUNIT_COVERAGE_STATS_COUNT=0
  _BASHUNIT_COVERAGE_STATS_LOOKUP=""

  export _BASHUNIT_COVERAGE_DATA_FILE
  export _BASHUNIT_COVERAGE_TRACKED_FILES
  export _BASHUNIT_COVERAGE_TRACKED_CACHE_FILE
  export _BASHUNIT_COVERAGE_TEST_HITS_FILE
}

function bashunit::coverage::enable_trap() {
  if ! bashunit::env::is_coverage_enabled; then
    return 0
  fi

  # Enable trap inheritance into functions
  set -T

  # Set DEBUG trap to record line execution
  # Use ${VAR:-} to handle unset variables when set -u is active (in subshells)
  # shellcheck disable=SC2154
  trap 'bashunit::coverage::record_line "${BASH_SOURCE[0]:-}" "${LINENO:-}"' DEBUG
}

function bashunit::coverage::disable_trap() {
  trap - DEBUG
  set +T
  # Flush any remaining buffered coverage data
  bashunit::coverage::flush_buffer
}

# Normalize file path to absolute
function bashunit::coverage::normalize_path() {
  local file="$1"

  # Normalize path to absolute
  if [ -f "$file" ]; then
    echo "$(cd "$(dirname "$file")" && pwd)/$(basename "$file")"
  else
    echo "$file"
  fi
}

# Get deduplicated list of tracked files
function bashunit::coverage::get_tracked_files() {
  if [ ! -f "$_BASHUNIT_COVERAGE_TRACKED_FILES" ]; then
    return
  fi
  sort -u "$_BASHUNIT_COVERAGE_TRACKED_FILES"
}

# Get coverage class (high/medium/low) based on percentage
function bashunit::coverage::get_coverage_class() {
  local pct="$1"
  if [ "$pct" -ge "${BASHUNIT_COVERAGE_THRESHOLD_HIGH:-80}" ]; then
    echo "high"
  elif [ "$pct" -ge "${BASHUNIT_COVERAGE_THRESHOLD_LOW:-50}" ]; then
    echo "medium"
  else
    echo "low"
  fi
}

function bashunit::coverage::get_color_for_class() {
  case "$1" in
  high) printf '%s' "$_BASHUNIT_COLOR_PASSED" ;;
  medium) printf '%s' "$_BASHUNIT_COLOR_SKIPPED" ;;
  low) printf '%s' "$_BASHUNIT_COLOR_FAILED" ;;
  esac
}

# Calculate percentage from hit and executable counts
function bashunit::coverage::calculate_percentage() {
  local hit="$1"
  local executable="$2"
  if [ "$executable" -gt 0 ]; then
    echo $((hit * 100 / executable))
  else
    echo "0"
  fi
}

# Get file coverage stats as "executable:hit:pct:class"
function bashunit::coverage::get_file_stats() {
  local file="$1"
  local stats executable hit pct class
  stats=$(bashunit::coverage::compute_file_coverage "$file")
  executable="${stats%%:*}"
  hit="${stats##*:}"
  pct=$(bashunit::coverage::calculate_percentage "$hit" "$executable")
  class=$(bashunit::coverage::get_coverage_class "$pct")
  echo "${executable}:${hit}:${pct}:${class}"
}

# Pre-computed file stats cache (avoids redundant per-file reads across reports)
_BASHUNIT_COVERAGE_STATS_FILES=()
_BASHUNIT_COVERAGE_STATS_EXEC=()
_BASHUNIT_COVERAGE_STATS_HIT=()
_BASHUNIT_COVERAGE_STATS_PCT=()
_BASHUNIT_COVERAGE_STATS_CLASS=()
_BASHUNIT_COVERAGE_STATS_COUNT=0
_BASHUNIT_COVERAGE_STATS_LOOKUP=""

# Pre-compute stats for all tracked files (call once before reports)
function bashunit::coverage::precompute_file_stats() {
  _BASHUNIT_COVERAGE_STATS_FILES=()
  _BASHUNIT_COVERAGE_STATS_EXEC=()
  _BASHUNIT_COVERAGE_STATS_HIT=()
  _BASHUNIT_COVERAGE_STATS_PCT=()
  _BASHUNIT_COVERAGE_STATS_CLASS=()
  _BASHUNIT_COVERAGE_STATS_COUNT=0
  _BASHUNIT_COVERAGE_STATS_LOOKUP=""

  local file
  while IFS= read -r file; do
    { [ -z "$file" ] || [ ! -f "$file" ]; } && continue

    local stats executable hit pct class
    stats=$(bashunit::coverage::compute_file_coverage "$file")
    executable="${stats%%:*}"
    hit="${stats##*:}"
    pct=$(bashunit::coverage::calculate_percentage "$hit" "$executable")
    class=$(bashunit::coverage::get_coverage_class "$pct")

    local idx="$_BASHUNIT_COVERAGE_STATS_COUNT"
    _BASHUNIT_COVERAGE_STATS_FILES[idx]="$file"
    _BASHUNIT_COVERAGE_STATS_EXEC[idx]="$executable"
    _BASHUNIT_COVERAGE_STATS_HIT[idx]="$hit"
    _BASHUNIT_COVERAGE_STATS_PCT[idx]="$pct"
    _BASHUNIT_COVERAGE_STATS_CLASS[idx]="$class"
    _BASHUNIT_COVERAGE_STATS_COUNT=$((idx + 1))
    _BASHUNIT_COVERAGE_STATS_LOOKUP="${_BASHUNIT_COVERAGE_STATS_LOOKUP}|${file}=${idx}|"
  done < <(bashunit::coverage::get_tracked_files)
}

# Look up cached stats for a file, returns "executable:hit:pct:class"
function bashunit::coverage::get_cached_stats() {
  local file="$1"
  case "$_BASHUNIT_COVERAGE_STATS_LOOKUP" in
  *"|${file}="*)
    local idx="${_BASHUNIT_COVERAGE_STATS_LOOKUP#*"|${file}="}"
    idx="${idx%%"|"*}"
    echo "${_BASHUNIT_COVERAGE_STATS_EXEC[idx]}:${_BASHUNIT_COVERAGE_STATS_HIT[idx]}:${_BASHUNIT_COVERAGE_STATS_PCT[idx]}:${_BASHUNIT_COVERAGE_STATS_CLASS[idx]}"
    return 0
    ;;
  esac
  bashunit::coverage::get_file_stats "$file"
}

function bashunit::coverage::record_line() {
  local file="$1"
  local lineno="$2"

  # Skip if no file or line
  { [ -z "$file" ] || [ -z "$lineno" ]; } && return 0

  # Skip if coverage data file doesn't exist (trap inherited by child process)
  [ -z "$_BASHUNIT_COVERAGE_DATA_FILE" ] && return 0

  # Fast in-memory should_track cache (avoids grep + file I/O per line)
  case "$_BASHUNIT_COVERAGE_TRACK_CACHE" in
  *"|${file}:0|"*) return 0 ;;
  *"|${file}:1|"*) ;;
  *)
    # Not cached yet — run full check and cache result
    if bashunit::coverage::should_track "$file"; then
      _BASHUNIT_COVERAGE_TRACK_CACHE="${_BASHUNIT_COVERAGE_TRACK_CACHE}|${file}:1|"
    else
      _BASHUNIT_COVERAGE_TRACK_CACHE="${_BASHUNIT_COVERAGE_TRACK_CACHE}|${file}:0|"
      return 0
    fi
    ;;
  esac

  # Fast in-memory path normalization cache (avoids cd + pwd subshell per line)
  local normalized_file=""
  case "$_BASHUNIT_COVERAGE_PATH_CACHE" in
  *"|${file}="*)
    # Extract cached value
    normalized_file="${_BASHUNIT_COVERAGE_PATH_CACHE#*"|${file}="}"
    normalized_file="${normalized_file%%"|"*}"
    ;;
  *)
    normalized_file=$(bashunit::coverage::normalize_path "$file")
    _BASHUNIT_COVERAGE_PATH_CACHE="${_BASHUNIT_COVERAGE_PATH_CACHE}|${file}=${normalized_file}|"
    ;;
  esac

  # Buffer the coverage data in memory
  _BASHUNIT_COVERAGE_BUFFER="${_BASHUNIT_COVERAGE_BUFFER}${normalized_file}:${lineno}
"
  # Also buffer test hit data if in a test context
  if [ -n "${_BASHUNIT_COVERAGE_CURRENT_TEST_FILE:-}" ] &&
    [ -n "${_BASHUNIT_COVERAGE_CURRENT_TEST_FN:-}" ]; then
    _BASHUNIT_COVERAGE_HITS_BUFFER="${_BASHUNIT_COVERAGE_HITS_BUFFER}${normalized_file}:${lineno}|${_BASHUNIT_COVERAGE_CURRENT_TEST_FILE}:${_BASHUNIT_COVERAGE_CURRENT_TEST_FN}
"
  fi

  _BASHUNIT_COVERAGE_BUFFER_COUNT=$((_BASHUNIT_COVERAGE_BUFFER_COUNT + 1))

  # Flush buffer to disk when threshold is reached
  if [ "$_BASHUNIT_COVERAGE_BUFFER_COUNT" -ge \
    "$_BASHUNIT_COVERAGE_BUFFER_LIMIT" ]; then
    bashunit::coverage::flush_buffer
  fi
}

function bashunit::coverage::flush_buffer() {
  [ -z "$_BASHUNIT_COVERAGE_BUFFER" ] && return 0

  # Determine output files (parallel-safe)
  local data_file="$_BASHUNIT_COVERAGE_DATA_FILE"
  local test_hits_file="$_BASHUNIT_COVERAGE_TEST_HITS_FILE"

  # Cache the parallel check to avoid function calls
  if [ -z "$_BASHUNIT_COVERAGE_IS_PARALLEL" ]; then
    if bashunit::parallel::is_enabled; then
      _BASHUNIT_COVERAGE_IS_PARALLEL="yes"
    else
      _BASHUNIT_COVERAGE_IS_PARALLEL="no"
    fi
  fi

  if [ "$_BASHUNIT_COVERAGE_IS_PARALLEL" = "yes" ]; then
    data_file="${_BASHUNIT_COVERAGE_DATA_FILE}.$$"
    test_hits_file="${_BASHUNIT_COVERAGE_TEST_HITS_FILE}.$$"
  fi

  # Write buffered data in a single I/O operation.
  # Use `builtin printf` so a user test spying/mocking the printf builtin
  # cannot shadow the coverage write and silently drop data (see issue #724).
  builtin printf '%s' "$_BASHUNIT_COVERAGE_BUFFER" >>"$data_file"

  if [ -n "$_BASHUNIT_COVERAGE_HITS_BUFFER" ]; then
    builtin printf '%s' "$_BASHUNIT_COVERAGE_HITS_BUFFER" >>"$test_hits_file"
  fi

  # Reset buffer
  _BASHUNIT_COVERAGE_BUFFER=""
  _BASHUNIT_COVERAGE_HITS_BUFFER=""
  _BASHUNIT_COVERAGE_BUFFER_COUNT=0
}

function bashunit::coverage::should_track() {
  local file="$1"

  # Skip empty paths
  [ -z "$file" ] && return 1

  # Skip if tracked files list doesn't exist (trap inherited by child process)
  [ -z "$_BASHUNIT_COVERAGE_TRACKED_FILES" ] && return 1

  # Check file-based cache for previous decision (Bash 3.0 compatible)
  # Cache format: "file:0" for excluded, "file:1" for tracked
  # In parallel mode, use per-process cache to avoid race conditions
  local cache_file="$_BASHUNIT_COVERAGE_TRACKED_CACHE_FILE"
  if bashunit::parallel::is_enabled && [ -n "$cache_file" ]; then
    cache_file="${cache_file}.$$"
    # Initialize per-process cache if needed
    [ ! -f "$cache_file" ] && [ -d "$(dirname "$cache_file")" ] && : >"$cache_file"
  fi
  if [ -n "$cache_file" ] && [ -f "$cache_file" ]; then
    local cached_decision
    # Use || true to prevent exit in strict mode when grep finds no match
    cached_decision=$(grep "^${file}:" "$cache_file" 2>/dev/null | head -1) || true
    if [ -n "$cached_decision" ]; then
      [ "${cached_decision##*:}" = "1" ] && return 0 || return 1
    fi
  fi

  # Normalize path
  local normalized_file
  normalized_file=$(bashunit::coverage::normalize_path "$file")

  # Check exclusion patterns
  # Save and restore IFS to avoid corrupting caller's environment
  local old_ifs="$IFS"
  IFS=','
  local pattern
  for pattern in $BASHUNIT_COVERAGE_EXCLUDE; do
    # shellcheck disable=SC2254
    case "$normalized_file" in
    *$pattern*)
      IFS="$old_ifs"
      # Cache exclusion decision (use per-process cache in parallel mode)
      { [ -n "$cache_file" ] && [ -f "$cache_file" ]; } && echo "${file}:0" >>"$cache_file"
      return 1
      ;;
    esac
  done

  # Check inclusion paths
  local matched=false
  local path
  for path in $BASHUNIT_COVERAGE_PATHS; do
    # Resolve relative paths
    local resolved_path
    case "$path" in
    /*)
      resolved_path="$path"
      ;;
    *)
      resolved_path="$(pwd)/$path"
      ;;
    esac

    case "$normalized_file" in
    "$resolved_path"*)
      matched=true
      break
      ;;
    esac
  done
  IFS="$old_ifs"

  if [ "$matched" = "false" ]; then
    # Cache exclusion decision (use per-process cache in parallel mode)
    { [ -n "$cache_file" ] && [ -f "$cache_file" ]; } && echo "${file}:0" >>"$cache_file"
    return 1
  fi

  # Cache tracking decision (use per-process cache in parallel mode)
  { [ -n "$cache_file" ] && [ -f "$cache_file" ]; } && echo "${file}:1" >>"$cache_file"

  # Track this file for later reporting
  # In parallel mode, use a per-process file to avoid race conditions
  local tracked_file="$_BASHUNIT_COVERAGE_TRACKED_FILES"
  if bashunit::parallel::is_enabled; then
    tracked_file="${_BASHUNIT_COVERAGE_TRACKED_FILES}.$$"
  fi

  # Only write if parent directory exists
  if [ -d "$(dirname "$tracked_file")" ]; then
    # Check if not already written to avoid duplicates
    if ! grep -q "^${normalized_file}$" "$tracked_file" 2>/dev/null; then
      echo "$normalized_file" >>"$tracked_file"
    fi
  fi

  return 0
}

function bashunit::coverage::aggregate_parallel() {
  # Aggregate per-process coverage files created during parallel execution
  local base_file="$_BASHUNIT_COVERAGE_DATA_FILE"
  local tracked_base="$_BASHUNIT_COVERAGE_TRACKED_FILES"
  local test_hits_base="$_BASHUNIT_COVERAGE_TEST_HITS_FILE"

  # Find and merge all per-process coverage data files
  # Use nullglob to handle case when no files match
  local pid_files pid_file
  pid_files=$(ls -1 "${base_file}."* 2>/dev/null) || true
  if [ -n "$pid_files" ]; then
    while IFS= read -r pid_file; do
      [ -f "$pid_file" ] || continue
      cat "$pid_file" >>"$base_file"
      rm -f "$pid_file"
    done <<<"$pid_files"
  fi

  # Find and merge all per-process tracked files lists
  pid_files=$(ls -1 "${tracked_base}."* 2>/dev/null) || true
  if [ -n "$pid_files" ]; then
    while IFS= read -r pid_file; do
      [ -f "$pid_file" ] || continue
      cat "$pid_file" >>"$tracked_base"
      rm -f "$pid_file"
    done <<<"$pid_files"
  fi

  # Find and merge all per-process test hits files
  if [ -n "$test_hits_base" ]; then
    pid_files=$(ls -1 "${test_hits_base}."* 2>/dev/null) || true
    if [ -n "$pid_files" ]; then
      while IFS= read -r pid_file; do
        [ -f "$pid_file" ] || continue
        cat "$pid_file" >>"$test_hits_base"
        rm -f "$pid_file"
      done <<<"$pid_files"
    fi
  fi

  # Deduplicate tracked files
  if [ -f "$tracked_base" ]; then
    sort -u "$tracked_base" -o "$tracked_base"
  fi
}

# Pre-compiled combined regex of all non-executable line patterns.
# Collapses multiple grep subshells into a single invocation per line for performance.
# Each alternation is fully self-anchored so semantics match the original per-pattern checks.
# Patterns covered (in order):
#   - comment-only lines (including shebang)
#   - function declarations (but not single-line functions with a body)
#   - brace-only lines
#   - control flow keywords (then, else, fi, do, done, esac, in, ;;, ;;&, ;&)
#   - loop terminators with redirection/pipe/fd (e.g. "done < file", "done | sort")
#   - case patterns like "--option)" or "*) # comment"
#   - standalone ) for arrays/subshells
_BASHUNIT_COVERAGE_NONEXEC_PATTERN='^[[:space:]]*#'
_BASHUNIT_COVERAGE_NONEXEC_PATTERN="${_BASHUNIT_COVERAGE_NONEXEC_PATTERN}"'|^[[:space:]]*(function[[:space:]]+)?[a-zA-Z_][a-zA-Z0-9_:]*[[:space:]]*\(\)[[:space:]]*\{?[[:space:]]*$'
_BASHUNIT_COVERAGE_NONEXEC_PATTERN="${_BASHUNIT_COVERAGE_NONEXEC_PATTERN}"'|^[[:space:]]*[\{\}][[:space:]]*$'
_BASHUNIT_COVERAGE_NONEXEC_PATTERN="${_BASHUNIT_COVERAGE_NONEXEC_PATTERN}"'|^[[:space:]]*(then|else|fi|do|done|esac|in|;;|;;&|;&)[[:space:]]*(#.*)?$'
_BASHUNIT_COVERAGE_NONEXEC_PATTERN="${_BASHUNIT_COVERAGE_NONEXEC_PATTERN}"'|^[[:space:]]*done[[:space:]]+[^[:space:]#].*$'
_BASHUNIT_COVERAGE_NONEXEC_PATTERN="${_BASHUNIT_COVERAGE_NONEXEC_PATTERN}"'|^[[:space:]]*[^\)]+\)[[:space:]]*(#.*)?$'
_BASHUNIT_COVERAGE_NONEXEC_PATTERN="${_BASHUNIT_COVERAGE_NONEXEC_PATTERN}"'|^[[:space:]]*\)[[:space:]]*(#.*)?$'

# Check if a line is executable (used by get_executable_lines and report_lcov)
# Arguments: line content, line number
# Returns: 0 if executable, 1 if not
function bashunit::coverage::is_executable_line() {
  local line="$1"
  local lineno="$2"

  # Unused but kept for API compatibility
  : "$lineno"

  # Skip empty lines (line with only whitespace) — built-in, no subshell
  [ -z "${line// /}" ] && return 1

  # Fast path: pure Bash checks for common non-executable patterns (no subshell)
  local stripped="${line#"${line%%[![:space:]]*}"}"
  local _trail="${stripped##*[![:space:]]}"
  local trimmed="${stripped%"$_trail"}"

  case "$trimmed" in
  '#'*) return 1 ;;      # Comments (including shebang)
  '{' | '}') return 1 ;; # Braces only
  esac

  local first="${trimmed%%[[:space:]]*}"
  case "$first" in
  'then' | 'else' | 'fi' | 'do' | 'done' | 'esac' | 'in' | ';;' | ';;&' | ';&' | ')')
    local rest="${trimmed#"$first"}"
    local _rl="${rest%%[![:space:]]*}"
    rest="${rest#"$_rl"}"
    case "$rest" in '' | '#'*) return 1 ;; esac
    ;;
  esac

  # Fallback: grep for complex patterns (function declarations, case patterns, done+redirection)
  [ "$(printf '%s' "$line" | "$GREP" -cE "$_BASHUNIT_COVERAGE_NONEXEC_PATTERN" || true)" -gt 0 ] && return 1

  return 0
}

function bashunit::coverage::get_executable_lines() {
  local file="$1"
  local count=0
  local lineno=0
  local line

  while IFS= read -r line || [ -n "$line" ]; do
    ((++lineno))
    bashunit::coverage::is_executable_line "$line" "$lineno" && ((++count))
  done <"$file"

  echo "$count"
}

function bashunit::coverage::get_hit_lines() {
  local file="$1"

  if [ ! -f "$_BASHUNIT_COVERAGE_DATA_FILE" ]; then
    echo "0"
    return
  fi

  # Get unique hit line numbers
  local hit_lines
  hit_lines=$( (grep "^${file}:" "$_BASHUNIT_COVERAGE_DATA_FILE" 2>/dev/null || true) |
    cut -d: -f2 | sort -u)

  if [ -z "$hit_lines" ]; then
    echo "0"
    return
  fi

  # Only count hits that correspond to executable lines
  # This prevents >100% coverage when DEBUG trap fires on non-executable lines

  # Pre-load file lines into indexed array (avoids sed per line)
  local -a file_lines=()
  local _idx=0 _fl
  while IFS= read -r _fl || [ -n "$_fl" ]; do
    file_lines[_idx]="$_fl"
    ((++_idx))
  done <"$file"

  local count=0
  local line_num
  for line_num in $hit_lines; do
    local line_content="${file_lines[$((line_num - 1))]:-}"
    [ -z "$line_content" ] && continue
    if bashunit::coverage::is_executable_line "$line_content" "$line_num"; then
      ((++count))
    fi
  done

  echo "$count"
}

function bashunit::coverage::get_line_hits() {
  local file="$1"
  local lineno="$2"

  if [ ! -f "$_BASHUNIT_COVERAGE_DATA_FILE" ]; then
    echo "0"
    return
  fi

  local count
  count=$("$GREP" -c "^${file}:${lineno}$" "$_BASHUNIT_COVERAGE_DATA_FILE" 2>/dev/null) || count=0
  echo "$count"
}

# Compute executable + hit counts for a file in a single source-file pass.
# Reuses get_all_line_hits to avoid scanning the coverage data per line.
# Output format: "executable:hit"
function bashunit::coverage::compute_file_coverage() {
  local file="$1"

  local -a hits_by_line=()
  local hit_lineno hit_count
  while IFS=: read -r hit_lineno hit_count; do
    [ -n "$hit_lineno" ] && hits_by_line[hit_lineno]=$hit_count
  done < <(bashunit::coverage::get_all_line_hits "$file")

  local executable=0 hit=0 lineno=0 line line_hits
  local -a cv_lines=()
  local _cli=0 _cl
  while IFS= read -r _cl || [ -n "$_cl" ]; do
    cv_lines[_cli]="$_cl"
    ((++_cli))
  done <"$file"

  for line in "${cv_lines[@]}"; do
    ((++lineno))
    bashunit::coverage::is_executable_line "$line" "$lineno" || continue
    ((++executable))
    line_hits=${hits_by_line[lineno]:-0}
    [ "$line_hits" -gt 0 ] && ((++hit))
  done

  echo "${executable}:${hit}"
}

# Detect whether a source line ends with a Bash line-continuation, i.e. an
# odd number of unescaped trailing backslashes with no trailing whitespace.
# Comment lines never continue. Used to propagate coverage hits from a
# statement's starting line to its continuation lines (see #722).
function bashunit::coverage::_ends_with_continuation() {
  local line="$1"
  local lead="${line#"${line%%[![:space:]]*}"}"
  case "$lead" in '#'*) return 1 ;; esac
  local trailing="${line##*[!\\]}"
  case "$line" in *[!\\]*) : ;; *) trailing="$line" ;; esac
  [ $((${#trailing} % 2)) -eq 1 ]
}

# Get all line hits for a file in one pass (performance optimization)
# Output format: one "lineno:count" per line
#
# Bash's DEBUG trap attributes a multi-line statement's execution to the line
# where the statement starts; backslash continuation lines never receive their
# own hit. To match the report's expectation that continuation lines are
# covered, the start line's count is propagated forward across the
# continuation chain (see #722).
function bashunit::coverage::get_all_line_hits() {
  local file="$1"

  if [ ! -f "$_BASHUNIT_COVERAGE_DATA_FILE" ]; then
    return
  fi

  # Extract all lines for this file, count occurrences of each line number.
  local -a counts=()
  local count lineno maxln=0
  while read -r count lineno; do
    if [ -n "$lineno" ]; then
      counts[lineno]=$count
      [ "$lineno" -gt "$maxln" ] && maxln=$lineno
    fi
  done < <(grep "^${file}:" "$_BASHUNIT_COVERAGE_DATA_FILE" 2>/dev/null |
    cut -d: -f2 | sort | uniq -c)

  if [ "$maxln" -eq 0 ]; then
    return
  fi

  # Read the source so continuation lines can be detected.
  local -a src=()
  local _i=0 _l
  while IFS= read -r _l || [ -n "$_l" ]; do
    src[_i]="$_l"
    ((++_i))
  done <"$file"

  local total=$_i
  [ "$maxln" -gt "$total" ] && total=$maxln

  # Propagate each start line's count forward across its continuation chain.
  local carry=0 idx h
  for ((idx = 1; idx <= total; idx++)); do
    h=${counts[idx]:-0}
    if [ "$carry" -gt 0 ] && [ "$h" -lt "$carry" ]; then
      h=$carry
      counts[idx]=$h
    fi
    if [ "$h" -gt 0 ] && bashunit::coverage::_ends_with_continuation "${src[idx - 1]:-}"; then
      carry=$h
    else
      carry=0
    fi
  done

  local ln
  for ((ln = 1; ln <= total; ln++)); do
    [ "${counts[ln]:-0}" -gt 0 ] && echo "${ln}:${counts[ln]}"
  done

  # The for loop's exit status leaks the last `[ -gt ]` test, which is 1 when the
  # final line has no hits; return 0 explicitly so callers under `set -e` (strict
  # mode) don't treat a successful run as a failure (see #722).
  return 0
}

# Get all test hits for a file in one pass (performance optimization)
# Output format: lineno|test_file:test_function (may have duplicates, one per hit)
function bashunit::coverage::get_all_line_tests() {
  local file="$1"

  if [ ! -f "${_BASHUNIT_COVERAGE_TEST_HITS_FILE:-}" ]; then
    return
  fi

  # Format in file: source_file:line|test_file:test_function
  # Output: lineno|test_file:test_function
  grep "^${file}:" "$_BASHUNIT_COVERAGE_TEST_HITS_FILE" 2>/dev/null |
    sed "s|^${file}:||" | sort -u
}

# Extract function definitions from a bash file
# Output format: function_name:start_line:end_line (one per function)
function bashunit::coverage::extract_functions() {
  local file="$1"

  local lineno=0
  local in_function=0
  local brace_count=0
  local current_fn=""
  local fn_start=0
  local line

  while IFS= read -r line || [ -n "$line" ]; do
    ((++lineno))

    # Check for function definition patterns
    # Pattern 1: function name() { or function name {
    # Pattern 2: name() { or name () {
    if [ "$in_function" -eq 0 ]; then
      local fn_name=""

      # Extract function name using pure Bash string operations (avoids sed subshell)
      local stripped="${line#"${line%%[![:space:]]*}"}"

      # Strip "function " prefix if present
      case "$stripped" in
      function[\ \	]*)
        stripped="${stripped#function}"
        stripped="${stripped#"${stripped%%[![:space:]]*}"}"
        ;;
      esac

      # Extract first word as candidate function name
      fn_name="${stripped%%[[:space:]\(\{]*}"

      # Validate: must start with valid identifier char, and rest must have () or {
      if [ -n "$fn_name" ]; then
        case "$fn_name" in
        [a-zA-Z_]*)
          local after_name="${stripped#"$fn_name"}"
          after_name="${after_name#"${after_name%%[![:space:]]*}"}"
          case "$after_name" in
          '()'* | '{'*) ;;
          *) fn_name="" ;;
          esac
          ;;
        *) fn_name="" ;;
        esac
      fi

      if [ -n "$fn_name" ]; then
        in_function=1
        current_fn="$fn_name"
        fn_start=$lineno
        brace_count=0

        # Count opening braces on this line
        local open_braces="${line//[^\{]/}"
        local close_braces="${line//[^\}]/}"
        local open_count=${#open_braces}
        local close_count=${#close_braces}
        brace_count=$((brace_count + open_count - close_count))

        # Single-line function: braces balance on same line and both present
        if [ "$brace_count" -eq 0 ] && [ "$open_count" -gt 0 ] && [ "$close_count" -gt 0 ]; then
          echo "${current_fn}|${fn_start}|${lineno}"
          in_function=0
          current_fn=""
        fi
        continue
      fi
    fi

    # Track braces inside function
    if [ "$in_function" -eq 1 ]; then
      local open_braces="${line//[^\{]/}"
      local close_braces="${line//[^\}]/}"
      brace_count=$((brace_count + ${#open_braces} - ${#close_braces}))

      # Function ended
      if [ "$brace_count" -le 0 ]; then
        echo "${current_fn}|${fn_start}|${lineno}"
        in_function=0
        current_fn=""
        brace_count=0
      fi
    fi
  done <"$file"

  # Handle unclosed function (shouldn't happen in valid code)
  if [ "$in_function" -eq 1 ] && [ -n "$current_fn" ]; then
    echo "${current_fn}|${fn_start}|${lineno}"
  fi
}

# Append "start:end" to a comma-separated arms string. Result is
# returned via the global _BASHUNIT_BRANCH_ARMS_OUT to avoid the cost
# of a subshell on a hot per-line path. Bash 3.0 cannot pass arrays
# (or namerefs) by reference, so a single output slot is the cheapest
# portable option.
_BASHUNIT_BRANCH_ARMS_OUT=""
function bashunit::coverage::_append_arm() {
  local existing="$1" arm_start="$2" arm_end="$3"
  if [ -z "$existing" ]; then
    _BASHUNIT_BRANCH_ARMS_OUT="${arm_start}:${arm_end}"
  else
    _BASHUNIT_BRANCH_ARMS_OUT="${existing},${arm_start}:${arm_end}"
  fi
}

# Detect whether a trimmed line is a case-pattern opener (ends with
# `)` optionally followed by whitespace and a comment). Avoids
# matching mid-line uses such as `cmd $(other)`.
function bashunit::coverage::_is_case_pattern_line() {
  local trimmed="$1"
  case "$trimmed" in
  *')'*) ;;
  *) return 1 ;;
  esac

  local before_paren="${trimmed%%')'*}"
  local after="${trimmed#"$before_paren"}"
  after="${after#)}"
  after="${after#"${after%%[![:space:]]*}"}"
  case "$after" in
  '' | '#'*) return 0 ;;
  esac
  return 1
}

# Extract branch points from a Bash file.
# Output format: <decision_line>|<kind>|<arm_start>:<arm_end>[,<arm_start>:<arm_end>]...
# kind ∈ {if, case}
# Scope: if/elif/else chains and case patterns. See adrs/adr-007-branch-coverage-mvp.md.
# The handlers below operate on the per-construct state arrays that
# extract_branches keeps as locals. Bash 3.0 has dynamic scoping for
# `local` vars, so the helpers see and mutate the caller's state
# without needing namerefs (which would require Bash 4.3+).

function bashunit::coverage::_branch_push_if() {
  local lineno=$1
  if_decision_line[if_depth]=$lineno
  if_arms[if_depth]=""
  if_arm_start[if_depth]=$((lineno + 1))
  if_depth=$((if_depth + 1))
}

function bashunit::coverage::_branch_close_if_arm() {
  local lineno=$1 idx=$((if_depth - 1))
  bashunit::coverage::_append_arm \
    "${if_arms[$idx]}" "${if_arm_start[$idx]}" "$((lineno - 1))"
  if_arms[idx]="$_BASHUNIT_BRANCH_ARMS_OUT"
  if_arm_start[idx]=$((lineno + 1))
}

function bashunit::coverage::_branch_emit_if() {
  local lineno=$1 idx=$((if_depth - 1))
  bashunit::coverage::_append_arm \
    "${if_arms[$idx]}" "${if_arm_start[$idx]}" "$((lineno - 1))"
  echo "${if_decision_line[$idx]}|if|${_BASHUNIT_BRANCH_ARMS_OUT}"
  if_depth=$idx
}

function bashunit::coverage::_branch_push_case() {
  local lineno=$1
  case_decision_line[case_depth]=$lineno
  case_arms[case_depth]=""
  case_arm_start[case_depth]=0
  case_in_pattern[case_depth]=0
  case_depth=$((case_depth + 1))
}

function bashunit::coverage::_branch_close_case_arm() {
  local lineno=$1 idx=$((case_depth - 1))
  [ "${case_in_pattern[$idx]}" = "1" ] || return 0
  bashunit::coverage::_append_arm \
    "${case_arms[$idx]}" "${case_arm_start[$idx]}" "$((lineno - 1))"
  case_arms[idx]="$_BASHUNIT_BRANCH_ARMS_OUT"
  case_in_pattern[idx]=0
}

function bashunit::coverage::_branch_emit_case() {
  local lineno=$1 idx=$((case_depth - 1))
  bashunit::coverage::_branch_close_case_arm "$lineno"
  if [ -n "${case_arms[$idx]}" ]; then
    echo "${case_decision_line[$idx]}|case|${case_arms[$idx]}"
  fi
  case_depth=$idx
}

function bashunit::coverage::_branch_open_case_pattern() {
  local lineno=$1 idx=$((case_depth - 1))
  case_arm_start[idx]=$((lineno + 1))
  case_in_pattern[idx]=1
}

function bashunit::coverage::extract_branches() {
  local file="$1"

  local -a lines=()
  local _i=0 _l
  while IFS= read -r _l || [ -n "$_l" ]; do
    lines[_i]="$_l"
    ((++_i))
  done <"$file"
  local total_lines=$_i

  # State arrays — read and mutated by the _branch_* helpers via Bash's
  # dynamic scoping. Each array is keyed by depth so nested constructs
  # work without associative arrays.
  local -a if_decision_line=() if_arms=() if_arm_start=()
  local if_depth=0
  local -a case_decision_line=() case_arms=() case_arm_start=() case_in_pattern=()
  local case_depth=0

  local lineno=0 line trimmed first
  while [ "$lineno" -lt "$total_lines" ]; do
    line="${lines[$lineno]}"
    lineno=$((lineno + 1))

    trimmed="${line#"${line%%[![:space:]]*}"}"
    case "$trimmed" in '' | '#'*) continue ;; esac
    first="${trimmed%%[[:space:]\;]*}"

    # Reserved-word patterns single-quoted to dodge `case ... esac`
    # parser confusion.
    case "$first" in
    'if') bashunit::coverage::_branch_push_if "$lineno" ;;
    'elif' | 'else')
      [ "$if_depth" -gt 0 ] && bashunit::coverage::_branch_close_if_arm "$lineno"
      ;;
    'fi')
      [ "$if_depth" -gt 0 ] && bashunit::coverage::_branch_emit_if "$lineno"
      ;;
    'case') bashunit::coverage::_branch_push_case "$lineno" ;;
    'esac')
      [ "$case_depth" -gt 0 ] && bashunit::coverage::_branch_emit_case "$lineno"
      ;;
    *)
      [ "$case_depth" -eq 0 ] && continue
      case "$trimmed" in
      ';;&'* | ';;'* | ';&'*)
        bashunit::coverage::_branch_close_case_arm "$lineno"
        ;;
      *)
        if bashunit::coverage::_is_case_pattern_line "$trimmed"; then
          bashunit::coverage::_branch_open_case_pattern "$lineno"
        fi
        ;;
      esac
      ;;
    esac
  done
}

# Sets _BASHUNIT_ARM_TAKEN_OUT to 1 iff any executable line in
# [arm_start..arm_end] has a recorded hit, else 0. Caller must have
# populated the hits_by_line and src_lines arrays in scope; Bash 3.0
# cannot pass arrays into a function. Result is returned via the
# global to avoid a per-arm subshell.
_BASHUNIT_ARM_TAKEN_OUT=0
function bashunit::coverage::_arm_taken() {
  local arm_start="$1" arm_end="$2" ln
  for ((ln = arm_start; ln <= arm_end; ln++)); do
    bashunit::coverage::is_executable_line \
      "${src_lines[$((ln - 1))]:-}" "$ln" || continue
    if [ "${hits_by_line[$ln]:-0}" -gt 0 ]; then
      _BASHUNIT_ARM_TAKEN_OUT=1
      return
    fi
  done
  _BASHUNIT_ARM_TAKEN_OUT=0
}

# Compute branch hit data for a file.
# Output format: <decision_line>|<block>|<branch_index>|<taken_count>
# block = sequential id per decision (0..N-1), branch_index = arm index (0..M-1).
# An arm is "taken" iff at least one executable line inside its range
# has a recorded hit. taken_count is 0 or 1 — MVP does not preserve
# per-arm hit counts.
function bashunit::coverage::compute_branch_hits() {
  local file="$1"

  local -a hits_by_line=()
  local _hl_ln _hl_cnt
  while IFS=: read -r _hl_ln _hl_cnt; do
    [ -n "$_hl_ln" ] && hits_by_line[_hl_ln]=$_hl_cnt
  done < <(bashunit::coverage::get_all_line_hits "$file")

  local -a src_lines=()
  local _sli=0 _sl
  while IFS= read -r _sl || [ -n "$_sl" ]; do
    src_lines[_sli]="$_sl"
    ((++_sli))
  done <"$file"

  local block=0 decision_line _kind arms branch_entry
  local -a arm_specs=()
  local arm arm_index
  while IFS= read -r branch_entry; do
    [ -z "$branch_entry" ] && continue
    IFS='|' read -r decision_line _kind arms <<<"$branch_entry"

    arm_index=0
    IFS=',' read -ra arm_specs <<<"$arms"
    for arm in "${arm_specs[@]}"; do
      bashunit::coverage::_arm_taken "${arm%%:*}" "${arm##*:}"
      echo "${decision_line}|${block}|${arm_index}|${_BASHUNIT_ARM_TAKEN_OUT}"
      arm_index=$((arm_index + 1))
    done

    block=$((block + 1))
  done < <(bashunit::coverage::extract_branches "$file")
}

function bashunit::coverage::get_percentage() {
  local total_executable=0
  local total_hit=0

  if [ "$_BASHUNIT_COVERAGE_STATS_COUNT" -gt 0 ]; then
    local i
    for ((i = 0; i < _BASHUNIT_COVERAGE_STATS_COUNT; i++)); do
      total_executable=$((total_executable + _BASHUNIT_COVERAGE_STATS_EXEC[i]))
      total_hit=$((total_hit + _BASHUNIT_COVERAGE_STATS_HIT[i]))
    done
  else
    while IFS= read -r file; do
      { [ -z "$file" ] || [ ! -f "$file" ]; } && continue

      local executable hit
      executable=$(bashunit::coverage::get_executable_lines "$file")
      hit=$(bashunit::coverage::get_hit_lines "$file")

      total_executable=$((total_executable + executable))
      total_hit=$((total_hit + hit))
    done < <(bashunit::coverage::get_tracked_files)
  fi

  bashunit::coverage::calculate_percentage "$total_hit" "$total_executable"
}

function bashunit::coverage::report_text() {
  if ! bashunit::env::is_coverage_enabled; then
    return 0
  fi

  local total_executable=0
  local total_hit=0
  local has_files=false

  echo ""
  echo "Coverage Report"
  echo "---------------"

  local file
  while IFS= read -r file; do
    { [ -z "$file" ] || [ ! -f "$file" ]; } && continue
    has_files=true

    local executable hit pct class stats rest
    stats=$(bashunit::coverage::get_cached_stats "$file")
    executable="${stats%%:*}"
    rest="${stats#*:}"
    hit="${rest%%:*}"
    rest="${rest#*:}"
    pct="${rest%%:*}"
    class="${rest#*:}"

    total_executable=$((total_executable + executable))
    total_hit=$((total_hit + hit))

    local color reset="$_BASHUNIT_COLOR_DEFAULT"
    color=$(bashunit::coverage::get_color_for_class "$class")

    # Display relative path
    local display_file="${file#"$(pwd)"/}"
    printf "%s%-40s %3d/%3d lines (%3d%%)%s\n" \
      "$color" "$display_file" "$hit" "$executable" "$pct" "$reset"
  done < <(bashunit::coverage::get_tracked_files)

  if [ "$has_files" != "true" ]; then
    echo "---------------"
    echo "Total: 0/0 (0%)"
    return 0
  fi

  echo "---------------"

  # Total
  local total_pct total_class
  total_pct=$(bashunit::coverage::calculate_percentage "$total_hit" "$total_executable")
  total_class=$(bashunit::coverage::get_coverage_class "$total_pct")

  local color reset="$_BASHUNIT_COLOR_DEFAULT"
  color=$(bashunit::coverage::get_color_for_class "$total_class")

  printf "%sTotal: %d/%d (%d%%)%s\n" \
    "$color" "$total_hit" "$total_executable" "$total_pct" "$reset"

  # Optional per-function summary (gated on BASHUNIT_COVERAGE_SHOW_FUNCTIONS)
  if [ "${BASHUNIT_COVERAGE_SHOW_FUNCTIONS:-false}" = "true" ]; then
    bashunit::coverage::report_text_functions
  fi

  # Optional uncovered hotspots (gated on BASHUNIT_COVERAGE_SHOW_UNCOVERED)
  if [ "${BASHUNIT_COVERAGE_SHOW_UNCOVERED:-false}" = "true" ]; then
    bashunit::coverage::report_text_uncovered
  fi

  # Show report location if generated
  if [ -n "$BASHUNIT_COVERAGE_REPORT" ]; then
    echo ""
    echo "Coverage report written to: $BASHUNIT_COVERAGE_REPORT"
  fi
}

# Compress a sorted list of integers into a comma-separated range
# string (e.g. "3 4 5 7 9 10" -> "3-5,7,9-10"). Result on
# _BASHUNIT_RANGES_OUT to avoid a subshell on each call.
_BASHUNIT_RANGES_OUT=""
function bashunit::coverage::_compress_ranges() {
  local out="" start="" end="" n
  for n in "$@"; do
    if [ -z "$start" ]; then
      start="$n"
      end="$n"
    elif [ "$n" -eq $((end + 1)) ]; then
      end="$n"
    else
      if [ "$start" = "$end" ]; then
        out="${out}${start},"
      else
        out="${out}${start}-${end},"
      fi
      start="$n"
      end="$n"
    fi
  done
  if [ -n "$start" ]; then
    if [ "$start" = "$end" ]; then
      out="${out}${start}"
    else
      out="${out}${start}-${end}"
    fi
  fi
  _BASHUNIT_RANGES_OUT="${out%,}"
}

# List executable lines that were never hit, grouped by file.
# Gated on BASHUNIT_COVERAGE_SHOW_UNCOVERED=true. Output is suppressed
# when no uncovered lines exist so a fully-covered run stays quiet.
function bashunit::coverage::report_text_uncovered() {
  local file
  local printed_header=false
  while IFS= read -r file; do
    { [ -z "$file" ] || [ ! -f "$file" ]; } && continue

    local -a hits_by_line=()
    local _hl_ln _hl_cnt
    while IFS=: read -r _hl_ln _hl_cnt; do
      [ -n "$_hl_ln" ] && hits_by_line[_hl_ln]=$_hl_cnt
    done < <(bashunit::coverage::get_all_line_hits "$file")

    local -a uncovered_lines=()
    local _ucount=0
    local lineno=0 line
    while IFS= read -r line || [ -n "$line" ]; do
      lineno=$((lineno + 1))
      bashunit::coverage::is_executable_line "$line" "$lineno" || continue
      local lh="${hits_by_line[$lineno]:-0}"
      if [ "$lh" -eq 0 ]; then
        uncovered_lines[_ucount]="$lineno"
        _ucount=$((_ucount + 1))
      fi
    done <"$file"

    [ "$_ucount" -eq 0 ] && continue

    if [ "$printed_header" != "true" ]; then
      echo ""
      echo "Uncovered Lines"
      echo "---------------"
      printed_header=true
    fi

    local display_file="${file#"$(pwd)"/}"
    local color="$_BASHUNIT_COLOR_FAILED" reset="$_BASHUNIT_COLOR_DEFAULT"
    local out
    bashunit::coverage::_compress_ranges "${uncovered_lines[@]}"
    out="$_BASHUNIT_RANGES_OUT"

    printf "%s%s:%s%s\n" "$color" "$display_file" "$out" "$reset"
  done < <(bashunit::coverage::get_tracked_files)
}

# Per-function coverage summary printed after the file table.
# Gated on BASHUNIT_COVERAGE_SHOW_FUNCTIONS=true to keep default output compact.
function bashunit::coverage::report_text_functions() {
  local file
  local printed_header=false
  while IFS= read -r file; do
    { [ -z "$file" ] || [ ! -f "$file" ]; } && continue

    local functions_data
    functions_data=$(bashunit::coverage::extract_functions "$file")
    [ -z "$functions_data" ] && continue

    local -a hits_by_line=()
    local _hl_ln _hl_cnt
    while IFS=: read -r _hl_ln _hl_cnt; do
      [ -n "$_hl_ln" ] && hits_by_line[_hl_ln]=$_hl_cnt
    done < <(bashunit::coverage::get_all_line_hits "$file")

    local -a file_lines=()
    local _fli=0 _fl
    while IFS= read -r _fl || [ -n "$_fl" ]; do
      file_lines[_fli]="$_fl"
      ((++_fli))
    done <"$file"

    local display_file="${file#"$(pwd)"/}"

    if [ "$printed_header" != "true" ]; then
      echo ""
      echo "Functions"
      echo "---------"
      printed_header=true
    fi
    echo "${display_file}"

    local fn_name fn_start fn_end ln fn_executable fn_hit
    local fn_pct fn_class color reset="$_BASHUNIT_COLOR_DEFAULT"
    while IFS='|' read -r fn_name fn_start fn_end; do
      [ -z "$fn_name" ] && continue

      fn_executable=0
      fn_hit=0
      for ((ln = fn_start; ln <= fn_end; ln++)); do
        bashunit::coverage::is_executable_line \
          "${file_lines[$((ln - 1))]:-}" "$ln" || continue
        fn_executable=$((fn_executable + 1))
        [ "${hits_by_line[$ln]:-0}" -gt 0 ] && fn_hit=$((fn_hit + 1))
      done

      fn_pct=$(bashunit::coverage::calculate_percentage "$fn_hit" "$fn_executable")
      fn_class=$(bashunit::coverage::get_coverage_class "$fn_pct")
      color=$(bashunit::coverage::get_color_for_class "$fn_class")

      printf "  %s%-38s %3d/%3d lines (%3d%%)%s\n" \
        "$color" "$fn_name" "$fn_hit" "$fn_executable" "$fn_pct" "$reset"
    done <<<"$functions_data"
  done < <(bashunit::coverage::get_tracked_files)
}

function bashunit::coverage::report_lcov() {
  local output_file="${1:-$BASHUNIT_COVERAGE_REPORT}"

  if [ -z "$output_file" ]; then
    return 0
  fi

  # Create output directory if needed
  mkdir -p "$(dirname "$output_file")"

  # Generate LCOV format
  {
    echo "TN:"

    while IFS= read -r file; do
      { [ -z "$file" ] || [ ! -f "$file" ]; } && continue

      echo "SF:$file"

      local -a hits_by_line=()
      local hit_lineno hit_count
      while IFS=: read -r hit_lineno hit_count; do
        [ -n "$hit_lineno" ] && hits_by_line[hit_lineno]=$hit_count
      done < <(bashunit::coverage::get_all_line_hits "$file")

      # Function records (FN/FNDA/FNF/FNH). Emit FN lines as we walk
      # and buffer the matching FNDA lines for emission after, per
      # LCOV convention.
      local fn_total=0 fn_hit=0 fn_name fn_start fn_end fln any_hit
      local -a fn_dn_records=()
      local _fdi=0
      while IFS='|' read -r fn_name fn_start fn_end; do
        [ -z "$fn_name" ] && continue
        echo "FN:${fn_start},${fn_name}"
        fn_total=$((fn_total + 1))

        any_hit=0
        for ((fln = fn_start; fln <= fn_end; fln++)); do
          if [ "${hits_by_line[$fln]:-0}" -gt 0 ]; then
            any_hit=1
            break
          fi
        done
        fn_dn_records[_fdi]="FNDA:${any_hit},${fn_name}"
        _fdi=$((_fdi + 1))
        [ "$any_hit" -eq 1 ] && fn_hit=$((fn_hit + 1))
      done < <(bashunit::coverage::extract_functions "$file")

      local fda
      for fda in ${fn_dn_records[@]+"${fn_dn_records[@]}"}; do
        echo "$fda"
      done
      echo "FNF:$fn_total"
      echo "FNH:$fn_hit"

      # Branch records (BRDA/BRF/BRH)
      local br_total=0 br_hit=0 br_line br_block br_idx br_taken
      while IFS='|' read -r br_line br_block br_idx br_taken; do
        [ -z "$br_line" ] && continue
        echo "BRDA:${br_line},${br_block},${br_idx},${br_taken}"
        br_total=$((br_total + 1))
        [ "$br_taken" -gt 0 ] && br_hit=$((br_hit + 1))
      done < <(bashunit::coverage::compute_branch_hits "$file")
      echo "BRF:$br_total"
      echo "BRH:$br_hit"

      local lineno=0 executable=0 hit=0 line line_hits
      local -a lcov_lines=()
      local _lli=0 _ll
      while IFS= read -r _ll || [ -n "$_ll" ]; do
        lcov_lines[_lli]="$_ll"
        ((++_lli))
      done <"$file"

      for line in "${lcov_lines[@]}"; do
        ((++lineno))
        bashunit::coverage::is_executable_line "$line" "$lineno" || continue
        ((++executable))
        local lh="${hits_by_line[$lineno]:-0}"
        [ "$lh" -gt 0 ] && ((++hit))
        echo "DA:${lineno},${lh}"
      done

      echo "LF:$executable"
      echo "LH:$hit"
      echo "end_of_record"
    done < <(bashunit::coverage::get_tracked_files)
  } >"$output_file"
}

function bashunit::coverage::check_threshold() {
  if [ -z "$BASHUNIT_COVERAGE_MIN" ]; then
    return 0
  fi

  local pct
  pct=$(bashunit::coverage::get_percentage)

  if [ "$pct" -lt "$BASHUNIT_COVERAGE_MIN" ]; then
    printf "%sCoverage %d%% is below minimum %d%%%s\n" \
      "$_BASHUNIT_COLOR_FAILED" "$pct" "$BASHUNIT_COVERAGE_MIN" "$_BASHUNIT_COLOR_DEFAULT"
    return 1
  fi

  return 0
}

# Escape HTML special characters
# Uses sed for cross-version bash compatibility (bash 3.2 vs 4.4+ handle & differently in replacement strings)
function bashunit::coverage::html_escape() {
  local text="$1"
  printf "%s" "$text" | sed "s/&/\&amp;/g; s/</\&lt;/g; s/>/\&gt;/g"
}

# Convert file path to safe filename for HTML
function bashunit::coverage::path_to_filename() {
  local file="$1"
  local display_file="${file#"$(pwd)"/}"
  # Replace / with _ and . with _
  local safe_name="${display_file//\//_}"
  echo "${safe_name//./_}"
}

function bashunit::coverage::report_html() {
  local output_dir="${1:-coverage/html}"

  if [ -z "$output_dir" ]; then
    return 0
  fi

  # Create output directory structure
  mkdir -p "$output_dir/files"

  # Collect file data for index
  local IFS=$' \t\n'
  local total_executable=0
  local total_hit=0
  local -a file_data=()
  local file_data_count=0
  local file=""

  while IFS= read -r file; do
    { [ -z "$file" ] || [ ! -f "$file" ]; } && continue

    local stats executable hit pct
    stats=$(bashunit::coverage::get_cached_stats "$file")
    executable="${stats%%:*}"
    stats="${stats#*:}"
    hit="${stats%%:*}"
    stats="${stats#*:}"
    pct="${stats%%:*}"

    total_executable=$((total_executable + executable))
    total_hit=$((total_hit + hit))

    local display_file="${file#"$(pwd)"/}"
    local safe_filename
    safe_filename=$(bashunit::coverage::path_to_filename "$file")

    file_data[file_data_count]="$display_file|$hit|$executable|$pct|$safe_filename"
    file_data_count=$((file_data_count + 1))

    # Generate individual file HTML
    bashunit::coverage::generate_file_html "$file" "$output_dir/files/${safe_filename}.html"
  done < <(bashunit::coverage::get_tracked_files)

  # Calculate total percentage
  local total_pct
  total_pct=$(bashunit::coverage::calculate_percentage "$total_hit" "$total_executable")

  # Get test results
  local tests_passed tests_failed tests_total
  tests_passed=$(bashunit::state::get_tests_passed)
  tests_failed=$(bashunit::state::get_tests_failed)
  tests_total=$((tests_passed + tests_failed))

  # Generate index.html
  bashunit::coverage::generate_index_html \
    "$output_dir/index.html" "$total_hit" "$total_executable" "$total_pct" \
    "$tests_total" "$tests_passed" "$tests_failed" ${file_data[@]+"${file_data[@]}"}

  echo "Coverage HTML report written to: $output_dir/index.html"
}

function bashunit::coverage::generate_index_html() {
  # Set normal IFS for array operations throughout the function (Bash 3.0/4.3 compatible)
  local IFS=$' \t\n'
  local output_file="$1"
  local total_hit="$2"
  local total_executable="$3"
  local total_pct="$4"
  local tests_total="$5"
  local tests_passed="$6"
  local tests_failed="$7"
  shift 7
  # Handle array passed as arguments - Bash 3.0 compatible
  local -a file_data=()
  local file_count=0
  if [ $# -gt 0 ]; then
    file_data=("$@")
    file_count=$#
  fi

  # Calculate uncovered lines and file count
  local total_uncovered=$((total_executable - total_hit))

  # Calculate gauge stroke offset (440 is full circle circumference)
  local gauge_offset=$((440 - (440 * total_pct / 100)))

  # Determine coverage level and colors for gauge
  local total_class gauge_color_start gauge_color_end gauge_text_gradient
  total_class=$(bashunit::coverage::get_coverage_class "$total_pct")
  case "$total_class" in
  high)
    gauge_color_start="#10b981"
    gauge_color_end="#34d399"
    gauge_text_gradient="linear-gradient(135deg, #10b981 0%, #34d399 100%)"
    ;;
  medium)
    gauge_color_start="#f59e0b"
    gauge_color_end="#fbbf24"
    gauge_text_gradient="linear-gradient(135deg, #f59e0b 0%, #fbbf24 100%)"
    ;;
  low)
    gauge_color_start="#ef4444"
    gauge_color_end="#f87171"
    gauge_text_gradient="linear-gradient(135deg, #ef4444 0%, #f87171 100%)"
    ;;
  esac

  {
    cat <<'EOF'
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Coverage Report | bashunit</title>
  <style>
    :root {
      --primary: #6366f1; --primary-dark: #4f46e5; --primary-light: #818cf8;
      --success: #10b981; --success-light: #34d399;
      --warning: #f59e0b; --warning-light: #fbbf24;
      --danger: #ef4444; --danger-light: #f87171;
      --bg-light: #ffffff; --bg-card: #f8fafc; --bg-hover: #f1f5f9;
      --text-primary: #0f172a; --text-secondary: #475569; --text-muted: #94a3b8;
      --border: #e2e8f0;
    }
    * { margin: 0; padding: 0; box-sizing: border-box; }
    body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; background: var(--bg-light); color: var(--text-primary); min-height: 100vh; line-height: 1.6; }
    .header { background: var(--bg-card); padding: 0; position: relative; overflow: hidden; border-bottom: 1px solid var(--border); }
    .header-content { position: relative; z-index: 1; max-width: 1400px; margin: 0 auto; padding: 40px 30px; }
    .header-top { display: flex; justify-content: space-between; align-items: center; margin-bottom: 30px; }
    .logo { display: flex; align-items: center; gap: 12px; }
    .logo img { width: 40px; height: 40px; }
    .logo-text { font-size: 1.5rem; font-weight: 700; letter-spacing: -0.5px; color: var(--text-primary); }
    .logo-text span { opacity: 0.6; font-weight: 400; }
    .header-badge { background: var(--bg-hover); padding: 8px 16px; border-radius: 20px; font-size: 0.85rem; font-weight: 500; color: var(--text-secondary); }
    .header-title { font-size: 2.5rem; font-weight: 800; margin-bottom: 8px; letter-spacing: -1px; color: var(--text-primary); }
    .header-subtitle { font-size: 1.1rem; opacity: 0.7; color: var(--text-secondary); }
    .main { max-width: 1400px; margin: 0 auto; padding: 40px 30px; }
    .gauge-section { background: var(--bg-card); border-radius: 20px; padding: 40px; margin-bottom: 30px; border: 1px solid var(--border); display: flex; align-items: center; gap: 60px; box-shadow: 0 1px 3px rgba(0,0,0,0.1); }
    .gauge-container { position: relative; width: 200px; height: 200px; flex-shrink: 0; }
    .gauge-bg { fill: none; stroke: #e5e7eb; stroke-width: 20; }
    .gauge-fill { fill: none; stroke: url(#gaugeGradient); stroke-width: 20; stroke-linecap: round; transform: rotate(-90deg); transform-origin: center; animation: gaugeAnimation 1.5s ease-out forwards; }
    @keyframes gaugeAnimation { from { stroke-dashoffset: 440; } }
    @keyframes fadeInUp { from { opacity: 0; } to { opacity: 1 } }
    .gauge-text { position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); text-align: center; width: 100%; }
EOF
    echo "    .gauge-percent { font-size: 3.5rem; font-weight: 800; background: ${gauge_text_gradient}; -webkit-background-clip: text; -webkit-text-fill-color: transparent; background-clip: text; line-height: 1; margin: 0; display: block; }"
    cat <<'EOF'
    .gauge-label { color: var(--text-secondary); font-size: 0.9rem; text-transform: uppercase; letter-spacing: 2px; margin: 0; display: block; }
    .gauge-info { flex: 1; }
    .gauge-title { font-size: 1.8rem; font-weight: 700; margin-bottom: 12px; }
    .gauge-description { color: var(--text-secondary); font-size: 1.05rem; margin-bottom: 24px; line-height: 1.7; }
    .breakdown-item { display: flex; align-items: center; gap: 6px; white-space: nowrap; }
    .breakdown-dot { width: 12px; height: 12px; border-radius: 50%; }
    .breakdown-dot.total { background: #94a3b8; }
    .breakdown-dot.covered { background: var(--success); }
    .breakdown-dot.uncovered { background: var(--danger); }
    .breakdown-dot.files { background: var(--warning); }
    .breakdown-dot.tests { background: #a78bfa; }
    .breakdown-dot.tests-passed { background: var(--success); }
    .breakdown-dot.tests-failed { background: var(--danger); }
    .breakdown-label { color: var(--text-secondary); font-size: 0.9rem; }
    .breakdown-value { font-weight: 600; color: var(--text-primary); }
    .compact-metrics { display: flex; flex-direction: column; gap: 10px; }
    .metrics-group { background: var(--bg-hover); padding: 12px 16px; border-radius: 8px; border-left: 3px solid var(--primary); }
    .metrics-group.coverage-group { border-left-color: var(--success); }
    .metrics-group.test-group { border-left-color: #a78bfa; }
    .metrics-group-title { font-size: 0.8rem; font-weight: 600; color: var(--text-secondary); text-transform: uppercase; letter-spacing: 0.5px; margin-bottom: 8px; }
    .metrics-inline { display: flex; gap: 16px; flex-wrap: wrap; align-items: center; font-size: 0.9rem; }
    .metrics-inline .breakdown-item { margin: 0; }
    .section-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 24px; flex-wrap: wrap; gap: 16px; }
    .section-title { font-size: 1.5rem; font-weight: 700; display: flex; align-items: center; gap: 12px; }
    .legend { display: flex; gap: 20px; background: #f1f5f9; padding: 12px 20px; border-radius: 10px; }
    .legend-item { display: flex; align-items: center; gap: 8px; font-size: 0.85rem; color: var(--text-secondary); pointer-events: none; }
    .legend-color { width: 16px; height: 16px; border-radius: 4px; }
    .legend-color.high { background: var(--success); }
    .legend-color.medium { background: var(--warning); }
    .legend-color.low { background: var(--danger); }
    .files-table { background: var(--bg-card); border-radius: 16px; overflow: hidden; border: 1px solid var(--border); box-shadow: 0 1px 3px rgba(0,0,0,0.1); }
    .files-table table { width: 100%; border-collapse: collapse; }
    .files-table th { background: #f8fafc; padding: 16px 24px; text-align: left; font-weight: 600; color: var(--text-secondary); font-size: 0.85rem; text-transform: uppercase; letter-spacing: 1px; border-bottom: 1px solid var(--border); }
    .files-table td { padding: 20px 24px; border-bottom: 1px solid var(--border); vertical-align: middle; }
    .files-table tr:last-child td { border-bottom: none; }
    .files-table tbody tr { transition: all 0.2s ease; animation: fadeInUp 0.5s ease-out forwards; opacity: 0; cursor: pointer; }
    .files-table tbody tr:hover { background: var(--bg-hover); }
    .file-info { display: flex; flex-direction: column; gap: 4px; }
    .file-name { font-weight: 600; color: var(--text-primary); text-decoration: none; font-size: 1rem; transition: color 0.2s; }
    .file-name:hover { color: var(--primary-light); }
    .file-path { color: var(--text-muted); font-size: 0.85rem; font-family: 'SF Mono', 'Consolas', 'Liberation Mono', Menlo, monospace; }
    .lines-info { text-align: center; }
    .lines-covered { font-weight: 700; font-size: 1.1rem; color: var(--text-primary); }
    .lines-total { color: var(--text-muted); font-size: 0.85rem; }
    .coverage-cell { width: 200px; }
    .coverage-bar-container { display: flex; align-items: center; gap: 16px; }
    .coverage-bar { flex: 1; height: 10px; background: var(--bg-hover); border-radius: 5px; overflow: hidden; }
    .coverage-bar-fill { height: 100%; border-radius: 5px; transition: width 1s ease-out; }
    .coverage-bar-fill.high { background: linear-gradient(90deg, var(--success) 0%, var(--success-light) 100%); }
    .coverage-bar-fill.medium { background: linear-gradient(90deg, var(--warning) 0%, var(--warning-light) 100%); }
    .coverage-bar-fill.low { background: linear-gradient(90deg, var(--danger) 0%, var(--danger-light) 100%); }
    .coverage-percent { font-weight: 700; font-size: 1rem; min-width: 50px; text-align: right; }
    .coverage-percent.high { color: var(--success); }
    .coverage-percent.medium { color: var(--warning); }
    .coverage-percent.low { color: var(--danger); }
    .view-btn { display: inline-flex; align-items: center; gap: 8px; padding: 10px 20px; background: var(--bg-hover); border: 1px solid var(--border); border-radius: 8px; color: var(--text-primary); text-decoration: none; font-size: 0.9rem; font-weight: 500; transition: all 0.2s; }
    .view-btn:hover { background: var(--primary); border-color: var(--primary); }
    .footer { max-width: 1400px; margin: 0 auto; padding: 40px 30px; text-align: center; border-top: 1px solid var(--border); }
    .footer-content { display: flex; justify-content: center; align-items: center; gap: 10px; flex-wrap: wrap; }
    .footer-text { color: var(--text-muted); font-size: 0.9rem; }
    .footer-link { color: var(--primary-light); text-decoration: none; font-weight: 500; transition: color 0.2s; }
    .footer-link:hover { color: var(--primary); }
    .footer-divider { width: 4px; height: 4px; background: var(--text-muted); border-radius: 50%; }
    @media (max-width: 768px) {
      .header-content { padding: 30px 20px; } .header-title { font-size: 1.8rem; }
      .main { padding: 30px 20px; }
      .gauge-section { flex-direction: column; padding: 30px; gap: 30px; }
      .gauge-container { width: 160px; height: 160px; }
      .gauge-text { position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); width: 100%; }
      .gauge-percent { font-size: 2.5rem; line-height: 1; margin: 0; }
      .gauge-label { font-size: 0.75rem; letter-spacing: 1.5px; margin: 0; }
      .metrics-inline { flex-direction: column; gap: 10px; align-items: flex-start; }
      .files-table th, .files-table td { padding: 15px; }
      .coverage-cell { width: auto; }
      .coverage-bar-container { flex-direction: column; align-items: flex-start; gap: 8px; }
      .coverage-bar { width: 100%; }
    }
  </style>
</head>
<body>
  <header class="header">
    <div class="header-content">
      <div class="header-top">
        <div class="logo">
          <img src="https://bashunit.com/logo.svg" alt="bashunit">
          <div class="logo-text">bashunit <span>coverage</span></div>
        </div>
EOF
    echo "        <div class=\"header-badge\">v${BASHUNIT_VERSION:-0.0.0}</div>"
    cat <<'EOF'
      </div>
      <h1 class="header-title">Code Coverage Report</h1>
      <p class="header-subtitle">Comprehensive line-by-line coverage analysis for your bash scripts</p>
    </div>
  </header>
  <main class="main">
    <section class="gauge-section">
      <div class="gauge-container">
        <svg viewBox="0 0 160 160" width="200" height="200">
          <defs>
            <linearGradient id="gaugeGradient" x1="0%" y1="0%" x2="100%" y2="0%">
EOF
    echo "              <stop offset=\"0%\" style=\"stop-color:${gauge_color_start}\"/>"
    echo "              <stop offset=\"100%\" style=\"stop-color:${gauge_color_end}\"/>"
    cat <<'EOF'
            </linearGradient>
          </defs>
          <circle class="gauge-bg" cx="80" cy="80" r="70"/>
EOF
    echo "          <circle class=\"gauge-fill\" cx=\"80\" cy=\"80\" r=\"70\" stroke-dasharray=\"440\" stroke-dashoffset=\"${gauge_offset}\"/>"
    cat <<'EOF'
        </svg>
        <div class="gauge-text">
EOF
    echo "          <div class=\"gauge-percent\">${total_pct}%</div>"
    cat <<'EOF'
          <div class="gauge-label">Coverage</div>
        </div>
      </div>
      <div class="gauge-info">
        <h2 class="gauge-title">Overall Code Coverage</h2>
EOF
    echo "        <p class=\"gauge-description\"><strong>${total_hit} of ${total_executable}</strong> executable lines covered across <strong>${file_count} files</strong>.</p>"
    cat <<'EOF'

        <div class="compact-metrics">
          <div class="metrics-group coverage-group">
            <div class="metrics-group-title">Coverage Metrics</div>
            <div class="metrics-inline">
              <div class="breakdown-item">
                <span class="breakdown-dot total"></span>
                <span class="breakdown-label">Total:</span>
EOF
    echo "                <span class=\"breakdown-value\">${total_executable} lines</span>"
    cat <<'EOF'
              </div>
              <div class="breakdown-item">
                <span class="breakdown-dot covered"></span>
                <span class="breakdown-label">Covered:</span>
EOF
    echo "                <span class=\"breakdown-value\">${total_hit} lines</span>"
    cat <<'EOF'
              </div>
              <div class="breakdown-item">
                <span class="breakdown-dot uncovered"></span>
                <span class="breakdown-label">Uncovered:</span>
EOF
    echo "                <span class=\"breakdown-value\">${total_uncovered} lines</span>"
    cat <<'EOF'
              </div>
            </div>
          </div>
          <div class="metrics-group test-group">
            <div class="metrics-group-title">Test Results</div>
            <div class="metrics-inline">
              <div class="breakdown-item">
                <span class="breakdown-dot files"></span>
                <span class="breakdown-label">Files:</span>
EOF
    echo "                <span class=\"breakdown-value\">${file_count}</span>"
    cat <<'EOF'
              </div>
              <div class="breakdown-item">
                <span class="breakdown-dot tests"></span>
                <span class="breakdown-label">Tests:</span>
EOF
    echo "                <span class=\"breakdown-value\">${tests_total} total</span>"
    cat <<'EOF'
              </div>
              <div class="breakdown-item">
                <span class="breakdown-dot tests-passed"></span>
                <span class="breakdown-label">Passed:</span>
EOF
    echo "                <span class=\"breakdown-value\">${tests_passed}</span>"
    cat <<'EOF'
              </div>
              <div class="breakdown-item">
                <span class="breakdown-dot tests-failed"></span>
                <span class="breakdown-label">Failed:</span>
EOF
    echo "                <span class=\"breakdown-value\">${tests_failed}</span>"
    cat <<'EOF'
              </div>
            </div>
          </div>
        </div>
      </div>
    </section>
    <section>
      <div class="section-header">
        <h2 class="section-title">File Coverage Details</h2>
        <div class="legend">
          <div class="legend-item">
            <span class="legend-color high"></span>
EOF
    echo "            <span>≥${BASHUNIT_COVERAGE_THRESHOLD_HIGH:-80}% High</span>"
    cat <<'EOF'
          </div>
          <div class="legend-item">
            <span class="legend-color medium"></span>
EOF
    echo "            <span>${BASHUNIT_COVERAGE_THRESHOLD_LOW:-50}-${BASHUNIT_COVERAGE_THRESHOLD_HIGH:-80}% Medium</span>"
    cat <<'EOF'
          </div>
          <div class="legend-item">
            <span class="legend-color low"></span>
EOF
    echo "            <span>&lt;${BASHUNIT_COVERAGE_THRESHOLD_LOW:-50}% Low</span>"
    cat <<'EOF'
          </div>
        </div>
      </div>
      <div class="files-table">
        <table>
          <thead>
            <tr>
              <th>File</th>
              <th style="text-align: center;">Lines</th>
              <th>Coverage</th>
            </tr>
          </thead>
          <tbody>
EOF

    local data display_file hit executable pct safe_filename
    for data in ${file_data[@]+"${file_data[@]}"}; do
      IFS='|' read -r display_file hit executable pct safe_filename <<<"$data"

      local class
      class=$(bashunit::coverage::get_coverage_class "$pct")

      echo "            <tr onclick=\"window.location='files/${safe_filename}.html'\">"
      echo "              <td>"
      echo "                <div class=\"file-info\">"
      echo "                  <a href=\"files/${safe_filename}.html\" class=\"file-name\">$(basename "$display_file")</a>"
      echo "                  <div class=\"file-path\">./${display_file}</div>"
      echo "                </div>"
      echo "              </td>"
      echo "              <td>"
      echo "                <div class=\"lines-info\">"
      echo "                  <div class=\"lines-covered\">${hit}</div>"
      echo "                  <div class=\"lines-total\">of ${executable} lines</div>"
      echo "                </div>"
      echo "              </td>"
      echo "              <td class=\"coverage-cell\">"
      echo "                <div class=\"coverage-bar-container\">"
      echo "                  <div class=\"coverage-bar\">"
      echo "                    <div class=\"coverage-bar-fill $class\" style=\"width: ${pct}%;\"></div>"
      echo "                  </div>"
      echo "                  <span class=\"coverage-percent $class\">${pct}%</span>"
      echo "                </div>"
      echo "              </td>"
      echo "            </tr>"
    done

    cat <<'EOF'
          </tbody>
        </table>
      </div>
    </section>
  </main>
  <footer class="footer">
    <div class="footer-content">
      <span class="footer-text">Generated by</span>
      <a href="https://bashunit.com" class="footer-link" target="_blank">bashunit</a>
      <span class="footer-divider"></span>
      <span class="footer-text">Documentation at</span>
      <a href="https://bashunit.com/coverage" class="footer-link" target="_blank">bashunit.com/coverage</a>
    </div>
  </footer>
</body>
</html>
EOF
  } >"$output_file"
}

function bashunit::coverage::generate_file_html() {
  local file="$1"
  local output_file="$2"

  local display_file="${file#"$(pwd)"/}"
  local executable hit pct class stats rest
  stats=$(bashunit::coverage::get_cached_stats "$file")
  executable="${stats%%:*}"
  rest="${stats#*:}"
  hit="${rest%%:*}"
  rest="${rest#*:}"
  pct="${rest%%:*}"
  class="${rest#*:}"
  local uncovered=$((executable - hit))

  # Pre-load all line hits into indexed array (performance optimization)
  local -a hits_by_line=()
  local _ln _cnt
  while IFS=: read -r _ln _cnt; do
    hits_by_line[_ln]=$_cnt
  done < <(bashunit::coverage::get_all_line_hits "$file")

  # Pre-load all file lines into indexed array (avoids sed per line)
  local -a file_lines=()
  local _fli=0 _fl
  while IFS= read -r _fl || [ -n "$_fl" ]; do
    file_lines[_fli]="$_fl"
    ((++_fli))
  done <"$file"

  # Pre-load test hits data into indexed array (for tooltips)
  # Index: line number, Value: newline-separated list of "test_file:test_function"
  # Using indexed array for Bash 3.0 compatibility (no associative arrays)
  local -a tests_by_line=()
  local _line_and_test
  while IFS= read -r _line_and_test; do
    [ -z "$_line_and_test" ] && continue
    local _tln="${_line_and_test%%|*}"
    local _tinfo="${_line_and_test#*|}"
    if [ -n "${tests_by_line[_tln]:-}" ]; then
      # Append only if not already present (avoid duplicates)
      # Use newline boundaries to prevent false positives (e.g., test_foo matching test_foo_bar)
      case $'\n'"${tests_by_line[_tln]}"$'\n' in
      *$'\n'"$_tinfo"$'\n'*)
        # already present, skip
        ;;
      *)
        tests_by_line[_tln]="${tests_by_line[_tln]}"$'\n'"${_tinfo}"
        ;;
      esac
    else
      tests_by_line[_tln]="$_tinfo"
    fi
  done < <(bashunit::coverage::get_all_line_tests "$file")

  # Count total lines and functions
  local total_lines
  total_lines=$(wc -l <"$file" | tr -d ' ')
  local non_executable=$((total_lines - executable))

  {
    cat <<'EOF'
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
EOF
    echo "  <title>$(basename "$display_file") | Coverage Report</title>"
    cat <<'EOF'
  <style>
    :root {
      --primary: #6366f1; --primary-dark: #4f46e5; --primary-light: #818cf8;
      --success: #10b981; --success-bg: rgba(16, 185, 129, 0.1); --success-border: rgba(16, 185, 129, 0.2);
      --warning: #f59e0b;
      --danger: #ef4444; --danger-bg: rgba(239, 68, 68, 0.1); --danger-border: rgba(239, 68, 68, 0.2);
      --bg-light: #ffffff; --bg-card: #f8fafc; --bg-hover: #e1e5ea; --bg-code: #f6f8fa;
      --text-primary: #0f172a; --text-secondary: #475569; --text-muted: #94a3b8;
      --border: #e2e8f0; --line-number-bg: #f8fafc;
    }
    * { margin: 0; padding: 0; box-sizing: border-box; }
    body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; background: var(--bg-light); color: var(--text-primary); min-height: 100vh; line-height: 1.6; }
    .header { background: var(--bg-card); border-bottom: 1px solid var(--border); padding: 20px 30px; position: sticky; top: 0; z-index: 100; backdrop-filter: blur(10px); box-shadow: 0 1px 3px rgba(0,0,0,0.1); }
    .header-content { max-width: 1600px; margin: 0 auto; display: flex; justify-content: space-between; align-items: center; flex-wrap: wrap; gap: 20px; }
    .nav-section { display: flex; align-items: center; gap: 20px; flex-wrap: wrap; }
    .back-btn { display: inline-flex; align-items: center; gap: 8px; padding: 12px 24px; background: #475569; border: 2px solid #475569; border-radius: 8px; color: #ffffff; text-decoration: none; font-size: 1rem; font-weight: 600; transition: all 0.2s; box-shadow: 0 2px 4px rgba(71, 85, 105, 0.2); }
    .back-btn:hover { background: #334155; border-color: #334155; box-shadow: 0 4px 12px rgba(51, 65, 85, 0.3); }
    .file-title { display: flex; align-items: center; gap: 12px; }
    .file-name { font-size: 1.3rem; font-weight: 700; font-family: 'SF Mono', 'Consolas', 'Liberation Mono', Menlo, monospace; }
    .stats-section { display: flex; align-items: center; gap: 30px; flex-wrap: wrap; }
    .stat-item { display: flex; align-items: center; gap: 10px; }
    .stat-badge { padding: 8px 16px; border-radius: 20px; font-weight: 600; font-size: 0.9rem; }
    .stat-badge.coverage.high { background: linear-gradient(135deg, var(--success) 0%, #34d399 100%); color: #000; }
    .stat-badge.coverage.medium { background: linear-gradient(135deg, var(--warning) 0%, #fbbf24 100%); color: #000; }
    .stat-badge.coverage.low { background: linear-gradient(135deg, var(--danger) 0%, #f87171 100%); color: #fff; }
    .stat-badge.lines { background: var(--bg-hover); color: var(--text-primary); }
    .stat-label { color: var(--text-secondary); font-size: 0.85rem; }
    .summary-bar { background: var(--bg-card); border-bottom: 1px solid var(--border); padding: 20px 30px; }
    .summary-content { max-width: 1600px; margin: 0 auto; display: flex; align-items: center; gap: 40px; flex-wrap: wrap; }
    .progress-section { flex: 1; min-width: 300px; }
    .progress-header { display: flex; justify-content: space-between; margin-bottom: 8px; }
    .progress-label { color: var(--text-secondary); font-size: 0.9rem; }
    .progress-percent { font-weight: 700; }
    .progress-percent.high { color: var(--success); }
    .progress-percent.medium { color: var(--warning); }
    .progress-percent.low { color: var(--danger); }
    .progress-bar { height: 12px; background: var(--bg-hover); border-radius: 6px; overflow: hidden; }
    .progress-fill { height: 100%; border-radius: 6px; transition: width 1s ease-out; }
    .progress-fill.high { background: linear-gradient(90deg, #059669 0%, #10b981 100%); }
    .progress-fill.medium { background: linear-gradient(90deg, #d97706 0%, #f59e0b 100%); }
    .progress-fill.low { background: linear-gradient(90deg, #dc2626 0%, #ef4444 100%); }
    .legend { display: flex; gap: 24px; flex-wrap: wrap; }
    .legend-item { display: flex; align-items: center; gap: 8px; font-size: 0.9rem; color: var(--text-secondary); pointer-events: none; }
    .legend-color { width: 16px; height: 16px; border-radius: 4px; }
    .legend-color.covered { background: var(--success); }
    .legend-color.uncovered { background: var(--danger); }
    .legend-color.neutral { background: var(--text-muted); }
    .code-container { max-width: 1600px; margin: 30px auto; padding: 0 30px; }
    .code-wrapper { background: var(--bg-code); border-radius: 16px; overflow: hidden; border: 1px solid var(--border); box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); }
    .code-header { background: var(--line-number-bg); padding: 16px 24px; display: flex; justify-content: space-between; align-items: center; border-bottom: 1px solid var(--border); flex-wrap: wrap; gap: 12px; }
    .code-path { font-family: 'SF Mono', 'Consolas', 'Liberation Mono', Menlo, monospace; font-size: 0.9rem; color: var(--text-secondary); }
    .code-stats { display: flex; gap: 16px; font-size: 0.85rem; }
    .code-stats span { padding: 4px 12px; background: #e5e7eb; border-radius: 4px; color: var(--text-secondary); }
    .code-body { overflow-x: auto; }
    .code-table { width: 100%; border-collapse: collapse; font-family: 'SF Mono', 'Consolas', 'Liberation Mono', Menlo, monospace; font-size: 13px; line-height: 1.6; }
    .code-table tr { transition: background 0.15s; }
    .line-num { width: 60px; padding: 2px 16px; text-align: right; color: #9ca3af; background: var(--line-number-bg); border-right: 1px solid var(--border); user-select: none; vertical-align: top; }
    .hits { width: 60px; padding: 2px 12px; text-align: center; color: #9ca3af; background: var(--line-number-bg); border-right: 1px solid var(--border); font-size: 0.85em; vertical-align: top; }
    .hits-badge { display: inline-block; padding: 2px 8px; border-radius: 10px; font-size: 0.8em; font-weight: 600; position: relative; }
    .hits-badge.has-tooltip { cursor: help; }
    .covered .hits-badge { background: var(--success-bg); color: var(--success); }
    .uncovered .hits-badge { background: var(--danger-bg); color: var(--danger); }
    .hits-tooltip { display: none; position: absolute; left: 100%; top: 50%; transform: translateY(-50%); margin-left: 12px; padding: 10px 14px; background: #1e293b; color: #f1f5f9; border-radius: 8px; font-size: 11px; font-weight: 400; white-space: normal; z-index: 100; box-shadow: 0 4px 12px rgba(0,0,0,0.15); min-width: 200px; max-width: 500px; width: max-content; }
    .hits-tooltip::after { content: ''; position: absolute; right: 100%; top: 50%; transform: translateY(-50%); border: 6px solid transparent; border-right-color: #1e293b; }
    .hits-badge:hover .hits-tooltip { display: block; }
    .hits-tooltip-title { font-weight: 600; margin-bottom: 6px; color: #94a3b8; font-size: 10px; text-transform: uppercase; letter-spacing: 0.5px; }
    .hits-tooltip-list { margin: 0; padding: 0; list-style: none; }
    .hits-tooltip-list li { padding: 3px 0; border-bottom: 1px solid #334155; font-family: 'SF Mono', 'Consolas', 'Liberation Mono', Menlo, monospace; }
    .hits-tooltip-list li:last-child { border-bottom: none; }
    .hits-tooltip-file { color: #60a5fa; }
    .hits-tooltip-fn { color: #a5b4fc; }
    .code { padding: 2px 20px; white-space: pre; vertical-align: top; }
    .covered { background: #d1fae5; }
    .covered .line-num, .covered .hits { background: #a7f3d0; border-color: var(--success-border); }
    .covered:hover { background: #ecfdf5; }
    .covered:hover .line-num, .covered:hover .hits { background: #d1fae5; }
    .uncovered { background: #fee2e2; }
    .uncovered .line-num, .uncovered .hits { background: #fecaca; border-color: var(--danger-border); }
    .uncovered:hover { background: #fef2f2; }
    .uncovered:hover .line-num, .uncovered:hover .hits { background: #fee2e2; }
    .function-summary { max-width: 1600px; margin: 30px auto; padding: 0 30px; }
    .function-table { background: var(--bg-card); border-radius: 16px; overflow: hidden; border: 1px solid var(--border); box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); width: 100%; border-collapse: collapse; }
    .function-table th { background: #f1f5f9; padding: 14px 20px; text-align: left; font-weight: 600; color: var(--text-secondary); font-size: 0.85rem; text-transform: uppercase; letter-spacing: 0.5px; border-bottom: 2px solid var(--border); }
    .function-table th:first-child { width: 40%; }
    .function-table th:nth-child(2), .function-table th:nth-child(3) { text-align: center; }
    .function-table td { padding: 12px 20px; border-bottom: 1px solid var(--border); vertical-align: middle; }
    .function-table tr:last-child td { border-bottom: none; }
    .function-table tbody tr { transition: background 0.15s; }
    .function-table tbody tr:hover { background: var(--bg-hover); }
    .function-table tbody tr.fn-covered { background: #f0fdf4; }
    .function-table tbody tr.fn-covered:hover { background: #dcfce7; }
    .function-table tbody tr.fn-partial { background: #fffbeb; }
    .function-table tbody tr.fn-partial:hover { background: #fef3c7; }
    .function-table tbody tr.fn-uncovered { background: #fef2f2; }
    .function-table tbody tr.fn-uncovered:hover { background: #fee2e2; }
    .fn-name { font-weight: 600; color: var(--primary); cursor: pointer; text-decoration: none; font-family: 'SF Mono', 'Consolas', 'Liberation Mono', Menlo, monospace; font-size: 0.95rem; }
    .fn-name:hover { color: var(--primary-dark); text-decoration: underline; }
    .fn-lines { text-align: center; color: var(--text-secondary); font-size: 0.9rem; }
    .fn-coverage-cell { text-align: center; }
    .fn-coverage-bar { display: flex; align-items: center; gap: 12px; justify-content: center; }
    .fn-progress { width: 100px; height: 8px; background: #e5e7eb; border-radius: 4px; overflow: hidden; }
    .fn-progress-fill { height: 100%; border-radius: 4px; }
    .fn-progress-fill.high { background: var(--success); }
    .fn-progress-fill.medium { background: var(--warning); }
    .fn-progress-fill.low { background: var(--danger); }
    .fn-pct { font-weight: 600; font-size: 0.9rem; min-width: 50px; text-align: right; }
    .fn-pct.high { color: var(--success); }
    .fn-pct.medium { color: var(--warning); }
    .fn-pct.low { color: var(--danger); }
    .line-anchor { scroll-margin-top: 200px; }
    .line-anchor:target { animation: highlightFade 4s ease-out forwards; }
    .line-anchor:target .line-num, .line-anchor:target .hits { animation: highlightFade 4s ease-out forwards; }
    @keyframes highlightFade {
      0% { background: #93c5fd; }
      70% { background: #dbeafe; }
      100% { background: transparent; }
    }
    .footer { max-width: 1600px; margin: 0 auto; padding: 40px 30px; text-align: center; }
    .footer-text { color: var(--text-muted); font-size: 0.9rem; }
    .footer-link { color: var(--primary-light); text-decoration: none; font-weight: 500; }
    .footer-link:hover { color: var(--primary); }
    @media (max-width: 768px) {
      .header { padding: 15px 20px; } .header-content { gap: 15px; }
      .stats-section { gap: 15px; } .summary-bar { padding: 15px 20px; }
      .summary-content { gap: 20px; } .code-container { padding: 0 15px; margin: 20px auto; }
      .code-header { padding: 12px 16px; } .line-num, .hits { padding: 2px 8px; }
      .code { padding: 2px 12px; }
    }
  </style>
</head>
<body>
  <header class="header">
    <div class="header-content">
      <div class="nav-section">
        <a href="../index.html" class="back-btn">← Back to Overview</a>
        <div class="file-title">
EOF
    echo "          <span class=\"file-name\">$(basename "$display_file")</span>"
    cat <<'EOF'
        </div>
      </div>
      <div class="stats-section">
        <div class="stat-item">
EOF
    echo "          <span class=\"stat-badge coverage $class\">${pct}%</span>"
    cat <<'EOF'
          <span class="stat-label">Coverage</span>
        </div>
        <div class="stat-item">
EOF
    echo "          <span class=\"stat-badge lines\">${hit}/${executable}</span>"
    cat <<'EOF'
          <span class="stat-label">Lines</span>
        </div>
      </div>
    </div>
  </header>
  <div class="summary-bar">
    <div class="summary-content">
      <div class="progress-section">
        <div class="progress-header">
          <span class="progress-label">Line Coverage Progress</span>
EOF
    echo "          <span class=\"progress-percent $class\">${pct}%</span>"
    cat <<'EOF'
        </div>
        <div class="progress-bar">
EOF
    echo "          <div class=\"progress-fill $class\" style=\"width: ${pct}%;\"></div>"
    cat <<'EOF'
        </div>
      </div>
      <div class="legend">
        <div class="legend-item">
          <span class="legend-color covered"></span>
EOF
    echo "          <span>${hit} lines covered</span>"
    cat <<'EOF'
        </div>
        <div class="legend-item">
          <span class="legend-color uncovered"></span>
EOF
    echo "          <span>${uncovered} lines uncovered</span>"
    cat <<'EOF'
        </div>
        <div class="legend-item">
          <span class="legend-color neutral"></span>
EOF
    echo "          <span>${non_executable} non-executable</span>"
    cat <<'EOF'
        </div>
      </div>
    </div>
  </div>
EOF

    # Extract functions and generate summary table
    local functions_data
    functions_data=$(bashunit::coverage::extract_functions "$file")

    if [ -n "$functions_data" ]; then
      cat <<'EOF'
  <div class="function-summary">
    <table class="function-table">
      <thead>
        <tr>
          <th>Function</th>
          <th>Lines</th>
          <th>Coverage</th>
        </tr>
      </thead>
      <tbody>
EOF
      local fn_entry
      while IFS= read -r fn_entry; do
        [ -z "$fn_entry" ] && continue
        local fn_name fn_start fn_end
        fn_name="${fn_entry%%|*}"
        local rest="${fn_entry#*|}"
        fn_start="${rest%%|*}"
        fn_end="${rest#*|}"

        # Calculate function coverage using pre-loaded hits data
        local fn_executable=0
        local fn_hit=0
        local ln
        for ((ln = fn_start; ln <= fn_end; ln++)); do
          local ln_content
          ln_content="${file_lines[$((ln - 1))]:-}"
          if bashunit::coverage::is_executable_line "$ln_content" "$ln"; then
            ((++fn_executable))
            local ln_hits=${hits_by_line[$ln]:-0}
            if [ "$ln_hits" -gt 0 ]; then
              ((++fn_hit))
            fi
          fi
        done

        local fn_pct fn_class row_class
        fn_pct=$(bashunit::coverage::calculate_percentage "$fn_hit" "$fn_executable")
        fn_class=$(bashunit::coverage::get_coverage_class "$fn_pct")
        case "$fn_class" in
        high) row_class="fn-covered" ;;
        medium) row_class="fn-partial" ;;
        low) row_class="fn-uncovered" ;;
        esac

        echo "        <tr class=\"$row_class\">"
        echo "          <td><a href=\"#line-${fn_start}\" class=\"fn-name\">${fn_name}</a></td>"
        echo "          <td class=\"fn-lines\">${fn_hit} / ${fn_executable}</td>"
        echo "          <td class=\"fn-coverage-cell\">"
        echo "            <div class=\"fn-coverage-bar\">"
        echo "              <div class=\"fn-progress\"><div class=\"fn-progress-fill ${fn_class}\" style=\"width: ${fn_pct}%;\"></div></div>"
        echo "              <span class=\"fn-pct ${fn_class}\">${fn_pct}%</span>"
        echo "            </div>"
        echo "          </td>"
        echo "        </tr>"
      done <<<"$functions_data"

      cat <<'EOF'
      </tbody>
    </table>
  </div>
EOF
    fi

    cat <<'EOF'
  <div class="code-container">
    <div class="code-wrapper">
      <div class="code-header">
EOF
    echo "        <span class=\"code-path\">./${display_file}</span>"
    echo "        <div class=\"code-stats\">"
    echo "          <span>${total_lines} total lines</span>"
    echo "        </div>"
    cat <<'EOF'
      </div>
      <div class="code-body">
        <table class="code-table">
EOF

    local lineno=0
    local line
    for line in "${file_lines[@]}"; do
      ((++lineno))

      local escaped_line
      escaped_line=$(bashunit::coverage::html_escape "$line")

      local row_class=""
      local hits_display=""

      if bashunit::coverage::is_executable_line "$line" "$lineno"; then
        # O(1) lookup from pre-loaded array
        local hits=${hits_by_line[$lineno]:-0}

        if [ "$hits" -gt 0 ]; then
          row_class="covered"

          # Check if we have test info for this line
          local test_info="${tests_by_line[$lineno]:-}"
          if [ -n "$test_info" ]; then
            # Build tooltip with test information
            local tooltip_html="<div class=\"hits-tooltip\"><div class=\"hits-tooltip-title\">Tests hitting this line</div><ul class=\"hits-tooltip-list\">"
            local test_file test_fn
            while IFS=':' read -r test_file test_fn; do
              [ -z "$test_file" ] && continue
              local short_file
              short_file=$(basename "$test_file")
              tooltip_html="$tooltip_html<li><span class=\"hits-tooltip-file\">${short_file}</span>:<span class=\"hits-tooltip-fn\">${test_fn}</span></li>"
            done <<<"$test_info"
            tooltip_html="$tooltip_html</ul></div>"
            hits_display="<span class=\"hits-badge has-tooltip\">${hits}×${tooltip_html}</span>"
          else
            hits_display="<span class=\"hits-badge\">${hits}×</span>"
          fi
        else
          row_class="uncovered"
          hits_display="<span class=\"hits-badge\">${hits}×</span>"
        fi
      fi

      echo "          <tr id=\"line-${lineno}\" class=\"$row_class line-anchor\">"
      echo "            <td class=\"line-num\">$lineno</td>"
      echo "            <td class=\"hits\">$hits_display</td>"
      echo "            <td class=\"code\">$escaped_line</td>"
      echo "          </tr>"
    done

    cat <<'EOF'
        </table>
      </div>
    </div>
  </div>
  <footer class="footer">
    <p class="footer-text">
      Generated by <a href="https://bashunit.com" class="footer-link" target="_blank">bashunit</a>
    </p>
  </footer>
</body>
</html>
EOF
  } >"$output_file"
}

function bashunit::coverage::cleanup() {
  if [ -n "$_BASHUNIT_COVERAGE_DATA_FILE" ]; then
    local coverage_dir
    coverage_dir=$(dirname "$_BASHUNIT_COVERAGE_DATA_FILE")
    rm -rf "$coverage_dir"
  fi
}

# clock.sh

_BASHUNIT_CLOCK_NOW_IMPL=""

function bashunit::clock::_choose_impl() {
  local shell_time
  # Use explicit indices for Bash 3.0 compatibility (empty array access fails with set -u)
  local attempts_count=0
  local attempts

  # 1. Try native shell EPOCHREALTIME (fastest - no subprocess, Bash 5.0+)
  attempts[attempts_count]="EPOCHREALTIME"
  attempts_count=$((attempts_count + 1))
  if shell_time="$(bashunit::clock::shell_time)"; then
    _BASHUNIT_CLOCK_NOW_IMPL="shell"
    return 0
  fi

  # 2. Unix date +%s%N (no subprocess overhead on supported systems)
  attempts[attempts_count]="date"
  attempts_count=$((attempts_count + 1))
  if ! bashunit::check_os::is_macos && ! bashunit::check_os::is_alpine; then
    local result
    result=$(date +%s%N 2>/dev/null)
    # A pure-digit result means %N expanded; a literal "N" (unsupported date)
    # contains a non-digit, so the digits-only check alone is sufficient.
    case "$result" in
    '' | *[!0-9]*) ;;
    *)
      _BASHUNIT_CLOCK_NOW_IMPL="date"
      return 0
      ;;
    esac
  fi

  # 3. Try Perl with Time::HiRes. Probe by reading the actual time (not an empty
  # `-e ""`) so the pending first read reuses this fork instead of paying a
  # second one; a non-digit/empty result means perl or Time::HiRes is missing, so
  # fall through. The value is seeded into the return slot for now_to_slot.
  attempts[attempts_count]="Perl"
  attempts_count=$((attempts_count + 1))
  if bashunit::dependencies::has_perl; then
    local perl_now
    perl_now="$(perl -MTime::HiRes -e 'printf("%.0f\n", Time::HiRes::time() * 1000000000)' 2>/dev/null)"
    case "$perl_now" in
    '' | *[!0-9]*) ;;
    *)
      _BASHUNIT_CLOCK_NOW_IMPL="perl"
      _BASHUNIT_CLOCK_NOW_OUT="$perl_now"
      return 0
      ;;
    esac
  fi

  # 4. Try Python 3 with time module
  attempts[attempts_count]="Python"
  attempts_count=$((attempts_count + 1))
  if bashunit::dependencies::has_python; then
    _BASHUNIT_CLOCK_NOW_IMPL="python"
    return 0
  fi

  # 5. Try Node.js
  attempts[attempts_count]="Node"
  attempts_count=$((attempts_count + 1))
  if bashunit::dependencies::has_node; then
    _BASHUNIT_CLOCK_NOW_IMPL="node"
    return 0
  fi

  # 6. Windows fallback with PowerShell
  attempts[attempts_count]="PowerShell"
  attempts_count=$((attempts_count + 1))
  if bashunit::check_os::is_windows && bashunit::dependencies::has_powershell; then
    _BASHUNIT_CLOCK_NOW_IMPL="powershell"
    return 0
  fi

  # 7. Very last fallback: seconds resolution only
  attempts[attempts_count]="date-seconds"
  attempts_count=$((attempts_count + 1))
  if date +%s &>/dev/null; then
    _BASHUNIT_CLOCK_NOW_IMPL="date-seconds"
    return 0
  fi

  # 8. All methods failed
  printf "bashunit::clock::now implementations tried: %s\n" "${attempts[*]}" >&2
  echo ""
  return 1
}

# Returns 0 when the chosen clock impl forks an interpreter (perl/python/node/
# powershell), so callers can skip optional timing to avoid a per-read fork (#765).
function bashunit::clock::is_expensive() {
  [ -n "$_BASHUNIT_CLOCK_NOW_IMPL" ] || bashunit::clock::_choose_impl >/dev/null 2>&1 || true
  case "$_BASHUNIT_CLOCK_NOW_IMPL" in
  perl | python | node | powershell) return 0 ;;
  *) return 1 ;;
  esac
}

_BASHUNIT_CLOCK_NOW_OUT=""

# Return-slot variant of bashunit::clock::now: writes the current time in
# nanoseconds into _BASHUNIT_CLOCK_NOW_OUT. The `shell` branch reads
# EPOCHREALTIME directly (folding in shell_time) so the per-test hot path pays
# no command-substitution fork on Bash 5.0+; `date-seconds` forks once instead
# of twice. Interpreter/`date` branches keep a single internal fork.
# Returns: 0 on success, 1 when no clock implementation is available.
function bashunit::clock::now_to_slot() {
  if [ -z "$_BASHUNIT_CLOCK_NOW_IMPL" ]; then
    _BASHUNIT_CLOCK_NOW_OUT=""
    bashunit::clock::_choose_impl || return 1
    # _choose_impl may have already read the current time while selecting an
    # interpreter impl (e.g. perl); reuse that value for this first read instead
    # of forking a second interpreter.
    if [ -n "$_BASHUNIT_CLOCK_NOW_OUT" ]; then
      return 0
    fi
  fi

  case "$_BASHUNIT_CLOCK_NOW_IMPL" in
  perl)
    _BASHUNIT_CLOCK_NOW_OUT="$(perl -MTime::HiRes -e 'printf("%.0f\n", Time::HiRes::time() * 1000000000)')"
    ;;
  python)
    _BASHUNIT_CLOCK_NOW_OUT="$(
      python - <<'EOF'
import time, sys
sys.stdout.write(str(int(time.time() * 1000000000)))
EOF
    )"
    ;;
  node)
    _BASHUNIT_CLOCK_NOW_OUT="$(node -e 'process.stdout.write((BigInt(Date.now()) * 1000000n).toString())')"
    ;;
  powershell)
    _BASHUNIT_CLOCK_NOW_OUT="$(powershell -Command "\
        \$unixEpoch = [DateTime]'1970-01-01 00:00:00';\
        \$now = [DateTime]::UtcNow;\
        \$ticksSinceEpoch = (\$now - \$unixEpoch).Ticks;\
        \$nanosecondsSinceEpoch = \$ticksSinceEpoch * 100;\
        Write-Output \$nanosecondsSinceEpoch\
      ")"
    ;;
  date)
    _BASHUNIT_CLOCK_NOW_OUT="$(date +%s%N)"
    ;;
  date-seconds)
    local seconds
    seconds=$(date +%s)
    _BASHUNIT_CLOCK_NOW_OUT="$((seconds * 1000000000))"
    ;;
  shell)
    # Read EPOCHREALTIME directly (no shell_time subshell) on the hot path;
    # both '.' and ',' decimal separators are handled for locale portability.
    local shell_time="${EPOCHREALTIME:-}"
    local seconds="${shell_time%%[.,]*}"
    local microseconds="${shell_time#*[.,]}"
    if [ "$seconds" = "$shell_time" ]; then
      microseconds=""
    fi
    # Pad to 6 digits and strip leading zeros for arithmetic
    microseconds="${microseconds}000000"
    microseconds="${microseconds:0:6}"
    microseconds="${microseconds#"${microseconds%%[!0]*}"}"
    microseconds="${microseconds:-0}"
    _BASHUNIT_CLOCK_NOW_OUT="$(((seconds * 1000000000) + (microseconds * 1000)))"
    ;;
  *)
    bashunit::clock::_choose_impl || return 1
    bashunit::clock::now_to_slot
    ;;
  esac
}

function bashunit::clock::now() {
  bashunit::clock::now_to_slot || return 1
  echo "$_BASHUNIT_CLOCK_NOW_OUT"
}

function bashunit::clock::shell_time() {
  # Get time directly from the shell variable EPOCHREALTIME (Bash 5+)
  [ -n "${EPOCHREALTIME+x}" ] && [ -n "$EPOCHREALTIME" ] && LC_ALL=C echo "$EPOCHREALTIME"
}

function bashunit::clock::total_runtime_in_milliseconds() {
  local end_time
  end_time=$(bashunit::clock::now)
  if [ -n "$end_time" ]; then
    bashunit::math::calculate "($end_time - $_BASHUNIT_START_TIME) / 1000000"
  else
    echo ""
  fi
}

function bashunit::clock::total_runtime_in_nanoseconds() {
  local end_time
  end_time=$(bashunit::clock::now)
  if [ -n "$end_time" ]; then
    bashunit::math::calculate "$end_time - $_BASHUNIT_START_TIME"
  else
    echo ""
  fi
}

function bashunit::clock::init() {
  _BASHUNIT_START_TIME=$(bashunit::clock::now)
}

# state.sh

# Cache base64 -w flag support (Alpine needs -w 0, macOS does not support -w).
# Scrape `base64 --help` once and match with a shell `case` instead of piping
# into a `grep` fork — same detection, one fewer fork per cold start.
_bashunit_base64_help="$(base64 --help 2>&1 || true)"
case "$_bashunit_base64_help" in
*-w*) _BASHUNIT_BASE64_WRAP_FLAG=true ;;
*) _BASHUNIT_BASE64_WRAP_FLAG=false ;;
esac
unset _bashunit_base64_help

_BASHUNIT_TESTS_PASSED=0
_BASHUNIT_TESTS_FAILED=0
_BASHUNIT_TESTS_SKIPPED=0
_BASHUNIT_TESTS_INCOMPLETE=0
_BASHUNIT_TESTS_SNAPSHOT=0
_BASHUNIT_TESTS_RISKY=0
_BASHUNIT_ASSERTIONS_PASSED=0
_BASHUNIT_ASSERTIONS_FAILED=0
_BASHUNIT_ASSERTIONS_SKIPPED=0
_BASHUNIT_ASSERTIONS_INCOMPLETE=0
_BASHUNIT_ASSERTIONS_SNAPSHOT=0
_BASHUNIT_DUPLICATED_FUNCTION_NAMES=""
_BASHUNIT_FILE_WITH_DUPLICATED_FUNCTION_NAMES=""
_BASHUNIT_DUPLICATED_TEST_FUNCTIONS_FOUND=false
_BASHUNIT_TEST_OUTPUT=""
_BASHUNIT_TEST_TITLE=""
_BASHUNIT_TEST_EXIT_CODE=0
_BASHUNIT_TEST_HOOK_FAILURE=""
_BASHUNIT_TEST_HOOK_MESSAGE=""
_BASHUNIT_CURRENT_TEST_INTERPOLATED_NAME=""
_BASHUNIT_ASSERTION_FAILED_IN_TEST=0

function bashunit::state::get_tests_passed() {
  echo "$_BASHUNIT_TESTS_PASSED"
}

function bashunit::state::add_tests_passed() {
  ((_BASHUNIT_TESTS_PASSED++)) || true
}

function bashunit::state::get_tests_failed() {
  echo "$_BASHUNIT_TESTS_FAILED"
}

function bashunit::state::add_tests_failed() {
  ((_BASHUNIT_TESTS_FAILED++)) || true
}

function bashunit::state::get_tests_skipped() {
  echo "$_BASHUNIT_TESTS_SKIPPED"
}

function bashunit::state::add_tests_skipped() {
  ((_BASHUNIT_TESTS_SKIPPED++)) || true
}

function bashunit::state::get_tests_incomplete() {
  echo "$_BASHUNIT_TESTS_INCOMPLETE"
}

function bashunit::state::add_tests_incomplete() {
  ((_BASHUNIT_TESTS_INCOMPLETE++)) || true
}

function bashunit::state::get_tests_snapshot() {
  echo "$_BASHUNIT_TESTS_SNAPSHOT"
}

function bashunit::state::add_tests_snapshot() {
  ((_BASHUNIT_TESTS_SNAPSHOT++)) || true
}

function bashunit::state::get_tests_risky() {
  echo "$_BASHUNIT_TESTS_RISKY"
}

function bashunit::state::add_tests_risky() {
  ((_BASHUNIT_TESTS_RISKY++)) || true
}

function bashunit::state::get_assertions_passed() {
  echo "$_BASHUNIT_ASSERTIONS_PASSED"
}

function bashunit::state::add_assertions_passed() {
  ((_BASHUNIT_ASSERTIONS_PASSED++)) || true
}

function bashunit::state::get_assertions_failed() {
  echo "$_BASHUNIT_ASSERTIONS_FAILED"
}

function bashunit::state::add_assertions_failed() {
  ((_BASHUNIT_ASSERTIONS_FAILED++)) || true
}

function bashunit::state::get_assertions_skipped() {
  echo "$_BASHUNIT_ASSERTIONS_SKIPPED"
}

function bashunit::state::add_assertions_skipped() {
  ((_BASHUNIT_ASSERTIONS_SKIPPED++)) || true
}

function bashunit::state::get_assertions_incomplete() {
  echo "$_BASHUNIT_ASSERTIONS_INCOMPLETE"
}

function bashunit::state::add_assertions_incomplete() {
  ((_BASHUNIT_ASSERTIONS_INCOMPLETE++)) || true
}

function bashunit::state::get_assertions_snapshot() {
  echo "$_BASHUNIT_ASSERTIONS_SNAPSHOT"
}

function bashunit::state::add_assertions_snapshot() {
  ((_BASHUNIT_ASSERTIONS_SNAPSHOT++)) || true
}

function bashunit::state::is_duplicated_test_functions_found() {
  echo "$_BASHUNIT_DUPLICATED_TEST_FUNCTIONS_FOUND"
}

function bashunit::state::set_duplicated_test_functions_found() {
  _BASHUNIT_DUPLICATED_TEST_FUNCTIONS_FOUND=true
}

function bashunit::state::get_duplicated_function_names() {
  echo "$_BASHUNIT_DUPLICATED_FUNCTION_NAMES"
}

function bashunit::state::set_duplicated_function_names() {
  _BASHUNIT_DUPLICATED_FUNCTION_NAMES="$1"
}

function bashunit::state::get_file_with_duplicated_function_names() {
  echo "$_BASHUNIT_FILE_WITH_DUPLICATED_FUNCTION_NAMES"
}

function bashunit::state::set_file_with_duplicated_function_names() {
  _BASHUNIT_FILE_WITH_DUPLICATED_FUNCTION_NAMES="$1"
}

function bashunit::state::add_test_output() {
  _BASHUNIT_TEST_OUTPUT="$_BASHUNIT_TEST_OUTPUT$1"
}

function bashunit::state::get_test_exit_code() {
  echo "$_BASHUNIT_TEST_EXIT_CODE"
}

function bashunit::state::set_test_exit_code() {
  _BASHUNIT_TEST_EXIT_CODE="$1"
}

function bashunit::state::get_test_title() {
  echo "$_BASHUNIT_TEST_TITLE"
}

function bashunit::state::set_test_title() {
  _BASHUNIT_TEST_TITLE="$1"
}

function bashunit::state::reset_test_title() {
  _BASHUNIT_TEST_TITLE=""
}

function bashunit::state::get_current_test_interpolated_function_name() {
  echo "$_BASHUNIT_CURRENT_TEST_INTERPOLATED_NAME"
}

function bashunit::state::set_current_test_interpolated_function_name() {
  _BASHUNIT_CURRENT_TEST_INTERPOLATED_NAME="$1"
}

function bashunit::state::reset_current_test_interpolated_function_name() {
  _BASHUNIT_CURRENT_TEST_INTERPOLATED_NAME=""
}

function bashunit::state::get_test_hook_failure() {
  echo "$_BASHUNIT_TEST_HOOK_FAILURE"
}

function bashunit::state::set_test_hook_failure() {
  _BASHUNIT_TEST_HOOK_FAILURE="$1"
}

function bashunit::state::reset_test_hook_failure() {
  _BASHUNIT_TEST_HOOK_FAILURE=""
}

function bashunit::state::get_test_hook_message() {
  echo "$_BASHUNIT_TEST_HOOK_MESSAGE"
}

function bashunit::state::set_test_hook_message() {
  _BASHUNIT_TEST_HOOK_MESSAGE="$1"
}

function bashunit::state::reset_test_hook_message() {
  _BASHUNIT_TEST_HOOK_MESSAGE=""
}

function bashunit::state::is_assertion_failed_in_test() {
  ((_BASHUNIT_ASSERTION_FAILED_IN_TEST))
}

function bashunit::state::mark_assertion_failed_in_test() {
  _BASHUNIT_ASSERTION_FAILED_IN_TEST=1
}

function bashunit::state::set_duplicated_functions_merged() {
  bashunit::state::set_duplicated_test_functions_found
  bashunit::state::set_file_with_duplicated_function_names "$1"
  bashunit::state::set_duplicated_function_names "$2"
}

function bashunit::state::initialize_assertions_count() {
  _BASHUNIT_ASSERTIONS_PASSED=0
  _BASHUNIT_ASSERTIONS_FAILED=0
  _BASHUNIT_ASSERTIONS_SKIPPED=0
  _BASHUNIT_ASSERTIONS_INCOMPLETE=0
  _BASHUNIT_ASSERTIONS_SNAPSHOT=0
  _BASHUNIT_TEST_OUTPUT=""
  _BASHUNIT_TEST_TITLE=""
  _BASHUNIT_TEST_HOOK_FAILURE=""
  _BASHUNIT_TEST_HOOK_MESSAGE=""
  _BASHUNIT_ASSERTION_FAILED_IN_TEST=0
}

# base64-encodes a field, writing the result into _BASHUNIT_STATE_ENCODED_OUT.
# Empty values (the common case for title/hook message, and output on a passing
# test) encode to an empty field with no base64 fork (#762). base64 of "" is ""
# anyway, so this stays wire-compatible.
_BASHUNIT_STATE_ENCODED_OUT=""
function bashunit::state::encode_field() {
  local value=$1
  if [ -z "$value" ]; then
    _BASHUNIT_STATE_ENCODED_OUT=""
    return
  fi
  if [ "$_BASHUNIT_BASE64_WRAP_FLAG" = true ]; then
    # Alpine requires the -w 0 option to avoid wrapping
    _BASHUNIT_STATE_ENCODED_OUT=$(echo -n "$value" | base64 -w 0)
  else
    _BASHUNIT_STATE_ENCODED_OUT=$(echo -n "$value" | base64)
  fi
}

function bashunit::state::export_subshell_context() {
  local encoded_test_output
  local encoded_test_title
  local encoded_test_hook_message

  bashunit::state::encode_field "$_BASHUNIT_TEST_OUTPUT"
  encoded_test_output=$_BASHUNIT_STATE_ENCODED_OUT
  bashunit::state::encode_field "$_BASHUNIT_TEST_TITLE"
  encoded_test_title=$_BASHUNIT_STATE_ENCODED_OUT
  bashunit::state::encode_field "$_BASHUNIT_TEST_HOOK_MESSAGE"
  encoded_test_hook_message=$_BASHUNIT_STATE_ENCODED_OUT

  # Emit the encoded result payload with `printf` (a builtin) instead of a
  # `cat <<EOF` heredoc: this runs once per test, so avoiding the fork removes
  # one process per test. The `\`-continued string keeps the per-field layout
  # and produces the exact same single line the heredoc did.
  local payload="\
##ASSERTIONS_FAILED=$_BASHUNIT_ASSERTIONS_FAILED\
##ASSERTIONS_PASSED=$_BASHUNIT_ASSERTIONS_PASSED\
##ASSERTIONS_SKIPPED=$_BASHUNIT_ASSERTIONS_SKIPPED\
##ASSERTIONS_INCOMPLETE=$_BASHUNIT_ASSERTIONS_INCOMPLETE\
##ASSERTIONS_SNAPSHOT=$_BASHUNIT_ASSERTIONS_SNAPSHOT\
##TEST_EXIT_CODE=$_BASHUNIT_TEST_EXIT_CODE\
##TEST_HOOK_FAILURE=$_BASHUNIT_TEST_HOOK_FAILURE\
##TEST_HOOK_MESSAGE=$encoded_test_hook_message\
##TEST_TITLE=$encoded_test_title\
##TEST_OUTPUT=$encoded_test_output##"
  printf '%s\n' "$payload"
}

function bashunit::state::calculate_total_assertions() {
  local input="$1"
  local total=0

  local numbers
  numbers=$(echo "$input" | grep -oE '##ASSERTIONS_\w+=[0-9]+' | grep -oE '[0-9]+')

  local number
  for number in $numbers; do
    total=$((total + number))
  done

  echo $total
}

function bashunit::state::print_line() {
  # shellcheck disable=SC2034
  local type=$1
  local line=$2

  ((_BASHUNIT_TOTAL_TESTS_COUNT++)) || true

  bashunit::state::add_test_output "[$type]$line"

  if bashunit::env::is_no_progress_enabled; then
    return
  fi

  if bashunit::env::is_tap_output_enabled; then
    bashunit::state::print_tap_line "$type" "$line"
    return
  fi

  if ! bashunit::env::is_simple_output_enabled; then
    printf "%s\n" "$line"
    return
  fi

  local char
  case "$type" in
  successful) char="." ;;
  failure) char="${_BASHUNIT_COLOR_FAILED}F${_BASHUNIT_COLOR_DEFAULT}" ;;
  failed) char="${_BASHUNIT_COLOR_FAILED}F${_BASHUNIT_COLOR_DEFAULT}" ;;
  failed_snapshot) char="${_BASHUNIT_COLOR_FAILED}F${_BASHUNIT_COLOR_DEFAULT}" ;;
  skipped) char="${_BASHUNIT_COLOR_SKIPPED}S${_BASHUNIT_COLOR_DEFAULT}" ;;
  incomplete) char="${_BASHUNIT_COLOR_INCOMPLETE}I${_BASHUNIT_COLOR_DEFAULT}" ;;
  snapshot) char="${_BASHUNIT_COLOR_SNAPSHOT}N${_BASHUNIT_COLOR_DEFAULT}" ;;
  risky) char="${_BASHUNIT_COLOR_RISKY}R${_BASHUNIT_COLOR_DEFAULT}" ;;
  error) char="${_BASHUNIT_COLOR_FAILED}E${_BASHUNIT_COLOR_DEFAULT}" ;;
  *) char="?" && bashunit::log "warning" "unknown test type '$type'" ;;
  esac

  if bashunit::parallel::is_enabled; then
    printf "%s" "$char"
  else
    if ((_BASHUNIT_TOTAL_TESTS_COUNT % 50 == 0)); then
      printf "%s\n" "$char"
    else
      printf "%s" "$char"
    fi
  fi
}

function bashunit::state::print_tap_line() {
  local type=$1
  local line=$2

  local clean_line
  clean_line=$(printf "%s" "$line" | sed 's/\x1B\[[0-9;]*[mK]//g')
  local test_name="${clean_line#*: }"
  test_name="${test_name%%$'\n'*}"
  # Strip trailing whitespace and duration
  test_name=$(printf "%s" "$test_name" | \
    sed 's/[[:space:]]*[0-9][0-9]*m\{0,1\}[[:space:]]*[0-9.]*[ms]*[[:space:]]*$//')

  case "$type" in
  successful)
    printf "ok %d - %s\n" "$_BASHUNIT_TOTAL_TESTS_COUNT" "$test_name"
    ;;
  failure | failed | failed_snapshot | error)
    printf "not ok %d - %s\n" "$_BASHUNIT_TOTAL_TESTS_COUNT" "$test_name"
    local detail_line
    printf "  ---\n"
    while IFS= read -r detail_line; do
      detail_line=$(printf "%s" "$detail_line" | sed 's/\x1B\[[0-9;]*[mK]//g')
      if [ -n "$detail_line" ] \
        && [ "$(echo "$detail_line" | "$GREP" -cF "Failed:" || true)" -eq 0 ] \
        && [ "$(echo "$detail_line" | "$GREP" -cF "Error:" || true)" -eq 0 ]; then
        local trimmed="${detail_line#"${detail_line%%[![:space:]]*}"}"
        printf "  %s\n" "$trimmed"
      fi
    done <<< "$clean_line"
    printf "  ...\n"
    ;;
  skipped)
    local skip_name="${test_name%%   *}"
    local skip_reason="${test_name#"$skip_name"}"
    skip_reason="${skip_reason#"${skip_reason%%[![:space:]]*}"}"
    if [ -n "$skip_reason" ]; then
      printf "ok %d - %s # SKIP %s\n" \
        "$_BASHUNIT_TOTAL_TESTS_COUNT" "$skip_name" "$skip_reason"
    else
      printf "ok %d - %s # SKIP\n" \
        "$_BASHUNIT_TOTAL_TESTS_COUNT" "$test_name"
    fi
    ;;
  incomplete)
    printf "ok %d - %s # TODO incomplete\n" \
      "$_BASHUNIT_TOTAL_TESTS_COUNT" "$test_name"
    ;;
  snapshot)
    printf "ok %d - %s # snapshot\n" \
      "$_BASHUNIT_TOTAL_TESTS_COUNT" "$test_name"
    ;;
  risky)
    printf "ok %d - %s # RISKY no assertions\n" \
      "$_BASHUNIT_TOTAL_TESTS_COUNT" "$test_name"
    ;;
  *)
    printf "not ok %d - %s\n" \
      "$_BASHUNIT_TOTAL_TESTS_COUNT" "$test_name"
    ;;
  esac
}

# colors.sh

# Pass in any number of ANSI SGR codes.
#
# Code reference:
#   https://en.wikipedia.org/wiki/ANSI_escape_code#SGR_(Select_Graphic_Rendition)_parameters
# Credit:
#   https://superuser.com/a/1119396
bashunit::sgr() {
  local codes=${1:-0}
  shift

  local c
  for c in "$@"; do
    codes="$codes;$c"
  done

  echo $'\e'"[${codes}m"
}

if bashunit::env::is_no_color_enabled; then
  _BASHUNIT_COLOR_BOLD=""
  _BASHUNIT_COLOR_FAINT=""
  _BASHUNIT_COLOR_BLACK=""
  _BASHUNIT_COLOR_FAILED=""
  _BASHUNIT_COLOR_PASSED=""
  _BASHUNIT_COLOR_SKIPPED=""
  _BASHUNIT_COLOR_INCOMPLETE=""
  _BASHUNIT_COLOR_SNAPSHOT=""
  _BASHUNIT_COLOR_RISKY=""
  _BASHUNIT_COLOR_RETURN_ERROR=""
  _BASHUNIT_COLOR_RETURN_SUCCESS=""
  _BASHUNIT_COLOR_RETURN_SKIPPED=""
  _BASHUNIT_COLOR_RETURN_INCOMPLETE=""
  _BASHUNIT_COLOR_RETURN_SNAPSHOT=""
  _BASHUNIT_COLOR_RETURN_RISKY=""
  _BASHUNIT_COLOR_DEFAULT=""
else
  _BASHUNIT_COLOR_BOLD="$(bashunit::sgr 1)"
  # Use SGR 90 (bright black / gray) instead of SGR 2 (faint), since
  # GitHub Actions' log renderer does not render the faint attribute.
  _BASHUNIT_COLOR_FAINT="$(bashunit::sgr 90)"
  _BASHUNIT_COLOR_BLACK="$(bashunit::sgr 30)"
  _BASHUNIT_COLOR_FAILED="$(bashunit::sgr 31)"
  _BASHUNIT_COLOR_PASSED="$(bashunit::sgr 32)"
  _BASHUNIT_COLOR_SKIPPED="$(bashunit::sgr 33)"
  _BASHUNIT_COLOR_INCOMPLETE="$(bashunit::sgr 36)"
  _BASHUNIT_COLOR_SNAPSHOT="$(bashunit::sgr 34)"
  _BASHUNIT_COLOR_RISKY="$(bashunit::sgr 35)"
  _BASHUNIT_COLOR_RETURN_ERROR="$(bashunit::sgr 41)$_BASHUNIT_COLOR_BLACK$_BASHUNIT_COLOR_BOLD"
  _BASHUNIT_COLOR_RETURN_SUCCESS="$(bashunit::sgr 42)$_BASHUNIT_COLOR_BLACK$_BASHUNIT_COLOR_BOLD"
  _BASHUNIT_COLOR_RETURN_SKIPPED="$(bashunit::sgr 43)$_BASHUNIT_COLOR_BLACK$_BASHUNIT_COLOR_BOLD"
  _BASHUNIT_COLOR_RETURN_INCOMPLETE="$(bashunit::sgr 46)$_BASHUNIT_COLOR_BLACK$_BASHUNIT_COLOR_BOLD"
  _BASHUNIT_COLOR_RETURN_SNAPSHOT="$(bashunit::sgr 44)$_BASHUNIT_COLOR_BLACK$_BASHUNIT_COLOR_BOLD"
  _BASHUNIT_COLOR_RETURN_RISKY="$(bashunit::sgr 45)$_BASHUNIT_COLOR_BLACK$_BASHUNIT_COLOR_BOLD"
  _BASHUNIT_COLOR_DEFAULT="$(bashunit::sgr 0)"
fi

# console_header.sh

function bashunit::console_header::print_version_with_env() {
  local filter=${1:-}
  shift || true

  if ! bashunit::env::is_show_header_enabled; then
    return
  fi

  bashunit::console_header::print_version "$filter" "$@"

  if bashunit::env::is_dev_mode_enabled; then
    printf "%sDev log:%s %s\n" \
      "${_BASHUNIT_COLOR_INCOMPLETE}" "${_BASHUNIT_COLOR_DEFAULT}" "$BASHUNIT_DEV_LOG"
  fi
}

##
# Prints the seed used to randomize test order, so a failing run can be replayed
# with `--seed <n>`.
##
function bashunit::console_header::print_random_order_seed() {
  local seed=$1
  printf "%sRandomized with seed:%s %s\n" \
    "${_BASHUNIT_COLOR_INCOMPLETE}" "${_BASHUNIT_COLOR_DEFAULT}" "$seed"
}

function bashunit::console_header::print_version() {
  local filter=${1:-}
  shift || true

  # Bash 3.0 compatible: check argument count after shift
  local files_count=$#
  local total_tests
  if [ "$files_count" -eq 0 ]; then
    total_tests=0
  elif bashunit::parallel::is_enabled && bashunit::env::is_simple_output_enabled; then
    # Skip counting in parallel+simple mode for faster startup
    total_tests=0
  else
    # Read via the return slot, not $(...): the capture subshell would discard
    # the provider-map cache find_total_tests builds per file, forcing the
    # runner to re-scan each file with a second awk fork.
    bashunit::helper::find_total_tests "$filter" "$@" >/dev/null
    total_tests=$_BASHUNIT_HELPER_TOTAL_TESTS_OUT
  fi

  if bashunit::env::is_header_ascii_art_enabled; then
    cat <<EOF
_               _                   _
| |__   __ _ ___| |__  __ __ ____ (_) |_
| '_ \ / _' / __| '_ \| | | | '_ \| | __|
| |_) | (_| \__ \ | | | |_| | | | | | |_
|_.__/ \__,_|___/_| |_|\___/|_| |_|_|\__|
EOF
    if [ "$total_tests" -eq 0 ]; then
      printf "%s\n" "$BASHUNIT_VERSION"
    else
      printf "%s | Tests: %s\n" "$BASHUNIT_VERSION" "$total_tests"
    fi
    return
  fi

  if [ "$total_tests" -eq 0 ]; then
    printf "%s%sbashunit%s - %s\n" \
      "$_BASHUNIT_COLOR_BOLD" "$_BASHUNIT_COLOR_PASSED" "$_BASHUNIT_COLOR_DEFAULT" "$BASHUNIT_VERSION"
  else
    printf "%s%sbashunit%s - %s | Tests: %s\n" \
      "${_BASHUNIT_COLOR_BOLD}" "${_BASHUNIT_COLOR_PASSED}" "${_BASHUNIT_COLOR_DEFAULT}" \
      "$BASHUNIT_VERSION" "$total_tests"
  fi
}

function bashunit::console_header::print_help() {
  cat <<EOF
Usage: bashunit <command> [arguments] [options]

Commands:
  test [path]         Run tests (default command)
  bench [path]        Run benchmarks
  assert <fn> <args>  Run standalone assertion
  doc [filter]        Display assertion documentation
  init [dir]          Initialize a new test directory
  learn               Start interactive tutorial
  watch [path]        Watch files and re-run tests on change
  upgrade             Upgrade bashunit to latest version

Global Options:
  -h, --help        Show this help message
  -v, --version     Display the current version

Run 'bashunit <command> --help' for command-specific options.

Examples:
  bashunit test tests/                Run all tests in directory
  bashunit tests/                     Run all tests (shorthand)
  bashunit bench                      Run all benchmarks
  bashunit assert equals "foo" "foo"  Run standalone assertion
  bashunit doc contains               Show docs for 'contains' assertions
  bashunit init                       Initialize test directory

More info: https://bashunit.com/command-line
EOF
}

function bashunit::console_header::print_test_help() {
  cat <<EOF
Usage: bashunit test [path] [options]
  bashunit [path] [options]

Run test files. If no path is provided, searches for tests in BASHUNIT_DEFAULT_PATH.

Arguments:
  path                        File or directory containing tests
    - Directories: runs all '*test.sh' files
    - Wildcards: supported to match multiple files

Options:
  -a, --assert <fn> <args>    Run a standalone assert function (deprecated: use 'bashunit assert')
  -e, --env, --boot <file>    Load a custom env/bootstrap file  (supports args)
  -f, --filter <name>         Only run tests matching the name
  --tag <name>                Only run tests with matching @tag (repeatable, OR logic)
  --exclude-tag <name>        Skip tests with matching @tag (repeatable, exclude wins)
  --log-junit, --report-junit <file>  Write JUnit XML report
  -j, --jobs <N|auto>         Run tests in parallel with max N concurrent jobs ("auto" = CPU cores)
  -p, --parallel              Run tests in parallel (unlimited concurrency)
  --no-parallel               Run tests sequentially
  -r, --report-html <file>    Write HTML report
  --report-tap <file>         Write TAP version 13 report
  --report-json <file>        Write machine-readable JSON report
  -s, --simple                Simple output (dots)
  --detailed                  Detailed output (default)
  --output <format>           Output format: tap (TAP version 13)
  -R, --run-all               Run all assertions (don't stop on first failure)
  -S, --stop-on-failure       Stop on first failure
  --test-timeout <seconds>    Fail a test if it runs longer than N seconds (0 = off)
  --retry <n>                 Re-run a failed test up to N extra times (0 = off)
  --random-order              Randomize test execution order
  --seed <n>                  Seed for --random-order (reproducible shuffle)
  --shard <i>/<n>             Run shard i of n (split the suite across runners)
  --rerun-failed              Replay only the tests that failed on the last run (.bashunit/last-failed)
  -vvv, --verbose             Show execution details
  --debug [file]              Enable shell debug mode
  --no-output                 Suppress all output
  --failures-only             Only show failures (suppress passed/skipped/incomplete)
  --fail-on-risky             Treat risky tests (no assertions) as failures
  --profile                   Report the slowest tests (count: BASHUNIT_PROFILE_COUNT, default 10)
  --no-progress               Suppress real-time progress, show only final results
  --show-output               Show test output on failure (default: enabled)
  --no-output-on-failure      Hide test output on failure
  --strict                    Enable strict shell mode (set -euo pipefail)
  --skip-env-file             Skip .env loading, use shell environment only
  -l, --login                 Run tests in login shell context
  -w, --watch                 Watch for changes and re-run tests
  --no-color                  Disable colored output (honors NO_COLOR env var)
  -h, --help                  Show this help message

Coverage:
  --coverage                   Enable code coverage tracking
  --coverage-paths <paths>     Source paths to track (default: auto-discover)
  --coverage-exclude <pats>    Patterns to exclude (comma-separated)
  --coverage-report [file]     Output file (default: coverage/lcov.info)
  --coverage-report-html [dir] HTML report (default: coverage/html)
  --coverage-min <pct>         Fail if coverage below percentage
  --no-coverage-report         Disable file output, console only

Examples:
  bashunit test tests/
  bashunit test tests/unit/ --parallel
  bashunit test --filter "user" tests/
  bashunit test -a equals "foo" "foo"
  bashunit test tests/ --coverage
  bashunit test tests/ --coverage --coverage-min 80
  bashunit test tests/ --coverage-report-html
EOF
}

function bashunit::console_header::print_bench_help() {
  cat <<EOF
Usage: bashunit bench [path] [options]

Run benchmark files. Searches for '*bench.sh' files.

Arguments:
  path                        File or directory containing benchmarks

Options:
  -e, --env, --boot <file>    Load a custom env/bootstrap file (supports args)
  -f, --filter <name>         Only run benchmarks matching the name
  -s, --simple                Simple output
  --detailed                  Detailed output (default)
  -vvv, --verbose             Show execution details
  --skip-env-file             Skip .env loading, use shell environment only
  -l, --login                 Run in login shell context
  --no-color                  Disable colored output (honors NO_COLOR env var)
  -h, --help                  Show this help message

Examples:
  bashunit bench
  bashunit bench benchmarks/
  bashunit bench --filter "parse"
EOF
}

function bashunit::console_header::print_doc_help() {
  cat <<EOF
Usage: bashunit doc [filter]

Display documentation for assertion functions.

Arguments:
filter                      Optional filter to show only matching assertions

Examples:
bashunit doc                Show all assertions
bashunit doc equals         Show assertions containing 'equals'
bashunit doc file           Show file-related assertions
EOF
}

function bashunit::console_header::print_init_help() {
  cat <<EOF
Usage: bashunit init [directory]

Initialize a new test directory with sample files.

Arguments:
  directory                   Target directory (default: tests)

Creates:
  - bootstrap.sh              Setup file for test configuration
  - example_test.sh           Sample test file to get started
  - .github/workflows/tests.yml  CI workflow using the official action

Examples:
  bashunit init               Create tests/ directory
  bashunit init spec          Create spec/ directory
EOF
}

function bashunit::console_header::print_learn_help() {
  cat <<EOF
Usage: bashunit learn

Start the interactive learning tutorial.

The tutorial includes 10 progressive lessons:
  1. Basics - Your First Test
  2. Assertions - Testing Different Conditions
  3. Setup & Teardown - Managing Test Lifecycle
  4. Testing Functions - Unit Testing Patterns
  5. Testing Scripts - Integration Testing
  6. Mocking - Test Doubles and Mocks
  7. Spies - Verifying Function Calls
  8. Data Providers - Parameterized Tests
  9. Exit Codes - Testing Success and Failure
  10. Complete Challenge - Real World Scenario

Your progress is saved automatically.
EOF
}

function bashunit::console_header::print_upgrade_help() {
  cat <<EOF
Usage: bashunit upgrade

Upgrade bashunit to the latest version.

Downloads and installs the newest release from GitHub.
EOF
}

function bashunit::console_header::print_assert_help() {
  cat <<EOF
Usage: bashunit assert <function> [args...]
  bashunit assert "<command>" <assertion1> <arg1> [<assertion2> <arg2>...]

Run standalone assertion(s) without creating a test file.

Single assertion:
  bashunit assert equals "foo" "foo"
  bashunit assert same "1" "1"
  bashunit assert contains "world" "hello world"
  bashunit assert exit_code 0 "echo 'success'"

Multiple assertions on command output:
  bashunit assert "echo 'error' && exit 1" exit_code "1" contains "error"
  bashunit assert "./my_script.sh" exit_code "0" contains "success" not_contains "error"

Arguments:
  function                    Assertion function name (with or without 'assert_' prefix)
  command                     Command to execute (for multi-assertion mode)
  assertion                   Assertion name (exit_code, contains, equals, etc.)
  arg                         Expected value for the assertion

Note: You can also use 'bashunit test --assert <fn> <args>' (deprecated).
  The 'bashunit assert' subcommand is the recommended approach.

More info: https://bashunit.com/standalone
EOF
}

function bashunit::console_header::print_watch_help() {
  cat << 'ENDOFHELP'
Usage: bashunit watch [path] [test-options]

Watch .sh files for changes and automatically re-run tests.

Arguments:
  [path]          Directory or file to watch and test (default: .)

Options:
  -h, --help      Show this help message
  Any option accepted by 'bashunit test' is also accepted here.

Requirements:
  Linux:  inotifywait  (sudo apt install inotify-tools)
  macOS:  fswatch      (brew install fswatch)

Examples:
  bashunit watch                      Watch current directory
  bashunit watch tests/               Watch the tests/ directory
  bashunit watch tests/ --filter user Watch and filter by name
  bashunit watch tests/ --simple      Watch with simple output
ENDOFHELP
}

# console_results.sh
# shellcheck disable=SC2155

_BASHUNIT_TOTAL_TESTS_COUNT=0

function bashunit::console_results::render_result() {
  if [ "$(bashunit::state::is_duplicated_test_functions_found)" = true ]; then
    bashunit::console_results::print_execution_time
    printf "%s%s%s\n" "${_BASHUNIT_COLOR_RETURN_ERROR}" "Duplicate test functions found" "${_BASHUNIT_COLOR_DEFAULT}"
    printf "File with duplicate functions: %s\n" "$(bashunit::state::get_file_with_duplicated_function_names)"
    printf "Duplicate functions: %s\n" "$(bashunit::state::get_duplicated_function_names)"
    return 1
  fi

  if bashunit::env::is_tap_output_enabled; then
    printf "1..%d\n" "$_BASHUNIT_TOTAL_TESTS_COUNT"
    if [ "$_BASHUNIT_TESTS_FAILED" -gt 0 ]; then
      return 1
    fi
    return 0
  fi

  if bashunit::env::is_simple_output_enabled; then
    printf "\n\n"
  fi

  # Cache state values to avoid repeated subshell invocations
  local tests_passed=$_BASHUNIT_TESTS_PASSED
  local tests_skipped=$_BASHUNIT_TESTS_SKIPPED
  local tests_incomplete=$_BASHUNIT_TESTS_INCOMPLETE
  local tests_snapshot=$_BASHUNIT_TESTS_SNAPSHOT
  local tests_failed=$_BASHUNIT_TESTS_FAILED
  local tests_risky=$_BASHUNIT_TESTS_RISKY
  local assertions_passed=$_BASHUNIT_ASSERTIONS_PASSED
  local assertions_skipped=$_BASHUNIT_ASSERTIONS_SKIPPED
  local assertions_incomplete=$_BASHUNIT_ASSERTIONS_INCOMPLETE
  local assertions_snapshot=$_BASHUNIT_ASSERTIONS_SNAPSHOT
  local assertions_failed=$_BASHUNIT_ASSERTIONS_FAILED

  local total_tests=0
  total_tests=$((total_tests + tests_passed))
  total_tests=$((total_tests + tests_skipped))
  total_tests=$((total_tests + tests_incomplete))
  total_tests=$((total_tests + tests_snapshot))
  total_tests=$((total_tests + tests_failed))
  total_tests=$((total_tests + tests_risky))

  local total_assertions=0
  total_assertions=$((total_assertions + assertions_passed))
  total_assertions=$((total_assertions + assertions_skipped))
  total_assertions=$((total_assertions + assertions_incomplete))
  total_assertions=$((total_assertions + assertions_snapshot))
  total_assertions=$((total_assertions + assertions_failed))

  printf "%sTests:     %s" "$_BASHUNIT_COLOR_FAINT" "$_BASHUNIT_COLOR_DEFAULT"
  if [ "$tests_passed" -gt 0 ] || [ "$assertions_passed" -gt 0 ]; then
    printf " %s%s passed%s," "$_BASHUNIT_COLOR_PASSED" "$tests_passed" "$_BASHUNIT_COLOR_DEFAULT"
  fi
  if [ "$tests_skipped" -gt 0 ] || [ "$assertions_skipped" -gt 0 ]; then
    printf " %s%s skipped%s," "$_BASHUNIT_COLOR_SKIPPED" "$tests_skipped" "$_BASHUNIT_COLOR_DEFAULT"
  fi
  if [ "$tests_incomplete" -gt 0 ] || [ "$assertions_incomplete" -gt 0 ]; then
    printf " %s%s incomplete%s," "$_BASHUNIT_COLOR_INCOMPLETE" "$tests_incomplete" "$_BASHUNIT_COLOR_DEFAULT"
  fi
  if [ "$tests_snapshot" -gt 0 ] || [ "$assertions_snapshot" -gt 0 ]; then
    printf " %s%s snapshot%s," "$_BASHUNIT_COLOR_SNAPSHOT" "$tests_snapshot" "$_BASHUNIT_COLOR_DEFAULT"
  fi
  if [ "$tests_failed" -gt 0 ] || [ "$assertions_failed" -gt 0 ]; then
    printf " %s%s failed%s," "$_BASHUNIT_COLOR_FAILED" "$tests_failed" "$_BASHUNIT_COLOR_DEFAULT"
  fi
  if [ "$tests_risky" -gt 0 ]; then
    printf " %s%s risky%s," "$_BASHUNIT_COLOR_RISKY" "$tests_risky" "$_BASHUNIT_COLOR_DEFAULT"
  fi
  printf " %s total\n" "$total_tests"

  printf "%sAssertions:%s" "$_BASHUNIT_COLOR_FAINT" "$_BASHUNIT_COLOR_DEFAULT"
  if [ "$tests_passed" -gt 0 ] || [ "$assertions_passed" -gt 0 ]; then
    printf " %s%s passed%s," "$_BASHUNIT_COLOR_PASSED" "$assertions_passed" "$_BASHUNIT_COLOR_DEFAULT"
  fi
  if [ "$tests_skipped" -gt 0 ] || [ "$assertions_skipped" -gt 0 ]; then
    printf " %s%s skipped%s," "$_BASHUNIT_COLOR_SKIPPED" "$assertions_skipped" "$_BASHUNIT_COLOR_DEFAULT"
  fi
  if [ "$tests_incomplete" -gt 0 ] || [ "$assertions_incomplete" -gt 0 ]; then
    printf " %s%s incomplete%s," "$_BASHUNIT_COLOR_INCOMPLETE" "$assertions_incomplete" "$_BASHUNIT_COLOR_DEFAULT"
  fi
  if [ "$tests_snapshot" -gt 0 ] || [ "$assertions_snapshot" -gt 0 ]; then
    printf " %s%s snapshot%s," "$_BASHUNIT_COLOR_SNAPSHOT" "$assertions_snapshot" "$_BASHUNIT_COLOR_DEFAULT"
  fi
  if [ "$tests_failed" -gt 0 ] || [ "$assertions_failed" -gt 0 ]; then
    printf " %s%s failed%s," "$_BASHUNIT_COLOR_FAILED" "$assertions_failed" "$_BASHUNIT_COLOR_DEFAULT"
  fi
  printf " %s total\n" "$total_assertions"

  if [ "$tests_failed" -gt 0 ]; then
    printf "\n%s%s%s\n" "$_BASHUNIT_COLOR_RETURN_ERROR" " Some tests failed " "$_BASHUNIT_COLOR_DEFAULT"
    bashunit::console_results::print_execution_time
    return 1
  fi

  if [ "$tests_risky" -gt 0 ]; then
    printf "\n%s%s%s\n" "$_BASHUNIT_COLOR_RETURN_RISKY" " Some tests risky (no assertions) " "$_BASHUNIT_COLOR_DEFAULT"
    bashunit::console_results::print_execution_time
    return 0
  fi

  if [ "$tests_incomplete" -gt 0 ]; then
    printf "\n%s%s%s\n" "$_BASHUNIT_COLOR_RETURN_INCOMPLETE" " Some tests incomplete " "$_BASHUNIT_COLOR_DEFAULT"
    bashunit::console_results::print_execution_time
    return 0
  fi

  if [ "$tests_skipped" -gt 0 ]; then
    printf "\n%s%s%s\n" "$_BASHUNIT_COLOR_RETURN_SKIPPED" " Some tests skipped " "$_BASHUNIT_COLOR_DEFAULT"
    bashunit::console_results::print_execution_time
    return 0
  fi

  if [ "$tests_snapshot" -gt 0 ]; then
    printf "\n%s%s%s\n" "$_BASHUNIT_COLOR_RETURN_SNAPSHOT" " Some snapshots created " "$_BASHUNIT_COLOR_DEFAULT"
    bashunit::console_results::print_execution_time
    return 0
  fi

  if [ "$total_tests" -eq 0 ]; then
    printf "\n%s%s%s\n" "$_BASHUNIT_COLOR_RETURN_ERROR" " No tests found " "$_BASHUNIT_COLOR_DEFAULT"
    bashunit::console_results::print_execution_time
    return 1
  fi

  printf "\n%s%s%s\n" "$_BASHUNIT_COLOR_RETURN_SUCCESS" " All tests passed " "$_BASHUNIT_COLOR_DEFAULT"
  bashunit::console_results::print_execution_time
  return 0
}

function bashunit::console_results::print_execution_time() {
  if ! bashunit::env::is_total_execution_time_enabled; then
    return
  fi

  local time
  time=$(bashunit::clock::total_runtime_in_milliseconds)
  # Strip decimal portion (integer truncation, Bash 3.0 compatible)
  time="${time%%.*}"
  time="${time:-0}"

  if [ "$time" -lt 1000 ]; then
    printf "${_BASHUNIT_COLOR_BOLD}%s${_BASHUNIT_COLOR_DEFAULT}\n" \
      "Time taken: ${time}ms"
    return
  fi

  local time_in_seconds=$((time / 1000))

  if [ "$time_in_seconds" -ge 60 ]; then
    local minutes=$((time_in_seconds / 60))
    local seconds=$((time_in_seconds % 60))
    printf "${_BASHUNIT_COLOR_BOLD}%s${_BASHUNIT_COLOR_DEFAULT}\n" \
      "Time taken: ${minutes}m ${seconds}s"
    return
  fi

  local integer_part=$((time / 1000))
  local decimal_part=$(( (time % 1000) / 10 ))
  local formatted_seconds
  formatted_seconds=$(printf "%d.%02d" "$integer_part" "$decimal_part")

  printf "${_BASHUNIT_COLOR_BOLD}%s${_BASHUNIT_COLOR_DEFAULT}\n" \
    "Time taken: ${formatted_seconds}s"
}

function bashunit::console_results::format_duration() {
  local duration_ms="$1"

  if [ "$duration_ms" -ge 60000 ]; then
    local time_in_seconds=$((duration_ms / 1000))
    local minutes=$((time_in_seconds / 60))
    local seconds=$((time_in_seconds % 60))
    echo "${minutes}m ${seconds}s"
  elif [ "$duration_ms" -ge 1000 ]; then
    local integer_part=$((duration_ms / 1000))
    local decimal_part=$(( (duration_ms % 1000) / 10 ))
    local formatted_seconds
    formatted_seconds=$(printf "%d.%02d" "$integer_part" "$decimal_part")
    echo "${formatted_seconds}s"
  else
    echo "${duration_ms}ms"
  fi
}

function bashunit::console_results::print_hook_completed() {
  local hook_name="$1"
  local duration_ms="$2"

  if bashunit::env::is_simple_output_enabled; then
    return
  fi

  if bashunit::env::is_failures_only_enabled; then
    return
  fi

  if bashunit::env::is_no_progress_enabled; then
    return
  fi

  if bashunit::env::is_tap_output_enabled; then
    return
  fi

  if bashunit::parallel::is_enabled; then
    return
  fi

  local line
  line=$(printf "%s● %s%s" \
    "$_BASHUNIT_COLOR_PASSED" "$hook_name" "$_BASHUNIT_COLOR_DEFAULT")

  local time_display
  time_display=$(bashunit::console_results::format_duration "$duration_ms")

  printf "%s\n" "$(bashunit::str::rpad "$line" "$time_display")"
}

function bashunit::console_results::print_successful_test() {
  local test_name=$1
  shift
  local duration=${1:-"0"}
  shift

  # Pure-bash concatenation (the printf only did %s substitution) to avoid a
  # $(...) fork per passing test (#764).
  local line
  if [ -z "$*" ]; then
    line="${_BASHUNIT_COLOR_PASSED}✓ Passed${_BASHUNIT_COLOR_DEFAULT}: ${test_name}"
  else
    local quoted_args=""
    local arg
    for arg in "$@"; do
      if [ -z "$quoted_args" ]; then
        quoted_args="'$arg'"
      else
        quoted_args="$quoted_args, '$arg'"
      fi
    done
    line="${_BASHUNIT_COLOR_PASSED}✓ Passed${_BASHUNIT_COLOR_DEFAULT}: ${test_name} (${quoted_args})"
  fi

  # Retry annotation (e.g. " (retry 1/2)") set by the runner when a test only
  # passed after retrying; empty in the common no-retry path.
  line="${line}${_BASHUNIT_RETRY_NOTE:-}"

  local full_line=$line
  if bashunit::env::is_show_execution_time_enabled; then
    local time_display
    if [ "$duration" -ge 60000 ]; then
      local time_in_seconds=$((duration / 1000))
      local minutes=$((time_in_seconds / 60))
      local seconds=$((time_in_seconds % 60))
      time_display="${minutes}m ${seconds}s"
    elif [ "$duration" -ge 1000 ]; then
      local integer_part=$((duration / 1000))
      local decimal_part=$(( (duration % 1000) / 10 ))
      local formatted_seconds
      formatted_seconds=$(printf "%d.%02d" "$integer_part" "$decimal_part")
      time_display="${formatted_seconds}s"
    else
      time_display="${duration}ms"
    fi
    full_line="$(bashunit::str::rpad "$line" "$time_display")"
  fi

  bashunit::state::print_line "successful" "$full_line"
}

##
# Returns a faint "    at <file>:<line>" suffix (preceded by a newline) pointing
# at the currently running test function, or an empty string when the location
# is unknown. Used to append source context to failure output.
##
function bashunit::console_results::test_location_suffix() {
  local location=${_BASHUNIT_TEST_LOCATION:-}
  if [ -z "$location" ]; then
    return 0
  fi

  printf "\n    ${_BASHUNIT_COLOR_FAINT}at %s${_BASHUNIT_COLOR_DEFAULT}" "$location"
}

function bashunit::console_results::print_failure_message() {
  local test_name=$1
  local failure_message=$2

  local line
  line="$(printf "\
${_BASHUNIT_COLOR_FAILED}✗ Failed${_BASHUNIT_COLOR_DEFAULT}: %s
    ${_BASHUNIT_COLOR_FAINT}Message:${_BASHUNIT_COLOR_DEFAULT} \
${_BASHUNIT_COLOR_BOLD}'%s'${_BASHUNIT_COLOR_DEFAULT}\n" \
    "${test_name}" "${failure_message}")"

  line="$line$(bashunit::console_results::test_location_suffix)"

  bashunit::state::print_line "failure" "$line"
}

##
# Renders a git word-diff of two files, indented, and echoes it. Colorized
# unless --no-color is active. Empty when git is unavailable or files match.
# Shared by the snapshot-failure and multiline assert-failure renderers.
# Arguments: $1 expected file path, $2 actual file path
##
function bashunit::console_results::render_diff() {
  local expected_file=$1
  local actual_file=$2

  if ! bashunit::dependencies::has_git; then
    return 0
  fi

  local color_flag="--color=always"
  if bashunit::env::is_no_color_enabled; then
    color_flag="--color=never"
  fi

  # `git diff` exits non-zero when the files differ; the `|| true` keeps that
  # from tripping `set -e`/`pipefail` under --strict. `tail -n +6` drops git's
  # header lines; `sed` indents the body.
  git diff --no-index --word-diff "$color_flag" \
    "$expected_file" "$actual_file" 2>/dev/null |
    tail -n +6 | sed "s/^/    /" || true
}

##
# Echoes a value's first line, appending an ellipsis when it spans several
# lines. Used to keep the inline quoted value on one line when a diff follows.
##
function bashunit::console_results::first_line_ellipsis() {
  local text=$1
  local first="${text%%$'\n'*}"
  if [ "$first" != "$text" ]; then
    printf '%s…' "$first"
  else
    printf '%s' "$text"
  fi
}

function bashunit::console_results::print_failed_test() {
  local function_name=$1
  local expected=$2
  local failure_condition_message=$3
  local actual=$4
  local extra_key=${5-}
  local extra_value=${6-}

  # For multiline values, render a unified diff below the header (git required,
  # opt out with BASHUNIT_NO_DIFF). Single-line output stays byte-identical.
  local show_diff=false
  case "$expected$actual" in
  *$'\n'*)
    if bashunit::env::is_diff_enabled && bashunit::dependencies::has_git; then
      show_diff=true
    fi
    ;;
  esac

  local display_expected=$expected
  local display_actual=$actual
  if [ "$show_diff" = true ]; then
    display_expected="$(bashunit::console_results::first_line_ellipsis "$expected")"
    display_actual="$(bashunit::console_results::first_line_ellipsis "$actual")"
  fi

  local line
  line="$(printf "\
${_BASHUNIT_COLOR_FAILED}✗ Failed${_BASHUNIT_COLOR_DEFAULT}: %s
    ${_BASHUNIT_COLOR_FAINT}Expected${_BASHUNIT_COLOR_DEFAULT} ${_BASHUNIT_COLOR_BOLD}'%s'${_BASHUNIT_COLOR_DEFAULT}
    ${_BASHUNIT_COLOR_FAINT}%s${_BASHUNIT_COLOR_DEFAULT} ${_BASHUNIT_COLOR_BOLD}'%s'${_BASHUNIT_COLOR_DEFAULT}\n" \
    "${function_name}" "${display_expected}" "${failure_condition_message}" "${display_actual}")"

  if [ "$show_diff" = true ]; then
    local _expected_file _actual_file
    _expected_file="$(bashunit::temp_file diff_expected)"
    _actual_file="$(bashunit::temp_file diff_actual)"
    printf '%s\n' "$expected" >"$_expected_file"
    printf '%s\n' "$actual" >"$_actual_file"
    line="$line
$(bashunit::console_results::render_diff "$_expected_file" "$_actual_file")"
    rm -f "$_expected_file" "$_actual_file"
  fi

  if [ -n "$extra_key" ]; then
    line="$line$(printf "\

    ${_BASHUNIT_COLOR_FAINT}%s${_BASHUNIT_COLOR_DEFAULT} ${_BASHUNIT_COLOR_BOLD}'%s'${_BASHUNIT_COLOR_DEFAULT}\n" \
      "${extra_key}" "${extra_value}")"
  fi

  line="$line$(bashunit::console_results::test_location_suffix)"

  bashunit::state::print_line "failed" "$line"
}

function bashunit::console_results::print_failed_snapshot_test() {
  local function_name=$1
  local snapshot_file=$2
  local actual_content=${3-}

  local line
  line="$(printf "${_BASHUNIT_COLOR_FAILED}✗ Failed${_BASHUNIT_COLOR_DEFAULT}: %s
    ${_BASHUNIT_COLOR_FAINT}Expected to match the snapshot${_BASHUNIT_COLOR_DEFAULT}\n" "$function_name")"

  if bashunit::dependencies::has_git; then
    local actual_file="${snapshot_file}.tmp"
    echo "$actual_content" >"$actual_file"

    line="$line$(bashunit::console_results::render_diff "$snapshot_file" "$actual_file")"
    rm "$actual_file"
  else
    line="$line$(bashunit::console_results::snapshot_line_diff \
      "$(cat "$snapshot_file")" "$actual_content")"
  fi

  bashunit::state::print_line "failed_snapshot" "$line"
}

##
# Renders a readable line-by-line diff between an expected snapshot and the
# actual content, used as a fallback when git is unavailable. Common lines are
# shown as context, expected-only lines are prefixed with '-' and actual-only
# lines with '+'. Bash 3.0+ compatible (no mapfile, no associative arrays).
# Arguments: $1 expected content, $2 actual content
##
function bashunit::console_results::snapshot_line_diff() {
  local expected=$1
  local actual=$2

  # Explicit empty-array init so referencing the arrays is safe under `set -u`
  # on Bash 4.4+ (Bash 3.x is lenient; newer Bash treats an unset array as unbound).
  # Declare and assign separately: bash 3.0 does not expand a compound array
  # assignment attached to `local`, it stores the literal "()" as element 0.
  local expected_lines actual_lines
  expected_lines=()
  actual_lines=()
  local _line=""
  local i=0
  while IFS= read -r _line || [ -n "$_line" ]; do
    expected_lines[i]=$_line
    i=$((i + 1))
  done <<EOF
$expected
EOF
  local expected_count=$i

  i=0
  while IFS= read -r _line || [ -n "$_line" ]; do
    actual_lines[i]=$_line
    i=$((i + 1))
  done <<EOF
$actual
EOF
  local actual_count=$i

  local max=$expected_count
  if [ "$actual_count" -gt "$max" ]; then
    max=$actual_count
  fi

  local out=""
  i=0
  while [ "$i" -lt "$max" ]; do
    local e="" a="" has_e=0 has_a=0
    if [ "$i" -lt "$expected_count" ]; then
      e=${expected_lines[i]:-}
      has_e=1
    fi
    if [ "$i" -lt "$actual_count" ]; then
      a=${actual_lines[i]:-}
      has_a=1
    fi

    if [ "$has_e" = 1 ] && [ "$has_a" = 1 ] && [ "$e" = "$a" ]; then
      out="$out$(printf "\n    ${_BASHUNIT_COLOR_FAINT}  %s${_BASHUNIT_COLOR_DEFAULT}" "$e")"
    else
      if [ "$has_e" = 1 ]; then
        out="$out$(printf "\n    ${_BASHUNIT_COLOR_FAILED}- %s${_BASHUNIT_COLOR_DEFAULT}" "$e")"
      fi
      if [ "$has_a" = 1 ]; then
        out="$out$(printf "\n    ${_BASHUNIT_COLOR_PASSED}+ %s${_BASHUNIT_COLOR_DEFAULT}" "$a")"
      fi
    fi
    i=$((i + 1))
  done

  printf "%s" "$out"
}

function bashunit::console_results::print_skipped_test() {
  local function_name=$1
  local reason=${2-}

  local line
  line="$(printf "${_BASHUNIT_COLOR_SKIPPED}↷ Skipped${_BASHUNIT_COLOR_DEFAULT}: %s\n" "${function_name}")"

  if [ -n "$reason" ]; then
    line="$line$(printf "${_BASHUNIT_COLOR_FAINT}    %s${_BASHUNIT_COLOR_DEFAULT}\n" "${reason}")"
  fi

  bashunit::state::print_line "skipped" "$line"
}

function bashunit::console_results::print_incomplete_test() {
  local function_name=$1
  local pending=${2-}

  local line
  line="$(printf "${_BASHUNIT_COLOR_INCOMPLETE}✒ Incomplete${_BASHUNIT_COLOR_DEFAULT}: %s\n" "${function_name}")"

  if [ -n "$pending" ]; then
    line="$line$(printf "${_BASHUNIT_COLOR_FAINT}    %s${_BASHUNIT_COLOR_DEFAULT}\n" "${pending}")"
  fi

  bashunit::state::print_line "incomplete" "$line"
}

function bashunit::console_results::print_snapshot_test() {
  local function_name=$1
  local test_name
  test_name=$(bashunit::helper::normalize_test_function_name "$function_name")

  local line
  line="$(printf "${_BASHUNIT_COLOR_SNAPSHOT}✎ Snapshot${_BASHUNIT_COLOR_DEFAULT}: %s\n" "${test_name}")"

  bashunit::state::print_line "snapshot" "$line"
}

function bashunit::console_results::print_risky_test() {
  local test_name=$1
  local duration=${2:-"0"}

  local line
  line=$(printf "%s⚠ Risky%s: %s" "$_BASHUNIT_COLOR_RISKY" "$_BASHUNIT_COLOR_DEFAULT" "$test_name")

  local full_line=$line
  if bashunit::env::is_show_execution_time_enabled; then
    local time_display
    time_display=$(bashunit::console_results::format_duration "$duration")
    full_line="$(bashunit::str::rpad "$line" "$time_display")"
  fi

  bashunit::state::print_line "risky" "$full_line"
}

function bashunit::console_results::print_error_test() {
  local function_name=$1
  local error="$2"
  local raw_output="${3:-}"

  local test_name
  test_name=$(bashunit::helper::normalize_test_function_name "$function_name")

  local line
  line="$(printf "${_BASHUNIT_COLOR_FAILED}✗ Error${_BASHUNIT_COLOR_DEFAULT}: %s
    ${_BASHUNIT_COLOR_FAINT}%s${_BASHUNIT_COLOR_DEFAULT}\n" "${test_name}" "${error}")"

  if [ -n "$raw_output" ] && bashunit::env::is_show_output_on_failure_enabled; then
    line="$line$(printf "    %sOutput:%s\n" "${_BASHUNIT_COLOR_FAINT}" "${_BASHUNIT_COLOR_DEFAULT}")"
    local output_line
    while IFS= read -r output_line; do
      line="$line$(printf "      %s\n" "$output_line")"
    done <<<"$raw_output"
  fi

  line="$line$(bashunit::console_results::test_location_suffix)"

  bashunit::state::print_line "error" "$line"
}

function bashunit::console_results::print_failing_tests_and_reset() {
  if [ -s "$FAILURES_OUTPUT_PATH" ]; then
    local total_failed
    total_failed=$(bashunit::state::get_tests_failed)

    if bashunit::env::is_simple_output_enabled; then
      printf "\n\n"
    fi

    if [ "$total_failed" -eq 1 ]; then
      echo -e "${_BASHUNIT_COLOR_BOLD}There was 1 failure:${_BASHUNIT_COLOR_DEFAULT}\n"
    else
      echo -e "${_BASHUNIT_COLOR_BOLD}There were $total_failed failures:${_BASHUNIT_COLOR_DEFAULT}\n"
    fi

    sed '${/^$/d;}' "$FAILURES_OUTPUT_PATH" | sed 's/^/|/'
    rm "$FAILURES_OUTPUT_PATH"

    echo ""
  fi
}

##
# Prints the slowest tests recorded during the run, sorted by duration
# descending, limited to BASHUNIT_PROFILE_COUNT entries. Reads the
# tab-separated records appended to PROFILE_OUTPUT_PATH (duration, name, file).
##
function bashunit::console_results::print_profile_and_reset() {
  if [ ! -s "$PROFILE_OUTPUT_PATH" ]; then
    rm -f "$PROFILE_OUTPUT_PATH"
    return
  fi

  local count="${BASHUNIT_PROFILE_COUNT:-10}"

  echo -e "\n${_BASHUNIT_COLOR_BOLD}Slowest tests:${_BASHUNIT_COLOR_DEFAULT}"

  local duration name file formatted
  # -rn on the first (numeric) field; head limits to the requested count.
  while IFS=$'\t' read -r duration name file; do
    formatted=$(bashunit::console_results::format_duration "$duration")
    printf "  %s\t%s (%s)\n" "$formatted" "$name" "$file"
  done < <(sort -t"$(printf '\t')" -k1 -rn "$PROFILE_OUTPUT_PATH" | head -n "$count")

  echo ""

  rm -f "$PROFILE_OUTPUT_PATH"
}

function bashunit::console_results::print_skipped_tests_and_reset() {
  if [ -s "$SKIPPED_OUTPUT_PATH" ] && bashunit::env::is_show_skipped_enabled; then
    local total_skipped
    total_skipped=$(bashunit::state::get_tests_skipped)

    if bashunit::env::is_simple_output_enabled; then
      printf "\n"
    fi

    if [ "$total_skipped" -eq 1 ]; then
      echo -e "${_BASHUNIT_COLOR_BOLD}There was 1 skipped test:${_BASHUNIT_COLOR_DEFAULT}\n"
    else
      echo -e "${_BASHUNIT_COLOR_BOLD}There were $total_skipped skipped tests:${_BASHUNIT_COLOR_DEFAULT}\n"
    fi

    tr -d '\r' <"$SKIPPED_OUTPUT_PATH" | sed '/^[[:space:]]*$/d' | sed 's/^/|/'
    rm "$SKIPPED_OUTPUT_PATH"

    echo ""
  fi
}

function bashunit::console_results::print_incomplete_tests_and_reset() {
  if [ -s "$INCOMPLETE_OUTPUT_PATH" ] && bashunit::env::is_show_incomplete_enabled; then
    local total_incomplete
    total_incomplete=$(bashunit::state::get_tests_incomplete)

    if bashunit::env::is_simple_output_enabled; then
      printf "\n"
    fi

    if [ "$total_incomplete" -eq 1 ]; then
      echo -e "${_BASHUNIT_COLOR_BOLD}There was 1 incomplete test:${_BASHUNIT_COLOR_DEFAULT}\n"
    else
      echo -e "${_BASHUNIT_COLOR_BOLD}There were $total_incomplete incomplete tests:${_BASHUNIT_COLOR_DEFAULT}\n"
    fi

    tr -d '\r' <"$INCOMPLETE_OUTPUT_PATH" | sed '/^[[:space:]]*$/d' | sed 's/^/|/'
    rm "$INCOMPLETE_OUTPUT_PATH"

    echo ""
  fi
}

function bashunit::console_results::print_risky_tests_and_reset() {
  if [ -s "$RISKY_OUTPUT_PATH" ]; then
    local total_risky
    total_risky=$(bashunit::state::get_tests_risky)

    if bashunit::env::is_simple_output_enabled; then
      printf "\n"
    fi

    if [ "$total_risky" -eq 1 ]; then
      echo -e "${_BASHUNIT_COLOR_BOLD}There was 1 risky test:${_BASHUNIT_COLOR_DEFAULT}\n"
    else
      echo -e "${_BASHUNIT_COLOR_BOLD}There were $total_risky risky tests:${_BASHUNIT_COLOR_DEFAULT}\n"
    fi

    tr -d '\r' <"$RISKY_OUTPUT_PATH" | sed '/^[[:space:]]*$/d' | sed 's/^/|/'
    rm "$RISKY_OUTPUT_PATH"

    echo ""
  fi
}

# helpers.sh

declare -r BASHUNIT_GIT_REPO="https://github.com/TypedDevs/bashunit"

#
# Walks up the call stack to find the first function that looks like a test function.
# A test function is one that starts with "test_" or "test" (camelCase).
# If no test function is found, falls back to the caller of the assertion function.
#
# @param $1 number Optional fallback depth (default: 2, i.e., the caller of the assertion)
#
# @return string The test function name, or fallback function name
#
_BASHUNIT_HELPER_TESTFN_OUT=""

#
# Return-slot variant of find_test_function_name: writes the result into
# _BASHUNIT_HELPER_TESTFN_OUT with no fork. Must be called at the SAME stack
# depth the echoing version would be captured at, so the fallback_depth default
# keeps pointing at the caller of the assertion (see the echoing wrapper below).
#
function bashunit::helper::find_test_function_name_to_slot() {
  local fallback_depth="${1:-2}"
  local i
  for ((i = 0; i < ${#FUNCNAME[@]}; i++)); do
    local fn="${FUNCNAME[$i]}"
    case "$fn" in
    test_* | test[A-Z]*)
      _BASHUNIT_HELPER_TESTFN_OUT=$fn
      return
      ;;
    esac
  done
  _BASHUNIT_HELPER_TESTFN_OUT=${FUNCNAME[$fallback_depth]:-}
}

function bashunit::helper::find_test_function_name() {
  local fallback_depth="${1:-2}"
  local i
  for ((i = 0; i < ${#FUNCNAME[@]}; i++)); do
    local fn="${FUNCNAME[$i]}"
    # Check if function starts with "test_" or "test" followed by uppercase.
    # Pure-bash globs avoid forking echo+grep on every call-stack frame (hot path).
    case "$fn" in
    test_* | test[A-Z]*)
      echo "$fn"
      return
      ;;
    esac
  done
  # No test function found, use fallback (caller of the assertion)
  # FUNCNAME[0] = bashunit::helper::find_test_function_name
  # FUNCNAME[1] = the assertion function (e.g., assert_same)
  # FUNCNAME[2] = caller of the assertion
  echo "${FUNCNAME[$fallback_depth]:-}"
}

#
# @param $1 string Eg: "test_some_logic_camelCase"
#
# @return string Eg: "Some logic camelCase"
#
_BASHUNIT_HELPER_NORMALIZED_OUT=""

#
# Return-slot variant of normalize_test_function_name: writes the result into
# _BASHUNIT_HELPER_NORMALIZED_OUT with no fork, removing the command-substitution
# fork at the (failure-path) call sites in the assertion layer.
#
# @param $1 string Eg: "test_some_logic_camelCase"
# @param $2 string Optional interpolated name
#
function bashunit::helper::normalize_test_function_name_to_slot() {
  local original_fn_name="${1-}"
  local interpolated_fn_name="${2-}"

  # Read the reserved-namespace state globals directly (the accessors just echo
  # them) to avoid a nested subshell fork on this per-test hot path (#764).
  local custom_title="${_BASHUNIT_TEST_TITLE:-}"
  if [ -n "$custom_title" ]; then
    _BASHUNIT_HELPER_NORMALIZED_OUT=$custom_title
    return
  fi

  if [ -z "${interpolated_fn_name-}" ]; then
    case "${original_fn_name}" in
    *"::"*)
      local state_interpolated_fn_name="${_BASHUNIT_CURRENT_TEST_INTERPOLATED_NAME:-}"

      if [ -n "$state_interpolated_fn_name" ]; then
        interpolated_fn_name="$state_interpolated_fn_name"
      fi
      ;;
    esac
  fi

  if [ -n "${interpolated_fn_name-}" ]; then
    original_fn_name="$interpolated_fn_name"
  fi

  local result

  # Remove the first "test_" prefix, if present
  result="${original_fn_name#test_}"
  # If no "test_" was removed (e.g., "testFoo"), remove the "test" prefix
  if [ "$result" = "$original_fn_name" ]; then
    result="${original_fn_name#test}"
  fi
  # Replace underscores with spaces
  result="${result//_/ }"
  # Capitalize the first letter (bash 3.0 compatible, no subprocess)
  local first_char="${result:0:1}"
  case "$first_char" in
  a) first_char='A' ;; b) first_char='B' ;; c) first_char='C' ;; d) first_char='D' ;;
  e) first_char='E' ;; f) first_char='F' ;; g) first_char='G' ;; h) first_char='H' ;;
  i) first_char='I' ;; j) first_char='J' ;; k) first_char='K' ;; l) first_char='L' ;;
  m) first_char='M' ;; n) first_char='N' ;; o) first_char='O' ;; p) first_char='P' ;;
  q) first_char='Q' ;; r) first_char='R' ;; s) first_char='S' ;; t) first_char='T' ;;
  u) first_char='U' ;; v) first_char='V' ;; w) first_char='W' ;; x) first_char='X' ;;
  y) first_char='Y' ;; z) first_char='Z' ;;
  esac
  result="${first_char}${result:1}"

  _BASHUNIT_HELPER_NORMALIZED_OUT=$result
}

#
# @param $1 string Eg: "test_some_logic_camelCase"
#
# @return string Eg: "Some logic camelCase"
#
function bashunit::helper::normalize_test_function_name() {
  bashunit::helper::normalize_test_function_name_to_slot "${1-}" "${2-}"
  echo "$_BASHUNIT_HELPER_NORMALIZED_OUT"
}

function bashunit::helper::escape_single_quotes() {
  local value="$1"
  # shellcheck disable=SC1003
  echo "${value//\'/'\'\\''\'}"
}

function bashunit::helper::interpolate_function_name() {
  local function_name="$1"
  shift

  # Placeholders look like "::N::", so a name without "::" can never interpolate.
  # Short-circuit to skip the per-arg escape_single_quotes forks in that case.
  case "$function_name" in
  *::*) ;;
  *)
    echo "$function_name"
    return
    ;;
  esac

  local -a args
  local args_count=$#
  args=("$@")
  local result="$function_name"

  local i
  for ((i = 0; i < args_count; i++)); do
    local placeholder="::$((i + 1))::"
    # shellcheck disable=SC2155
    local value="$(bashunit::helper::escape_single_quotes "${args[$i]}")"
    value="'$value'"
    result="${result//${placeholder}/${value}}"
  done

  echo "$result"
}

function bashunit::helper::encode_base64() {
  local value="$1"

  # Handle empty string specially - base64 of "" is "", which gets lost in line parsing
  if [ -z "$value" ]; then
    printf '%s' "_BASHUNIT_EMPTY_"
    return
  fi

  if [ "$_BASHUNIT_BASE64_WRAP_FLAG" = true ]; then
    printf '%s' "$value" | base64 -w 0
  elif command -v base64 >/dev/null; then
    printf '%s' "$value" | base64 | tr -d '\n'
  else
    printf '%s' "$value" | openssl enc -base64 -A
  fi
}

function bashunit::helper::decode_base64() {
  local value="$1"

  # Empty input decodes to empty; short-circuit to skip the base64 fork (#762).
  if [ -z "$value" ] || [ "$value" = "_BASHUNIT_EMPTY_" ]; then
    printf ''
    return
  fi

  if command -v base64 >/dev/null; then
    printf '%s' "$value" | base64 -d
  else
    printf '%s' "$value" | openssl enc -d -base64
  fi
}

function bashunit::helper::check_duplicate_functions() {
  local script="$1"

  # Handle directory changes in set_up_before_script (issue #529)
  if [ ! -f "$script" ] && [ -n "${BASHUNIT_WORKING_DIR:-}" ]; then
    script="$BASHUNIT_WORKING_DIR/$script"
  fi

  # One awk pass over the file finds each test-function definition and emits only
  # the names seen more than once, folding the former grep + awk + sort + uniq
  # chain into a single awk (#761). The END block insertion-sorts the (tiny,
  # usually empty) duplicate list itself, so no `sort` fork is needed to keep
  # the output deterministic.
  local duplicates
  duplicates=$(awk '
    /^[[:space:]]*(function[[:space:]]+)?test[a-zA-Z_][a-zA-Z0-9_]*[[:space:]]*\(\)[[:space:]]*\{/ {
      for (i = 1; i <= NF; i++) {
        if ($i ~ /^test[a-zA-Z_][a-zA-Z0-9_]*\(\)$/) {
          name = $i
          gsub(/\(\)/, "", name)
          if (++seen[name] == 2) {
            dup[name] = 1
          }
          break
        }
      }
    }
    END {
      n = 0
      for (name in dup) {
        names[++n] = name
      }
      for (i = 2; i <= n; i++) {
        v = names[i]
        j = i - 1
        while (j >= 1 && names[j] > v) {
          names[j + 1] = names[j]
          j--
        }
        names[j + 1] = v
      }
      for (i = 1; i <= n; i++) {
        print names[i]
      }
    }
  ' "$script")
  if [ -n "$duplicates" ]; then
    bashunit::state::set_duplicated_functions_merged "$script" "$duplicates"
    return 1
  fi
  return 0
}

#
# @param $1 string Eg: "prefix"
# @param $2 string Eg: "filter"
# @param $3 array Eg: "[fn1, fn2, prefix_filter_fn3, fn4, ...]"
#
# @return array Eg: "[prefix_filter_fn3, ...]" The filtered functions with prefix
#
function bashunit::helper::get_functions_to_run() {
  local prefix=$1
  local filter=${2/test_/}
  local function_names=$3

  local filtered_functions=""

  local fn
  for fn in $function_names; do
    local _fn_match=false
    case "$fn" in ${prefix}_*${filter}*) _fn_match=true ;; esac
    if [ "$_fn_match" = true ]; then
      local _dup=false
      case "$filtered_functions" in *" $fn"*) _dup=true ;; esac
      if [ "$_dup" = true ]; then
        return 1
      fi
      filtered_functions="$filtered_functions $fn"
    fi
  done

  echo "${filtered_functions# }"
}

#
# @param $1 string Eg: "do_something"
#
function bashunit::helper::execute_function_if_exists() {
  local fn_name="$1"

  if declare -F "$fn_name" >/dev/null 2>&1; then
    "$fn_name"
    return $?
  fi

  return 0
}

#
# @param $1 string Eg: "do_something"
#
function bashunit::helper::unset_if_exists() {
  unset "$1" 2>/dev/null
}

function bashunit::helper::find_files_recursive() {
  ## Remove trailing slash using parameter expansion
  local path="${1%%/}"
  local pattern="${2:-*[tT]est.sh}"

  # When the pattern targets *.sh test files, also match the .bash variant. Both
  # the plain `*test.sh` and the default glob `*[tT]est.sh` end in `.sh`; a case
  # match on either (no grep fork) is enough to decide.
  local alt_pattern=""
  case "$pattern" in
  *test.sh | *'[tT]est.sh') alt_pattern="${pattern%.sh}.bash" ;;
  esac

  local _has_glob=false
  case "$path" in *"*"*) _has_glob=true ;; esac
  if [ "$_has_glob" = true ]; then
    if [ -n "$alt_pattern" ]; then
      eval "find $path -type f \( -name \"$pattern\" -o -name \"$alt_pattern\" \)" | sort -u
    else
      eval "find $path -type f -name \"$pattern\"" | sort -u
    fi
  elif [ -d "$path" ]; then
    if [ -n "$alt_pattern" ]; then
      find "$path" -type f \( -name "$pattern" -o -name "$alt_pattern" \) | sort -u
    else
      find "$path" -type f -name "$pattern" | sort -u
    fi
  else
    echo "$path"
  fi
}

_BASHUNIT_HELPER_VARNAME_OUT=""

# Return-slot variant of normalize_variable_name: writes the result into
# _BASHUNIT_HELPER_VARNAME_OUT with no fork, for hot-path callers that would
# otherwise capture it with a command substitution (e.g. snapshot path
# resolution, which calls it twice per assertion).
function bashunit::helper::normalize_variable_name_to_slot() {
  local input_string="$1"
  local normalized_string="${input_string//[^a-zA-Z0-9_]/_}"

  # First character must be alpha or underscore. Empty string also gets a `_`
  # prefix to satisfy the same identifier rule. Uses pure-bash globbing to
  # avoid a per-call grep fork (called once per test via generate_id).
  case "${normalized_string:0:1}" in
  [a-zA-Z_]) ;;
  *) normalized_string="_$normalized_string" ;;
  esac

  _BASHUNIT_HELPER_VARNAME_OUT=$normalized_string
}

function bashunit::helper::normalize_variable_name() {
  bashunit::helper::normalize_variable_name_to_slot "$1"
  builtin echo "$_BASHUNIT_HELPER_VARNAME_OUT"
}

# Provider map for the most recently scanned script. Scanning a file once and
# caching the test-function -> provider-function pairs replaces a per-test
# grep+sed fork with a pure-bash lookup on the hot path (issue #763).
_BASHUNIT_PROVIDER_MAP_SCRIPT=""
_BASHUNIT_PROVIDER_MAP_FNS=()
_BASHUNIT_PROVIDER_MAP_PROVIDERS=()
_BASHUNIT_PROVIDER_FN_OUT=""
# Set true when the scanned file carries the "# bashunit: no-parallel-tests"
# opt-out; detected in the same awk pass to avoid a per-file grep fork (#774).
_BASHUNIT_PROVIDER_MAP_NO_PARALLEL=false

#
# Resolves a script path, applying the issue #529 working-dir fallback.
# Writes the resolved path into _BASHUNIT_PROVIDER_RESOLVED_OUT (empty if unreadable).
#
_BASHUNIT_PROVIDER_RESOLVED_OUT=""
function bashunit::helper::_resolve_provider_script() {
  local script=$1
  # Handle directory changes in set_up_before_script (issue #529)
  if [ ! -f "$script" ] && [ -n "${BASHUNIT_WORKING_DIR:-}" ]; then
    script="$BASHUNIT_WORKING_DIR/$script"
  fi
  if [ ! -f "$script" ]; then
    _BASHUNIT_PROVIDER_RESOLVED_OUT=""
    return
  fi
  _BASHUNIT_PROVIDER_RESOLVED_OUT=$script
}

#
# Scans a script once and caches its test-function -> provider-function pairs.
# Memoized by resolved path, so repeated calls for the same file do not rescan.
#
# @param $1 string Path to the test script
#
function bashunit::helper::build_provider_map() {
  bashunit::helper::_resolve_provider_script "$1"
  local script=$_BASHUNIT_PROVIDER_RESOLVED_OUT

  if [ -z "$script" ]; then
    # Unreadable path: reset to an empty map keyed to this argument so a
    # follow-up lookup returns empty without rescanning.
    _BASHUNIT_PROVIDER_MAP_SCRIPT="$1"
    _BASHUNIT_PROVIDER_MAP_FNS=()
    _BASHUNIT_PROVIDER_MAP_PROVIDERS=()
    _BASHUNIT_PROVIDER_MAP_NO_PARALLEL=false
    return
  fi

  if [ "$script" = "$_BASHUNIT_PROVIDER_MAP_SCRIPT" ]; then
    return
  fi

  _BASHUNIT_PROVIDER_MAP_SCRIPT="$script"
  _BASHUNIT_PROVIDER_MAP_FNS=()
  _BASHUNIT_PROVIDER_MAP_PROVIDERS=()
  _BASHUNIT_PROVIDER_MAP_NO_PARALLEL=false

  local count=0
  local fn provider
  # Single awk pass emits "<fn>\t<provider>" for every function whose
  # definition is at most two lines below a `# @data_provider` (or
  # `# data_provider`) annotation, mirroring the previous grep -B2 + sed.
  # A reserved sentinel fn name carries the no-parallel-tests flag out of the
  # single awk pass; real fn names are identifiers so they never collide.
  while IFS=$'\t' read -r fn provider; do
    [ -z "$fn" ] && continue
    if [ "$fn" = "@@no_parallel@@" ]; then
      [ "$provider" = "1" ] && _BASHUNIT_PROVIDER_MAP_NO_PARALLEL=true
      continue
    fi
    _BASHUNIT_PROVIDER_MAP_FNS[count]="$fn"
    _BASHUNIT_PROVIDER_MAP_PROVIDERS[count]="$provider"
    count=$((count + 1))
  done < <(awk '
    /^# bashunit: no-parallel-tests/ { no_parallel = 1; next }
    /^[[:space:]]*#[[:space:]]*@?data_provider[[:space:]]+/ {
      p = $0
      sub(/^[[:space:]]*#[[:space:]]*@?data_provider[[:space:]]+/, "", p)
      sub(/[[:space:]]+$/, "", p)
      pending = p
      pending_line = NR
      next
    }
    {
      if (pending != "" && NR - pending_line <= 2) {
        if (match($0, /^[[:space:]]*(function[[:space:]]+)?[A-Za-z_][A-Za-z0-9_:]*[[:space:]]*\(\)/)) {
          fn = $0
          sub(/^[[:space:]]*(function[[:space:]]+)?/, "", fn)
          sub(/[[:space:]]*\(\).*/, "", fn)
          printf "%s\t%s\n", fn, pending
          pending = ""
        }
      } else if (pending != "" && NR - pending_line > 2) {
        pending = ""
      }
    }
    END { printf "@@no_parallel@@\t%d\n", no_parallel }
  ' "$script" 2>/dev/null)
}

#
# Pure-bash lookup against the cached provider map.
# Writes the provider-function name (or empty) into _BASHUNIT_PROVIDER_FN_OUT.
#
# @param $1 string Test-function name
#
function bashunit::helper::provider_for_function() {
  local function_name=$1
  local i=0
  local total=${#_BASHUNIT_PROVIDER_MAP_FNS[@]}
  while [ "$i" -lt "$total" ]; do
    if [ "${_BASHUNIT_PROVIDER_MAP_FNS[i]}" = "$function_name" ]; then
      _BASHUNIT_PROVIDER_FN_OUT="${_BASHUNIT_PROVIDER_MAP_PROVIDERS[i]}"
      return
    fi
    i=$((i + 1))
  done
  _BASHUNIT_PROVIDER_FN_OUT=""
}

function bashunit::helper::get_provider_data() {
  local function_name="$1"
  local script="$2"

  bashunit::helper::build_provider_map "$script"
  bashunit::helper::provider_for_function "$function_name"

  if [ -n "$_BASHUNIT_PROVIDER_FN_OUT" ]; then
    bashunit::helper::execute_function_if_exists "$_BASHUNIT_PROVIDER_FN_OUT"
  fi
}

function bashunit::helper::trim() {
  local input_string="$1"
  local trimmed_string

  trimmed_string="${input_string#"${input_string%%[![:space:]]*}"}"
  trimmed_string="${trimmed_string%"${trimmed_string##*[![:space:]]}"}"

  echo "$trimmed_string"
}

function bashunit::helper::get_latest_tag() {
  if ! bashunit::dependencies::has_git; then
    return 1
  fi

  # Floating major tags (e.g. v0) are not releases and must not win
  git ls-remote --tags "$BASHUNIT_GIT_REPO" |
    awk '{print $2}' |
    sed 's|^refs/tags/||' |
    grep -v '\^{}' |
    grep -E '^[0-9]+\.[0-9]+(\.[0-9]+)?$' |
    sort -Vr |
    head -n 1
}

# Also written by find_total_tests so a main-shell caller can read the count
# without a $() capture (which would discard the provider-map cache built here).
_BASHUNIT_HELPER_TOTAL_TESTS_OUT=0

function bashunit::helper::find_total_tests() {
  local filter=${1:-}
  shift || true

  _BASHUNIT_HELPER_TOTAL_TESTS_OUT=0
  if [ $# -eq 0 ]; then
    echo 0
    return
  fi

  local total_count=0
  local file

  for file in "$@"; do
    if [ ! -f "$file" ]; then
      continue
    fi

    # Build the provider map in THIS shell before the counting subshell: the
    # subshell inherits it (its own build call becomes a cache hit), and when
    # the caller runs in the main shell the runner's later build for the same
    # file is a cache hit too — one awk scan per file instead of two.
    bashunit::helper::build_provider_map "$file"

    local file_count
    file_count=$( (
      # shellcheck source=/dev/null
      source "$file"
      local all_fn_names
      all_fn_names=$(compgen -A function)
      local filtered_functions
      filtered_functions=$(bashunit::helper::get_functions_to_run "test" "$filter" "$all_fn_names") || true

      local count=0
      local IFS=$' \t\n'
      if [ -n "$filtered_functions" ]; then
        local -a functions_to_run=()
        # shellcheck disable=SC2206
        functions_to_run=($filtered_functions)
        # shellcheck disable=SC2034
        local -a provider_data=()
        local provider_data_count=0
        local fn_name line
        # Scan once; functions without a provider count as 1 with no fork (#763).
        bashunit::helper::build_provider_map "$file"
        for fn_name in "${functions_to_run[@]+"${functions_to_run[@]}"}"; do
          bashunit::helper::provider_for_function "$fn_name"
          if [ -z "$_BASHUNIT_PROVIDER_FN_OUT" ]; then
            count=$((count + 1))
            continue
          fi
          provider_data_count=0
          while IFS=" " read -r line; do
            [ -z "$line" ] && continue
            provider_data_count=$((provider_data_count + 1))
          done <<<"$(bashunit::helper::execute_function_if_exists "$_BASHUNIT_PROVIDER_FN_OUT")"

          if [ "$provider_data_count" -eq 0 ]; then
            count=$((count + 1))
          else
            count=$((count + provider_data_count))
          fi
        done
      fi

      echo "$count"
    ))

    total_count=$((total_count + file_count))
  done

  _BASHUNIT_HELPER_TOTAL_TESTS_OUT=$total_count
  echo "$total_count"
}

function bashunit::helper::load_test_files() {
  local filter="${1:-}"
  shift || true
  # Bash 3.0 compatible: use $# after shift to check for files
  local has_files=$#

  if [ "$has_files" -eq 0 ]; then
    if [ -n "${BASHUNIT_DEFAULT_PATH:-}" ]; then
      bashunit::helper::find_files_recursive "$BASHUNIT_DEFAULT_PATH"
    fi
  else
    printf "%s\n" "$@"
  fi
}

function bashunit::helper::load_bench_files() {
  local filter="${1:-}"
  shift || true
  # Bash 3.0 compatible: use $# after shift to check for files
  local has_files=$#

  if [ "$has_files" -eq 0 ]; then
    if [ -n "${BASHUNIT_DEFAULT_PATH:-}" ]; then
      bashunit::helper::find_files_recursive "$BASHUNIT_DEFAULT_PATH" '*[bB]ench.sh'
    fi
  else
    printf "%s\n" "$@"
  fi
}

#
# @param $1 string function name
# @return number line number of the function in the source file
#
function bashunit::helper::get_function_line_number() {
  local fn_name=$1

  # Enable extdebug only inside the subshell so the caller's setting is not
  # clobbered. With extdebug, `declare -F` prints "<name> <line> <file>"; parse
  # the line number with shell word-splitting instead of forking awk.
  local declaration
  declaration=$(
    shopt -s extdebug
    declare -F "$fn_name"
  )
  declaration="${declaration#* }"
  echo "${declaration%% *}"
}

# Writes a sanitized, process-unique id into _BASHUNIT_HELPER_ID_OUT.
# Return-slot form so the per-test caller avoids a $(...) capture fork (#764).
# Arguments: $1 basename
_BASHUNIT_HELPER_ID_OUT=""
function bashunit::helper::generate_id() {
  local basename="$1"
  # Inline normalize_variable_name + random_str to avoid two forks per call.
  # generate_id is called once per test and per file load.
  local sanitized="${basename//[^a-zA-Z0-9_]/_}"
  case "${sanitized:0:1}" in
  [a-zA-Z_]) ;;
  *) sanitized="_$sanitized" ;;
  esac
  if bashunit::env::is_parallel_run_enabled; then
    local _chars='abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'
    local _suffix=''
    local _i
    for ((_i = 0; _i < 6; _i++)); do
      _suffix="$_suffix${_chars:RANDOM%${#_chars}:1}"
    done
    _BASHUNIT_HELPER_ID_OUT="${sanitized}_$$_${_suffix}"
  else
    _BASHUNIT_HELPER_ID_OUT="${sanitized}_$$"
  fi
}

#
# Parses a file path that may contain a filter suffix.
# Supports two syntaxes:
#   - path::function_name (filter by function name)
#   - path:line_number (filter by line number)
#
# @param $1 string Eg: "tests/test.sh::test_foo" or "tests/test.sh:123"
#
# @return string Two lines: first is file path, second is filter (or empty)
#
function bashunit::helper::parse_file_path_filter() {
  local input="$1"
  local file_path=""
  local filter=""

  # Check for :: syntax (function name filter)
  case "$input" in *"::"*)
    file_path="${input%%::*}"
    filter="${input#*::}"
    ;;
  *)
    # Check for :number syntax (line number filter): a non-empty path, a
    # colon, then digits to the end of string. Pure-bash parameter expansion
    # avoids forking grep+sed.
    local line_number="${input##*:}"
    local maybe_path="${input%:*}"
    case "$line_number" in
    '' | *[!0-9]*)
      file_path="$input"
      ;;
    *)
      if [ -n "$maybe_path" ] && [ "$maybe_path" != "$input" ]; then
        # Line number will be resolved to function name later
        file_path="$maybe_path"
        filter="__line__:${line_number}"
      else
        file_path="$input"
      fi
      ;;
    esac
    ;;
  esac

  echo "$file_path"
  echo "$filter"
}

#
# Finds the test function that contains a given line number in a file.
#
# @param $1 string File path
# @param $2 number Line number
#
# @return string The function name, or empty if not found
#
function bashunit::helper::find_function_at_line() {
  local file="$1"
  local target_line="$2"

  if [ ! -f "$file" ]; then
    return 1
  fi

  # Find all test function definitions and their line numbers
  local best_match=""
  local best_line=0

  local line_num content
  while IFS=: read -r line_num content; do
    # Extract function name from the line
    local fn_name=""
    local fn_pattern='^[[:space:]]*(function[[:space:]]+)?(test[a-zA-Z_][a-zA-Z0-9_]*)[[:space:]]*\(\).*'
    fn_name=$(echo "$content" | sed -nE "s/$fn_pattern/\2/p")

    if [ -n "$fn_name" ] && [ "$line_num" -le "$target_line" ] && [ "$line_num" -gt "$best_line" ]; then
      best_match="$fn_name"
      best_line="$line_num"
    fi
  done < <(grep -n -E '^[[:space:]]*(function[[:space:]]+)?test[a-zA-Z_][a-zA-Z0-9_]*[[:space:]]*\(\)' "$file")

  echo "$best_match"
}

# Tags map for the most recently scanned script. Mirrors the provider map
# (build_provider_map): scanning a file once and caching each test-function ->
# comma-separated tags pair replaces a per-test grep/sed backward walk with a
# pure-bash lookup on the hot path when `--tag`/`--exclude-tag` is used (#773).
_BASHUNIT_TAGS_MAP_SCRIPT=""
_BASHUNIT_TAGS_MAP_FNS=()
_BASHUNIT_TAGS_MAP_TAGS=()
_BASHUNIT_TAGS_OUT=""

#
# Scans a script once and caches its test-function -> tags pairs.
# Memoized by resolved path, so repeated calls for the same file do not rescan.
#
# @param $1 string Path to the test script
#
function bashunit::helper::build_tags_map() {
  local script=$1
  # Handle directory changes in set_up_before_script (issue #529)
  if [ ! -f "$script" ] && [ -n "${BASHUNIT_WORKING_DIR:-}" ]; then
    script="$BASHUNIT_WORKING_DIR/$script"
  fi

  if [ ! -f "$script" ]; then
    # Unreadable path: reset to an empty map keyed to this argument so a
    # follow-up lookup returns empty without rescanning.
    _BASHUNIT_TAGS_MAP_SCRIPT="$1"
    _BASHUNIT_TAGS_MAP_FNS=()
    _BASHUNIT_TAGS_MAP_TAGS=()
    return
  fi

  if [ "$script" = "$_BASHUNIT_TAGS_MAP_SCRIPT" ]; then
    return
  fi

  _BASHUNIT_TAGS_MAP_SCRIPT="$script"
  _BASHUNIT_TAGS_MAP_FNS=()
  _BASHUNIT_TAGS_MAP_TAGS=()

  local count=0
  local fn tags
  # Single awk pass emits "<fn>\t<tags>" for every function that carries at
  # least one `# @tag <name>` comment in the contiguous comment block directly
  # above its definition, mirroring the previous per-function backward walk.
  # Tags accumulate nearest-to-the-function first (same order the old walk
  # produced). A blank or non-comment line breaks the association; other
  # comment lines keep the block open. Both `function test_x` and `test_x()`
  # definition styles are recognised.
  while IFS=$'\t' read -r fn tags; do
    [ -z "$fn" ] && continue
    _BASHUNIT_TAGS_MAP_FNS[count]="$fn"
    _BASHUNIT_TAGS_MAP_TAGS[count]="$tags"
    count=$((count + 1))
  done < <(awk '
    /^[[:space:]]*#[[:space:]]*@tag[[:space:]]/ {
      t = $0
      sub(/^[[:space:]]*#[[:space:]]*@tag[[:space:]]+/, "", t)
      tags = (tags == "" ? t : t "," tags)
      next
    }
    /^[[:space:]]*#/ { next }
    /^[[:space:]]*(function[[:space:]]+)?[A-Za-z_][A-Za-z0-9_:]*[[:space:]]*\(\)/ {
      fn = $0
      sub(/^[[:space:]]*(function[[:space:]]+)?/, "", fn)
      sub(/[[:space:]]*\(\).*/, "", fn)
      if (tags != "") printf "%s\t%s\n", fn, tags
      tags = ""
      next
    }
    { tags = "" }
  ' "$script" 2>/dev/null)
}

#
# Pure-bash lookup against the cached tags map.
# Writes the comma-separated tags (or empty) into _BASHUNIT_TAGS_OUT.
#
# @param $1 string Test-function name
#
function bashunit::helper::tags_for_function() {
  local function_name=$1
  local i=0
  local total=${#_BASHUNIT_TAGS_MAP_FNS[@]}
  while [ "$i" -lt "$total" ]; do
    if [ "${_BASHUNIT_TAGS_MAP_FNS[i]}" = "$function_name" ]; then
      _BASHUNIT_TAGS_OUT="${_BASHUNIT_TAGS_MAP_TAGS[i]}"
      return
    fi
    i=$((i + 1))
  done
  _BASHUNIT_TAGS_OUT=""
}

#
# Extracts @tag annotations for a specific function from a test file.
# Thin wrapper over the cached tags map, kept for callers that want the tags
# on stdout. Hot-path call sites use build_tags_map + tags_for_function to
# avoid the subshell fork.
#
# @param $1 string Function name
# @param $2 string Script file path
#
# @return string Comma-separated list of tags, or empty if none
#
function bashunit::helper::get_tags_for_function() {
  bashunit::helper::build_tags_map "$2"
  bashunit::helper::tags_for_function "$1"
  echo "$_BASHUNIT_TAGS_OUT"
}

#
# Checks if a function's tags match the include/exclude filters.
# Include uses OR logic (any match passes).
# Exclude uses OR logic (any match fails).
# Exclude takes precedence over include.
#
# @param $1 string Comma-separated tags for the function
# @param $2 string Comma-separated include tags (empty = no filter)
# @param $3 string Comma-separated exclude tags (empty = no filter)
#
# @return 0 if function should run, 1 if it should be skipped
#
function bashunit::helper::function_matches_tags() {
  local fn_tags="$1"
  local include_tags="$2"
  local exclude_tags="$3"

  # Check exclude tags first (exclude wins over include)
  if [ -n "$exclude_tags" ]; then
    local IFS=','
    local etag
    for etag in $exclude_tags; do
      local check_tag
      for check_tag in $fn_tags; do
        if [ "$check_tag" = "$etag" ]; then
          return 1
        fi
      done
    done
  fi

  # Check include tags (OR logic: any match passes)
  if [ -n "$include_tags" ]; then
    if [ -z "$fn_tags" ]; then
      return 1
    fi
    local IFS=','
    local itag
    for itag in $include_tags; do
      local check_tag
      for check_tag in $fn_tags; do
        if [ "$check_tag" = "$itag" ]; then
          return 0
        fi
      done
    done
    return 1
  fi

  return 0
}

# test_title.sh

function bashunit::set_test_title() {
  bashunit::state::set_test_title "$1"
}

# upgrade.sh

function bashunit::upgrade::upgrade() {
  local install_dir="${BASHUNIT_INSTALL_DIR:-}"
  if [ -z "$install_dir" ]; then
    install_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
  fi
  local target="$install_dir/bashunit"

  local latest_tag
  latest_tag="$(bashunit::helper::get_latest_tag)"

  if [ -z "$latest_tag" ]; then
    echo "Failed to resolve latest bashunit version. Check your internet connection and that 'git' is installed." >&2
    return 1
  fi

  if [ "$BASHUNIT_VERSION" = "$latest_tag" ]; then
    echo "> You are already on latest version"
    return 0
  fi

  echo "> Upgrading bashunit to latest version"

  local url="https://github.com/TypedDevs/bashunit/releases/download/$latest_tag/bashunit"
  local err_file
  err_file="$(mktemp 2>/dev/null || echo "/tmp/bashunit_upgrade_err.$$")"
  local download_status=0
  bashunit::io::download_to "$url" "$target" 2>"$err_file" || download_status=$?

  if [ "$download_status" -ne 0 ]; then
    echo "Failed to download bashunit $latest_tag from $url" >&2
    if [ -s "$err_file" ]; then
      echo "Reason:" >&2
      sed 's/^/  /' "$err_file" >&2
    fi
    rm -f "$err_file" "$target"
    return 1
  fi
  rm -f "$err_file"

  if [ ! -s "$target" ]; then
    echo "Failed to download bashunit $latest_tag from $url (empty file)" >&2
    rm -f "$target"
    return 1
  fi

  if ! chmod u+x "$target"; then
    echo "Failed to make $target executable" >&2
    return 1
  fi

  echo "> bashunit upgraded successfully to latest version $latest_tag"
}

# watch.sh

# bashunit watch mode
# Watches test and source files for changes and re-runs tests automatically.
# Requires: inotifywait (inotify-tools) on Linux, or fswatch on macOS.

function bashunit::watch::_command_exists() {
  command -v "$1" &>/dev/null
}

function bashunit::watch::is_available() {
  if bashunit::watch::_command_exists inotifywait; then
    echo "inotifywait"
  elif bashunit::watch::_command_exists fswatch; then
    echo "fswatch"
  else
    echo "polling"
  fi
}

function bashunit::watch::run() {
  local path="${1:-.}"
  shift
  # Declare and assign separately: bash 3.0 does not expand a compound array
  # assignment attached to `local`, it collapses "$@" into one literal element.
  local extra_args
  extra_args=("$@")

  local tool
  tool=$(bashunit::watch::is_available)

  if [ "$tool" = "polling" ]; then
    bashunit::watch::_print_polling_notice "$path"
  else
    printf "%sbashunit --watch%s  watching: %s\n\n" \
      "${_BASHUNIT_COLOR_PASSED}" "${_BASHUNIT_COLOR_DEFAULT}" "$path"
  fi

  # Run once immediately before entering the watch loop
  bashunit::watch::run_tests "$path" "${extra_args[@]+"${extra_args[@]}"}"

  while true; do
    bashunit::watch::wait_for_change "$tool" "$path"
    printf "\n%s[change detected — re-running tests]%s\n\n" \
      "${_BASHUNIT_COLOR_SKIPPED}" "${_BASHUNIT_COLOR_DEFAULT}"
    bashunit::watch::run_tests "$path" "${extra_args[@]+"${extra_args[@]}"}"
  done
}

function bashunit::watch::run_tests() {
  local path="$1"
  shift
  # Re-invoke bashunit test in a subshell so state resets cleanly each run
  "$BASHUNIT_ROOT_DIR/bashunit" test "$path" "$@"
  return $?
}

function bashunit::watch::_print_polling_notice() {
  local path="$1"
  printf "%sbashunit --watch%s  polling: %s (every %ss)\n\n" \
    "${_BASHUNIT_COLOR_PASSED}" "${_BASHUNIT_COLOR_DEFAULT}" \
    "$path" "${BASHUNIT_WATCH_INTERVAL:-2}"
  printf "  No 'inotifywait' or 'fswatch' found; using pure-shell polling.\n"
  printf "  Install one for instant triggers:\n"
  printf "    Linux:  sudo apt install inotify-tools\n"
  printf "    macOS:  brew install fswatch\n\n"
}

# Lists watched *.sh files modified since the sentinel file was touched.
# Non-empty output means a rerun is due. `find -newer` is POSIX and avoids the
# GNU/BSD `stat` flag divergence.
function bashunit::watch::_poll_changes() {
  local sentinel="$1"
  local path="$2"
  find "$path" -name '*.sh' -newer "$sentinel" -print 2>/dev/null
}

function bashunit::watch::wait_for_change() {
  local tool="$1"
  local path="$2"

  case "$tool" in
  polling)
    local sentinel
    sentinel="$(bashunit::temp_dir watch)/sentinel"
    while true; do
      : >"$sentinel"
      sleep "${BASHUNIT_WATCH_INTERVAL:-2}"
      if [ -n "$(bashunit::watch::_poll_changes "$sentinel" "$path")" ]; then
        return 0
      fi
    done
    ;;
  inotifywait)
    inotifywait \
      --quiet \
      --recursive \
      --event modify,create,delete,move \
      --include '.*\.sh$' \
      "$path" 2>/dev/null
    ;;
  fswatch)
    # fswatch outputs one line per event; we only need the first one
    fswatch \
      --recursive \
      --include='.*\.sh$' \
      --exclude='.*' \
      --one-event \
      "$path" 2>/dev/null
    ;;
  esac
}

# assertions.sh


# assert.sh

# Helper to mark assertion as failed and set the guard flag
function bashunit::assert::mark_failed() {
  bashunit::state::add_assertions_failed
  bashunit::state::mark_assertion_failed_in_test
}

# Guard clause to skip assertion if one already failed in test (when stop-on-assertion is enabled)
function bashunit::assert::should_skip() {
  bashunit::env::is_stop_on_assertion_failure_enabled && ((_BASHUNIT_ASSERTION_FAILED_IN_TEST))
}

_BASHUNIT_ASSERT_LABEL_OUT=""

# Resolve assertion label into the slot _BASHUNIT_ASSERT_LABEL_OUT with no fork:
# use custom label if provided, otherwise derive from the test function name.
# Must be called at the same stack depth as the echoing wrapper so the test-frame
# fallback keeps resolving against the caller of the assertion.
function bashunit::assert::label_to_slot() {
  local custom_label="${1:-}"
  if [ -n "$custom_label" ]; then
    _BASHUNIT_ASSERT_LABEL_OUT=$custom_label
    return
  fi
  bashunit::helper::find_test_function_name_to_slot
  bashunit::helper::normalize_test_function_name_to_slot "$_BASHUNIT_HELPER_TESTFN_OUT"
  _BASHUNIT_ASSERT_LABEL_OUT=$_BASHUNIT_HELPER_NORMALIZED_OUT
}

# Resolve assertion label: use custom label if provided, otherwise derive from test function name
function bashunit::assert::label() {
  bashunit::assert::label_to_slot "${1:-}"
  builtin echo "$_BASHUNIT_ASSERT_LABEL_OUT"
}

function bashunit::fail() {
  bashunit::assert::should_skip && return 0

  local message="${1:-${FUNCNAME[1]}}"

  bashunit::helper::find_test_function_name_to_slot
  bashunit::helper::normalize_test_function_name_to_slot "$_BASHUNIT_HELPER_TESTFN_OUT"
  local label=$_BASHUNIT_HELPER_NORMALIZED_OUT
  bashunit::assert::mark_failed
  bashunit::console_results::print_failure_message "${label}" "$message"
}

function assert_true() {
  bashunit::assert::should_skip && return 0

  local actual="$1"

  # Check for expected literal values first
  case "$actual" in
  "")
    bashunit::handle_bool_assertion_failure "true or 0" "$actual"
    return
    ;;
  "true" | "0")
    bashunit::state::add_assertions_passed
    return
    ;;
  "false" | "1")
    bashunit::handle_bool_assertion_failure "true or 0" "$actual"
    return
    ;;
  esac

  # Run command or eval and check the exit code
  bashunit::run_command_or_eval "$actual"
  local exit_code=$?

  if [ "$exit_code" -ne 0 ]; then
    bashunit::handle_bool_assertion_failure "command or function with zero exit code" "exit code: $exit_code"
  else
    bashunit::state::add_assertions_passed
  fi
}

function assert_false() {
  bashunit::assert::should_skip && return 0

  local actual="$1"

  # Check for expected literal values first
  case "$actual" in
  "")
    bashunit::handle_bool_assertion_failure "false or 1" "$actual"
    return
    ;;
  "false" | "1")
    bashunit::state::add_assertions_passed
    return
    ;;
  "true" | "0")
    bashunit::handle_bool_assertion_failure "false or 1" "$actual"
    return
    ;;
  esac

  # Run command or eval and check the exit code
  bashunit::run_command_or_eval "$actual"
  local exit_code=$?

  if [ "$exit_code" -eq 0 ]; then
    bashunit::handle_bool_assertion_failure "command or function with non-zero exit code" "exit code: $exit_code"
  else
    bashunit::state::add_assertions_passed
  fi
}

function bashunit::run_command_or_eval() {
  local cmd="$1"

  case "$cmd" in
  eval\ * | eval)
    eval "${cmd#eval }" &>/dev/null
    ;;
  *[=[:space:]]* | "")
    # An alias name never contains "=" or whitespace, so this can't be an alias
    # invocation: run it directly. Guarding here also stops `alias -- "$cmd"`
    # below from *defining* an alias as a side effect when "$cmd" looks like
    # "name=value" (which would wrongly succeed).
    "$cmd" &>/dev/null
    ;;
  *)
    # Detect aliases with the `alias` builtin instead of forking
    # `command -v | grep`: it exits 0 only for a defined alias, matching the
    # old `^alias` check for functions/binaries/unknown commands (all non-zero).
    if alias -- "$cmd" >/dev/null 2>&1; then
      eval "$cmd" &>/dev/null
    else
      "$cmd" &>/dev/null
    fi
    ;;
  esac
  return $?
}

function bashunit::handle_bool_assertion_failure() {
  local expected="$1"
  local got="$2"
  bashunit::helper::find_test_function_name_to_slot
  bashunit::helper::normalize_test_function_name_to_slot "$_BASHUNIT_HELPER_TESTFN_OUT"
  local label=$_BASHUNIT_HELPER_NORMALIZED_OUT

  bashunit::assert::mark_failed
  bashunit::console_results::print_failed_test "$label" "$expected" "but got " "$got"
}

function assert_same() {
  bashunit::assert::should_skip && return 0

  local expected="$1"
  local actual="$2"
  local label_override="${3:-}"

  if [ "$expected" != "$actual" ]; then
    bashunit::assert::label_to_slot "${label_override:-}"
    local label=$_BASHUNIT_ASSERT_LABEL_OUT
    bashunit::assert::mark_failed
    bashunit::console_results::print_failed_test "${label}" "${expected}" "but got " "${actual}"
    return
  fi

  bashunit::state::add_assertions_passed
}

function assert_equals() {
  bashunit::assert::should_skip && return 0

  local expected="$1"
  local actual="$2"
  local label_override="${3:-}"

  bashunit::str::strip_ansi_to_slot "$actual"
  local actual_cleaned=$_BASHUNIT_STR_STRIPPED_OUT
  bashunit::str::strip_ansi_to_slot "$expected"
  local expected_cleaned=$_BASHUNIT_STR_STRIPPED_OUT

  if [ "$expected_cleaned" != "$actual_cleaned" ]; then
    bashunit::assert::label_to_slot "${label_override:-}"
    local label=$_BASHUNIT_ASSERT_LABEL_OUT
    bashunit::assert::mark_failed
    bashunit::console_results::print_failed_test "${label}" "${expected_cleaned}" "but got " "${actual_cleaned}"
    return
  fi

  bashunit::state::add_assertions_passed
}

function assert_not_equals() {
  bashunit::assert::should_skip && return 0

  local expected="$1"
  local actual="$2"
  local label_override="${3:-}"

  bashunit::str::strip_ansi_to_slot "$actual"
  local actual_cleaned=$_BASHUNIT_STR_STRIPPED_OUT
  bashunit::str::strip_ansi_to_slot "$expected"
  local expected_cleaned=$_BASHUNIT_STR_STRIPPED_OUT

  if [ "$expected_cleaned" = "$actual_cleaned" ]; then
    bashunit::assert::label_to_slot "${label_override:-}"
    local label=$_BASHUNIT_ASSERT_LABEL_OUT
    bashunit::assert::mark_failed
    bashunit::console_results::print_failed_test "${label}" "${expected_cleaned}" "to not be" "${actual_cleaned}"
    return
  fi

  bashunit::state::add_assertions_passed
}

function assert_empty() {
  bashunit::assert::should_skip && return 0

  local expected="$1"
  local label_override="${2:-}"

  if [ "$expected" != "" ]; then
    bashunit::assert::label_to_slot "${label_override:-}"
    local label=$_BASHUNIT_ASSERT_LABEL_OUT
    bashunit::assert::mark_failed
    bashunit::console_results::print_failed_test "${label}" "to be empty" "but got " "${expected}"
    return
  fi

  bashunit::state::add_assertions_passed
}

function assert_not_empty() {
  bashunit::assert::should_skip && return 0

  local expected="$1"
  local label_override="${2:-}"

  if [ "$expected" = "" ]; then
    bashunit::assert::label_to_slot "${label_override:-}"
    local label=$_BASHUNIT_ASSERT_LABEL_OUT
    bashunit::assert::mark_failed
    bashunit::console_results::print_failed_test "${label}" "to not be empty" "but got " "${expected}"
    return
  fi

  bashunit::state::add_assertions_passed
}

function assert_not_same() {
  bashunit::assert::should_skip && return 0

  local expected="$1"
  local actual="$2"
  local label_override="${3:-}"

  if [ "$expected" = "$actual" ]; then
    bashunit::assert::label_to_slot "${label_override:-}"
    local label=$_BASHUNIT_ASSERT_LABEL_OUT
    bashunit::assert::mark_failed
    bashunit::console_results::print_failed_test "${label}" "${expected}" "to not be" "${actual}"
    return
  fi

  bashunit::state::add_assertions_passed
}

function assert_contains() {
  bashunit::assert::should_skip && return 0
  local IFS=$' \t\n'

  local expected="$1"
  local -a actual_arr
  actual_arr=("${@:2}")
  local label_override=""
  local actual
  actual=$(printf '%s\n' "${actual_arr[@]}")

  case "$actual" in
  *"$expected"*) ;;
  *)
    bashunit::assert::label_to_slot "${label_override:-}"
    local label=$_BASHUNIT_ASSERT_LABEL_OUT
    bashunit::assert::mark_failed
    bashunit::console_results::print_failed_test "${label}" "${actual}" "to contain" "${expected}"
    return
    ;;
  esac

  bashunit::state::add_assertions_passed
}

function assert_contains_ignore_case() {
  bashunit::assert::should_skip && return 0

  local expected="$1"
  local actual="$2"
  local label_override="${3:-}"

  # Bash 3.0 compatible: use tr for case-insensitive comparison
  # (shopt nocasematch was introduced in Bash 3.1)
  local expected_lower
  local actual_lower
  expected_lower=$(printf '%s' "$expected" | tr '[:upper:]' '[:lower:]')
  actual_lower=$(printf '%s' "$actual" | tr '[:upper:]' '[:lower:]')

  case "$actual_lower" in
  *"$expected_lower"*) ;;
  *)
    bashunit::assert::label_to_slot "${label_override:-}"
    local label=$_BASHUNIT_ASSERT_LABEL_OUT
    bashunit::assert::mark_failed
    bashunit::console_results::print_failed_test "${label}" "${actual}" "to contain" "${expected}"
    return
    ;;
  esac

  bashunit::state::add_assertions_passed
}

function assert_not_contains() {
  local label_override=""
  bashunit::assert::should_skip && return 0
  local IFS=$' \t\n'

  local expected="$1"
  local -a actual_arr
  actual_arr=("${@:2}")
  local actual
  actual=$(printf '%s\n' "${actual_arr[@]}")

  case "$actual" in
  *"$expected"*)
    bashunit::assert::label_to_slot "${label_override:-}"
    local label=$_BASHUNIT_ASSERT_LABEL_OUT
    bashunit::assert::mark_failed
    bashunit::console_results::print_failed_test "${label}" "${actual}" "to not contain" "${expected}"
    return
    ;;
  esac

  bashunit::state::add_assertions_passed
}

function assert_matches() {
  bashunit::assert::should_skip && return 0
  local IFS=$' \t\n'

  local expected="$1"
  local -a actual_arr
  actual_arr=("${@:2}")
  local actual
  actual=$(printf '%s\n' "${actual_arr[@]}")

  if [ "$(printf '%s' "$actual" | "$GREP" -cE "$expected" || true)" -eq 0 ]; then
    # Retry with newlines collapsed for cross-line patterns
    if [ "$(printf '%s' "$actual" | tr '\n' ' ' | "$GREP" -cE "$expected" || true)" -eq 0 ]; then
      bashunit::helper::find_test_function_name_to_slot
      bashunit::helper::normalize_test_function_name_to_slot "$_BASHUNIT_HELPER_TESTFN_OUT"
      local label=$_BASHUNIT_HELPER_NORMALIZED_OUT
      bashunit::assert::mark_failed
      bashunit::console_results::print_failed_test "${label}" "${actual}" "to match" "${expected}"
      return
    fi
  fi

  bashunit::state::add_assertions_passed
}

function assert_not_matches() {
  local label_override=""
  bashunit::assert::should_skip && return 0
  local IFS=$' \t\n'

  local expected="$1"
  local -a actual_arr
  actual_arr=("${@:2}")
  local actual
  actual=$(printf '%s\n' "${actual_arr[@]}")

  # Check both line-by-line and with newlines collapsed for cross-line patterns
  if [ "$(printf '%s' "$actual" | "$GREP" -cE "$expected" || true)" -gt 0 ] ||
    [ "$(printf '%s' "$actual" | tr '\n' ' ' | "$GREP" -cE "$expected" || true)" -gt 0 ]; then
    bashunit::assert::label_to_slot "${label_override:-}"
    local label=$_BASHUNIT_ASSERT_LABEL_OUT
    bashunit::assert::mark_failed
    bashunit::console_results::print_failed_test "${label}" "${actual}" "to not match" "${expected}"
    return
  fi

  bashunit::state::add_assertions_passed
}

function assert_exec() {
  bashunit::assert::should_skip && return 0
  local label_override=""

  local cmd="$1"
  shift

  local expected_exit=0
  local expected_stdout=""
  local expected_stderr=""
  local stdout_needle=""
  local stdout_no_needle=""
  local stderr_needle=""
  local stderr_no_needle=""
  local stdin_input=""
  local check_stdout=false
  local check_stderr=false
  local check_stdout_contains=false
  local check_stdout_not_contains=false
  local check_stderr_contains=false
  local check_stderr_not_contains=false
  local check_stdin=false

  while [ $# -gt 0 ]; do
    case "$1" in
    --exit)
      expected_exit="$2"
      shift 2
      ;;
    --stdout)
      expected_stdout="$2"
      check_stdout=true
      shift 2
      ;;
    --stderr)
      expected_stderr="$2"
      check_stderr=true
      shift 2
      ;;
    --stdout-contains)
      stdout_needle="$2"
      check_stdout_contains=true
      shift 2
      ;;
    --stdout-not-contains)
      stdout_no_needle="$2"
      check_stdout_not_contains=true
      shift 2
      ;;
    --stderr-contains)
      stderr_needle="$2"
      check_stderr_contains=true
      shift 2
      ;;
    --stderr-not-contains)
      stderr_no_needle="$2"
      check_stderr_not_contains=true
      shift 2
      ;;
    --stdin)
      stdin_input="$2"
      check_stdin=true
      shift 2
      ;;
    *)
      shift
      ;;
    esac
  done

  local stdout_file stderr_file
  stdout_file=$("$MKTEMP")
  stderr_file=$("$MKTEMP")

  if $check_stdin; then
    local stdin_file
    stdin_file=$("$MKTEMP")
    printf '%s' "$stdin_input" >"$stdin_file"
    eval "$cmd" <"$stdin_file" >"$stdout_file" 2>"$stderr_file"
    local exit_code=$?
    rm -f "$stdin_file"
  else
    eval "$cmd" >"$stdout_file" 2>"$stderr_file"
    local exit_code=$?
  fi

  local stdout
  stdout=$(cat "$stdout_file")
  local stderr
  stderr=$(cat "$stderr_file")

  rm -f "$stdout_file" "$stderr_file"

  local expected_desc="exit: $expected_exit"
  local actual_desc="exit: $exit_code"
  local failed=0

  if [ "$exit_code" -ne "$expected_exit" ]; then
    failed=1
  fi

  if $check_stdout; then
    expected_desc="$expected_desc"$'\n'"stdout: $expected_stdout"
    actual_desc="$actual_desc"$'\n'"stdout: $stdout"
    if [ "$stdout" != "$expected_stdout" ]; then
      failed=1
    fi
  fi

  if $check_stdout_contains; then
    expected_desc="$expected_desc"$'\n'"stdout contains: $stdout_needle"
    actual_desc="$actual_desc"$'\n'"stdout: $stdout"
    case "$stdout" in
    *"$stdout_needle"*) ;;
    *) failed=1 ;;
    esac
  fi

  if $check_stdout_not_contains; then
    expected_desc="$expected_desc"$'\n'"stdout not contains: $stdout_no_needle"
    actual_desc="$actual_desc"$'\n'"stdout: $stdout"
    case "$stdout" in
    *"$stdout_no_needle"*) failed=1 ;;
    esac
  fi

  if $check_stderr; then
    expected_desc="$expected_desc"$'\n'"stderr: $expected_stderr"
    actual_desc="$actual_desc"$'\n'"stderr: $stderr"
    if [ "$stderr" != "$expected_stderr" ]; then
      failed=1
    fi
  fi

  if $check_stderr_contains; then
    expected_desc="$expected_desc"$'\n'"stderr contains: $stderr_needle"
    actual_desc="$actual_desc"$'\n'"stderr: $stderr"
    case "$stderr" in
    *"$stderr_needle"*) ;;
    *) failed=1 ;;
    esac
  fi

  if $check_stderr_not_contains; then
    expected_desc="$expected_desc"$'\n'"stderr not contains: $stderr_no_needle"
    actual_desc="$actual_desc"$'\n'"stderr: $stderr"
    case "$stderr" in
    *"$stderr_no_needle"*) failed=1 ;;
    esac
  fi

  if [ "$failed" -eq 1 ]; then
    bashunit::assert::label_to_slot "${label_override:-}"
    local label=$_BASHUNIT_ASSERT_LABEL_OUT
    bashunit::assert::mark_failed
    bashunit::console_results::print_failed_test "$label" "$expected_desc" "but got " "$actual_desc"
    return
  fi

  bashunit::state::add_assertions_passed
}

function assert_exit_code() {
  local actual_exit_code=${3-"$?"} # Capture $? before guard check
  local label_override=""
  bashunit::assert::should_skip && return 0

  local expected_exit_code="$1"

  if [ "$actual_exit_code" -ne "$expected_exit_code" ]; then
    bashunit::assert::label_to_slot "${label_override:-}"
    local label=$_BASHUNIT_ASSERT_LABEL_OUT
    bashunit::assert::mark_failed
    bashunit::console_results::print_failed_test "${label}" "${actual_exit_code}" "to be" "${expected_exit_code}"
    return
  fi

  bashunit::state::add_assertions_passed
}

function assert_successful_code() {
  local actual_exit_code=${3-"$?"} # Capture $? before guard check
  local label_override=""
  bashunit::assert::should_skip && return 0

  local expected_exit_code=0

  if [ "$actual_exit_code" -ne "$expected_exit_code" ]; then
    bashunit::assert::label_to_slot "${label_override:-}"
    local label=$_BASHUNIT_ASSERT_LABEL_OUT
    bashunit::assert::mark_failed
    bashunit::console_results::print_failed_test \
      "${label}" "${actual_exit_code}" "to be exactly" "${expected_exit_code}"
    return
  fi

  bashunit::state::add_assertions_passed
}

function assert_unsuccessful_code() {
  local actual_exit_code=${3-"$?"} # Capture $? before guard check
  local label_override=""
  bashunit::assert::should_skip && return 0

  if [ "$actual_exit_code" -eq 0 ]; then
    bashunit::assert::label_to_slot "${label_override:-}"
    local label=$_BASHUNIT_ASSERT_LABEL_OUT
    bashunit::assert::mark_failed
    bashunit::console_results::print_failed_test "${label}" "${actual_exit_code}" "to be non-zero" "but was 0"
    return
  fi

  bashunit::state::add_assertions_passed
}

function assert_general_error() {
  local actual_exit_code=${3-"$?"} # Capture $? before guard check
  local label_override=""
  bashunit::assert::should_skip && return 0

  local expected_exit_code=1

  if [ "$actual_exit_code" -ne "$expected_exit_code" ]; then
    bashunit::assert::label_to_slot "${label_override:-}"
    local label=$_BASHUNIT_ASSERT_LABEL_OUT
    bashunit::assert::mark_failed
    bashunit::console_results::print_failed_test \
      "${label}" "${actual_exit_code}" "to be exactly" "${expected_exit_code}"
    return
  fi

  bashunit::state::add_assertions_passed
}

function assert_command_not_found() {
  local actual_exit_code=${3-"$?"} # Capture $? before guard check
  local label_override=""
  bashunit::assert::should_skip && return 0

  local expected_exit_code=127

  if [ "$actual_exit_code" -ne "$expected_exit_code" ]; then
    bashunit::assert::label_to_slot "${label_override:-}"
    local label=$_BASHUNIT_ASSERT_LABEL_OUT
    bashunit::assert::mark_failed
    bashunit::console_results::print_failed_test \
      "${label}" "${actual_exit_code}" "to be exactly" "${expected_exit_code}"
    return
  fi

  bashunit::state::add_assertions_passed
}

function assert_string_starts_with() {
  local label_override=""
  bashunit::assert::should_skip && return 0
  local IFS=$' \t\n'

  local expected="$1"
  local -a actual_arr
  actual_arr=("${@:2}")
  local actual
  actual=$(printf '%s\n' "${actual_arr[@]}")

  case "$actual" in
  "$expected"*) ;;
  *)
    bashunit::assert::label_to_slot "${label_override:-}"
    local label=$_BASHUNIT_ASSERT_LABEL_OUT
    bashunit::assert::mark_failed
    bashunit::console_results::print_failed_test "${label}" "${actual}" "to start with" "${expected}"
    return
    ;;
  esac

  bashunit::state::add_assertions_passed
}

function assert_string_not_starts_with() {
  bashunit::assert::should_skip && return 0

  local expected="$1"
  local actual="$2"
  local label_override="${3:-}"

  case "$actual" in
  "$expected"*)
    bashunit::assert::label_to_slot "${label_override:-}"
    local label=$_BASHUNIT_ASSERT_LABEL_OUT
    bashunit::assert::mark_failed
    bashunit::console_results::print_failed_test "${label}" "${actual}" "to not start with" "${expected}"
    return
    ;;
  esac

  bashunit::state::add_assertions_passed
}

function assert_string_ends_with() {
  local label_override=""
  bashunit::assert::should_skip && return 0
  local IFS=$' \t\n'

  local expected="$1"
  local -a actual_arr
  actual_arr=("${@:2}")
  local actual
  actual=$(printf '%s\n' "${actual_arr[@]}")

  case "$actual" in
  *"$expected") ;;
  *)
    bashunit::assert::label_to_slot "${label_override:-}"
    local label=$_BASHUNIT_ASSERT_LABEL_OUT
    bashunit::assert::mark_failed
    bashunit::console_results::print_failed_test "${label}" "${actual}" "to end with" "${expected}"
    return
    ;;
  esac

  bashunit::state::add_assertions_passed
}

function assert_string_not_ends_with() {
  local label_override=""
  bashunit::assert::should_skip && return 0
  local IFS=$' \t\n'

  local expected="$1"
  local -a actual_arr
  actual_arr=("${@:2}")
  local actual
  actual=$(printf '%s\n' "${actual_arr[@]}")

  case "$actual" in
  *"$expected")
    bashunit::assert::label_to_slot "${label_override:-}"
    local label=$_BASHUNIT_ASSERT_LABEL_OUT
    bashunit::assert::mark_failed
    bashunit::console_results::print_failed_test "${label}" "${actual}" "to not end with" "${expected}"
    return
    ;;
  esac

  bashunit::state::add_assertions_passed
}

function assert_less_than() {
  bashunit::assert::should_skip && return 0

  local expected="$1"
  local actual="$2"
  local label_override="${3:-}"

  if ! [ "$actual" -lt "$expected" ]; then
    bashunit::assert::label_to_slot "${label_override:-}"
    local label=$_BASHUNIT_ASSERT_LABEL_OUT
    bashunit::assert::mark_failed
    bashunit::console_results::print_failed_test "${label}" "${actual}" "to be less than" "${expected}"
    return
  fi

  bashunit::state::add_assertions_passed
}

function assert_less_or_equal_than() {
  bashunit::assert::should_skip && return 0

  local expected="$1"
  local actual="$2"
  local label_override="${3:-}"

  if ! [ "$actual" -le "$expected" ]; then
    bashunit::assert::label_to_slot "${label_override:-}"
    local label=$_BASHUNIT_ASSERT_LABEL_OUT
    bashunit::assert::mark_failed
    bashunit::console_results::print_failed_test "${label}" "${actual}" "to be less or equal than" "${expected}"
    return
  fi

  bashunit::state::add_assertions_passed
}

function assert_greater_than() {
  bashunit::assert::should_skip && return 0

  local expected="$1"
  local actual="$2"
  local label_override="${3:-}"

  if ! [ "$actual" -gt "$expected" ]; then
    bashunit::assert::label_to_slot "${label_override:-}"
    local label=$_BASHUNIT_ASSERT_LABEL_OUT
    bashunit::assert::mark_failed
    bashunit::console_results::print_failed_test "${label}" "${actual}" "to be greater than" "${expected}"
    return
  fi

  bashunit::state::add_assertions_passed
}

function assert_greater_or_equal_than() {
  bashunit::assert::should_skip && return 0

  local expected="$1"
  local actual="$2"
  local label_override="${3:-}"

  if ! [ "$actual" -ge "$expected" ]; then
    bashunit::assert::label_to_slot "${label_override:-}"
    local label=$_BASHUNIT_ASSERT_LABEL_OUT
    bashunit::assert::mark_failed
    bashunit::console_results::print_failed_test "${label}" "${actual}" "to be greater or equal than" "${expected}"
    return
  fi

  bashunit::state::add_assertions_passed
}

##
# Whether a value looks like a number (integer or decimal, optional sign).
# Returns: 0 when numeric, 1 otherwise.
##
function bashunit::assert::_is_numeric() {
  local value="$1"
  case "$value" in
  '' | *[!0-9.+-]*) return 1 ;;
  esac
  # Must contain at least one digit (rejects ".", "-", "+").
  case "$value" in
  *[0-9]*) return 0 ;;
  esac
  return 1
}

##
# Asserts the actual value is within +/- delta of the expected value:
# |actual - expected| <= delta. Supports floats via bashunit::math::calculate.
# Arguments: $1 - expected, $2 - actual, $3 - delta
##
function assert_within_delta() {
  bashunit::assert::should_skip && return 0

  local expected="$1"
  local actual="$2"
  local delta="$3"

  if ! bashunit::assert::_is_numeric "$expected" ||
    ! bashunit::assert::_is_numeric "$actual" ||
    ! bashunit::assert::_is_numeric "$delta"; then
    bashunit::assert::label_to_slot
    bashunit::assert::mark_failed
    bashunit::console_results::print_failed_test \
      "${_BASHUNIT_ASSERT_LABEL_OUT}" "${expected} ${actual} ${delta}" \
      "to all be numeric" "but got a non-numeric value"
    return
  fi

  local diff
  diff="$(bashunit::math::calculate "$expected - $actual")"
  case "$diff" in
  -*) diff="${diff#-}" ;;
  esac

  if [ "$(bashunit::math::calculate "$diff <= $delta")" != "1" ]; then
    bashunit::assert::label_to_slot
    bashunit::assert::mark_failed
    bashunit::console_results::print_failed_test \
      "${_BASHUNIT_ASSERT_LABEL_OUT}" "${actual}" "to be within ${delta} of" "${expected}"
    return
  fi

  bashunit::state::add_assertions_passed
}

function assert_line_count() {
  bashunit::assert::should_skip && return 0
  local IFS=$' \t\n'

  local expected="$1"
  local -a input_arr
  input_arr=("${@:2}")
  local label_override=""
  local input_str
  input_str=$(printf '%s\n' ${input_arr+"${input_arr[@]}"})

  if [ -z "$input_str" ]; then
    local actual=0
  else
    # Count lines without forking: one line plus each real newline, plus each
    # literal "\n" (backslash-n) escape, which counts as an extra line break.
    local actual=1
    local _rest="$input_str"
    while [ "$_rest" != "${_rest#*$'\n'}" ]; do
      _rest="${_rest#*$'\n'}"
      actual=$((actual + 1))
    done
    _rest="$input_str"
    while [ "$_rest" != "${_rest#*\\n}" ]; do
      _rest="${_rest#*\\n}"
      actual=$((actual + 1))
    done
  fi

  if [ "$expected" != "$actual" ]; then
    bashunit::assert::label_to_slot "${label_override:-}"
    local label=$_BASHUNIT_ASSERT_LABEL_OUT

    bashunit::assert::mark_failed
    bashunit::console_results::print_failed_test "${label}" "${input_str}" \
      "to contain number of lines equal to" "${expected}" \
      "but found" "${actual}"
    return
  fi

  bashunit::state::add_assertions_passed
}

function bashunit::format_to_regex() {
  local format="$1"
  local regex=""
  local i=0
  local len=${#format}

  while [ $i -lt "$len" ]; do
    local char="${format:$i:1}"
    if [ "$char" = "%" ] && [ $((i + 1)) -lt "$len" ]; then
      local next="${format:$((i + 1)):1}"
      case "$next" in
      d) regex="${regex}[0-9]+" ;;
      i) regex="${regex}[+-]?[0-9]+" ;;
      f) regex="${regex}[+-]?[0-9]*\\.?[0-9]+" ;;
      s) regex="${regex}[^ ]+" ;;
      x) regex="${regex}[0-9a-fA-F]+" ;;
      e) regex="${regex}[+-]?[0-9]*\\.?[0-9]+[eE][+-]?[0-9]+" ;;
      %) regex="${regex}%" ;;
      *)
        regex="${regex}%${next}"
        ;;
      esac
      i=$((i + 2))
    else
      case "$char" in
      . | '*' | '+' | '?' | '(' | ')' | '[' | ']' | '{' | '}' | '|' | '^' | '$')
        regex="${regex}\\${char}"
        ;;
      \\)
        regex="${regex}\\\\"
        ;;
      *)
        regex="${regex}${char}"
        ;;
      esac
      i=$((i + 1))
    fi
  done

  printf '%s' "^${regex}$"
}

function assert_string_matches_format() {
  bashunit::assert::should_skip && return 0

  local format="$1"
  local actual="$2"
  local label_override="${3:-}"

  local regex
  regex="$(bashunit::format_to_regex "$format")"

  if [ "$(printf '%s' "$actual" | "$GREP" -cE "$regex" || true)" -eq 0 ]; then
    bashunit::assert::label_to_slot "${label_override:-}"
    local label=$_BASHUNIT_ASSERT_LABEL_OUT
    bashunit::assert::mark_failed
    bashunit::console_results::print_failed_test "${label}" "${actual}" "to match format" "${format}"
    return
  fi

  bashunit::state::add_assertions_passed
}

function assert_string_not_matches_format() {
  bashunit::assert::should_skip && return 0

  local format="$1"
  local actual="$2"
  local label_override="${3:-}"

  local regex
  regex="$(bashunit::format_to_regex "$format")"

  if [ "$(printf '%s' "$actual" | "$GREP" -cE "$regex" || true)" -gt 0 ]; then
    bashunit::assert::label_to_slot "${label_override:-}"
    local label=$_BASHUNIT_ASSERT_LABEL_OUT
    bashunit::assert::mark_failed
    bashunit::console_results::print_failed_test "${label}" "${actual}" "to not match format" "${format}"
    return
  fi

  bashunit::state::add_assertions_passed
}

# assert_arrays.sh

function assert_arrays_equal() {
  bashunit::assert::should_skip && return 0

  local label
  label="$(bashunit::assert::label)"

  local -a expected_values=()
  local -a actual_values=()
  local found_separator=false
  local argument

  for argument in "$@"; do
    if [ "$found_separator" = false ] && [ "$argument" = "--" ]; then
      found_separator=true
      continue
    fi

    if [ "$found_separator" = true ]; then
      actual_values[${#actual_values[@]}]="$argument"
    else
      expected_values[${#expected_values[@]}]="$argument"
    fi
  done

  if [ "$found_separator" = false ]; then
    bashunit::assert::mark_failed
    bashunit::console_results::print_failed_test "$label" "--" "but got " "missing array separator"
    return
  fi

  if [ "${#expected_values[@]}" -ne "${#actual_values[@]}" ]; then
    bashunit::assert::mark_failed
    bashunit::console_results::print_failed_test \
      "$label" "${expected_values[*]}" "but got " "${actual_values[*]}" \
      "Expected length" "${#expected_values[@]}, actual length ${#actual_values[@]}"
    return
  fi

  local index
  for ((index = 0; index < ${#expected_values[@]}; index++)); do
    if [ "${expected_values[$index]}" != "${actual_values[$index]}" ]; then
      bashunit::assert::mark_failed
      bashunit::console_results::print_failed_test \
        "$label" "${expected_values[*]}" "but got " "${actual_values[*]}" \
        "Different index" "$index"
      return
    fi
  done

  bashunit::state::add_assertions_passed
}

function assert_array_contains() {
  bashunit::assert::should_skip && return 0

  local expected="$1"
  local test_fn
  test_fn="$(bashunit::helper::find_test_function_name)"
  local label
  label="$(bashunit::helper::normalize_test_function_name "$test_fn")"
  shift

  local -a actual
  actual=("$@")

  case "${actual[*]:-}" in
  *"$expected"*)
    ;;
  *)
    bashunit::assert::mark_failed
    bashunit::console_results::print_failed_test "${label}" "${actual[*]}" "to contain" "${expected}"
    return
    ;;
  esac

  bashunit::state::add_assertions_passed
}

function assert_array_length() {
  bashunit::assert::should_skip && return 0

  local expected="$1"
  local test_fn
  test_fn="$(bashunit::helper::find_test_function_name)"
  local label
  label="$(bashunit::helper::normalize_test_function_name "$test_fn")"
  shift

  # Use $# / $* rather than building an array: on Bash 3.0 under `set -u`,
  # expanding "$@" into an array with zero elements is an unbound-variable error.
  local actual_length="$#"

  if [ "$expected" != "$actual_length" ]; then
    bashunit::assert::mark_failed
    bashunit::console_results::print_failed_test \
      "${label}" "$*" "to have length ${expected}" "but got ${actual_length}"
    return
  fi

  bashunit::state::add_assertions_passed
}

function assert_array_not_contains() {
  bashunit::assert::should_skip && return 0

  local expected="$1"
  local test_fn
  test_fn="$(bashunit::helper::find_test_function_name)"
  local label
  label="$(bashunit::helper::normalize_test_function_name "$test_fn")"
  shift
  local -a actual
  actual=("$@")

  case "${actual[*]:-}" in
  *"$expected"*)
    bashunit::assert::mark_failed
    bashunit::console_results::print_failed_test "${label}" "${actual[*]}" "to not contain" "${expected}"
    return
    ;;
  esac

  bashunit::state::add_assertions_passed
}

# assert_dates.sh

function bashunit::date::to_epoch() {
  local input="$1"

  # Already epoch seconds (all digits)
  case "$input" in
  *[!0-9]*) ;; # contains non-digits, continue to ISO parsing
  *)
    echo "$input"
    return 0
    ;;
  esac

  # Handle Z (UTC) suffix explicitly: BusyBox needs TZ=UTC, BSD needs +0000
  case "$input" in
  *Z)
    local utc_input="${input%Z}"
    local utc_norm="${utc_input/T/ }"
    local epoch
    # GNU/BusyBox: parse in explicit UTC
    epoch=$(TZ=UTC date -d "$utc_input" +%s 2>/dev/null) && { echo "$epoch"; return 0; }
    epoch=$(TZ=UTC date -d "$utc_norm" +%s 2>/dev/null) && { echo "$epoch"; return 0; }
    # BSD: use +0000 offset which %z understands
    epoch=$(date -j -f "%Y-%m-%dT%H:%M:%S%z" "${utc_input}+0000" +%s 2>/dev/null) && { echo "$epoch"; return 0; }
    echo "$input"
    return 1
    ;;
  esac

  # Normalize ISO 8601: replace T with space, strip tz offset
  local normalized="$input"
  normalized="${normalized/T/ }"
  # Strip timezone offset (+HHMM or -HHMM) at end for initial parsing
  case "$normalized" in
  *[+-][0-9][0-9][0-9][0-9])
    normalized="${normalized%[+-][0-9][0-9][0-9][0-9]}"
    ;;
  esac

  # Format conversion (GNU vs BSD date)
  local epoch
  # Try GNU date first (-d flag) with original input
  epoch=$(date -d "$input" +%s 2>/dev/null) && {
    echo "$epoch"
    return 0
  }
  # If input has timezone offset, parse in UTC and adjust manually (BusyBox)
  case "$input" in
  *[+-][0-9][0-9][0-9][0-9])
    epoch=$(TZ=UTC date -d "$normalized" +%s 2>/dev/null) && {
      local ilen=${#input}
      local ostart=$((ilen - 5))
      local osign="${input:$ostart:1}"
      local ohh="${input:$((ostart + 1)):2}"
      local omm="${input:$((ostart + 3)):2}"
      local osecs=$(( (10#$ohh * 3600) + (10#$omm * 60) ))
      if [ "$osign" = "+" ]; then
        osecs=$(( -osecs ))
      fi
      echo $(( epoch + osecs ))
      return 0
    }
    ;;
  esac
  # Try GNU date with normalized (space-separated) input
  if [ "$normalized" != "$input" ]; then
    epoch=$(date -d "$normalized" +%s 2>/dev/null) && {
      echo "$epoch"
      return 0
    }
  fi
  # Try BSD date (-j -f flag) with ISO 8601 datetime + timezone offset
  epoch=$(date -j -f "%Y-%m-%dT%H:%M:%S%z" "$input" +%s 2>/dev/null) && {
    echo "$epoch"
    return 0
  }
  # Try BSD date with ISO 8601 datetime format
  epoch=$(date -j -f "%Y-%m-%dT%H:%M:%S" "$input" +%s 2>/dev/null) && {
    echo "$epoch"
    return 0
  }
  # Try BSD date with space-separated datetime format
  epoch=$(date -j -f "%Y-%m-%d %H:%M:%S" "$input" +%s 2>/dev/null) && {
    echo "$epoch"
    return 0
  }
  # Try BSD date with date-only format (append midnight for deterministic results)
  epoch=$(date -j -f "%Y-%m-%d %H:%M:%S" "$input 00:00:00" +%s 2>/dev/null) && {
    echo "$epoch"
    return 0
  }

  # Unsupported format
  echo "$input"
  return 1
}

function assert_date_equals() {
  bashunit::assert::should_skip && return 0

  local expected
  expected="$(bashunit::date::to_epoch "$1")"
  local actual
  actual="$(bashunit::date::to_epoch "$2")"

  if [ "$actual" -ne "$expected" ]; then
    local test_fn
    test_fn="$(bashunit::helper::find_test_function_name)"
    local label
    label="$(bashunit::helper::normalize_test_function_name "$test_fn")"
    bashunit::assert::mark_failed
    bashunit::console_results::print_failed_test "${label}" "${actual}" "to be equal to" "${expected}"
    return
  fi

  bashunit::state::add_assertions_passed
}

function assert_date_before() {
  bashunit::assert::should_skip && return 0

  local expected
  expected="$(bashunit::date::to_epoch "$1")"
  local actual
  actual="$(bashunit::date::to_epoch "$2")"

  if [ "$actual" -ge "$expected" ]; then
    local test_fn
    test_fn="$(bashunit::helper::find_test_function_name)"
    local label
    label="$(bashunit::helper::normalize_test_function_name "$test_fn")"
    bashunit::assert::mark_failed
    bashunit::console_results::print_failed_test "${label}" "${actual}" "to be before" "${expected}"
    return
  fi

  bashunit::state::add_assertions_passed
}

function assert_date_after() {
  bashunit::assert::should_skip && return 0

  local expected
  expected="$(bashunit::date::to_epoch "$1")"
  local actual
  actual="$(bashunit::date::to_epoch "$2")"

  if [ "$actual" -le "$expected" ]; then
    local test_fn
    test_fn="$(bashunit::helper::find_test_function_name)"
    local label
    label="$(bashunit::helper::normalize_test_function_name "$test_fn")"
    bashunit::assert::mark_failed
    bashunit::console_results::print_failed_test "${label}" "${actual}" "to be after" "${expected}"
    return
  fi

  bashunit::state::add_assertions_passed
}

function assert_date_within_range() {
  bashunit::assert::should_skip && return 0

  local from
  from="$(bashunit::date::to_epoch "$1")"
  local to
  to="$(bashunit::date::to_epoch "$2")"
  local actual
  actual="$(bashunit::date::to_epoch "$3")"

  if [ "$actual" -lt "$from" ] || [ "$actual" -gt "$to" ]; then
    local test_fn
    test_fn="$(bashunit::helper::find_test_function_name)"
    local label
    label="$(bashunit::helper::normalize_test_function_name "$test_fn")"
    bashunit::assert::mark_failed
    bashunit::console_results::print_failed_test "${label}" "${actual}" "to be between" "${from} and ${to}"
    return
  fi

  bashunit::state::add_assertions_passed
}

function assert_date_within_delta() {
  bashunit::assert::should_skip && return 0

  local expected
  expected="$(bashunit::date::to_epoch "$1")"
  local actual
  actual="$(bashunit::date::to_epoch "$2")"
  local delta="$3"

  local diff=$((actual - expected))
  if [ "$diff" -lt 0 ]; then
    diff=$((-diff))
  fi

  if [ "$diff" -gt "$delta" ]; then
    local test_fn
    test_fn="$(bashunit::helper::find_test_function_name)"
    local label
    label="$(bashunit::helper::normalize_test_function_name "$test_fn")"
    bashunit::assert::mark_failed
    bashunit::console_results::print_failed_test "${label}" "${actual}" "to be within" "${delta} seconds of ${expected}"
    return
  fi

  bashunit::state::add_assertions_passed
}

# assert_duration.sh

function bashunit::duration::measure_ms() {
  local command="$1"

  local start_ns
  start_ns=$(bashunit::clock::now)

  eval "$command" >/dev/null 2>&1

  local end_ns
  end_ns=$(bashunit::clock::now)

  local elapsed_ms
  elapsed_ms=$(bashunit::math::calculate "($end_ns - $start_ns) / 1000000" | awk '{printf "%.0f", $1}')

  echo "$elapsed_ms"
}

function assert_duration() {
  bashunit::assert::should_skip && return 0

  local command="$1"
  local threshold_ms="$2"

  local elapsed_ms
  elapsed_ms=$(bashunit::duration::measure_ms "$command")

  if [ "$elapsed_ms" -gt "$threshold_ms" ]; then
    local test_fn
    test_fn="$(bashunit::helper::find_test_function_name)"
    local label
    label="$(bashunit::helper::normalize_test_function_name "$test_fn")"
    bashunit::assert::mark_failed
    bashunit::console_results::print_failed_test "${label}" "${threshold_ms}" "to complete within (ms)" "${command}"
    return
  fi

  bashunit::state::add_assertions_passed
}

function assert_duration_less_than() {
  bashunit::assert::should_skip && return 0

  local command="$1"
  local threshold_ms="$2"

  local elapsed_ms
  elapsed_ms=$(bashunit::duration::measure_ms "$command")

  if [ "$elapsed_ms" -ge "$threshold_ms" ]; then
    local test_fn
    test_fn="$(bashunit::helper::find_test_function_name)"
    local label
    label="$(bashunit::helper::normalize_test_function_name "$test_fn")"
    bashunit::assert::mark_failed
    bashunit::console_results::print_failed_test "${label}" "${threshold_ms}" "to complete within (ms)" "${command}"
    return
  fi

  bashunit::state::add_assertions_passed
}

function assert_duration_greater_than() {
  bashunit::assert::should_skip && return 0

  local command="$1"
  local threshold_ms="$2"

  local elapsed_ms
  elapsed_ms=$(bashunit::duration::measure_ms "$command")

  if [ "$elapsed_ms" -le "$threshold_ms" ]; then
    local test_fn
    test_fn="$(bashunit::helper::find_test_function_name)"
    local label
    label="$(bashunit::helper::normalize_test_function_name "$test_fn")"
    bashunit::assert::mark_failed
    bashunit::console_results::print_failed_test "${label}" "${threshold_ms}" "to take at least (ms)" "${command}"
    return
  fi

  bashunit::state::add_assertions_passed
}

# assert_files.sh

function assert_file_exists() {
  bashunit::assert::should_skip && return 0

  local expected="$1"
  local test_fn
  test_fn="$(bashunit::helper::find_test_function_name)"
  local label="${3:-$(bashunit::helper::normalize_test_function_name "$test_fn")}"

  if [ ! -f "$expected" ]; then
    bashunit::assert::mark_failed
    bashunit::console_results::print_failed_test "${label}" "${expected}" "to exist but" "do not exist"
    return
  fi

  bashunit::state::add_assertions_passed
}

function assert_file_not_exists() {
  bashunit::assert::should_skip && return 0

  local expected="$1"
  local test_fn
  test_fn="$(bashunit::helper::find_test_function_name)"
  local label="${3:-$(bashunit::helper::normalize_test_function_name "$test_fn")}"

  if [ -f "$expected" ]; then
    bashunit::assert::mark_failed
    bashunit::console_results::print_failed_test "${label}" "${expected}" "to not exist but" "the file exists"
    return
  fi

  bashunit::state::add_assertions_passed
}

function assert_is_file() {
  bashunit::assert::should_skip && return 0

  local expected="$1"
  local test_fn
  test_fn="$(bashunit::helper::find_test_function_name)"
  local label="${3:-$(bashunit::helper::normalize_test_function_name "$test_fn")}"

  if [ ! -f "$expected" ]; then
    bashunit::assert::mark_failed
    bashunit::console_results::print_failed_test "${label}" "${expected}" "to be a file" "but is not a file"
    return
  fi

  bashunit::state::add_assertions_passed
}

function assert_is_file_empty() {
  bashunit::assert::should_skip && return 0

  local expected="$1"
  local test_fn
  test_fn="$(bashunit::helper::find_test_function_name)"
  local label="${3:-$(bashunit::helper::normalize_test_function_name "$test_fn")}"

  if [ -s "$expected" ]; then
    bashunit::assert::mark_failed
    bashunit::console_results::print_failed_test "${label}" "${expected}" "to be empty" "but is not empty"
    return
  fi

  bashunit::state::add_assertions_passed
}

function assert_files_equals() {
  bashunit::assert::should_skip && return 0

  local expected="$1"
  local actual="$2"

  if [ "$(diff -u "$expected" "$actual")" != '' ]; then
    local test_fn
    test_fn="$(bashunit::helper::find_test_function_name)"
    local label
    label="$(bashunit::helper::normalize_test_function_name "$test_fn")"
    bashunit::assert::mark_failed

    bashunit::console_results::print_failed_test "${label}" "${expected}" "Compared" "${actual}" \
      "Diff" "$(diff -u "$expected" "$actual" | sed '1,2d')"
    return
  fi

  bashunit::state::add_assertions_passed
}

function assert_files_not_equals() {
  bashunit::assert::should_skip && return 0

  local expected="$1"
  local actual="$2"

  if [ "$(diff -u "$expected" "$actual")" = '' ]; then
    local test_fn
    test_fn="$(bashunit::helper::find_test_function_name)"
    local label
    label="$(bashunit::helper::normalize_test_function_name "$test_fn")"
    bashunit::assert::mark_failed

    bashunit::console_results::print_failed_test "${label}" "${expected}" "Compared" "${actual}" \
      "Diff" "Files are equals"
    return
  fi

  bashunit::state::add_assertions_passed
}

function assert_file_contains() {
  bashunit::assert::should_skip && return 0

  local file="$1"
  local string="$2"

  if ! grep -F -q "$string" "$file"; then
    local test_fn
    test_fn="$(bashunit::helper::find_test_function_name)"
    local label
    label="$(bashunit::helper::normalize_test_function_name "$test_fn")"
    bashunit::assert::mark_failed

    bashunit::console_results::print_failed_test "${label}" "${file}" "to contain" "${string}"
    return
  fi

  bashunit::state::add_assertions_passed
}

function assert_file_not_contains() {
  bashunit::assert::should_skip && return 0

  local file="$1"
  local string="$2"

  if grep -q "$string" "$file"; then
    local test_fn
    test_fn="$(bashunit::helper::find_test_function_name)"
    local label
    label="$(bashunit::helper::normalize_test_function_name "$test_fn")"
    bashunit::assert::mark_failed

    bashunit::console_results::print_failed_test "${label}" "${file}" "to not contain" "${string}"
    return
  fi

  bashunit::state::add_assertions_passed
}

##
# Normalizes an octal file mode to its decimal value, dropping leading zeros
# (so "0755" and "755" compare equal). Echoes nothing on invalid octal input.
# Arguments: $1 - octal mode string
##
function bashunit::assert::_octal_to_decimal() {
  local mode="$1"
  case "$mode" in
  '' | *[!0-7]*) return 1 ;;
  esac
  printf '%d' "$((8#$mode))"
}

##
# Asserts a file has the expected octal permission mode (e.g. "644", "0755").
# Arguments: $1 - expected octal mode, $2 - file path
##
function assert_file_permissions() {
  bashunit::assert::should_skip && return 0

  local expected="$1"
  local file="$2"
  local test_fn
  test_fn="$(bashunit::helper::find_test_function_name)"
  local label
  label="$(bashunit::helper::normalize_test_function_name "$test_fn")"

  if [ ! -e "$file" ]; then
    bashunit::assert::mark_failed
    bashunit::console_results::print_failed_test \
      "${label}" "${file}" "to have permissions ${expected}" "but the file does not exist"
    return
  fi

  local actual
  actual="$(stat -c '%a' "$file" 2>/dev/null || stat -f '%Lp' "$file" 2>/dev/null)"

  local expected_dec actual_dec
  expected_dec="$(bashunit::assert::_octal_to_decimal "$expected")"
  actual_dec="$(bashunit::assert::_octal_to_decimal "$actual")"

  if [ "$expected_dec" != "$actual_dec" ]; then
    bashunit::assert::mark_failed
    bashunit::console_results::print_failed_test \
      "${label}" "${file}" "to have permissions ${expected}" "but got ${actual}"
    return
  fi

  bashunit::state::add_assertions_passed
}

# assert_folders.sh

function assert_directory_exists() {
  bashunit::assert::should_skip && return 0

  local expected="$1"
  local test_fn
  test_fn="$(bashunit::helper::find_test_function_name)"
  local label="${2:-$(bashunit::helper::normalize_test_function_name "$test_fn")}"

  if [ ! -d "$expected" ]; then
    bashunit::assert::mark_failed
    bashunit::console_results::print_failed_test "${label}" "${expected}" "to exist but" "do not exist"
    return
  fi

  bashunit::state::add_assertions_passed
}

function assert_directory_not_exists() {
  bashunit::assert::should_skip && return 0

  local expected="$1"
  local test_fn
  test_fn="$(bashunit::helper::find_test_function_name)"
  local label="${2:-$(bashunit::helper::normalize_test_function_name "$test_fn")}"

  if [ -d "$expected" ]; then
    bashunit::assert::mark_failed
    bashunit::console_results::print_failed_test "${label}" "${expected}" "to not exist but" "the directory exists"
    return
  fi

  bashunit::state::add_assertions_passed
}

function assert_is_directory() {
  bashunit::assert::should_skip && return 0

  local expected="$1"
  local test_fn
  test_fn="$(bashunit::helper::find_test_function_name)"
  local label="${2:-$(bashunit::helper::normalize_test_function_name "$test_fn")}"

  if [ ! -d "$expected" ]; then
    bashunit::assert::mark_failed
    bashunit::console_results::print_failed_test "${label}" "${expected}" "to be a directory" "but is not a directory"
    return
  fi

  bashunit::state::add_assertions_passed
}

function assert_is_directory_empty() {
  bashunit::assert::should_skip && return 0

  local expected="$1"
  local test_fn
  test_fn="$(bashunit::helper::find_test_function_name)"
  local label="${2:-$(bashunit::helper::normalize_test_function_name "$test_fn")}"

  if [ ! -d "$expected" ] || [ -n "$(ls -A "$expected")" ]; then
    bashunit::assert::mark_failed
    bashunit::console_results::print_failed_test "${label}" "${expected}" "to be empty" "but is not empty"
    return
  fi

  bashunit::state::add_assertions_passed
}

function assert_is_directory_not_empty() {
  bashunit::assert::should_skip && return 0

  local expected="$1"
  local test_fn
  test_fn="$(bashunit::helper::find_test_function_name)"
  local label="${2:-$(bashunit::helper::normalize_test_function_name "$test_fn")}"

  if [ ! -d "$expected" ] || [ -z "$(ls -A "$expected")" ]; then
    bashunit::assert::mark_failed
    bashunit::console_results::print_failed_test "${label}" "${expected}" "to not be empty" "but is empty"
    return
  fi

  bashunit::state::add_assertions_passed
}

function assert_is_directory_readable() {
  bashunit::assert::should_skip && return 0

  local expected="$1"
  local test_fn
  test_fn="$(bashunit::helper::find_test_function_name)"
  local label="${2:-$(bashunit::helper::normalize_test_function_name "$test_fn")}"

  if [ ! -d "$expected" ] || [ ! -r "$expected" ] || [ ! -x "$expected" ]; then
    bashunit::assert::mark_failed
    bashunit::console_results::print_failed_test "${label}" "${expected}" "to be readable" "but is not readable"
    return
  fi

  bashunit::state::add_assertions_passed
}

function assert_is_directory_not_readable() {
  bashunit::assert::should_skip && return 0

  local expected="$1"
  local test_fn
  test_fn="$(bashunit::helper::find_test_function_name)"
  local label="${2:-$(bashunit::helper::normalize_test_function_name "$test_fn")}"

  if [ ! -d "$expected" ] || { [ -r "$expected" ] && [ -x "$expected" ]; }; then
    bashunit::assert::mark_failed
    bashunit::console_results::print_failed_test "${label}" "${expected}" "to be not readable" "but is readable"
    return
  fi

  bashunit::state::add_assertions_passed
}

function assert_is_directory_writable() {
  bashunit::assert::should_skip && return 0

  local expected="$1"
  local test_fn
  test_fn="$(bashunit::helper::find_test_function_name)"
  local label="${2:-$(bashunit::helper::normalize_test_function_name "$test_fn")}"

  if [ ! -d "$expected" ] || [ ! -w "$expected" ]; then
    bashunit::assert::mark_failed
    bashunit::console_results::print_failed_test "${label}" "${expected}" "to be writable" "but is not writable"
    return
  fi

  bashunit::state::add_assertions_passed
}

function assert_is_directory_not_writable() {
  bashunit::assert::should_skip && return 0

  local expected="$1"
  local test_fn
  test_fn="$(bashunit::helper::find_test_function_name)"
  local label="${2:-$(bashunit::helper::normalize_test_function_name "$test_fn")}"

  if [ ! -d "$expected" ] || [ -w "$expected" ]; then
    bashunit::assert::mark_failed
    bashunit::console_results::print_failed_test "${label}" "${expected}" "to be not writable" "but is writable"
    return
  fi

  bashunit::state::add_assertions_passed
}

# assert_json.sh

function bashunit::assert_json::require_jq() {
  if ! command -v jq >/dev/null 2>&1; then
    bashunit::skip "jq is required for JSON assertions"
    return 1
  fi
  return 0
}

function assert_json_key_exists() {
  bashunit::assert::should_skip && return 0
  bashunit::assert_json::require_jq || return 0

  local key="$1"
  local json="$2"

  local result
  if ! result=$(printf '%s' "$json" | jq -e "$key" 2>/dev/null) || [ "$result" = "null" ]; then
    local test_fn
    test_fn="$(bashunit::helper::find_test_function_name)"
    local label
    label="$(bashunit::helper::normalize_test_function_name "$test_fn")"
    bashunit::assert::mark_failed
    bashunit::console_results::print_failed_test "${label}" "${json}" "to have key" "${key}"
    return
  fi

  bashunit::state::add_assertions_passed
}

function assert_json_contains() {
  bashunit::assert::should_skip && return 0
  bashunit::assert_json::require_jq || return 0

  local key="$1"
  local expected="$2"
  local json="$3"

  local result
  if ! result=$(printf '%s' "$json" | jq -e -r "$key" 2>/dev/null) || [ "$result" = "null" ]; then
    local test_fn
    test_fn="$(bashunit::helper::find_test_function_name)"
    local label
    label="$(bashunit::helper::normalize_test_function_name "$test_fn")"
    bashunit::assert::mark_failed
    bashunit::console_results::print_failed_test "${label}" "${json}" "to have key" "${key}"
    return
  fi

  if [ "$result" != "$expected" ]; then
    local test_fn
    test_fn="$(bashunit::helper::find_test_function_name)"
    local label
    label="$(bashunit::helper::normalize_test_function_name "$test_fn")"
    bashunit::assert::mark_failed
    bashunit::console_results::print_failed_test "${label}" "${expected}" "but got " "${result}"
    return
  fi

  bashunit::state::add_assertions_passed
}

function assert_json_equals() {
  bashunit::assert::should_skip && return 0
  bashunit::assert_json::require_jq || return 0

  local expected="$1"
  local actual="$2"

  local expected_sorted
  expected_sorted=$(printf '%s' "$expected" | jq -S '.' 2>/dev/null)
  local actual_sorted
  actual_sorted=$(printf '%s' "$actual" | jq -S '.' 2>/dev/null)

  if [ "$expected_sorted" != "$actual_sorted" ]; then
    local test_fn
    test_fn="$(bashunit::helper::find_test_function_name)"
    local label
    label="$(bashunit::helper::normalize_test_function_name "$test_fn")"
    bashunit::assert::mark_failed
    bashunit::console_results::print_failed_test "${label}" "${expected}" "but got " "${actual}"
    return
  fi

  bashunit::state::add_assertions_passed
}

# assert_snapshot.sh
# shellcheck disable=SC2155

# Strips all carriage returns, then any trailing newlines, entirely in bash.
# Reproduces the previous `$(echo -n "$in" | tr -d '\r')` (command substitution
# drops trailing newlines) without the two forks. Result in _snapshot_normalized.
function bashunit::snapshot::normalize_actual() {
  local normalized="${1//$'\r'/}"
  while [ "${normalized%$'\n'}" != "$normalized" ]; do
    normalized="${normalized%$'\n'}"
  done
  _snapshot_normalized=$normalized
}

function assert_match_snapshot() {
  local _snapshot_normalized
  bashunit::snapshot::normalize_actual "$1"
  local actual=$_snapshot_normalized
  bashunit::helper::find_test_function_name_to_slot
  local test_fn=$_BASHUNIT_HELPER_TESTFN_OUT
  bashunit::snapshot::resolve_file "${2:-}" "$test_fn"
  local snapshot_file=$_BASHUNIT_SNAPSHOT_FILE_OUT

  if [ ! -f "$snapshot_file" ]; then
    bashunit::snapshot::initialize "$snapshot_file" "$actual"
    return
  fi

  bashunit::snapshot::compare "$actual" "$snapshot_file" "$test_fn"
}

function assert_match_snapshot_ignore_colors() {
  # Only fork sed when the input actually carries an escape sequence; plain,
  # colorless output takes a pure-bash fast path. The sed pattern is kept
  # identical to the historic one (strip `\x1B[...[mK]` only) so on-disk
  # snapshots stay byte-compatible.
  local stripped=$1
  case "$stripped" in
  *$'\e'*) stripped=$(printf '%s' "$stripped" | sed 's/\x1B\[[0-9;]*[mK]//g') ;;
  esac
  local _snapshot_normalized
  bashunit::snapshot::normalize_actual "$stripped"
  local actual=$_snapshot_normalized
  bashunit::helper::find_test_function_name_to_slot
  local test_fn=$_BASHUNIT_HELPER_TESTFN_OUT
  bashunit::snapshot::resolve_file "${2:-}" "$test_fn"
  local snapshot_file=$_BASHUNIT_SNAPSHOT_FILE_OUT

  if [ ! -f "$snapshot_file" ]; then
    bashunit::snapshot::initialize "$snapshot_file" "$actual"
    return
  fi

  bashunit::snapshot::compare "$actual" "$snapshot_file" "$test_fn"
}

function bashunit::snapshot::match_with_placeholder() {
  local actual="$1"
  local snapshot="$2"
  local placeholder="${BASHUNIT_SNAPSHOT_PLACEHOLDER:-::ignore::}"
  local token="__BASHUNIT_IGNORE__"

  local sanitized="${snapshot//$placeholder/$token}"
  local escaped=$(printf '%s' "$sanitized" | sed -e 's/[.[\\^$*+?{}()|]/\\&/g')
  local regex="^${escaped//$token/(.|\\n)*}$"

  if command -v perl >/dev/null 2>&1; then
    echo "$actual" | REGEX="$regex" perl -0 -e '
      my $r = $ENV{REGEX};
      my $input = join("", <STDIN>);
      exit($input =~ /$r/s ? 0 : 1);
    ' && return 0 || return 1
  else
    # No perl: build the pattern exactly like the perl branch — swap the
    # placeholder for a token that survives escaping, escape the regex
    # metacharacters, then turn the token into `.*`. (The previous order,
    # escaping after inserting `.*`, escaped the `.*` itself and broke every
    # fallback match.) grep matches line-by-line, so unlike the perl branch a
    # placeholder cannot span multiple lines here.
    local fallback="${snapshot//$placeholder/$token}"
    fallback=$(printf '%s' "$fallback" | sed -e 's/[.[\\^$*+?{}()|]/\\&/g')
    fallback="^${fallback//$token/.*}$"
    echo "$actual" | grep -Eq "$fallback" && return 0 || return 1
  fi
}

# Writes the resolved snapshot path into _BASHUNIT_SNAPSHOT_FILE_OUT (no fork).
# Derives the path from BASH_SOURCE[2] using parameter expansion instead of
# dirname/basename, keeping the exact string the previous version produced.
_BASHUNIT_SNAPSHOT_FILE_OUT=""
function bashunit::snapshot::resolve_file() {
  local file_hint="$1"
  local func_name="$2"

  if [ -n "$file_hint" ]; then
    _BASHUNIT_SNAPSHOT_FILE_OUT=$file_hint
    return
  fi

  # dirname via parameter expansion. `dirname "foo.sh"` (no slash) is ".", which
  # `${src%/*}` cannot yield, so special-case the slashless path.
  local src="${BASH_SOURCE[2]}"
  local dir_part
  case "$src" in
  */*) dir_part="${src%/*}" ;;
  *) dir_part="." ;;
  esac
  local base_part="${src##*/}"

  bashunit::helper::normalize_variable_name_to_slot "$base_part"
  local test_file=$_BASHUNIT_HELPER_VARNAME_OUT
  bashunit::helper::normalize_variable_name_to_slot "$func_name"
  local name="$_BASHUNIT_HELPER_VARNAME_OUT.snapshot"

  _BASHUNIT_SNAPSHOT_FILE_OUT="./${dir_part}/snapshots/${test_file}.${name}"
}

function bashunit::snapshot::initialize() {
  local path="$1"
  local content="$2"
  mkdir -p "$(dirname "$path")"
  echo "$content" >"$path"
  bashunit::state::add_assertions_snapshot
}

function bashunit::snapshot::compare() {
  local actual="$1"
  local snapshot_path="$2"
  local func_name="$3"

  # `$(<file)` reads without forking cat/tr; command substitution drops trailing
  # newlines exactly like the previous `$(tr -d '\r' <file)`. Strip the carriage
  # returns in bash afterwards.
  local snapshot
  snapshot=$(<"$snapshot_path")
  snapshot="${snapshot//$'\r'/}"

  if ! bashunit::snapshot::match_with_placeholder "$actual" "$snapshot"; then
    local label=$(bashunit::helper::normalize_test_function_name "$func_name")
    bashunit::state::add_assertions_failed
    bashunit::console_results::print_failed_snapshot_test "$label" "$snapshot_path" "$actual"
    return 1
  fi

  bashunit::state::add_assertions_passed
}

# skip_todo.sh

function bashunit::skip() {
  local reason=${1-}
  local label
  label="$(bashunit::helper::normalize_test_function_name "${FUNCNAME[1]}")"

  bashunit::console_results::print_skipped_test "${label}" "${reason}"

  bashunit::state::add_assertions_skipped
}

function bashunit::todo() {
  local pending=${1-}
  local label
  label="$(bashunit::helper::normalize_test_function_name "${FUNCNAME[1]}")"

  bashunit::console_results::print_incomplete_test "${label}" "${pending}"

  bashunit::state::add_assertions_incomplete
}

# test_doubles.sh

declare -a _BASHUNIT_MOCKED_FUNCTIONS=()

# Spy/mock state is keyed by the sanitized command name and stored in globals
# prefixed with `_BASHUNIT_SPY_` so they cannot collide with caller locals.
# A test that does `local foo_times_file=...` is harmless because the helper
# resolves `_BASHUNIT_SPY_foo_TIMES_FILE` instead.

function bashunit::unmock() {
  local command=$1

  if [ "${#_BASHUNIT_MOCKED_FUNCTIONS[@]}" -eq 0 ]; then
    return
  fi

  local i
  for i in "${!_BASHUNIT_MOCKED_FUNCTIONS[@]}"; do
    if [ "${_BASHUNIT_MOCKED_FUNCTIONS[$i]:-}" = "$command" ]; then
      unset "_BASHUNIT_MOCKED_FUNCTIONS[$i]"
      unset -f "$command"
      local variable
      variable="$(bashunit::helper::normalize_variable_name "$command")"
      local times_file_var="_BASHUNIT_SPY_${variable}_TIMES_FILE"
      local params_file_var="_BASHUNIT_SPY_${variable}_PARAMS_FILE"
      [ -f "${!times_file_var-}" ] && rm -f "${!times_file_var}"
      [ -f "${!params_file_var-}" ] && rm -f "${!params_file_var}"
      unset "$times_file_var"
      unset "$params_file_var"
      break
    fi
  done
}

function bashunit::mock() {
  local command=$1
  shift

  if [ $# -gt 0 ]; then
    eval "function $command() { $* \"\$@\"; }"
  else
    eval "function $command() { builtin echo \"$($CAT)\" ; }"
  fi

  export -f "${command?}"

  _BASHUNIT_MOCKED_FUNCTIONS[${#_BASHUNIT_MOCKED_FUNCTIONS[@]}]="$command"
}

function bashunit::spy() {
  local command=$1
  local exit_code_or_impl="${2:-}"
  local variable
  variable="$(bashunit::helper::normalize_variable_name "$command")"

  local times_file params_file
  local test_id="${BASHUNIT_CURRENT_TEST_ID:-global}"
  times_file=$(bashunit::temp_file "${test_id}_${variable}_times")
  params_file=$(bashunit::temp_file "${test_id}_${variable}_params")
  echo 0 >"$times_file"
  : >"$params_file"
  export "_BASHUNIT_SPY_${variable}_TIMES_FILE"="$times_file"
  export "_BASHUNIT_SPY_${variable}_PARAMS_FILE"="$params_file"

  local body_suffix=""
  if [[ "$exit_code_or_impl" =~ ^[0-9]+$ ]]; then
    body_suffix="return $exit_code_or_impl"
  elif [ -n "$exit_code_or_impl" ]; then
    body_suffix="$exit_code_or_impl \"\$@\""
  fi

  eval "function $command() {
    local raw=\"\$*\"
    local serialized=\"\"
    local arg
    for arg in \"\$@\"; do
      serialized=\"\$serialized\$(builtin printf '%q' \"\$arg\")$'\\x1f'\"
    done
    serialized=\${serialized%$'\\x1f'}
    builtin printf '%s\x1e%s\\n' \"\$raw\" \"\$serialized\" >> '$params_file'
    local _c
    _c=\$(cat '$times_file' 2>/dev/null || builtin echo 0)
    _c=\$((_c+1))
    builtin echo \"\$_c\" > '$times_file'
    $body_suffix
  }"

  export -f "${command?}"

  _BASHUNIT_MOCKED_FUNCTIONS[${#_BASHUNIT_MOCKED_FUNCTIONS[@]}]="$command"
}

function assert_have_been_called() {
  local command=$1
  local variable
  variable="$(bashunit::helper::normalize_variable_name "$command")"
  local file_var="_BASHUNIT_SPY_${variable}_TIMES_FILE"
  local times=0
  if [ -f "${!file_var-}" ]; then
    times=$(cat "${!file_var}" 2>/dev/null || builtin echo 0)
  fi
  local label="${2:-$(bashunit::helper::normalize_test_function_name "${FUNCNAME[1]}")}"

  if [ "$times" -eq 0 ]; then
    bashunit::state::add_assertions_failed
    bashunit::console_results::print_failed_test "${label}" "${command}" "to have been called" "once"
    return
  fi

  bashunit::state::add_assertions_passed
}

function assert_have_been_called_with() {
  local command=$1
  shift

  local index=""
  # A trailing all-digits arg selects the nth recorded call. Pure-bash glob
  # avoids forking echo+grep on every assertion.
  case "${!#}" in
  '' | *[!0-9]*) ;;
  *)
    index=${!#}
    set -- "${@:1:$#-1}"
    ;;
  esac

  local expected="$*"

  local variable
  variable="$(bashunit::helper::normalize_variable_name "$command")"
  local file_var="_BASHUNIT_SPY_${variable}_PARAMS_FILE"
  local line=""
  if [ -f "${!file_var-}" ]; then
    if [ -n "$index" ]; then
      line=$(sed -n "${index}p" "${!file_var}" 2>/dev/null || true)
    else
      line=$(tail -n 1 "${!file_var}" 2>/dev/null || true)
    fi
  fi

  local raw
  IFS=$'\x1e' read -r raw _ <<<"$line" || true

  if [ "$expected" != "$raw" ]; then
    bashunit::state::add_assertions_failed
    bashunit::console_results::print_failed_test "$(bashunit::helper::normalize_test_function_name \
      "${FUNCNAME[1]}")" "$expected" "but got " "$raw"
    return
  fi

  bashunit::state::add_assertions_passed
}

function assert_have_been_called_times() {
  local expected_count=$1
  local command=$2
  local variable
  variable="$(bashunit::helper::normalize_variable_name "$command")"
  local file_var="_BASHUNIT_SPY_${variable}_TIMES_FILE"
  local times=0
  if [ -f "${!file_var-}" ]; then
    times=$(cat "${!file_var}" 2>/dev/null || builtin echo 0)
  fi
  local label="${3:-$(bashunit::helper::normalize_test_function_name "${FUNCNAME[1]}")}"
  if [ "$times" -ne "$expected_count" ]; then
    bashunit::state::add_assertions_failed
    bashunit::console_results::print_failed_test "${label}" "${command}" \
      "to have been called" "${expected_count} times" \
      "actual" "${times} times"
    return
  fi

  bashunit::state::add_assertions_passed
}

function assert_have_been_called_nth_with() {
  local nth=$1
  local command=$2
  shift 2
  local expected="$*"

  local variable
  variable="$(bashunit::helper::normalize_variable_name "$command")"
  local times_file_var="_BASHUNIT_SPY_${variable}_TIMES_FILE"
  local file_var="_BASHUNIT_SPY_${variable}_PARAMS_FILE"
  local label
  label="$(bashunit::helper::normalize_test_function_name "${FUNCNAME[1]}")"

  local times=0
  if [ -f "${!times_file_var-}" ]; then
    times=$(cat "${!times_file_var}" 2>/dev/null || builtin echo 0)
  fi

  if [ "$nth" -gt "$times" ]; then
    bashunit::state::add_assertions_failed
    bashunit::console_results::print_failed_test "${label}" \
      "expected call" "at index ${nth} but" "only called ${times} times"
    return
  fi

  local line=""
  if [ -f "${!file_var-}" ]; then
    line=$(sed -n "${nth}p" "${!file_var}" 2>/dev/null || true)
  fi

  local raw
  IFS=$'\x1e' read -r raw _ <<<"$line" || true

  if [ "$expected" != "$raw" ]; then
    bashunit::state::add_assertions_failed
    bashunit::console_results::print_failed_test "${label}" \
      "$expected" "but got " "$raw"
    return
  fi

  bashunit::state::add_assertions_passed
}

function assert_not_called() {
  local command=$1
  local label="${2:-$(bashunit::helper::normalize_test_function_name "${FUNCNAME[1]}")}"
  assert_have_been_called_times 0 "$command" "$label"
}

# doc.sh

# This function returns the embedded assertions.md content.
# During development, it reads from the file.
# During build, this function is replaced with actual content.
function bashunit::doc::get_embedded_docs() {
  cat <<'__BASHUNIT_DOCS_EOF__'
---
description: "Complete reference of bashunit assertions for testing bash scripts: assert equals, contains, matches, exit codes, files, arrays and more, with examples."
---

# Assertions

When creating tests, you'll need to verify your commands and functions.
We provide assertions for these checks.
Below is their documentation.

## assert_true
> `assert_true bool|function|command`

Reports an error if the argument result in a truthy value: `true` or `0`.

- [assert_false](#assert-false) is similar but different.

::: code-group
```bash [Example]
function test_success() {
  assert_true true
  assert_true 0
  assert_true "eval return 0"
  assert_true mock_true
}

function test_failure() {
  assert_true false
  assert_true 1
  assert_true "eval return 1"
  assert_true mock_false
}
```
```bash [globals.sh]
function mock_true() {
  return 0
}
function mock_false() {
  return 1
}
```
:::

## assert_false
> `assert_false bool|function|command`

Reports an error if the argument result in a falsy value: `false` or `1`.

- [assert_true](#assert-true) is similar but different.

::: code-group
```bash [Example]
function test_success() {
  assert_false false
  assert_false 1
  assert_false "eval return 1"
  assert_false mock_false
}

function test_failure() {
  assert_false true
  assert_false 0
  assert_false "eval return 0"
  assert_false mock_true
}
```
```bash [globals.sh]
function mock_true() {
  return 0
}
function mock_false() {
  return 1
}
```
:::

## assert_same
> `assert_same "expected" "actual"`

Reports an error if the `expected` and `actual` are not the same - including special chars.

- [assert_not_same](#assert-not-same) is the inverse of this assertion and takes the same arguments.
- [assert_equals](#assert-equals) is similar but ignoring the special chars.

::: code-group
```bash [Example]
function test_success() {
  assert_same "foo" "foo"
}

function test_failure() {
  assert_same "foo" "bar"
}
```
:::

## assert_equals
> `assert_equals "expected" "actual"`

Reports an error if the two variables `expected` and `actual` are not equal ignoring the special chars like ANSI Escape Sequences (colors) and other special chars like tabs and new lines.

- [assert_same](#assert-same) is similar but including special chars.

::: code-group
```bash [Example]
function test_success() {
  assert_equals "foo" "\e[31mfoo"
}

function test_failure() {
  assert_equals "\e[31mfoo" "\e[31mfoo"
}
```
:::

## assert_contains
> `assert_contains "needle" "haystack"`

Reports an error if `needle` is not a substring of `haystack`.

- [assert_not_contains](#assert-not-contains) is the inverse of this assertion and takes the same arguments.

::: code-group
```bash [Example]
function test_success() {
  assert_contains "foo" "foobar"
}

function test_failure() {
  assert_contains "baz" "foobar"
}
```

:::

## assert_contains_ignore_case
> `assert_contains_ignore_case "needle" "haystack"`

Reports an error if `needle` is not a substring of `haystack`.
Differences in casing are ignored when needle is searched for in haystack.

::: code-group
```bash [Example]
function test_success() {
  assert_contains_ignore_case "foo" "FooBar"
}
function test_failure() {
  assert_contains_ignore_case "baz" "FooBar"
}
```
:::

## assert_empty
> `assert_empty "actual"`

Reports an error if `actual` is not empty.

- [assert_not_empty](#assert-not-empty) is the inverse of this assertion and takes the same arguments.

::: code-group
```bash [Example]
function test_success() {
  assert_empty ""
}

function test_failure() {
  assert_empty "foo"
}
```
:::

## assert_matches
> `assert_matches "pattern" "value"`

Reports an error if `value` does not match the regular expression `pattern`.

- [assert_not_matches](#assert-not-matches) is the inverse of this assertion and takes the same arguments.

::: code-group
```bash [Example]
function test_success() {
  assert_matches "^foo" "foobar"
}

function test_failure() {
  assert_matches "^bar" "foobar"
}
```
:::

## assert_string_starts_with
> `assert_string_starts_with "needle" "haystack"`

Reports an error if `haystack` does not starts with `needle`.

- [assert_string_not_starts_with](#assert-string-not-starts-with) is the inverse of this assertion and takes the same arguments.

::: code-group
```bash [Example]
function test_success() {
  assert_string_starts_with "foo" "foobar"
}

function test_failure() {
  assert_string_starts_with "baz" "foobar"
}
```
:::

## assert_string_ends_with
> `assert_string_ends_with "needle" "haystack"`

Reports an error if `haystack` does not ends with `needle`.

- [assert_string_not_ends_with](#assert-string-not-ends-with) is the inverse of this assertion and takes the same arguments.

::: code-group
```bash [Example]
function test_success() {
  assert_string_ends_with "bar" "foobar"
}

function test_failure() {
  assert_string_ends_with "foo" "foobar"
}
```
:::

## assert_string_matches_format
> `assert_string_matches_format "format" "value"`

Reports an error if `value` does not match the `format` string. The format string uses PHPUnit-style placeholders:

| Placeholder | Matches |
|-------------|---------|
| `%d` | One or more digits |
| `%i` | Signed integer (e.g. `+1`, `-42`) |
| `%f` | Floating point number (e.g. `3.14`) |
| `%s` | One or more non-whitespace characters |
| `%x` | Hexadecimal (e.g. `ff00ab`) |
| `%e` | Scientific notation (e.g. `1.5e10`) |
| `%%` | Literal `%` character |

- [assert_string_not_matches_format](#assert-string-not-matches-format) is the inverse of this assertion and takes the same arguments.

::: code-group
```bash [Example]
function test_success() {
  assert_string_matches_format "%d items found" "42 items found"
  assert_string_matches_format "%s has %d items at %f each" "cart has 5 items at 9.99 each"
}

function test_failure() {
  assert_string_matches_format "%d items" "hello world"
}
```
:::

## assert_line_count
> `assert_line_count "count" "haystack"`

Reports an error if `haystack` does not contain `count` lines.

::: code-group
```bash [Example]
function test_success() {
  local string="this is line one
this is line two
this is line three"

  assert_line_count 3 "$string"
}

function test_failure() {
  assert_line_count 2 "foobar"
}
```
:::

## assert_less_than
> `assert_less_than "expected" "actual"`

Reports an error if `actual` is not less than `expected`.

- [assert_greater_than](#assert-greater-than) is the inverse of this assertion and takes the same arguments.

::: code-group
```bash [Example]
function test_success() {
  assert_less_than "999" "1"
}

function test_failure() {
  assert_less_than "1" "999"
}
```
:::

## assert_less_or_equal_than
> `assert_less_or_equal_than "expected" "actual"`

Reports an error if `actual` is not less than or equal to `expected`.

- [assert_greater_than](#assert-greater-or-equal-than) is the inverse of this assertion and takes the same arguments.

::: code-group
```bash [Example]
function test_success() {
  assert_less_or_equal_than "999" "1"
}

function test_success_with_two_equal_numbers() {
  assert_less_or_equal_than "999" "999"
}

function test_failure() {
  assert_less_or_equal_than "1" "999"
}
```
:::

## assert_greater_than
> `assert_greater_than "expected" "actual"`

Reports an error if `actual` is not greater than `expected`.

- [assert_less_than](#assert-less-than) is the inverse of this assertion and takes the same arguments.

::: code-group
```bash [Example]
function test_success() {
  assert_greater_than "1" "999"
}

function test_failure() {
  assert_greater_than "999" "1"
}
```
:::

## assert_greater_or_equal_than
> `assert_greater_or_equal_than "expected" "actual"`

Reports an error if `actual` is not greater than or equal to `expected`.

- [assert_less_or_equal_than](#assert-less-or-equal-than) is the inverse of this assertion and takes the same arguments.

::: code-group
```bash [Example]
function test_success() {
  assert_greater_or_equal_than "1" "999"
}

function test_success_with_two_equal_numbers() {
  assert_greater_or_equal_than "999" "999"
}

function test_failure() {
  assert_greater_or_equal_than "999" "1"
}
```
:::

## assert_within_delta
> `assert_within_delta "expected" "actual" "delta"`

Reports an error if `actual` is not within `delta` of `expected`
(i.e. `|actual - expected| > delta`). Supports floating-point values.
Useful for timing or measured values where exact equality is too strict.

::: code-group
```bash [Example]
function test_success() {
  assert_within_delta "3.14159" "3.14" "0.01"
}

function test_failure() {
  assert_within_delta "100" "105" "3"
}
```
:::

## assert_date_equals
> `assert_date_equals "expected" "actual"`

Reports an error if the two date values `expected` and `actual` are not equal.

Inputs are automatically converted to epoch seconds. Supported formats:
- Epoch seconds (integers): `1700000000`
- ISO 8601 date: `2023-11-14`
- ISO 8601 datetime: `2023-11-14T12:00:00`
- ISO 8601 datetime with UTC Z: `2023-11-14T12:00:00Z`
- ISO 8601 datetime with timezone offset: `2023-11-14T12:00:00+0100`
- Space-separated datetime: `2023-11-14 12:00:00`

You can mix formats in the same assertion (e.g., one epoch, one ISO).

::: code-group
```bash [Example]
function test_success() {
  local now
  now="$(date +%s)"

  assert_date_equals "$now" "$now"
}

function test_failure() {
  assert_date_equals "1700000000" "1600000000"
}
```
:::

## assert_date_before
> `assert_date_before "expected" "actual"`

Reports an error if `actual` is not before `expected` (i.e. `actual` must be less than `expected`).

Inputs are automatically converted to epoch seconds. See [assert_date_equals](#assert_date_equals) for supported formats.

::: code-group
```bash [Example]
function test_success() {
  assert_date_before "1700000000" "1600000000"
}

function test_failure() {
  assert_date_before "1700000000" "1800000000"
}
```
:::

## assert_date_after
> `assert_date_after "expected" "actual"`

Reports an error if `actual` is not after `expected` (i.e. `actual` must be greater than `expected`).

Inputs are automatically converted to epoch seconds. See [assert_date_equals](#assert_date_equals) for supported formats.

::: code-group
```bash [Example]
function test_success() {
  assert_date_after "1600000000" "1700000000"
}

function test_failure() {
  assert_date_after "1600000000" "1500000000"
}
```
:::

## assert_date_within_range
> `assert_date_within_range "from" "to" "actual"`

Reports an error if `actual` does not fall between `from` and `to` (inclusive).

Inputs are automatically converted to epoch seconds. See [assert_date_equals](#assert_date_equals) for supported formats.

::: code-group
```bash [Example]
function test_success() {
  assert_date_within_range "1600000000" "1800000000" "1700000000"
}

function test_failure() {
  assert_date_within_range "1600000000" "1800000000" "1900000000"
}
```
:::

## assert_date_within_delta
> `assert_date_within_delta "expected" "actual" "delta"`

Reports an error if `actual` is not within `delta` seconds of `expected`.

Inputs are automatically converted to epoch seconds. See [assert_date_equals](#assert_date_equals) for supported formats.

::: code-group
```bash [Example]
function test_success() {
  local now
  now="$(date +%s)"
  local five_seconds_later=$(( now + 5 ))

  assert_date_within_delta "$now" "$five_seconds_later" "10"
}

function test_failure() {
  assert_date_within_delta "1700000000" "1700000020" "5"
}
```
:::

## assert_exit_code
> `assert_exit_code "expected"`

Reports an error if the exit code of the last executed command is not equal to `expected`.

This assertion captures `$?` from the command executed **before** calling the assertion.
It does **not** execute a string command passed as a second parameter.

::: tip
Use [assert_exec](#assert-exec) if you want to pass a command as a string and check its exit code:
`assert_exec "your_command" --exit 0`
:::

- [assert_successful_code](#assert-successful-code), [assert_unsuccessful_code](#assert-unsuccessful-code), [assert_general_error](#assert-general-error) and [assert_command_not_found](#assert-command-not-found)
are more semantic versions of this assertion, for which you don't need to specify an exit code.

::: code-group
```bash [Example]
function test_success_checking_previous_command() {
  function foo() {
    return 1
  }

  foo

  assert_exit_code "1"
}

function test_success_with_external_command() {
  touch /tmp/myfile

  assert_exit_code "0"
}

function test_failure() {
  function foo() {
    return 1
  }

  foo

  assert_exit_code "0"
}
```
:::

## assert_exec
> `assert_exec "command" [--exit <code>] [--stdout "text"] [--stderr "text"] [--stdout-contains "needle"] [--stdout-not-contains "needle"] [--stderr-contains "needle"] [--stderr-not-contains "needle"] [--stdin "input"]`

Runs `command` capturing its exit status, standard output and standard error and
checks all provided expectations. When `--exit` is omitted the expected exit
status defaults to `0`.

Use `--stdin` to feed input into interactive commands (e.g. commands using
`read`). Multiple answers can be passed by separating them with newlines.

Use `--stdout-contains` / `--stdout-not-contains` (and the `stderr-*` variants)
for substring matching when you don't want to assert against the full output.

::: code-group
```bash [Example]
function sample() {
  echo "out"
  echo "err" >&2
  return 1
}

function test_success() {
  assert_exec sample --exit 1 --stdout "out" --stderr "err"
}

function test_failure() {
  assert_exec sample --exit 0 --stdout "out" --stderr "err"
}
```

```bash [Interactive]
function question() {
  local name lang
  read -r name
  read -r lang
  echo "Your name is $name and you prefer $lang."
}

function test_interactive_prompt() {
  assert_exec question \
    --stdin "Chemaclass"$'\n'"Phel-Lang"$'\n' \
    --stdout-contains "Your name is Chemaclass and you prefer Phel-Lang." \
    --stdout-not-contains "Delphi" \
    --exit 0
}
```
:::

## assert_arrays_equal
> `assert_arrays_equal "expected..." -- "actual..."`

Reports an error if the arrays have different lengths or any element differs at the same index.

Use `--` to separate the expected array from the actual array.

::: code-group
```bash [Example]
function test_success() {
  local expected=(foo bar baz)
  local actual=(foo bar baz)

  assert_arrays_equal "${expected[@]}" -- "${actual[@]}"
}

function test_failure() {
  local expected=(foo bar baz)
  local actual=(foo baz bar)

  assert_arrays_equal "${expected[@]}" -- "${actual[@]}"
}
```
:::

## assert_array_contains
> `assert_array_contains "needle" "haystack"`

Reports an error if `needle` is not an element of `haystack`.

- [assert_array_not_contains](#assert-array-not-contains) is the inverse of this assertion and takes the same arguments.

::: code-group
```bash [Example]
function test_success() {
  local haystack=(foo bar baz)

  assert_array_contains "bar" "${haystack[@]}"
}

function test_failure() {
  local haystack=(foo bar baz)

  assert_array_contains "foobar" "${haystack[@]}"
}
```
:::

## assert_array_length
> `assert_array_length "expected_length" "array"`

Reports an error if `array` does not have exactly `expected_length` elements.

::: code-group
```bash [Example]
function test_success() {
  local haystack=(foo bar baz)

  assert_array_length 3 "${haystack[@]}"
}

function test_failure() {
  local haystack=(foo bar baz)

  assert_array_length 2 "${haystack[@]}"
}
```
:::

## assert_successful_code
> `assert_successful_code`

Reports an error if the exit code of the last executed command is not successful (`0`).

This assertion captures `$?` from the command executed **before** calling the assertion.
It does **not** execute a string command passed as a parameter.

::: tip
Use [assert_exec](#assert-exec) if you want to pass a command as a string and check its exit code:
`assert_exec "your_command"` (defaults to expecting exit code 0)
:::

- [assert_exit_code](#assert-exit-code) is the full version of this assertion where you can specify the expected exit code.

::: code-group
```bash [Example]
function test_success_with_function() {
  function foo() {
    return 0
  }

  foo

  assert_successful_code
}

function test_success_with_external_command() {
  touch /tmp/myfile

  assert_successful_code
}

function test_failure() {
  function foo() {
    return 1
  }

  foo

  assert_successful_code
}
```
:::

## assert_unsuccessful_code
> `assert_unsuccessful_code`

Reports an error if the exit code of the last executed command is not unsuccessful (non-zero).

This assertion captures `$?` from the command executed **before** calling the assertion.
It does **not** execute a string command passed as a parameter.

::: tip
Use [assert_exec](#assert-exec) if you want to pass a command as a string and check its exit code:
`assert_exec "your_command" --exit 1`
:::

- [assert_exit_code](#assert-exit-code) is the full version of this assertion where you can specify the expected exit code.

::: code-group
```bash [Example]
function test_success_with_function() {
  function foo() {
    return 1
  }

  foo

  assert_unsuccessful_code
}

function test_success_with_failing_command() {
  ls /nonexistent_path 2>/dev/null

  assert_unsuccessful_code
}

function test_failure() {
  function foo() {
    return 0
  }

  foo

  assert_unsuccessful_code
}
```
:::

## assert_general_error
> `assert_general_error`

Reports an error if the exit code of the last executed command is not a general error (`1`).

This assertion captures `$?` from the command executed **before** calling the assertion.
It does **not** execute a string command passed as a parameter.

::: tip
Use [assert_exec](#assert-exec) if you want to pass a command as a string and check its exit code:
`assert_exec "your_command" --exit 1`
:::

- [assert_exit_code](#assert-exit-code) is the full version of this assertion where you can specify the expected exit code.

::: code-group
```bash [Example]
function test_success_with_function() {
  function foo() {
    return 1
  }

  foo

  assert_general_error
}

function test_success_with_external_command() {
  grep "nonexistent" /dev/null

  assert_general_error
}

function test_failure() {
  function foo() {
    return 0
  }

  foo

  assert_general_error
}
```
:::

## assert_command_not_found
> `assert_command_not_found`

Reports an error if the last executed command did not return a "command not found" exit code (`127`).

This assertion captures `$?` from the command executed **before** calling the assertion.
It does **not** execute a string command passed as a parameter.

::: tip
Use [assert_exec](#assert-exec) if you want to pass a command as a string and check its exit code:
`assert_exec "nonexistent_command" --exit 127`
:::

- [assert_exit_code](#assert-exit-code) is the full version of this assertion where you can specify the expected exit code.

::: code-group
```bash [Example]
function test_success_with_nonexistent_command() {
  nonexistent_command 2>/dev/null

  assert_command_not_found
}

function test_failure_with_existing_command() {
  ls > /dev/null 2>&1

  assert_command_not_found
}
```
:::

## assert_file_exists
> `assert_file_exists "file"`

Reports an error if `file` does not exists, or it is a directory.

- [assert_file_not_exists](#assert-file-not-exists) is the inverse of this assertion and takes the same arguments.

::: code-group
```bash [Example]
function test_success() {
  local file_path="foo.txt"
  touch "$file_path"

  assert_file_exists "$file_path"
  rm "$file_path"
}

function test_failure() {
  local file_path="foo.txt"
  rm -f $file_path

  assert_file_exists "$file_path"
}
```
:::

## assert_file_contains
> `assert_file_contains "file" "search"`

Reports an error if `file` does not contains the search string.

- [assert_file_not_contains](#assert-file-not-contains) is the inverse of this assertion and takes the same arguments.

::: code-group
```bash [Example]
function test_success() {
  local file="/tmp/file-path.txt"
  echo -e "original content" > "$file"

  assert_file_contains "$file" "content"
}

function test_failure() {
  local file="/tmp/file-path.txt"
  echo -e "original content" > "$file"

  assert_file_contains "$file" "non existing"
}
```
:::

## assert_file_permissions
> `assert_file_permissions "mode" "file"`

Reports an error if `file` does not have the expected octal permission `mode`
(e.g. `644`, `0755`). A leading zero is optional (`0755` and `755` are equal).
Works on both Linux (GNU `stat`) and macOS (BSD `stat`).

::: code-group
```bash [Example]
function test_success() {
  local file="/tmp/file-path.txt"
  touch "$file"
  chmod 600 "$file"

  assert_file_permissions "600" "$file"
}

function test_failure() {
  local file="/tmp/file-path.txt"
  touch "$file"
  chmod 644 "$file"

  assert_file_permissions "600" "$file"
}
```
:::

## assert_is_file
> `assert_is_file "file"`

Reports an error if `file` is not a file.

::: code-group
```bash [Example]
function test_success() {
  local file_path="foo.txt"
  touch "$file_path"

  assert_is_file "$file_path"
  rm "$file_path"
}

function test_failure() {
  local dir_path="bar"
  mkdir "$dir_path"

  assert_is_file "$dir_path"
  rmdir "$dir_path"
}
```
:::

## assert_is_file_empty
> `assert_is_file_empty "file"`

Reports an error if `file` is not empty.

::: code-group
```bash [Example]
function test_success() {
  local file_path="foo.txt"
  touch "$file_path"

  assert_is_file_empty "$file_path"
  rm "$file_path"
}

function test_failure() {
  local file_path="foo.txt"
  echo "bar" > "$file_path"

  assert_is_file_empty "$file_path"
  rm "$file_path"
}
```
:::

## assert_directory_exists
> `assert_directory_exists "directory"`

Reports an error if `directory` does not exist.

- [assert_directory_not_exists](#assert-directory-not-exists) is the inverse of this assertion and takes the same arguments.

::: code-group
```bash [Example]
function test_success() {
  local directory="/var"

  assert_directory_exists "$directory"
}

function test_failure() {
  local directory="/nonexistent_directory"

  assert_directory_exists "$directory"
}
```
:::

## assert_is_directory
> `assert_is_directory "directory"`

Reports an error if `directory` is not a directory.

::: code-group
```bash [Example]
function test_success() {
  local directory="/var"

  assert_is_directory "$directory"
}

function test_failure() {
  local file="/etc/hosts"

  assert_is_directory "$file"
}
```
:::

## assert_is_directory_empty
> `assert_is_directory_empty "directory"`

Reports an error if `directory` is not an empty directory.

- [assert_is_directory_not_empty](#assert-is-directory-not-empty) is the inverse of this assertion and takes the same arguments.

::: code-group
```bash [Example]
function test_success() {
  local directory="/home/user/empty_directory"
  mkdir "$directory"

  assert_is_directory_empty "$directory"
}

function test_failure() {
  local directory="/etc"

  assert_is_directory_empty "$directory"
}
```
:::

## assert_is_directory_readable
> `assert_is_directory_readable "directory"`

Reports an error if `directory` is not a readable directory.

- [assert_is_directory_not_readable](#assert-is-directory-not-readable) is the inverse of this assertion and takes the same arguments.

::: code-group
```bash [Example]
function test_success() {
  local directory="/var"

  assert_is_directory_readable "$directory"
}

function test_failure() {
  local directory="/home/user/test"
  chmod -r "$directory"

  assert_is_directory_readable "$directory"
}
```
:::

## assert_is_directory_writable
> `assert_is_directory_writable "directory"`

Reports an error if `directory` is not a writable directory.

- [assert_is_directory_not_writable](#assert-is-directory-not-writable) is the inverse of this assertion and takes the same arguments.

::: code-group
```bash [Example]
function test_success() {
  local directory="/tmp"

  assert_is_directory_writable "$directory"
}

function test_failure() {
  local directory="/home/user/test"
  chmod -w "$directory"

  assert_is_directory_writable "$directory"
}
```
:::

## assert_files_equals
> `assert_files_equals "expected" "actual"`

Reports an error if `expected` and `actual` are not equals.

- [assert_files_not_equals](#assert-files-not-equals) is the inverse of this assertion and takes the same arguments.

::: code-group
```bash [Example]
function test_success() {
  local expected="/tmp/file1.txt"
  local actual="/tmp/file2.txt"

  echo "file content" > "$expected"
  echo "file content" > "$actual"

  assert_files_equals "$expected" "$actual"
}

function test_failure() {
  local expected="/tmp/file1.txt"
  local actual="/tmp/file2.txt"

  echo "file content" > "$expected"
  echo "different content" > "$actual"

  assert_files_equals "$expected" "$actual"
}
```
```[Output]
✓ Passed: Success
✗ Failed: Failure
    Expected '/tmp/file1.txt'
    Compared '/tmp/file2.txt'
    Diff '@@ -1 +1 @@
-file content
+different content'
```
:::

## assert_not_same
> `assert_not_same "expected" "actual"`

Reports an error if the two variables `expected` and `actual` are the same value.

- [assert_same](#assert-same) is the inverse of this assertion and takes the same arguments.

::: code-group
```bash [Example]
function test_success() {
  assert_not_same "foo" "bar"
}

function test_failure() {
  assert_not_same "foo" "foo"
}
```
:::

## assert_not_contains
> `assert_not_contains "needle" "haystack"`

Reports an error if `needle` is a substring of `haystack`.

- [assert_contains](#assert-contains) is the inverse of this assertion and takes the same arguments.

::: code-group
```bash [Example]
function test_success() {
  assert_not_contains "baz" "foobar"
}

function test_failure() {
  assert_not_contains "foo" "foobar"
}
```
:::

## assert_string_not_starts_with
> `assert_string_not_starts_with "needle" "haystack"`

Reports an error if `haystack` does starts with `needle`.

- [assert_string_starts_with](#assert-string-starts-with) is the inverse of this assertion and takes the same arguments.

::: code-group
```bash [Example]
function test_success() {
  assert_string_not_starts_with "bar" "foobar"
}

function test_failure() {
  assert_string_not_starts_with "foo" "foobar"
}
```
:::

## assert_string_not_ends_with
> `assert_string_not_ends_with "needle" "haystack"`

Reports an error if `haystack` does ends with `needle`.

- [assert_string_ends_with](#assert-string-ends-with) is the inverse of this assertion and takes the same arguments.

::: code-group
```bash [Example]
function test_success() {
  assert_string_not_ends_with "foo" "foobar"
}

function test_failure() {
  assert_string_not_ends_with "bar" "foobar"
}
```
:::

## assert_not_empty
> `assert_not_empty "actual"`

Reports an error if `actual` is empty.

- [assert_empty](#assert-empty) is the inverse of this assertion and takes the same arguments.

::: code-group
```bash [Example]
function test_success() {
  assert_not_empty "foo"
}

function test_failure() {
  assert_not_empty ""
}
```
:::

## assert_not_matches
> `assert_not_matches "pattern" "value"`

Reports an error if `value` matches the regular expression `pattern`.

- [assert_matches](#assert-matches) is the inverse of this assertion and takes the same arguments.

::: code-group
```bash [Example]
function test_success() {
  assert_not_matches "foo$" "foobar"
}

function test_failure() {
  assert_not_matches "bar$" "foobar"
}
```
:::

## assert_string_not_matches_format
> `assert_string_not_matches_format "format" "value"`

Reports an error if `value` matches the `format` string. See [assert_string_matches_format](#assert-string-matches-format) for supported placeholders.

- [assert_string_matches_format](#assert-string-matches-format) is the inverse of this assertion and takes the same arguments.

::: code-group
```bash [Example]
function test_success() {
  assert_string_not_matches_format "%d items" "hello world"
}

function test_failure() {
  assert_string_not_matches_format "%d items" "42 items"
}
```
:::

## assert_array_not_contains
> `assert_array_not_contains "needle" "haystack"`

Reports an error if `needle` is an element of `haystack`.

- [assert_array_contains](#assert-array-contains) is the inverse of this assertion and takes the same arguments.

::: code-group
```bash [Example]
function test_success() {
  local haystack=(foo bar baz)

  assert_array_not_contains "foobar" "${haystack[@]}"
}

function test_failure() {
  local haystack=(foo bar baz)

  assert_array_not_contains "baz" "${haystack[@]}"
}
```
:::

## assert_file_not_exists
> `assert_file_not_exists "file"`

Reports an error if `file` does exists.

- [assert_file_exists](#assert-file-exists) is the inverse of this assertion and takes the same arguments.

::: code-group
```bash [Example]
function test_success() {
  local file_path="foo.txt"
  touch "$file_path"
  rm "$file_path"

  assert_file_not_exists "$file_path"
}

function test_failed() {
  local file_path="foo.txt"
  touch "$file_path"

  assert_file_not_exists "$file_path"
  rm "$file_path"
}
```
:::

## assert_file_not_contains
> `assert_file_not_contains "file" "search"`

Reports an error if `file` contains the search string.

- [assert_file_contains](#assert-file-contains) is the inverse of this assertion and takes the same arguments.

::: code-group
```bash [Example]
function test_success() {
  local file="/tmp/file-path.txt"
  echo -e "original content" > "$file"

  assert_file_not_contains "$file" "non existing"
}

function test_failure() {
  local file="/tmp/file-path.txt"
  echo -e "original content" > "$file"

  assert_file_not_contains "$file" "content"
}
```
:::

## assert_directory_not_exists
> `assert_directory_not_exists "directory"`

Reports an error if `directory` exists.

- [assert_directory_exists](#assert-directory-exists) is the inverse of this assertion and takes the same arguments.

::: code-group
```bash [Example]
function test_success() {
  local directory="/nonexistent_directory"

  assert_directory_not_exists "$directory"
}

function test_failure() {
  local directory="/var"

  assert_directory_not_exists "$directory"
}
```
:::

## assert_is_directory_not_empty
> `assert_is_directory_not_empty "directory"`

Reports an error if `directory` is empty.

- [assert_is_directory_empty](#assert-is-directory-empty) is the inverse of this assertion and takes the same arguments.

::: code-group
```bash [Example]
function test_success() {
  local directory="/etc"

  assert_is_directory_not_empty "$directory"
}

function test_failure() {
  local directory="/home/user/empty_directory"
  mkdir "$directory"

  assert_is_directory_not_empty "$directory"
}
```
:::

## assert_is_directory_not_readable
> `assert_is_directory_not_readable "directory"`

Reports an error if `directory` is readable.

- [assert_is_directory_readable](#assert-is-directory-readable) is the inverse of this assertion and takes the same arguments.

::: code-group
```bash [Example]
function test_success() {
  local directory="/home/user/test"
  chmod -r "$directory"

  assert_is_directory_not_readable "$directory"
}

function test_failure() {
  local directory="/var"

  assert_is_directory_not_readable "$directory"
}
```
:::

## assert_is_directory_not_writable
> `assert_is_directory_not_writable "directory"`

Reports an error if `directory` is writable.

- [assert_is_directory_writable](#assert-is-directory-writable) is the inverse of this assertion and takes the same arguments.

::: code-group
```bash [Example]
function test_success() {
  local directory="/home/user/test"
  chmod -w "$directory"

  assert_is_directory_not_writable "$directory"
}

function test_failure() {
  local directory="/tmp"

  assert_is_directory_not_writable "$directory"
}
```
:::


## assert_files_not_equals
> `assert_files_not_equals "expected" "actual"`

Reports an error if `expected` and `actual` are not equals.

- [assert_files_equals](#assert-files-equals) is the inverse of this assertion and takes the same arguments.

::: code-group
```bash [Example]
function test_success() {
  local expected="/tmp/file1.txt"
  local actual="/tmp/file2.txt"

  echo "file content" > "$expected"
  echo "different content" > "$actual"

  assert_files_not_equals "$expected" "$actual"
}

function test_failure() {

  local expected="/tmp/file1.txt"
  local actual="/tmp/file2.txt"

  echo "file content" > "$expected"
  echo "file content" > "$actual"

  assert_files_not_equals "$expected" "$actual"
}
```
```[Output]
✓ Passed: Success
✗ Failed: Failure
    Expected '/tmp/file1.txt'
    Compared '/tmp/file2.txt'
    Diff 'Files are equals'
```
:::

## assert_json_key_exists
> `assert_json_key_exists "key" "json"`

Reports an error if `key` does not exist in the JSON string. Uses [jq](https://jqlang.github.io/jq/) syntax for key paths. Requires `jq` to be installed; if missing the test is skipped.

::: code-group
```bash [Example]
function test_success() {
  assert_json_key_exists ".name" '{"name":"bashunit","version":"1.0"}'
  assert_json_key_exists ".data.id" '{"data":{"id":42}}'
}

function test_failure() {
  assert_json_key_exists ".missing" '{"name":"bashunit"}'
}
```
:::

## assert_json_contains
> `assert_json_contains "key" "expected" "json"`

Reports an error if `key` does not exist in the JSON string or its value does not equal `expected`. Uses [jq](https://jqlang.github.io/jq/) syntax for key paths. Requires `jq` to be installed; if missing the test is skipped.

::: code-group
```bash [Example]
function test_success() {
  assert_json_contains ".name" "bashunit" '{"name":"bashunit","version":"1.0"}'
  assert_json_contains ".count" "42" '{"count":42}'
}

function test_failure() {
  assert_json_contains ".name" "other" '{"name":"bashunit"}'
  assert_json_contains ".missing" "value" '{"name":"bashunit"}'
}
```
:::

## assert_json_equals
> `assert_json_equals "expected" "actual"`

Reports an error if the two JSON strings are not structurally equal. Key order is ignored. Requires `jq` to be installed; if missing the test is skipped.

::: code-group
```bash [Example]
function test_success() {
  assert_json_equals '{"b":2,"a":1}' '{"a":1,"b":2}'
}

function test_failure() {
  assert_json_equals '{"a":1}' '{"a":2}'
}
```
:::

## assert_duration
> `assert_duration "command" threshold_ms`

Reports an error if `command` takes longer than `threshold_ms` milliseconds to execute. Uses the framework's portable clock internally.

::: code-group
```bash [Example]
function test_success() {
  assert_duration "echo hello" 500
}

function test_failure() {
  assert_duration "sleep 2" 1000
}
```
:::

## assert_duration_less_than
> `assert_duration_less_than "command" threshold_ms`

Reports an error if `command` takes `threshold_ms` milliseconds or more to execute. Stricter than [assert_duration](#assert-duration) which allows equal values.

::: code-group
```bash [Example]
function test_success() {
  assert_duration_less_than "echo hello" 500
}

function test_failure() {
  assert_duration_less_than "sleep 2" 1000
}
```
:::

## assert_duration_greater_than
> `assert_duration_greater_than "command" threshold_ms`

Reports an error if `command` completes in `threshold_ms` milliseconds or less. Useful for verifying that a command takes at least a minimum amount of time.

::: code-group
```bash [Example]
function test_success() {
  assert_duration_greater_than "sleep 1" 500
}

function test_failure() {
  assert_duration_greater_than "echo hello" 5000
}
```
:::

## bashunit::fail
> `bashunit::fail "failure message"`

Unambiguously reports an error message. Useful for reporting specific message
when testing situations not covered by any `assert_*` functions.

::: code-group
```bash [Example]
function test_success() {
  if [ "$(date +%-H)" -gt 25 ]; then
    bashunit::fail "Something is very wrong with your clock"
  fi
}
function test_failure() {
  if [ "$(date +%-H)" -lt 25 ]; then
    bashunit::fail "This test will always fail"
  fi
}
```
:::

## Related

- [Custom asserts](/custom-asserts) — build your own domain-specific assertions
- [Test doubles](/test-doubles) — mocks and spies for isolated tests
- [Data providers](/data-providers) — run the same assertions over many inputs
- [Globals](/globals) — `bashunit::` helper functions
__BASHUNIT_DOCS_EOF__
}

# Single awk pass over the embedded docs: the previous line-by-line shell loop
# forked an `echo | sed` pipe per line (~3.2k forks, ~5s for the ~1.6k-line
# docs page); one awk fork does the same work in milliseconds (#832).
function bashunit::doc::print_asserts() {
  local filter="${1:-}"

  bashunit::doc::get_embedded_docs | awk -v filter="$filter" '
    {
      if ($0 ~ /^## /) {
        # Heading word: the leading [A-Za-z0-9_]* run after "## ". Only
        # assert*/bashunit* headings are doc entries; prose headings like
        # "## Related" fall through and are treated as regular content.
        fn = substr($0, 4)
        sub(/[^A-Za-z0-9_].*$/, "", fn)
        if (fn ~ /^(assert|bashunit)/) {
          if (filter == "" || index(fn, filter) > 0) {
            should_print = 1
            print $0
            doc = ""
          } else {
            should_print = 0
          }
          next
        }
      }

      if (should_print) {
        if ($0 ~ /^```/) {
          print "--------------"
          print doc
          should_print = 0
          next
        }
        if ($0 ~ /^::: code-group/) next

        # Remove markdown link brackets and anchor tags. The bracket class
        # uses the POSIX []][ idiom: busybox awk (Alpine) rejects
        # backslash-escaped brackets inside a bracket expression.
        line = $0
        gsub(/[][]/, "", line)
        gsub(/ *\(#[-a-z0-9]+\)/, "", line)
        doc = doc line "\n"
      }
    }
  '
}

# reports.sh
# shellcheck disable=SC2155

_BASHUNIT_REPORTS_TEST_FILES=()
_BASHUNIT_REPORTS_TEST_NAMES=()
_BASHUNIT_REPORTS_TEST_STATUSES=()
_BASHUNIT_REPORTS_TEST_DURATIONS=()
_BASHUNIT_REPORTS_TEST_ASSERTIONS=()
_BASHUNIT_REPORTS_TEST_FAILURES=()
_BASHUNIT_REPORTS_TEST_LINES=()

function bashunit::reports::add_test_snapshot() {
  bashunit::reports::add_test "$1" "$2" "$3" "$4" "snapshot"
}

function bashunit::reports::add_test_incomplete() {
  bashunit::reports::add_test "$1" "$2" "$3" "$4" "incomplete"
}

function bashunit::reports::add_test_skipped() {
  bashunit::reports::add_test "$1" "$2" "$3" "$4" "skipped"
}

function bashunit::reports::add_test_passed() {
  bashunit::reports::add_test "$1" "$2" "$3" "$4" "passed"
}

function bashunit::reports::add_test_risky() {
  bashunit::reports::add_test "$1" "$2" "$3" "$4" "risky"
}

function bashunit::reports::add_test_failed() {
  bashunit::reports::add_test "$1" "$2" "$3" "$4" "failed" "$5"
}

# Returns 0 when any report output is requested.
function bashunit::reports::is_enabled() {
  [ -n "${BASHUNIT_LOG_JUNIT:-}" ] ||
    [ -n "${BASHUNIT_REPORT_HTML:-}" ] ||
    [ -n "${BASHUNIT_LOG_GHA:-}" ] ||
    [ -n "${BASHUNIT_REPORT_TAP:-}" ] ||
    [ -n "${BASHUNIT_REPORT_JSON:-}" ]
}

function bashunit::reports::add_test() {
  # Skip tracking when no report output is requested
  bashunit::reports::is_enabled || return 0

  local file="$1"
  local test_name="$2"
  local duration="$3"
  local assertions="$4"
  local status="$5"
  local failure_message="${6:-}"

  # Capture the line number from the current test location ("file:line"),
  # but only when it belongs to this test's file, so a stale location from a
  # prior test never mislabels this entry.
  local line=""
  case "${_BASHUNIT_TEST_LOCATION:-}" in
    "$file":*) line="${_BASHUNIT_TEST_LOCATION##*:}" ;;
  esac

  _BASHUNIT_REPORTS_TEST_FILES[${#_BASHUNIT_REPORTS_TEST_FILES[@]}]="$file"
  _BASHUNIT_REPORTS_TEST_NAMES[${#_BASHUNIT_REPORTS_TEST_NAMES[@]}]="$test_name"
  _BASHUNIT_REPORTS_TEST_STATUSES[${#_BASHUNIT_REPORTS_TEST_STATUSES[@]}]="$status"
  _BASHUNIT_REPORTS_TEST_ASSERTIONS[${#_BASHUNIT_REPORTS_TEST_ASSERTIONS[@]}]="$assertions"
  _BASHUNIT_REPORTS_TEST_DURATIONS[${#_BASHUNIT_REPORTS_TEST_DURATIONS[@]}]="$duration"
  _BASHUNIT_REPORTS_TEST_FAILURES[${#_BASHUNIT_REPORTS_TEST_FAILURES[@]}]="$failure_message"
  _BASHUNIT_REPORTS_TEST_LINES[${#_BASHUNIT_REPORTS_TEST_LINES[@]}]="$line"
}

function bashunit::reports::__xml_escape() {
  local text="$1"
  # Strip ANSI escape sequences and control characters invalid in XML 1.0,
  # then escape XML special characters (& first to avoid double-escaping)
  echo "$text" \
    | sed -e 's/\x1b\[[0-9;]*[a-zA-Z]//g' \
    | tr -d '\000-\010\013\014\016-\037' \
    | sed -e 's/&/\&amp;/g' -e 's/</\&lt;/g' -e 's/>/\&gt;/g' -e 's/"/\&quot;/g' -e "s/'/\&apos;/g"
}

# Escapes a string for embedding in a JSON string literal (pure Bash, no jq).
# Strips ANSI/control chars that cannot appear inline, keeps \t\r\n as escapes.
function bashunit::reports::__json_escape() {
  local text="$1"
  text=$(printf '%s' "$text" | sed -e 's/\x1b\[[0-9;]*[a-zA-Z]//g' | tr -d '\000-\010\013\014\016-\037')
  # Backslash first so escapes added below are not doubled.
  text="${text//\\/\\\\}"
  text="${text//\"/\\\"}"
  text="${text//$'\t'/\\t}"
  text="${text//$'\r'/\\r}"
  text="${text//$'\n'/\\n}"
  printf '%s' "$text"
}

function bashunit::reports::generate_junit_xml() {
  local output_file="$1"

  local tests_skipped=$(bashunit::state::get_tests_skipped)
  local tests_incomplete=$(bashunit::state::get_tests_incomplete)
  local tests_failed=$(bashunit::state::get_tests_failed)
  local time_ms=$(bashunit::clock::total_runtime_in_milliseconds)
  local time
  time=$(LC_ALL=C awk -v ms="$time_ms" 'BEGIN {printf "%.3f", ms/1000}')

  {
    echo "<?xml version=\"1.0\" encoding=\"UTF-8\"?>"
    echo "<testsuites>"
    echo "  <testsuite name=\"bashunit\" tests=\"${#_BASHUNIT_REPORTS_TEST_NAMES[@]}\""
    echo "             failures=\"$tests_failed\" errors=\"0\""
    echo "             skipped=\"$(( tests_skipped + tests_incomplete ))\""
    echo "             time=\"$time\">"

    local i
    for i in "${!_BASHUNIT_REPORTS_TEST_NAMES[@]}"; do
      local file="${_BASHUNIT_REPORTS_TEST_FILES[$i]:-}"
      local name="${_BASHUNIT_REPORTS_TEST_NAMES[$i]:-}"
      local status="${_BASHUNIT_REPORTS_TEST_STATUSES[$i]:-}"
      local test_time_ms="${_BASHUNIT_REPORTS_TEST_DURATIONS[$i]:-}"
      local failure_message="${_BASHUNIT_REPORTS_TEST_FAILURES[$i]:-}"
      local test_time
      test_time=$(LC_ALL=C awk -v ms="$test_time_ms" 'BEGIN {printf "%.3f", ms/1000}')

      echo "    <testcase file=\"$file\""
      echo "        name=\"$name\""
      echo "        time=\"$test_time\">"

      # Add failure element for failed tests with actual failure message
      if [ "$status" = "failed" ]; then
        local escaped_message
        escaped_message=$(bashunit::reports::__xml_escape "$failure_message")
        echo "      <failure message=\"Test failed\">$escaped_message</failure>"
      elif [ "$status" = "risky" ]; then
        echo "      <skipped message=\"Test has no assertions (risky)\"/>"
      elif [ "$status" = "skipped" ]; then
        echo "      <skipped/>"
      elif [ "$status" = "incomplete" ]; then
        echo "      <skipped message=\"Test incomplete\"/>"
      fi

      echo "    </testcase>"
    done

    echo "  </testsuite>"
    echo "</testsuites>"
  } >"$output_file"
}

##
# Prepares a failure message for a TAP YAML diagnostic block: strips ANSI escape
# sequences, collapses newlines to spaces and doubles single quotes so the value
# is safe inside a YAML single-quoted scalar. Bash 3.0+ compatible.
##
function bashunit::reports::__tap_message() {
  echo "$1" \
    | sed -e 's/\x1b\[[0-9;]*[a-zA-Z]//g' \
    | tr '\n' ' ' \
    | sed -e "s/'/''/g"
}

##
# Writes results in TAP version 13 format (https://testanything.org).
# Passing/snapshot -> "ok", failed -> "not ok" with a YAML diagnostic,
# skipped/risky -> "# SKIP", incomplete -> "# TODO".
# Arguments: $1 - output file
##
function bashunit::reports::generate_report_tap() {
  local output_file="$1"
  local total="${#_BASHUNIT_REPORTS_TEST_NAMES[@]}"

  {
    echo "TAP version 13"
    echo "1..$total"

    local i seq=0
    for i in "${!_BASHUNIT_REPORTS_TEST_NAMES[@]}"; do
      seq=$((seq + 1))
      local name="${_BASHUNIT_REPORTS_TEST_NAMES[$i]:-}"
      local status="${_BASHUNIT_REPORTS_TEST_STATUSES[$i]:-}"
      local failure_message="${_BASHUNIT_REPORTS_TEST_FAILURES[$i]:-}"

      case "$status" in
      failed)
        echo "not ok $seq - $name"
        echo "  ---"
        echo "  message: '$(bashunit::reports::__tap_message "$failure_message")'"
        echo "  ..."
        ;;
      skipped)
        echo "ok $seq - $name # SKIP"
        ;;
      risky)
        echo "ok $seq - $name # SKIP risky (no assertions)"
        ;;
      incomplete)
        echo "ok $seq - $name # TODO"
        ;;
      *)
        echo "ok $seq - $name"
        ;;
      esac
    done
  } >"$output_file"
}

function bashunit::reports::generate_report_json() {
  local output_file="$1"
  local total="${#_BASHUNIT_REPORTS_TEST_NAMES[@]}"

  local passed=0 failed=0 skipped=0 incomplete=0 duration_total=0
  local i
  for i in "${!_BASHUNIT_REPORTS_TEST_NAMES[@]}"; do
    duration_total=$((duration_total + ${_BASHUNIT_REPORTS_TEST_DURATIONS[$i]:-0}))
    case "${_BASHUNIT_REPORTS_TEST_STATUSES[$i]:-}" in
    failed) failed=$((failed + 1)) ;;
    skipped) skipped=$((skipped + 1)) ;;
    incomplete) incomplete=$((incomplete + 1)) ;;
    # snapshot and risky ran without failing, so they count as passed here; the
    # per-test "status" field below preserves the exact category.
    *) passed=$((passed + 1)) ;;
    esac
  done

  {
    printf '{\n'
    printf '  "summary": { "total": %d, "passed": %d, "failed": %d,' \
      "$total" "$passed" "$failed"
    printf ' "skipped": %d, "incomplete": %d, "duration_ms": %d },\n' \
      "$skipped" "$incomplete" "$duration_total"
    printf '  "tests": [\n'
    local seq=0
    for i in "${!_BASHUNIT_REPORTS_TEST_NAMES[@]}"; do
      local file name status duration message sep
      file=$(bashunit::reports::__json_escape "${_BASHUNIT_REPORTS_TEST_FILES[$i]:-}")
      name=$(bashunit::reports::__json_escape "${_BASHUNIT_REPORTS_TEST_NAMES[$i]:-}")
      status="${_BASHUNIT_REPORTS_TEST_STATUSES[$i]:-}"
      duration="${_BASHUNIT_REPORTS_TEST_DURATIONS[$i]:-0}"
      message=$(bashunit::reports::__json_escape "${_BASHUNIT_REPORTS_TEST_FAILURES[$i]:-}")
      sep=","
      [ "$seq" -eq "$((total - 1))" ] && sep=""
      printf '    { "file": "%s", "name": "%s", "status": "%s", "duration_ms": %d, "message": "%s" }%s\n' \
        "$file" "$name" "$status" "$duration" "$message" "$sep"
      seq=$((seq + 1))
    done
    printf '  ]\n'
    printf '}\n'
  } >"$output_file"
}

function bashunit::reports::__gha_encode() {
  local text="$1"
  # Strip ANSI escape sequences first (one sed call)
  text=$(printf '%s' "$text" | sed -e 's/\x1b\[[0-9;]*[a-zA-Z]//g')
  # Percent-encode reserved chars per GHA workflow-commands spec.
  # Bash 3.0+ parameter expansion avoids extra awk/sed calls.
  # Order matters: encode '%' first so the sequences we inject stay literal.
  text="${text//%/%25}"
  text="${text//$'\r'/%0D}"
  text="${text//$'\n'/%0A}"
  printf '%s' "$text"
}

# Echoes GitHub Actions workflow-command annotations to stdout.
# Arguments: $1 - "failed-only" to emit just errors (default: all reportable).
function bashunit::reports::print_gha_annotations() {
  local only="${1:-all}"

  local i
  for i in "${!_BASHUNIT_REPORTS_TEST_NAMES[@]}"; do
    local file="${_BASHUNIT_REPORTS_TEST_FILES[$i]:-}"
    local name="${_BASHUNIT_REPORTS_TEST_NAMES[$i]:-}"
    local status="${_BASHUNIT_REPORTS_TEST_STATUSES[$i]:-}"
    local failure_message="${_BASHUNIT_REPORTS_TEST_FAILURES[$i]:-}"
    local line="${_BASHUNIT_REPORTS_TEST_LINES[$i]:-}"
    local level="" message=""

    case "$status" in
      failed)
        level="error"
        message="$failure_message"
        ;;
      risky)
        level="warning"
        message="Test has no assertions (risky)"
        ;;
      incomplete)
        level="notice"
        message="Test incomplete"
        ;;
      *)
        continue
        ;;
    esac

    if [ "$only" = "failed-only" ] && [ "$status" != "failed" ]; then
      continue
    fi

    local location="file=${file}"
    if [ -n "$line" ]; then
      location="${location},line=${line}"
    fi

    local encoded_message
    encoded_message=$(bashunit::reports::__gha_encode "$message")
    echo "::${level} ${location},title=${name}::${encoded_message}"
  done
}

function bashunit::reports::generate_gha_log() {
  local output_file="$1"

  bashunit::reports::print_gha_annotations all >"$output_file"
}

function bashunit::reports::generate_report_html() {
  local output_file="$1"

  local test_passed=$(bashunit::state::get_tests_passed)
  local tests_skipped=$(bashunit::state::get_tests_skipped)
  local tests_incomplete=$(bashunit::state::get_tests_incomplete)
  local tests_snapshot=$(bashunit::state::get_tests_snapshot)
  local tests_failed=$(bashunit::state::get_tests_failed)
  local time=$(bashunit::clock::total_runtime_in_milliseconds)

  # Temporary file to store test cases by file (use mktemp for parallel safety)
  local temp_file
  temp_file=$(mktemp "${TMPDIR:-/tmp}/bashunit-report.XXXXXX")

  # Collect test cases by file
  : >"$temp_file" # Clear temp file if it exists
  local i
  for i in "${!_BASHUNIT_REPORTS_TEST_NAMES[@]}"; do
    local file="${_BASHUNIT_REPORTS_TEST_FILES[$i]:-}"
    local name="${_BASHUNIT_REPORTS_TEST_NAMES[$i]:-}"
    local status="${_BASHUNIT_REPORTS_TEST_STATUSES[$i]:-}"
    local test_time="${_BASHUNIT_REPORTS_TEST_DURATIONS[$i]:-}"
    local test_case="$file|$name|$status|$test_time"

    echo "$test_case" >>"$temp_file"
  done

  {
    echo "<!DOCTYPE html>"
    echo "<html lang=\"en\">"
    echo "<head>"
    echo "  <meta charset=\"UTF-8\">"
    echo "  <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">"
    echo "  <title>Test Report</title>"
    echo "  <style>"
    echo "    body { font-family: Arial, sans-serif; }"
    echo "    table { width: 100%; border-collapse: collapse; }"
    echo "    th, td { border: 1px solid #ddd; padding: 8px; text-align: left; }"
    echo "    th { background-color: #f2f2f2; }"
    echo "    .passed { background-color: #dff0d8; }"
    echo "    .failed { background-color: #f2dede; }"
    echo "    .skipped { background-color: #fcf8e3; }"
    echo "    .incomplete { background-color: #d9edf7; }"
    echo "    .snapshot { background-color: #dfe6e9; }"
    echo "    .risky { background-color: #f5e6f5; }"
    echo "  </style>"
    echo "</head>"
    echo "<body>"
    echo "  <h1>Test Report</h1>"
    echo "  <table>"
    echo "    <thead>"
    echo "      <tr>"
    echo "        <th>Total Tests</th>"
    echo "        <th>Passed</th>"
    echo "        <th>Failed</th>"
    echo "        <th>Incomplete</th>"
    echo "        <th>Skipped</th>"
    echo "        <th>Snapshot</th>"
    echo "        <th>Time (ms)</th>"
    echo "      </tr>"
    echo "    </thead>"
    echo "    <tbody>"
    echo "      <tr>"
    echo "        <td>${#_BASHUNIT_REPORTS_TEST_NAMES[@]}</td>"
    echo "        <td>$test_passed</td>"
    echo "        <td>$tests_failed</td>"
    echo "        <td>$tests_incomplete</td>"
    echo "        <td>$tests_skipped</td>"
    echo "        <td>$tests_snapshot</td>"
    echo "        <td>$time</td>"
    echo "      </tr>"
    echo "    </tbody>"
    echo "  </table>"
    echo "  <p>Time: $time ms</p>"

    # Read the temporary file and group by file
    local current_file=""
    local file name status test_time
    while IFS='|' read -r file name status test_time; do
      if [ "$file" != "$current_file" ]; then
        if [ -n "$current_file" ]; then
          echo "    </tbody>"
          echo "  </table>"
        fi
        echo "  <h2>File: $file</h2>"
        echo "  <table>"
        echo "    <thead>"
        echo "      <tr>"
        echo "        <th>Test Name</th>"
        echo "        <th>Status</th>"
        echo "        <th>Time (ms)</th>"
        echo "      </tr>"
        echo "    </thead>"
        echo "    <tbody>"
        current_file="$file"
      fi
      echo "      <tr class=\"$status\">"
      echo "        <td>$name</td>"
      echo "        <td>$status</td>"
      echo "        <td>$test_time</td>"
      echo "      </tr>"
    done <"$temp_file"

    # Close the last table
    if [ -n "$current_file" ]; then
      echo "    </tbody>"
      echo "  </table>"
    fi

    echo "</body>"
    echo "</html>"
  } >"$output_file"

  # Clean up temporary file
  rm -f "$temp_file"
}

# rerun.sh

##
# --rerun-failed support.
#
# Recording (every run, regardless of the flag): each failing test appends its
# raw "<test_file>:<function_name>" identity to the shared collection temp file
# RERUN_FAILED_OUTPUT_PATH (created in env.sh). File appends work across the
# parallel test subshells, so both modes share one collector. At the end of a
# run the collector is persisted to the cache file (deduped); a fully green run
# truncates it.
#
# Replay (only with --rerun-failed): the cache is loaded, discovery is
# restricted to the recorded files, and each file's functions are filtered to
# the recorded names. --filter/--tag still apply on top (intersection).
##

# Entries loaded for replay: newline-delimited "<file>:<fn>". Empty when none.
_BASHUNIT_RERUN_ENTRIES=""

##
# Path to the persisted cache file. Defaults to ".bashunit/last-failed" under
# the working directory; BASHUNIT_RERUN_CACHE_DIR overrides the directory.
##
function bashunit::rerun::cache_file() {
  echo "${BASHUNIT_RERUN_CACHE_DIR:-.bashunit}/last-failed"
}

function bashunit::rerun::is_enabled() {
  [ "${BASHUNIT_RERUN_FAILED:-false}" = true ]
}

##
# Appends a failed test's raw identity to the collection temp file.
# Arguments: $1 test file path, $2 raw function name.
##
function bashunit::rerun::record() {
  local test_file=$1
  local fn_name=$2
  [ -n "${RERUN_FAILED_OUTPUT_PATH:-}" ] || return 0
  printf '%s:%s\n' "$test_file" "$fn_name" >>"$RERUN_FAILED_OUTPUT_PATH" 2>/dev/null || true
}

##
# Persists the collected failures to the cache (deduped, first-seen order).
# A run with no collected failures truncates an existing cache. Write errors
# (e.g. a read-only working directory) are ignored silently.
##
function bashunit::rerun::persist() {
  local cache
  cache="$(bashunit::rerun::cache_file)"
  local collected="${RERUN_FAILED_OUTPUT_PATH:-}"

  if [ -n "$collected" ] && [ -s "$collected" ]; then
    local dir="${cache%/*}"
    if [ "$dir" != "$cache" ]; then
      mkdir -p "$dir" 2>/dev/null || return 0
    fi
    awk '!seen[$0]++' "$collected" >"$cache" 2>/dev/null || true
  elif [ -f "$cache" ]; then
    : >"$cache" 2>/dev/null || true
  fi
}

##
# Loads the cache into _BASHUNIT_RERUN_ENTRIES (empty when the cache is absent).
##
function bashunit::rerun::load() {
  local cache
  cache="$(bashunit::rerun::cache_file)"
  _BASHUNIT_RERUN_ENTRIES=""
  [ -f "$cache" ] || return 0
  _BASHUNIT_RERUN_ENTRIES="$(cat "$cache" 2>/dev/null)"
}

function bashunit::rerun::has_entries() {
  [ -n "$_BASHUNIT_RERUN_ENTRIES" ]
}

##
# Echoes the distinct test files from the loaded entries (first-seen order).
##
function bashunit::rerun::files() {
  [ -n "$_BASHUNIT_RERUN_ENTRIES" ] || return 0
  printf '%s\n' "$_BASHUNIT_RERUN_ENTRIES" | awk '
    NF {
      file = $0
      sub(/:[^:]*$/, "", file)
      if (!seen[file]++) print file
    }'
}

##
# Returns 0 when "<file>:<fn>" is among the loaded entries.
##
function bashunit::rerun::allows() {
  local file=$1
  local fn=$2
  case "
$_BASHUNIT_RERUN_ENTRIES
" in
  *"
$file:$fn
"*) return 0 ;;
  esac
  return 1
}

##
# Filters a space-separated function list down to the ones recorded for a file.
# Arguments: $1 test file path, $2 space-separated function names.
##
function bashunit::rerun::filter_functions() {
  local file=$1
  local functions=$2
  local kept=""
  local fn
  for fn in $functions; do
    if bashunit::rerun::allows "$file" "$fn"; then
      kept="$kept $fn"
    fi
  done
  echo "${kept# }"
}

# runner.sh
# shellcheck disable=SC2155

# Pre-compiled regex pattern for parsing test result assertions
if [ -z "${_BASHUNIT_RUNNER_PARSE_RESULT_REGEX+x}" ]; then
  declare -r _BASHUNIT_RUNNER_PARSE_RESULT_REGEX='ASSERTIONS_FAILED=([0-9]*)##'\
'ASSERTIONS_PASSED=([0-9]*)##ASSERTIONS_SKIPPED=([0-9]*)##'\
'ASSERTIONS_INCOMPLETE=([0-9]*)##ASSERTIONS_SNAPSHOT=([0-9]*)##TEST_EXIT_CODE=([0-9]*)'
fi

function bashunit::runner::restore_workdir() {
  cd "$BASHUNIT_WORKING_DIR" 2>/dev/null || true
}

##
# Whether the running Bash has a reliable `set -o pipefail`. Bash 3.0 shipped a
# broken pipefail (a failing pipeline can wrongly report success), which makes
# `--strict` unsound; on 3.0 we fall back to `set -eu` without pipefail.
# Returns: 0 when pipefail is reliable (Bash >= 3.1), 1 otherwise.
##
function bashunit::runner::_supports_reliable_pipefail() {
  if [ "${BASH_VERSINFO[0]:-0}" -gt 3 ]; then
    return 0
  fi
  [ "${BASH_VERSINFO[0]:-0}" -eq 3 ] && [ "${BASH_VERSINFO[1]:-0}" -ge 1 ]
}

# Caches BASHUNIT_COVERAGE into _BASHUNIT_COVERAGE_ON ("1"|"0") so hot-path checks
# avoid a function dispatch per call. Call once after arg parsing; tests that
# toggle BASHUNIT_COVERAGE mid-run must call this again to refresh.
function bashunit::runner::sync_coverage_flag() {
  if [ "${BASHUNIT_COVERAGE-}" = "true" ]; then
    _BASHUNIT_COVERAGE_ON=1
  else
    _BASHUNIT_COVERAGE_ON=0
  fi
}

function bashunit::runner::source_login_shell_profiles() {
  # shellcheck disable=SC1091
  [ -f /etc/profile ] && source /etc/profile 2>/dev/null || true
  # shellcheck disable=SC1090
  [ -f ~/.bash_profile ] && source ~/.bash_profile 2>/dev/null || true
  # shellcheck disable=SC1090
  [ -f ~/.bash_login ] && source ~/.bash_login 2>/dev/null || true
  # shellcheck disable=SC1090
  [ -f ~/.profile ] && source ~/.profile 2>/dev/null || true
}

function bashunit::runner::export_test_identity() {
  local test_file=$1
  local fn_name=$2
  bashunit::helper::generate_id "$fn_name"
  export BASHUNIT_CURRENT_TEST_ID="$_BASHUNIT_HELPER_ID_OUT"
  bashunit::runner::resolve_test_location "$test_file" "$fn_name"
  export _BASHUNIT_TEST_LOCATION
  if [ "${_BASHUNIT_COVERAGE_ON:-0}" = 1 ]; then
    export _BASHUNIT_COVERAGE_CURRENT_TEST_FILE="$test_file"
    export _BASHUNIT_COVERAGE_CURRENT_TEST_FN="$fn_name"
  fi
}

##
# Resolves "<test_file>:<line>" for a test function and writes it into the
# global _BASHUNIT_TEST_LOCATION, using `declare -F` under `extdebug` to read
# the definition line. Falls back to just the file path when the line cannot be
# determined. Bash 3.0+ compatible. Writes a global slot (no extra subshell).
# Arguments: $1 test file, $2 function name
##
function bashunit::runner::resolve_test_location() {
  local test_file=$1
  local fn_name=$2

  # Enable extdebug only inside the command-substitution subshell so it never
  # leaks into the parent shell — globally toggling extdebug interferes with
  # `set -e`/DEBUG-trap behavior under --strict.
  local def line=""
  def="$(shopt -s extdebug; declare -F "$fn_name" 2>/dev/null)" || true

  # `declare -F` (with extdebug) prints "<name> <line> <file>".
  if [ -n "$def" ]; then
    line=${def#* }
    line=${line%% *}
  fi

  if [ -n "$line" ]; then
    _BASHUNIT_TEST_LOCATION="${test_file}:${line}"
  else
    _BASHUNIT_TEST_LOCATION="$test_file"
  fi
}

# Writes the interpolated test-function name into _BASHUNIT_RUNNER_INTERP_OUT.
# Arguments: $1 fn_name, $@ test arguments
function bashunit::runner::apply_interpolated_title() {
  local fn_name=$1
  shift

  # Only "::N::"-style names interpolate; skip the capture fork for the rest.
  case "$fn_name" in
  *::*) ;;
  *)
    bashunit::state::reset_current_test_interpolated_function_name
    _BASHUNIT_RUNNER_INTERP_OUT=$fn_name
    return
    ;;
  esac

  local interpolated
  interpolated="$(bashunit::helper::interpolate_function_name "$fn_name" "$@")"
  if [ "$interpolated" != "$fn_name" ]; then
    bashunit::state::set_current_test_interpolated_function_name "$interpolated"
  else
    bashunit::state::reset_current_test_interpolated_function_name
  fi
  _BASHUNIT_RUNNER_INTERP_OUT=$interpolated
}

# Hot-path result helpers below return their value via a dedicated global slot
# (`_BASHUNIT_RUNNER_*_OUT`) instead of stdout. This avoids the per-test
# `$(...)` subshell capture that dominated the result-parsing hot path. Callers
# invoke the helper and immediately read the slot:
#
#   bashunit::runner::extract_subshell_type "$subshell_output"
#   type=$_BASHUNIT_RUNNER_TYPE_OUT
#
# A dedicated slot per helper (rather than one shared slot) means nested or
# adjacent calls cannot clobber each other and callers don't need to copy out
# before every other helper runs.
_BASHUNIT_RUNNER_FIELD_OUT=""
_BASHUNIT_RUNNER_TOTAL_OUT=""
_BASHUNIT_RUNNER_TYPE_OUT=""
_BASHUNIT_RUNNER_OUTPUT_OUT=""
_BASHUNIT_RUNNER_INTERP_OUT=""
_BASHUNIT_RUNNER_COUNTS_FAILED_OUT=0
_BASHUNIT_RUNNER_COUNTS_PASSED_OUT=0
_BASHUNIT_RUNNER_COUNTS_SKIPPED_OUT=0
_BASHUNIT_RUNNER_COUNTS_INCOMPLETE_OUT=0
_BASHUNIT_RUNNER_COUNTS_SNAPSHOT_OUT=0
_BASHUNIT_RUNNER_COUNTS_EXIT_CODE_OUT=0
_BASHUNIT_RUNNER_RUNTIME_ERROR_OUT=""
_BASHUNIT_RUNNER_SUBSHELL_OUTPUT_OUT=""
# Suffix appended to a passed-test line when it only passed after retrying.
_BASHUNIT_RETRY_NOTE=""

# Writes the value of an encoded field (##KEY=value##) into _BASHUNIT_RUNNER_FIELD_OUT.
# Arguments: $1 test_execution_result, $2 key
function bashunit::runner::extract_encoded_field() {
  local test_execution_result=$1
  local key=$2
  local marker="##${key}="
  case "$test_execution_result" in
  *"$marker"*)
    local rest="${test_execution_result#*"$marker"}"
    _BASHUNIT_RUNNER_FIELD_OUT="${rest%%##*}"
    ;;
  *) _BASHUNIT_RUNNER_FIELD_OUT="" ;;
  esac
}

# Writes the sum of all ASSERTIONS_* counters into _BASHUNIT_RUNNER_TOTAL_OUT.
# Arguments: $1 test_execution_result
function bashunit::runner::compute_total_assertions() {
  local test_execution_result=$1
  local failed passed skipped incomplete snapshot
  failed="${test_execution_result##*##ASSERTIONS_FAILED=}"
  failed="${failed%%##*}"
  passed="${test_execution_result##*##ASSERTIONS_PASSED=}"
  passed="${passed%%##*}"
  skipped="${test_execution_result##*##ASSERTIONS_SKIPPED=}"
  skipped="${skipped%%##*}"
  incomplete="${test_execution_result##*##ASSERTIONS_INCOMPLETE=}"
  incomplete="${incomplete%%##*}"
  snapshot="${test_execution_result##*##ASSERTIONS_SNAPSHOT=}"
  snapshot="${snapshot%%##*}"
  local total
  total=$((${failed:-0} + ${passed:-0} + ${skipped:-0}))
  total=$((total + ${incomplete:-0} + ${snapshot:-0}))
  _BASHUNIT_RUNNER_TOTAL_OUT=$total
}

# Writes the subshell type marker (text inside leading [...]) into _BASHUNIT_RUNNER_TYPE_OUT.
# Arguments: $1 subshell_output
function bashunit::runner::extract_subshell_type() {
  local subshell_output=$1
  local type="${subshell_output%%]*}"
  _BASHUNIT_RUNNER_TYPE_OUT="${type#[}"
}

# Writes the subshell output (minus the leading [type] marker, with embedded
# status markers replaced by newlines) into _BASHUNIT_RUNNER_OUTPUT_OUT.
# Arguments: $1 subshell_output
function bashunit::runner::format_subshell_output() {
  local subshell_output=$1
  local line="${subshell_output#*]}"
  line=${line//\[failed\]/$'\n'}
  line=${line//\[skipped\]/$'\n'}
  line=${line//\[incomplete\]/$'\n'}
  _BASHUNIT_RUNNER_OUTPUT_OUT=$line
}

##
# Appends a profiling record (duration, test name, file) to PROFILE_OUTPUT_PATH.
# Uses a tab-separated, append-only line so it aggregates correctly across the
# subshells spawned by parallel runs.
# Arguments: $1 duration (ms), $2 test name, $3 test file
##
function bashunit::runner::record_profile() {
  local duration=$1
  local test_name=$2
  local test_file=$3
  printf '%s\t%s\t%s\n' "$duration" "$test_name" "$test_file" >>"$PROFILE_OUTPUT_PATH"
}

# Writes the detected runtime-error message (empty when none) into
# _BASHUNIT_RUNNER_RUNTIME_ERROR_OUT. Return-slot form avoids a per-test fork
# on the hot path (#764).
# Arguments: $1 runtime_output
function bashunit::runner::detect_runtime_error() {
  local runtime_output=$1
  _BASHUNIT_RUNNER_RUNTIME_ERROR_OUT=""
  case "$runtime_output" in
  *"command not found"* | *"unbound variable"* | *"permission denied"* | \
    *"no such file or directory"* | *"syntax error"* | *"bad substitution"* | \
    *"division by 0"* | *"cannot allocate memory"* | *"bad file descriptor"* | \
    *"segmentation fault"* | *"illegal option"* | *"argument list too long"* | \
    *"readonly variable"* | *"missing keyword"* | *"killed"* | \
    *"cannot execute binary file"* | *"invalid arithmetic operator"* | \
    *"ambiguous redirect"* | *"integer expression expected"* | \
    *"too many arguments"* | *"value too great"* | \
    *"not a valid identifier"* | *"unexpected EOF"*)
    local runtime_error="${runtime_output#*: }"
    _BASHUNIT_RUNNER_RUNTIME_ERROR_OUT="${runtime_error//$'\n'/}"
    ;;
  esac
}

##
# Maps a process exit code to a human-readable description when it indicates the
# test was killed by a signal (128 + signal) or timed out. Returns an empty
# string for ordinary exit codes. Bash 3.0+ compatible.
# Arguments: $1 exit code
##
function bashunit::runner::classify_kill_signal() {
  local code=$1

  case "$code" in
  124) printf 'Timed out (killed by `timeout`)' ;;
  130) printf 'Interrupted (SIGINT)' ;;
  137) printf 'Killed (SIGKILL — out of memory or forced termination)' ;;
  143) printf 'Terminated (SIGTERM — e.g. a timeout)' ;;
  *)
    # Generic "killed by signal N" for other 128+N codes (signals 1..64)
    case "$code" in
    '' | *[!0-9]*) return 0 ;;
    esac
    if [ "$code" -gt 128 ] && [ "$code" -le 192 ]; then
      printf 'Killed by signal %s' "$((code - 128))"
    fi
    ;;
  esac
}

function bashunit::runner::print_verbose_test_summary() {
  local test_file=$1
  local fn_name=$2
  local duration=$3
  local test_execution_result=$4

  if bashunit::env::is_simple_output_enabled; then
    echo ""
  fi

  printf '%*s\n' "$TERMINAL_WIDTH" '' | tr ' ' '='
  printf "%s\n" "File:     $test_file"
  printf "%s\n" "Function: $fn_name"
  printf "%s\n" "Duration: $duration ms"
  local raw_text=${test_execution_result%%##ASSERTIONS_*}
  [ -n "$raw_text" ] && printf "%s" "Raw text: $raw_text"
  printf "%s\n" "##ASSERTIONS_${test_execution_result#*##ASSERTIONS_}"
  printf '%*s\n' "$TERMINAL_WIDTH" '' | tr ' ' '-'
}

# Returns 0 when this Bash supports `wait -n` (Bash 4.3+), 1 otherwise.
function bashunit::runner::_supports_wait_n() {
  local major="${BASH_VERSINFO[0]:-0}"
  local minor="${BASH_VERSINFO[1]:-0}"
  if [ "$major" -gt 4 ]; then
    return 0
  fi
  if [ "$major" -eq 4 ] && [ "$minor" -ge 3 ]; then
    return 0
  fi
  return 1
}

_BASHUNIT_RUNNER_RUNNING_JOBS_OUT=0

# Counts running background jobs into _BASHUNIT_RUNNER_RUNNING_JOBS_OUT. `jobs -pr`
# still needs one command substitution, but the line count is pure-bash, so this
# drops the extra `wc` fork per poll iteration on the parallel hot path (#761).
function bashunit::runner::_count_running_jobs() {
  local running
  running=$(jobs -pr)
  if [ -z "$running" ]; then
    _BASHUNIT_RUNNER_RUNNING_JOBS_OUT=0
    return
  fi
  local newlines="${running//[!$'\n']/}"
  _BASHUNIT_RUNNER_RUNNING_JOBS_OUT=$((${#newlines} + 1))
}

function bashunit::runner::wait_for_job_slot() {
  local max_jobs="${BASHUNIT_PARALLEL_JOBS:-0}"
  if [ "$max_jobs" -le 0 ]; then
    return 0
  fi

  if bashunit::runner::_supports_wait_n; then
    # Bash 4.3+: block until any child exits. No polling, no sleep latency.
    bashunit::runner::_count_running_jobs
    while [ "$_BASHUNIT_RUNNER_RUNNING_JOBS_OUT" -ge "$max_jobs" ]; do
      wait -n 2>/dev/null || break
      bashunit::runner::_count_running_jobs
    done
    return 0
  fi

  # Bash 3.x fallback: adaptive poll starting at 50ms, growing to 200ms to
  # reduce `jobs -r` overhead on long-running tests while staying responsive.
  local delay="0.05"
  local iterations=0
  while true; do
    bashunit::runner::_count_running_jobs
    if [ "$_BASHUNIT_RUNNER_RUNNING_JOBS_OUT" -lt "$max_jobs" ]; then
      break
    fi
    sleep "$delay"
    iterations=$((iterations + 1))
    if [ "$iterations" -eq 4 ]; then
      delay="0.1"
    elif [ "$iterations" -eq 20 ]; then
      delay="0.2"
    fi
  done
}

function bashunit::runner::load_test_files() {
  local filter=$1
  local tag_filter="${2:-}"
  local exclude_tag_filter="${3:-}"
  shift 3
  local IFS=$' \t\n'
  local -a files
  files=("$@")
  local -a scripts_ids=()
  local scripts_ids_count=0

  # Randomize file execution order (deterministic for the resolved seed).
  if bashunit::env::is_random_order_enabled; then
    local -a _shuffled_files=()
    local _sf
    while IFS= read -r _sf; do
      [ -n "$_sf" ] && _shuffled_files[${#_shuffled_files[@]}]=$_sf
    done < <(printf '%s\n' "${files[@]+"${files[@]}"}" | bashunit::math::shuffle "$(bashunit::env::seed)")
    files=("${_shuffled_files[@]+"${_shuffled_files[@]}"}")
  fi

  bashunit::runner::sync_coverage_flag

  # Initialize coverage tracking if enabled
  if [ "$_BASHUNIT_COVERAGE_ON" = 1 ]; then
    # Auto-discover coverage paths if not explicitly set
    if [ -z "$BASHUNIT_COVERAGE_PATHS" ]; then
      BASHUNIT_COVERAGE_PATHS=$(bashunit::coverage::auto_discover_paths "${files[@]}")
      # Fallback: if auto-discovery yields no paths, track the src/ folder
      if [ -z "$BASHUNIT_COVERAGE_PATHS" ]; then
        BASHUNIT_COVERAGE_PATHS="src/"
      fi
    fi
    bashunit::coverage::init
  fi

  local test_file
  for test_file in "${files[@]+"${files[@]}"}"; do
    if [ ! -f "$test_file" ]; then
      continue
    fi
    unset BASHUNIT_CURRENT_TEST_ID
    bashunit::helper::generate_id "${test_file}"
    export BASHUNIT_CURRENT_SCRIPT_ID="$_BASHUNIT_HELPER_ID_OUT"
    scripts_ids[scripts_ids_count]="${BASHUNIT_CURRENT_SCRIPT_ID}"
    scripts_ids_count=$((scripts_ids_count + 1))
    bashunit::internal_log "Loading file" "$test_file"
    # Files are sourced sequentially in this loop (parallel workers fork after),
    # so a fixed path in the run dir is safe: `2>` truncates it per file and the
    # run-dir cleanup removes it, saving a mktemp and an rm fork per file.
    local source_err_file source_err source_status
    source_err_file="$_BASHUNIT_RUN_OUTPUT_DIR/source_err"
    # shellcheck source=/dev/null
    source "$test_file" 2>"$source_err_file"
    source_status=$?
    # A test file may enable `set -euo pipefail` at its top level; sourcing
    # runs that in THIS shell, so a later non-zero status in the loop (e.g. a
    # failing set_up_before_script) would kill the whole run mid-suite with no
    # summary. Strictness is applied per-test in execute_test_body — reset the
    # runner loop to its set +euo invariant (see main.sh exec_tests) (#836).
    set +euo pipefail
    source_err=""
    if [ -s "$source_err_file" ]; then
      source_err="$(cat "$source_err_file")"
    fi
    # A non-zero source status, or a syntax-error line on stderr, means the file
    # failed to load. Match the captured stderr with `case` (no grep fork).
    local source_failed=false
    if [ "$source_status" -ne 0 ]; then
      source_failed=true
    else
      case "$source_err" in
      *"syntax error"* | *"unexpected EOF"*) source_failed=true ;;
      esac
    fi
    if [ "$source_failed" = true ]; then
      local message="$source_err"
      [ -z "$message" ] && message="Failed to source '$test_file' (exit $source_status)"
      bashunit::runner::record_file_hook_failure \
        "source" "$test_file" "$message" 1 true
      bashunit::runner::clean_set_up_and_tear_down_after_script
      bashunit::runner::restore_workdir
      continue
    fi
    # Update function cache after sourcing new test file (compgen is a builtin)
    _BASHUNIT_CACHED_ALL_FUNCTIONS=$(compgen -A function)
    # Check if any tests match the filter before rendering header or running hooks
    local filtered_functions
    filtered_functions=$(bashunit::helper::get_functions_to_run "test" "$filter" "$_BASHUNIT_CACHED_ALL_FUNCTIONS")
    local functions_for_script
    functions_for_script=$(bashunit::runner::functions_for_script "$test_file" "$filtered_functions")
    # Full pre-tag/rerun list: these are unset once the file has been
    # processed, whatever subset actually runs (#829).
    local _script_fns_to_clean="$functions_for_script"
    # Apply tag filtering to the early check as well
    if [ -n "$tag_filter" ] || [ -n "$exclude_tag_filter" ]; then
      bashunit::helper::build_tags_map "$test_file"
      local _early_filtered=""
      local _early_fn
      for _early_fn in $functions_for_script; do
        bashunit::helper::tags_for_function "$_early_fn"
        if bashunit::helper::function_matches_tags "$_BASHUNIT_TAGS_OUT" "$tag_filter" "$exclude_tag_filter"; then
          _early_filtered="$_early_filtered $_early_fn"
        fi
      done
      functions_for_script="${_early_filtered# }"
    fi
    # Replay filtering: keep only the functions recorded as failing last run.
    if bashunit::rerun::is_enabled && bashunit::rerun::has_entries; then
      functions_for_script=$(bashunit::rerun::filter_functions "$test_file" "$functions_for_script")
    fi
    if [ -z "$functions_for_script" ]; then
      bashunit::runner::clean_script_test_functions "$_script_fns_to_clean"
      bashunit::runner::clean_set_up_and_tear_down_after_script
      bashunit::runner::restore_workdir
      continue
    fi
    # Render header BEFORE set_up_before_script so user sees activity immediately
    bashunit::runner::render_running_file_header "$test_file"
    # Call hook directly (not with `if !`) to preserve errexit behavior inside the hook
    bashunit::runner::run_set_up_before_script "$test_file"
    local setup_before_script_status=$?
    if [ $setup_before_script_status -ne 0 ]; then
      # Count the test functions that couldn't run due to set_up_before_script
      # failure and add them as failed (minus 1 since the hook failure already
      # counts as 1). Use this file's own function list — scanning the cached
      # ALL-functions set would also count fns left over from earlier files
      # and inflate the totals (#836).
      if [ -n "$functions_for_script" ]; then
        # Bash 3.0 compatible: separate declaration and assignment for arrays
        local functions_to_run
        # shellcheck disable=SC2206
        functions_to_run=($functions_for_script)
        local additional_failures=$((${#functions_to_run[@]} - 1))
        local i
        for ((i = 0; i < additional_failures; i++)); do
          bashunit::state::add_tests_failed
        done
      fi
      # Same cleanup as the success path: without it the file's test functions
      # leak into the next iteration's counts and the main shell (#829, #836).
      bashunit::runner::clean_script_test_functions "$_script_fns_to_clean"
      bashunit::runner::clean_set_up_and_tear_down_after_script
      if ! bashunit::parallel::is_enabled; then
        bashunit::cleanup_script_temp_files
      fi
      bashunit::runner::restore_workdir
      continue
    fi
    local _cached_fns="$functions_for_script"
    if bashunit::parallel::is_enabled; then
      bashunit::runner::wait_for_job_slot
      bashunit::runner::call_test_functions \
        "$test_file" "$filter" "$tag_filter" \
        "$exclude_tag_filter" "$_cached_fns" 2>/dev/null &
    else
      bashunit::runner::call_test_functions \
        "$test_file" "$filter" "$tag_filter" \
        "$exclude_tag_filter" "$_cached_fns"
    fi
    bashunit::runner::run_tear_down_after_script "$test_file"
    bashunit::runner::clean_script_test_functions "$_script_fns_to_clean"
    bashunit::runner::clean_set_up_and_tear_down_after_script
    if ! bashunit::parallel::is_enabled; then
      bashunit::cleanup_script_temp_files
    fi
    bashunit::internal_log "Finished file" "$test_file"
    bashunit::runner::restore_workdir
  done

  if bashunit::parallel::is_enabled; then
    wait
    bashunit::runner::spinner &
    local spinner_pid=$!
    bashunit::parallel::aggregate_test_results "$TEMP_DIR_PARALLEL_TEST_SUITE"
    # Kill the spinner once the aggregation finishes
    disown "$spinner_pid" 2>/dev/null || true
    kill "$spinner_pid" 2>/dev/null || true
    printf "\r  \r" # Clear the spinner output
    local script_id
    for script_id in "${scripts_ids[@]+"${scripts_ids[@]}"}"; do
      export BASHUNIT_CURRENT_SCRIPT_ID="${script_id}"
      bashunit::cleanup_script_temp_files
    done
  fi
}

function bashunit::runner::load_bench_files() {
  local filter=$1
  shift
  local IFS=$' \t\n'
  local -a files
  files=("$@")

  local bench_file
  for bench_file in "${files[@]+"${files[@]}"}"; do
    [ -f "$bench_file" ] || continue
    unset BASHUNIT_CURRENT_TEST_ID
    bashunit::helper::generate_id "${bench_file}"
    export BASHUNIT_CURRENT_SCRIPT_ID="$_BASHUNIT_HELPER_ID_OUT"
    # shellcheck source=/dev/null
    source "$bench_file"
    # Reset the loop's shell-mode invariant; a bench file may set -euo at top
    # level and sourcing runs that in this shell (see the test loop) (#836).
    set +euo pipefail
    # Update function cache after sourcing new bench file (compgen is a builtin)
    _BASHUNIT_CACHED_ALL_FUNCTIONS=$(compgen -A function)
    # Call hook directly (not with `if !`) to preserve errexit behavior inside the hook
    bashunit::runner::run_set_up_before_script "$bench_file"
    local setup_before_script_status=$?
    if [ $setup_before_script_status -ne 0 ]; then
      # Count the bench functions that couldn't run due to set_up_before_script failure
      # and add them as failed (minus 1 since the hook failure already counts as 1)
      local filtered_functions
      filtered_functions=$(bashunit::helper::get_functions_to_run "bench" "$filter" "$_BASHUNIT_CACHED_ALL_FUNCTIONS")
      if [ -n "$filtered_functions" ]; then
        # Bash 3.0 compatible: separate declaration and assignment for arrays
        local functions_to_run
        # shellcheck disable=SC2206
        functions_to_run=($filtered_functions)
        local additional_failures=$((${#functions_to_run[@]} - 1))
        local i
        for ((i = 0; i < additional_failures; i++)); do
          bashunit::state::add_tests_failed
        done
      fi
      bashunit::runner::clean_set_up_and_tear_down_after_script
      bashunit::cleanup_script_temp_files
      bashunit::runner::restore_workdir
      continue
    fi
    bashunit::runner::call_bench_functions "$bench_file" "$filter"
    bashunit::runner::run_tear_down_after_script "$bench_file"
    bashunit::runner::clean_set_up_and_tear_down_after_script
    bashunit::cleanup_script_temp_files
    bashunit::runner::restore_workdir
  done
}

function bashunit::runner::spinner() {
  # Only show spinner when output is to a terminal
  if [ ! -t 1 ]; then
    # Not a terminal, just wait silently
    while true; do sleep 1; done
    return
  fi

  # Don't show spinner in no-progress mode
  if bashunit::env::is_no_progress_enabled; then
    while true; do sleep 1; done
    return
  fi

  if bashunit::env::is_simple_output_enabled; then
    printf "\n"
  fi

  local delay=0.1
  local spin_chars="|/-\\"
  while true; do
    local i
    for ((i = 0; i < ${#spin_chars}; i++)); do
      printf "\r%s" "${spin_chars:$i:1}"
      sleep "$delay"
    done
  done
}

function bashunit::runner::functions_for_script() {
  local script="$1"
  local all_fn_names="$2"

  # Resolve "<name> <line> <file>" for the given names, enabling extdebug only
  # inside the capture subshell so the caller's setting is untouched.
  local declarations
  # shellcheck disable=SC2086
  declarations=$(
    shopt -s extdebug
    declare -F $all_fn_names 2>/dev/null
  )

  # Keep the functions defined in this script, insertion-sorted by definition
  # line. Pure bash: the old `awk | sort | awk` pipeline cost three forks and
  # ran twice per file, while a file's function list is small (tens of names).
  local -a fns=()
  local -a fn_lines=()
  local count=0
  local name line file i
  while read -r name line file; do
    [ "$file" = "$script" ] || continue
    i=$count
    while [ "$i" -gt 0 ] && [ "${fn_lines[i - 1]}" -gt "$line" ]; do
      fns[i]=${fns[i - 1]}
      fn_lines[i]=${fn_lines[i - 1]}
      i=$((i - 1))
    done
    fns[i]=$name
    fn_lines[i]=$line
    count=$((count + 1))
  done <<EOF
$declarations
EOF

  i=0
  while [ "$i" -lt "$count" ]; do
    echo "${fns[i]}"
    i=$((i + 1))
  done
}

function bashunit::runner::parse_data_provider_args() {
  local input="$1"
  local current_arg=""
  local in_quotes=false
  local had_quotes=false # Track if arg was quoted (to preserve empty quoted strings)
  local quote_char=""
  local escaped=false
  local IFS=$' \t\n'
  local i=0
  local arg=""
  local encoded_arg
  local -a args=()
  local args_count=0

  # Check for unescaped shell metacharacters that would break eval or cause
  # globbing. Combines the leading-metachar case and the embedded-metachar
  # case into a single regex to avoid a second grep subprocess per call.
  local has_metachar=false
  if [ "$(echo "$input" | "$GREP" -cE '(^|[^\])[|&;*]' || true)" -gt 0 ]; then
    has_metachar=true
  fi

  # Try eval first (needed for $'...' from printf '%q'), unless metacharacters present
  if [ "$has_metachar" = false ] && eval "args=($input)" 2>/dev/null; then
    # Check if args has elements after eval
    args_count=0
    local _tmp arg
    for _tmp in ${args+"${args[@]}"}; do args_count=$((args_count + 1)); done
    if [ "$args_count" -gt 0 ]; then
      # Successfully parsed - remove sentinel if present
      local last_idx=$((args_count - 1))
      if [ -z "${args[$last_idx]}" ]; then
        unset 'args[$last_idx]'
      fi
      # Print args and return early
      for arg in "${args[@]+"${args[@]}"}"; do
        encoded_arg="$(bashunit::helper::encode_base64 "${arg}")"
        printf '%s\n' "$encoded_arg"
      done
      return
    fi
  fi

  # Fallback: parse args from the input string into an array, respecting quotes and escapes
  local i
  for ((i = 0; i < ${#input}; i++)); do
    local char="${input:$i:1}"
    if [ "$escaped" = true ]; then
      case "$char" in
      t) current_arg="$current_arg"$'\t' ;;
      n) current_arg="$current_arg"$'\n' ;;
      *) current_arg="$current_arg$char" ;;
      esac
      escaped=false
    elif [ "$char" = "\\" ]; then
      escaped=true
    elif [ "$in_quotes" = false ]; then
      case "$char" in
      "$")
        # Handle $'...' syntax
        if [ "${input:$i:2}" = "$'" ]; then
          in_quotes=true
          had_quotes=true
          quote_char="'"
          # Skip the $
          i=$((i + 1))
        else
          current_arg="$current_arg$char"
        fi
        ;;
      "'" | '"')
        in_quotes=true
        had_quotes=true
        quote_char="$char"
        ;;
      " " | $'\t')
        # Add if non-empty OR if was quoted (to preserve empty quoted strings like '')
        if [ -n "$current_arg" ] || [ "$had_quotes" = true ]; then
          args[args_count]="$current_arg"
          args_count=$((args_count + 1))
        fi
        current_arg=""
        had_quotes=false
        ;;
      *)
        current_arg="$current_arg$char"
        ;;
      esac
    elif [ "$char" = "$quote_char" ]; then
      in_quotes=false
      quote_char=""
    else
      current_arg="$current_arg$char"
    fi
  done
  args[args_count]="$current_arg"
  args_count=$((args_count + 1))
  # Remove all trailing empty strings
  while [ "$args_count" -gt 0 ]; do
    local last_idx=$((args_count - 1))
    if [ -z "${args[$last_idx]}" ]; then
      unset 'args[$last_idx]'
      args_count=$((args_count - 1))
    else
      break
    fi
  done
  # Print one arg per line to stdout, base64-encoded to preserve newlines in the data
  local arg
  for arg in ${args+"${args[@]}"}; do
    encoded_arg="$(bashunit::helper::encode_base64 "${arg}")"
    printf '%s\n' "$encoded_arg"
  done
}

function bashunit::runner::call_test_functions() {
  local script="$1"
  local filter="$2"
  local tag_filter="${3:-}"
  local exclude_tag_filter="${4:-}"
  local cached_functions="${5:-}"
  local IFS=$' \t\n'
  local -a functions_to_run=()
  local functions_to_run_count=0

  if [ -n "$cached_functions" ]; then
    # Use pre-computed function list from load_test_files (already tag-filtered)
    local _fn
    for _fn in $cached_functions; do
      [ -z "$_fn" ] && continue
      functions_to_run[functions_to_run_count]="$_fn"
      functions_to_run_count=$((functions_to_run_count + 1))
    done
  else
    # Fallback: compute function list (for direct calls without cache)
    local prefix="test"
    local filtered_functions
    filtered_functions=$(bashunit::helper::get_functions_to_run \
      "$prefix" "$filter" "$_BASHUNIT_CACHED_ALL_FUNCTIONS")
    local _fn
    while IFS= read -r _fn; do
      [ -z "$_fn" ] && continue
      functions_to_run[functions_to_run_count]="$_fn"
      functions_to_run_count=$((functions_to_run_count + 1))
    done < <(bashunit::runner::functions_for_script "$script" "$filtered_functions")

    # Apply tag filtering if --tag or --exclude-tag was specified
    if [ -n "$tag_filter" ] || [ -n "$exclude_tag_filter" ]; then
      bashunit::helper::build_tags_map "$script"
      local -a tag_filtered=()
      local tag_filtered_count=0
      local _tf_fn
      for _tf_fn in "${functions_to_run[@]+"${functions_to_run[@]}"}"; do
        bashunit::helper::tags_for_function "$_tf_fn"
        if bashunit::helper::function_matches_tags "$_BASHUNIT_TAGS_OUT" "$tag_filter" "$exclude_tag_filter"; then
          tag_filtered[tag_filtered_count]="$_tf_fn"
          tag_filtered_count=$((tag_filtered_count + 1))
        fi
      done
      functions_to_run=("${tag_filtered[@]+"${tag_filtered[@]}"}")
      functions_to_run_count=$tag_filtered_count
    fi
  fi

  # Randomize function order within this file. The seed is mixed with a stable
  # per-file value (cksum of the path) so different files get different orders
  # while staying reproducible for the resolved seed.
  if bashunit::env::is_random_order_enabled && [ "$functions_to_run_count" -gt 1 ]; then
    local _base _crc _fn_seed
    _base=$(bashunit::env::seed)
    _crc=$(printf '%s' "$script" | cksum | cut -d' ' -f1)
    _fn_seed=$(((_base + _crc) & 2147483647))
    local -a _shuffled_fns=()
    local _sfn
    while IFS= read -r _sfn; do
      [ -n "$_sfn" ] && _shuffled_fns[${#_shuffled_fns[@]}]=$_sfn
    done < <(printf '%s\n' "${functions_to_run[@]+"${functions_to_run[@]}"}" | bashunit::math::shuffle "$_fn_seed")
    functions_to_run=("${_shuffled_fns[@]+"${_shuffled_fns[@]}"}")
    functions_to_run_count=${#functions_to_run[@]}
  fi

  if [ "$functions_to_run_count" -le 0 ]; then
    return
  fi

  bashunit::helper::check_duplicate_functions "$script" || true

  local -a provider_data=()
  local provider_data_count=0
  local -a parsed_data=()
  local parsed_data_count=0

  # Scan the file once; per-test provider lookups below are pure-bash (#763).
  # The same pass also detects the no-parallel-tests opt-out (#774).
  bashunit::helper::build_provider_map "$script"

  local allow_test_parallel=true
  if [ "$_BASHUNIT_PROVIDER_MAP_NO_PARALLEL" = true ]; then
    allow_test_parallel=false
  fi

  # Pre-create the file's result dir before spawning test workers: they all
  # publish into it, and checking `[ -d ]` inside a worker races its siblings
  # (every worker would still pay the mkdir fork).
  if bashunit::parallel::is_enabled && [ "$allow_test_parallel" = true ]; then
    local _suite_base="${script##*/}"
    mkdir -p "${TEMP_DIR_PARALLEL_TEST_SUITE}/${_suite_base%.sh}" 2>/dev/null || true
  fi

  for fn_name in "${functions_to_run[@]+"${functions_to_run[@]}"}"; do
    if bashunit::parallel::is_enabled && bashunit::parallel::must_stop_on_failure; then
      break
    fi

    # No data provider found: run once without forking to capture provider output.
    bashunit::helper::provider_for_function "$fn_name"
    if [ -z "$_BASHUNIT_PROVIDER_FN_OUT" ]; then
      if bashunit::parallel::is_enabled && [ "$allow_test_parallel" = true ]; then
        bashunit::runner::wait_for_job_slot
        bashunit::runner::run_test "$script" "$fn_name" &
      else
        bashunit::runner::run_test "$script" "$fn_name"
      fi
      unset -v fn_name
      continue
    fi

    provider_data=()
    provider_data_count=0
    local line
    while IFS=" " read -r line; do
      [ -z "$line" ] && continue
      provider_data[provider_data_count]="$line"
      provider_data_count=$((provider_data_count + 1))
    done <<<"$(bashunit::helper::execute_function_if_exists "$_BASHUNIT_PROVIDER_FN_OUT")"

    # Execute the test function for each line of data
    local data
    for data in "${provider_data[@]+"${provider_data[@]}"}"; do
      parsed_data=()
      parsed_data_count=0
      local line
      while IFS= read -r line; do
        [ -z "$line" ] && continue
        parsed_data[parsed_data_count]="$(bashunit::helper::decode_base64 "${line}")"
        parsed_data_count=$((parsed_data_count + 1))
      done <<<"$(bashunit::runner::parse_data_provider_args "$data")"
      if bashunit::parallel::is_enabled && [ "$allow_test_parallel" = true ]; then
        bashunit::runner::wait_for_job_slot
        bashunit::runner::run_test "$script" "$fn_name" ${parsed_data+"${parsed_data[@]}"} &
      else
        bashunit::runner::run_test "$script" "$fn_name" ${parsed_data+"${parsed_data[@]}"}
      fi
    done
    unset -v fn_name
  done

  # Wait for all parallel tests within this file to complete
  if bashunit::parallel::is_enabled && [ "$allow_test_parallel" = true ]; then
    wait
  fi
}

function bashunit::runner::call_bench_functions() {
  local script="$1"
  local filter="$2"
  local IFS=$' \t\n'
  local prefix="bench"

  # Use cached function names for better performance
  local filtered_functions
  filtered_functions=$(bashunit::helper::get_functions_to_run \
    "$prefix" "$filter" "$_BASHUNIT_CACHED_ALL_FUNCTIONS")
  local -a functions_to_run=()
  local functions_to_run_count=0
  local _fn
  while IFS= read -r _fn; do
    [ -z "$_fn" ] && continue
    functions_to_run[functions_to_run_count]="$_fn"
    functions_to_run_count=$((functions_to_run_count + 1))
  done < <(bashunit::runner::functions_for_script "$script" "$filtered_functions")

  if [ "$functions_to_run_count" -le 0 ]; then
    return
  fi

  if bashunit::env::is_bench_mode_enabled; then
    bashunit::runner::render_running_file_header "$script"
  fi

  local fn_name
  for fn_name in "${functions_to_run[@]+"${functions_to_run[@]}"}"; do
    read -r revs its max_ms <<<"$(bashunit::benchmark::parse_annotations "$fn_name" "$script")"
    bashunit::benchmark::run_function "$fn_name" "$revs" "$its" "$max_ms"
    unset -v fn_name
  done

  if ! bashunit::env::is_simple_output_enabled; then
    echo ""
  fi
}

function bashunit::runner::render_running_file_header() {
  local script="$1"
  local force="${2:-false}"

  bashunit::internal_log "Running file" "$script"

  if [ "$force" != true ] && bashunit::parallel::is_enabled; then
    return
  fi

  # Suppress file headers in failures-only mode
  if bashunit::env::is_failures_only_enabled; then
    return
  fi

  # Suppress file headers in no-progress mode
  if bashunit::env::is_no_progress_enabled; then
    return
  fi

  if bashunit::env::is_tap_output_enabled; then
    printf "# %s\n" "$script"
  elif ! bashunit::env::is_simple_output_enabled; then
    if bashunit::env::is_verbose_enabled; then
      printf "\n${_BASHUNIT_COLOR_BOLD}%s${_BASHUNIT_COLOR_DEFAULT}\n" "Running $script"
    else
      printf "${_BASHUNIT_COLOR_BOLD}%s${_BASHUNIT_COLOR_DEFAULT}\n" "Running $script"
    fi
  elif bashunit::env::is_verbose_enabled; then
    printf "\n\n${_BASHUNIT_COLOR_BOLD}%s${_BASHUNIT_COLOR_DEFAULT}" "Running $script"
  fi
}

# Result slots for the timeout-aware execution path (see run_with_timeout).
_BASHUNIT_RUNNER_EXEC_OUT=""
_BASHUNIT_RUNNER_TIMED_OUT="false"

##
# Runs a single test inside the capture subshell: sets up the EXIT trap that
# encodes assertion counts/exit code, runs set_up, applies the shell mode and
# finally invokes the test function. Meant to be called from a subshell (either
# the `$(...)` capture or a backgrounded job), so its `set`/`trap`/`exit` calls
# stay isolated. Emits the test stdout (with stderr merged) followed by the
# encoded context from cleanup_on_exit.
# Arguments: $1 test file, $2 function name, $@ test args
##
function bashunit::runner::execute_test_body() {
  local test_file=$1
  shift
  local fn_name=$1
  shift

  # Save subshell stdout to FD 5 so the EXIT trap can restore it.
  # When set -e kills the subshell during a redirected block in
  # execute_test_hook, the redirect leaks into the EXIT trap,
  # causing export_subshell_context output to be lost.
  exec 5>&1
  # shellcheck disable=SC2064
  trap "exit_code=\$?; bashunit::runner::cleanup_on_exit \"$test_file\" \"\$exit_code\"" EXIT
  bashunit::state::initialize_assertions_count

  if bashunit::env::is_login_shell_enabled; then
    bashunit::runner::source_login_shell_profiles
  fi

  # Enable coverage tracking early to include set_up/tear_down hooks
  if [ "${_BASHUNIT_COVERAGE_ON:-0}" = 1 ]; then
    bashunit::coverage::enable_trap
  fi

  # Run set_up and capture exit code without || to preserve errexit behavior
  # shellcheck disable=SC2030
  _BASHUNIT_SETUP_COMPLETED=false
  local setup_exit_code=0
  bashunit::runner::run_set_up "$test_file"
  setup_exit_code=$?
  _BASHUNIT_SETUP_COMPLETED=true
  if [ $setup_exit_code -ne 0 ]; then
    exit $setup_exit_code
  fi

  # Apply shell mode setting for test execution
  if bashunit::env::is_strict_mode_enabled; then
    set -eu
    # Bash 3.0 ships a broken pipefail; only enable it where it is reliable.
    if bashunit::runner::_supports_reliable_pipefail; then
      set -o pipefail
    else
      set +o pipefail
    fi
  else
    set +euo pipefail
  fi

  # 2>&1: Redirects the std-error (FD 2) to the std-output (FD 1).
  # points to the original std-output.
  "$fn_name" "$@" 2>&1
}

##
# Prints an encoded subshell result for a test that timed out: empty assertion
# counters and exit code 124 (the conventional "timed out" code, already mapped
# by classify_kill_signal). The empty TEST_HOOK_MESSAGE/TITLE/OUTPUT fields would
# base64-encode to an empty string anyway, so the line is emitted directly rather
# than mutating the shared _BASHUNIT_* globals (it mirrors the layout produced by
# bashunit::state::export_subshell_context). Bash 3.0+ compatible.
##
function bashunit::runner::build_timeout_result() {
  printf '%s' "##ASSERTIONS_FAILED=0##ASSERTIONS_PASSED=0##ASSERTIONS_SKIPPED=0\
##ASSERTIONS_INCOMPLETE=0##ASSERTIONS_SNAPSHOT=0##TEST_EXIT_CODE=124\
##TEST_HOOK_FAILURE=##TEST_HOOK_MESSAGE=##TEST_TITLE=##TEST_OUTPUT=##"
}

##
# Runs the test body with a watchdog that kills it after BASHUNIT_TEST_TIMEOUT
# seconds. The body runs as a backgrounded job in its own process group (set -m)
# so the watchdog can SIGTERM/SIGKILL the whole tree — a hanging test usually
# blocks in a child process, which signalling the subshell alone cannot reach.
# Writes the captured result to _BASHUNIT_RUNNER_EXEC_OUT and "true"/"false" to
# _BASHUNIT_RUNNER_TIMED_OUT. Bash 3.0+ compatible (validated on Bash 3.2).
# Arguments: $1 test file, $2 function name, $@ test args
##
function bashunit::runner::run_with_timeout() {
  local test_file=$1
  shift
  local fn_name=$1
  shift
  local secs
  secs=$(bashunit::env::test_timeout_secs)

  # NOTE: these must NOT use bashunit::temp_file — that prefixes the current
  # test id, and cleanup_on_exit (run inside the test subshell) would unlink
  # them via cleanup_testcase_temp_files before we read them back here.
  local tmp_dir="${BASHUNIT_TEMP_DIR:-${TMPDIR:-/tmp}}"
  local out_file marker_file
  out_file="$("$MKTEMP" "$tmp_dir/bashunit_timeout_out.XXXXXXX")"
  marker_file="$("$MKTEMP" "$tmp_dir/bashunit_timeout_marker.XXXXXXX")"
  rm -f "$marker_file"

  # Both jobs run in their own process group (set -m) so each can be killed as a
  # whole tree. The body MUST run in an explicit ( ) subshell: a backgrounded { }
  # group does not run its EXIT trap on normal completion, which would drop the
  # encoded assertion context. The watchdog's fds are detached from the caller so
  # a lingering `sleep` can never hold a captured stdout pipe open.
  set -m
  (bashunit::runner::execute_test_body "$test_file" "$fn_name" "$@") >"$out_file" 2>&1 &
  local test_pid=$!
  (
    sleep "$secs"
    # Only a still-running test can have timed out. Without this guard a watchdog
    # that outlived a missed teardown (see below) would mark an already-finished
    # fast test as timed out.
    kill -0 "$test_pid" 2>/dev/null || exit 0
    : >"$marker_file"
    kill -TERM -"$test_pid" 2>/dev/null
    sleep 0.3
    kill -KILL -"$test_pid" 2>/dev/null
  ) </dev/null >/dev/null 2>&1 &
  local watchdog_pid=$!
  set +m

  wait "$test_pid" 2>/dev/null
  # Stop the watchdog by its pid AND its group. `set -m` does not reliably make a
  # backgrounded subshell a group leader in a non-interactive shell, so the
  # group-only kill intermittently misses, letting the watchdog sleep its full
  # timeout and fire against a test that already passed. The direct-pid signal is
  # always deliverable; the group signal also reaps the `sleep` child.
  kill -TERM "$watchdog_pid" 2>/dev/null
  kill -TERM -"$watchdog_pid" 2>/dev/null
  wait "$watchdog_pid" 2>/dev/null

  if [ -f "$marker_file" ]; then
    _BASHUNIT_RUNNER_TIMED_OUT="true"
    _BASHUNIT_RUNNER_EXEC_OUT="$(bashunit::runner::build_timeout_result)"
  else
    _BASHUNIT_RUNNER_TIMED_OUT="false"
    _BASHUNIT_RUNNER_EXEC_OUT="$(cat "$out_file" 2>/dev/null)"
  fi

  rm -f "$out_file" "$marker_file"
}

# Per-test duration is consumed by --profile, --verbose, report files, and the
# execution-time display. When none are active we can skip the clock reads,
# which matters when the clock forks an interpreter (#765).
function bashunit::runner::needs_test_duration() {
  bashunit::env::is_profile_enabled && return 0
  bashunit::env::is_verbose_enabled && return 0
  bashunit::reports::is_enabled && return 0
  bashunit::env::is_show_execution_time_enabled && return 0
  return 1
}

function bashunit::runner::run_test() {
  local start_time=0

  local test_file="$1"
  shift
  local fn_name="$1"
  shift

  bashunit::internal_log "Running test" "$fn_name" "$*"
  bashunit::runner::export_test_identity "$test_file" "$fn_name"

  bashunit::state::reset_test_title
  bashunit::runner::apply_interpolated_title "$fn_name" "$@"
  local interpolated_fn_name=$_BASHUNIT_RUNNER_INTERP_OUT
  local current_assertions_failed="$_BASHUNIT_ASSERTIONS_FAILED"
  local current_assertions_snapshot="$_BASHUNIT_ASSERTIONS_SNAPSHOT"
  local current_assertions_incomplete="$_BASHUNIT_ASSERTIONS_INCOMPLETE"
  local current_assertions_skipped="$_BASHUNIT_ASSERTIONS_SKIPPED"

  # (FD = File Descriptor)
  # Duplicate the current std-output (FD 1) and assigns it to FD 3.
  # This means that FD 3 now points to wherever the std-output was pointing.
  exec 3>&1

  local test_execution_result
  local timed_out="false"
  bashunit::env::resolve_retry_count
  local retry_max=$_BASHUNIT_RETRY_VALIDATED
  local retries_used=0
  local measure_duration=false
  bashunit::runner::needs_test_duration && measure_duration=true
  # Retry wraps ONLY execution: a failed attempt is judged from its encoded
  # result without committing, so the parse/report/counter path below still runs
  # exactly once (on the final attempt) and nothing is double-counted. Each fork
  # in --parallel retries itself before writing its single .result file.
  while :; do
    if [ "$measure_duration" = true ]; then
      bashunit::clock::now_to_slot
      start_time=$_BASHUNIT_CLOCK_NOW_OUT
    fi
    if bashunit::env::is_test_timeout_enabled; then
      bashunit::runner::run_with_timeout "$test_file" "$fn_name" "$@"
      test_execution_result="$_BASHUNIT_RUNNER_EXEC_OUT"
      timed_out="$_BASHUNIT_RUNNER_TIMED_OUT"
    else
      test_execution_result=$(bashunit::runner::execute_test_body "$test_file" "$fn_name" "$@")
    fi

    local attempt_runtime_output="${test_execution_result%%##ASSERTIONS_*}"
    bashunit::runner::detect_runtime_error "$attempt_runtime_output"
    local attempt_runtime_error=$_BASHUNIT_RUNNER_RUNTIME_ERROR_OUT
    bashunit::runner::extract_result_counts "$test_execution_result"
    # Mirror the commit-phase failure test exactly (runtime error, non-zero exit,
    # or a failed assertion); snapshot/incomplete/skipped/risky are not failures.
    if [ -z "$attempt_runtime_error" ] &&
      [ "$_BASHUNIT_RUNNER_COUNTS_EXIT_CODE_OUT" -eq 0 ] &&
      [ "$_BASHUNIT_RUNNER_COUNTS_FAILED_OUT" -eq 0 ]; then
      break
    fi
    [ "$retries_used" -ge "$retry_max" ] && break
    retries_used=$((retries_used + 1))
  done

  # Closes FD 3, which was used temporarily to hold the original stdout.
  exec 3>&-

  local duration=0
  if [ "$measure_duration" = true ]; then
    bashunit::clock::now_to_slot
    local end_time=$_BASHUNIT_CLOCK_NOW_OUT
    duration=$(((end_time - start_time) / 1000000))
  fi

  if bashunit::env::is_profile_enabled; then
    bashunit::runner::record_profile "$duration" "$interpolated_fn_name" "$test_file"
  fi

  if bashunit::env::is_verbose_enabled; then
    bashunit::runner::print_verbose_test_summary \
      "$test_file" "$fn_name" "$duration" "$test_execution_result"
  fi

  bashunit::runner::decode_subshell_output "$test_execution_result"
  local subshell_output=$_BASHUNIT_RUNNER_SUBSHELL_OUTPUT_OUT

  if [ -n "$subshell_output" ]; then
    bashunit::runner::extract_subshell_type "$subshell_output"
    local type=$_BASHUNIT_RUNNER_TYPE_OUT
    bashunit::runner::format_subshell_output "$subshell_output"
    subshell_output=$_BASHUNIT_RUNNER_OUTPUT_OUT
    if ! bashunit::env::is_failures_only_enabled; then
      bashunit::state::print_line "$type" "$subshell_output"
    fi
  fi

  # Reuse the final attempt's values (the loop always runs at least once and
  # its locals persist in this function scope), instead of recomputing and
  # forking detect_runtime_error a second time (#764).
  local runtime_output=$attempt_runtime_output
  local runtime_error=$attempt_runtime_error

  # parse_result accumulates _BASHUNIT_TEST_EXIT_CODE; reset it so each test's
  # exit code is read in isolation (a non-zero/timed-out test must not poison
  # the next one).
  _BASHUNIT_TEST_EXIT_CODE=0
  bashunit::runner::parse_result "$fn_name" "$test_execution_result" "$@"

  local test_exit_code="$_BASHUNIT_TEST_EXIT_CODE"

  bashunit::runner::compute_total_assertions "$test_execution_result"
  local total_assertions=$_BASHUNIT_RUNNER_TOTAL_OUT

  bashunit::runner::extract_encoded_field "$test_execution_result" "TEST_TITLE"
  local encoded_test_title=$_BASHUNIT_RUNNER_FIELD_OUT
  bashunit::runner::extract_encoded_field "$test_execution_result" "TEST_HOOK_FAILURE"
  local hook_failure=$_BASHUNIT_RUNNER_FIELD_OUT
  bashunit::runner::extract_encoded_field "$test_execution_result" "TEST_HOOK_MESSAGE"
  local encoded_hook_message=$_BASHUNIT_RUNNER_FIELD_OUT

  local test_title=""
  [ -n "$encoded_test_title" ] && test_title="$(bashunit::helper::decode_base64 "$encoded_test_title")"
  local hook_message=""
  [ -n "$encoded_hook_message" ] && hook_message="$(bashunit::helper::decode_base64 "$encoded_hook_message")"

  bashunit::set_test_title "$test_title"
  bashunit::helper::normalize_test_function_name_to_slot "$fn_name" "$interpolated_fn_name"
  local label=$_BASHUNIT_HELPER_NORMALIZED_OUT
  bashunit::state::reset_test_title
  bashunit::state::reset_current_test_interpolated_function_name

  local failure_label="$label"
  local failure_function="$fn_name"
  if [ -n "$hook_failure" ]; then
    bashunit::helper::normalize_test_function_name_to_slot "$hook_failure"
    failure_label=$_BASHUNIT_HELPER_NORMALIZED_OUT
    failure_function="$hook_failure"
  fi

  if [ -n "$runtime_error" ] || [ "$test_exit_code" -ne 0 ]; then
    bashunit::state::add_tests_failed
    bashunit::rerun::record "$test_file" "$fn_name"
    local error_message="$runtime_error"
    if [ -n "$hook_failure" ] && [ -n "$hook_message" ]; then
      error_message="$hook_message"
    elif [ -z "$error_message" ] && [ -n "$hook_message" ]; then
      error_message="$hook_message"
    fi

    # When the test was killed by a signal (or timed out), replace an empty or
    # generic "Killed" message with a specific cause.
    if [ -z "$hook_failure" ]; then
      local kill_message
      kill_message=$(bashunit::runner::classify_kill_signal "$test_exit_code")
      if [ -n "$kill_message" ]; then
        case "$error_message" in
        '' | *[Kk]illed* | *[Tt]erminated*) error_message="$kill_message" ;;
        esac
      fi
    fi

    # A test that exceeded BASHUNIT_TEST_TIMEOUT gets a clear, specific message.
    if [ "$timed_out" = "true" ]; then
      error_message="Test timed out after $(bashunit::env::test_timeout_secs)s"
    fi

    bashunit::console_results::print_error_test "$failure_function" "$error_message" "$runtime_output"
    bashunit::reports::add_test_failed "$test_file" "$failure_label" "$duration" "$total_assertions" "$error_message"
    bashunit::runner::write_failure_result_output "$test_file" "$failure_function" "$error_message" "$runtime_output"
    bashunit::internal_log "Test error" "$failure_label" "$error_message"

    if bashunit::env::is_stop_on_failure_enabled; then
      if bashunit::parallel::is_enabled; then
        bashunit::parallel::mark_stop_on_failure
      else
        exit "$EXIT_CODE_STOP_ON_FAILURE"
      fi
    fi
    return
  fi

  if [ "$current_assertions_failed" != "$_BASHUNIT_ASSERTIONS_FAILED" ]; then
    bashunit::state::add_tests_failed
    bashunit::rerun::record "$test_file" "$fn_name"
    bashunit::reports::add_test_failed "$test_file" "$label" "$duration" "$total_assertions" "$subshell_output"
    local assertion_runtime_output
    assertion_runtime_output="$(
      bashunit::runner::extract_assertion_runtime_output "$runtime_output" "$subshell_output"
    )"
    bashunit::runner::write_failure_result_output \
      "$test_file" "$fn_name" "$subshell_output" "$assertion_runtime_output"

    bashunit::internal_log "Test failed" "$label"

    if bashunit::env::is_stop_on_failure_enabled; then
      if bashunit::parallel::is_enabled; then
        bashunit::parallel::mark_stop_on_failure
      else
        exit "$EXIT_CODE_STOP_ON_FAILURE"
      fi
    fi
    return
  fi

  if [ "$current_assertions_snapshot" != "$_BASHUNIT_ASSERTIONS_SNAPSHOT" ]; then
    bashunit::state::add_tests_snapshot
    # In failures-only mode, suppress snapshot test output
    if ! bashunit::env::is_failures_only_enabled; then
      bashunit::console_results::print_snapshot_test "$label"
    fi
    bashunit::reports::add_test_snapshot "$test_file" "$label" "$duration" "$total_assertions"
    bashunit::internal_log "Test snapshot" "$label"
    return
  fi

  if [ "$current_assertions_incomplete" != "$_BASHUNIT_ASSERTIONS_INCOMPLETE" ]; then
    bashunit::state::add_tests_incomplete
    bashunit::reports::add_test_incomplete "$test_file" "$label" "$duration" "$total_assertions"
    bashunit::runner::write_incomplete_result_output "$test_file" "$fn_name" "$subshell_output"
    bashunit::internal_log "Test incomplete" "$label"
    return
  fi

  if [ "$current_assertions_skipped" != "$_BASHUNIT_ASSERTIONS_SKIPPED" ]; then
    bashunit::state::add_tests_skipped
    bashunit::reports::add_test_skipped "$test_file" "$label" "$duration" "$total_assertions"
    bashunit::runner::write_skipped_result_output "$test_file" "$fn_name" "$subshell_output"
    bashunit::internal_log "Test skipped" "$label"
    return
  fi

  # Check for risky test (zero assertions)
  if [ "$total_assertions" -eq 0 ]; then
    if bashunit::env::is_fail_on_risky_enabled; then
      local risky_msg="Test has no assertions (risky)"
      bashunit::state::add_tests_failed
      bashunit::rerun::record "$test_file" "$fn_name"
      bashunit::console_results::print_error_test "$fn_name" "$risky_msg"
      bashunit::reports::add_test_failed "$test_file" "$label" "$duration" "$total_assertions" "$risky_msg"
      bashunit::runner::write_failure_result_output "$test_file" "$fn_name" "$risky_msg"
      bashunit::internal_log "Test failed (risky)" "$label"
      if bashunit::env::is_stop_on_failure_enabled; then
        if bashunit::parallel::is_enabled; then
          bashunit::parallel::mark_stop_on_failure
        else
          exit "$EXIT_CODE_STOP_ON_FAILURE"
        fi
      fi
      return
    fi
    bashunit::state::add_tests_risky
    if ! bashunit::env::is_failures_only_enabled; then
      bashunit::console_results::print_risky_test "${label}" "$duration"
    fi
    bashunit::reports::add_test_risky "$test_file" "$label" "$duration" "$total_assertions"
    bashunit::runner::write_risky_result_output "$test_file" "$fn_name"
    bashunit::internal_log "Test risky" "$label"
    return
  fi

  # A test that only passed after retrying is annotated so flakiness stays visible.
  _BASHUNIT_RETRY_NOTE=""
  if [ "$retries_used" -gt 0 ]; then
    _BASHUNIT_RETRY_NOTE=" (retry $retries_used/$retry_max)"
  fi
  # In failures-only mode, suppress successful test output
  if ! bashunit::env::is_failures_only_enabled; then
    if [ "$fn_name" = "$interpolated_fn_name" ]; then
      bashunit::console_results::print_successful_test "${label}" "$duration" "$@"
    else
      bashunit::console_results::print_successful_test "${label}" "$duration"
    fi
  fi
  _BASHUNIT_RETRY_NOTE=""
  bashunit::state::add_tests_passed
  bashunit::reports::add_test_passed "$test_file" "$label" "$duration" "$total_assertions"
  bashunit::internal_log "Test passed" "$label"
}

function bashunit::runner::cleanup_on_exit() {
  local test_file="$1"
  local exit_code="$2"

  # Disable coverage trap before cleanup to avoid interference
  if [ "${_BASHUNIT_COVERAGE_ON:-0}" = 1 ]; then
    bashunit::coverage::disable_trap
  fi

  set +e

  # Detect unexpected subshell exit during set_up (Issue #611).
  # When 'source' of a non-existent file fails under set -eE, the ERR trap
  # does not fire. On macOS Bash 3.2, $? is 0 in the EXIT trap; on Linux
  # Bash 5.x, $? is 1. In both cases the hook failure is not recorded.
  # Additionally, the stdout redirect from execute_test_hook leaks into the
  # EXIT trap. Restore stdout from saved FD 5 so export_subshell_context
  # output reaches test_execution_result.
  # shellcheck disable=SC2031
  if [ "${_BASHUNIT_SETUP_COMPLETED:-true}" != "true" ]; then
    exec 1>&5
    if [ "$exit_code" -eq 0 ]; then
      exit_code=1
    fi
    if [ -z "${_BASHUNIT_TEST_HOOK_FAILURE:-}" ]; then
      bashunit::state::set_test_hook_failure "set_up"
      bashunit::state::set_test_hook_message "Hook 'set_up' failed unexpectedly (e.g., source of non-existent file)"
    fi
  fi

  # Don't use || here - it disables ERR trap in the entire call chain
  bashunit::runner::run_tear_down "$test_file"
  local teardown_status=$?
  bashunit::runner::clear_mocks
  bashunit::cleanup_testcase_temp_files

  if [ $teardown_status -ne 0 ]; then
    bashunit::state::set_test_exit_code "$teardown_status"
  else
    bashunit::state::set_test_exit_code "$exit_code"
  fi

  bashunit::state::export_subshell_context
}

# Writes the decoded subshell output into _BASHUNIT_RUNNER_SUBSHELL_OUTPUT_OUT.
# The empty case (a passing test with no captured output) short-circuits with
# no subshell at all; only the non-empty path pays the base64 fork (#762/#764).
# Arguments: $1 test_execution_result
function bashunit::runner::decode_subshell_output() {
  local test_execution_result="$1"

  local test_output_base64="${test_execution_result##*##TEST_OUTPUT=}"
  test_output_base64="${test_output_base64%%##*}"
  if [ -z "$test_output_base64" ] || [ "$test_output_base64" = "_BASHUNIT_EMPTY_" ]; then
    _BASHUNIT_RUNNER_SUBSHELL_OUTPUT_OUT=""
    return
  fi
  _BASHUNIT_RUNNER_SUBSHELL_OUTPUT_OUT="$(bashunit::helper::decode_base64 "$test_output_base64")"
}

function bashunit::runner::is_simple_progress_output() {
  local output="$1"

  [ -n "$output" ] || return 1

  local color
  for color in \
    "$_BASHUNIT_COLOR_DEFAULT" \
    "$_BASHUNIT_COLOR_PASSED" \
    "$_BASHUNIT_COLOR_FAILED" \
    "$_BASHUNIT_COLOR_SKIPPED" \
    "$_BASHUNIT_COLOR_INCOMPLETE" \
    "$_BASHUNIT_COLOR_SNAPSHOT" \
    "$_BASHUNIT_COLOR_RISKY"; do
    [ -n "$color" ] && output="${output//"$color"/}"
  done

  local i
  local char
  for ((i = 0; i < ${#output}; i++)); do
    char="${output:$i:1}"
    case "$char" in
    "." | "F" | "S" | "I" | "N" | "R" | "E" | "?") ;;
    *) return 1 ;;
    esac
  done

  return 0
}

function bashunit::runner::line_exists_in_output() {
  local needle="$1"
  local haystack="$2"
  local line

  while IFS= read -r line || [ -n "$line" ]; do
    [ "$line" = "$needle" ] && return 0
  done <<<"$haystack"

  return 1
}

function bashunit::runner::extract_assertion_runtime_output() {
  local runtime_output="$1"
  local rendered_assertion_output="$2"
  local filtered_output=""
  local line

  while IFS= read -r line || [ -n "$line" ]; do
    if bashunit::runner::line_exists_in_output "$line" "$rendered_assertion_output"; then
      continue
    fi
    if bashunit::runner::is_simple_progress_output "$line"; then
      continue
    fi

    [ -n "$filtered_output" ] && filtered_output="$filtered_output"$'\n'
    filtered_output="$filtered_output$line"
  done <<<"$runtime_output"

  runtime_output="$filtered_output"

  while [ -n "$runtime_output" ]; do
    case "$runtime_output" in
    *$'\n') runtime_output="${runtime_output%$'\n'}" ;;
    *) break ;;
    esac
  done

  echo "$runtime_output"
}

function bashunit::runner::parse_result() {
  local fn_name=$1
  shift
  local execution_result=$1
  shift
  local IFS=$' \t\n'
  local -a args
  args=("$@")

  if bashunit::parallel::is_enabled; then
    bashunit::runner::parse_result_parallel "$fn_name" "$execution_result" ${args+"${args[@]}"}
  else
    bashunit::runner::parse_result_sync "$fn_name" "$execution_result"
  fi
}

function bashunit::runner::parse_result_parallel() {
  local fn_name=$1
  shift
  local execution_result=$1
  shift
  local IFS=$' \t\n'
  local -a args
  args=("$@")

  # This runs once per test in every parallel worker, so avoid per-test forks:
  # derive the suite dir name with parameter expansion (no basename), only
  # mkdir when the dir is missing (first test of the file wins the race,
  # `-p` makes the losers no-ops), and skip arg sanitizing entirely for the
  # common no-provider-args case.
  local test_suite_base="${test_file##*/}"
  local test_suite_dir="${TEMP_DIR_PARALLEL_TEST_SUITE}/${test_suite_base%.sh}"
  [ -d "$test_suite_dir" ] || mkdir -p "$test_suite_dir"

  local sanitized_args=""
  if [ -n "${args[*]+"${args[*]}"}" ]; then
    sanitized_args=$(echo "${args[*]}" | tr '[:upper:]' '[:lower:]' | sed -E 's/[^a-z0-9]+/-/g; s/^-|-$//')
  fi
  local template
  if [ -z "$sanitized_args" ]; then
    template="${fn_name}.XXXXXX"
  else
    template="${fn_name}-${sanitized_args}.XXXXXX"
  fi

  local unique_test_result_file
  if unique_test_result_file=$("$MKTEMP" -p "$test_suite_dir" "$template" 2>/dev/null); then
    true
  else
    unique_test_result_file=$("$MKTEMP" "$test_suite_dir/$template")
  fi
  mv "$unique_test_result_file" "${unique_test_result_file}.result"
  unique_test_result_file="${unique_test_result_file}.result"

  bashunit::internal_log "[PARA]" "fn_name:$fn_name" "execution_result:$execution_result"

  bashunit::runner::parse_result_sync "$fn_name" "$execution_result"

  echo "$execution_result" >"$unique_test_result_file"
}

# shellcheck disable=SC2295
##
# Parses the encoded per-test result's last line into the counts out-slots
# (_BASHUNIT_RUNNER_COUNTS_*_OUT). Pure read: never mutates the cumulative
# _BASHUNIT_ASSERTIONS_* / _BASHUNIT_TEST_EXIT_CODE state, so the retry loop can
# judge an attempt's outcome without committing it.
##
function bashunit::runner::extract_result_counts() {
  local execution_result=$1

  local result_line
  result_line="${execution_result##*$'\n'}"

  local assertions_failed=0
  local assertions_passed=0
  local assertions_skipped=0
  local assertions_incomplete=0
  local assertions_snapshot=0
  local test_exit_code=0

  # Extract values using parameter expansion instead of spawning grep/sed subprocesses
  case "$result_line" in
  *"ASSERTIONS_FAILED="*"##ASSERTIONS_PASSED="*)
    local _tail
    _tail="${result_line##*ASSERTIONS_FAILED=}"
    assertions_failed="${_tail%%##*}"
    _tail="${result_line##*ASSERTIONS_PASSED=}"
    assertions_passed="${_tail%%##*}"
    _tail="${result_line##*ASSERTIONS_SKIPPED=}"
    assertions_skipped="${_tail%%##*}"
    _tail="${result_line##*ASSERTIONS_INCOMPLETE=}"
    assertions_incomplete="${_tail%%##*}"
    _tail="${result_line##*ASSERTIONS_SNAPSHOT=}"
    assertions_snapshot="${_tail%%##*}"
    _tail="${result_line##*TEST_EXIT_CODE=}"
    test_exit_code="${_tail%%##*}"
    # Strip any trailing non-digit suffix (end of line) from the final field
    test_exit_code="${test_exit_code%%[!0-9]*}"
    : "${assertions_failed:=0}"
    : "${assertions_passed:=0}"
    : "${assertions_skipped:=0}"
    : "${assertions_incomplete:=0}"
    : "${assertions_snapshot:=0}"
    : "${test_exit_code:=0}"
    ;;
  esac

  _BASHUNIT_RUNNER_COUNTS_FAILED_OUT=$assertions_failed
  _BASHUNIT_RUNNER_COUNTS_PASSED_OUT=$assertions_passed
  _BASHUNIT_RUNNER_COUNTS_SKIPPED_OUT=$assertions_skipped
  _BASHUNIT_RUNNER_COUNTS_INCOMPLETE_OUT=$assertions_incomplete
  _BASHUNIT_RUNNER_COUNTS_SNAPSHOT_OUT=$assertions_snapshot
  _BASHUNIT_RUNNER_COUNTS_EXIT_CODE_OUT=$test_exit_code
}

function bashunit::runner::parse_result_sync() {
  local fn_name=$1
  local execution_result=$2

  bashunit::runner::extract_result_counts "$execution_result"

  bashunit::internal_log "[SYNC]" "fn_name:$fn_name" "execution_result:$execution_result"

  _BASHUNIT_ASSERTIONS_PASSED=$((_BASHUNIT_ASSERTIONS_PASSED + _BASHUNIT_RUNNER_COUNTS_PASSED_OUT))
  _BASHUNIT_ASSERTIONS_FAILED=$((_BASHUNIT_ASSERTIONS_FAILED + _BASHUNIT_RUNNER_COUNTS_FAILED_OUT))
  _BASHUNIT_ASSERTIONS_SKIPPED=$((_BASHUNIT_ASSERTIONS_SKIPPED + _BASHUNIT_RUNNER_COUNTS_SKIPPED_OUT))
  _BASHUNIT_ASSERTIONS_INCOMPLETE=$((_BASHUNIT_ASSERTIONS_INCOMPLETE + _BASHUNIT_RUNNER_COUNTS_INCOMPLETE_OUT))
  _BASHUNIT_ASSERTIONS_SNAPSHOT=$((_BASHUNIT_ASSERTIONS_SNAPSHOT + _BASHUNIT_RUNNER_COUNTS_SNAPSHOT_OUT))
  _BASHUNIT_TEST_EXIT_CODE=$((_BASHUNIT_TEST_EXIT_CODE + _BASHUNIT_RUNNER_COUNTS_EXIT_CODE_OUT))

  bashunit::internal_log "result_summary" \
    "failed:$_BASHUNIT_RUNNER_COUNTS_FAILED_OUT" \
    "passed:$_BASHUNIT_RUNNER_COUNTS_PASSED_OUT" \
    "skipped:$_BASHUNIT_RUNNER_COUNTS_SKIPPED_OUT" \
    "incomplete:$_BASHUNIT_RUNNER_COUNTS_INCOMPLETE_OUT" \
    "snapshot:$_BASHUNIT_RUNNER_COUNTS_SNAPSHOT_OUT" \
    "exit_code:$_BASHUNIT_RUNNER_COUNTS_EXIT_CODE_OUT"
}

function bashunit::runner::write_failure_result_output() {
  local test_file=$1
  local fn_name=$2
  local error_msg=$3
  local raw_output="${4:-}"

  local line_number
  line_number=$(bashunit::helper::get_function_line_number "$fn_name")

  local test_nr="*"
  if ! bashunit::parallel::is_enabled; then
    test_nr=$(bashunit::state::get_tests_failed)
  fi

  local output_section=""
  if [ -n "$raw_output" ] && bashunit::env::is_show_output_on_failure_enabled; then
    output_section="\n    Output:\n$raw_output"
  fi

  local source_context=""
  if [ -n "$line_number" ] && [ -f "$test_file" ]; then
    source_context=$(bashunit::runner::get_failure_source_context \
      "$test_file" "$line_number")
  fi

  echo -e "$test_nr) $test_file:$line_number\n$error_msg$output_section$source_context" \
    >>"$FAILURES_OUTPUT_PATH"
}

function bashunit::runner::get_failure_source_context() {
  local file=$1
  local fn_line=$2

  # Read the file once (a bash builtin loop) instead of forking `sed` to fetch
  # each line and `grep` to test each line for the closing brace. The fork count
  # no longer grows with the function length.
  local line_text line_num=0 assert_lines="" stripped trimmed
  while IFS= read -r line_text || [ -n "$line_text" ]; do
    line_num=$((line_num + 1))
    # Skip everything up to and including the function definition line.
    if [ "$line_num" -le "$fn_line" ]; then
      continue
    fi
    # Stop at the closing brace of the function (a line that is only `}`).
    stripped="${line_text#"${line_text%%[![:space:]]*}"}"
    stripped="${stripped%"${stripped##*[![:space:]]}"}"
    if [ "$stripped" = "}" ]; then
      break
    fi
    # Collect lines containing assert calls
    case "$line_text" in
    *assert_* | *assert\ *)
      trimmed="${line_text#"${line_text%%[![:space:]]*}"}"
      assert_lines="${assert_lines}\n    ${_BASHUNIT_COLOR_FAINT}${line_num}:${_BASHUNIT_COLOR_DEFAULT} ${trimmed}"
      ;;
    esac
  done <"$file"

  if [ -n "$assert_lines" ]; then
    echo -e "\n    ${_BASHUNIT_COLOR_FAINT}Source:${_BASHUNIT_COLOR_DEFAULT}${assert_lines}"
  fi
}

function bashunit::runner::write_skipped_result_output() {
  local test_file=$1
  local fn_name=$2
  local output_msg=$3

  local line_number
  line_number=$(bashunit::helper::get_function_line_number "$fn_name")

  local test_nr="*"
  if ! bashunit::parallel::is_enabled; then
    test_nr=$(bashunit::state::get_tests_skipped)
  fi

  echo -e "$test_nr) $test_file:$line_number\n$output_msg" >>"$SKIPPED_OUTPUT_PATH"
}

function bashunit::runner::write_incomplete_result_output() {
  local test_file=$1
  local fn_name=$2
  local output_msg=$3

  local line_number
  line_number=$(bashunit::helper::get_function_line_number "$fn_name")

  local test_nr="*"
  if ! bashunit::parallel::is_enabled; then
    test_nr=$(bashunit::state::get_tests_incomplete)
  fi

  echo -e "$test_nr) $test_file:$line_number\n$output_msg" >>"$INCOMPLETE_OUTPUT_PATH"
}

function bashunit::runner::write_risky_result_output() {
  local test_file=$1
  local fn_name=$2

  local line_number
  line_number=$(bashunit::helper::get_function_line_number "$fn_name")

  local test_nr="*"
  if ! bashunit::parallel::is_enabled; then
    test_nr=$(bashunit::state::get_tests_risky)
  fi

  echo -e "$test_nr) $test_file:$line_number\nTest has no assertions (risky)" >>"$RISKY_OUTPUT_PATH"
}

function bashunit::runner::record_file_hook_failure() {
  local hook_name="$1"
  local test_file="$2"
  local hook_output="$3"
  local status="$4"
  local render_header="${5:-false}"

  if [ "$render_header" = true ]; then
    bashunit::runner::render_running_file_header "$test_file" true
  fi

  if [ -z "$hook_output" ]; then
    hook_output="Hook '$hook_name' failed with exit code $status"
  fi

  bashunit::state::add_tests_failed
  bashunit::console_results::print_error_test "$hook_name" "$hook_output"
  local _normalized_hook
  _normalized_hook="$(bashunit::helper::normalize_test_function_name "$hook_name")"
  bashunit::reports::add_test_failed "$test_file" "$_normalized_hook" 0 0 "$hook_output"
  bashunit::runner::write_failure_result_output "$test_file" "$hook_name" "$hook_output"

  return "$status"
}

function bashunit::runner::execute_file_hook() {
  local hook_name="$1"
  local test_file="$2"
  local render_header="${3:-false}"

  declare -F "$hook_name" >/dev/null 2>&1 || return 0

  local hook_output=""
  local status=0
  local hook_output_file
  hook_output_file=$(bashunit::temp_file "${hook_name}_output")

  # Enable errtrace to catch any failing command in the hook.
  # Using -E (errtrace) without -e (errexit) prevents the main process from
  # exiting on source failures (Bash 3.2 doesn't trigger ERR trap with -eE).
  # The ERR trap saves the exit status to a global variable, cleans up shell
  # options, and returns from the hook function to prevent subsequent commands
  # from executing.
  # Variables set before the failure are preserved since we don't use a subshell.
  _BASHUNIT_HOOK_ERR_STATUS=0
  set -E
  if bashunit::env::is_strict_mode_enabled; then
    set -uo pipefail
  fi
  # The trap returns from the function where the failure occurred (early-exit
  # semantics for intermediate failing commands) — but only when that frame is
  # NOT this executor: on Bash >= 4 the trap also fires HERE when the hook call
  # itself returns non-zero, and an unconditional return skipped
  # record_file_hook_failure entirely (silent failures, off-by-one counts, #836).
  # shellcheck disable=SC2154
  trap '_BASHUNIT_HOOK_ERR_STATUS=$?
    if [ "${FUNCNAME[0]:-}" != "bashunit::runner::execute_file_hook" ]; then
      set +Eu +o pipefail
      trap - ERR
      return $_BASHUNIT_HOOK_ERR_STATUS
    fi' ERR

  {
    "$hook_name"
  } >"$hook_output_file" 2>&1
  # Real exit status of the hook, read from $? (this function runs without -e,
  # so a failing compound does not exit). The ERR-trap global alone is not
  # enough: a hook ending in a failing `cmd && var=x` guard returns non-zero
  # without ever firing the trap (&& lists are ERR-exempt), which silently
  # swallowed the failure (#836).
  status=$?
  if [ "$status" -eq 0 ]; then
    status=$_BASHUNIT_HOOK_ERR_STATUS
  fi

  trap - ERR
  set +Eu +o pipefail

  if [ -f "$hook_output_file" ]; then
    hook_output=""
    local line
    while IFS= read -r line; do
      [ -z "$hook_output" ] && hook_output="$line" || hook_output="$hook_output"$'\n'"$line"
    done <"$hook_output_file"
    rm -f "$hook_output_file"
  fi

  if [ $status -ne 0 ]; then
    bashunit::runner::record_file_hook_failure "$hook_name" "$test_file" "$hook_output" "$status" "$render_header"
    return $status
  fi

  if [ -n "$hook_output" ] && bashunit::env::is_verbose_enabled; then
    printf "%s\n" "$hook_output"
  fi

  return 0
}

function bashunit::runner::run_set_up() {
  local _test_file="${1-}"
  bashunit::internal_log "run_set_up"
  bashunit::runner::execute_test_hook 'set_up'
}

function bashunit::runner::run_set_up_before_script() {
  local test_file="$1"
  bashunit::internal_log "run_set_up_before_script"

  # Check if hook exists first
  if ! declare -F "set_up_before_script" >/dev/null 2>&1; then
    return 0
  fi

  local start_time
  start_time=$(bashunit::clock::now)

  # Enable coverage trap to attribute lines executed during set_up_before_script
  if [ "${_BASHUNIT_COVERAGE_ON:-0}" = 1 ]; then
    bashunit::coverage::enable_trap
  fi

  # Execute the hook (render_header=false since header is already rendered)
  bashunit::runner::execute_file_hook 'set_up_before_script' "$test_file" false
  local status=$?

  # Disable coverage trap after hook execution
  if [ "${_BASHUNIT_COVERAGE_ON:-0}" = 1 ]; then
    bashunit::coverage::disable_trap
  fi

  local end_time
  end_time=$(bashunit::clock::now)
  local duration_ns=$((end_time - start_time))
  local duration_ms=$((duration_ns / 1000000))

  # Print completion message only if hook succeeded
  if [ $status -eq 0 ]; then
    bashunit::console_results::print_hook_completed "set_up_before_script" "$duration_ms"
  fi

  return $status
}

function bashunit::runner::run_tear_down() {
  local _test_file="${1-}"
  bashunit::internal_log "run_tear_down"
  bashunit::runner::execute_test_hook 'tear_down'
}

function bashunit::runner::execute_test_hook() {
  local hook_name="$1"

  declare -F "$hook_name" >/dev/null 2>&1 || return 0

  local hook_output=""
  local status=0
  local hook_output_file
  hook_output_file=$(bashunit::temp_file "${hook_name}_output")

  # Enable errtrace to catch any failing command in the hook.
  # Using -E (errtrace) without -e (errexit) prevents the subshell from
  # exiting on source failures (Bash 3.2 doesn't trigger ERR trap with -eE).
  # The ERR trap saves the exit status to a global variable, cleans up shell
  # options, and returns from the hook function to prevent subsequent commands
  # from executing.
  # Variables set before the failure are preserved since we don't use a subshell.
  _BASHUNIT_HOOK_ERR_STATUS=0
  set -E
  if bashunit::env::is_strict_mode_enabled; then
    set -uo pipefail
  fi
  # See the twin comment in execute_file_hook: conditional return keeps the
  # early-exit semantics for intermediate failures without silently returning
  # from THIS executor when the trap re-fires here on Bash >= 4 (#836).
  # shellcheck disable=SC2154
  trap '_BASHUNIT_HOOK_ERR_STATUS=$?
    if [ "${FUNCNAME[0]:-}" != "bashunit::runner::execute_test_hook" ]; then
      set +Eu +o pipefail
      trap - ERR
      return $_BASHUNIT_HOOK_ERR_STATUS
    fi' ERR

  {
    "$hook_name"
  } >"$hook_output_file" 2>&1
  # Real hook status from $?; the trap global alone misses failing
  # `cmd && var=x` guards (&& lists are ERR-exempt) (#836).
  status=$?
  if [ "$status" -eq 0 ]; then
    status=$_BASHUNIT_HOOK_ERR_STATUS
  fi

  trap - ERR
  set +Eu +o pipefail

  if [ -f "$hook_output_file" ]; then
    hook_output=""
    local line
    while IFS= read -r line; do
      [ -z "$hook_output" ] && hook_output="$line" || hook_output="$hook_output"$'\n'"$line"
    done <"$hook_output_file"
    rm -f "$hook_output_file"
  fi

  if [ $status -ne 0 ]; then
    local message="$hook_output"
    if [ -n "$hook_output" ]; then
      printf "%s" "$hook_output"
    else
      message="Hook '$hook_name' failed with exit code $status"
      printf "%s\n" "$message" >&2
    fi
    bashunit::runner::record_test_hook_failure "$hook_name" "$message" "$status"
    return "$status"
  fi

  if [ -n "$hook_output" ]; then
    printf "%s" "$hook_output"
  fi

  return 0
}

function bashunit::runner::record_test_hook_failure() {
  local hook_name="$1"
  local hook_message="$2"
  local status="$3"

  if [ -n "$_BASHUNIT_TEST_HOOK_FAILURE" ]; then
    return "$status"
  fi

  bashunit::state::set_test_hook_failure "$hook_name"
  bashunit::state::set_test_hook_message "$hook_message"

  return "$status"
}

function bashunit::runner::clear_mocks() {
  if [ "${#_BASHUNIT_MOCKED_FUNCTIONS[@]}" -eq 0 ]; then
    return
  fi

  local i
  for i in "${!_BASHUNIT_MOCKED_FUNCTIONS[@]}"; do
    bashunit::unmock "${_BASHUNIT_MOCKED_FUNCTIONS[$i]:-}"
  done
}

function bashunit::runner::run_tear_down_after_script() {
  local test_file="$1"
  bashunit::internal_log "run_tear_down_after_script"

  # Check if hook exists first
  if ! declare -F "tear_down_after_script" >/dev/null 2>&1; then
    # Add blank line after tests if no tear_down hook
    if ! bashunit::env::is_simple_output_enabled &&
      ! bashunit::env::is_failures_only_enabled &&
      ! bashunit::env::is_no_progress_enabled &&
      ! bashunit::parallel::is_enabled; then
      echo ""
    fi
    return 0
  fi

  local start_time
  start_time=$(bashunit::clock::now)

  # Enable coverage trap to attribute lines executed during tear_down_after_script
  if [ "${_BASHUNIT_COVERAGE_ON:-0}" = 1 ]; then
    bashunit::coverage::enable_trap
  fi

  # Execute the hook
  bashunit::runner::execute_file_hook 'tear_down_after_script' "$test_file"
  local status=$?

  # Disable coverage trap after hook execution
  if [ "${_BASHUNIT_COVERAGE_ON:-0}" = 1 ]; then
    bashunit::coverage::disable_trap
  fi

  local end_time
  end_time=$(bashunit::clock::now)
  local duration_ns=$((end_time - start_time))
  local duration_ms=$((duration_ns / 1000000))

  # Print completion message only if hook succeeded
  if [ $status -eq 0 ]; then
    bashunit::console_results::print_hook_completed "tear_down_after_script" "$duration_ms"
  fi

  # Add blank line after tear_down output
  if ! bashunit::env::is_simple_output_enabled &&
    ! bashunit::env::is_failures_only_enabled &&
    ! bashunit::env::is_no_progress_enabled &&
    ! bashunit::parallel::is_enabled; then
    echo ""
  fi

  return $status
}

##
# Unset a file's test functions once the file has been processed.
#
# Test files are sourced into the main shell, and their functions used to stay
# defined for the whole run: every test's $() subshell then forked an
# ever-growing shell, making multi-file runs quadratic in file count (#829).
# In parallel mode the file's workers have already forked (with their own copy
# of the functions) by the time this runs, so unsetting here is race-free.
# Arguments: $1 - whitespace-separated test function names
##
function bashunit::runner::clean_script_test_functions() {
  local IFS=$' \t\n'
  local fn
  for fn in $1; do
    unset -f "$fn" 2>/dev/null || true
  done
}

function bashunit::runner::clean_set_up_and_tear_down_after_script() {
  bashunit::internal_log "clean_set_up_and_tear_down_after_script"
  bashunit::helper::unset_if_exists 'set_up'
  bashunit::helper::unset_if_exists 'tear_down'
  bashunit::helper::unset_if_exists 'set_up_before_script'
  bashunit::helper::unset_if_exists 'tear_down_after_script'
}

# benchmark.sh

_BASHUNIT_BENCH_NAMES=()
_BASHUNIT_BENCH_REVS=()
_BASHUNIT_BENCH_ITS=()
_BASHUNIT_BENCH_AVERAGES=()
_BASHUNIT_BENCH_MAX_MILLIS=()

function bashunit::benchmark::parse_annotations() {
  local fn_name=$1
  local script=$2
  local revs=1
  local its=1
  local max_ms=""

  local annotation
  annotation=$(awk "/function[[:space:]]+${fn_name}[[:space:]]*\(/ {print prev; exit} {prev=\$0}" "$script")

  local _extracted
  _extracted=$(echo "$annotation" | sed -n 's/.*@revs=\([0-9][0-9]*\).*/\1/p')
  if [ -n "$_extracted" ]; then
    revs="$_extracted"
  else
    _extracted=$(echo "$annotation" | sed -n 's/.*@revolutions=\([0-9][0-9]*\).*/\1/p')
    if [ -n "$_extracted" ]; then
      revs="$_extracted"
    fi
  fi

  _extracted=$(echo "$annotation" | sed -n 's/.*@its=\([0-9][0-9]*\).*/\1/p')
  if [ -n "$_extracted" ]; then
    its="$_extracted"
  else
    _extracted=$(echo "$annotation" | sed -n 's/.*@iterations=\([0-9][0-9]*\).*/\1/p')
    if [ -n "$_extracted" ]; then
      its="$_extracted"
    fi
  fi

  _extracted=$(echo "$annotation" | sed -n 's/.*@max_ms=\([0-9.][0-9.]*\).*/\1/p')
  if [ -n "$_extracted" ]; then
    max_ms="$_extracted"
  fi

  if [ -n "$max_ms" ]; then
    echo "$revs" "$its" "$max_ms"
  else
    echo "$revs" "$its"
  fi
}

function bashunit::benchmark::add_result() {
  _BASHUNIT_BENCH_NAMES[${#_BASHUNIT_BENCH_NAMES[@]}]="$1"
  _BASHUNIT_BENCH_REVS[${#_BASHUNIT_BENCH_REVS[@]}]="$2"
  _BASHUNIT_BENCH_ITS[${#_BASHUNIT_BENCH_ITS[@]}]="$3"
  _BASHUNIT_BENCH_AVERAGES[${#_BASHUNIT_BENCH_AVERAGES[@]}]="$4"
  _BASHUNIT_BENCH_MAX_MILLIS[${#_BASHUNIT_BENCH_MAX_MILLIS[@]}]="$5"
}

# shellcheck disable=SC2155
function bashunit::benchmark::run_function() {
  local fn_name=$1
  local revs=$2
  local its=$3
  local max_ms=$4
  local IFS=$' \t\n'
  local -a durations=()
  local durations_count=0
  local i r

  for ((i = 1; i <= its; i++)); do
    local start_time=$(bashunit::clock::now)
    (
      for ((r = 1; r <= revs; r++)); do
        "$fn_name" >/dev/null 2>&1
      done
    )
    local end_time=$(bashunit::clock::now)
    local dur_ns=$(bashunit::math::calculate "($end_time - $start_time)")
    local dur_ms=$(bashunit::math::calculate "$dur_ns / 1000000")
    durations[durations_count]="$dur_ms"
    durations_count=$((durations_count + 1))

    if bashunit::env::is_bench_mode_enabled; then
      local label="$(bashunit::helper::normalize_test_function_name "$fn_name")"
      local line="$label [$i/$its] ${dur_ms} ms"
      bashunit::state::print_line "successful" "$line"
    fi
  done

  local sum=0
  local d
  for d in "${durations[@]+"${durations[@]}"}"; do
    sum=$(bashunit::math::calculate "$sum + $d")
  done
  local avg=$(bashunit::math::calculate "$sum / ${#durations[@]}")
  bashunit::benchmark::add_result "$fn_name" "$revs" "$its" "$avg" "$max_ms"
}

function bashunit::benchmark::print_results() {
  if ! bashunit::env::is_bench_mode_enabled; then
    return
  fi

  if ((${#_BASHUNIT_BENCH_NAMES[@]} == 0)); then
    return
  fi

  if bashunit::env::is_simple_output_enabled; then
    printf "\n"
  fi

  printf "\nBenchmark Results (avg ms)\n"
  bashunit::print_line 80 "="
  printf "\n"

  local IFS=$' \t\n'
  local has_threshold=false
  local val
  for val in "${_BASHUNIT_BENCH_MAX_MILLIS[@]+"${_BASHUNIT_BENCH_MAX_MILLIS[@]}"}"; do
    if [ -n "$val" ]; then
      has_threshold=true
      break
    fi
  done

  if $has_threshold; then
    printf '%-40s %6s %6s %10s %12s\n' "Name" "Revs" "Its" "Avg(ms)" "Status"
  else
    printf '%-40s %6s %6s %10s\n' "Name" "Revs" "Its" "Avg(ms)"
  fi

  local i
  for i in "${!_BASHUNIT_BENCH_NAMES[@]}"; do
    local name="${_BASHUNIT_BENCH_NAMES[$i]:-}"
    local revs="${_BASHUNIT_BENCH_REVS[$i]:-}"
    local its="${_BASHUNIT_BENCH_ITS[$i]:-}"
    local avg="${_BASHUNIT_BENCH_AVERAGES[$i]:-}"
    local max_ms="${_BASHUNIT_BENCH_MAX_MILLIS[$i]:-}"

    if [ -z "$max_ms" ]; then
      printf '%-40s %6s %6s %10s\n' "$name" "$revs" "$its" "$avg"
      continue
    fi

    if [ "$avg" -le "$max_ms" ]; then
      local raw="≤ ${max_ms}"
      local padded
      padded=$(printf "%14s" "$raw")
      printf '%-40s %6s %6s %10s %12s\n' "$name" "$revs" "$its" "$avg" "$padded"
      continue
    fi

    local raw="> ${max_ms}"
    local padded
    padded=$(printf "%12s" "$raw")
    printf '%-40s %6s %6s %10s %s%s%s\n' \
      "$name" "$revs" "$its" "$avg" \
      "$_BASHUNIT_COLOR_FAILED" "$padded" "${_BASHUNIT_COLOR_DEFAULT}"
  done

  bashunit::console_results::print_execution_time
}

# bashunit.sh

# This file provides a facade to developers who wants
# to interact with the internals of bashunit.
# e.g. adding custom assertions

function bashunit::assertion_failed() {
  bashunit::assert::should_skip && return 0

  local expected=$1
  local actual=$2
  local failure_condition_message=${3:-"but got "}

  local test_fn
  test_fn="$(bashunit::helper::find_test_function_name)"
  local label
  label="$(bashunit::helper::normalize_test_function_name "$test_fn")"
  bashunit::assert::mark_failed
  bashunit::console_results::print_failed_test "${label}" "${expected}" \
    "$failure_condition_message" "${actual}"
}

function bashunit::assertion_passed() {
  bashunit::assert::should_skip && return 0

  bashunit::state::add_assertions_passed
}

# init.sh

function bashunit::init::project() {
  local tests_dir="${1:-$BASHUNIT_DEFAULT_PATH}"
  mkdir -p "$tests_dir"

  local bootstrap_file="$tests_dir/bootstrap.sh"
  if [ ! -f "$bootstrap_file" ]; then
    cat >"$bootstrap_file" <<'SH'
#!/opt/local/bin/bash
set -euo pipefail
# Place your common test setup here
SH
    chmod +x "$bootstrap_file"
    echo "> Created $bootstrap_file"
  fi

  local example_test="$tests_dir/example_test.sh"
  if [ ! -f "$example_test" ]; then
    cat >"$example_test" <<'SH'
#!/opt/local/bin/bash

function test_bashunit_is_installed() {
  assert_same "bashunit is installed" "bashunit is installed"
}
SH
    chmod +x "$example_test"
    echo "> Created $example_test"
  fi

  local workflow_dir=".github/workflows"
  local workflow_file="$workflow_dir/tests.yml"
  if [ ! -f "$workflow_file" ]; then
    mkdir -p "$workflow_dir"
    cat >"$workflow_file" <<SH
name: Tests
on: [pull_request, push]
jobs:
  tests:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: TypedDevs/bashunit@v0
        with:
          args: $tests_dir
SH
    echo "> Created $workflow_file"
  fi

  local env_file=".env"
  local env_line="BASHUNIT_BOOTSTRAP=$bootstrap_file"
  if [ -f "$env_file" ]; then
    if grep -q "^BASHUNIT_BOOTSTRAP=" "$env_file"; then
      if bashunit::check_os::is_macos; then
        sed -i '' -e "s/^BASHUNIT_BOOTSTRAP=/#&/" "$env_file"
      else
        sed -i -e "s/^BASHUNIT_BOOTSTRAP=/#&/" "$env_file"
      fi
    fi
    echo "$env_line" >>"$env_file"
  else
    echo "$env_line" >"$env_file"
  fi

  echo "> bashunit initialized in $tests_dir"
}

# learn.sh
# shellcheck disable=SC2016

##
# Interactive learning module for bashunit
# Provides guided tutorials and exercises to learn bashunit
##

LEARN_TEMP_DIR=""
declare -r LEARN_PROGRESS_FILE="$HOME/.bashunit_learn_progress"

##
# Initialize learning environment
##
function bashunit::learn::init() {
  LEARN_TEMP_DIR=$("${MKTEMP:-mktemp}" -d "${TMPDIR:-/tmp}/bashunit_learn.XXXXXXXX")
  mkdir -p tests
}

##
# Cleanup learning environment
##
function bashunit::learn::cleanup() {
  if [ -n "${LEARN_TEMP_DIR:-}" ] && [ -d "$LEARN_TEMP_DIR" ]; then
    rm -rf "$LEARN_TEMP_DIR"
  fi
}

##
# Print the learning menu
##
function bashunit::learn::print_menu() {
  cat <<EOF
${_BASHUNIT_COLOR_BOLD}${_BASHUNIT_COLOR_PASSED}bashunit${_BASHUNIT_COLOR_DEFAULT} - Interactive Learning

Choose a lesson:

  ${_BASHUNIT_COLOR_BOLD}1.${_BASHUNIT_COLOR_DEFAULT} Basics - Your First Test
  ${_BASHUNIT_COLOR_BOLD}2.${_BASHUNIT_COLOR_DEFAULT} Assertions - Testing Different Conditions
  ${_BASHUNIT_COLOR_BOLD}3.${_BASHUNIT_COLOR_DEFAULT} Setup & Teardown - Managing Test Lifecycle
  ${_BASHUNIT_COLOR_BOLD}4.${_BASHUNIT_COLOR_DEFAULT} Testing Functions - Unit Testing Patterns
  ${_BASHUNIT_COLOR_BOLD}5.${_BASHUNIT_COLOR_DEFAULT} Testing Scripts - Integration Testing
  ${_BASHUNIT_COLOR_BOLD}6.${_BASHUNIT_COLOR_DEFAULT} Mocking - Test Doubles and Mocks
  ${_BASHUNIT_COLOR_BOLD}7.${_BASHUNIT_COLOR_DEFAULT} Spies - Verifying Function Calls
  ${_BASHUNIT_COLOR_BOLD}8.${_BASHUNIT_COLOR_DEFAULT} Data Providers - Parameterized Tests
  ${_BASHUNIT_COLOR_BOLD}9.${_BASHUNIT_COLOR_DEFAULT} Exit Codes - Testing Success and Failure
  ${_BASHUNIT_COLOR_BOLD}10.${_BASHUNIT_COLOR_DEFAULT} Complete Challenge - Real World Scenario

  ${_BASHUNIT_COLOR_BOLD}p.${_BASHUNIT_COLOR_DEFAULT} Show Progress
  ${_BASHUNIT_COLOR_BOLD}r.${_BASHUNIT_COLOR_DEFAULT} Reset Progress
  ${_BASHUNIT_COLOR_BOLD}q.${_BASHUNIT_COLOR_DEFAULT} Quit

Enter your choice:
EOF
}

##
# Main learning loop
##
function bashunit::learn::start() {
  bashunit::learn::init

  trap 'bashunit::learn::cleanup' EXIT

  while true; do
    echo ""
    bashunit::learn::print_menu
    read -r choice
    echo ""

    case "$choice" in
    1) bashunit::learn::lesson_basics || true ;;
    2) bashunit::learn::lesson_assertions || true ;;
    3) bashunit::learn::lesson_lifecycle || true ;;
    4) bashunit::learn::lesson_functions || true ;;
    5) bashunit::learn::lesson_scripts || true ;;
    6) bashunit::learn::lesson_mocking || true ;;
    7) bashunit::learn::lesson_spies || true ;;
    8) bashunit::learn::lesson_data_providers || true ;;
    9) bashunit::learn::lesson_exit_codes || true ;;
    10) bashunit::learn::lesson_challenge || true ;;
    p) bashunit::learn::show_progress ;;
    r) bashunit::learn::reset_progress ;;
    q)
      echo "${_BASHUNIT_COLOR_PASSED}Happy testing!${_BASHUNIT_COLOR_DEFAULT}"
      break
      ;;
    *)
      echo "${_BASHUNIT_COLOR_FAILED}Invalid choice. Please try again.${_BASHUNIT_COLOR_DEFAULT}"
      ;;
    esac
  done

  bashunit::learn::cleanup
}

##
# Mark lesson as completed
##
function bashunit::learn::mark_completed() {
  local lesson=$1
  echo "$lesson" >>"$LEARN_PROGRESS_FILE"
}

##
# Check if lesson is completed
##
function bashunit::learn::is_completed() {
  local lesson=$1
  [ -f "$LEARN_PROGRESS_FILE" ] && [ "$("$GREP" -c "^$lesson$" "$LEARN_PROGRESS_FILE" || true)" -gt 0 ]
}

##
# Show learning progress
##
function bashunit::learn::show_progress() {
  if [ ! -f "$LEARN_PROGRESS_FILE" ]; then
    echo "${_BASHUNIT_COLOR_INCOMPLETE}No progress yet. Start with lesson 1!${_BASHUNIT_COLOR_DEFAULT}"
    return
  fi

  echo "${_BASHUNIT_COLOR_BOLD}Your Progress:${_BASHUNIT_COLOR_DEFAULT}"
  echo ""

  local total_lessons=10
  local completed=0

  local i
  for i in $(seq 1 $total_lessons); do
    if bashunit::learn::is_completed "lesson_$i"; then
      echo "  ${_BASHUNIT_COLOR_PASSED}✓${_BASHUNIT_COLOR_DEFAULT} Lesson $i completed"
      ((++completed)) || true
    else
      echo "  ${_BASHUNIT_COLOR_INCOMPLETE}○${_BASHUNIT_COLOR_DEFAULT} Lesson $i"
    fi
  done

  echo ""
  echo "Progress: $completed/$total_lessons lessons completed"

  if [ $completed -eq $total_lessons ]; then
    echo ""
    printf "%s%s🎉 Congratulations! You've completed all lessons!%s\n" \
      "$_BASHUNIT_COLOR_PASSED" "$_BASHUNIT_COLOR_BOLD" "$_BASHUNIT_COLOR_DEFAULT"
  fi

  read -p "Press Enter to continue..." -r
}

##
# Reset learning progress
##
function bashunit::learn::reset_progress() {
  rm -f "$LEARN_PROGRESS_FILE"
  echo "${_BASHUNIT_COLOR_PASSED}Progress reset successfully.${_BASHUNIT_COLOR_DEFAULT}"
  read -p "Press Enter to continue..." -r
}

##
# Create the example file automatically
# Arguments: $1 - filename, $2 - file content
##
function bashunit::learn::create_example_file() {
  local filename=$1
  local content=$2

  echo ""
  echo "Creating example file ${_BASHUNIT_COLOR_BOLD}$filename${_BASHUNIT_COLOR_DEFAULT}..."
  echo "$content" >"$filename"
  chmod +x "$filename"
  echo "${_BASHUNIT_COLOR_PASSED}✓ Created $filename${_BASHUNIT_COLOR_DEFAULT}"
  echo ""
  echo "File created! Edit it to complete the TODO items, then run this lesson again."
  read -p "Press Enter to continue..." -r
  return 0
}

##
# Run a lesson test and check results
##
function bashunit::learn::run_lesson_test() {
  local test_file=$1
  local lesson_number=$2

  echo "${_BASHUNIT_COLOR_BOLD}Running your test...${_BASHUNIT_COLOR_DEFAULT}"
  echo ""

  if "$BASHUNIT_ROOT_DIR/bashunit" "$test_file" --simple; then
    echo ""
    printf "%s%s✓ Excellent! Lesson %s completed!%s\n" \
      "$_BASHUNIT_COLOR_PASSED" "$_BASHUNIT_COLOR_BOLD" "$lesson_number" "$_BASHUNIT_COLOR_DEFAULT"
    bashunit::learn::mark_completed "lesson_$lesson_number"
    read -p "Press Enter to continue..." -r
    return 0
  else
    echo ""
    echo "${_BASHUNIT_COLOR_FAILED}Not quite right. Review the requirements and try again.${_BASHUNIT_COLOR_DEFAULT}"
    read -p "Press Enter to continue..." -r
    return 1
  fi
}

##
# Lesson 1: Basics - Your First Test
##
function bashunit::learn::lesson_basics() {
  clear
  cat <<'EOF'
╔════════════════════════════════════════════════════════════════╗
║                    Lesson 1: Your First Test                   ║
╚════════════════════════════════════════════════════════════════╝

Welcome to bashunit! Let's write your first test.

CONCEPT: A test is a function that starts with 'test_' and uses
assertions to verify behavior.

TASK: Create a test file that checks if two values are equal.

File: tests/first_test.sh
───────────────────────────────────────────────────────────────
#!/opt/local/bin/bash

function test_bashunit_works() {
  # TODO: Use assert_same to check if "hello" equals "hello"
  # Hint: assert_same "expected" "actual"
}
───────────────────────────────────────────────────────────────

TIPS:
  • The assert_same function takes two arguments:
    assert_same "expected" "actual"
  • Test functions must start with "test_" prefix
  • Always quote your strings to avoid word splitting
  • Keep test files in a tests/ directory for better organization
EOF

  local default_file="tests/first_test.sh"
  echo ""
  printf "When ready, enter file path %s[%s]%s: " \
    "${_BASHUNIT_COLOR_FAINT}" "$default_file" "${_BASHUNIT_COLOR_DEFAULT}"
  read -r test_file
  test_file="${test_file:-$default_file}"

  if [ ! -f "$test_file" ]; then
    local template='#!/usr/bin/env bash

function test_bashunit_works() {
  # TODO: Use assert_same to check if "hello" equals "hello"
  # Hint: assert_same "expected" "actual"
}'

    bashunit::learn::create_example_file "$test_file" "$template"
    return 1
  fi

  # Check if file contains assert_same
  if [ "$("$GREP" -c "assert_same" "$test_file" || true)" -eq 0 ]; then
    echo "${_BASHUNIT_COLOR_FAILED}Your test should use assert_same${_BASHUNIT_COLOR_DEFAULT}"
    read -p "Press Enter to continue..." -r
    return 1
  fi

  bashunit::learn::run_lesson_test "$test_file" 1
}

##
# Lesson 2: Assertions - Testing Different Conditions
##
function bashunit::learn::lesson_assertions() {
  clear
  cat <<'EOF'
╔════════════════════════════════════════════════════════════════╗
║              Lesson 2: Testing Different Conditions            ║
╚════════════════════════════════════════════════════════════════╝

CONCEPT: bashunit provides many assertion functions for different checks:
  • assert_same - exact equality
  • assert_contains - substring check
  • assert_matches - regex pattern
  • assert_not_same - inequality
  • assert_empty - checks if value is empty
  • assert_not_empty - checks if value is not empty

TASK: Write a test file with 3 different assertions.

File: tests/assertions_test.sh
───────────────────────────────────────────────────────────────
#!/opt/local/bin/bash

function test_multiple_assertions() {
  local message="Hello, bashunit!"

  # TODO: Check that message contains "bashunit"
  # Hint: assert_contains "substring" "$message"

  # TODO: Check that message matches the pattern "Hello.*!"
  # Hint: assert_matches "pattern" "$message"

  # TODO: Check that message is not empty
  # Hint: assert_not_empty "$message"
}
───────────────────────────────────────────────────────────────

TIPS:
  • assert_same checks exact equality (useful for strings/numbers)
  • assert_contains is more flexible for partial matches
  • assert_matches uses regex patterns (e.g., "^[0-9]+$" for numbers)
  • Explore more: assert_empty, assert_true, assert_false
EOF

  local default_file="tests/assertions_test.sh"
  echo ""
  printf "When ready, enter file path %s[%s]%s: " \
    "${_BASHUNIT_COLOR_FAINT}" "$default_file" "${_BASHUNIT_COLOR_DEFAULT}"
  read -r test_file
  test_file="${test_file:-$default_file}"

  if [ ! -f "$test_file" ]; then
    local template='#!/usr/bin/env bash

function test_multiple_assertions() {
  local message="Hello, bashunit!"

  # TODO: Check that message contains "bashunit"
  # Hint: assert_contains "substring" "$message"

  # TODO: Check that message matches the pattern "Hello.*!"
  # Hint: assert_matches "pattern" "$message"

  # TODO: Check that message is not empty
  # Hint: assert_not_empty "$message"
}'

    bashunit::learn::create_example_file "$test_file" "$template"
    return 1
  fi

  if [ "$("$GREP" -c "assert_contains" "$test_file" || true)" -eq 0 ] ||
    [ "$("$GREP" -c "assert_matches" "$test_file" || true)" -eq 0 ] ||
    [ "$("$GREP" -c "assert_not_empty" "$test_file" || true)" -eq 0 ]; then
    echo "${_BASHUNIT_COLOR_FAILED}Your test should use all three assertion types${_BASHUNIT_COLOR_DEFAULT}"
    read -p "Press Enter to continue..." -r
    return 1
  fi

  bashunit::learn::run_lesson_test "$test_file" 2
}

##
# Lesson 3: Setup & Teardown - Managing Test Lifecycle
##
function bashunit::learn::lesson_lifecycle() {
  clear
  cat <<'EOF'
╔════════════════════════════════════════════════════════════════╗
║           Lesson 3: Setup and Teardown Functions               ║
╚════════════════════════════════════════════════════════════════╝

CONCEPT: Tests often need preparation and cleanup. bashunit provides:
  • set_up() - runs before EACH test
  • tear_down() - runs after EACH test
  • set_up_before_script() - runs once before ALL tests
  • tear_down_after_script() - runs once after ALL tests

TASK: Create a test that uses setup and teardown to manage files.

File: tests/lifecycle_test.sh
───────────────────────────────────────────────────────────────
#!/opt/local/bin/bash

function set_up() {
  # Create a temp file before each test
  # TODO: export TEST_FILE="/tmp/test_$$"
  # TODO: echo "test content" > "$TEST_FILE"
}

function tear_down() {
  # Clean up after each test
  # TODO: rm -f "$TEST_FILE"
}

function test_file_exists() {
  # TODO: assert_file_exists "$TEST_FILE"
}

function test_file_has_content() {
  # TODO: assert_file_contains "test content" "$TEST_FILE"
}
───────────────────────────────────────────────────────────────

TIPS:
  • set_up() runs before EACH test (good for test isolation)
  • set_up_before_script() runs ONCE before all tests (good for expensive setup)
  • Always clean up in tear_down() to avoid polluting other tests
  • Use $$ for unique temp file names to avoid conflicts
EOF

  local default_file="tests/lifecycle_test.sh"
  echo ""
  printf "When ready, enter file path %s[%s]%s: " \
    "${_BASHUNIT_COLOR_FAINT}" "$default_file" "${_BASHUNIT_COLOR_DEFAULT}"
  read -r test_file
  test_file="${test_file:-$default_file}"

  if [ ! -f "$test_file" ]; then
    local template='#!/usr/bin/env bash

function set_up() {
  # Create a temp file before each test
  # TODO: export TEST_FILE="/tmp/test_$$"
  # TODO: echo "test content" > "$TEST_FILE"
}

function tear_down() {
  # Clean up after each test
  # TODO: rm -f "$TEST_FILE"
}

function test_file_exists() {
  # TODO: assert_file_exists "$TEST_FILE"
}

function test_file_has_content() {
  # TODO: assert_file_contains "test content" "$TEST_FILE"
}'

    bashunit::learn::create_example_file "$test_file" "$template"
    return 1
  fi

  if [ "$("$GREP" -c "function set_up()" "$test_file" || true)" -eq 0 ] ||
    [ "$("$GREP" -c "function tear_down()" "$test_file" || true)" -eq 0 ]; then
    echo "${_BASHUNIT_COLOR_FAILED}Your test should define set_up and tear_down functions${_BASHUNIT_COLOR_DEFAULT}"
    read -p "Press Enter to continue..." -r
    return 1
  fi

  bashunit::learn::run_lesson_test "$test_file" 3
}

##
# Lesson 4: Testing Functions
##
function bashunit::learn::lesson_functions() {
  clear
  cat <<'EOF'
╔════════════════════════════════════════════════════════════════╗
║              Lesson 4: Testing Bash Functions                  ║
╚════════════════════════════════════════════════════════════════╝

CONCEPT: To test functions, source the file containing them, then
call them in your tests.

TASK: Create a script with a function, then test it.

File: calculator.sh (source code)
───────────────────────────────────────────────────────────────
#!/opt/local/bin/bash

function add() {
  echo $(($1 + $2))
}
───────────────────────────────────────────────────────────────

File: tests/calculator_test.sh (test file)
───────────────────────────────────────────────────────────────
#!/opt/local/bin/bash

function set_up() {
  # TODO: Source calculator.sh from parent directory
  # Hint: source ../calculator.sh
}

function test_add_positive_numbers() {
  # TODO: Test that add 2 3 returns "5"
  # Hint: result=$(add 2 3)
  # Hint: assert_same "5" "$result"
}

function test_add_negative_numbers() {
  # TODO: Test that add -2 -3 returns "-5"
  # Hint: result=$(add -2 -3)
  # Hint: assert_same "-5" "$result"
}
───────────────────────────────────────────────────────────────

TIPS:
  • Source files in set_up() to reload them fresh for each test
  • Capture function output with: result=$(function_name args)
  • Test edge cases: positive, negative, zero, large numbers
  • Source files from parent directory: source ../file.sh
EOF

  local default_file="tests/calculator_test.sh"
  echo ""
  printf "When ready, enter TEST file path %s[%s]%s: " \
    "${_BASHUNIT_COLOR_FAINT}" "$default_file" "${_BASHUNIT_COLOR_DEFAULT}"
  read -r test_file
  test_file="${test_file:-$default_file}"

  if [ ! -f "$test_file" ]; then
    local template='#!/usr/bin/env bash

function set_up() {
  # TODO: Source calculator.sh from parent directory
  # Hint: source ../calculator.sh
}

function test_add_positive_numbers() {
  # TODO: Test that add 2 3 returns "5"
  # Hint: result=$(add 2 3)
  # Hint: assert_same "5" "$result"
}

function test_add_negative_numbers() {
  # TODO: Test that add -2 -3 returns "-5"
  # Hint: result=$(add -2 -3)
  # Hint: assert_same "-5" "$result"
}'

    bashunit::learn::create_example_file "$test_file" "$template"
    return 1
  fi

  if [ "$("$GREP" -c "source" "$test_file" || true)" -eq 0 ]; then
    echo "${_BASHUNIT_COLOR_FAILED}Your test should source the calculator.sh file${_BASHUNIT_COLOR_DEFAULT}"
    read -p "Press Enter to continue..." -r
    return 1
  fi

  bashunit::learn::run_lesson_test "$test_file" 4
}

##
# Lesson 5: Testing Scripts
##
function bashunit::learn::lesson_scripts() {
  clear
  cat <<'EOF'
╔════════════════════════════════════════════════════════════════╗
║                 Lesson 5: Testing Bash Scripts                 ║
╚════════════════════════════════════════════════════════════════╝

CONCEPT: Scripts that execute commands directly are tested differently.
Run them and capture their output.

TASK: Create a script and test its output.

File: greeter.sh (source code)
───────────────────────────────────────────────────────────────
#!/opt/local/bin/bash
name=${1:-World}
echo "Hello, $name!"
───────────────────────────────────────────────────────────────

File: tests/greeter_test.sh (test file)
───────────────────────────────────────────────────────────────
#!/opt/local/bin/bash

function test_default_greeting() {
  # TODO: Run greeter.sh from parent directory and capture output
  # Hint: output=$(../greeter.sh)

  # TODO: Assert output contains "Hello, World!"
  # Hint: assert_contains "Hello, World!" "$output"
}

function test_custom_greeting() {
  # TODO: Run greeter.sh with argument "Alice"
  # Hint: output=$(../greeter.sh "Alice")

  # TODO: Assert output contains "Hello, Alice!"
  # Hint: assert_contains "Hello, Alice!" "$output"
}
───────────────────────────────────────────────────────────────

TIPS:
  • Use command substitution: output=$(./script.sh)
  • Make scripts executable: chmod +x script.sh
  • Test both default behavior and with various arguments
  • Scripts run in subshells, so they can't modify parent environment
  • Run scripts from parent directory: ../script.sh
EOF

  local default_file="tests/greeter_test.sh"
  echo ""
  printf "When ready, enter TEST file path %s[%s]%s: " \
    "${_BASHUNIT_COLOR_FAINT}" "$default_file" "${_BASHUNIT_COLOR_DEFAULT}"
  read -r test_file
  test_file="${test_file:-$default_file}"

  if [ ! -f "$test_file" ]; then
    local template='#!/usr/bin/env bash

function test_default_greeting() {
  # TODO: Run greeter.sh from parent directory and capture output
  # Hint: output=$(../greeter.sh)

  # TODO: Assert output contains "Hello, World!"
  # Hint: assert_contains "Hello, World!" "$output"
}

function test_custom_greeting() {
  # TODO: Run greeter.sh with argument "Alice"
  # Hint: output=$(../greeter.sh "Alice")

  # TODO: Assert output contains "Hello, Alice!"
  # Hint: assert_contains "Hello, Alice!" "$output"
}'

    bashunit::learn::create_example_file "$test_file" "$template"
    return 1
  fi

  bashunit::learn::run_lesson_test "$test_file" 5
}

##
# Lesson 6: Mocking
##
function bashunit::learn::lesson_mocking() {
  clear
  cat <<'EOF'
╔════════════════════════════════════════════════════════════════╗
║               Lesson 6: Mocking External Commands              ║
╚════════════════════════════════════════════════════════════════╝

CONCEPT: Mocks let you override external commands or functions to
control their behavior in tests.

TASK: Test a function that uses external commands.

File: system_info.sh (source code)
───────────────────────────────────────────────────────────────
#!/opt/local/bin/bash

function get_system_info() {
  echo "OS: $(uname -s)"
}
───────────────────────────────────────────────────────────────

File: tests/system_info_test.sh (test file)
───────────────────────────────────────────────────────────────
#!/opt/local/bin/bash

function set_up() {
  source ../system_info.sh
}

function test_system_info_on_linux() {
  # TODO: Mock uname to return "Linux"
  # Hint: mock uname echo "Linux"

  local output
  output=$(get_system_info)

  # TODO: Assert output contains "OS: Linux"
}

function test_system_info_on_macos() {
  # TODO: Mock uname to return "Darwin"

  local output
  output=$(get_system_info)

  # TODO: Assert output contains "OS: Darwin"
}
───────────────────────────────────────────────────────────────

TIPS:
  • Mocks replace commands/functions with custom behavior
  • Syntax: mock command_name echo "mocked output"
  • Mocks are automatically cleaned up after each test
  • Use mocks to avoid calling expensive external commands
EOF

  local default_file="tests/system_info_test.sh"
  echo ""
  printf "When ready, enter TEST file path %s[%s]%s: " \
    "${_BASHUNIT_COLOR_FAINT}" "$default_file" "${_BASHUNIT_COLOR_DEFAULT}"
  read -r test_file
  test_file="${test_file:-$default_file}"

  if [ ! -f "$test_file" ]; then
    local template='#!/usr/bin/env bash

function set_up() {
  source ../system_info.sh
}

function test_system_info_on_linux() {
  # TODO: Mock uname to return "Linux"
  # Hint: mock uname echo "Linux"

  local output
  output=$(get_system_info)

  # TODO: Assert output contains "OS: Linux"
}

function test_system_info_on_macos() {
  # TODO: Mock uname to return "Darwin"

  local output
  output=$(get_system_info)

  # TODO: Assert output contains "OS: Darwin"
}'

    bashunit::learn::create_example_file "$test_file" "$template"
    return 1
  fi

  if [ "$("$GREP" -c "mock" "$test_file" || true)" -eq 0 ]; then
    echo "${_BASHUNIT_COLOR_FAILED}Your test should use mock${_BASHUNIT_COLOR_DEFAULT}"
    read -p "Press Enter to continue..." -r
    return 1
  fi

  bashunit::learn::run_lesson_test "$test_file" 6
}

##
# Lesson 7: Spies
##
function bashunit::learn::lesson_spies() {
  clear
  cat <<'EOF'
╔════════════════════════════════════════════════════════════════╗
║              Lesson 7: Spies - Verifying Calls                 ║
╚════════════════════════════════════════════════════════════════╝

CONCEPT: Spies let you verify that functions were called with specific
arguments or a certain number of times.

KEY DIFFERENCE: Spies track calls without changing behavior, while
mocks (Lesson 6) replace the function entirely with custom behavior.

TASK: Use spies to verify function calls.

File: deploy.sh
───────────────────────────────────────────────────────────────
#!/opt/local/bin/bash

function deploy_app() {
  git push origin main
  docker build -t myapp .
  docker push myapp
}
───────────────────────────────────────────────────────────────

File: deploy_test.sh
───────────────────────────────────────────────────────────────
#!/opt/local/bin/bash

function set_up() {
  source deploy.sh
}

function test_deploy_calls_git_push() {
  # TODO: Create spies for git and docker
  # Hint: spy git
  # Hint: spy docker

  deploy_app

  # TODO: Assert git was called
  # Hint: assert_have_been_called git

  # TODO: Assert docker was called
}

function test_deploy_calls_docker_twice() {
  # TODO: Spy on docker

  deploy_app

  # TODO: Assert docker was called exactly 2 times
  # Hint: assert_have_been_called_times 2 docker
}
───────────────────────────────────────────────────────────────

TIPS:
  • Spies track calls but don't change behavior (unlike mocks)
  • assert_have_been_called - verifies at least one call
  • assert_have_been_called_times N - verifies exact call count
  • assert_have_been_called_with - verifies specific arguments
  • Spies are cleaned up automatically after each test
EOF

  local default_file="deploy_test.sh"
  echo ""
  printf "When ready, enter TEST file path %s[%s]%s: " \
    "${_BASHUNIT_COLOR_FAINT}" "$default_file" "${_BASHUNIT_COLOR_DEFAULT}"
  read -r test_file
  test_file="${test_file:-$default_file}"

  if [ ! -f "$test_file" ]; then
    local template='#!/usr/bin/env bash

function set_up() {
  source deploy.sh
}

function test_deploy_calls_git_push() {
  # TODO: Create spies for git and docker
  # Hint: spy git
  # Hint: spy docker

  deploy_app

  # TODO: Assert git was called
  # Hint: assert_have_been_called git

  # TODO: Assert docker was called
}

function test_deploy_calls_docker_twice() {
  # TODO: Spy on docker

  deploy_app

  # TODO: Assert docker was called exactly 2 times
  # Hint: assert_have_been_called_times 2 docker
}'

    bashunit::learn::create_example_file "$test_file" "$template"
    return 1
  fi

  if [ "$("$GREP" -c "spy" "$test_file" || true)" -eq 0 ]; then
    echo "${_BASHUNIT_COLOR_FAILED}Your test should use spy${_BASHUNIT_COLOR_DEFAULT}"
    read -p "Press Enter to continue..." -r
    return 1
  fi

  bashunit::learn::run_lesson_test "$test_file" 7
}

##
# Lesson 8: Data Providers
##
function bashunit::learn::lesson_data_providers() {
  clear
  cat <<'EOF'
╔════════════════════════════════════════════════════════════════╗
║           Lesson 8: Data Providers - Parameterized Tests       ║
╚════════════════════════════════════════════════════════════════╝

CONCEPT: Data providers let you run the same test with different inputs.
Define a function that echoes test data, one per line.

HOW IT WORKS: Each line from data_provider_* becomes $1 in your test.
The test runs once for each line of data.

TASK: Test multiple email formats using a data provider.

File: validator.sh
───────────────────────────────────────────────────────────────
#!/opt/local/bin/bash

function is_valid_email() {
  local email_pattern='^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$'
  [ "$(echo "$1" | "$GREP" -cE "$email_pattern" || true)" -gt 0 ]
}
───────────────────────────────────────────────────────────────

File: validator_test.sh
───────────────────────────────────────────────────────────────
#!/opt/local/bin/bash

function set_up() {
  source validator.sh
}

function data_provider_valid_emails() {
  # TODO: Echo valid email addresses, one per line
  # Example: echo "user@example.com"
}

function test_valid_emails() {
  # $1 contains the email from data provider
  # TODO: Assert is_valid_email succeeds
  # Hint: assert_successful_code "is_valid_email \"$1\""
}

function data_provider_invalid_emails() {
  # TODO: Echo invalid email addresses, one per line
  # Example: echo "not-an-email"
}

function test_invalid_emails() {
  # TODO: Assert is_valid_email fails
  # Hint: assert_general_error "is_valid_email \"$1\""
}
───────────────────────────────────────────────────────────────

TIPS:
  • Data providers must be named: data_provider_<test_name>
  • Each line of output becomes one test case
  • The test function receives the line as $1
  • Great for testing multiple inputs without duplicating code
  • You can have multiple data provider/test pairs in one file
EOF

  local default_file="validator_test.sh"
  echo ""
  printf "When ready, enter TEST file path %s[%s]%s: " \
    "${_BASHUNIT_COLOR_FAINT}" "$default_file" "${_BASHUNIT_COLOR_DEFAULT}"
  read -r test_file
  test_file="${test_file:-$default_file}"

  if [ ! -f "$test_file" ]; then
    local template='#!/usr/bin/env bash

function set_up() {
  source validator.sh
}

function data_provider_valid_emails() {
  # TODO: Echo valid email addresses, one per line
  # Example: echo "user@example.com"
}

function test_valid_emails() {
  # $1 contains the email from data provider
  # TODO: Assert is_valid_email succeeds
  # Hint: assert_successful_code "is_valid_email \"$1\""
}

function data_provider_invalid_emails() {
  # TODO: Echo invalid email addresses, one per line
  # Example: echo "not-an-email"
}

function test_invalid_emails() {
  # TODO: Assert is_valid_email fails
  # Hint: assert_general_error "is_valid_email \"$1\""
}'

    bashunit::learn::create_example_file "$test_file" "$template"
    return 1
  fi

  if [ "$("$GREP" -c "function data_provider_" "$test_file" || true)" -eq 0 ]; then
    echo "${_BASHUNIT_COLOR_FAILED}Your test should define data provider functions${_BASHUNIT_COLOR_DEFAULT}"
    read -p "Press Enter to continue..." -r
    return 1
  fi

  bashunit::learn::run_lesson_test "$test_file" 8
}

##
# Lesson 9: Exit Codes
##
function bashunit::learn::lesson_exit_codes() {
  clear
  cat <<'EOF'
╔════════════════════════════════════════════════════════════════╗
║             Lesson 9: Testing Exit Codes                       ║
╚════════════════════════════════════════════════════════════════╝

CONCEPT: Exit codes indicate success (0) or failure (non-zero).
bashunit provides assertions to test them:
  • assert_successful_code - expects exit code 0
  • assert_general_error - expects exit code 1
  • assert_exit_code N - expects specific exit code N

TASK: Test different exit codes.

File: checker.sh
───────────────────────────────────────────────────────────────
#!/opt/local/bin/bash

function check_file() {
  if [ ! -e "$1" ]; then
    echo "File not found" >&2
    return 127
  fi

  if [ ! -r "$1" ]; then
    echo "Permission denied" >&2
    return 1
  fi

  echo "File OK"
  return 0
}
───────────────────────────────────────────────────────────────

File: checker_test.sh
───────────────────────────────────────────────────────────────
#!/opt/local/bin/bash

function set_up() {
  source checker.sh
  # Create a test file
  export TEST_FILE="/tmp/test_file_$$"
  touch "$TEST_FILE"
}

function tear_down() {
  rm -f "$TEST_FILE"
}

function test_existing_file_returns_success() {
  # TODO: Assert check_file succeeds with TEST_FILE
  # Hint: assert_successful_code "check_file '$TEST_FILE'"
}

function test_missing_file_returns_127() {
  # TODO: Assert check_file returns exit code 127 for missing file
  # Hint: assert_exit_code 127 "check_file '/nonexistent/file'"
}
───────────────────────────────────────────────────────────────

TIPS:
  • Exit code 0 = success (assert_successful_code)
  • Exit code 1 = general error (assert_general_error)
  • Other codes = specific errors (assert_exit_code N)
  • Bash uses 'return N' in functions, 'exit N' in scripts
  • Common codes: 127=not found, 126=not executable, 2=misuse
EOF

  local default_file="checker_test.sh"
  echo ""
  printf "When ready, enter TEST file path %s[%s]%s: " \
    "${_BASHUNIT_COLOR_FAINT}" "$default_file" "${_BASHUNIT_COLOR_DEFAULT}"
  read -r test_file
  test_file="${test_file:-$default_file}"

  if [ ! -f "$test_file" ]; then
    local template='#!/usr/bin/env bash

function set_up() {
  source checker.sh
  # Create a test file
  export TEST_FILE="/tmp/test_file_$$"
  touch "$TEST_FILE"
}

function tear_down() {
  rm -f "$TEST_FILE"
}

function test_existing_file_returns_success() {
  # TODO: Assert check_file succeeds with TEST_FILE
  # Hint: assert_successful_code "check_file '\''$TEST_FILE'\''"
}

function test_missing_file_returns_127() {
  # TODO: Assert check_file returns exit code 127 for missing file
  # Hint: assert_exit_code 127 "check_file '\''/nonexistent/file'\''"
}'

    bashunit::learn::create_example_file "$test_file" "$template"
    return 1
  fi

  local _exit_assert_pattern="assert_successful_code\|assert_exit_code\|assert_general_error"
  if [ "$("$GREP" -c "$_exit_assert_pattern" "$test_file" || true)" -eq 0 ]; then
    echo "${_BASHUNIT_COLOR_FAILED}Your test should use exit code assertions${_BASHUNIT_COLOR_DEFAULT}"
    read -p "Press Enter to continue..." -r
    return 1
  fi

  bashunit::learn::run_lesson_test "$test_file" 9
}

##
# Lesson 10: Complete Challenge
##
function bashunit::learn::lesson_challenge() {
  clear
  cat <<'EOF'
╔════════════════════════════════════════════════════════════════╗
║          Lesson 10: Complete Challenge - Backup Script         ║
╚════════════════════════════════════════════════════════════════╝

FINAL CHALLENGE: Combine everything you've learned!

CONCEPT: Real-world tests combine multiple concepts: lifecycle
management, assertions, exit codes, and test doubles.

TASK: Create a backup script and comprehensive tests.

File: backup.sh
───────────────────────────────────────────────────────────────
#!/opt/local/bin/bash

function create_backup() {
  local source=$1
  local dest=$2

  if [ ! -d "$source" ]; then
    echo "Source directory not found" >&2
    return 1
  fi

  tar -czf "$dest" -C "$source" .
  echo "Backup created: $dest"
}
───────────────────────────────────────────────────────────────

File: backup_test.sh
───────────────────────────────────────────────────────────────
#!/opt/local/bin/bash

Your test must include:
  1. set_up and tear_down functions
  2. Test successful backup creation
  3. Test failure when source doesn't exist
  4. Mock or spy on tar command
  5. Verify backup file exists
  6. Check output message

TIP: Combine patterns from all previous lessons!
EOF

  local default_file="backup_test.sh"
  echo ""
  printf "When ready, enter TEST file path %s[%s]%s: " \
    "${_BASHUNIT_COLOR_FAINT}" "$default_file" "${_BASHUNIT_COLOR_DEFAULT}"
  read -r test_file
  test_file="${test_file:-$default_file}"

  if [ ! -f "$test_file" ]; then
    local template='#!/usr/bin/env bash

function set_up() {
  source backup.sh
  # TODO: Create test directories and variables
}

function tear_down() {
  # TODO: Clean up test files
}

function test_successful_backup() {
  # TODO: Test backup creation
}

function test_backup_failure_when_source_missing() {
  # TODO: Test failure case
}

# Add more tests as needed:
# - Mock or spy on tar command
# - Verify backup file exists
# - Check output message
#
# TIPS:
# - Combine lifecycle (set_up/tear_down) with file assertions
# - Use spies to verify tar was called correctly
# - Test both success and failure scenarios
# - Mock external commands to avoid side effects'

    bashunit::learn::create_example_file "$test_file" "$template"
    return 1
  fi

  # Verify the test has key components
  local -a missing_components=()
  local missing_components_count=0

  if [ "$("$GREP" -c "function set_up()" "$test_file" || true)" -eq 0 ]; then
    missing_components[missing_components_count]="set_up function"
    missing_components_count=$((missing_components_count + 1))
  fi

  if [ "$("$GREP" -c "function tear_down()" "$test_file" || true)" -eq 0 ]; then
    missing_components[missing_components_count]="tear_down function"
    missing_components_count=$((missing_components_count + 1))
  fi

  if [ "$missing_components_count" -gt 0 ]; then
    echo "${_BASHUNIT_COLOR_FAILED}Missing required components:${_BASHUNIT_COLOR_DEFAULT}"
    printf "  - %s\n" "${missing_components[@]}"
    read -p "Press Enter to continue..." -r
    return 1
  fi

  if bashunit::learn::run_lesson_test "$test_file" 10; then
    echo ""
    echo "${_BASHUNIT_COLOR_PASSED}${_BASHUNIT_COLOR_BOLD}"
    cat <<'EOF'
╔════════════════════════════════════════════════════════════════╗
║                   🎉 CONGRATULATIONS! 🎉                       ║
║                                                                ║
║          You've completed all bashunit lessons!                ║
║                                                                ║
║  You now know how to:                                          ║
║    ✓ Write and run tests                                       ║
║    ✓ Use various assertions                                    ║
║    ✓ Manage test lifecycle                                     ║
║    ✓ Test functions and scripts                                ║
║    ✓ Mock external dependencies                                ║
║    ✓ Spy on function calls                                     ║
║    ✓ Use data providers                                        ║
║    ✓ Test exit codes                                           ║
║                                                                ║
║  Next steps:                                                   ║
║    • Explore https://bashunit.com                              ║
║    • Check out /common-patterns for more examples              ║
║    • Start testing your own bash scripts!                      ║
╚════════════════════════════════════════════════════════════════╝
EOF
    echo "${_BASHUNIT_COLOR_DEFAULT}"
    read -p "Press Enter to continue..." -r
  fi
}

# main.sh

##
# Validates a `--shard <index>/<total>` spec and exports the parts, or prints an
# error and exits non-zero. Requires numeric index/total with 1 <= index <= total.
##
function bashunit::main::set_shard_or_exit() {
  local spec="${1:-}"
  local index total
  case "$spec" in
  */*)
    index="${spec%%/*}"
    total="${spec##*/}"
    ;;
  *)
    index=""
    total=""
    ;;
  esac
  case "$index" in '' | *[!0-9]*) index="" ;; esac
  case "$total" in '' | *[!0-9]*) total="" ;; esac

  if [ -z "$index" ] || [ -z "$total" ] ||
    [ "$total" -lt 1 ] || [ "$index" -lt 1 ] || [ "$index" -gt "$total" ]; then
    printf "%sError: --shard must be <index>/<total> with 1 <= index <= total (e.g. 1/4).%s\n" \
      "${_BASHUNIT_COLOR_FAILED}" "${_BASHUNIT_COLOR_DEFAULT}" >&2
    exit 1
  fi

  BASHUNIT_SHARD_INDEX="$index"
  export -n BASHUNIT_SHARD_INDEX
  BASHUNIT_SHARD_TOTAL="$total"
  export -n BASHUNIT_SHARD_TOTAL
}

#############################
# Subcommand: test
#############################
function bashunit::main::cmd_test() {
  local filter=""
  local tag_filter=""
  local exclude_tag_filter=""
  local IFS=$' \t\n'
  local -a raw_args=()
  local raw_args_count=0
  local -a args=()
  local args_count=0
  local assert_fn=""
  local _bashunit_coverage_opt_set=false

  # Parse test-specific options.
  #
  # Flag branches assign WITHOUT export and strip the export attribute with
  # `export -n`: run-mode flags are this-process-only. Everything that reads
  # them (runner, reporters, parallel workers) runs in this shell or its
  # subshells, which inherit unexported variables — while exec'd children
  # (nested bashunit runs: bashunit's own acceptance suite under
  # `build.sh --verify`, or a user's script under test that calls bashunit)
  # must NOT inherit the parent's flags (#834, #837). The explicit `export -n`
  # also clears an export attribute stamped by an allexport .env load.
  # Pair any newly exported-by-necessity flag with a comment naming the exec'd
  # consumer, and extend tests/acceptance/fixtures/flag_env_leak/leak_probe.sh.
  while [ $# -gt 0 ]; do
    case "$1" in
    -a | --assert)
      assert_fn="$2"
      shift
      ;;
    -f | --filter)
      filter="$2"
      shift
      ;;
    --tag)
      if [ -z "$tag_filter" ]; then
        tag_filter="$2"
      else
        tag_filter="$tag_filter,$2"
      fi
      shift
      ;;
    --exclude-tag)
      if [ -z "$exclude_tag_filter" ]; then
        exclude_tag_filter="$2"
      else
        exclude_tag_filter="$exclude_tag_filter,$2"
      fi
      shift
      ;;
    -s | --simple)
      BASHUNIT_SIMPLE_OUTPUT=true
      export -n BASHUNIT_SIMPLE_OUTPUT
      ;;
    --detailed)
      BASHUNIT_SIMPLE_OUTPUT=false
      export -n BASHUNIT_SIMPLE_OUTPUT
      ;;
    --output)
      BASHUNIT_OUTPUT_FORMAT="$2"
      export -n BASHUNIT_OUTPUT_FORMAT
      shift
      ;;
    --debug)
      local output_file="${2:-}"
      if [ -n "$output_file" ] && [ "${output_file:0:1}" != "-" ]; then
        exec >"$output_file" 2>&1
        shift
      fi
      set -x
      ;;
    -S | --stop-on-failure)
      # This-process-only: parallel stop uses a flag file and sync stop uses
      # exit codes, so no child process needs it — exported (including via an
      # allexport .env load, hence export -n) it leaked into nested bashunit
      # runs, aborting them before rerun::persist could write
      # .bashunit/last-failed (broke verify's acceptance tests, #834).
      BASHUNIT_STOP_ON_FAILURE=true
      export -n BASHUNIT_STOP_ON_FAILURE
      ;;
    -p | --parallel)
      BASHUNIT_PARALLEL_RUN=true
      export -n BASHUNIT_PARALLEL_RUN
      ;;
    -j | --jobs)
      BASHUNIT_PARALLEL_RUN=true
      export -n BASHUNIT_PARALLEL_RUN
      # "auto" caps at the detected core count; wait_for_job_slot needs an
      # integer, so resolve it here rather than leaking the string downstream.
      if [ "$2" = "auto" ]; then
        BASHUNIT_PARALLEL_JOBS="$(bashunit::check_os::nproc)"
        export -n BASHUNIT_PARALLEL_JOBS
      else
        BASHUNIT_PARALLEL_JOBS="$2"
        export -n BASHUNIT_PARALLEL_JOBS
      fi
      shift
      ;;
    --no-parallel)
      BASHUNIT_PARALLEL_RUN=false
      export -n BASHUNIT_PARALLEL_RUN
      ;;
    --test-timeout)
      BASHUNIT_TEST_TIMEOUT="$2"
      export -n BASHUNIT_TEST_TIMEOUT
      shift
      ;;
    --retry)
      BASHUNIT_RETRY="$2"
      export -n BASHUNIT_RETRY
      shift
      ;;
    --random-order)
      BASHUNIT_RANDOM_ORDER=true
      export -n BASHUNIT_RANDOM_ORDER
      ;;
    --seed)
      BASHUNIT_SEED="$2"
      export -n BASHUNIT_SEED
      shift
      ;;
    --shard)
      bashunit::main::set_shard_or_exit "$2"
      shift
      ;;
    --rerun-failed)
      BASHUNIT_RERUN_FAILED=true
      export -n BASHUNIT_RERUN_FAILED
      ;;
    -w | --watch)
      BASHUNIT_WATCH_MODE=true
      export -n BASHUNIT_WATCH_MODE
      ;;
    -e | --env | --boot)
      # Support: --env "bootstrap.sh arg1 arg2"
      local boot_file="${2%% *}"
      local boot_args="${2#* }"
      if [ "$boot_args" != "$2" ]; then
        BASHUNIT_BOOTSTRAP_ARGS="$boot_args"
        export -n BASHUNIT_BOOTSTRAP_ARGS
      fi
      # Export all variables from the env file so they're available in subshells
      # (e.g., process substitution used in load_test_files)
      set -o allexport
      # shellcheck disable=SC1090,SC2086
      source "$boot_file" ${BASHUNIT_BOOTSTRAP_ARGS:-}
      set +o allexport
      shift
      ;;
    # Report flags are this-process-only: reports are generated by the main
    # shell after aggregation, no child process reads these. `export -n` also
    # strips an export attribute inherited from an allexport .env load —
    # otherwise nested bashunit runs (bashunit's own acceptance tests, or a
    # user's scripts under test that call bashunit) silently write their own
    # reports over the parent's files and blow the per-run fork budget (#834).
    --log-junit | --report-junit)
      BASHUNIT_LOG_JUNIT="$2"
      export -n BASHUNIT_LOG_JUNIT
      shift
      ;;
    --log-gha)
      BASHUNIT_LOG_GHA="$2"
      export -n BASHUNIT_LOG_GHA
      shift
      ;;
    -r | --report-html)
      BASHUNIT_REPORT_HTML="$2"
      export -n BASHUNIT_REPORT_HTML
      shift
      ;;
    --report-tap)
      BASHUNIT_REPORT_TAP="$2"
      export -n BASHUNIT_REPORT_TAP
      shift
      ;;
    --report-json)
      BASHUNIT_REPORT_JSON="$2"
      export -n BASHUNIT_REPORT_JSON
      shift
      ;;
    --no-output)
      BASHUNIT_NO_OUTPUT=true
      export -n BASHUNIT_NO_OUTPUT
      ;;
    -vvv | --verbose)
      BASHUNIT_VERBOSE=true
      export -n BASHUNIT_VERBOSE
      ;;
    -h | --help)
      bashunit::console_header::print_test_help
      exit 0
      ;;
    --show-skipped)
      BASHUNIT_SHOW_SKIPPED=true
      export -n BASHUNIT_SHOW_SKIPPED
      ;;
    --show-incomplete)
      BASHUNIT_SHOW_INCOMPLETE=true
      export -n BASHUNIT_SHOW_INCOMPLETE
      ;;
    --failures-only)
      BASHUNIT_FAILURES_ONLY=true
      export -n BASHUNIT_FAILURES_ONLY
      ;;
    --fail-on-risky)
      BASHUNIT_FAIL_ON_RISKY=true
      export -n BASHUNIT_FAIL_ON_RISKY
      ;;
    --profile)
      BASHUNIT_PROFILE=true
      export -n BASHUNIT_PROFILE
      ;;
    --show-output)
      BASHUNIT_SHOW_OUTPUT_ON_FAILURE=true
      export -n BASHUNIT_SHOW_OUTPUT_ON_FAILURE
      ;;
    --no-output-on-failure)
      BASHUNIT_SHOW_OUTPUT_ON_FAILURE=false
      export -n BASHUNIT_SHOW_OUTPUT_ON_FAILURE
      ;;
    --no-progress)
      BASHUNIT_NO_PROGRESS=true
      export -n BASHUNIT_NO_PROGRESS
      ;;
    --strict)
      BASHUNIT_STRICT_MODE=true
      export -n BASHUNIT_STRICT_MODE
      ;;
    -R | --run-all)
      BASHUNIT_STOP_ON_ASSERTION_FAILURE=false
      export -n BASHUNIT_STOP_ON_ASSERTION_FAILURE
      ;;
    --skip-env-file)
      BASHUNIT_SKIP_ENV_FILE=true
      export -n BASHUNIT_SKIP_ENV_FILE
      ;;
    -l | --login)
      BASHUNIT_LOGIN_SHELL=true
      export -n BASHUNIT_LOGIN_SHELL
      ;;
    --no-color)
      # shellcheck disable=SC2034
      BASHUNIT_NO_COLOR=true
      ;;
    --coverage)
      # Don't export - prevents nested bashunit runs from inheriting coverage
      # shellcheck disable=SC2034
      BASHUNIT_COVERAGE=true
      ;;
    --coverage-paths)
      # shellcheck disable=SC2034
      BASHUNIT_COVERAGE_PATHS="$2"
      shift
      ;;
    --coverage-exclude)
      # shellcheck disable=SC2034
      BASHUNIT_COVERAGE_EXCLUDE="$2"
      shift
      ;;
    --coverage-report)
      # shellcheck disable=SC2034
      BASHUNIT_COVERAGE_REPORT="$2"
      _bashunit_coverage_opt_set=true
      shift
      ;;
    --coverage-min)
      # shellcheck disable=SC2034
      BASHUNIT_COVERAGE_MIN="$2"
      _bashunit_coverage_opt_set=true
      shift
      ;;
    --no-coverage-report)
      # shellcheck disable=SC2034
      BASHUNIT_COVERAGE_REPORT=""
      ;;
    --coverage-report-html)
      # shellcheck disable=SC2034
      # Use default if no value provided or next arg is a flag
      if [ -z "${2:-}" ]; then
        BASHUNIT_COVERAGE_REPORT_HTML="coverage/html"
      else
        case "${2:-}" in
        -*)
          BASHUNIT_COVERAGE_REPORT_HTML="coverage/html"
          ;;
        *)
          BASHUNIT_COVERAGE_REPORT_HTML="$2"
          shift
          ;;
        esac
      fi
      _bashunit_coverage_opt_set=true
      ;;
    *)
      raw_args[raw_args_count]="$1"
      raw_args_count=$((raw_args_count + 1))
      ;;
    esac
    shift
  done

  # Auto-enable coverage when any coverage output option is specified
  if [ "$_bashunit_coverage_opt_set" = true ]; then
    # shellcheck disable=SC2034
    BASHUNIT_COVERAGE=true
  fi

  # Expand positional arguments and extract inline filters
  # Skip filter parsing for assert mode - args are not file paths
  local inline_filter=""
  local inline_filter_file=""
  if [ "$raw_args_count" -gt 0 ]; then
    if [ -n "$assert_fn" ]; then
      # Assert mode: pass args as-is without file path processing
      args=("${raw_args[@]}")
      args_count="$raw_args_count"
    else
      # Test mode: process file paths and extract inline filters
      local arg
      for arg in "${raw_args[@]+"${raw_args[@]}"}"; do
        local parsed_path parsed_filter
        {
          read -r parsed_path
          read -r parsed_filter
        } < <(bashunit::helper::parse_file_path_filter "$arg")

        # If an inline filter was found, store it
        if [ -n "$parsed_filter" ]; then
          inline_filter="$parsed_filter"
          inline_filter_file="$parsed_path"
        fi

        local file
        while IFS= read -r file; do
          args[args_count]="$file"
          args_count=$((args_count + 1))
        done < <(bashunit::helper::find_files_recursive "$parsed_path" '*[tT]est.sh')
      done

      # Resolve line number filter to function name
      case "$inline_filter" in
      "__line__:"*)
        local line_number="${inline_filter#__line__:}"
        local resolved_file="${inline_filter_file}"

        # If the file path was a pattern, use the first resolved file
        if [ "$args_count" -gt 0 ]; then
          resolved_file="${args[0]}"
        fi

        inline_filter=$(bashunit::helper::find_function_at_line "$resolved_file" "$line_number")
        if [ -z "$inline_filter" ]; then
          printf "%sError: No test function found at line %s in %s%s\n" \
            "${_BASHUNIT_COLOR_FAILED}" "$line_number" "$resolved_file" "${_BASHUNIT_COLOR_DEFAULT}"
          exit 1
        fi
        ;;
      esac

      # Use inline filter if no -f filter was provided
      if [ -z "$filter" ] && [ -n "$inline_filter" ]; then
        filter="$inline_filter"
      fi
    fi
  fi

  # --rerun-failed: restrict discovery to the files recorded as failing last
  # run. Function-level filtering happens in the runner; --filter/--tag still
  # apply on top. With no recorded failures, fall back to the full suite.
  if [ -z "$assert_fn" ] && bashunit::rerun::is_enabled; then
    bashunit::rerun::load
    if bashunit::rerun::has_entries; then
      local -a _rerun_files=()
      local _rerun_file
      while IFS= read -r _rerun_file; do
        [ -z "$_rerun_file" ] && continue
        # Skip entries pointing at deleted files, don't crash.
        [ -f "$_rerun_file" ] || continue
        _rerun_files[${#_rerun_files[@]}]="$_rerun_file"
      done < <(bashunit::rerun::files)
      if [ "${#_rerun_files[@]}" -gt 0 ]; then
        args=("${_rerun_files[@]}")
        args_count=${#args[@]}
      fi
    else
      printf "%sNo previously failing tests recorded; running the full suite.%s\n" \
        "${_BASHUNIT_COLOR_SKIPPED}" "${_BASHUNIT_COLOR_DEFAULT}"
    fi
  fi

  # Optional bootstrap
  # shellcheck disable=SC1090,SC2086
  [ -f "${BASHUNIT_BOOTSTRAP:-}" ] && source "$BASHUNIT_BOOTSTRAP" ${BASHUNIT_BOOTSTRAP_ARGS:-}

  if [ "${BASHUNIT_NO_OUTPUT:-false}" = true ]; then
    exec >/dev/null 2>&1
  fi

  # Disable strict mode for test execution to allow:
  # - Empty array expansion (set +u)
  # - Non-zero exit codes from failing tests (set +e)
  # - Pipe failures in test output (set +o pipefail)
  set +euo pipefail
  if [ -n "$assert_fn" ]; then
    # Disable coverage for assert mode - it's meant for running single assertions,
    # not tracking code coverage. This also prevents issues when parent bashunit
    # runs with coverage and calls subprocess bashunit with -a flag.
    BASHUNIT_COVERAGE=false
    export -n BASHUNIT_COVERAGE
    bashunit::main::exec_assert "$assert_fn" ${args+"${args[@]}"}
  else
    if [ "${BASHUNIT_WATCH_MODE:-false}" = true ]; then
      bashunit::main::watch_loop \
        "$filter" "$tag_filter" "$exclude_tag_filter" \
        ${args+"${args[@]}"}
    else
      if [ "$args_count" -gt 0 ]; then
        bashunit::main::exec_tests \
          "$filter" "$tag_filter" "$exclude_tag_filter" \
          "${args[@]}"
      else
        bashunit::main::exec_tests \
          "$filter" "$tag_filter" "$exclude_tag_filter"
      fi
    fi
  fi
}

#############################
# Subcommand: bench
#############################
function bashunit::main::cmd_bench() {
  local filter=""
  local IFS=$' \t\n'
  local -a raw_args=()
  local raw_args_count=0
  local -a args=()
  local args_count=0

  BASHUNIT_BENCH_MODE=true
  export -n BASHUNIT_BENCH_MODE

  # Parse bench-specific options
  while [ $# -gt 0 ]; do
    case "$1" in
    -f | --filter)
      filter="$2"
      shift
      ;;
    -s | --simple)
      BASHUNIT_SIMPLE_OUTPUT=true
      export -n BASHUNIT_SIMPLE_OUTPUT
      ;;
    --detailed)
      BASHUNIT_SIMPLE_OUTPUT=false
      export -n BASHUNIT_SIMPLE_OUTPUT
      ;;
    -e | --env | --boot)
      # Support: --env "bootstrap.sh arg1 arg2"
      local boot_file="${2%% *}"
      local boot_args="${2#* }"
      if [ "$boot_args" != "$2" ]; then
        BASHUNIT_BOOTSTRAP_ARGS="$boot_args"
        export -n BASHUNIT_BOOTSTRAP_ARGS
      fi
      # Export all variables from the env file so they're available in subshells
      # (e.g., process substitution used in load_test_files)
      set -o allexport
      # shellcheck disable=SC1090,SC2086
      source "$boot_file" ${BASHUNIT_BOOTSTRAP_ARGS:-}
      set +o allexport
      shift
      ;;
    -vvv | --verbose)
      BASHUNIT_VERBOSE=true
      export -n BASHUNIT_VERBOSE
      ;;
    --skip-env-file)
      BASHUNIT_SKIP_ENV_FILE=true
      export -n BASHUNIT_SKIP_ENV_FILE
      ;;
    -l | --login)
      BASHUNIT_LOGIN_SHELL=true
      export -n BASHUNIT_LOGIN_SHELL
      ;;
    --no-color)
      # shellcheck disable=SC2034
      BASHUNIT_NO_COLOR=true
      ;;
    -h | --help)
      bashunit::console_header::print_bench_help
      exit 0
      ;;
    *)
      raw_args[raw_args_count]="$1"
      raw_args_count=$((raw_args_count + 1))
      ;;
    esac
    shift
  done

  # Expand positional arguments
  if [ "$raw_args_count" -gt 0 ]; then
    local arg file
    for arg in "${raw_args[@]+"${raw_args[@]}"}"; do
      while IFS= read -r file; do
        args[args_count]="$file"
        args_count=$((args_count + 1))
      done < <(bashunit::helper::find_files_recursive "$arg" '*[bB]ench.sh')
    done
  fi

  # Optional bootstrap
  # shellcheck disable=SC1090,SC2086
  [ -f "${BASHUNIT_BOOTSTRAP:-}" ] && source "$BASHUNIT_BOOTSTRAP" ${BASHUNIT_BOOTSTRAP_ARGS:-}

  set +euo pipefail

  # Bash 3.0 compatible: only pass args if we have files
  if [ "$args_count" -gt 0 ]; then
    bashunit::main::exec_benchmarks "$filter" "${args[@]}"
  else
    bashunit::main::exec_benchmarks "$filter"
  fi
}

#############################
# Subcommand: doc
#############################
function bashunit::main::cmd_doc() {
  case "${1:-}" in
  -h | --help)
    bashunit::console_header::print_doc_help
    exit 0
    ;;
  esac

  bashunit::doc::print_asserts "${1:-}"
  exit 0
}

#############################
# Subcommand: init
#############################
function bashunit::main::cmd_init() {
  case "${1:-}" in
  -h | --help)
    bashunit::console_header::print_init_help
    exit 0
    ;;
  esac

  bashunit::init::project "${1:-}"
  exit 0
}

#############################
# Subcommand: learn
#############################
function bashunit::main::cmd_learn() {
  case "${1:-}" in
  -h | --help)
    bashunit::console_header::print_learn_help
    exit 0
    ;;
  esac

  bashunit::learn::start
  exit 0
}

#############################
# Subcommand: watch
#############################
function bashunit::main::cmd_watch() {
  local path=""
  local -a extra_args=()

  while [ $# -gt 0 ]; do
    case "$1" in
    -h | --help)
      bashunit::console_header::print_watch_help
      exit 0
      ;;
    -f | --filter)
      # Forward the filter flag and its value to the underlying test run
      extra_args[${#extra_args[@]}]="$1"
      shift || true
      if [ $# -gt 0 ]; then
        extra_args[${#extra_args[@]}]="$1"
      fi
      ;;
    -*)
      extra_args[${#extra_args[@]}]="$1"
      ;;
    *)
      if [ -z "$path" ]; then
        path="$1"
      else
        extra_args[${#extra_args[@]}]="$1"
      fi
      ;;
    esac
    shift || true
  done

  [ -z "$path" ] && path="."

  bashunit::watch::run "$path" "${extra_args[@]+"${extra_args[@]}"}"
}

#############################
# Subcommand: upgrade
#############################
function bashunit::main::cmd_upgrade() {
  case "${1:-}" in
  -h | --help)
    bashunit::console_header::print_upgrade_help
    exit 0
    ;;
  esac

  bashunit::upgrade::upgrade
  exit 0
}

#############################
# Subcommand: assert
#############################

# Check if a name corresponds to an assertion function (not a file or command)
function bashunit::main::is_assertion_function() {
  local name="$1"
  declare -F "assert_$name" &>/dev/null || declare -F "$name" &>/dev/null
}

# Check if assertion operates on exit codes
function bashunit::main::is_exit_code_assertion() {
  local name="$1"
  case "$name" in
  exit_code | successful_code | unsuccessful_code | general_error | command_not_found)
    return 0
    ;;
  *)
    return 1
    ;;
  esac
}

function bashunit::main::cmd_assert() {
  case "${1:-}" in
  -h | --help)
    bashunit::console_header::print_assert_help
    exit 0
    ;;
  esac

  local first_arg="${1:-}"
  if [ -z "$first_arg" ]; then
    printf "%sError: Assert function name or command is required.%s\n" \
      "${_BASHUNIT_COLOR_FAILED}" "${_BASHUNIT_COLOR_DEFAULT}"
    bashunit::console_header::print_assert_help
    exit 1
  fi

  # Disable strict mode for assert execution
  set +euo pipefail

  # Route to appropriate handler based on first argument
  if bashunit::main::is_assertion_function "$first_arg"; then
    # Old single-assertion syntax: bashunit assert <fn> <args...>
    local assert_fn="$first_arg"
    shift
    bashunit::main::exec_assert "$assert_fn" "$@"
  elif [ $# -ge 2 ] && bashunit::main::is_assertion_function "$2"; then
    # New multi-assertion syntax: bashunit assert "<cmd>" <assertion1> <arg1> ...
    # Detected by: first arg is not assertion, but second arg is an assertion name
    bashunit::main::exec_multi_assert "$@"
  else
    # Fallback: try as single assertion (may fail with function not found)
    bashunit::main::exec_assert "$@"
  fi
  exit $?
}

#############################
# Watch mode
#############################
function bashunit::main::watch_get_checksum() {
  local IFS=$' \t\n'
  local -a paths=("$@")

  local file checksum=""
  for file in "${paths[@]+"${paths[@]}"}"; do
    if [ -d "$file" ]; then
      local found
      found=$(find "$file" -name '*.sh' -type f \
        -exec stat -c '%Y %n' {} + 2>/dev/null ||
        find "$file" -name '*.sh' -type f \
          -exec stat -f '%m %N' {} + 2>/dev/null) || true
      checksum="${checksum}${found}"
    elif [ -f "$file" ]; then
      local mtime
      mtime=$(stat -c '%Y' "$file" 2>/dev/null ||
        stat -f '%m' "$file" 2>/dev/null) || true
      checksum="${checksum}${mtime} ${file}"
    fi
  done
  echo "$checksum"
}

function bashunit::main::watch_loop() {
  local filter="$1"
  local tag_filter="${2:-}"
  local exclude_tag_filter="${3:-}"
  shift 3

  local IFS=$' \t\n'
  local -a watch_paths=("$@")
  [ -d "src" ] && watch_paths[${#watch_paths[@]}]="src"

  trap 'printf "\n%sWatch mode stopped.%s\n" \
    "${_BASHUNIT_COLOR_SKIPPED}" "${_BASHUNIT_COLOR_DEFAULT}"; \
    exit 0' INT

  local last_checksum=""
  while true; do
    local current_checksum
    current_checksum=$(bashunit::main::watch_get_checksum \
      "${watch_paths[@]}")

    if [ "$current_checksum" != "$last_checksum" ]; then
      last_checksum="$current_checksum"
      bashunit::io::clear_screen
      printf "%s[watch] Running tests...%s\n\n" \
        "${_BASHUNIT_COLOR_SKIPPED}" \
        "${_BASHUNIT_COLOR_DEFAULT}"

      (
        if [ $# -gt 0 ]; then
          bashunit::main::exec_tests \
            "$filter" "$tag_filter" \
            "$exclude_tag_filter" "$@"
        else
          bashunit::main::exec_tests \
            "$filter" "$tag_filter" \
            "$exclude_tag_filter"
        fi
      ) || true

      printf "\n%s[watch] Waiting for changes...%s\n" \
        "${_BASHUNIT_COLOR_SKIPPED}" \
        "${_BASHUNIT_COLOR_DEFAULT}"
    fi
    sleep 1
  done
}

#############################
# Test execution
#############################
function bashunit::main::exec_tests() {
  local filter=$1
  local tag_filter="${2:-}"
  local exclude_tag_filter="${3:-}"
  shift 3

  # Bash 3.0 compatible: collect files into array
  local test_files
  local test_files_count=0
  local _line
  while IFS= read -r _line; do
    [ -z "$_line" ] && continue
    test_files[test_files_count]="$_line"
    test_files_count=$((test_files_count + 1))
  done < <(bashunit::helper::load_test_files "$filter" "$@")

  bashunit::internal_log "exec_tests" "filter:$filter" "files:${test_files[*]:-}"

  if [ "$test_files_count" -eq 0 ]; then
    printf "%sError: At least one file path is required.%s\n" "${_BASHUNIT_COLOR_FAILED}" "${_BASHUNIT_COLOR_DEFAULT}"
    bashunit::console_header::print_help
    exit 1
  fi

  # Split the suite across runners: keep the files whose position matches this
  # shard (round-robin), so all shards together cover the whole suite with no
  # overlap. An empty shard (more shards than files) is valid and runs nothing.
  if bashunit::env::is_shard_enabled; then
    local _shard_index _shard_total
    _shard_index=$(bashunit::env::shard_index)
    _shard_total=$(bashunit::env::shard_total)
    local -a _sharded=()
    local _i=0
    while [ "$_i" -lt "$test_files_count" ]; do
      if [ "$((_i % _shard_total))" -eq "$((_shard_index - 1))" ]; then
        _sharded[${#_sharded[@]}]="${test_files[_i]}"
      fi
      _i=$((_i + 1))
    done
    test_files=("${_sharded[@]+"${_sharded[@]}"}")
    test_files_count=${#test_files[@]}
    bashunit::internal_log "shard" "index:$_shard_index" "total:$_shard_total" "files:$test_files_count"
  fi

  # Trap SIGINT (Ctrl-C) and call the cleanup function
  trap 'bashunit::main::cleanup' SIGINT
  trap '[ $? -eq $EXIT_CODE_STOP_ON_FAILURE ] && bashunit::main::handle_stop_on_failure_sync' EXIT

  # Resolve parallel mode once now that --parallel/--no-parallel are parsed, so
  # the per-test is_enabled reads a global instead of re-checking env + OS.
  bashunit::parallel::resolve_enabled

  if bashunit::env::is_parallel_run_enabled && ! bashunit::parallel::is_enabled; then
    printf "%sWarning: Parallel tests are supported on macOS, Ubuntu and Windows.\n" "${_BASHUNIT_COLOR_INCOMPLETE}"
    printf "For other OS (like Alpine), --parallel is not enabled due to inconsistent results,\n"
    printf "particularly involving race conditions.%s " "${_BASHUNIT_COLOR_DEFAULT}"
    printf "%sFallback using --no-parallel%s\n" "${_BASHUNIT_COLOR_SKIPPED}" "${_BASHUNIT_COLOR_DEFAULT}"
  fi

  if bashunit::parallel::is_enabled; then
    bashunit::parallel::init
  fi

  if bashunit::env::is_tap_output_enabled; then
    printf "TAP version 13\n"
  else
    bashunit::console_header::print_version_with_env "$filter" "${test_files[@]}"
  fi

  # Resolve the shuffle seed once (generating one if absent) so it can be printed
  # for replay and inherited by parallel test-file subshells.
  if bashunit::env::is_random_order_enabled; then
    if [ -z "${BASHUNIT_SEED:-}" ]; then
      BASHUNIT_SEED=$RANDOM
      export -n BASHUNIT_SEED
    fi
    if ! bashunit::env::is_tap_output_enabled; then
      bashunit::console_header::print_random_order_seed "$BASHUNIT_SEED"
    fi
  fi

  if bashunit::env::is_verbose_enabled; then
    if bashunit::env::is_simple_output_enabled; then
      echo ""
    fi
    printf '%*s\n' "$TERMINAL_WIDTH" '' | tr ' ' '#'
    printf "%s\n" "Filter:      ${filter:-None}"
    printf "%s\n" "Total files: ${#test_files[@]}"
    printf "%s\n" "Test files:"
    printf -- "- %s\n" "${test_files[@]}"
    printf '%*s\n' "$TERMINAL_WIDTH" '' | tr ' ' '.'
    bashunit::env::print_verbose
    printf '%*s\n' "$TERMINAL_WIDTH" '' | tr ' ' '#'
  fi

  bashunit::runner::load_test_files "$filter" "$tag_filter" "$exclude_tag_filter" "${test_files[@]}"

  if bashunit::parallel::is_enabled; then
    wait
  fi

  if bashunit::parallel::is_enabled && bashunit::parallel::must_stop_on_failure; then
    printf "\r%sStop on failure enabled...%s\n" "${_BASHUNIT_COLOR_SKIPPED}" "${_BASHUNIT_COLOR_DEFAULT}"
  fi

  if ! bashunit::env::is_tap_output_enabled; then
    bashunit::console_results::print_failing_tests_and_reset
    bashunit::console_results::print_risky_tests_and_reset
    bashunit::console_results::print_incomplete_tests_and_reset
    bashunit::console_results::print_skipped_tests_and_reset
  fi
  bashunit::console_results::render_result
  exit_code=$?

  if bashunit::env::is_profile_enabled; then
    bashunit::console_results::print_profile_and_reset
  fi

  if [ -n "$BASHUNIT_LOG_JUNIT" ]; then
    bashunit::reports::generate_junit_xml "$BASHUNIT_LOG_JUNIT"
  fi

  if [ -n "$BASHUNIT_LOG_GHA" ]; then
    bashunit::reports::generate_gha_log "$BASHUNIT_LOG_GHA"
  fi

  if [ -n "$BASHUNIT_REPORT_HTML" ]; then
    bashunit::reports::generate_report_html "$BASHUNIT_REPORT_HTML"
  fi

  if [ -n "$BASHUNIT_REPORT_TAP" ]; then
    bashunit::reports::generate_report_tap "$BASHUNIT_REPORT_TAP"
  fi

  if [ -n "$BASHUNIT_REPORT_JSON" ]; then
    bashunit::reports::generate_report_json "$BASHUNIT_REPORT_JSON"
  fi

  # Generate coverage report if enabled
  if bashunit::env::is_coverage_enabled; then
    # Aggregate per-process coverage data from parallel runs
    if bashunit::parallel::is_enabled; then
      bashunit::coverage::aggregate_parallel
    fi

    bashunit::coverage::precompute_file_stats

    bashunit::coverage::report_text

    if [ -n "$BASHUNIT_COVERAGE_REPORT" ]; then
      bashunit::coverage::report_lcov "$BASHUNIT_COVERAGE_REPORT"
    fi

    if [ -n "$BASHUNIT_COVERAGE_REPORT_HTML" ]; then
      bashunit::coverage::report_html "$BASHUNIT_COVERAGE_REPORT_HTML"
    fi

    # Check minimum threshold
    if ! bashunit::coverage::check_threshold; then
      exit_code=1
    fi

    bashunit::coverage::cleanup
  fi

  if bashunit::parallel::is_enabled; then
    bashunit::parallel::cleanup
  fi

  # Persist this run's failing tests so a later --rerun-failed can replay them.
  bashunit::rerun::persist

  # The rerun cache is read from the run dir above, so clean up only after it.
  bashunit::env::cleanup_run_output_dir

  bashunit::internal_log "Finished tests" "exit_code:$exit_code"
  exit $exit_code
}

function bashunit::main::exec_benchmarks() {
  local filter=$1
  shift

  # Bash 3.0 compatible: collect files into array
  local bench_files
  local bench_files_count=0
  local _line
  while IFS= read -r _line; do
    [ -z "$_line" ] && continue
    bench_files[bench_files_count]="$_line"
    bench_files_count=$((bench_files_count + 1))
  done < <(bashunit::helper::load_bench_files "$filter" "$@")

  bashunit::internal_log "exec_benchmarks" "filter:$filter" "files:${bench_files[*]:-}"

  if [ "$bench_files_count" -eq 0 ]; then
    printf "%sError: At least one file path is required.%s\n" "${_BASHUNIT_COLOR_FAILED}" "${_BASHUNIT_COLOR_DEFAULT}"
    bashunit::console_header::print_help
    exit 1
  fi

  bashunit::console_header::print_version_with_env "$filter" "${bench_files[@]}"

  bashunit::runner::load_bench_files "$filter" "${bench_files[@]}"

  bashunit::benchmark::print_results

  bashunit::internal_log "Finished benchmarks"
}

function bashunit::main::cleanup() {
  printf "%sCaught Ctrl-C, killing all child processes...%s\n" \
    "${_BASHUNIT_COLOR_SKIPPED}" "${_BASHUNIT_COLOR_DEFAULT}"
  # Kill all child processes of this script
  pkill -P $$
  bashunit::cleanup_script_temp_files
  if bashunit::parallel::is_enabled; then
    bashunit::parallel::cleanup
  fi
  bashunit::env::cleanup_run_output_dir
  exit 1
}

function bashunit::main::handle_stop_on_failure_sync() {
  printf "\n%sStop on failure enabled...%s\n" "${_BASHUNIT_COLOR_SKIPPED}" "${_BASHUNIT_COLOR_DEFAULT}"
  bashunit::console_results::print_failing_tests_and_reset
  bashunit::console_results::print_risky_tests_and_reset
  bashunit::console_results::print_incomplete_tests_and_reset
  bashunit::console_results::print_skipped_tests_and_reset
  bashunit::console_results::render_result
  if bashunit::env::is_profile_enabled; then
    bashunit::console_results::print_profile_and_reset
  fi
  bashunit::cleanup_script_temp_files
  if bashunit::parallel::is_enabled; then
    bashunit::parallel::cleanup
  fi
  bashunit::env::cleanup_run_output_dir
  exit 1
}

function bashunit::main::exec_assert() {
  local original_assert_fn=$1
  local -a args=()
  local args_count=$(($# - 1))
  [ $# -gt 1 ] && args=("${@:2}")

  local assert_fn=$original_assert_fn

  # Check if the function exists
  if ! type "$assert_fn" >/dev/null 2>&1; then
    assert_fn="assert_$assert_fn"
    if ! type "$assert_fn" >/dev/null 2>&1; then
      echo "Function $original_assert_fn does not exist." 1>&2
      exit 127
    fi
  fi

  # Get the last argument safely by calculating the array length
  local last_index=$((args_count - 1))
  local last_arg="${args[$last_index]}"
  local output=""
  local inner_exit_code=0
  local bashunit_exit_code=0

  # Handle different assert_* functions
  case "$assert_fn" in
  assert_exit_code)
    output=$(bashunit::main::handle_assert_exit_code "$last_arg")
    inner_exit_code=$?
    # Remove the last argument and append the exit code
    args=("${args[@]:0:last_index}")
    args[last_index]="$inner_exit_code"
    ;;
  *)
    # Add more cases here for other assert_* handlers if needed
    ;;
  esac

  if [ -n "$output" ]; then
    echo "$output" 1>&1
    assert_fn="assert_same"
  fi

  # Set a friendly test title for CLI assert command output
  bashunit::state::set_test_title "assert ${original_assert_fn#assert_}"

  # Run the assertion function and write into stderr
  "$assert_fn" "${args[@]}" 1>&2
  bashunit_exit_code=$?

  if [ "$(bashunit::state::get_tests_failed)" -gt 0 ] || [ "$(bashunit::state::get_assertions_failed)" -gt 0 ]; then
    return 1
  fi

  return "$bashunit_exit_code"
}

function bashunit::main::handle_assert_exit_code() {
  local cmd="$1"
  local output
  local inner_exit_code=0

  if command -v "${cmd%% *}" >/dev/null 2>&1; then
    output=$(eval "$cmd" 2>&1 || echo "inner_exit_code:$?")
    local last_line
    last_line=$(echo "$output" | tail -n 1)
    if [ "$(echo "$last_line" | "$GREP" -c 'inner_exit_code:[0-9]*' || true)" -gt 0 ]; then
      inner_exit_code=$(echo "$last_line" | grep -o 'inner_exit_code:[0-9]*' | cut -d':' -f2)
      local _re='^[0-9]+$'
      if [ "$(echo "$inner_exit_code" | "$GREP" -cE "$_re" || true)" -eq 0 ]; then
        inner_exit_code=1
      fi
      output=$(echo "$output" | sed '$d')
    fi
    echo "$output"
    return "$inner_exit_code"
  else
    echo "Command not found: $cmd" 1>&2
    return 127
  fi
}

# Execute multiple assertions on a single command output
# Usage: exec_multi_assert "command" assertion1 arg1 [assertion2 arg2 ...]
function bashunit::main::exec_multi_assert() {
  local cmd="$1"
  shift

  # Require at least one assertion
  if [ $# -lt 1 ]; then
    printf "%sError: Multi-assertion mode requires at least one assertion.%s\n" \
      "${_BASHUNIT_COLOR_FAILED}" "${_BASHUNIT_COLOR_DEFAULT}" 1>&2
    printf "Usage: bashunit assert \"<command>\" <assertion1> <arg1> [<assertion2> <arg2>...]\n" 1>&2
    return 1
  fi

  # Check that assertions come in pairs (assertion + arg)
  if [ $# -lt 2 ] || [ $(($# % 2)) -ne 0 ]; then
    local assertion_name="${1:-}"
    printf "%sError: Missing argument for assertion '%s'.%s\n" \
      "${_BASHUNIT_COLOR_FAILED}" "$assertion_name" "${_BASHUNIT_COLOR_DEFAULT}" 1>&2
    return 1
  fi

  # Execute command and capture output + exit code
  local stdout
  local cmd_exit_code
  stdout=$(eval "$cmd" 2>&1)
  cmd_exit_code=$?

  # Print stdout for user visibility
  if [ -n "$stdout" ]; then
    echo "$stdout" 1>&1
  fi

  # Parse and execute assertions in pairs
  local overall_result=0
  while [ $# -gt 0 ]; do
    local assertion_name="$1"
    local assertion_arg="${2:-}"

    if [ -z "$assertion_arg" ]; then
      printf "%sError: Missing argument for assertion '%s'.%s\n" \
        "${_BASHUNIT_COLOR_FAILED}" "$assertion_name" "${_BASHUNIT_COLOR_DEFAULT}" 1>&2
      return 1
    fi

    shift 2

    # Resolve assertion function name
    local assert_fn="$assertion_name"
    if ! type "$assert_fn" &>/dev/null; then
      assert_fn="assert_$assertion_name"
      if ! type "$assert_fn" &>/dev/null; then
        printf "%sError: Unknown assertion '%s'.%s\n" \
          "${_BASHUNIT_COLOR_FAILED}" "$assertion_name" "${_BASHUNIT_COLOR_DEFAULT}" 1>&2
        return 1
      fi
    fi

    # Set test title for this assertion
    bashunit::state::set_test_title "assert ${assertion_name#assert_}"

    # Execute assertion with appropriate argument
    if bashunit::main::is_exit_code_assertion "$assertion_name"; then
      # Exit code assertion: pass expected value and captured exit code
      "$assert_fn" "$assertion_arg" "" "$cmd_exit_code" 1>&2
    else
      # Output assertion: pass expected value and captured stdout
      "$assert_fn" "$assertion_arg" "$stdout" 1>&2
    fi

    if [ "$(bashunit::state::get_assertions_failed)" -gt 0 ]; then
      overall_result=1
    fi
  done

  return $overall_result
}

#!/opt/local/bin/bash
set -euo pipefail

declare -r BASHUNIT_MIN_BASH_VERSION="3.0"

function _check_bash_version() {
  local current_version
  if [[ -n ${BASHUNIT_TEST_BASH_VERSION:-} ]]; then
    # Checks if BASHUNIT_TEST_BASH_VERSION is set (typically for testing purposes)
    current_version="${BASHUNIT_TEST_BASH_VERSION}"
  elif [[ -n ${BASH_VERSINFO+set} ]]; then
    # Checks if the special Bash array BASH_VERSINFO exists. This array is only defined in Bash.
    current_version="${BASH_VERSINFO[0]}.${BASH_VERSINFO[1]}"
  else
    # If not in Bash (e.g., running from Zsh). The pipeline extracts just the major.minor version (e.g., 3.0).
    current_version="$(bash --version | head -n1 | cut -d' ' -f4 | cut -d. -f1,2)"
  fi

  local major
  IFS=. read -r major _ <<<"$current_version"

  if ((major < 3)); then
    printf 'Bashunit requires Bash >= %s. Current version: %s\n' "$BASHUNIT_MIN_BASH_VERSION" "$current_version" >&2
    exit 1
  fi
}

_check_bash_version

# shellcheck disable=SC2034
declare -r BASHUNIT_VERSION="0.42.0"

# shellcheck disable=SC2155
declare -r BASHUNIT_ROOT_DIR="$(dirname "${BASH_SOURCE[0]}")"
export BASHUNIT_ROOT_DIR

# Capture working directory at startup (before any test changes it)
declare -r BASHUNIT_WORKING_DIR="$PWD"
export BASHUNIT_WORKING_DIR

# Early scan for flags that must be set before loading env.sh
for arg in "$@"; do
  case "$arg" in
  --skip-env-file)
    # this-process-only (see the flag-parse loop in src/main.sh) (#837)
    BASHUNIT_SKIP_ENV_FILE=true
    export -n BASHUNIT_SKIP_ENV_FILE
    ;;
  -l | --login)
    BASHUNIT_LOGIN_SHELL=true
    export -n BASHUNIT_LOGIN_SHELL
    ;;
  --no-color)
    # shellcheck disable=SC2034
    BASHUNIT_NO_COLOR=true
    ;;
  esac
done


bashunit::check_os::init
bashunit::clock::init

# Subcommand detection
_SUBCOMMAND=""

case "${1:-}" in
  test | bench | doc | init | learn | upgrade | assert | watch)
    _SUBCOMMAND="$1"
    shift
    ;;
  -v | --version)
    bashunit::console_header::print_version
    exit 0
    ;;
  -h | --help)
    bashunit::console_header::print_help
    exit 0
    ;;
  -*)
    # Flag without subcommand → assume "test"
    _SUBCOMMAND="test"
    ;;
  "")
    # No arguments → assume "test" (uses BASHUNIT_DEFAULT_PATH)
    _SUBCOMMAND="test"
    ;;
  *)
    # Path argument → assume "test"
    _SUBCOMMAND="test"
    ;;
esac

# Route to subcommand handler
case "$_SUBCOMMAND" in
  test) bashunit::main::cmd_test "$@" ;;
  bench) bashunit::main::cmd_bench "$@" ;;
  doc) bashunit::main::cmd_doc "$@" ;;
  init) bashunit::main::cmd_init "$@" ;;
  learn) bashunit::main::cmd_learn "$@" ;;
  upgrade) bashunit::main::cmd_upgrade "$@" ;;
  assert) bashunit::main::cmd_assert "$@" ;;
  watch) bashunit::main::cmd_watch "$@" ;;
esac
