#!/bin/bash

#----------------------------------------------------------------------
# Test suite for InterIMAP
# Copyright © 2019 Guilhem Moulin <guilhem@fripost.org>
#
# This program 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.
#
# This program 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 this program.  If not, see <http://www.gnu.org/licenses/>.
#----------------------------------------------------------------------

set -ue
PATH=/usr/bin:/bin
export PATH

if [ $# -ne 1 ]; then
    printf "Usage: %s TESTNAME\\n" "$0" >&2
    exit 1
fi

TEST="${1%/}"
TEST="${TEST##*/}"
NAME="${TEST#[0-9]*-}"
TESTDIR="$(dirname -- "$0")/$TEST"
if [ ! -d "$TESTDIR" ]; then
    printf "ERROR: Not a directory: %s\\n" "$TESTDIR" >&2
    exit 1
fi

ROOTDIR="$(mktemp --tmpdir="${TMPDIR:-/dev/shm}" --directory "$NAME.XXXXXXXXXX")"
trap 'rm -rf -- "$ROOTDIR"' EXIT INT TERM

STDOUT="$ROOTDIR/stdout"
STDERR="$ROOTDIR/stderr"
TMPDIR="$ROOTDIR/tmp"
mkdir -- "$TMPDIR" "$ROOTDIR/home"

# Set environment for the given user
environ_set() {
    local user="$1" home
    eval home="\$HOME_$user"
    ENVIRON=(
        PATH="$PATH"
        USER="$user"
        HOME="$home"
        XDG_CONFIG_HOME="$home/.config"
        XDG_DATA_HOME="$home/.local/share"
    )
}

# Prepare the test harness
prepare() {
    declare -a ENVIRON=()
    local src cfg target u home
    # copy dovecot config
    for src in "$TESTDIR/local.conf" "$TESTDIR"/remote*.conf; do
        [ -r "$src" ] || continue
        u="${src#"$TESTDIR/"}"
        u="${u%.conf}"
        home="$ROOTDIR/home/$u"
        export "HOME_$u"="$home"
        mkdir -pm0755 -- "$home/.local/bin"
        mkdir -pm0700 -- "$home/.config/dovecot"
        cat >"$home/.config/dovecot/config" <<-EOF
			log_path = /dev/null
			mail_home = $ROOTDIR/home/%u
			ssl = no
		EOF
        cat >>"$home/.config/dovecot/config" <"$src"
        environ_set "$u"
        cat >"$home/.local/bin/doveadm" <<-EOF
			#!/bin/sh
			exec env -i ${ENVIRON[@]@Q} \\
			    doveadm -c ${home@Q}/.config/dovecot/config "\$@"
		EOF
        chmod +x -- "$home/.local/bin/doveadm"
    done

    # copy interimap config
    mkdir -pm0700 -- "$HOME_local/.local/share/interimap"
    mkdir -pm0700 -- "$HOME_local/.config/interimap"
    for cfg in "$TESTDIR"/remote*.conf; do
        cfg="${cfg#"$TESTDIR/remote"}"
        cfg="${cfg%.conf}"
        u="remote$cfg"
        eval home="\$HOME_$u"
        if [ -f "$TESTDIR/interimap.conf" ]; then
            cat <"$TESTDIR/interimap.conf" >>"$HOME_local/.config/interimap/config$cfg"
        fi
        cat >>"$HOME_local/.config/interimap/config$cfg" <<-EOF
			database = $u.db
 
			[local]
			type = tunnel
			command = exec ${HOME_local@Q}/.local/bin/doveadm exec imap
			null-stderr = NO

			[remote]
			type = tunnel
			command = exec ${home@Q}/.local/bin/doveadm exec imap
			null-stderr = NO
		EOF
    done
}
prepare

# Wrappers for interimap(1) and doveadm(1)
interimap() {
    declare -a ENVIRON=()
    environ_set "local"
    env -i "${ENVIRON[@]}" perl -I./lib -T ./interimap "$@"
}
doveadm() {
    if [ $# -le 2 ] || [ "$1" != "-u" ]; then
        echo "Usage: doveadm -u USER ..." >&2
        exit 1
    fi
    local u="$2" home
    eval home="\$HOME_$u"
    shift 2
    "$home/.local/bin/doveadm" "$@"
}

# Sample (random) message
sample_message() {
    cat <<-EOF
		From: <sender@example.net>
		To: <recipient@example.net>
		Date: $(date -R)
		Message-ID: <$(< /proc/sys/kernel/random/uuid)@example.net>

	EOF
    local len="$(shuf -i1-4096 -n1)"
    xxd -ps -c30 -l"$len" /dev/urandom # 3 to 8329 bytes
}

# Wrapper for dovecot-lda(1)
deliver() {
    local -a argv
    while [ $# -gt 0 ] && [ "$1" != "--" ]; do
        argv+=( "$1" )
        shift
    done
    if [ $# -gt 0 ] && [ "$1" = "--" ]; then
        shift
    fi
    doveadm "${argv[@]}" exec dovecot-lda -e "$@"
}

# Dump test results 
dump_test_result() {
    local below=">>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>"
    local above="<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<"
    local src u home
    declare -a ENVIRON=()
    for src in "$TESTDIR/local.conf" "$TESTDIR"/remote*.conf; do
        u="${src#"$TESTDIR/"}"
        u="${u%.conf}"
        environ_set "$u"
        eval home="\$HOME_$u"
        printf "%s dovecot configuration:\\n%s\\n" "$u" "$below"
        env -i "${ENVIRON[@]}" doveconf -c "$home/.config/dovecot/config" -n
        printf "%s\\n\\n" "$above"
    done

    printf "(local) interimap configuration:\\n%s\\n" "$below"
    cat <"$HOME_local/.config/interimap/config"
    printf "%s\\n\\n" "$above"

    printf "standard output was:\\n%s\\n" "$below"
    cat <"$STDOUT"
    printf "%s\\n\\n" "$above"

    printf "standard error was:\\n%s\\n" "$below"
    cat <"$STDERR"
    printf "%s\\n\\n" "$above"
}

# Check mailbox consistency between the local/remote server and interimap's database
check_mailbox_status() {
    local mailbox="$1" lns="inbox" lsep lprefix rns="inbox" rsep rprefix
    lsep="$(doveconf -c "$HOME_local/.config/dovecot/config" -h "namespace/$lns/separator")"
    lprefix="$(doveconf -c "$HOME_local/.config/dovecot/config" -h "namespace/$lns/prefix")"
    rsep="$(doveconf -c "$HOME_remote/.config/dovecot/config" -h "namespace/$lns/separator")"
    rprefix="$(doveconf -c "$HOME_remote/.config/dovecot/config" -h "namespace/$lns/prefix")"

    local blob="x'$(printf "%s" "$mailbox" | tr "$lsep" "\\0" | xxd -c256 -ps)'"
    local rmailbox="$(printf "%s" "$mailbox" | tr "$lsep" "$rsep")"
    check_mailbox_status2 "$blob" "$lprefix$mailbox" "remote" "$rprefix$rmailbox"
}
check_mailbox_status2() {
    local blob="$1" lmailbox="$2" u="$3" rmailbox="$4"
    local lUIDVALIDITY lUIDNEXT lHIGHESTMODSEQ rUIDVALIDITY rUIDNEXT rHIGHESTMODSEQ
    read lUIDVALIDITY lUIDNEXT lHIGHESTMODSEQ rUIDVALIDITY rUIDNEXT rHIGHESTMODSEQ < <(
        sqlite3 "$XDG_DATA_HOME/interimap/$u.db" <<-EOF
			.mode csv
			.separator " " "\\n"
			SELECT l.UIDVALIDITY, l.UIDNEXT, l.HIGHESTMODSEQ, r.UIDVALIDITY, r.UIDNEXT, r.HIGHESTMODSEQ
			FROM mailboxes m JOIN local l ON m.idx = l.idx JOIN remote r ON m.idx = r.idx
			WHERE mailbox = $blob
		EOF
    )
    lHIGHESTMODSEQ="$(printf "%llu" "$lHIGHESTMODSEQ")"
    rHIGHESTMODSEQ="$(printf "%llu" "$rHIGHESTMODSEQ")"
    local MESSAGES
    read MESSAGES < <( sqlite3 "$XDG_DATA_HOME/interimap/$u.db" <<-EOF
			.mode csv
			.separator " " "\\n"
            SELECT COUNT(*)
			FROM mailboxes a JOIN mapping b ON a.idx = b.idx
			WHERE mailbox = $blob
		EOF
    )
    check_mailbox_status_values "local" "$lmailbox"  $lUIDVALIDITY $lUIDNEXT $lHIGHESTMODSEQ $MESSAGES
    check_mailbox_status_values "$u"    "$rmailbox" $rUIDVALIDITY $rUIDNEXT $rHIGHESTMODSEQ $MESSAGES

    local a b
    a="$(doveadm -u "local" -f "flow" mailbox status "messages unseen vsize" -- "$lmailbox" | \
            sed -nr '/.*\s+(\w+=[0-9]+\s+\w+=[0-9]+\s+\w+=[0-9]+)$/ {s//\1/p;q}')"
    b="$(doveadm -u "$u" -f "flow" mailbox status "messages unseen vsize" -- "$rmailbox" | \
            sed -nr '/.*\s+(\w+=[0-9]+\s+\w+=[0-9]+\s+\w+=[0-9]+)$/ {s//\1/p;q}')"
    if [ "$a" != "$b" ]; then
        echo "Mailbox $lmailbox status differs: \"$a\" != \"$b\"" >&2
        exit 1
    fi
}
check_mailbox_status_values() {
    local user="$1" mailbox="$2" UIDVALIDITY="$3" UIDNEXT="$4" HIGHESTMODSEQ="$5" MESSAGES="$6" x xs v k
    xs="$(doveadm -u "$user" -f "flow" mailbox status "uidvalidity uidnext highestmodseq messages" -- "$mailbox" | \
            sed -nr '/.*\s+(\w+=[0-9]+\s+\w+=[0-9]+\s+\w+=[0-9]+\s+\w+=[0-9]+)$/ {s//\1/p;q}')"
    [ -n "$xs" ] || exit 1
    for x in $xs; do
        k="${x%%=*}"
        case "${k^^[a-z]}" in
            UIDVALIDITY) v="$UIDVALIDITY";;
            UIDNEXT) v="$UIDNEXT";;
            HIGHESTMODSEQ) v="$HIGHESTMODSEQ";;
            MESSAGES) v="$MESSAGES";;
            *) echo "Uh? $x" >&2; exit 1
        esac
        if [ "${x#*=}" != "$v" ]; then
            echo "$user($mailbox): ${k^^[a-z]} doesn't match! ${x#*=} != $v" >&2
            exit 1
        fi
    done
}
check_mailboxes_status() {
    local mailbox
    for mailbox in "$@"; do
        check_mailbox_status "$mailbox"
    done
}

# Check mailbox list constency between the local and remote servers
check_mailbox_list() {
    local m i lns="inbox" lsep lprefix rns="inbox" rsep rprefix sub=
    lsep="$(doveconf -c "$HOME_local/.config/dovecot/config" -h "namespace/$lns/separator")"
    lprefix="$(doveconf -c "$HOME_local/.config/dovecot/config" -h "namespace/$lns/prefix")"
    rsep="$(doveconf -c "$HOME_remote/.config/dovecot/config" -h "namespace/$lns/separator")"
    rprefix="$(doveconf -c "$HOME_remote/.config/dovecot/config" -h "namespace/$lns/prefix")"
    if [ $# -gt 0 ] && [ "$1" = "-s" ]; then
        sub="-s"
        shift
    fi

    declare -a lmailboxes=() rmailboxes=()
    if [ $# -eq 0 ]; then
        lmailboxes=( "${lprefix}*" )
        rmailboxes=( "${rprefix}*" )
    else
        for m in "$@"; do
            lmailboxes+=( "$lprefix$m" )
            rmailboxes+=( "$rprefix${m//"$lsep"/"$rsep"}" )
        done
    fi

    mapfile -t lmailboxes < <( doveadm -u "local" mailbox list $sub -- "${lmailboxes[@]}" )
    for ((i = 0; i < ${#lmailboxes[@]}; i++)); do
        lmailboxes[i]="${lmailboxes[i]#"$lprefix"}"
    done

    mapfile -t rmailboxes < <( doveadm -u "remote" mailbox list $sub -- "${rmailboxes[@]}" )
    for ((i = 0; i < ${#rmailboxes[@]}; i++)); do
        rmailboxes[i]="${rmailboxes[i]#"$rprefix"}"
        rmailboxes[i]="${rmailboxes[i]//"$rsep"/"$lsep"}"
    done

    local IFS=$'\n'
    diff -u --label="local/mailboxes" --label="remote/mailboxes" \
        <( printf "%s" "${lmailboxes[*]}" | sort ) <( printf "%s" "${rmailboxes[*]}" | sort )
}

# Wrappers for grep(1) and `grep -C`
xgrep() {
    if ! grep -q "$@"; then
        printf "\`grep %s\` failed on line %d\\n" "${*@Q}" ${BASH_LINENO[0]} >&2
        exit 1
    fi
}
xcgrep() {
    local m="$1" n
    shift
    if ! n="$(grep -c "$@")" || [ $m -ne $n ]; then
        printf "\`grep -c %s\` failed on line %d: %d != %d\\n" "${*@Q}" ${BASH_LINENO[0]} "$m" "$n" >&2
        exit 1
    fi
}

# Run test in a sub-shell
declare -a ENVIRON=()
environ_set "local"
export TMPDIR TESTDIR STDOUT STDERR "${ENVIRON[@]}"
export -f environ_set doveadm interimap sample_message deliver
export -f check_mailbox_status check_mailbox_status_values check_mailbox_status2
export -f check_mailboxes_status check_mailbox_list xgrep xcgrep
printf "%s..." "$TEST"
if ! bash -ue "$TESTDIR/run" >"$STDOUT" 2>"$STDERR"; then
    echo " FAILED"
    dump_test_result
    exit 1
else
    echo " OK"
    if grep -Paq "\\x00" -- "$STDOUT" "$STDERR"; then
        printf "\\tWarn: binary output (outstanding \\0)!\\n"
    fi
    exit 0
fi