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

BATS_COUNT_ONLY=''
BATS_TEST_FILTER=''
BATS_EXTENDED_SYNTAX=''

# Command and flags used by bats_perform_tests when it re-invokes bats-exec-test
# in a new process to run bats_perform_test.
BATS_PERFORM_TEST_CMD=("$0")

while [[ "$#" -ne 0 ]]; do
  case "$1" in
  -c)
    BATS_COUNT_ONLY=1
    ;;
  -f)
    BATS_TEST_FILTER="$2"
    shift
    ;;
  -x)
    BATS_EXTENDED_SYNTAX='-x'
    BATS_PERFORM_TEST_CMD+=('-x')
    ;;
  *)
    break
    ;;
  esac
  shift
done

BATS_TEST_FILENAME="$1"
if [[ -z "$BATS_TEST_FILENAME" ]]; then
  printf 'usage: bats-exec-test <filename>\n' >&2
  exit 1
elif [[ ! -f "$BATS_TEST_FILENAME" ]]; then
  printf 'bats: %s does not exist\n' "$BATS_TEST_FILENAME" >&2
  exit 1
else
  shift
fi

BATS_TEST_DIRNAME="${BATS_TEST_FILENAME%/*}"
BATS_TEST_NAMES=()

load() {
  local name="$1"
  local filename

  if [[ "${name:0:1}" == '/' ]]; then
    filename="${name}"
  else
    filename="$BATS_TEST_DIRNAME/${name}.bash"
  fi

  if [[ ! -f "$filename" ]]; then
    printf 'bats: %s does not exist\n' "$filename" >&2
    exit 1
  fi

  source "${filename}"
}

run() {
  local origFlags="$-"
  set +eET
  local origIFS="$IFS"
  output="$("$@" 2>&1)"
  status="$?"
  IFS=$'\n' lines=($output)
  IFS="$origIFS"
  set "-$origFlags"
}

setup() {
  return 0
}

teardown() {
  return 0
}

skip() {
  BATS_TEST_SKIPPED="${1:-1}"
  BATS_TEST_COMPLETED=1
  exit 0
}

bats_test_begin() {
  BATS_TEST_DESCRIPTION="$1"
  if [[ -n "$BATS_EXTENDED_SYNTAX" ]]; then
    printf 'begin %d %s\n' "$BATS_TEST_NUMBER" "$BATS_TEST_DESCRIPTION" >&3
  fi
  setup
}

bats_test_function() {
  local test_name="$1"
  BATS_TEST_NAMES+=("$test_name")
}

bats_capture_stack_trace() {
  local test_file
  local funcname
  local i

  BATS_STACK_TRACE=()

  for ((i=2; i != ${#FUNCNAME[@]}; ++i)); do
    # Use BATS_TEST_SOURCE if necessary to work around Bash < 4.4 bug whereby
    # calling an exported function erases the test file's BASH_SOURCE entry.
    test_file="${BASH_SOURCE[$i]:-$BATS_TEST_SOURCE}"
    funcname="${FUNCNAME[$i]}"
    BATS_STACK_TRACE+=("${BASH_LINENO[$((i-1))]} $funcname $test_file")
    if [[ "$test_file" == "$BATS_TEST_SOURCE" ]]; then
      case "$funcname" in
      "$BATS_TEST_NAME"|setup|teardown)
        break
        ;;
      esac
    fi
  done
}

bats_print_stack_trace() {
  local frame
  local index=1
  local count="${#@}"
  local filename
  local lineno

  for frame in "$@"; do
    bats_frame_filename "$frame" 'filename'
    bats_trim_filename "$filename" 'filename'
    bats_frame_lineno "$frame" 'lineno'

    if [[ $index -eq 1 ]]; then
      printf '# ('
    else
      printf '#  '
    fi

    local fn
    bats_frame_function "$frame" 'fn'
    if [[ "$fn" != "$BATS_TEST_NAME" ]]; then
      printf "from function \`%s' " "$fn"
    fi

    if [[ $index -eq $count ]]; then
      printf 'in test file %s, line %d)\n' "$filename" "$lineno"
    else
      printf 'in file %s, line %d,\n' "$filename" "$lineno"
    fi

    ((++index))
  done
}

bats_print_failed_command() {
  local frame="${BATS_STACK_TRACE[${#BATS_STACK_TRACE[@]}-1]}"
  local filename
  local lineno
  local failed_line
  local failed_command

  bats_frame_filename "$frame" 'filename'
  bats_frame_lineno "$frame" 'lineno'
  bats_extract_line "$filename" "$lineno" 'failed_line'
  bats_strip_string "$failed_line" 'failed_command'
  printf '%s' "#   \`${failed_command}' "

  if [[ "$BATS_ERROR_STATUS" -eq 1 ]]; then
    printf 'failed\n'
  else
    printf 'failed with status %d\n' "$BATS_ERROR_STATUS"
  fi
}

bats_frame_lineno() {
  printf -v "$2" '%s' "${1%% *}"
}

bats_frame_function() {
  local __bff_function="${1#* }"
  printf -v "$2" '%s' "${__bff_function%% *}"
}

bats_frame_filename() {
  local __bff_filename="${1#* }"
  __bff_filename="${__bff_filename#* }"

  if [[ "$__bff_filename" == "$BATS_TEST_SOURCE" ]]; then
    __bff_filename="$BATS_TEST_FILENAME"
  fi
  printf -v "$2" '%s' "$__bff_filename"
}

bats_extract_line() {
  local __bats_extract_line_line
  local __bats_extract_line_index=0

  while IFS= read -r __bats_extract_line_line; do
    if [[ "$((++__bats_extract_line_index))" -eq "$2" ]]; then
      printf -v "$3" '%s' "${__bats_extract_line_line%$'\r'}"
      break
    fi
  done <"$1"
}

bats_strip_string() {
  [[ "$1" =~ ^[[:space:]]*(.*)[[:space:]]*$ ]]
  printf -v "$2" '%s' "${BASH_REMATCH[1]}"
}

bats_trim_filename() {
  printf -v "$2" '%s' "${1#$BATS_CWD/}"
}

bats_debug_trap() {
  if [[ "$BASH_SOURCE" != "$1" ]]; then
    bats_capture_stack_trace
    BATS_LINENO="$BATS_CURRENT_LINENO"
    BATS_CURRENT_LINENO="${BASH_LINENO[0]}"
  fi
}

# For some versions of Bash, the `ERR` trap may not always fire for every
# command failure, but the `EXIT` trap will. Also, some command failures may not
# set `$?` properly. See #72 and #81 for details.
#
# For this reason, we call `bats_error_trap` at the very beginning of
# `bats_teardown_trap` (the `DEBUG` trap for the call will move
# `BATS_CURRENT_LINENO` to `BATS_LINENO`) and check the value of
# `$BATS_TEST_COMPLETED` before taking other actions. We also adjust the exit
# status value if needed.
#
# See `bats_exit_trap` for an additional EXIT error handling case when `$?`
# isn't set properly during `teardown()` errors.
bats_error_trap() {
  local status="$?"
  if [[ -z "$BATS_TEST_COMPLETED" ]]; then
    BATS_ERROR_STATUS="${BATS_ERROR_STATUS:-$status}"
    if [[ "$BATS_ERROR_STATUS" -eq 0 ]]; then
      BATS_ERROR_STATUS=1
    fi
    BATS_STACK_TRACE[0]="$BATS_LINENO ${BATS_STACK_TRACE[0]#* }"
    trap - debug
  fi
}

bats_teardown_trap() {
  bats_error_trap
  local status=0
  teardown >>"$BATS_OUT" 2>&1 || status="$?"

  if [[ $status -eq 0 ]]; then
    BATS_TEARDOWN_COMPLETED=1
  elif [[ -n "$BATS_TEST_COMPLETED" ]]; then
    BATS_ERROR_STATUS="$status"
  fi

  bats_exit_trap
}

bats_exit_trap() {
  local line
  local status
  local skipped=''
  trap - err exit

  if [[ -n "$BATS_TEST_SKIPPED" ]]; then
    skipped=' # skip'
    if [[ "$BATS_TEST_SKIPPED" != '1' ]]; then
      skipped+=" $BATS_TEST_SKIPPED"
    fi
  fi

  if [[ -z "$BATS_TEST_COMPLETED" || -z "$BATS_TEARDOWN_COMPLETED" ]]; then
    if [[ "$BATS_ERROR_STATUS" -eq 0 ]]; then
      # For some versions of bash, `$?` may not be set properly for some error
      # conditions before triggering the EXIT trap directly (see #72 and #81).
      # Thanks to the `BATS_TEARDOWN_COMPLETED` signal, this will pinpoint such
      # errors if they happen during `teardown()` when `bats_perform_test` calls
      # `bats_teardown_trap` directly after the test itself passes.
      #
      # If instead the test fails, and the `teardown()` error happens while
      # `bats_teardown_trap` runs as the EXIT trap, the test will fail with no
      # output, since there's no way to reach the `bats_exit_trap` call.
      BATS_STACK_TRACE[0]="$BATS_LINENO ${BATS_STACK_TRACE[0]#* }"
      BATS_ERROR_STATUS=1
    fi
    printf 'not ok %d %s\n' "$BATS_TEST_NUMBER" "$BATS_TEST_DESCRIPTION" >&3
    bats_print_stack_trace "${BATS_STACK_TRACE[@]}" >&3
    bats_print_failed_command >&3

    while IFS= read -r line; do
      printf '# %s\n' "$line"
    done <"$BATS_OUT" >&3
    if [[ -n "$line" ]]; then
      printf '# %s\n' "$line"
    fi
    status=1
  else
    printf 'ok %d %s%s\n' "$BATS_TEST_NUMBER" "$BATS_TEST_DESCRIPTION" \
      "$skipped" >&3
    status=0
  fi

  rm -f "$BATS_OUT"
  exit "$status"
}

bats_perform_tests() {
  local test_number=1
  local status=0

  printf '1..%d\n' "$#"

  for test_name in "$@"; do
    if ! "${BATS_PERFORM_TEST_CMD[@]}" "$BATS_TEST_FILENAME" "$test_name" \
      "$test_number"; then
      status=1
    fi
    ((++test_number))
  done
  exit "$status"
}

bats_perform_test() {
  BATS_TEST_NAME="$1"
  if declare -F "$BATS_TEST_NAME" >/dev/null; then
    BATS_TEST_NUMBER="$2"
    if [[ -z "$BATS_TEST_NUMBER" ]]; then
      printf '1..1\n'
      BATS_TEST_NUMBER=1
    fi

    # Some versions of Bash will reset BASH_LINENO to the first line of the
    # function when the ERR trap fires. All versions of Bash appear to reset it
    # on an unbound variable access error. bats_debug_trap will fire both before
    # the offending line is executed, and when the error is triggered.
    # Consequently, we use `BATS_LINENO` to point to the line number seen by the
    # first call to bats_debug_trap, _before_ the ERR trap or unbound variable
    # access fires.
    BATS_CURRENT_LINENO=0
    BATS_LINENO=0
    BATS_STACK_TRACE=()

    BATS_TEST_COMPLETED=
    BATS_TEST_SKIPPED=
    BATS_TEARDOWN_COMPLETED=
    BATS_ERROR_STATUS=
    trap "bats_debug_trap \"\$BASH_SOURCE\"" debug
    trap 'bats_error_trap' err
    trap 'bats_teardown_trap' exit
    "$BATS_TEST_NAME" >>"$BATS_OUT" 2>&1
    BATS_TEST_COMPLETED=1
    trap 'bats_exit_trap' exit
    bats_teardown_trap

  else
    printf "bats: unknown test name \`%s'\n" "$BATS_TEST_NAME" >&2
    exit 1
  fi
}

if [[ -z "$TMPDIR" ]]; then
  BATS_TMPDIR='/tmp'
else
  BATS_TMPDIR="${TMPDIR%/}"
fi

BATS_TMPNAME="$BATS_TMPDIR/bats.$$"
BATS_PARENT_TMPNAME="$BATS_TMPDIR/bats.$PPID"
BATS_OUT="${BATS_TMPNAME}.out"

bats_preprocess_source() {
  BATS_TEST_SOURCE="${BATS_TMPNAME}.src"
  . bats-preprocess <<< "$(< "$BATS_TEST_FILENAME")"$'\n' > "$BATS_TEST_SOURCE"
  trap 'bats_cleanup_preprocessed_source' err exit
  trap 'bats_cleanup_preprocessed_source; exit 1' int

  bats_detect_duplicate_test_case_names
}

bats_cleanup_preprocessed_source() {
  rm -f "$BATS_TEST_SOURCE"
}

bats_detect_duplicate_test_case_names() {
  local test_names=()
  local test_dupes=()
  local line

  while read -r line; do
    if [[ ! "$line" =~ ^bats_test_function\  ]]; then
      continue
    fi
    line="${line%$'\r'}"
    line="${line#* }"

    if [[ " ${test_names[*]} " == *" $line "* ]]; then
      test_dupes+=("$line")
      continue
    fi
    test_names+=("$line")
  done <"$BATS_TEST_SOURCE"

  if [[ "${#test_dupes[@]}" -ne 0 ]]; then
    printf 'bats warning: duplicate test name(s) in %s: %s\n' \
      "$BATS_TEST_FILENAME" "${test_dupes[*]}" >&2
  fi
}

bats_evaluate_preprocessed_source() {
  if [[ -z "$BATS_TEST_SOURCE" ]]; then
    BATS_TEST_SOURCE="${BATS_PARENT_TMPNAME}.src"
  fi
  source "$BATS_TEST_SOURCE"
}

exec 3<&1

# When bats-exec-test is invoked by bats or bats-exec-suite, bats_perform_tests
# reinvokes bats-exec-suite to run a single test case via bats_perform_test in a
# new process. This is necessary so that `set -e` and all the traps set by
# `bats_perform_test` don't interfere with the parent bats-exec-test process.
if [[ "$#" -eq 0 ]]; then
  bats_preprocess_source
  bats_evaluate_preprocessed_source

  if [[ -n "$BATS_COUNT_ONLY" ]]; then
    printf '%d\n' "${#BATS_TEST_NAMES[@]}"
  else
    bats_perform_tests "${BATS_TEST_NAMES[@]}"
  fi
else
  bats_evaluate_preprocessed_source
  bats_perform_test "$@"
fi
