# refute_line
# ===========
#
# Summary: Fail if the unexpected line is found in the output (default) or at a specific line number.
#
# Usage: refute_line [-n index] [-p | -e] [--] <unexpected>
#
# Options:
#   -n, --index <idx> Match the <idx>th line
#   -p, --partial     Match if `unexpected` is a substring of `$output` or line <idx>
#   -e, --regexp      Treat `unexpected` as an extended regular expression
#   <unexpected>      The unexpected line string, substring, or regular expression.
#
# IO:
#   STDERR - details, on failure
#            error message, on error
# Globals:
#   output
#   lines
# Returns:
#   0 - if match not found
#   1 - otherwise
#
# Similarly to `refute_output`, this function verifies that a command or function does not produce the unexpected output.
# (It is the logical complement of `assert_line`.)
# It checks that the unexpected line does not appear in the output (default) or at a specific line number.
# Matching can be literal (default), partial or regular expression.
#
# ## Looking for a line in the output
#
# By default, the entire output is searched for the unexpected line.
# The assertion fails if the unexpected line is found in `${lines[@]}`.
#
#   ```bash
#   @test 'refute_line() looking for line' {
#     run echo $'have-0\nwant\nhave-2'
#     refute_line 'want'
#   }
#   ```
#
# On failure, the unexpected line, the index of its first match and the output with the matching line highlighted are displayed.
#
#   ```
#   -- line should not be in output --
#   line  : want
#   index : 1
#   output (3 lines):
#     have-0
#   > want
#     have-2
#   --
#   ```
#
# ## Matching a specific line
#
# When the `--index <idx>` option is used (`-n <idx>` for short), the unexpected line is matched only against the line identified by the given index.
# The assertion fails if the unexpected line equals `${lines[<idx>]}`.
#
#   ```bash
#   @test 'refute_line() specific line' {
#     run echo $'have-0\nwant-1\nhave-2'
#     refute_line --index 1 'want-1'
#   }
#   ```
#
# On failure, the index and the unexpected line are displayed.
#
#   ```
#   -- line should differ --
#   index : 1
#   line  : want-1
#   --
#   ```
#
# ## Partial matching
#
# Partial matching can be enabled with the `--partial` option (`-p` for short).
# When used, a match fails if the unexpected *substring* is found in the matched line.
#
#   ```bash
#   @test 'refute_line() partial matching' {
#     run echo $'have 1\nwant 2\nhave 3'
#     refute_line --partial 'want'
#   }
#   ```
#
# On failure, in addition to the details of literal matching, the substring is also displayed.
# When used with `--index <idx>` the substring replaces the unexpected line.
#
#   ```
#   -- no line should contain substring --
#   substring : want
#   index     : 1
#   output (3 lines):
#     have 1
#   > want 2
#     have 3
#   --
#   ```
#
# ## Regular expression matching
#
# Regular expression matching can be enabled with the `--regexp` option (`-e` for short).
# When used, a match fails if the *extended regular expression* matches the line being tested.
#
# *__Note__:
# As expected, the anchors `^` and `$` bind to the beginning and the end (respectively) of the matched line.*
#
#   ```bash
#   @test 'refute_line() regular expression matching' {
#     run echo $'Foobar v0.1.0\nRelease date: 2015-11-29'
#     refute_line --index 0 --regexp '^Foobar v[0-9]+\.[0-9]+\.[0-9]$'
#   }
#   ```
#
# On failure, in addition to the details of literal matching, the regular expression is also displayed.
# When used with `--index <idx>` the regular expression replaces the unexpected line.
#
#   ```
#   -- regular expression should not match line --
#   index  : 0
#   regexp : ^Foobar v[0-9]+\.[0-9]+\.[0-9]$
#   line   : Foobar v0.1.0
#   --
#   ```
# FIXME(ztombol): Display `${lines[@]}' instead of `$output'!
refute_line() {
  __refute_stream_line "$@"
}

# refute_stderr_line
# ==================
#
# Summary: Fail if the unexpected line is found in the stderr (default) or at a specific line number.
#
# Usage: refute_stderr_line [-n index] [-p | -e] [--] <unexpected>
#
# Options:
#   -n, --index <idx> Match the <idx>th line
#   -p, --partial     Match if `unexpected` is a substring of `$stderr` or line <idx>
#   -e, --regexp      Treat `unexpected` as an extended regular expression
#   <unexpected>      The unexpected line string, substring, or regular expression.
#
# IO:
#   STDERR - details, on failure
#            error message, on error
# Globals:
#   stderr
#   stderr_lines
# Returns:
#   0 - if match not found
#   1 - otherwise
#
# Similarly to `refute_stderr`, this function verifies that a command or function does not produce the unexpected stderr.
# (It is the logical complement of `assert_stderr_line`.)
# It checks that the unexpected line does not appear in the stderr (default) or at a specific line number.
# Matching can be literal (default), partial or regular expression.
#
refute_stderr_line() {
  __refute_stream_line "$@"
}

__refute_stream_line() {
  local -r caller=${FUNCNAME[1]}
  local -i is_match_line=0
  local -i is_mode_partial=0
  local -i is_mode_regexp=0

  if [[ "${caller}" == "refute_line" ]]; then
    : "${lines?}"
    local -ar stream_lines=("${lines[@]}")
    local -r stream_type=output
  elif [[ "${caller}" == "refute_stderr_line" ]]; then
    : "${stderr_lines?}"
    local -ar stream_lines=("${stderr_lines[@]}")
    local -r stream_type=stderr
  else
    # Unknown caller
    echo "Unexpected call to \`${FUNCNAME[0]}\`
Did you mean to call \`refute_line\` or \`refute_stderr_line\`?" |
      batslib_decorate "ERROR: ${FUNCNAME[0]}" |
      fail
    return $?
  fi

  # 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: ${caller}" \
        | 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: ${caller}" \
    | 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: ${caller}" \
    | fail
    return $?
  fi

  # Matching.
  if (( is_match_line )); then
    # Specific line.
    if (( is_mode_regexp )); then
      if [[ ${stream_lines[$idx]} =~ $unexpected ]]; then
        batslib_print_kv_single 6 \
        'index' "$idx" \
        'regexp' "$unexpected" \
        'line'  "${stream_lines[$idx]}" \
        | batslib_decorate 'regular expression should not match line' \
        | fail
      fi
    elif (( is_mode_partial )); then
      if [[ ${stream_lines[$idx]} == *"$unexpected"* ]]; then
        batslib_print_kv_single 9 \
        'index'     "$idx" \
        'substring' "$unexpected" \
        'line'      "${stream_lines[$idx]}" \
        | batslib_decorate 'line should not contain substring' \
        | fail
      fi
    else
      if [[ ${stream_lines[$idx]} == "$unexpected" ]]; then
        batslib_print_kv_single 5 \
        'index' "$idx" \
        'line'  "${stream_lines[$idx]}" \
        | batslib_decorate 'line should differ' \
        | fail
      fi
    fi
  else
    # Line contained in output/error stream.
    if (( is_mode_regexp )); then
      local -i idx
      for (( idx = 0; idx < ${#stream_lines[@]}; ++idx )); do
        if [[ ${stream_lines[$idx]} =~ $unexpected ]]; then
          { local -ar single=( 'regexp' "$unexpected" 'index' "$idx" )
            local -a may_be_multi=( "${stream_type}" "${!stream_type}" )
            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 < ${#stream_lines[@]}; ++idx )); do
        if [[ ${stream_lines[$idx]} == *"$unexpected"* ]]; then
          { local -ar single=( 'substring' "$unexpected" 'index' "$idx" )
            local -a may_be_multi=( "${stream_type}" "${!stream_type}" )
            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 < ${#stream_lines[@]}; ++idx )); do
        if [[ ${stream_lines[$idx]} == "$unexpected" ]]; then
          { local -ar single=( 'line' "$unexpected" 'index' "$idx" )
            local -a may_be_multi=( "${stream_type}" "${!stream_type}" )
            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 ${stream_type}" \
          | fail
          return $?
        fi
      done
    fi
  fi
}
