#!/bin/sh
#
# unittest - unit tests framework for shell scripts.
#
# https://github.com/macie/unittest.sh
#
# Copyright (c) 2014-2023 Maciej Żok <maciek.zok@gmail.com>
# MIT License (http://opensource.org/licenses/MIT)


#
#   MESSAGES
#

unittest__print_result() {
    # Write test status message to stdout
    # $1 - test location
    # $2 - test status
    # prefix: utt9t_
    utt9t_color_default=''
    utt9t_color_location=''
    utt9t_color_status=''

    if { [ -n "${CLICOLOR_FORCE}" ] && [ "${CLICOLOR_FORCE}" -ne 0 ]; } || { [ -t 1 ] && [ -z "${NO_COLOR}" ]; }; then  # stdout is interactive terminal
        utt9t_color_default='\033[0m'
        case $2 in
            PASS)  # location: default; status: green
                utt9t_color_location='\033[0m'
                utt9t_color_status='\033[32m'
                ;;
            FAIL)  # location: red; status: white on red
                utt9t_color_location='\033[31m'
                utt9t_color_status='\033[97;41m'
                ;;
            SKIP)  # location: gray; status: gray
                utt9t_color_location='\033[37m'
                utt9t_color_status='\033[37m'
                ;;
        esac
    fi

    printf "${utt9t_color_location}%s\t${utt9t_color_status}%s${utt9t_color_default}\n" "$1" "$2"

    unset -v utt9t_color_default utt9t_color_location utt9t_color_status
    return 0
}

unittest__print_debug() {
    # $1 - category
    # $2-... - paragraphs
    # prefix: utt13o_
    utt13o_color=''
    utt13o_color_default=''

    if { [ -n "${CLICOLOR_FORCE}" ] && [ "${CLICOLOR_FORCE}" -ne 0 ]; } || { [ -t 2 ] && [ -z "${NO_COLOR}" ]; }; then  # stderr is interactive terminal
        utt13o_color='\033[34m'  # blue
        utt13o_color_default='\033[0m'
        utt13o_color_quote='\033[37m'  # gray
    fi

    printf "\n${utt13o_color}-- %s${utt13o_color_default}\n\n" "$1" >&2

    shift 1
    for utt13o_paragraph in "$@"; do
        # shellcheck disable=SC2059
        case ${utt13o_paragraph} in
            ' '*) printf "${utt13o_color_quote}" >&2 ;;
            *)  ;;
        esac
        printf "%s${utt13o_color_default}\n\n" "${utt13o_paragraph}" >&2
    done

    unset -v utt13o_paragraph utt13o_color utt13o_color_default
    return 0
}

#
#   ASSERTIONS
#

# shellcheck disable=SC2317
test() {
    # same arguments as command test(1)
    UT4e1_test_error_msg=$(/bin/test "$@" 2>&1)
    case $? in
        0)  ;;
        1)
            unittest__print_debug "FAILED TEST [${UNITTEST_CURRENT}]" \
                'I expected:' \
                "    test$(printf " '%s'" "$@")" \
                'to be true, but the result was false.'
            return 1
            ;;
        *)
            unittest__print_debug "INVALID ASSERTION [${UNITTEST_CURRENT}]" \
                'I tried to check' \
                "    test$(printf " '%s'" "$@")" \
                'but I got error with message:' \
                "    ${UT4e1_test_error_msg}" \
                'Did you use proper operator?' \
                "Hint: Some operators requires specific type of values. Read 'man test' to learn more."
            return 1
            ;;
    esac

    return 0
}


#
#   FUNCTIONS
#

##
# Find files with tests (test_*.sh).
# SYNOPSIS:
#     unittest__test_files [directory...]
# OPERANDS:
#     directory - A pathname of directory to search in. If no directory is
#         given, it will look for 'tests' directory inside current one. If
#         a directory is '-', it will use stdin.
# STDIN:
#     (optional) List of directories to search in. Used when call with '-' argument.
# STDOUT:
#     List of 'test_*.sh' file paths.
# STDERR:
#     (optional) Debug/error message.
# EXIT STATUS:
#     0 - Successfully traversed all directories.
#    >0 - An error occurred.
# EXAMPLES:
#     unittest__test_files ./unit_tests ./integration_tests
#     ls ../ | unittest__test_files -
# CAVEATS:
#     Calling it inside pipeline without '-' will disregards standard input and
#     use defaults instead.
unittest__test_files() {
    {
        if [ "$1" = '-' ]; then
            cat -
        else
            printf '%s\n' "$@"
        fi
     } |
     while read -r unittest_dir; do
         if ! find "${unittest_dir:-./}" -path "*${unittest_dir:-tests/}*" -name 'test_*.sh' 2>/dev/null; then
             unittest__print_debug 'TESTS NOT FOUND' \
                 "I was looking for 'test_*.sh' files inside '${unittest_dir:-tests/}' directory using:" \
                 "    $ find \"${unittest_dir:-./}\" -path \"*${unittest_dir:-tests/}*\" -name 'test_*.sh' -print" \
                 'but instead of files I got an error with message:' \
                 "    $(find "${unittest_dir:-./}" -path "*${unittest_dir:-tests/}*" -name 'test_*.sh' -print 2>&1)"
             unset -v unittest_dir
             return 1
         fi
         unset -v unittest_dir
     done

     return $?
}

##
# Run tests from given files.
# STDIN: List of files.
# STDOUT: Test name with status.
# STDERR: (optional) Debug/error message.
# EXIT STATUS:
#     0 - All tests passed.
#    >0 - Some tests failed.
##
unittest__run() {
    # prefix: utt8r_

    UNITTEST_STATUS=0
    while read -r utt8r_testfile; do
        (
            utt8r_beforeAll=$(sed -n 's/^[ \t]*\(beforeAll\)[ \t]*(.*/\1/p' "${utt8r_testfile}")
            utt8r_afterAll=$(sed -n 's/^[ \t]*\(afterAll\)[ \t]*(.*/\1/p' "${utt8r_testfile}")
            utt8r_beforeEach=$(sed -n -e 's/^[ \t]*\(beforeEach\)[ \t]*(.*/\1/p' -e 's/^[ \t]*\(setUp\)[ \t]*(.*/\1/p' "${utt8r_testfile}")
            utt8r_afterEach=$(sed -n -e 's/^[ \t]*\(afterEach\)[ \t]*(.*/\1/p' -e 's/^[ \t]*\(tearDown\)[ \t]*(.*/\1/p' "${utt8r_testfile}")
            utt8r_tests=$(sed -n 's/^[ \t]*\(x\{0,1\}test_[^(=]*\)(.*/\1/p' "${utt8r_testfile}")

			# shellcheck source=/dev/null
            . "${utt8r_testfile}"
            ${utt8r_beforeAll}
            for _current_testcase in ${utt8r_tests}; do
                UNITTEST_CURRENT="${utt8r_testfile#./}:${_current_testcase}"

                case ${_current_testcase} in
                    x*)
                        unittest__print_result "${UNITTEST_CURRENT}" 'SKIP'
                        ;;
                    *)
                        ${utt8r_beforeEach}
                        # test result is status of last command in test
                        if ${_current_testcase}; then
                            unittest__print_result "${UNITTEST_CURRENT}" 'PASS'
                        else
                            # last command in test failed
                            UNITTEST_STATUS=1
                            unittest__print_result "${UNITTEST_CURRENT}" 'FAIL'
                        fi
                        ${utt8r_afterEach}
                        ;;
                esac
            done
            ${utt8r_afterAll}
            unset -v UNITTEST_CURRENT
            exit ${UNITTEST_STATUS}
        )
        UNITTEST_STATUS=$?
    done
    return ${UNITTEST_STATUS}
}


#
#   MAIN ROUTINE
#

{
    # Color output by default: on supported terminals when NO_COLOR is not set.
    # Supported terminals are recognized based on TERM variable. When TERM is
    # not set (for example inside CI environment) we assume that terminal is dumb.
    if [ -z "${NO_COLOR}" ] && [ "$(TERM=${TERM:-dumb} tput colors)" -lt 8 ]; then
        NO_COLOR='YES'
    fi

    case $# in
        0)  # discovery mode
            unittest__test_files | unittest__run
            exit $?
            ;;
        1)
            case $1 in
                -h|--help)
                    cat >&2 <<-'EOF'
						unittest - unit tests framework for shell scripts.

						Usage:
						  unittest [options] [test_directory | test_file]

						Options:
						  -h, --help           Show this help and exit.
						  -v, --version        Show version number and exit.

						Without any arguments it will run all tests from 'tests' directory.
						EOF
                    exit 0
                    ;;

                -v|--version)
                    printf 'unittest 23.11\n' >&2
                    exit 0
                    ;;
                *)  # specified directory/file
                    if [ -d "$1" ]; then
                        unittest__test_files "$1" | unittest__run
                        exit $?
                    elif [ -f "$1" ]; then
                        printf '%s\n' "$1" | unittest__run
                        exit $?
                    fi
                    ;;
            esac
    esac

    unittest__print_debug 'INVALID USAGE' \
        "I cannot understand '$*' option. Did you want to use option or did you misspell file/directory?" \
        'Hint: Find valid usage with:' \
        '    $ unittest -h'
    exit 64  # EX_USAGE
}