#!/usr/bin/env bash
#
# Run one or more tests.
#
# Command-line usage to run all tests.
#
#     ./run-tests
#
# To run only one test, run "tests/test-name".
#
# Usage within a test as a template. Source run-tests to get functions, export
# any necessary variables, then call runTest.
#
#     #!/usr/bin/env bash
#     cd "${0%/*}" || exit 1
#     . ../run-tests
#
#     export template="This is a template"
#     export expected="This is a template"
#     runTest
#
# When used within the test, you control various aspects with environment
# variables or functions.
#
# - The content passed into mo is either the variable "$template" or the output
#   of the function called template.
# - The expected result is either "$expected" or the function called expected.
# - The expected return code is "$returnCode" and defaults to 0.
# - The arguments to pass to mo is the array "${arguments[@]}" and defaults to ().
#
# When $MO_DEBUG is set to a non-empty value, the test does not run, but mo is
# simply executed directly. This allows for calling mo in the same manner as
# the test but does not buffer output nor expect the output to match the
# expected.
#
# When $MO_DEBUG_TEST is set to a non-empty value, the expected and actual
# results are shown using "declare -p" to provide an easier time seeing the
# differences, especially with whitespace.

testCase() {
    echo "Input: $1"
    echo "Expected: $2"
}

indirect() {
    unset -v "$1"
    printf -v "$1" '%s' "$2"
}

getValue() {
    local name temp len hardSpace

    name=$2
    hardSpace=" "

    if declare -f "$name" &> /dev/null; then
        temp=$("$name"; echo -n "$hardSpace")
        len=$((${#temp} - 1))

        if [[ "${temp:$len}" == "$hardSpace" ]]; then
            temp=${temp:0:$len}
        fi
    else
        temp=${!name}
    fi

    local "$1" && indirect "$1" "$temp"
}

runTest() (
    local testTemplate testExpected testActual hardSpace len testReturnCode testFail

    hardSpace=" "
    . ../mo

    getValue testTemplate template
    getValue testExpected expected

    if [[ -n "${MO_DEBUG:-}" ]]; then
        echo -n "$testTemplate" | mo ${arguments[@]+"${arguments[@]}"} 2>&1

        return $?
    fi

    testActual=$(echo -n "$testTemplate" | mo ${arguments[@]+"${arguments[@]}"} 2>&1; echo -n "$hardSpace$?")
    testReturnCode=${testActual##*$hardSpace}
    testActual=${testActual%$hardSpace*}
    testFail=false

    if [[ "$testActual" != "$testExpected" ]]; then
        echo "Failure"
        echo "Expected:"
        echo "$testExpected"
        echo "Actual:"
        echo "$testActual"

        if [[ -n "${MO_DEBUG_TEST-}" ]]; then
            declare -p testExpected
            # Align the two declare outputs
            echo -n "  "
            declare -p testActual
        fi

        testFail=true
    fi

    if [[ "$testReturnCode" != "$returnCode" ]]; then
        echo "Expected return code $returnCode, but got $testReturnCode"
        testFail=true
    fi

    if [[ "$testFail" == "true" ]]; then
        return 1
    fi

    return 0
)

runTestFile() (
    local file=$1

    echo "Test: $file"
    "$file"
)

runTests() (
    PASS=0
    FAIL=0

    if [[ $# -gt 0 ]]; then
        for TEST in "$@"; do
            runTestFile "$TEST" && PASS=$((PASS + 1)) || FAIL=$((FAIL + 1))
        done
    else
        cd "${0%/*}"
        for TEST in tests/*; do
            if [[ -f "$TEST" ]]; then
                runTestFile "$TEST" && PASS=$((PASS + 1)) || FAIL=$((FAIL + 1))
            fi
        done
    fi

    echo ""
    echo "Pass: $PASS"
    echo "Fail: $FAIL"

    if [[ $FAIL -gt 0 ]]; then
        exit 1
    fi
)

# Clear test related variables
template="Template not defined"
expected="Expected not defined"
returnCode=0
arguments=()

# If sourced, load functions.
# If executed, perform the actions as expected.
if [[ "$0" == "${BASH_SOURCE[0]}" ]] || [[ -z "${BASH_SOURCE[0]}" ]]; then
    runTests ${@+"${@}"}
fi