#!/usr/bin/env bash
# check_os.sh

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

function bashunit::check_os::init() {
  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
}

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

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() {
  [[ "$(uname)" == "Linux" ]]
}

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

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

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::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

# Strip ANSI escape codes and control characters
function bashunit::str::strip_ansi() {
  local input="$1"
  echo -e "$input" | sed -E 's/\x1B\[[0-9;]*[mK]//g; s/[[:cntrl:]]//g'
}

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
  # shellcheck disable=SC2155
  local clean_left_text=$(bashunit::str::strip_ansi "$left_text")

  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

  # Rebuild the text with ANSI codes intact, preserving the truncation
  local 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+="${left_text:$j:1}"
        ((j++))
      done
      result_left_text+="${left_text:$j:1}"  # Append the final 'm'
      ((j++))
    elif [[ "$char" == "$original_char" ]]; then
      # Match the actual character
      result_left_text+="$char"
      ((i++))
      ((j++))
    else
      ((j++))
    fi
  done

  local remaining_space
  if $is_truncated ; then
    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
    # Copy any remaining characters after the truncation point
    result_left_text+="${left_text:$j}"
    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=''
  for (( i=0; i<length; i++ )); do
    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
    rm -rf "$BASHUNIT_TEMP_DIR/${BASHUNIT_CURRENT_TEST_ID}"_*
  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
      printf '%q' "$arg"
      first=false
    else
      printf ' %q' "$arg"
    fi
  done
  printf ' %q\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
}

# io.sh

function bashunit::io::download_to() {
  local url="$1"
  local output="$2"
  if bashunit::dependencies::has_curl; then
    curl -L -J -o "$output" "$url" 2>/dev/null
  elif bashunit::dependencies::has_wget; then
    wget -q -O "$output" "$url" 2>/dev/null
  else
    return 1
  fi
}

# math.sh

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

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

  if [[ "$expr" == *.* ]]; then
    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')
  fi

  # Remove leading zeros from integers
  expr=$(echo "$expr" | sed -E 's/\b0*([1-9][0-9]*)/\1/g')

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

# parallel.sh

function bashunit::parallel::aggregate_test_results() {
  local temp_dir_parallel_test_suite=$1

  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

  for script_dir in "$temp_dir_parallel_test_suite"/*; do
    shopt -s nullglob
    local 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

    for result_file in "${result_files[@]}"; do
      local result_line
      result_line=$(tail -n 1 < "$result_file")

      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

      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
  rm -rf "$TEMP_DIR_PARALLEL_TEST_SUITE"
}

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

function bashunit::parallel::is_enabled() {
  bashunit::internal_log "bashunit::parallel::is_enabled" \
    "requested:$BASHUNIT_PARALLEL_RUN" "os:${_BASHUNIT_OS:-Unknown}"

  if bashunit::env::is_parallel_run_enabled && \
    (bashunit::check_os::is_macos || bashunit::check_os::is_ubuntu || bashunit::check_os::is_windows); then
    return 0
  fi
  return 1
}

# env.sh

# shellcheck disable=SC2034

# Load .env file (skip if --preserve-env is used to keep shell environment intact)
if [[ "${BASHUNIT_PRESERVE_ENV:-false}" != "true" ]]; then
  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_REPORT_HTML=""

: "${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_REPORT_HTML:=${REPORT_HTML:=$_BASHUNIT_DEFAULT_REPORT_HTML}}"

# 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"
_BASHUNIT_DEFAULT_SHOW_EXECUTION_TIME="true"
_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_PRESERVE_ENV="false"
_BASHUNIT_DEFAULT_LOGIN_SHELL="false"

: "${BASHUNIT_PARALLEL_RUN:=${PARALLEL_RUN:=$_BASHUNIT_DEFAULT_PARALLEL_RUN}}"
: "${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_PRESERVE_ENV:=${PRESERVE_ENV:=$_BASHUNIT_DEFAULT_PRESERVE_ENV}}"
: "${BASHUNIT_LOGIN_SHELL:=${LOGIN_SHELL:=$_BASHUNIT_DEFAULT_LOGIN_SHELL}}"

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

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() {
  [[ "$BASHUNIT_SHOW_EXECUTION_TIME" == "true" ]]
}

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_preserve_env_enabled() {
  [[ "$BASHUNIT_PRESERVE_ENV" == "true" ]]
}

function bashunit::env::is_login_shell_enabled() {
  [[ "$BASHUNIT_LOGIN_SHELL" == "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 keys=(
    "BASHUNIT_DEFAULT_PATH"
    "BASHUNIT_DEV_LOG"
    "BASHUNIT_BOOTSTRAP"
    "BASHUNIT_BOOTSTRAP_ARGS"
    "BASHUNIT_LOG_JUNIT"
    "BASHUNIT_REPORT_HTML"
    "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_PRESERVE_ENV"
    "BASHUNIT_LOGIN_SHELL"
  )

  local max_length=0

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

  for key in "${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)"
FAILURES_OUTPUT_PATH=$(mktemp)
SKIPPED_OUTPUT_PATH=$(mktemp)
INCOMPLETE_OUTPUT_PATH=$(mktemp)
CAT="$(command -v cat)"

# Initialize temp directory once at startup for performance
BASHUNIT_TEMP_DIR="${TMPDIR:-/tmp}/bashunit/tmp"
mkdir -p "$BASHUNIT_TEMP_DIR" 2>/dev/null || true

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

# clock.sh

_BASHUNIT_CLOCK_NOW_IMPL=""

function bashunit::clock::_choose_impl() {
  local shell_time
  local attempts=()

  # 1. Try Perl with Time::HiRes
  attempts+=("Perl")
  if bashunit::dependencies::has_perl && perl -MTime::HiRes -e "" &>/dev/null; then
    _BASHUNIT_CLOCK_NOW_IMPL="perl"
    return 0
  fi

  # 2. Try Python 3 with time module
  attempts+=("Python")
  if bashunit::dependencies::has_python; then
    _BASHUNIT_CLOCK_NOW_IMPL="python"
    return 0
  fi

  # 3. Try Node.js
  attempts+=("Node")
  if bashunit::dependencies::has_node; then
    _BASHUNIT_CLOCK_NOW_IMPL="node"
    return 0
  fi
  # 4. Windows fallback with PowerShell
  attempts+=("PowerShell")
  if bashunit::check_os::is_windows && bashunit::dependencies::has_powershell; then
    _BASHUNIT_CLOCK_NOW_IMPL="powershell"
    return 0
  fi

  # 5. Unix fallback using `date +%s%N` (if not macOS or Alpine)
  attempts+=("date")
  if ! bashunit::check_os::is_macos && ! bashunit::check_os::is_alpine; then
    local result
    result=$(date +%s%N 2>/dev/null)
    if [[ "$result" != *N && "$result" =~ ^[0-9]+$ ]]; then
      _BASHUNIT_CLOCK_NOW_IMPL="date"
      return 0
    fi
  fi

  # 6. Try using native shell EPOCHREALTIME (if available)
  attempts+=("EPOCHREALTIME")
  if shell_time="$(bashunit::clock::shell_time)"; then
    _BASHUNIT_CLOCK_NOW_IMPL="shell"
    return 0
  fi

  # 7. Very last fallback: seconds resolution only
  attempts[${#attempts[@]}]="date-seconds"
  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
}

function bashunit::clock::now() {
  if [[ -z "$_BASHUNIT_CLOCK_NOW_IMPL" ]]; then
    bashunit::clock::_choose_impl || return 1
  fi

  case "$_BASHUNIT_CLOCK_NOW_IMPL" in
    perl)
      perl -MTime::HiRes -e 'printf("%.0f\n", Time::HiRes::time() * 1000000000)'
      ;;
    python)
      python - <<'EOF'
import time, sys
sys.stdout.write(str(int(time.time() * 1000000000)))
EOF
      ;;
    node)
      node -e 'process.stdout.write((BigInt(Date.now()) * 1000000n).toString())'
      ;;
    powershell)
      powershell -Command "\
        \$unixEpoch = [DateTime]'1970-01-01 00:00:00';\
        \$now = [DateTime]::UtcNow;\
        \$ticksSinceEpoch = (\$now - \$unixEpoch).Ticks;\
        \$nanosecondsSinceEpoch = \$ticksSinceEpoch * 100;\
        Write-Output \$nanosecondsSinceEpoch\
      "
      ;;
    date)
      date +%s%N
      ;;
    date-seconds)
      local seconds
      seconds=$(date +%s)
      bashunit::math::calculate "$seconds * 1000000000"
      ;;
    shell)
      # shellcheck disable=SC2155
      local shell_time="$(bashunit::clock::shell_time)"
      local seconds="${shell_time%%.*}"
      local microseconds="${shell_time#*.}"
      bashunit::math::calculate "($seconds * 1000000000) + ($microseconds * 1000)"
      ;;
    *)
      bashunit::clock::_choose_impl || return 1
      bashunit::clock::now
      ;;
  esac
}

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

_BASHUNIT_TESTS_PASSED=0
_BASHUNIT_TESTS_FAILED=0
_BASHUNIT_TESTS_SKIPPED=0
_BASHUNIT_TESTS_INCOMPLETE=0
_BASHUNIT_TESTS_SNAPSHOT=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_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+="$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
}

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

  local encoded_test_hook_message

  if base64 --help 2>&1 | grep -q -- "-w"; then
    # Alpine requires the -w 0 option to avoid wrapping
    encoded_test_output=$(echo -n "$_BASHUNIT_TEST_OUTPUT" | base64 -w 0)
    encoded_test_title=$(echo -n "$_BASHUNIT_TEST_TITLE" | base64 -w 0)
    encoded_test_hook_message=$(echo -n "$_BASHUNIT_TEST_HOOK_MESSAGE" | base64 -w 0)
  else
    # macOS and others: default base64 without wrapping
    encoded_test_output=$(echo -n "$_BASHUNIT_TEST_OUTPUT" | base64)
    encoded_test_title=$(echo -n "$_BASHUNIT_TEST_TITLE" | base64)
    encoded_test_hook_message=$(echo -n "$_BASHUNIT_TEST_HOOK_MESSAGE" | base64)
  fi

  cat <<EOF
##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\
##
EOF
}

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]+')

  for number in $numbers; do
    ((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_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}" ;;
    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
}

# 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

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

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

_BASHUNIT_COLOR_BOLD="$(bashunit::sgr 1)"
_BASHUNIT_COLOR_FAINT="$(bashunit::sgr 2)"
_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_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_DEFAULT="$(bashunit::sgr 0)"

# console_header.sh

function bashunit::console_header::print_version_with_env() {
  local filter=${1:-}
  local files=("${@:2}")

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

  bashunit::console_header::print_version "$filter" "${files[@]}"

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

function bashunit::console_header::print_version() {
  local filter=${1:-}
  if [[ -n "$filter" ]]; then
   shift
  fi

  local files=("$@")
  local total_tests
  if [[ ${#files[@]} -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
    total_tests=$(bashunit::helper::find_total_tests "$filter" "${files[@]}")
  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 "${_BASHUNIT_COLOR_BOLD}${_BASHUNIT_COLOR_PASSED}bashunit${_BASHUNIT_COLOR_DEFAULT} - %s | Tests: %s\n"\
      "$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
  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.typeddevs.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
  --log-junit <file>          Write JUnit XML report
  -p, --parallel              Run tests in parallel (default)
  --no-parallel               Run tests sequentially
  -r, --report-html <file>    Write HTML report
  -s, --simple                Simple output (dots)
  --detailed                  Detailed output (default)
  -R, --run-all               Run all assertions (don't stop on first failure)
  -S, --stop-on-failure       Stop on first failure
  -vvv, --verbose             Show execution details
  --debug [file]              Enable shell debug mode
  --no-output                 Suppress all output
  --strict                    Enable strict shell mode (set -euo pipefail)
  --preserve-env              Skip .env loading, use shell environment only
  -l, --login                 Run tests in login shell context
  -h, --help                  Show this help message

Examples:
  bashunit test tests/
  bashunit test tests/unit/ --parallel
  bashunit test --filter "user" tests/
  bashunit test -a equals "foo" "foo"
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
  --preserve-env              Skip .env loading, use shell environment only
  -l, --login                 Run in login shell context
  -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

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...]

Run a standalone assertion function without creating a test file.

Arguments:
  function                    Assertion function name (with or without 'assert_' prefix)
  args...                     Arguments to pass to the assertion function

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

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

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

# 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_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 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 += tests_passed)) || true
  ((total_tests += tests_skipped)) || true
  ((total_tests += tests_incomplete)) || true
  ((total_tests += tests_snapshot)) || true
  ((total_tests += tests_failed)) || true

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

  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
  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_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_show_execution_time_enabled; then
    return
  fi

  local time=$(bashunit::clock::total_runtime_in_milliseconds | awk '{printf "%.0f", $1}')

  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 ))
  local remainder_ms=$(( time % 1000 ))
  local formatted_seconds=$(echo "$time_in_seconds.$remainder_ms" | awk '{printf "%.0f", $1}')

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

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

  local line
  if [[ -z "$*" ]]; then
    line=$(printf "%s✓ Passed%s: %s" "$_BASHUNIT_COLOR_PASSED" "$_BASHUNIT_COLOR_DEFAULT" "$test_name")
  else
    local quoted_args=""
    for arg in "$@"; do
      if [[ -z "$quoted_args" ]]; then
        quoted_args="'$arg'"
      else
        quoted_args="$quoted_args, '$arg'"
      fi
    done
    line=$(printf "%s✓ Passed%s: %s (%s)" \
      "$_BASHUNIT_COLOR_PASSED" "$_BASHUNIT_COLOR_DEFAULT" "$test_name" "$quoted_args")
  fi

  local full_line=$line
  if bashunit::env::is_show_execution_time_enabled; then
    full_line="$(printf "%s\n" "$(bashunit::str::rpad "$line" "$duration ms")")"
  fi

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

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}")"

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

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-}

  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}" "${expected}" "${failure_condition_message}" "${actual}")"

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

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

  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"

    local git_diff_output
    git_diff_output="$(git diff --no-index --word-diff --color=always \
      "$snapshot_file" "$actual_file" 2>/dev/null \
        | tail -n +6 | sed "s/^/    /")"

    line+="$git_diff_output"
    rm "$actual_file"
  fi

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

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+="$(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+="$(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_error_test() {
  local function_name=$1
  local error="$2"

  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}")"

  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
}

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
}

# 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
#
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
    if [[ "$fn" == test_* ]] || [[ "$fn" =~ ^test[A-Z] ]]; then
      echo "$fn"
      return
    fi
  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"
#
function bashunit::helper::normalize_test_function_name() {
  local original_fn_name="${1-}"
  local interpolated_fn_name="${2-}"

  local custom_title
  custom_title="$(bashunit::state::get_test_title)"
  if [[ -n "$custom_title" ]]; then
    echo "$custom_title"
    return
  fi

  if [[ -z "${interpolated_fn_name-}" && "${original_fn_name}" == *"::"* ]]; then
    local state_interpolated_fn_name
    state_interpolated_fn_name="$(bashunit::state::get_current_test_interpolated_function_name)"

    if [[ -n "$state_interpolated_fn_name" ]]; then
      interpolated_fn_name="$state_interpolated_fn_name"
    fi
  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.2 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}"

  echo "$result"
}

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
  local args=("$@")
  local result="$function_name"

  for ((i=0; i<${#args[@]}; 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"

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

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

  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

  local filtered_lines
  filtered_lines=$(grep -E '^[[:space:]]*(function[[:space:]]+)?test[a-zA-Z_][a-zA-Z0-9_]*\s*\(\)\s*\{' "$script")

  local function_names
  function_names=$(echo "$filtered_lines" | awk '{
    for (i=1; i<=NF; i++) {
      if ($i ~ /^test[a-zA-Z_][a-zA-Z0-9_]*\(\)$/) {
        gsub(/\(\)/, "", $i)
        print $i
        break
      }
    }
  }')

  local duplicates
  duplicates=$(echo "$function_names" | sort | uniq -d)
  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=""

  for fn in $function_names; do
    if [[ $fn == ${prefix}_*${filter}* ]]; then
      if [[ $filtered_functions == *" $fn"* ]]; then
        return 1
      fi
      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}"

  local alt_pattern=""
  if [[ $pattern == *test.sh ]] || [[ $pattern =~ \[tT\]est\.sh$ ]]; then
    alt_pattern="${pattern%.sh}.bash"
  fi

  if [[ "$path" == *"*"* ]]; 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
}

function bashunit::helper::normalize_variable_name() {
  local input_string="$1"
  local normalized_string

  normalized_string="${input_string//[^a-zA-Z0-9_]/_}"

  if [[ ! $normalized_string =~ ^[a-zA-Z_] ]]; then
    normalized_string="_$normalized_string"
  fi

  echo "$normalized_string"
}

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

  # Handle directory changes in set_up_before_script (issue #529)
  # If relative path doesn't exist, try with BASHUNIT_WORKING_DIR
  if [[ ! -f "$script" && -n "${BASHUNIT_WORKING_DIR:-}" ]]; then
    script="$BASHUNIT_WORKING_DIR/$script"
  fi

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

  local data_provider_function
  data_provider_function=$(
    # shellcheck disable=SC1087
    grep -B 2 -E "function[[:space:]]+$function_name[[:space:]]*\(\)" "$script" 2>/dev/null | \
    sed -nE 's/^[[:space:]]*# *@?data_provider[[:space:]]+//p'
  )

  if [[ -n "$data_provider_function" ]]; then
    bashunit::helper::execute_function_if_exists "$data_provider_function"
  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

  git ls-remote --tags "$BASHUNIT_GIT_REPO" |
    awk '{print $2}' |
    sed 's|^refs/tags/||' |
    sort -Vr |
    head -n 1
}

function bashunit::helper::find_total_tests() {
    local filter=${1:-}
    local files=("${@:2}")

    if [[ ${#files[@]} -eq 0 ]]; then
        echo 0
        return
    fi

    local total_count=0
    local file

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

        local file_count
        file_count=$( (
            # shellcheck source=/dev/null
            source "$file"
            local all_fn_names
            all_fn_names=$(declare -F | awk '{print $3}')
            local filtered_functions
            filtered_functions=$(bashunit::helper::get_functions_to_run "test" "$filter" "$all_fn_names") || true

            local count=0
            if [[ -n "$filtered_functions" ]]; then
                # shellcheck disable=SC2206
                # shellcheck disable=SC2207
                local functions_to_run=($filtered_functions)
                for fn_name in "${functions_to_run[@]}"; do
                    local provider_data=()
                    while IFS=" " read -r line; do
                        provider_data+=("$line")
                    done <<< "$(bashunit::helper::get_provider_data "$fn_name" "$file")"

                    if [[ "${#provider_data[@]}" -eq 0 ]]; then
                        count=$((count + 1))
                    else
                        count=$((count + ${#provider_data[@]}))
                    fi
                done
            fi

            echo "$count"
        ) )

        total_count=$((total_count + file_count))
    done

    echo "$total_count"
}

function bashunit::helper::load_test_files() {
  local filter=$1
  local files=("${@:2}")

  local test_files=()

  if [[ "${#files[@]}" -eq 0 ]]; then
    if [[ -n "${BASHUNIT_DEFAULT_PATH}" ]]; then
      while IFS='' read -r line; do
        test_files+=("$line")
      done < <(bashunit::helper::find_files_recursive "$BASHUNIT_DEFAULT_PATH")
    fi
  else
    test_files=("${files[@]}")
  fi

  printf "%s\n" "${test_files[@]}"
}

function bashunit::helper::load_bench_files() {
  local filter=$1
  local files=("${@:2}")

  local bench_files=()

  if [[ "${#files[@]}" -eq 0 ]]; then
    if [[ -n "${BASHUNIT_DEFAULT_PATH}" ]]; then
      while IFS='' read -r line; do
        bench_files+=("$line")
      done < <(bashunit::helper::find_files_recursive "$BASHUNIT_DEFAULT_PATH" '*[bB]ench.sh')
    fi
  else
    bench_files=("${files[@]}")
  fi

  printf "%s\n" "${bench_files[@]}"
}

#
# @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

  shopt -s extdebug
  local line_number
  line_number=$(declare -F "$fn_name" | awk '{print $2}')
  shopt -u extdebug

  echo "$line_number"
}

function bashunit::helper::generate_id() {
  local basename="$1"
  local sanitized_basename
  sanitized_basename="$(bashunit::helper::normalize_variable_name "$basename")"
  if bashunit::env::is_parallel_run_enabled; then
    echo "${sanitized_basename}_$$_$(bashunit::random_str 6)"
  else
    echo "${sanitized_basename}_$$"
  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)
  if [[ "$input" == *"::"* ]]; then
    file_path="${input%%::*}"
    filter="${input#*::}"
  # Check for :number syntax (line number filter)
  elif [[ "$input" =~ ^(.+):([0-9]+)$ ]]; then
    file_path="${BASH_REMATCH[1]}"
    local line_number="${BASH_REMATCH[2]}"
    # Line number will be resolved to function name later
    filter="__line__:${line_number}"
  else
    file_path="$input"
  fi

  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

  while IFS=: read -r line_num content; do
    # Extract function name from the line
    local fn_name=""
    if [[ "$content" =~ ^[[:space:]]*(function[[:space:]]+)?(test[a-zA-Z_][a-zA-Z0-9_]*)[[:space:]]*\(\) ]]; then
      fn_name="${BASH_REMATCH[2]}"
    fi

    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"
}

# test_title.sh

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

# upgrade.sh

function bashunit::upgrade::upgrade() {
  local script_path
  script_path="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
  local latest_tag
  latest_tag="$(bashunit::helper::get_latest_tag)"

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

  echo "> Upgrading bashunit to latest version"
  cd "$script_path" || exit

  local url="https://github.com/TypedDevs/bashunit/releases/download/$latest_tag/bashunit"
  if ! bashunit::io::download_to "$url" "bashunit"; then
    echo "Failed to download bashunit"
  fi

  chmod u+x "bashunit"

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

# 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 ))
}

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

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

  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_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
    "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
    "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"

  if [[ "$cmd" =~ ^eval ]]; then
    eval "${cmd#eval }" &> /dev/null
  elif [[ "$(command -v "$cmd")" =~ ^alias ]]; then
    eval "$cmd" &> /dev/null
  else
    "$cmd" &> /dev/null
  fi
  return $?
}

function bashunit::handle_bool_assertion_failure() {
  local expected="$1"
  local got="$2"
  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 " "$got"
}

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

  local expected="$1"
  local actual="$2"

  if [[ "$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}" "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 actual_cleaned
  actual_cleaned=$(bashunit::str::strip_ansi "$actual")
  local expected_cleaned
  expected_cleaned=$(bashunit::str::strip_ansi "$expected")

  if [[ "$expected_cleaned" != "$actual_cleaned" ]]; 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_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 actual_cleaned
  actual_cleaned=$(bashunit::str::strip_ansi "$actual")
  local expected_cleaned
  expected_cleaned=$(bashunit::str::strip_ansi "$expected")

  if [[ "$expected_cleaned" == "$actual_cleaned" ]]; 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_cleaned}" "but got " "${actual_cleaned}"
    return
  fi

  bashunit::state::add_assertions_passed
}

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

  local expected="$1"

  if [[ "$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}" "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"

  if [[ "$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}" "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"

  if [[ "$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}" "but got " "${actual}"
    return
  fi

  bashunit::state::add_assertions_passed
}

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

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

  if ! [[ $actual == *"$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 contain" "${expected}"
    return
  fi

  bashunit::state::add_assertions_passed
}

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

  local expected="$1"
  local actual="$2"

  shopt -s nocasematch

  if ! [[ $actual =~ $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 contain" "${expected}"
    shopt -u nocasematch
    return
  fi

  shopt -u nocasematch
  bashunit::state::add_assertions_passed
}

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

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

  if [[ $actual == *"$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 not contain" "${expected}"
    return
  fi

  bashunit::state::add_assertions_passed
}

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

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

  if ! [[ $actual =~ $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 match" "${expected}"
    return
  fi

  bashunit::state::add_assertions_passed
}

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

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

  if [[ $actual =~ $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 not match" "${expected}"
    return
  fi

  bashunit::state::add_assertions_passed
}

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

  local cmd="$1"
  shift

  local expected_exit=0
  local expected_stdout=""
  local expected_stderr=""
  local check_stdout=false
  local check_stderr=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
        ;;
      *)
        shift
        ;;
    esac
  done

  local stdout_file stderr_file
  stdout_file=$(mktemp)
  stderr_file=$(mktemp)

  eval "$cmd" >"$stdout_file" 2>"$stderr_file"
  local exit_code=$?

  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+=$'\n'"stdout: $expected_stdout"
    actual_desc+=$'\n'"stdout: $stdout"
    if [[ "$stdout" != "$expected_stdout" ]]; then
      failed=1
    fi
  fi

  if $check_stderr; then
    expected_desc+=$'\n'"stderr: $expected_stderr"
    actual_desc+=$'\n'"stderr: $stderr"
    if [[ "$stderr" != "$expected_stderr" ]]; then
      failed=1
    fi
  fi

  if [[ $failed -eq 1 ]]; 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_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
  bashunit::assert::should_skip && return 0

  local expected_exit_code="$1"

  if [[ "$actual_exit_code" -ne "$expected_exit_code" ]]; 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_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
  bashunit::assert::should_skip && return 0

  local expected_exit_code=0

  if [[ "$actual_exit_code" -ne "$expected_exit_code" ]]; 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_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
  bashunit::assert::should_skip && return 0

  if [[ "$actual_exit_code" -eq 0 ]]; 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_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
  bashunit::assert::should_skip && return 0

  local expected_exit_code=1

  if [[ "$actual_exit_code" -ne "$expected_exit_code" ]]; 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_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
  bashunit::assert::should_skip && return 0

  local expected_exit_code=127

  if [[ $actual_exit_code -ne "$expected_exit_code" ]]; 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_exit_code}" "to be exactly" "${expected_exit_code}"
    return
  fi

  bashunit::state::add_assertions_passed
}

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

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

  if [[ $actual != "$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 start with" "${expected}"
    return
  fi

  bashunit::state::add_assertions_passed
}

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

  local expected="$1"
  local actual="$2"

  if [[ $actual == "$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 not start with" "${expected}"
    return
  fi

  bashunit::state::add_assertions_passed
}

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

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

  if [[ $actual != *"$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 end with" "${expected}"
    return
  fi

  bashunit::state::add_assertions_passed
}

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

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

  if [[ $actual == *"$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 not end with" "${expected}"
    return
  fi

  bashunit::state::add_assertions_passed
}

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

  local expected="$1"
  local actual="$2"

  if ! [[ "$actual" -lt "$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 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"

  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 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"

  if ! [[ "$actual" -gt "$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 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"

  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 greater or equal than" "${expected}"
    return
  fi

  bashunit::state::add_assertions_passed
}

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

  local expected="$1"
  local input_arr=("${@:2}")
  local input_str
  input_str=$(printf '%s\n' "${input_arr[@]}")

  if [ -z "$input_str" ]; then
    local actual=0
  else
    local actual
    actual=$(echo "$input_str" | wc -l | tr -d '[:blank:]')
    local additional_new_lines
    additional_new_lines=$(grep -o '\\n' <<< "$input_str" | wc -l | tr -d '[:blank:]')
    ((actual+=additional_new_lines))
  fi

  if [[ "$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}" "${input_str}"\
      "to contain number of lines equal to" "${expected}"\
      "but found" "${actual}"
    return
  fi

  bashunit::state::add_assertions_passed
}

# assert_arrays.sh

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 actual=("${@}")

  if ! [[ "${actual[*]}" == *"$expected"* ]]; then
    bashunit::assert::mark_failed
    bashunit::console_results::print_failed_test "${label}" "${actual[*]}" "to contain" "${expected}"
    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 actual=("$@")

  if [[ "${actual[*]}" == *"$expected"* ]]; then
    bashunit::assert::mark_failed
    bashunit::console_results::print_failed_test "${label}" "${actual[*]}" "to not contain" "${expected}"
    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
}

# 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_snapshot.sh
# shellcheck disable=SC2155

function assert_match_snapshot() {
  local actual=$(echo -n "$1" | tr -d '\r')
  local test_fn
  test_fn="$(bashunit::helper::find_test_function_name)"
  local snapshot_file=$(bashunit::snapshot::resolve_file "${2:-}" "$test_fn")

  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() {
  local actual=$(echo -n "$1" | sed 's/\x1B\[[0-9;]*[mK]//g' | tr -d '\r')
  local test_fn
  test_fn="$(bashunit::helper::find_test_function_name)"
  local snapshot_file=$(bashunit::snapshot::resolve_file "${2:-}" "$test_fn")

  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
    local fallback=$(printf '%s' "$snapshot" | sed -e "s|$placeholder|.*|g" -e 's/[][\.^$*+?{}|()]/\\&/g')
    fallback="^${fallback}$"
    echo "$actual" | grep -Eq "$fallback" && return 0 || return 1
  fi
}

function bashunit::snapshot::resolve_file() {
  local file_hint="$1"
  local func_name="$2"

  if [[ -n "$file_hint" ]]; then
    echo "$file_hint"
  else
    local dir="./$(dirname "${BASH_SOURCE[2]}")/snapshots"
    local test_file="$(bashunit::helper::normalize_variable_name "$(basename "${BASH_SOURCE[2]}")")"
    local name="$(bashunit::helper::normalize_variable_name "$func_name").snapshot"
    echo "${dir}/${test_file}.${name}"
  fi
}

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"

  local snapshot
  snapshot=$(tr -d '\r' < "$snapshot_path")

  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=()

function bashunit::unmock() {
  local command=$1

  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="${variable}_times_file"
      local params_file_var="${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() { echo \"$($CAT)\" ; }"
  fi

  export -f "${command?}"

  _BASHUNIT_MOCKED_FUNCTIONS+=("$command")
}

function bashunit::spy() {
  local command=$1
  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 "${variable}_times_file"="$times_file"
  export "${variable}_params_file"="$params_file"

  eval "function $command() {
    local raw=\"\$*\"
    local serialized=\"\"
    local arg
    for arg in \"\$@\"; do
      serialized+=\"\$(printf '%q' \"\$arg\")$'\\x1f'\"
    done
    serialized=\${serialized%$'\\x1f'}
    printf '%s\x1e%s\\n' \"\$raw\" \"\$serialized\" >> '$params_file'
    local _c
    _c=\$(cat '$times_file' 2>/dev/null || echo 0)
    _c=\$((_c+1))
    echo \"\$_c\" > '$times_file'
  }"

  export -f "${command?}"

  _BASHUNIT_MOCKED_FUNCTIONS+=("$command")
}

function assert_have_been_called() {
  local command=$1
  local variable
  variable="$(bashunit::helper::normalize_variable_name "$command")"
  local file_var="${variable}_times_file"
  local times=0
  if [[ -f "${!file_var-}" ]]; then
    times=$(cat "${!file_var}" 2>/dev/null || 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=""
  if [[ ${!#} =~ ^[0-9]+$ ]]; then
    index=${!#}
    set -- "${@:1:$#-1}"
  fi

  local expected="$*"

  local variable
  variable="$(bashunit::helper::normalize_variable_name "$command")"
  local file_var="${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="${variable}_times_file"
  local times=0
  if [[ -f "${!file_var-}" ]]; then
    times=$(cat "${!file_var}" 2>/dev/null || 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_not_called() {
  local command=$1
  local label="${2:-$(bashunit::helper::normalize_test_function_name "${FUNCNAME[1]}")}"
  assert_have_been_called_times 0 "$command" "$label"
}

# 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=()

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_failed() {
  bashunit::reports::add_test "$1" "$2" "$3" "$4" "failed"
}

function bashunit::reports::add_test() {
  # Skip tracking when no report output is requested
  [[ -n "${BASHUNIT_LOG_JUNIT:-}" || -n "${BASHUNIT_REPORT_HTML:-}" ]] || return 0

  local file="$1"
  local test_name="$2"
  local duration="$3"
  local assertions="$4"
  local status="$5"

  _BASHUNIT_REPORTS_TEST_FILES+=("$file")
  _BASHUNIT_REPORTS_TEST_NAMES+=("$test_name")
  _BASHUNIT_REPORTS_TEST_STATUSES+=("$status")
  _BASHUNIT_REPORTS_TEST_ASSERTIONS+=("$assertions")
  _BASHUNIT_REPORTS_TEST_DURATIONS+=("$duration")
}

function bashunit::reports::generate_junit_xml() {
  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)

  {
    echo "<?xml version=\"1.0\" encoding=\"UTF-8\"?>"
    echo "<testsuites>"
    echo "  <testsuite name=\"bashunit\" tests=\"${#_BASHUNIT_REPORTS_TEST_NAMES[@]}\""
    echo "             passed=\"$test_passed\" failures=\"$tests_failed\" incomplete=\"$tests_incomplete\""
    echo "             skipped=\"$tests_skipped\" snapshot=\"$tests_snapshot\""
    echo "             time=\"$time\">"

    for i in "${!_BASHUNIT_REPORTS_TEST_NAMES[@]}"; do
      local file="${_BASHUNIT_REPORTS_TEST_FILES[$i]}"
      local name="${_BASHUNIT_REPORTS_TEST_NAMES[$i]}"
      local assertions="${_BASHUNIT_REPORTS_TEST_ASSERTIONS[$i]}"
      local status="${_BASHUNIT_REPORTS_TEST_STATUSES[$i]}"
      local test_time="${_BASHUNIT_REPORTS_TEST_DURATIONS[$i]}"

      echo "    <testcase file=\"$file\""
      echo "        name=\"$name\""
      echo "        status=\"$status\""
      echo "        assertions=\"$assertions\""
      echo "        time=\"$test_time\">"
      echo "    </testcase>"
    done

    echo "  </testsuite>"
    echo "</testsuites>"
  } > "$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
  local temp_file="temp_test_cases.txt"

  # Collect test cases by file
  : > "$temp_file"  # Clear temp file if it exists
  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 "  </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=""
    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"
}

# 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
}

function bashunit::runner::load_test_files() {
  local filter=$1
  shift
  local files=("${@}")
  local scripts_ids=()

  for test_file in "${files[@]}"; do
    if [[ ! -f $test_file ]]; then
      continue
    fi
    unset BASHUNIT_CURRENT_TEST_ID
    export BASHUNIT_CURRENT_SCRIPT_ID="$(bashunit::helper::generate_id "${test_file}")"
    scripts_ids+=("${BASHUNIT_CURRENT_SCRIPT_ID}")
    bashunit::internal_log "Loading file" "$test_file"
    # shellcheck source=/dev/null
    source "$test_file"
    # Update function cache after sourcing new test file
    _BASHUNIT_CACHED_ALL_FUNCTIONS=$(declare -F | awk '{print $3}')
    # 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)
      local filtered_functions
      filtered_functions=$(bashunit::helper::get_functions_to_run "test" "$filter" "$_BASHUNIT_CACHED_ALL_FUNCTIONS")
      if [[ -n "$filtered_functions" ]]; then
        # shellcheck disable=SC2206
        local functions_to_run=($filtered_functions)
        local additional_failures=$((${#functions_to_run[@]} - 1))
        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
      if ! bashunit::parallel::is_enabled; then
        bashunit::cleanup_script_temp_files
      fi
      bashunit::runner::restore_workdir
      continue
    fi
    if bashunit::parallel::is_enabled; then
      bashunit::runner::call_test_functions "$test_file" "$filter" 2>/dev/null &
    else
      bashunit::runner::call_test_functions "$test_file" "$filter"
    fi
    bashunit::runner::run_tear_down_after_script "$test_file"
    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" && kill "$spinner_pid" &>/dev/null
    printf "\r  \r" # Clear the spinner output
    for script_id in "${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 files=("${@}")

  for bench_file in "${files[@]}"; do
    [[ -f $bench_file ]] || continue
    unset BASHUNIT_CURRENT_TEST_ID
    export BASHUNIT_CURRENT_SCRIPT_ID="$(bashunit::helper::generate_id "${bench_file}")"
    # shellcheck source=/dev/null
    source "$bench_file"
    # Update function cache after sourcing new bench file
    _BASHUNIT_CACHED_ALL_FUNCTIONS=$(declare -F | awk '{print $3}')
    # 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
        # shellcheck disable=SC2206
        local functions_to_run=($filtered_functions)
        local additional_failures=$((${#functions_to_run[@]} - 1))
        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

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

  local delay=0.1
  local spin_chars="|/-\\"
  while true; do
    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"

  # Filter the names down to the ones defined in the script, sort them by line number
  shopt -s extdebug
  # shellcheck disable=SC2086
  declare -F $all_fn_names |
    awk -v s="$script" '$3 == s {print $1" " $2}' |
    sort -k2 -n |
    awk '{print $1}'
  shopt -u extdebug
}

function bashunit::runner::parse_data_provider_args() {
  local input="$1"
  local current_arg=""
  local in_quotes=false
  local quote_char=""
  local escaped=false
  local i
  local arg
  local encoded_arg
  local -a args=()

  # Check for shell metacharacters that would break eval or cause globbing
  local has_metachar=false
  if [[ "$input" =~ [^\\][\|\&\;\*] ]] || [[ "$input" =~ ^[\|\&\;\*] ]]; 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 && [[ ${#args[@]} -gt 0 ]]; then
    # Successfully parsed - remove sentinel if present
    local last_idx=$((${#args[@]} - 1))
    if [[ -z "${args[$last_idx]}" ]]; then
      unset 'args[$last_idx]'
    fi
    # Print args and return early
    for arg in "${args[@]}"; do
      encoded_arg="$(bashunit::helper::encode_base64 "${arg}")"
      printf '%s\n' "$encoded_arg"
    done
    return
  fi

  # Fallback: parse args from the input string into an array, respecting quotes and escapes
  for ((i=0; i<${#input}; i++)); do
    local char="${input:$i:1}"
    if [ "$escaped" = true ]; then
      case "$char" in
        t) current_arg+=$'\t' ;;
        n) current_arg+=$'\n' ;;
        *) 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
            quote_char="'"
            # Skip the $
            i=$((i + 1))
          else
            current_arg+="$char"
          fi
          ;;
        "'" | '"')
          in_quotes=true
          quote_char="$char"
          ;;
        " " | $'\t')
          # Only add non-empty arguments to avoid duplicates from consecutive separators
          if [[ -n "$current_arg" ]]; then
            args+=("$current_arg")
          fi
          current_arg=""
          ;;
        *)
          current_arg+="$char"
          ;;
      esac
    elif [ "$char" = "$quote_char" ]; then
      in_quotes=false
      quote_char=""
    else
      current_arg+="$char"
    fi
  done
  args+=("$current_arg")
  # Remove all trailing empty strings
  while [[ ${#args[@]} -gt 0 ]]; do
    local last_idx=$((${#args[@]} - 1))
    if [[ -z "${args[$last_idx]}" ]]; then
      unset 'args[$last_idx]'
    else
      break
    fi
  done
  # Print one arg per line to stdout, base64-encoded to preserve newlines in the data
  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 prefix="test"
  # Use cached function names for better performance
  local filtered_functions
  filtered_functions=$(bashunit::helper::get_functions_to_run \
    "$prefix" "$filter" "$_BASHUNIT_CACHED_ALL_FUNCTIONS")
  # shellcheck disable=SC2207
  local functions_to_run=($(bashunit::runner::functions_for_script "$script" "$filtered_functions"))

  if [[ "${#functions_to_run[@]}" -le 0 ]]; then
    return
  fi

  bashunit::runner::render_running_file_header "$script"
  bashunit::helper::check_duplicate_functions "$script" || true

  for fn_name in "${functions_to_run[@]}"; do
    if bashunit::parallel::is_enabled && bashunit::parallel::must_stop_on_failure; then
      break
    fi

    local provider_data=()
    while IFS=" " read -r line; do
      provider_data+=("$line")
    done <<< "$(bashunit::helper::get_provider_data "$fn_name" "$script")"

    # No data provider found
    if [[ "${#provider_data[@]}" -eq 0 ]]; then
      bashunit::runner::run_test "$script" "$fn_name"
      unset fn_name
      continue
    fi

    # Execute the test function for each line of data
    for data in "${provider_data[@]}"; do
      local parsed_data=()
      while IFS= read -r line; do
        parsed_data+=( "$(bashunit::helper::decode_base64 "${line}")" )
      done <<< "$(bashunit::runner::parse_data_provider_args "$data")"
      bashunit::runner::run_test "$script" "$fn_name" "${parsed_data[@]}"
    done
    unset fn_name
  done

  if ! bashunit::env::is_simple_output_enabled; then
    echo ""
  fi
}

function bashunit::runner::call_bench_functions() {
  local script="$1"
  local filter="$2"
  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")
  # shellcheck disable=SC2207
  local functions_to_run=($(bashunit::runner::functions_for_script "$script" "$filtered_functions"))

  if [[ "${#functions_to_run[@]}" -le 0 ]]; then
    return
  fi

  if bashunit::env::is_bench_mode_enabled; then
    bashunit::runner::render_running_file_header "$script"
  fi

  for fn_name in "${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 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

  if ! 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
}

function bashunit::runner::run_test() {
  local start_time
  start_time=$(bashunit::clock::now)

  local test_file="$1"
  shift
  local fn_name="$1"
  shift

  bashunit::internal_log "Running test" "$fn_name" "$*"
  # Export a unique test identifier so that test doubles can
  # create temporary files scoped per test run. This prevents
  # race conditions when running tests in parallel.
  export BASHUNIT_CURRENT_TEST_ID="$(bashunit::helper::generate_id "$fn_name")"

  bashunit::state::reset_test_title

  local interpolated_fn_name="$(bashunit::helper::interpolate_function_name "$fn_name" "$@")"
  if [[ "$interpolated_fn_name" != "$fn_name" ]]; then
    bashunit::state::set_current_test_interpolated_function_name "$interpolated_fn_name"
  else
    bashunit::state::reset_current_test_interpolated_function_name
  fi
  local current_assertions_failed="$(bashunit::state::get_assertions_failed)"
  local current_assertions_snapshot="$(bashunit::state::get_assertions_snapshot)"
  local current_assertions_incomplete="$(bashunit::state::get_assertions_incomplete)"
  local current_assertions_skipped="$(bashunit::state::get_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=$(
    # shellcheck disable=SC2064
    trap 'exit_code=$?; bashunit::runner::cleanup_on_exit "$test_file" "$exit_code"' EXIT
    bashunit::state::initialize_assertions_count

    # Source login shell profiles if enabled
    if bashunit::env::is_login_shell_enabled; then
      # 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
    fi

    # Run set_up and capture exit code without || to preserve errexit behavior
    local setup_exit_code=0
    bashunit::runner::run_set_up "$test_file"
    setup_exit_code=$?
    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 -euo pipefail
    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

  )

  # Closes FD 3, which was used temporarily to hold the original stdout.
  exec 3>&-

  local end_time=$(bashunit::clock::now)
  local duration_ns=$((end_time - start_time))
  local duration=$((duration_ns / 1000000))

  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" "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: ${test_execution_result%%##ASSERTIONS_*}"
    printf "%s\n" "##ASSERTIONS_${test_execution_result#*##ASSERTIONS_}"
    printf '%*s\n' "$TERMINAL_WIDTH" '' | tr ' ' '-'
  fi

  local subshell_output=$(bashunit::runner::decode_subshell_output "$test_execution_result")

  if [[ -n "$subshell_output" ]]; then
    # Formatted as "[type]line" @see `bashunit::state::print_line()`
    local type="${subshell_output%%]*}" # Remove everything after "]"
    type="${type#[}"                    # Remove the leading "["
    local line="${subshell_output#*]}"  # Remove everything before and including "]"

    # Replace [type] with a newline to split the messages
    line="${line//\[failed\]/$'\n'}"       # Replace [failed] with newline
    line="${line//\[skipped\]/$'\n'}"      # Replace [skipped] with newline
    line="${line//\[incomplete\]/$'\n'}"   # Replace [incomplete] with newline

    bashunit::state::print_line "$type" "$line"

    subshell_output=$line
  fi

  local runtime_output="${test_execution_result%%##ASSERTIONS_*}"

  local runtime_error=""
  for error 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"; do
    if [[ "$runtime_output" == *"$error"* ]]; then
      runtime_error="${runtime_output#*: }"      # Remove everything up to and including ": "
      runtime_error="${runtime_error//$'\n'/}"   # Remove all newlines using parameter expansion
      break
    fi
  done

  bashunit::runner::parse_result "$fn_name" "$test_execution_result" "$@"

  local total_assertions="$(bashunit::state::calculate_total_assertions "$test_execution_result")"
  local test_exit_code="$(bashunit::state::get_test_exit_code)"

  local encoded_test_title
  encoded_test_title="${test_execution_result##*##TEST_TITLE=}"
  encoded_test_title="${encoded_test_title%%##*}"
  local test_title=""
  [[ -n "$encoded_test_title" ]] && test_title="$(bashunit::helper::decode_base64 "$encoded_test_title")"

  local encoded_hook_failure
  encoded_hook_failure="${test_execution_result##*##TEST_HOOK_FAILURE=}"
  encoded_hook_failure="${encoded_hook_failure%%##*}"
  local hook_failure=""
  if [[ "$encoded_hook_failure" != "$test_execution_result" ]]; then
    hook_failure="$encoded_hook_failure"
  fi

  local encoded_hook_message
  encoded_hook_message="${test_execution_result##*##TEST_HOOK_MESSAGE=}"
  encoded_hook_message="${encoded_hook_message%%##*}"
  local hook_message=""
  if [[ -n "$encoded_hook_message" ]]; then
    hook_message="$(bashunit::helper::decode_base64 "$encoded_hook_message")"
  fi

  bashunit::set_test_title "$test_title"
  local label
  label="$(bashunit::helper::normalize_test_function_name "$fn_name" "$interpolated_fn_name")"
  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
    failure_label="$(bashunit::helper::normalize_test_function_name "$hook_failure")"
    failure_function="$hook_failure"
  fi

  if [[ -n $runtime_error || $test_exit_code -ne 0 ]]; then
    bashunit::state::add_tests_failed
    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
    bashunit::console_results::print_error_test "$failure_function" "$error_message"
    bashunit::reports::add_test_failed "$test_file" "$failure_label" "$duration" "$total_assertions"
    bashunit::runner::write_failure_result_output "$test_file" "$failure_function" "$error_message"
    bashunit::internal_log "Test error" "$failure_label" "$error_message"
    return
  fi

  if [[ "$current_assertions_failed" != "$(bashunit::state::get_assertions_failed)" ]]; then
    bashunit::state::add_tests_failed
    bashunit::reports::add_test_failed "$test_file" "$label" "$duration" "$total_assertions"
    bashunit::runner::write_failure_result_output "$test_file" "$fn_name" "$subshell_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::state::get_assertions_snapshot)" ]]; then
    bashunit::state::add_tests_snapshot
    bashunit::console_results::print_snapshot_test "$label"
    bashunit::reports::add_test_snapshot "$test_file" "$label" "$duration" "$total_assertions"
    bashunit::internal_log "Test snapshot" "$label"
    return
  fi

  if [[ "$current_assertions_incomplete" != "$(bashunit::state::get_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::state::get_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

  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
  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"

  set +e
  # 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
}

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%%##*}"
  bashunit::helper::decode_base64 "$test_output_base64"
}

function bashunit::runner::parse_result() {
  local fn_name=$1
  shift
  local execution_result=$1
  shift
  local args=("$@")

  if bashunit::parallel::is_enabled; then
    bashunit::runner::parse_result_parallel "$fn_name" "$execution_result" "${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 args=("$@")

  local test_suite_dir="${TEMP_DIR_PARALLEL_TEST_SUITE}/$(basename "$test_file" .sh)"
  mkdir -p "$test_suite_dir"

  local sanitized_args
  sanitized_args=$(echo "${args[*]}" | tr '[:upper:]' '[:lower:]' | sed -E 's/[^a-z0-9]+/-/g; s/^-|-$//')
  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
function bashunit::runner::parse_result_sync() {
  local fn_name=$1
  local execution_result=$2

  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

  # Use pre-compiled regex constant
  if [[ $result_line =~ $_BASHUNIT_RUNNER_PARSE_RESULT_REGEX ]]; then
    assertions_failed="${BASH_REMATCH[1]}"
    assertions_passed="${BASH_REMATCH[2]}"
    assertions_skipped="${BASH_REMATCH[3]}"
    assertions_incomplete="${BASH_REMATCH[4]}"
    assertions_snapshot="${BASH_REMATCH[5]}"
    test_exit_code="${BASH_REMATCH[6]}"
  fi

  bashunit::internal_log "[SYNC]" "fn_name:$fn_name" "execution_result:$execution_result"

  ((_BASHUNIT_ASSERTIONS_PASSED += assertions_passed)) || true
  ((_BASHUNIT_ASSERTIONS_FAILED += assertions_failed)) || true
  ((_BASHUNIT_ASSERTIONS_SKIPPED += assertions_skipped)) || true
  ((_BASHUNIT_ASSERTIONS_INCOMPLETE += assertions_incomplete)) || true
  ((_BASHUNIT_ASSERTIONS_SNAPSHOT += assertions_snapshot)) || true
  ((_BASHUNIT_TEST_EXIT_CODE += test_exit_code)) || true

  bashunit::internal_log "result_summary" \
    "failed:$assertions_failed" \
    "passed:$assertions_passed" \
    "skipped:$assertions_skipped" \
    "incomplete:$assertions_incomplete" \
    "snapshot:$assertions_snapshot" \
    "exit_code:$test_exit_code"
}

function bashunit::runner::write_failure_result_output() {
  local test_file=$1
  local fn_name=$2
  local error_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_failed)
  fi

  echo -e "$test_nr) $test_file:$line_number\n$error_msg" >> "$FAILURES_OUTPUT_PATH"
}

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::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"
  bashunit::reports::add_test_failed "$test_file" "$(bashunit::helper::normalize_test_function_name "$hook_name")" 0 0
  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 errexit and errtrace to catch any failing command in the hook.
  # The ERR trap saves the exit status to a global variable (since return value
  # from trap doesn't propagate properly), disables errexit (to prevent caller
  # from exiting) and returns from the hook function, preventing subsequent
  # commands from executing.
  # Variables set before the failure are preserved since we don't use a subshell.
  _BASHUNIT_HOOK_ERR_STATUS=0
  set -eE
  trap '_BASHUNIT_HOOK_ERR_STATUS=$?; set +eE; trap - ERR; return $_BASHUNIT_HOOK_ERR_STATUS' ERR

  {
    "$hook_name"
  } >"$hook_output_file" 2>&1

  # Capture exit status from global variable and clean up
  status=$_BASHUNIT_HOOK_ERR_STATUS
  trap - ERR
  set +eE

  if [[ -f "$hook_output_file" ]]; then
    hook_output=""
    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" ]]; 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"
  bashunit::runner::execute_file_hook 'set_up_before_script' "$test_file" true
}

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 errexit and errtrace to catch any failing command in the hook.
  # The ERR trap saves the exit status to a global variable (since return value
  # from trap doesn't propagate properly), disables errexit (to prevent caller
  # from exiting) and returns from the hook function, preventing subsequent
  # commands from executing.
  # Variables set before the failure are preserved since we don't use a subshell.
  _BASHUNIT_HOOK_ERR_STATUS=0
  set -eE
  trap '_BASHUNIT_HOOK_ERR_STATUS=$?; set +eE; trap - ERR; return $_BASHUNIT_HOOK_ERR_STATUS' ERR

  {
    "$hook_name"
  } >"$hook_output_file" 2>&1

  # Capture exit status from global variable and clean up
  status=$_BASHUNIT_HOOK_ERR_STATUS
  trap - ERR
  set +eE

  if [[ -f "$hook_output_file" ]]; then
    hook_output=""
    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::state::get_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() {
  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"
  bashunit::runner::execute_file_hook 'tear_down_after_script' "$test_file"
}

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'
}

# 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'
#!/usr/bin/env 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'
#!/usr/bin/env 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 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
##

declare -r LEARN_TEMP_DIR="/tmp/bashunit_learn_$$"
declare -r LEARN_PROGRESS_FILE="$HOME/.bashunit_learn_progress"

##
# Initialize learning environment
##
function bashunit::learn::init() {
  mkdir -p "$LEARN_TEMP_DIR"
  mkdir -p tests
}

##
# Cleanup learning environment
##
function bashunit::learn::cleanup() {
  rm -rf "$LEARN_TEMP_DIR"
}

##
# 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 -q "^$lesson$" "$LEARN_PROGRESS_FILE"
}

##
# 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

  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++))
    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
───────────────────────────────────────────────────────────────
#!/usr/bin/env 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 -q "assert_same" "$test_file"; 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
───────────────────────────────────────────────────────────────
#!/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"
}
───────────────────────────────────────────────────────────────

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 -q "assert_contains" "$test_file" || \
      ! grep -q "assert_matches" "$test_file" || \
      ! grep -q "assert_not_empty" "$test_file"; 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
───────────────────────────────────────────────────────────────
#!/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"
}
───────────────────────────────────────────────────────────────

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 -q "function set_up()" "$test_file" || \
      ! grep -q "function tear_down()" "$test_file"; 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)
───────────────────────────────────────────────────────────────
#!/usr/bin/env bash

function add() {
  echo $(($1 + $2))
}
───────────────────────────────────────────────────────────────

File: tests/calculator_test.sh (test file)
───────────────────────────────────────────────────────────────
#!/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"
}
───────────────────────────────────────────────────────────────

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 -q "source" "$test_file"; 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)
───────────────────────────────────────────────────────────────
#!/usr/bin/env bash
name=${1:-World}
echo "Hello, $name!"
───────────────────────────────────────────────────────────────

File: tests/greeter_test.sh (test file)
───────────────────────────────────────────────────────────────
#!/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"
}
───────────────────────────────────────────────────────────────

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)
───────────────────────────────────────────────────────────────
#!/usr/bin/env bash

function get_system_info() {
  echo "OS: $(uname -s)"
}
───────────────────────────────────────────────────────────────

File: tests/system_info_test.sh (test file)
───────────────────────────────────────────────────────────────
#!/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"
}
───────────────────────────────────────────────────────────────

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 -q "mock" "$test_file"; 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
───────────────────────────────────────────────────────────────
#!/usr/bin/env bash

function deploy_app() {
  git push origin main
  docker build -t myapp .
  docker push myapp
}
───────────────────────────────────────────────────────────────

File: deploy_test.sh
───────────────────────────────────────────────────────────────
#!/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
}
───────────────────────────────────────────────────────────────

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 -q "spy" "$test_file"; 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
───────────────────────────────────────────────────────────────
#!/usr/bin/env bash

function is_valid_email() {
  [[ $1 =~ ^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$ ]]
}
───────────────────────────────────────────────────────────────

File: validator_test.sh
───────────────────────────────────────────────────────────────
#!/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\""
}
───────────────────────────────────────────────────────────────

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 -q "function data_provider_" "$test_file"; 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
───────────────────────────────────────────────────────────────
#!/usr/bin/env 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
───────────────────────────────────────────────────────────────
#!/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'"
}
───────────────────────────────────────────────────────────────

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

  if ! grep -q "assert_successful_code\|assert_exit_code\|assert_general_error" "$test_file"; 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
───────────────────────────────────────────────────────────────
#!/usr/bin/env 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
───────────────────────────────────────────────────────────────
#!/usr/bin/env 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 missing_components=()

  if ! grep -q "function set_up()" "$test_file"; then
    missing_components+=("set_up function")
  fi

  if ! grep -q "function tear_down()" "$test_file"; then
    missing_components+=("tear_down function")
  fi

  if [[ ${#missing_components[@]} -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.typeddevs.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
}

# 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__'
__BASHUNIT_DOCS_EOF__
}

function bashunit::doc::print_asserts() {
  local filter="${1:-}"
  local line
  local docstring=""
  local fn=""
  local should_print=0

  while IFS='' read -r line || [[ -n "$line" ]]; do
    if [[ $line =~ ^##\ ([A-Za-z0-9_]+) ]]; then
      fn="${BASH_REMATCH[1]}"
      if [[ -z "$filter" || "$fn" == *"$filter"* ]]; then
        should_print=1
        echo "$line"
        docstring=""
      else
        should_print=0
      fi
      continue
    fi

    if (( should_print )); then
      if [[ "$line" =~ ^\`\`\` ]]; then
        echo "--------------"
        echo "$docstring"
        should_print=0
        continue
      fi

      [[ "$line" == "::: code-group"* ]] && continue

      # Remove markdown link brackets and anchor tags
      line="${line//[\[\]]/}"
      line="$(sed -E 's/ *\(#[-a-z0-9]+\)//g' <<< "$line")"
      docstring+="$line"$'\n'
    fi
  done <<< "$(bashunit::doc::get_embedded_docs)"
}

# 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
}

# main.sh

#############################
# Subcommand: test
#############################
function bashunit::main::cmd_test() {
  local filter=""
  local raw_args=()
  local args=()
  local assert_fn=""

  # Parse test-specific options
  while [[ $# -gt 0 ]]; do
    case "$1" in
      -a|--assert)
        assert_fn="$2"
        shift
        ;;
      -f|--filter)
        filter="$2"
        shift
        ;;
      -s|--simple)
        export BASHUNIT_SIMPLE_OUTPUT=true
        ;;
      --detailed)
        export BASHUNIT_SIMPLE_OUTPUT=false
        ;;
      --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)
        export BASHUNIT_STOP_ON_FAILURE=true
        ;;
      -p|--parallel)
        export BASHUNIT_PARALLEL_RUN=true
        ;;
      --no-parallel)
        export BASHUNIT_PARALLEL_RUN=false
        ;;
      -e|--env|--boot)
        # Support: --env "bootstrap.sh arg1 arg2"
        local boot_file="${2%% *}"
        local boot_args="${2#* }"
        if [[ "$boot_args" != "$2" ]]; then
          export BASHUNIT_BOOTSTRAP_ARGS="$boot_args"
        fi
        # shellcheck disable=SC1090,SC2086
        source "$boot_file" ${BASHUNIT_BOOTSTRAP_ARGS:-}
        shift
        ;;
      --log-junit)
        export BASHUNIT_LOG_JUNIT="$2"
        shift
        ;;
      -r|--report-html)
        export BASHUNIT_REPORT_HTML="$2"
        shift
        ;;
      --no-output)
        export BASHUNIT_NO_OUTPUT=true
        ;;
      -vvv|--verbose)
        export BASHUNIT_VERBOSE=true
        ;;
      -h|--help)
        bashunit::console_header::print_test_help
        exit 0
        ;;
      --show-skipped)
        export BASHUNIT_SHOW_SKIPPED=true
        ;;
      --show-incomplete)
        export BASHUNIT_SHOW_INCOMPLETE=true
        ;;
      --strict)
        export BASHUNIT_STRICT_MODE=true
        ;;
      -R|--run-all)
        export BASHUNIT_STOP_ON_ASSERTION_FAILURE=false
        ;;
      --preserve-env)
        export BASHUNIT_PRESERVE_ENV=true
        ;;
      -l|--login)
        export BASHUNIT_LOGIN_SHELL=true
        ;;
      *)
        raw_args+=("$1")
        ;;
    esac
    shift
  done

  # 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[@]} -gt 0 ]]; then
    if [[ -n "$assert_fn" ]]; then
      # Assert mode: pass args as-is without file path processing
      args=("${raw_args[@]}")
    else
      # Test mode: process file paths and extract inline filters
      for arg in "${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

        while IFS= read -r file; do
          args+=("$file")
        done < <(bashunit::helper::find_files_recursive "$parsed_path" '*[tT]est.sh')
      done

      # Resolve line number filter to function name
      if [[ "$inline_filter" == "__line__:"* ]]; then
        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[@]} -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
      fi

      # Use inline filter if no -f filter was provided
      if [[ -z "$filter" && -n "$inline_filter" ]]; then
        filter="$inline_filter"
      fi
    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
    bashunit::main::exec_assert "$assert_fn" "${args[@]}"
  else
    bashunit::main::exec_tests "$filter" "${args[@]}"
  fi
}

#############################
# Subcommand: bench
#############################
function bashunit::main::cmd_bench() {
  local filter=""
  local raw_args=()
  local args=()

  export BASHUNIT_BENCH_MODE=true
  source "$BASHUNIT_ROOT_DIR/src/benchmark.sh"

  # Parse bench-specific options
  while [[ $# -gt 0 ]]; do
    case "$1" in
      -f|--filter)
        filter="$2"
        shift
        ;;
      -s|--simple)
        export BASHUNIT_SIMPLE_OUTPUT=true
        ;;
      --detailed)
        export BASHUNIT_SIMPLE_OUTPUT=false
        ;;
      -e|--env|--boot)
        # Support: --env "bootstrap.sh arg1 arg2"
        local boot_file="${2%% *}"
        local boot_args="${2#* }"
        if [[ "$boot_args" != "$2" ]]; then
          export BASHUNIT_BOOTSTRAP_ARGS="$boot_args"
        fi
        # shellcheck disable=SC1090,SC2086
        source "$boot_file" ${BASHUNIT_BOOTSTRAP_ARGS:-}
        shift
        ;;
      -vvv|--verbose)
        export BASHUNIT_VERBOSE=true
        ;;
      --preserve-env)
        export BASHUNIT_PRESERVE_ENV=true
        ;;
      -l|--login)
        export BASHUNIT_LOGIN_SHELL=true
        ;;
      -h|--help)
        bashunit::console_header::print_bench_help
        exit 0
        ;;
      *)
        raw_args+=("$1")
        ;;
    esac
    shift
  done

  # Expand positional arguments
  if [[ ${#raw_args[@]} -gt 0 ]]; then
    for arg in "${raw_args[@]}"; do
      while IFS= read -r file; do
        args+=("$file")
      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

  bashunit::main::exec_benchmarks "$filter" "${args[@]}"
}

#############################
# Subcommand: doc
#############################
function bashunit::main::cmd_doc() {
  if [[ "${1:-}" == "-h" || "${1:-}" == "--help" ]]; then
    bashunit::console_header::print_doc_help
    exit 0
  fi

  bashunit::doc::print_asserts "${1:-}"
  exit 0
}

#############################
# Subcommand: init
#############################
function bashunit::main::cmd_init() {
  if [[ "${1:-}" == "-h" || "${1:-}" == "--help" ]]; then
    bashunit::console_header::print_init_help
    exit 0
  fi

  bashunit::init::project "${1:-}"
  exit 0
}

#############################
# Subcommand: learn
#############################
function bashunit::main::cmd_learn() {
  if [[ "${1:-}" == "-h" || "${1:-}" == "--help" ]]; then
    bashunit::console_header::print_learn_help
    exit 0
  fi

  bashunit::learn::start
  exit 0
}

#############################
# Subcommand: upgrade
#############################
function bashunit::main::cmd_upgrade() {
  if [[ "${1:-}" == "-h" || "${1:-}" == "--help" ]]; then
    bashunit::console_header::print_upgrade_help
    exit 0
  fi

  bashunit::upgrade::upgrade
  exit 0
}

#############################
# Subcommand: assert
#############################
function bashunit::main::cmd_assert() {
  if [[ "${1:-}" == "-h" || "${1:-}" == "--help" ]]; then
    bashunit::console_header::print_assert_help
    exit 0
  fi

  local assert_fn="${1:-}"
  if [[ -z "$assert_fn" ]]; then
    printf "%sError: Assert function name is required.%s\n" "${_BASHUNIT_COLOR_FAILED}" "${_BASHUNIT_COLOR_DEFAULT}"
    bashunit::console_header::print_assert_help
    exit 1
  fi

  shift
  local args=("$@")

  # Disable strict mode for assert execution
  set +euo pipefail

  bashunit::main::exec_assert "$assert_fn" "${args[@]}"
  exit $?
}

#############################
# Test execution
#############################
function bashunit::main::exec_tests() {
  local filter=$1
  local files=("${@:2}")

  local test_files=()
  while IFS= read -r line; do
    test_files+=("$line")
  done < <(bashunit::helper::load_test_files "$filter" "${files[@]}")

  bashunit::internal_log "exec_tests" "filter:$filter" "files:${test_files[*]}"

  if [[ ${#test_files[@]} -eq 0 || -z "${test_files[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

  # 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

  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

  bashunit::console_header::print_version_with_env "$filter" "${test_files[@]}"

  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" "${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

  bashunit::console_results::print_failing_tests_and_reset
  bashunit::console_results::print_incomplete_tests_and_reset
  bashunit::console_results::print_skipped_tests_and_reset
  bashunit::console_results::render_result
  exit_code=$?

  if [[ -n "$BASHUNIT_LOG_JUNIT" ]]; then
    bashunit::reports::generate_junit_xml "$BASHUNIT_LOG_JUNIT"
  fi

  if [[ -n "$BASHUNIT_REPORT_HTML" ]]; then
    bashunit::reports::generate_report_html "$BASHUNIT_REPORT_HTML"
  fi

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

  bashunit::internal_log "Finished tests" "exit_code:$exit_code"
  exit $exit_code
}

function bashunit::main::exec_benchmarks() {
  local filter=$1
  local files=("${@:2}")

  local bench_files=()
  while IFS= read -r line; do
    bench_files+=("$line")
  done < <(bashunit::helper::load_bench_files "$filter" "${files[@]}")

  bashunit::internal_log "exec_benchmarks" "filter:$filter" "files:${bench_files[*]}"

  if [[ ${#bench_files[@]} -eq 0 || -z "${bench_files[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
  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_incomplete_tests_and_reset
  bashunit::console_results::print_skipped_tests_and_reset
  bashunit::console_results::render_result
  bashunit::cleanup_script_temp_files
  if bashunit::parallel::is_enabled; then
    bashunit::parallel::cleanup
  fi
  exit 1
}

function bashunit::main::exec_assert() {
  local original_assert_fn=$1
  local 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[@]} - 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+=("$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%% *}") ]]; 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 -q 'inner_exit_code:[0-9]*'; then
      inner_exit_code=$(echo "$last_line" | grep -o 'inner_exit_code:[0-9]*' | cut -d':' -f2)
      if ! [[ $inner_exit_code =~ ^[0-9]+$ ]]; 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
}

#!/usr/bin/env bash
set -euo pipefail

declare -r BASHUNIT_MIN_BASH_VERSION="3.2"

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.2).
    current_version="$(bash --version | head -n1 | cut -d' ' -f4 | cut -d. -f1,2)"
  fi

  local major minor
  IFS=. read -r major minor _ <<< "$current_version"

  if (( major < 3 )) || { (( major == 3 )) && (( minor < 2 )); }; 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.29.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
    --preserve-env)
      export BASHUNIT_PRESERVE_ENV=true
      ;;
    -l|--login)
      export BASHUNIT_LOGIN_SHELL=true
      ;;
  esac
done


bashunit::check_os::init
bashunit::clock::init

# Subcommand detection
_SUBCOMMAND=""

case "${1:-}" in
  test|bench|doc|init|learn|upgrade|assert)
    _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 "$@" ;;
esac
