#!/bin/sh

AWK="mawk"
colordiff=$(command -v colordiff 2>&-)

help_shtest() {
    cat <<EOF
shtest -- run command line tests
Usage: shtest [ OPTS ] TEST+
Options:
    -s EXPR -- shell expression which will get test command piped to
        e.g. 'zsh -c "emulate sh;sh"' or 'bash --posix'; default is "/bin/sh"
    -q -- quite test; exit code set to number of failed tests
        default is verbose output
    -r -- refill all tests output (like cram -iy)
    -c -- clear all model output from test (higher priority than -r)
    -i NUM -- indent (default is 4)
    -b -- fail test on unhandled error code
    -e -- keep default environment (don't inherit LC_ALL and PATH from shell)
    -E ENV -- set custom environment prefix for each call
    -f -- force tests ignoring faults
    -C -- do not colorize diff
    -d -- discard commands stderr
    -x -- show which commands are executed
    -h -- this help
EOF
}

check_string_chars() {
    case "$1" in
        $2) printf "%s\n" "$3"; return 1; ;;
    esac
}

# defaults
shtest_shell='/bin/sh'
shtest_quite=''
shtest_refill=''
shtest_clear=''
shtest_indent="    "
shtest_brick=''
shtest_keepenv=''
shtest_setenv=''
shtest_force=''
shtest_colordiff=1
shtest_discard=''
shtest_showexec=''

: ${1?$(help_shtest)}

OPTIND=1
while getopts s:qrci:bfeE:Cdxh opt; do
    case "${opt}" in
        s)  shtest_shell="${OPTARG}" ;;
        q)  shtest_quite=1 ;;
        r)  shtest_refill=1 ;;
        c)  shtest_clear=1 ;;
        i)  check_string_chars "${OPTARG}" "*[!0-9]*" "Incorrect indent (must be number)" || return 1
            shtest_indent=''
            while [ "${OPTARG}" -ne 0 ]; do
                shtest_indent="${shtest_indent} "; OPTARG=$(expr $OPTARG - 1); done
            ;;
        b)  shtest_brick=1 ;;
        f)  shtest_force=1 ;;
        e)  shtest_keepenv=1 ;;
        E)  shtest_setenv="${OPTARG}" ;;
        C)  shtest_colordiff='' ;;
        d)  shtest_discard=1 ;;
        x)  shtest_showexec=1 ;;
        h)  help_shtest; exit 0 ;;
    esac
done

shift $(expr $OPTIND - 1)

custom_env=''
if [ -z "${shtest_keepenv}" ]; then
    custom_env=$(printf 'LC_ALL="%s" PATH="%s" %s' "${LC_ALL}" "${PATH}" "${shtest_setenv}" )
else
    custom_env=$(printf 'LC_ALL="%s"' "C")
fi

#
# functions
#
#

command_prefix="${shtest_indent}$ "
auxcommand_prefix="${shtest_indent}% "
multiline_prefix="${shtest_indent}> "
exitcode_prefix="${shtest_indent}? "

dump_line() {
    printf "%s\n" "${line}" >> "${errfile}"
}

nl="$(printf '\n ')"; nl="${nl% }"

test_parser() {
    # parse_state:
    # empty - not in any state
    #   command/auxcommand
    # 1 multiline
    state_multiline=1
    #   exitcode
    # 2 output
    state_output=2
    # 3 ready to execute
    state_ready=3
    case "${line}" in
        "${auxcommand_prefix}"*)
            command="${line#${auxcommand_prefix}}"
            cmdlineno="${lineno}"
            parse_state="${state_ready}"
            auxcommand=1
            dump_line
            ;;
        "${command_prefix}"*)
            if [ -z "${parse_state}" ]; then
                command="${line#${command_prefix}}"
                cmdlineno="${lineno}"
                parse_state="${state_multiline}"
                test_count=$(expr ${test_count} + 1)
                dump_line
            else
                buffer="${line}"
                parse_state="${state_ready}"
            fi
            ;;
        "${multiline_prefix}"*)
            case "${parse_state}" in
                "${state_multiline}") command="${command:+${command}${nl}}${line#${multiline_prefix}}" ;;
                '') next ;;
                "${state_output}") output="${output:+${output}${nl}}${line#${shtest_indent}}" ;;
            esac
            dump_line
            ;;
        "${exitcode_prefix}"*)
            case "${parse_state}" in
                "${state_multiline}")
                    exitcode="${line#${exitcode_prefix}}"
                    parse_state="${state_output}"
                    ;;
                '') next ;;
                *)
                    parse_state="${state_output}"
                    output="${output:+${output}${nl}}${line#${shtest_indent}}"
                    ;;
            esac
            [ -z "${shtest_clear}" ] && dump_line
            ;;
        "${shtest_indent}"*)
            case "${parse_state}" in
                "${state_output}")
                    output="${output:+${output}${nl}}${line#${shtest_indent}}"
                    parse_state="${state_output}"
                    ;;
                '') dump_line; return ;;
                *)
                    parse_state="${state_output}"
                    output="${output:+${output}${nl}}${line#${shtest_indent}}"
                    ;;
            esac
            # this isn't dumped by default
            ;;
        '')
            if [ -n "${parse_state}" ]; then
                parse_state="${state_ready}"
                buffer=''; fullbuffer=1
            else
                dump_line
            fi
            ;;
        *)
            if [ -n "${parse_state}" ]; then
                parse_state="${state_ready}"
                buffer="${line}"
            else
                dump_line
            fi
            ;;
    esac
}

report() {
    [ -n "${shtest_quite}" ] || printf >&2 "%s\n" "$*"
}

process_result() {
    local e
    if [ -n "${shtest_refill}" ]; then
        sed "s/^/${shtest_indent}/" "${result}" >> "${errfile}"
        return
    fi
    e=$(printf "%s\n" "${output}" | sed 's/\\/\\\\/g')
    ${AWK} -v output="$e" -- '
        BEGIN { split(output,o,"\n")}
        {
            if (o[NR] ~ / \(re\)$/) {
                sub(" \(re\)$","",o[NR]);
                if ($0 !~ "^" o[NR] "$" ) exit NR
                next
            } else if ($0 != o[NR]) exit NR
        }
        END { t=NR+1; if (o[t]!="") exit t}
        ' "${result}" 2>/dev/null
    e=$?
    if [ "$e" -ne 0 ]; then
        diff_result=$( printf "%s\n" "${output}" | diff -u "${result}" - )
        printf "%s\n" "${diff_result}" >> "${errfile}"
        [ -n "${shtest_quite}" ] && return 1
        printf "Test failed: %s\n" "${command}"
        printf "Test failed at %s line:\n" "${cmdlineno}"
        printf "%s\n" "${diff_result}" | \
            if [ -n "${shtest_colordiff}" -a -n "${colordiff}" ]; then colordiff; else cat; fi
        return 1
    else
        sed "s/^/${shtest_indent}/" "${result}" >> "${errfile}"
    fi
}

runcommand() {
    if [ -n "${shtest_discard}" ]; then
        printf '%s %s' "${custom_env}" "${command}" | eval "${shtest_shell}" 2>/dev/null
        cmdexitcode="$?"
    else
        printf '%s %s' "${custom_env}" "${command}" | eval "${shtest_shell}" 2>&1
        cmdexitcode="$?"
    fi
}

test_result() {
    if [ "${parse_state}" = "${state_ready}" -a -n "${command}" ]; then
        if [ -z "${shtest_clear}" ]; then
            [ -z "${auxcommand}" -a -n "${shtest_showexec}" ] && \
                report "Executing ${command}"
            if [ -n "${auxcommand}" ]; then
                runcommand
            else
                runcommand > "${result}"
            fi
            if [ -z "${auxcommand}" ]; then
                process_result || {
                    test_failed=$(expr ${test_failed} + 1 )
                    [ -z "${shtest_force}" -a -z "${shtest_refill}" ] && exit ${test_count}
                }
                if [ -z "${shtest_refill}" ]; then
                    case "${cmdexitcode}" in
                        80)             exit 0 ;;
                        83)             exit 83 ;;
                        "${exitcode}")  : ;;
                        *)
                            test_unhandledexit=$( expr ${test_unhandledexit} + 1)
                            report "Unhandled exit code ${cmdexitcode} " \
                                "(expected ${exitcode}) at line ${cmdlineno}"
                            [ -z "${shtest_force}" -a -n "${shtest_brick}" ] && exit 81
                            ;;
                    esac
                fi
            fi
        fi
        command=''; output=''; exitcode='0'; auxcommand=''; parse_state=''
    fi
}

# args:
# 1 -- file with tests
run_test() {
    local IFS command buffer fullbuffer output exitcode auxcommand line errfile lineno cmdlineno test_count test_failed parse_state
    lineno=0; cmdlineno=0; test_count=0; test_failed=0; test_unhandledexit=0
    errfile="${1%.t}.err"; : > "${errfile}"
    command=''; buffer=''; fullbuffer=''; output=''; exitcode='0'; auxcommand=''; parse_state=''
    report "Starting tests from $1"
    result=$(mktemp || { report "Can't create temp file!"; exit 82 ; } )
    trap "rm ${result} 2>/dev/null" INT TERM EXIT
    backifs="${IFS}"
    IFS=""
    while :; do
        if [ -n "${buffer}" -o -n "${fullbuffer}" ]; then
            line="${buffer}"; buffer=''; fullbuffer=''
        else
            lineno=$(expr ${lineno} + 1)
            read -r line || break
        fi
        test_parser
        test_result
    done < "$1"
    parse_state="${state_ready}"
    test_result
    rm "${result}" 2>/dev/null
    report "Finished ${test_count} test from $1, ${test_failed} failed, " \
        "${test_unhandledexit} unhandled exit codes"
    if [ -n "${shtest_refill}" -o -n "${shtest_clear}" ]; then
        mv "${errfile}" "$1"
        [ "$?" -ne 0 ] && report "Can't move refilled tests file back!"
    elif [ "${test_failed}" = "0" ]; then
        rm "${errfile}" 2>/dev/null
    fi
}

#
# main
#
#

for test_suite in "$@"; do
    run_test "${test_suite}"
done

exit 0

