#!/usr/bin/bash
set -e

# Assume failure of stubbed command
status=1
program="${0##*/}"
# shellcheck disable=SC2018,SC2019  # anything not A-Z0-9 will be _
# the "\n" is necessary to avoid adding a trailing _ to the name
PROGRAM="$(echo "${program}" | tr a-z A-Z | tr -C "A-Z0-9\n" '_')"

if [[ "$PROGRAM" =~ ^[^A-Z_]+(.*)$ ]]; then
  # remove leading non A-Z_ characters to make a valid variable name
  PROGRAM="${BASH_REMATCH[1]}"
fi

_STUB_PLAN="${PROGRAM}_STUB_PLAN"
_STUB_RUN="${PROGRAM}_STUB_RUN"
_STUB_INDEX="${PROGRAM}_STUB_INDEX"
_STUB_RESULT="${PROGRAM}_STUB_RESULT"
_STUB_END="${PROGRAM}_STUB_END"
_STUB_DEBUG="${PROGRAM}_STUB_DEBUG"

debug() {
  if [ -n "${!_STUB_DEBUG-}" ] ; then
    echo "bats-mock($program): $*" >&"${!_STUB_DEBUG}"
  fi
}

[ -e "${!_STUB_PLAN}" ] || exit 1
[ -n "${!_STUB_RUN}" ] || eval "${_STUB_RUN}"="${BATS_MOCK_TMPDIR}/${program}-stub-run"

# Initialize or load the stub run information.
eval "${_STUB_INDEX}"=1
eval "${_STUB_RESULT}"=0
# shellcheck source=stub.bash
[ ! -e "${!_STUB_RUN}" ] || source "${!_STUB_RUN}"

if [ -z "${!_STUB_END}" ]; then
  debug "got $program $*"
fi

# Loop over each line in the plan.
index=0
match_found=0
while IFS= read -r line; do
  index=$((index + 1))

  # debug "bats-mock: [idx $index, want ${!_STUB_INDEX}] $line"

  if [ -z "${!_STUB_END}" ] && [ $index -eq "${!_STUB_INDEX}" ]; then
    # We found the plan line we're interested in.
    # Start off by assuming success.
    match_found=1

    arguments=("$@")
    debug "arguments [${#arguments[@]}] = $(printf "'%q' " "${arguments[@]}")"

    # Split the line into an array of arguments to match and a command to run.
    # If the line does not contain ' : ' and does not start with a colon
    # then the call is assumed to match any arguments and execute the command.
    # Special case: Lines starting with double colon are also handled that way
    # and get the double colons removed
    command=" $line"
    if [[ "$line" == ::* ]]; then
      command="${line#::}"
    elif [ "$command" != "${command/ : }" ]; then
      patterns="${command%% : *}"
      command="${command#* : }"

      parsed_patterns=()

      # Parse patterns into tokens using eval to respect quoted
      # strings. This is less than ideal, but the pattern input
      # is also already eval'd elsewhere. At least this handles
      # things like newlines (which xargs doesn't)
      origFlags="$-"
      set -f
      eval "parsed_patterns=(${patterns})"
      set "-$origFlags"

      debug "patterns  [${#parsed_patterns[@]}] = $(printf "'%q' " "${parsed_patterns[@]}")"

      # Match the expected argument patterns to actual
      # arguments.
      for (( i=0; i<${#parsed_patterns[@]}; i++ )); do
        pattern="${parsed_patterns[$i]}"
        argument="${arguments[$i]}"

        if [ "$pattern" = '*' ]; then
          continue
        fi

        case "$argument" in
          # uncomment this line for partial pattern matching
          # will break existing * matching and generate a shellcheck warning
          # $pattern ) ;;
          "$pattern" ) ;;
          * ) debug "$(printf "match failed at idx %d, expected '%q', got '%q'" "$i" "$pattern" "$argument")"
              match_found=2
              break ;;
        esac
      done

      # Check if there are unmatched arguments
      if [[ ${#arguments[@]} -gt ${#parsed_patterns[@]} ]] ; then
        idx="${#parsed_patterns[@]}"
        argument="${arguments[$idx]}"
        debug "$(printf "unexpected argument '%q' at idx %d" "$argument" "$idx")"
        match_found=3
      fi
    fi
    break
  fi
done < "${!_STUB_PLAN}"


if [ -n "${!_STUB_END}" ]; then
  debug "unstubbing"

  if [ ! -f "${!_STUB_RUN}" ] && [ -n "${!_STUB_DEBUG-}" ] ; then
    echo "The stub for ${program} wasn't run"
    exit 1
  fi

  # Clean up the run file.
  "$BATS_MOCK_REAL_rm" -f "${!_STUB_RUN}"

  # If the number of lines in the plan is larger than
  # the requested index, we failed.
  if [ $index -ge "${!_STUB_INDEX}" ]; then
    eval "${_STUB_RESULT}"=1
  fi

  # Return the result.
  exit "${!_STUB_RESULT}"
else
  # If the arguments matched, evaluate the command
  # in a subshell. Otherwise, log the failure.
  if [ $match_found -eq 1 ] ; then
    debug "running $command"
    set +e
    ( eval "$command" )
    status="$?"
    debug "command result was $status"
    set -e
  else
    debug "no plan row found"
    eval "${_STUB_RESULT}"=1
  fi

  # Write out the run information.
  { echo "${_STUB_INDEX}=$((${!_STUB_INDEX} + 1))"
    echo "${_STUB_RESULT}=${!_STUB_RESULT}"
  } > "${!_STUB_RUN}"

  debug "result ${!_STUB_RESULT}"
  exit "$status"
fi
