#!/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