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

# Variables used in other scripts.
BATS_ENABLE_TIMING=''
BATS_EXTENDED_SYNTAX=''
BATS_TRACE_LEVEL="${BATS_TRACE_LEVEL:-0}"
BATS_PRINT_OUTPUT_ON_FAILURE="${BATS_PRINT_OUTPUT_ON_FAILURE:-}"
BATS_SHOW_OUTPUT_OF_SUCCEEDING_TESTS="${BATS_SHOW_OUTPUT_OF_SUCCEEDING_TESTS:-}"
BATS_VERBOSE_RUN="${BATS_VERBOSE_RUN:-}"
BATS_GATHER_TEST_OUTPUTS_IN="${BATS_GATHER_TEST_OUTPUTS_IN:-}"
BATS_TEST_NAME_PREFIX="${BATS_TEST_NAME_PREFIX:-}"

while [[ "$#" -ne 0 ]]; do
  case "$1" in
  -T)
    BATS_ENABLE_TIMING='-T'
    ;;
  -x)
    # shellcheck disable=SC2034
    BATS_EXTENDED_SYNTAX='-x'
    ;;
  --dummy-flag) ;;

  --trace)
    ((++BATS_TRACE_LEVEL)) # avoid returning 0
    ;;
  --print-output-on-failure)
    BATS_PRINT_OUTPUT_ON_FAILURE=1
    ;;
  --show-output-of-passing-tests)
    BATS_SHOW_OUTPUT_OF_SUCCEEDING_TESTS=1
    ;;
  --verbose-run)
    BATS_VERBOSE_RUN=1
    ;;
  --gather-test-outputs-in)
    shift
    BATS_GATHER_TEST_OUTPUTS_IN="$1"
    ;;
  *)
    break
    ;;
  esac
  shift
done

export BATS_TEST_FILENAME="$1"
export BATS_TEST_NAME="$2"
export BATS_SUITE_TEST_NUMBER="$3"
export BATS_TEST_NUMBER="$4"
BATS_TEST_TRY_NUMBER="$5"

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
fi

bats_create_test_tmpdirs() {
  local tests_tmpdir="${BATS_RUN_TMPDIR}/test"
  if ! mkdir -p "$tests_tmpdir"; then
    printf 'Failed to create: %s\n' "$tests_tmpdir" >&2
    exit 1
  fi

  BATS_TEST_TMPDIR="$tests_tmpdir/$BATS_SUITE_TEST_NUMBER"
  if ! mkdir "$BATS_TEST_TMPDIR"; then
    printf 'Failed to create BATS_TEST_TMPDIR%d: %s\n' "$BATS_TEST_TRY_NUMBER" "$BATS_TEST_TMPDIR" >&2
    exit 1
  fi

  printf "%s\n" "$BATS_TEST_NAME" >"$BATS_TEST_TMPDIR.name"

  export BATS_TEST_TMPDIR
}

# load the test helper functions like `load` or `run` that are needed to run a (preprocessed) .bats file without bash errors
# shellcheck source=lib/bats-core/test_functions.bash disable=SC2153
source "$BATS_ROOT/lib/bats/test_functions.bash"
# shellcheck source=lib/bats-core/tracing.bash disable=SC2153
source "$BATS_ROOT/lib/bats/tracing.bash"

bats_teardown_trap() {
  bats_check_status_from_trap
  local bats_teardown_trap_status=0

  # `bats_teardown_trap` is always called with one parameter (BATS_TEARDOWN_STARTED)
  # The second parameter is optional and corresponds for to the killer pid
  # that will be forwarded to the exit trap to kill it if necesary
  local killer_pid=${2:-}

  # mark the start of this function to distinguish where skip is called
  # parameter 1 will signify the reason why this function was called
  # this is used to identify when this is called as exit trap function
  BATS_TEARDOWN_STARTED=${1:-1}
  teardown >>"$BATS_OUT" 2>&1 || bats_teardown_trap_status="$?"

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

  bats_exit_trap "$killer_pid"
}

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

bats_exit_trap() {
  local status
  local exit_metadata=''
  local killer_pid=${1:-}
  trap - ERR EXIT
  if [[ -n "${BATS_TEST_TIMEOUT:-}" ]]; then
    # Kill the watchdog in the case of of kernel finished before the timeout
    bats_abort_timeout_countdown "$killer_pid" || status=1
  fi

  if [[ -n "$BATS_TEST_SKIPPED" ]]; then
    exit_metadata=' # skip'
    if [[ "$BATS_TEST_SKIPPED" != '1' ]]; then
      exit_metadata+=" $BATS_TEST_SKIPPED"
    fi
  elif [[ "${BATS_TIMED_OUT-NOTSET}" != NOTSET ]]; then
    exit_metadata=" # timeout after ${BATS_TEST_TIMEOUT}s"
  fi

  BATS_TEST_TIME=''
  if [[ -n "$BATS_ENABLE_TIMING" ]]; then
    BATS_TEST_TIME=" in "$(($(get_mills_since_epoch) - BATS_TEST_START_TIME))"ms"
  fi

  local print_bats_out="${BATS_SHOW_OUTPUT_OF_SUCCEEDING_TESTS}"

  local should_retry=''
  if [[ -z "$BATS_TEST_COMPLETED" || -z "$BATS_TEARDOWN_COMPLETED" || "${BATS_INTERRUPTED-NOTSET}" != NOTSET ]]; 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_ERROR_STATUS=1
    fi
    if bats_should_retry_test; then
      should_retry=1
      status=126                # signify retry
      rm -r "$BATS_TEST_TMPDIR" # clean up for retry
    else
      printf 'not ok %d %s%s\n' "$BATS_SUITE_TEST_NUMBER" "${BATS_TEST_NAME_PREFIX:-}${BATS_TEST_DESCRIPTION}${BATS_TEST_TIME}" "$exit_metadata" >&3
      local stack_trace
      bats_get_failure_stack_trace stack_trace
      bats_print_stack_trace "${stack_trace[@]}" >&3
      bats_print_failed_command "${stack_trace[@]}" >&3

      if [[ $BATS_PRINT_OUTPUT_ON_FAILURE ]]; then
        if [[ -n "${output:-}" ]]; then
          printf "Last output:\n%s\n" "$output"
        fi
        if [[ -n "${stderr:-}" ]]; then
          printf "Last stderr: \n%s\n" "$stderr"
        fi
      fi >>"$BATS_OUT"

      print_bats_out=1
      status=1
      local state=failed
    fi
  else
    printf 'ok %d %s%s\n' "$BATS_SUITE_TEST_NUMBER" "${BATS_TEST_NAME_PREFIX:-}${BATS_TEST_DESCRIPTION}${BATS_TEST_TIME}" \
      "$exit_metadata" >&3
    status=0
    local state=passed
  fi

  if [[ -z "$should_retry" ]]; then
    printf "%s %s\t%s\n" "$state" "$BATS_TEST_FILENAME" "$BATS_TEST_NAME" >>"$BATS_RUNLOG_FILE"

    if [[ $print_bats_out ]]; then
      bats_prefix_lines_for_tap_output <"$BATS_OUT" | bats_replace_filename >&3
    fi
  fi
  if [[ $BATS_GATHER_TEST_OUTPUTS_IN ]]; then
    local try_suffix=
    if [[ -n "$should_retry" ]]; then
      try_suffix="-try$BATS_TEST_TRY_NUMBER"
    fi
    cp "$BATS_OUT" "$BATS_GATHER_TEST_OUTPUTS_IN/$BATS_SUITE_TEST_NUMBER$try_suffix-$BATS_TEST_DESCRIPTION.log"
  fi
  rm -f "$BATS_OUT"
  exit "$status"
}

# Marks the test as failed due to timeout.
# The actual termination of subprocesses is done via pkill in the background
# process in bats_start_timeout_countdown
bats_timeout_trap() {
  BATS_TIMED_OUT=1
  BATS_DEBUG_LAST_STACK_TRACE_IS_VALID=
  exit 1
}

bats_get_child_processes_of() { # <parent-pid>
  local -ri parent_pid=${1?}
  {
    read -ra header
    local pid_col ppid_col
    for ((i = 0; i < ${#header[@]}; ++i)); do
      if [[ ${header[$i]} == "PID" ]]; then
        pid_col=$i
      fi
      if [[ ${header[$i]} == "PPID" ]]; then
        ppid_col=$i
      fi
    done
    while read -ra row; do
      if ((${row[$ppid_col]} == parent_pid)); then
        printf "%d\n" "${row[$pid_col]}"
      fi
    done
  } < <(ps -ef "$parent_pid")
}

bats_kill_childprocesses_of() { # <parent-pid>
  local -ir parent_pid="${1?}"
  if command -v pkill; then
    pkill -P "$parent_pid"
  else
    # kill in reverse order (latest first)
    while read -r pid; do
      kill "$pid"
    done < <(bats_get_child_processes_of "$parent_pid" | sort -r)
  fi >/dev/null
}

# sets a timeout for this process
#
# using SIGABRT for interprocess communication.
# Ruled out:
# USR1/2 - not available on Windows
# SIGALRM - interferes with sleep:
#           "sleep(3) may be implemented using SIGALRM; mixing calls to alarm()
#           and sleep(3) is a bad idea." ~ https://linux.die.net/man/2/alarm
bats_start_timeout_countdown() { # <timeout>
  local -ri timeout=$1
  local -ri target_pid=$$
  # shellcheck disable=SC2064
  trap "bats_timeout_trap $target_pid" ABRT
  if ! (command -v ps || command -v pkill) >/dev/null; then
    printf "Error: Cannot execute timeout because neither pkill nor ps are available on this system!\n" >&2
    exit 1
  fi
  # Start another process to kill the children of this process
  (
    sleep "$timeout" &
    # sleep won't recieve signals, so we use wait below
    # and kill sleep explicitly when signalled to do so
    # shellcheck disable=SC2064
    trap "kill $!; exit 0" ABRT
    wait
    if kill -ABRT "$target_pid"; then
      # get rid of signal blocking child processes (like sleep)
      bats_kill_childprocesses_of "$target_pid"
    fi &>/dev/null
  ) &
}

bats_abort_timeout_countdown() {
  # kill the countdown process, don't care if its still there
  kill -ABRT "$1" &>/dev/null || true
}

get_mills_since_epoch() {
  local ms_since_epoch
  ms_since_epoch=$(date +%s%N)
  if [[ "$ms_since_epoch" == *N || "${#ms_since_epoch}" -lt 19 ]]; then
    ms_since_epoch=$(($(date +%s) * 1000))
  else
    ms_since_epoch=$((ms_since_epoch / 1000000))
  fi

  printf "%d\n" "$ms_since_epoch"
}

bats_perform_test() {
  if ! declare -F "$BATS_TEST_NAME" &>/dev/null; then
    local quoted_test_name
    bats_quote_code quoted_test_name "$BATS_TEST_NAME"
    printf "bats: unknown test name %s\n" "$quoted_test_name" >&2
    exit 1
  fi

  local BATS_killer_pid=''
  if [[ -n "${BATS_TEST_TIMEOUT:-}" ]]; then
    bats_start_timeout_countdown "$BATS_TEST_TIMEOUT"
    BATS_killer_pid=$!
  fi
  BATS_TEST_COMPLETED=
  BATS_TEST_SKIPPED=
  BATS_TEARDOWN_COMPLETED=
  BATS_ERROR_STATUS=
  bats_setup_tracing
  # use parameter to mark this call as trap call
  # shellcheck disable=SC2064
  trap "bats_teardown_trap as-exit-trap $BATS_killer_pid" EXIT

  BATS_TEST_START_TIME=$(get_mills_since_epoch)
  "$BATS_TEST_NAME" >>"$BATS_OUT" 2>&1 4>&1

  BATS_TEST_COMPLETED=1
  # shellcheck disable=SC2064
  trap "bats_exit_trap $BATS_killer_pid" EXIT
  bats_teardown_trap "" "$BATS_killer_pid" # pass empty parameter to signify call outside trap
}

trap bats_interrupt_trap INT

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

exec 3<&1

bats_create_test_tmpdirs
# Run the given test.
bats_evaluate_preprocessed_source
bats_perform_test
