#
# bats-assert - Common assertions for Bats
#
# Written in 2016 by Zoltan Tombol <zoltan dot tombol at gmail dot com>
#
# To the extent possible under law, the author(s) have dedicated all
# copyright and related and neighboring rights to this software to the
# public domain worldwide. This software is distributed without any
# warranty.
#
# You should have received a copy of the CC0 Public Domain Dedication
# along with this software. If not, see
# <http://creativecommons.org/publicdomain/zero/1.0/>.
#

#
# assert.bash
# -----------
#
# Assertions are functions that perform a test and output relevant
# information on failure to help debugging. They return 1 on failure
# and 0 otherwise.
#
# All output is formatted for readability using the functions of
# `output.bash' and sent to the standard error.
#

# Fail and display the expression if it evaluates to false.
#
# NOTE: The expression must be a simple command. Compound commands, such
#       as `[[', can be used only when executed with `bash -c'.
#
# Globals:
#   none
# Arguments:
#   $1 - expression
# Returns:
#   0 - expression evaluates to TRUE
#   1 - otherwise
# Outputs:
#   STDERR - details, on failure
assert() {
  if ! "$@"; then
    batslib_print_kv_single 10 'expression' "$*" \
      | batslib_decorate 'assertion failed' \
      | fail
  fi
}

# Fail and display the expression if it evaluates to true.
#
# NOTE: The expression must be a simple command. Compound commands, such
#       as `[[', can be used only when executed with `bash -c'.
#
# Globals:
#   none
# Arguments:
#   $1 - expression
# Returns:
#   0 - expression evaluates to FALSE
#   1 - otherwise
# Outputs:
#   STDERR - details, on failure
refute() {
  if "$@"; then
    batslib_print_kv_single 10 'expression' "$*" \
      | batslib_decorate 'assertion succeeded, but it was expected to fail' \
      | fail
  fi
}

# Fail and display details if the expected and actual values do not
# equal. Details include both values.
#
# Globals:
#   none
# Arguments:
#   $1 - actual value
#   $2 - expected value
# Returns:
#   0 - values equal
#   1 - otherwise
# Outputs:
#   STDERR - details, on failure
assert_equal() {
  if [[ $1 != "$2" ]]; then
    batslib_print_kv_single_or_multi 8 \
        'expected' "$2" \
        'actual'   "$1" \
      | batslib_decorate 'values do not equal' \
      | fail
  fi
}

# Fail and display details if `$status' is not 0. Details include
# `$status' and `$output'.
#
# Globals:
#   status
#   output
# Arguments:
#   none
# Returns:
#   0 - `$status' is 0
#   1 - otherwise
# Outputs:
#   STDERR - details, on failure
assert_success() {
  if (( status != 0 )); then
    { local -ir width=6
      batslib_print_kv_single "$width" 'status' "$status"
      batslib_print_kv_single_or_multi "$width" 'output' "$output"
    } | batslib_decorate 'command failed' \
      | fail
  fi
}

# Fail and display details if `$status' is 0. Details include `$output'.
#
# Optionally, when the expected status is specified, fail when it does
# not equal `$status'. In this case, details include the expected and
# actual status, and `$output'.
#
# Globals:
#   status
#   output
# Arguments:
#   $1 - [opt] expected status
# Returns:
#   0 - `$status' is not 0, or
#       `$status' equals the expected status
#   1 - otherwise
# Outputs:
#   STDERR - details, on failure
assert_failure() {
  (( $# > 0 )) && local -r expected="$1"
  if (( status == 0 )); then
    batslib_print_kv_single_or_multi 6 'output' "$output" \
      | batslib_decorate 'command succeeded, but it was expected to fail' \
      | fail
  elif (( $# > 0 )) && (( status != expected )); then
    { local -ir width=8
      batslib_print_kv_single "$width" \
          'expected' "$expected" \
          'actual'   "$status"
      batslib_print_kv_single_or_multi "$width" \
          'output' "$output"
    } | batslib_decorate 'command failed as expected, but status differs' \
      | fail
  fi
}

# Fail and display details if `$output' does not match the expected
# output. The expected output can be specified either by the first
# parameter or on the standard input.
#
# By default, literal matching is performed. The assertion fails if the
# expected output does not equal `$output'. Details include both values.
#
# Option `--partial' enables partial matching. The assertion fails if
# the expected substring cannot be found in `$output'.
#
# Option `--regexp' enables regular expression matching. The assertion
# fails if the extended regular expression does not match `$output'. An
# invalid regular expression causes an error to be displayed.
#
# It is an error to use partial and regular expression matching
# simultaneously.
#
# Globals:
#   output
# Options:
#   -p, --partial - partial matching
#   -e, --regexp - extended regular expression matching
# Arguments:
#   $1 - [=STDIN] expected output
# Returns:
#   0 - expected matches the actual output
#   1 - otherwise
# Inputs:
#   STDIN - [=$1] expected output
# Outputs:
#   STDERR - details, on failure
#            error message, on error
assert_output() {
  local -i is_mode_partial=0
  local -i is_mode_regexp=0

  # Handle options.
  while (( $# > 0 )); do
    case "$1" in
      -p|--partial) is_mode_partial=1; shift ;;
      -e|--regexp) is_mode_regexp=1; shift ;;
      --) shift; break ;;
      *) break ;;
    esac
  done

  if (( is_mode_partial )) && (( is_mode_regexp )); then
    echo "\`--partial' and \`--regexp' are mutually exclusive" \
      | batslib_decorate 'ERROR: assert_output' \
      | fail
    return $?
  fi

  # Arguments.
  local expected
  (( $# == 0 )) && expected="$(cat -)" || expected="$1"

  # Matching.
  if (( is_mode_regexp )); then
    if [[ '' =~ $expected ]] || (( $? == 2 )); then
      echo "Invalid extended regular expression: \`$expected'" \
        | batslib_decorate 'ERROR: assert_output' \
        | fail
      return $?
    fi
    if ! [[ $output =~ $expected ]]; then
      batslib_print_kv_single_or_multi 6 \
          'regexp'  "$expected" \
          'output' "$output" \
        | batslib_decorate 'regular expression does not match output' \
        | fail
    fi
  elif (( is_mode_partial )); then
    if [[ $output != *"$expected"* ]]; then
      batslib_print_kv_single_or_multi 9 \
          'substring' "$expected" \
          'output'    "$output" \
        | batslib_decorate 'output does not contain substring' \
        | fail
    fi
  else
    if [[ $output != "$expected" ]]; then
      batslib_print_kv_single_or_multi 8 \
          'expected' "$expected" \
          'actual'   "$output" \
        | batslib_decorate 'output differs' \
        | fail
    fi
  fi
}

# Fail and display details if `$output' matches the unexpected output.
# The unexpected output can be specified either by the first parameter
# or on the standard input.
#
# By default, literal matching is performed. The assertion fails if the
# unexpected output equals `$output'. Details include `$output'.
#
# Option `--partial' enables partial matching. The assertion fails if
# the unexpected substring is found in `$output'. The unexpected
# substring is added to details.
#
# Option `--regexp' enables regular expression matching. The assertion
# fails if the extended regular expression does matches `$output'. The
# regular expression is added to details. An invalid regular expression
# causes an error to be displayed.
#
# It is an error to use partial and regular expression matching
# simultaneously.
#
# Globals:
#   output
# Options:
#   -p, --partial - partial matching
#   -e, --regexp - extended regular expression matching
# Arguments:
#   $1 - [=STDIN] unexpected output
# Returns:
#   0 - unexpected matches the actual output
#   1 - otherwise
# Inputs:
#   STDIN - [=$1] unexpected output
# Outputs:
#   STDERR - details, on failure
#            error message, on error
refute_output() {
  local -i is_mode_partial=0
  local -i is_mode_regexp=0

  # Handle options.
  while (( $# > 0 )); do
    case "$1" in
      -p|--partial) is_mode_partial=1; shift ;;
      -e|--regexp) is_mode_regexp=1; shift ;;
      --) shift; break ;;
      *) break ;;
    esac
  done

  if (( is_mode_partial )) && (( is_mode_regexp )); then
    echo "\`--partial' and \`--regexp' are mutually exclusive" \
      | batslib_decorate 'ERROR: refute_output' \
      | fail
    return $?
  fi

  # Arguments.
  local unexpected
  (( $# == 0 )) && unexpected="$(cat -)" || unexpected="$1"

  if (( is_mode_regexp == 1 )) && [[ '' =~ $unexpected ]] || (( $? == 2 )); then
    echo "Invalid extended regular expression: \`$unexpected'" \
      | batslib_decorate 'ERROR: refute_output' \
      | fail
    return $?
  fi

  # Matching.
  if (( is_mode_regexp )); then
    if [[ $output =~ $unexpected ]] || (( $? == 0 )); then
      batslib_print_kv_single_or_multi 6 \
          'regexp'  "$unexpected" \
          'output' "$output" \
        | batslib_decorate 'regular expression should not match output' \
        | fail
    fi
  elif (( is_mode_partial )); then
    if [[ $output == *"$unexpected"* ]]; then
      batslib_print_kv_single_or_multi 9 \
          'substring' "$unexpected" \
          'output'    "$output" \
        | batslib_decorate 'output should not contain substring' \
        | fail
    fi
  else
    if [[ $output == "$unexpected" ]]; then
      batslib_print_kv_single_or_multi 6 \
          'output' "$output" \
        | batslib_decorate 'output equals, but it was expected to differ' \
        | fail
    fi
  fi
}

# Fail and display details if the expected line is not found in the
# output (default) or in a specific line of it.
#
# By default, the entire output is searched for the expected line. The
# expected line is matched against every element of `${lines[@]}'. If no
# match is found, the assertion fails. Details include the expected line
# and `${lines[@]}'.
#
# When `--index <idx>' is specified, only the <idx>-th line is matched.
# If the expected line does not match `${lines[<idx>]}', the assertion
# fails. Details include <idx> and the compared lines.
#
# By default, literal matching is performed. A literal match fails if
# the expected string does not equal the matched string.
#
# Option `--partial' enables partial matching. A partial match fails if
# the expected substring is not found in the target string.
#
# Option `--regexp' enables regular expression matching. A regular
# expression match fails if the extended regular expression does not
# match the target string. An invalid regular expression causes an error
# to be displayed.
#
# It is an error to use partial and regular expression matching
# simultaneously.
#
# Mandatory arguments to long options are mandatory for short options
# too.
#
# Globals:
#   output
#   lines
# Options:
#   -n, --index <idx> - match the <idx>-th line
#   -p, --partial - partial matching
#   -e, --regexp - extended regular expression matching
# Arguments:
#   $1 - expected line
# Returns:
#   0 - match found
#   1 - otherwise
# Outputs:
#   STDERR - details, on failure
#            error message, on error
# FIXME(ztombol): Display `${lines[@]}' instead of `$output'!
assert_line() {
  local -i is_match_line=0
  local -i is_mode_partial=0
  local -i is_mode_regexp=0

  # Handle options.
  while (( $# > 0 )); do
    case "$1" in
      -n|--index)
        if (( $# < 2 )) || ! [[ $2 =~ ^([0-9]|[1-9][0-9]+)$ ]]; then
          echo "\`--index' requires an integer argument: \`$2'" \
            | batslib_decorate 'ERROR: assert_line' \
            | fail
          return $?
        fi
        is_match_line=1
        local -ri idx="$2"
        shift 2
        ;;
      -p|--partial) is_mode_partial=1; shift ;;
      -e|--regexp) is_mode_regexp=1; shift ;;
      --) shift; break ;;
      *) break ;;
    esac
  done

  if (( is_mode_partial )) && (( is_mode_regexp )); then
    echo "\`--partial' and \`--regexp' are mutually exclusive" \
      | batslib_decorate 'ERROR: assert_line' \
      | fail
    return $?
  fi

  # Arguments.
  local -r expected="$1"

  if (( is_mode_regexp == 1 )) && [[ '' =~ $expected ]] || (( $? == 2 )); then
    echo "Invalid extended regular expression: \`$expected'" \
      | batslib_decorate 'ERROR: assert_line' \
      | fail
    return $?
  fi

  # Matching.
  if (( is_match_line )); then
    # Specific line.
    if (( is_mode_regexp )); then
      if ! [[ ${lines[$idx]} =~ $expected ]]; then
        batslib_print_kv_single 6 \
            'index' "$idx" \
            'regexp' "$expected" \
            'line'  "${lines[$idx]}" \
          | batslib_decorate 'regular expression does not match line' \
          | fail
      fi
    elif (( is_mode_partial )); then
      if [[ ${lines[$idx]} != *"$expected"* ]]; then
        batslib_print_kv_single 9 \
            'index'     "$idx" \
            'substring' "$expected" \
            'line'      "${lines[$idx]}" \
          | batslib_decorate 'line does not contain substring' \
          | fail
      fi
    else
      if [[ ${lines[$idx]} != "$expected" ]]; then
        batslib_print_kv_single 8 \
            'index'    "$idx" \
            'expected' "$expected" \
            'actual'   "${lines[$idx]}" \
          | batslib_decorate 'line differs' \
          | fail
      fi
    fi
  else
    # Contained in output.
    if (( is_mode_regexp )); then
      local -i idx
      for (( idx = 0; idx < ${#lines[@]}; ++idx )); do
        [[ ${lines[$idx]} =~ $expected ]] && return 0
      done
      { local -ar single=(
          'regexp'  "$expected"
        )
        local -ar may_be_multi=(
          'output' "$output"
        )
        local -ir width="$( batslib_get_max_single_line_key_width \
                              "${single[@]}" "${may_be_multi[@]}" )"
        batslib_print_kv_single "$width" "${single[@]}"
        batslib_print_kv_single_or_multi "$width" "${may_be_multi[@]}"
      } | batslib_decorate 'no output line matches regular expression' \
        | fail
    elif (( is_mode_partial )); then
      local -i idx
      for (( idx = 0; idx < ${#lines[@]}; ++idx )); do
        [[ ${lines[$idx]} == *"$expected"* ]] && return 0
      done
      { local -ar single=(
          'substring' "$expected"
        )
        local -ar may_be_multi=(
          'output'    "$output"
        )
        local -ir width="$( batslib_get_max_single_line_key_width \
                              "${single[@]}" "${may_be_multi[@]}" )"
        batslib_print_kv_single "$width" "${single[@]}"
        batslib_print_kv_single_or_multi "$width" "${may_be_multi[@]}"
      } | batslib_decorate 'no output line contains substring' \
        | fail
    else
      local -i idx
      for (( idx = 0; idx < ${#lines[@]}; ++idx )); do
        [[ ${lines[$idx]} == "$expected" ]] && return 0
      done
      { local -ar single=(
          'line'   "$expected"
        )
        local -ar may_be_multi=(
          'output' "$output"
        )
        local -ir width="$( batslib_get_max_single_line_key_width \
                            "${single[@]}" "${may_be_multi[@]}" )"
        batslib_print_kv_single "$width" "${single[@]}"
        batslib_print_kv_single_or_multi "$width" "${may_be_multi[@]}"
      } | batslib_decorate 'output does not contain line' \
        | fail
    fi
  fi
}

# Fail and display details if the unexpected line is found in the output
# (default) or in a specific line of it.
#
# By default, the entire output is searched for the unexpected line. The
# unexpected line is matched against every element of `${lines[@]}'. If
# a match is found, the assertion fails. Details include the unexpected
# line, the index of the first match and `${lines[@]}' with the matching
# line highlighted if `${lines[@]}' is longer than one line.
#
# When `--index <idx>' is specified, only the <idx>-th line is matched.
# If the unexpected line matches `${lines[<idx>]}', the assertion fails.
# Details include <idx> and the unexpected line.
#
# By default, literal matching is performed. A literal match fails if
# the unexpected string does not equal the matched string.
#
# Option `--partial' enables partial matching. A partial match fails if
# the unexpected substring is found in the target string. When used with
# `--index <idx>', the unexpected substring is also displayed on
# failure.
#
# Option `--regexp' enables regular expression matching. A regular
# expression match fails if the extended regular expression matches the
# target string. When used with `--index <idx>', the regular expression
# is also displayed on failure. An invalid regular expression causes an
# error to be displayed.
#
# It is an error to use partial and regular expression matching
# simultaneously.
#
# Mandatory arguments to long options are mandatory for short options
# too.
#
# Globals:
#   output
#   lines
# Options:
#   -n, --index <idx> - match the <idx>-th line
#   -p, --partial - partial matching
#   -e, --regexp - extended regular expression matching
# Arguments:
#   $1 - unexpected line
# Returns:
#   0 - match not found
#   1 - otherwise
# Outputs:
#   STDERR - details, on failure
#            error message, on error
# FIXME(ztombol): Display `${lines[@]}' instead of `$output'!
refute_line() {
  local -i is_match_line=0
  local -i is_mode_partial=0
  local -i is_mode_regexp=0

  # Handle options.
  while (( $# > 0 )); do
    case "$1" in
      -n|--index)
        if (( $# < 2 )) || ! [[ $2 =~ ^([0-9]|[1-9][0-9]+)$ ]]; then
          echo "\`--index' requires an integer argument: \`$2'" \
            | batslib_decorate 'ERROR: refute_line' \
            | fail
          return $?
        fi
        is_match_line=1
        local -ri idx="$2"
        shift 2
        ;;
      -p|--partial) is_mode_partial=1; shift ;;
      -e|--regexp) is_mode_regexp=1; shift ;;
      --) shift; break ;;
      *) break ;;
    esac
  done

  if (( is_mode_partial )) && (( is_mode_regexp )); then
    echo "\`--partial' and \`--regexp' are mutually exclusive" \
      | batslib_decorate 'ERROR: refute_line' \
      | fail
    return $?
  fi

  # Arguments.
  local -r unexpected="$1"

  if (( is_mode_regexp == 1 )) && [[ '' =~ $unexpected ]] || (( $? == 2 )); then
    echo "Invalid extended regular expression: \`$unexpected'" \
      | batslib_decorate 'ERROR: refute_line' \
      | fail
    return $?
  fi

  # Matching.
  if (( is_match_line )); then
    # Specific line.
    if (( is_mode_regexp )); then
      if [[ ${lines[$idx]} =~ $unexpected ]] || (( $? == 0 )); then
        batslib_print_kv_single 6 \
            'index' "$idx" \
            'regexp' "$unexpected" \
            'line'  "${lines[$idx]}" \
          | batslib_decorate 'regular expression should not match line' \
          | fail
      fi
    elif (( is_mode_partial )); then
      if [[ ${lines[$idx]} == *"$unexpected"* ]]; then
        batslib_print_kv_single 9 \
            'index'     "$idx" \
            'substring' "$unexpected" \
            'line'      "${lines[$idx]}" \
          | batslib_decorate 'line should not contain substring' \
          | fail
      fi
    else
      if [[ ${lines[$idx]} == "$unexpected" ]]; then
        batslib_print_kv_single 5 \
            'index' "$idx" \
            'line'  "${lines[$idx]}" \
          | batslib_decorate 'line should differ' \
          | fail
      fi
    fi
  else
    # Line contained in output.
    if (( is_mode_regexp )); then
      local -i idx
      for (( idx = 0; idx < ${#lines[@]}; ++idx )); do
        if [[ ${lines[$idx]} =~ $unexpected ]]; then
          { local -ar single=(
              'regexp'  "$unexpected"
              'index'  "$idx"
            )
            local -a may_be_multi=(
              'output' "$output"
            )
            local -ir width="$( batslib_get_max_single_line_key_width \
                                "${single[@]}" "${may_be_multi[@]}" )"
            batslib_print_kv_single "$width" "${single[@]}"
            if batslib_is_single_line "${may_be_multi[1]}"; then
              batslib_print_kv_single "$width" "${may_be_multi[@]}"
            else
              may_be_multi[1]="$( printf '%s' "${may_be_multi[1]}" \
                                    | batslib_prefix \
                                    | batslib_mark '>' "$idx" )"
              batslib_print_kv_multi "${may_be_multi[@]}"
            fi
          } | batslib_decorate 'no line should match the regular expression' \
            | fail
          return $?
        fi
      done
    elif (( is_mode_partial )); then
      local -i idx
      for (( idx = 0; idx < ${#lines[@]}; ++idx )); do
        if [[ ${lines[$idx]} == *"$unexpected"* ]]; then
          { local -ar single=(
              'substring' "$unexpected"
              'index'     "$idx"
            )
            local -a may_be_multi=(
              'output'    "$output"
            )
            local -ir width="$( batslib_get_max_single_line_key_width \
                                "${single[@]}" "${may_be_multi[@]}" )"
            batslib_print_kv_single "$width" "${single[@]}"
            if batslib_is_single_line "${may_be_multi[1]}"; then
              batslib_print_kv_single "$width" "${may_be_multi[@]}"
            else
              may_be_multi[1]="$( printf '%s' "${may_be_multi[1]}" \
                                    | batslib_prefix \
                                    | batslib_mark '>' "$idx" )"
              batslib_print_kv_multi "${may_be_multi[@]}"
            fi
          } | batslib_decorate 'no line should contain substring' \
            | fail
          return $?
        fi
      done
    else
      local -i idx
      for (( idx = 0; idx < ${#lines[@]}; ++idx )); do
        if [[ ${lines[$idx]} == "$unexpected" ]]; then
          { local -ar single=(
              'line'   "$unexpected"
              'index'  "$idx"
            )
            local -a may_be_multi=(
              'output' "$output"
            )
            local -ir width="$( batslib_get_max_single_line_key_width \
                                "${single[@]}" "${may_be_multi[@]}" )"
            batslib_print_kv_single "$width" "${single[@]}"
            if batslib_is_single_line "${may_be_multi[1]}"; then
              batslib_print_kv_single "$width" "${may_be_multi[@]}"
            else
              may_be_multi[1]="$( printf '%s' "${may_be_multi[1]}" \
                                    | batslib_prefix \
                                    | batslib_mark '>' "$idx" )"
              batslib_print_kv_multi "${may_be_multi[@]}"
            fi
          } | batslib_decorate 'line should not be in output' \
            | fail
          return $?
        fi
      done
    fi
  fi
}
