#!/usr/bin/env bash
set -e

export BATS_VERSION='1.14.0'
VALID_FORMATTERS="pretty, junit, tap, tap13"

version() {
  printf 'Bats %s\n' "$BATS_VERSION"
}

abort() {
  local print_usage=1
  if [[ ${1:-} == --no-print-usage ]]; then
    print_usage=
    shift
  fi
  printf 'Error: %s\n' "$1" >&2
  if [[ -n $print_usage ]]; then
    usage >&2
  fi
  exit 1
}

usage() {
  local cmd="${0##*/}"

  cat <<HELP_TEXT_HEADER
Usage: ${cmd} [OPTIONS] <tests>
       ${cmd} [-h | -v]

HELP_TEXT_HEADER

  cat <<'HELP_TEXT_BODY'
  <tests> is the path to a Bats test file, or the path to a directory
  containing Bats test files (ending with ".bats")

  --abort                   Stop execution of suite on first failed test
  --allow-empty-suite       Exit with code 0 (instead of the default code 1) when no tests are found.
  -c, --count               Count test cases without running any tests
  --code-quote-style <style>
                            A two character string of code quote delimiters
                            or 'custom' which requires setting $BATS_BEGIN_CODE_QUOTE and
                            $BATS_END_CODE_QUOTE. Can also be set via $BATS_CODE_QUOTE_STYLE
  --line-reference-format   Controls how file/line references e.g. in stack traces are printed:
                              - comma_line (default): a.bats, line 1
                              - colon:  a.bats:1
                              - uri: file:///tests/a.bats:1
                              - custom: provide your own via defining bats_format_file_line_reference_custom
                                        with parameters <filename> <line>, store via `printf -v "$output"`
  -f, --filter <regex>      Only run tests that match the regular expression
  --negative-filter <regex> Only run tests that do not match the regular expression
  --filter-status <status>  Only run tests with the given status in the last completed (no CTRL+C/SIGINT) run.
                            Valid <status> values are:
                              failed - runs tests that failed or were not present in the last run
                              missed - runs tests that were not present in the last run
  --filter-tags <comma-separated-tag-list>
                            Only run tests that match all the tags in the list (&&).
                            You can negate a tag via prepending '!'.
                            Specifying this flag multiple times allows for logical or (||):
                            `--filter-tags A,B --filter-tags A,!C` matches tags (A && B) || (A && !C)
  -F, --formatter <type>    Switch between formatters: pretty (default),
                              tap (default w/o term), tap13, junit, /<absolute path to formatter>
  --gather-test-outputs-in <directory>
                            Gather the output of failing *and* passing tests
                            as files in directory (if existing, must be empty)
  -h, --help                Display this help message
  -j, --jobs <jobs>         Number of parallel jobs (requires GNU parallel or shenwei356/rush)
  --parallel-binary-name    Name of parallel binary
  --no-tempdir-cleanup      Preserve test output temporary directory
  --errexit                 Enable errexit (set -e) for commands run in `run`
  --no-parallelize-across-files
                            Serialize test file execution instead of running
                            them in parallel (requires --jobs >1)
  --no-parallelize-within-files
                            Serialize test execution within files instead of
                            running them in parallel (requires --jobs >1)
  --report-formatter <type> Switch between reporters (same options as --formatter)
  -o, --output <dir>        Directory to write report files (must exist)
  -p, --pretty              Shorthand for "--formatter pretty"
  --print-output-on-failure Automatically print the value of `$output` on failed tests
  -r, --recursive           Include tests in subdirectories
  --show-output-of-passing-tests
                            Print output of passing tests
  -t, --tap                 Shorthand for "--formatter tap"
  -T, --timing              Add timing information to tests
  -x, --trace               Print test commands as they are executed (like `set -x`)
  --verbose-run             Make `run` print `$output` by default
  -v, --version             Display the version number

  For more information, see https://github.com/bats-core/bats-core
HELP_TEXT_BODY
}

expand_path() {
  if [[ -z "$1" ]]; then
    abort --no-print-usage "Path is empty string."
  fi

  local path="${1%/}"
  local dirname="${path%/*}"
  if [[ -z "$dirname" ]]; then
    dirname=/
  fi
  local result="$2"
  local OLDPWD="$PWD"

  if [[ "$dirname" == "$path" ]]; then
    dirname="$PWD"
  else
    cd "$dirname"
    dirname="$PWD"
    cd "$OLDPWD"
  fi
  printf -v "$result" '%s/%s' "$dirname" "${path##*/}"
}

BATS_LIBEXEC="$(
  cd "$(dirname "$(bats_readlinkf "${BASH_SOURCE[0]}")")"
  pwd
)"
export BATS_LIBEXEC
export BATS_CWD="$PWD"
export PATH="$BATS_LIBEXEC:$PATH"
export BATS_ROOT_PID=$$
export BATS_TMPDIR="${TMPDIR:-/tmp}"
BATS_TMPDIR=${BATS_TMPDIR%/} # chop off trailing / to avoid duplication
export BATS_RUN_TMPDIR=
export BATS_GUARANTEED_MINIMUM_VERSION=0.0.0
export BATS_LIB_PATH=${BATS_LIB_PATH-/usr/lib/bats}
BATS_REPORT_OUTPUT_DIR=${BATS_REPORT_OUTPUT_DIR-.}
export BATS_LINE_REFERENCE_FORMAT=${BATS_LINE_REFERENCE_FORMAT-comma_line}

if [[ ! -d "${BATS_TMPDIR}" ]]; then
  printf "Error: BATS_TMPDIR (%s) does not exist or is not a directory" "${BATS_TMPDIR}" >&2
  exit 1
elif [[ ! -w "${BATS_TMPDIR}" ]]; then
  printf "Error: BATS_TMPDIR (%s) is not writable" "${BATS_TMPDIR}" >&2
  exit 1
fi

arguments=()

# list of single char options that don't expect a value
single_char_flags="hvcprtTx"

# Unpack single-character options bundled together, e.g. -cr, -pr.
for arg in "$@"; do
  if [[ "$arg" =~ ^-[^-]. ]]; then
    index=1
    while option="${arg:$((index++)):1}"; do
      if [[ -z "$option" ]]; then
        break
      fi
      if [[ "$single_char_flags" != *$option* && -n "${arg:index}" ]]; then
        printf "Error: -%s is not allowed within pack of flags.\n" "$option"
        printf "       Please put it last (e.g. \`-rF junit\` instead of \`-Fr junit\`), or on its own (\`-r -F junit\`)!\n"
        exit 1
      fi
      arguments+=("-$option")
    done
  else
    arguments+=("$arg")
  fi
  shift
done

set -- "${arguments[@]}"
arguments=()

unset flags recursive formatter_flags
flags=('--dummy-flag')           # add a dummy flag to prevent unset variable errors on empty array expansion in old bash versions
formatter_flags=('--dummy-flag') # add a dummy flag to prevent unset variable errors on empty array expansion in old bash versions
formatter=${BATS_FORMATTER:-'tap'}
report_formatter=''
recursive=
setup_suite_file=''
export BATS_TEMPDIR_CLEANUP=1

while [[ "$#" -ne 0 ]]; do
  case "$1" in
  -h | --help)
    version
    usage
    exit 0
    ;;
  -v | --version)
    version
    exit 0
    ;;
  -c | --count)
    flags+=('-c')
    ;;
  --allow-empty-suite)
    flags+=(--allow-empty-suite)
    ;;
  -f | --filter)
    shift
    flags+=('-f' "$1")
    ;;
  --negative-filter)
    shift
    flags+=('--negative-filter' "$1")
    ;;
  -F | --formatter)
    shift
    # allow cat formatter to see extended output but don't advertise to users
    if [[ $1 =~ ^(pretty|junit|tap|tap13|cat|/.*)$ ]]; then
      formatter="$1"
    else
      printf "Unknown formatter '%s', valid options are %s\n" "$1" "${VALID_FORMATTERS}"
      exit 1
    fi
    ;;
  --report-formatter)
    shift
    if [[ $1 =~ ^(cat|pretty|junit|tap|tap13|/.*)$ ]]; then
      report_formatter="$1"
    else
      printf "Unknown report formatter '%s', valid options are %s\n" "$1" "${VALID_FORMATTERS}"
      exit 1
    fi
    ;;
  -o | --output)
    shift
    BATS_REPORT_OUTPUT_DIR="$1"
    ;;
  -p | --pretty)
    formatter='pretty'
    ;;
  -j | --jobs)
    shift
    flags+=('-j' "$1")
    ;;
  --parallel-binary-name)
    shift
    flags+=('--parallel-binary-name' "$1")
    ;;
  -r | --recursive)
    recursive=1
    ;;
  -t | --tap)
    formatter='tap'
    ;;
  -T | --timing)
    flags+=('-T')
    formatter_flags+=('-T')
    ;;
  --abort)
    flags+=('--abort')
    ;;
  # this flag is now a no-op, as it is the parallel default
  --parallel-preserve-environment) ;;

  --no-parallelize-across-files)
    flags+=("--no-parallelize-across-files")
    ;;
  --no-parallelize-within-files)
    flags+=("--no-parallelize-within-files")
    ;;
  --no-tempdir-cleanup)
    BATS_TEMPDIR_CLEANUP=''
    ;;
  --errexit)
    BATS_RUN_ERREXIT=1
    ;;
  --tempdir) # for internal test consumption only!
    BATS_RUN_TMPDIR="$2"
    shift
    ;;
  -x | --trace)
    flags+=(--trace)
    ;;
  --print-output-on-failure)
    flags+=(--print-output-on-failure)
    ;;
  --show-output-of-passing-tests)
    flags+=(--show-output-of-passing-tests)
    ;;
  --verbose-run)
    flags+=(--verbose-run)
    ;;
  --gather-test-outputs-in)
    shift
    output_dir="$1"
    # Convert relative path to absolute to handle tests that change directory
    # This ensures the path remains valid even if tests use 'cd'
    if [[ $output_dir != /* ]]; then
        output_dir="$PWD/$output_dir"
    fi
    if [ -d "$output_dir" ]; then
      if ! find "$output_dir" -mindepth 1 -exec false {} + 2>/dev/null; then
        abort --no-print-usage "Directory '$output_dir' must be empty for --gather-test-outputs-in"
      fi
    elif ! mkdir "$output_dir" 2>/dev/null; then
      abort --no-print-usage "Could not create '$output_dir' for --gather-test-outputs-in"
    fi
    flags+=(--gather-test-outputs-in "$output_dir")
    ;;
  --setup-suite-file)
    shift
    setup_suite_file="$1"
    ;;
  --code-quote-style)
    shift
    BATS_CODE_QUOTE_STYLE="$1"
    ;;
  --filter-status)
    shift
    flags+=('--filter-status' "$1")
    ;;
  --filter-tags)
    shift
    flags+=('--filter-tags' "$1")
    ;;
  --line-reference-format)
    shift
    BATS_LINE_REFERENCE_FORMAT=$1
    ;;
  -*)
    abort "Bad command line option '$1'"
    ;;
  *)
    arguments+=("$1")
    ;;
  esac
  shift
done

if [[ ! $BATS_LINE_REFERENCE_FORMAT =~ (custom|comma_line|colon|uri) ]]; then
  abort "Invalid BATS_LINE_REFERENCE_FORMAT '$BATS_LINE_REFERENCE_FORMAT' (e.g. via --line-reference-format)"
fi

if [[ -n "${BATS_RUN_TMPDIR:-}" ]]; then
  if [[ -d "$BATS_RUN_TMPDIR" ]]; then
    printf "Error: BATS_RUN_TMPDIR (%s) already exists\n" "$BATS_RUN_TMPDIR" >&2
    printf "Reusing old run directories can lead to unexpected results ... aborting!\n" >&2
    exit 1
  elif ! mkdir -p "$BATS_RUN_TMPDIR"; then
    printf "Error: Failed to create BATS_RUN_TMPDIR (%s)\n" "$BATS_RUN_TMPDIR" >&2
    exit 1
  fi
elif ! BATS_RUN_TMPDIR=$(mktemp -d "${BATS_TMPDIR}/bats-run-XXXXXX"); then
  printf "Error: Failed to create BATS_RUN_TMPDIR (%s) with mktemp\n" "${BATS_TMPDIR}/bats-run-XXXXXX" >&2
  exit 1
fi

export BATS_WARNING_FILE="${BATS_RUN_TMPDIR}/warnings.log"
export BATS_RUN_ERREXIT=${BATS_RUN_ERREXIT:-}

bats_exit_trap() {
  if [[ -s "$BATS_WARNING_FILE" ]]; then
    local pre_cat='' post_cat=''
    if [[ $formatter == pretty ]]; then
      pre_cat=$'\x1B[31m'
      post_cat=$'\x1B[0m'
    fi
    printf "\nThe following warnings were encountered during tests:\n%s" "$pre_cat"
    cat "$BATS_WARNING_FILE"
    printf "%s" "$post_cat"
  fi >&2

  if [[ -n "$BATS_TEMPDIR_CLEANUP" ]]; then
    rm -rf "$BATS_RUN_TMPDIR"
  else
    printf "BATS_RUN_TMPDIR: %s\n" "$BATS_RUN_TMPDIR" >&2
  fi
}

trap bats_exit_trap EXIT

if [[ "$formatter" != "tap" ]]; then
  flags+=('-x')
fi

if [[ -n "$report_formatter" && "$report_formatter" != "tap" ]]; then
  flags+=('-x')
fi

if [[ "$formatter" == "junit" ]]; then
  flags+=('-T')
  formatter_flags+=('--base-path' "${arguments[0]}")
fi
if [[ "$report_formatter" == "junit" ]]; then
  flags+=('-T')
  report_formatter_flags+=('--base-path' "${arguments[0]}")
fi

if [[ "$formatter" == "pretty" ]]; then
  formatter_flags+=('--base-path' "${arguments[0]}")
fi

# if we don't need to filter extended syntax, use the faster formatter
if [[ "$formatter" == tap && -z "$report_formatter" ]]; then
  formatter="cat"
fi

if [[ "$formatter" == pretty ]]; then
  if [[ -z "${CI:-}" && -t 0 && -t 1 ]] && command -v tput >/dev/null; then
    formatter_flags+=(--interactive)
  fi
fi

bats_check_formatter() { # <formatter-path>
  local -r formatter="$1"
  if [[ ! -f "$formatter" ]]; then
    printf "ERROR: Formatter '%s' is not readable!\n" "$formatter"
    exit 1
  elif [[ ! -x "$formatter" ]]; then
    printf "ERROR: Formatter '%s' is not executable!\n" "$formatter"
    exit 1
  fi
}

if [[ $formatter == /* ]]; then # absolute paths are direct references to formatters
  bats_check_formatter "$formatter"
  interpolated_formatter="$formatter"
else
  interpolated_formatter="bats-format-${formatter}"
fi

if [[ "${#arguments[@]}" -eq 0 ]]; then
  abort 'Must specify at least one <test>'
fi

if [[ -n "$report_formatter" ]]; then
  if [[ ! -w "${BATS_REPORT_OUTPUT_DIR}" ]]; then
    abort "Output path ${BATS_REPORT_OUTPUT_DIR} is not writeable"
  fi
  # only set BATS_REPORT_FILENAME if none was given
  if [[ -z "${BATS_REPORT_FILENAME:-}" ]]; then
    case "$report_formatter" in
    tap | tap13)
      BATS_REPORT_FILENAME="report.tap"
      ;;
    junit)
      BATS_REPORT_FILENAME="report.xml"
      ;;
    *)
      BATS_REPORT_FILENAME="report.log"
      ;;
    esac
  fi
fi

if [[ $report_formatter == /* ]]; then # absolute paths are direct references to formatters
  bats_check_formatter "$report_formatter"
  interpolated_report_formatter="${report_formatter}"
else
  interpolated_report_formatter="bats-format-${report_formatter}"
fi

if [[ "${BATS_CODE_QUOTE_STYLE-BATS_CODE_QUOTE_STYLE_UNSET}" == BATS_CODE_QUOTE_STYLE_UNSET ]]; then
  BATS_CODE_QUOTE_STYLE="\`'"
fi

case "${BATS_CODE_QUOTE_STYLE}" in
??)
  BATS_BEGIN_CODE_QUOTE="${BATS_CODE_QUOTE_STYLE::1}"
  BATS_END_CODE_QUOTE="${BATS_CODE_QUOTE_STYLE:1:1}"
  export BATS_BEGIN_CODE_QUOTE BATS_END_CODE_QUOTE
  ;;
custom)
  if [[ ${BATS_BEGIN_CODE_QUOTE-BATS_BEGIN_CODE_QUOTE_UNSET} == BATS_BEGIN_CODE_QUOTE_UNSET ||
    ${BATS_END_CODE_QUOTE-BATS_BEGIN_CODE_QUOTE_UNSET} == BATS_BEGIN_CODE_QUOTE_UNSET ]]; then
    printf "ERROR: BATS_CODE_QUOTE_STYLE=custom requires BATS_BEGIN_CODE_QUOTE and BATS_END_CODE_QUOTE to be set\n" >&2
    exit 1
  fi
  ;;
*)
  printf "ERROR: Unknown BATS_CODE_QUOTE_STYLE: %s\n" "$BATS_CODE_QUOTE_STYLE" >&2
  exit 1
  ;;
esac

if [[ -n "$setup_suite_file" && ! -f "$setup_suite_file" ]]; then
  abort "--setup-suite-file $setup_suite_file does not exist!"
fi

filenames=()
for ((i=0; i < ${#arguments[@]}; ++i)); do
  filename="${arguments[i]}"
  if [[ -z "$filename" ]]; then
    abort --no-print-usage "File path must not be empty! (Path number $((i + 1)))"
  fi
  expand_path "$filename" 'filename'

  if [[ -z "$setup_suite_file" ]]; then
    if [[ -d "$filename" ]]; then
      dirname="$filename"
    else
      dirname="${filename%/*}"
    fi
    potential_setup_suite_file="$dirname/setup_suite.bash"
    if [[ -e "$potential_setup_suite_file" ]]; then
      setup_suite_file="$potential_setup_suite_file"
    fi
  fi

  if [[ -d "$filename" ]]; then
    shopt -s nullglob
    if [[ "$recursive" -eq 1 ]]; then
      while IFS= read -r -d $'\0' file; do
        filenames+=("$file")
      done < <(find -L "$filename" -type f -name "*.${BATS_FILE_EXTENSION:-bats}" -print0 | sort -z)
    else
      for suite_filename in "$filename"/*."${BATS_FILE_EXTENSION:-bats}"; do
        filenames+=("$suite_filename")
      done
    fi
    shopt -u nullglob
  else
    filenames+=("$filename")
  fi
done

if [[ -n "$setup_suite_file" ]]; then
  flags+=("--setup-suite-file" "$setup_suite_file")
fi

# shellcheck source=lib/bats-core/validator.bash
source "$BATS_ROOT/$BATS_LIBDIR/bats-core/validator.bash"

trap 'BATS_INTERRUPTED=true' INT # let the lower levels handle the interruption

set -o pipefail execfail

# pipe stdin into command and to stdout
# pipe command stdout to file
bats_tee() { # <output-file> <command...>
  local output_file=$1 status=0
  shift
  exec 3<&1 # use FD3 to get around pipe
  tee >(cat >&3) | "$@" >"$output_file" || status=$?
  if (( status != 0 )); then
    printf "ERROR: command \`%s\` failed with status %d\n" "$*" "$status" >&2
  fi
  return $status
}

if [[ -n "$report_formatter" ]]; then
  exec bats-exec-suite "${flags[@]}" "${filenames[@]}" |
    bats_tee "${BATS_REPORT_OUTPUT_DIR}/${BATS_REPORT_FILENAME}" "$interpolated_report_formatter" "${report_formatter_flags[@]}" |
    bats_test_count_validator |
    "$interpolated_formatter" "${formatter_flags[@]}"
else
  exec bats-exec-suite "${flags[@]}" "${filenames[@]}" |
    bats_test_count_validator |
    "$interpolated_formatter" "${formatter_flags[@]}"
fi
