#
# 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
#   -, --stdin - read expected output from the standard input
# Arguments:
#   $1 - 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
    local -i is_mode_nonempty=0
    local -i use_stdin=0

    # Handle options.
    if (($# == 0)); then
        is_mode_nonempty=1
    fi

    while (($# > 0)); do
        case "$1" in
            -p | --partial)
                is_mode_partial=1
                shift
                ;;
            -e | --regexp)
                is_mode_regexp=1
                shift
                ;;
            - | --stdin)
                use_stdin=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
    if ((use_stdin)); then
        expected="$(cat -)"
    else
        expected="$1"
    fi

    # Matching.
    if ((is_mode_nonempty)); then
        if [ -z "$output" ]; then
            echo 'expected non-empty output, but output was empty' |
                batslib_decorate 'no output' |
                fail
        fi
    elif ((is_mode_regexp)); then
        if [[ '' =~ $expected ]] || (($? == 2)); then
            echo "Invalid extended regular expression: \`$expected'" |
                batslib_decorate 'ERROR: assert_output' |
                fail
        elif ! [[ $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
#   -, --stdin - read unexpected output from the standard input
# Arguments:
#   $1 - 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
    local -i is_mode_empty=0
    local -i use_stdin=0

    # Handle options.
    if (($# == 0)); then
        is_mode_empty=1
    fi

    while (($# > 0)); do
        case "$1" in
            -p | --partial)
                is_mode_partial=1
                shift
                ;;
            -e | --regexp)
                is_mode_regexp=1
                shift
                ;;
            - | --stdin)
                use_stdin=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
    if ((use_stdin)); then
        unexpected="$(cat -)"
    else
        unexpected="$1"
    fi

    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_empty)); then
        if [ -n "$output" ]; then
            batslib_print_kv_single_or_multi 6 \
                'output' "$output" |
                batslib_decorate 'output non-empty, but expected no output' |
                fail
        fi
    elif ((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
}
