#!/bin/bash
#-------------------------------------------------------------------------------
# Copyright (C) 2012-2019 British Crown (Met Office) & Contributors.
#
# This file is part of Rose, a framework for meteorological suites.
#
# Rose is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Rose is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Rose. If not, see <http://www.gnu.org/licenses/>.
#-------------------------------------------------------------------------------
# NAME
#     test_header
#
# SYNOPSIS
#     . $ROSE_HOME/t/lib/bash/test_header
#
# DESCRIPTION
#     Provide bash shell functions for writing tests for "rose" commands to
#     output in Perl's TAP format. Add "set -eu". Create a temporary working
#     directory $TEST_DIR and change to it. Automatically increment test number.
#     If $ROSE_HOME is not specified, set it to point to the "rose" source tree
#     containing this script. Add $ROSE_HOME/bin to the front of $PATH.
#
# FUNCTIONS
#     tests N
#         echo "1..$N".
#     skip N REASON
#         echo "ok $((++T)) # skip REASON" N times, where T is the test number.
#     skip_all REASON
#         echo "1..0 # SKIP $REASON" and exit.
#     pass TEST_KEY
#         echo "ok $T - $TEST_KEY" where T is the current test number.
#     fail TEST_KEY
#         echo "not ok $T - $TEST_KEY" where T is the current test number.
#     run_pass TEST_KEY COMMAND ...
#         Run $COMMAND. pass/fail $TEST_KEY if $COMMAND returns true/false.
#         Write STDOUT and STDERR in $TEST_KEY.out and $TEST_KEY.err.
#     run_fail TEST_KEY COMMAND ...
#         Run $COMMAND. pass/fail $TEST_KEY if $COMMAND returns false/true.
#         Write STDOUT and STDERR in $TEST_KEY.out and $TEST_KEY.err.
#     file_cmp TEST_KEY FILE_ACTUAL [FILE_EXPECT]
#         Compare contents in $FILE_ACTUAL and $FILE_EXPECT. pass/fail
#         $TEST_KEY if contents are identical/different. If $FILE_EXPECT is "-"
#         or not defined, compare $FILE_ACTUAL with STDIN to this function.
#     file_cmp_any TEST_KEY FILE_ACTUAL [FILE_EXPECT]
#         As file_cmp, but FILE_EXPECT should consist of more than one
#         contents set to compare against, separated by a line matching
#         /^__filesep__$/. Iff any contents match, the test passes.
#     file_test TEST_KEY FILE [OPTION]
#         pass/fail $TEST_KEY if "test $OPTION $FILE" returns 0/1. $OPTION is
#         -e if not specified.
#     file_grep TEST_KEY PATTERN FILE
#         Run "grep -q PATTERN FILE". pass/fail $TEST_KEY accordingly.
#     file_grep_fail TEST_KEY PATTERN FILE
#         Run "grep -q PATTERN FILE". pass $TEST_KEY if $PATTERN is not present
#         and fail otherwise.
#     bad_smtpd_init
#         Start a server daemon that will not reply correctly to SMTP.
#         Write host:port setting to the variable TEST_SMTPD_HOST. Write
#         pid of daemon to TEST_SMTPD_PID.
#     bad_smtpd_kill
#         Kill the faux-SMTP server daemon process.
#     mock_smtpd_init
#         Start a mock SMTP server daemon for testing. Write host:port setting
#         to the variable TEST_SMTPD_HOST. Write pid of daemon to
#         TEST_SMTPD_PID. Write log to TEST_SMTPD_LOG.
#     mock_smtpd_kill
#         Kill the mock SMTP server daemon process.
#     poll COMMAND
#         Run COMMAND in 1 second intervals for a minute until COMMAND returns
#         a non-zero value.
#     port_is_busy $PORT
#         Return 0 if $PORT is busy or 1 if $PORT is not busy.
#     rose_ws_init $NS $UTIL
#         Start a Rose web service server. Test server OK. Write to these shell
#         variables on success:
#         * TEST_ROSE_WS_PID - PID of the service server
#         * TEST_ROSE_WS_PORT - Port of the service server on localhost.
#         * TEST_ROSE_WS_URL - URL of service server.
#         E.g. rose_ws_init rose bush
#     rose_ws_kill
#         Kill a web service server started by "rose_ws_init" and remove
#         generated log and status files.
#     rose_ws_json_greps TEST_KEY JSON-FILE EXPECTED [EXPECTED ...]
#         Load JSON content in argument 1
#         Each remaining argument is an expected content in the JSON data in
#         the form: [keys, value] where "keys" is a list containing keys or
#         indexes to an expected item in the data structure, and the value is
#         an expected value. A key in keys can be a simple dict key or an
#         array index. It can also be a dict {attr_key: attr_value, ...}. In
#         which case, the expected data item is under a list of dicts, where a
#         unique dict in the list contains all elements attr_key: attr_value.
#
# VARIABLES
#     TEST_DIR
#         Tests will run in this temporary directory.
#     TEST_KEY_BASE
#         The base name without the "*.t" extension of the test file.
#     TEST_NUMBER
#         Number of tests already run.
#     TEST_SOURCE_DIR
#         The directory containing of the current test file.
#-------------------------------------------------------------------------------
set -eu

test_finally() {
    trap '' 'EXIT'
    trap '' 'INT'
    if [[ -n "${TEST_DIR:-}" ]]; then
        cd ~
        rm -rf "${TEST_DIR}"
    fi
    if [[ -n "${TEST_SMTPD_PID:-}" ]]; then
        kill "${TEST_SMTPD_PID}"
    fi
    # Allow custom clean up
    if declare -F 'my_test_finally' 1>'/dev/null' 2>&1; then
        my_test_finally "$@"
    fi
}
trap 'test_finally EXIT' 'EXIT'
trap 'test_finally INT' 'INT'

TEST_NUMBER=0

tests() {
    echo "1..$1"
}

skip() {
    local N_SKIPS=$1
    shift 1
    local I=0
    while ((I++ < N_SKIPS)); do
        echo "ok $((++TEST_NUMBER)) # skip $@"
    done
}

skip_all() {
    echo "1..0 # SKIP $@"
    exit
}

pass() {
    echo "ok $((++TEST_NUMBER)) - $@"
}

fail() {
    echo "not ok $((++TEST_NUMBER)) - $@"
}

run_pass() {
    local TEST_KEY=$1
    shift 1
    if ! "$@" 1>$TEST_KEY.out 2>$TEST_KEY.err; then
        fail $TEST_KEY
        return
    fi
    pass $TEST_KEY
}

run_fail() {
    local TEST_KEY=$1
    shift 1
    if "$@" 1>$TEST_KEY.out 2>$TEST_KEY.err; then
        fail $TEST_KEY
        return
    fi
    pass $TEST_KEY
}

file_cmp() {
    local TEST_KEY=$1
    local FILE_ACTUAL=$2
    local FILE_EXPECT=${3:--}
    if diff -u "${FILE_EXPECT}" "${FILE_ACTUAL}" >&2; then
        pass $TEST_KEY
        return
    fi
    fail $TEST_KEY
}

file_cmp_any() {
    local TEST_KEY=$1
    local FILE_ACTUAL=$2
    local FILE_EXPECT=${3:--}
    cat $FILE_EXPECT | \
        csplit --prefix="$TEST_KEY-cmp-any-csplit" -  /^__filesep__$/ {*}
    for SPLIT_FILENAME in $TEST_KEY-cmp-any-csplit*; do
        sed -i "/^__filesep__$/d" $SPLIT_FILENAME
        if cmp -s $SPLIT_FILENAME $FILE_ACTUAL; then
            pass $TEST_KEY
            return
        fi
    done
    for SPLIT_FILENAME in $TEST_KEY-cmp-any-csplit*; do
        diff -u $SPLIT_FILENAME $FILE_ACTUAL >&2
    done
    fail $TEST_KEY
}

file_test() {
    local TEST_KEY=$1
    local FILE=$2
    local OPTION=${3:--e}
    if test $OPTION $FILE; then
        pass $TEST_KEY
    else
        fail $TEST_KEY
    fi
}

file_grep() {
    local TEST_KEY=$1
    local PATTERN=$2
    local FILE=$3
    if grep -q -e "$PATTERN" $FILE; then
        pass $TEST_KEY
        return
    fi
    fail $TEST_KEY
}

file_pcregrep() {
    local TEST_KEY="$1"
    local PATTERN="$2"
    local FILE="$3"
    if _pcregrep "${PATTERN}" "${FILE}"; then
        pass "${TEST_KEY}"
        return
    fi
    fail "${TEST_KEY}"
}

_pcregrep() {
    python2 - "$@" <<'__PYTHON__'
import re
import sys

pattern, filename = sys.argv[1:3]
sys.exit(int(not re.search(pattern, open(filename).read(), re.M | re.S)))
__PYTHON__
}

file_grep_fail() {
    local TEST_KEY=$1
    local PATTERN=$2
    local FILE=$3
    if grep -q -e "$PATTERN" $FILE; then
        fail $TEST_KEY
        return
    fi
    pass $TEST_KEY
}

bad_smtpd_init() {
    local SMTPD_PORT=
    for SMTPD_PORT in 8026 8126 8226 8326 8426 8526 8626 8726 8826 8926; do
        local SMTPD_HOST=localhost:$SMTPD_PORT
        local SMTPD_LOG="$TEST_DIR/badsmtpd.log"
        # Piping the code directly to python does not work, for some reason.
        cat >$TEST_DIR/smtpd.py <<__PYTHON__
import socket, sys
soc = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
soc.bind(('localhost', $SMTPD_PORT))
sys.stdout.write("bound\n")
sys.stdout.flush()
soc.listen(5)
while True:
    client_soc, address = soc.accept()
    # Antisocially cut off the connection!
    client_soc.close()
__PYTHON__
       python2 $TEST_DIR/smtpd.py 1>"$SMTPD_LOG" 2>&1 &
       local SMTPD_PID=$!
       while ! grep -q '^bound$' "$SMTPD_LOG" 2>/dev/null; do
            if ps $SMTPD_PID 1>/dev/null 2>&1; then
                sleep 1
            else
                rm -f "$SMTPD_LOG"
                unset SMTPD_HOST SMTPD_LOG SMTPD_PID
                break
            fi
        done
        if [[ -n ${SMTPD_PID:-} ]]; then
            TEST_SMTPD_HOST=$SMTPD_HOST
            TEST_SMTPD_PID=$SMTPD_PID
            TEST_SMTPD_LOG=$SMTPD_LOG
            break
        fi
    done
}

bad_smtpd_kill() {
    if [[ -n ${TEST_SMTPD_PID:-} ]] && ps $TEST_SMTPD_PID >/dev/null 2>&1; then
        kill $TEST_SMTPD_PID
        wait $TEST_SMTPD_PID 2>/dev/null || true
        unset TEST_SMTPD_HOST TEST_SMTPD_PID
    fi
}

mock_smtpd_init() {
    local SMTPD_PORT=
    for SMTPD_PORT in 8025 8125 8225 8325 8425 8525 8625 8725 8825 8925; do 
        local SMTPD_HOST=localhost:$SMTPD_PORT
        local SMTPD_LOG="$TEST_DIR/smtpd.log"
        python2 -m smtpd -c DebuggingServer -d -n "$SMTPD_HOST" \
            1>"$SMTPD_LOG" 2>&1 &
        local SMTPD_PID=$!
        while ! grep -q 'DebuggingServer started' "$SMTPD_LOG" 2>/dev/null; do
            if ps $SMTPD_PID 1>/dev/null 2>&1; then
                sleep 1
            else
                rm -f "$SMTPD_LOG"
                unset SMTPD_HOST SMTPD_LOG SMTPD_PID
                break
            fi
        done
        if [[ -n ${SMTPD_PID:-} ]]; then
            TEST_SMTPD_HOST=$SMTPD_HOST
            TEST_SMTPD_PID=$SMTPD_PID
            TEST_SMTPD_LOG=$SMTPD_LOG
            break
        fi
    done
}

mock_smtpd_kill() {
    if [[ -n ${TEST_SMTPD_PID:-} ]] && ps $TEST_SMTPD_PID >/dev/null 2>&1; then
        kill $TEST_SMTPD_PID
        wait $TEST_SMTPD_PID 2>/dev/null || true
        unset TEST_SMTPD_HOST TEST_SMTPD_LOG TEST_SMTPD_PID
    fi
}

poll() {
    local TIMEOUT=$(($(date +%s) + 60)) # wait 1 minute
    while (($(date +%s) < TIMEOUT)) && eval "$@"; do
        sleep 1
    done
}

port_is_busy() {
    local PORT="${1}"
    if type -P netcat 1>/dev/null; then
        HOSTNAME="${HOSTNAME:-"$(hostname)"}"
        HOSTNAME="${HOSTNAME:-'localhost'}"
        netcat -z "${HOSTNAME}" "${PORT}"
    else
        netstat -atun | grep -q "127.0.0.1:${PORT}"
    fi
}

TEST_ROSE_WS_PID=
TEST_ROSE_WS_PORT=
TEST_ROSE_WS_URL=
rose_ws_init() {
    local NS="$1"
    local UTIL="$2"
    local PORT="$((${RANDOM} + 10000))"
    while port_is_busy "${PORT}"; do
        PORT="$((${RANDOM} + 10000))"
    done

    TEST_KEY="${TEST_KEY_BASE}-${NS}-${UTIL}"
    "${NS}" "${UTIL}" '-R' 'start' "${PORT}" \
        0<'/dev/null' 1>"${NS}-${UTIL}.out" 2>"${NS}-${UTIL}.err" &
    TEST_ROSE_WS_PID="$!"
    T_INIT="$(date '+%s')"
    while ! port_is_busy "${PORT}" && (($(date '+%s') < ${T_INIT} + 60)); do
        sleep 1
    done
    TEST_ROSE_WS_PORT="${PORT}"
    if port_is_busy "${TEST_ROSE_WS_PORT}"; then
        pass "${TEST_KEY}"
        TEST_ROSE_WS_URL="http://${HOSTNAME}:${TEST_ROSE_WS_PORT}/${NS}-${UTIL}/"
    else
        fail "${TEST_KEY}"
        rose_ws_kill
    fi
}

rose_ws_kill() {
    if [[ -n "${TEST_ROSE_WS_PID}" ]]; then
        kill "${TEST_ROSE_WS_PID}" 2>'/dev/null'
        wait 2>'/dev/null'
    fi
    if [[ -n "${TEST_ROSE_WS_PORT}" ]]; then
        rm -fr "${HOME}/.metomi/"*"-0.0.0.0-${TEST_ROSE_WS_PORT}"* 2>'/dev/null'
    fi
    TEST_ROSE_WS_PID=
    TEST_ROSE_WS_PORT=
    TEST_ROSE_WS_URL=
}

rose_ws_json_greps() {
    local TEST_KEY="$1"
    shift 1
    run_pass "${TEST_KEY}" python2 - "$@" <<'__PYTHON__'
import ast
import json
import sys

data = json.load(open(sys.argv[1]))
for item in sys.argv[2:]:
    keys, value = ast.literal_eval(item)
    datum = data
    try:
        for key in keys:
            if isinstance(key, dict):
                for datum_item in datum:
                    if all([datum_item.get(k) == v for k, v in key.items()]):
                        datum = datum_item
                        break
                else:
                    raise KeyError
            else:
                datum = datum[key]
        if datum != value:
            raise ValueError((item, datum))
    except IndexError:
        raise IndexError(item)
    except KeyError:
        raise KeyError(item)
__PYTHON__
    if [[ -s "${TEST_KEY}.err" ]]; then
        cat "${TEST_KEY}.err" >&2
    fi
}

ROSE_HOME=${ROSE_HOME:-$(cd $(dirname $BASH_SOURCE)/../../.. && pwd)}
PATH=$ROSE_HOME/bin:$PATH

DIFFTOOL=$(rose config '--default=diff -u' t difftool)

TEST_KEY_BASE=$(basename $0 .t)
TEST_SOURCE_DIR=$(cd $(dirname $0) && pwd)
TEST_DIR=$(mktemp -d)
cd $TEST_DIR

set +e
