#!/usr/bin/env bash
set -e -o pipefail
trap 'echo 1>&2 "FAILED ($(elapsed)s)"; exit 1' 0

die() { echo 1>&2 -e "FAILURE:" "$@"; exit 1; }

errorlist=()
adderror() {
    echo 2>&1 '***** ERROR:' "$@"
    errorlist+=("$*");
}

elapsed_start=$(date +%s)
elapsed() { echo $(( $(date +%s) - $elapsed_start )); }

docker_unavailable() {
    die 'Cannot run $docker due to setup_docker() failure (see above).'
}

setup_docker() {
    declare -g docker=docker
    if ! $docker --version >/dev/null 2>&1; then
        echo 1>&2 "WARNING: Cannot run '$docker' command. Check path?"
        docker=docker_unavailable
    elif ! $docker info >/dev/null 2>&1; then
        docker='sudo docker'
        if ! sudo -v 2>/dev/null; then
            echo 1>&2 "WARNING: Cannot sudo to run '$docker'; start proxy?"
            docker=docker_unavailable
        fi
    fi
}

header() {
    echo '┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'
    [[ -z "$@" ]] || echo "┃   " "$@"
}

test_typecheck() {
    echo '───── Type check'
    local args=()
    $verbose && args+=('-v')
    mypy "${args[@]}"   # default args in pyproject.toml.
}

test_unittest() {
    echo '───── Unit tests'
    pytest #"$@"        # default args in pyproject.toml.
}

test_dryrun() {
    echo '───── Dry run tests'

    local contname=$RANDOM$RANDOM-delete-me     # name that must not exist
    $docker container inspect "$contname" >/dev/null \
        && die "Container $contname unexpectedly exists; remove it?"

    #   * We do not use -q here because we want to test that stdout/stderr
    #     are properly interleaved.
    #   * As well as --dry-run, this also confirms that various parameters
    #     of the `docker` commands, particularly `docker run`, are correct.
    output=$(2>&1 dent --dry-run -R -B drytest:xx \
                           --run-opt=-P -r=--cpu-count=1 \
                           $contname true) \
        || die "exitcode=$? output='$output'"
    expected=$(echo '
        .* -----.Removing.image
        .* docker.rmi.-f
        .* ---.Building.image.
        .* docker.build.--no-cache
        .* ---.Creating.new.container
        .* docker.run.--name=[[:digit:]]*-delete-me
        .*    --rm=false.*--detach=true.*--tty=false
        .*    -P.*--cpu-count=1
        .*   /bin/sleep.2147483647
        ' | tr -d '\n ' )
    [[ $output =~ $expected ]] || die "dryrun bad output:\n$output"
    echo 'dryrun ok'
    $verbose && {
        echo '──── Dryrun Output ────'
        echo "$output"
        echo '───────────────────────'
    }
    return 0    # Avoid error if $verbose is false.
}

test_nonbuild() {
    #   We are probably ok using a single image name for multiple
    #   (even simultaneous) test runs because the image never changes.
    etest_image=dent/test/nonbuild
    etest_container=dent-test-nonbuild.$$

    header "Non-build tests"

    echo '≡≡≡≡≡ Setup'
    echo "Test image: $etest_image"
    echo "Test container: $etest_container"
    etest_image_sha=$(
    echo '
        FROM alpine:latest
        CMD ["/bin/sleep", "10"]
    ' | $docker build -q -t $etest_image -)
    echo "Built test image as $etest_image_sha"

    echo '≡≡≡≡≡ Pre-existing container tests'
    #   The test container made from this image is run with --rm so it will
    #   automatically clean itself up shortly, regardless of test status.
    echo -n "docker container rm -f $etest_container: "
    $docker container rm -f $etest_container || true
    echo -n "docker run --name $etest_container $etest_image: "
    $docker run --rm --detach --name $etest_container $etest_image

    echo '===== Exec in running container (stdin is terminal if Test is)'
    output=$(dent -q $etest_container /bin/echo -n etest 0100 </dev/tty) \
        || die "exitcode=$? output='$output'"
    [[ $output = 'etest 0100' ]] || die "bad output='$output'"
    echo "ok output='$output'"

    echo '===== Exec in running container (stdin not terminal)'
    output=$(dent -q $etest_container /bin/echo -n etest 0110 </dev/null) \
        || die "exitcode=$? output='$output'"
    [[ $output = 'etest 0110' ]] || die "bad output='$output'"
    echo "ok output='$output'"

    echo '===== Starts stopped container before exec'
    echo -n "docker stop $etest_container: "
    $docker stop -t 0 $etest_container  # Removes itself when stopped
    echo -n "docker create $etest_container: "
    $docker create --rm --name $etest_container $etest_image
    output=$(dent -q $etest_container /bin/echo -n etest 0120) \
        || die "exitcode=$? output='$output'"
    [[ $output = 'etest 0120' ]] || die "bad output='$output'"
    echo "ok output='$output'"

    $keep_images || {
        echo -n "docker image rm -f $etest_image: "
        $docker image rm -f "$etest_image" || true
    }

    echo '≡≡≡≡≡ Container creation from missing pre-existing image '
    #   We need to confirm that dent tries to download the image, rather than
    #   trying to build it locally. This means the image must not already
    #   be on the existing system, but we can't delete local images because
    #   they might be in use by other images or containers. So the best option
    #   is to use an image that doesn't exist at all, and simply confirm the
    #   attempt to download and its failure.
    #
    #   XXX Unfortunately this test actually tries to download the image
    #   (in order to fail doing it), and so it's slow. There seems to be no
    #   way to tell `docker run` not to pull the image but just fail
    #   immediately.
    badname='k7k22zhtj6bv:nonexistimage'
    echo "docker container rm -f $etest_container"
    $docker container rm -f $etest_container >/dev/null
    echo "dent -q -i $badname $etest_container /bin/echo -n etest 0220"
    exitcode=0; output=$(
        dent -q -i "$badname" $etest_container /bin/echo -n etest 0220
        ) || exitcode=$?
    [[ $exitcode -eq 1 ]] || die "exitcode=$? ≠ 1; output='$output'"
    [[ $output = '' ]] || die "bad output='$output'"
    echo "ok output='$output'"

    #   XXX This assumes the presence of `alpine:latest`, which needs to
    #   be maually downloaded to make the test pass. We should probably
    #   be creating our own local (and tiny) image to use instead.
    echo '≡≡≡≡≡ Container creation from present pre-existing image'
    echo "docker container rm -f $etest_container"
    $docker container rm -f $etest_container >/dev/null
    echo "dent -q -i alpine:latest $etest_container /bin/echo -n etest 0200"
    exitcode=0; output=$(
        dent -q -i alpine:latest $etest_container /bin/echo -n etest 0200
        ) || exitcode=$?

    echo "docker container rm -f $etest_container"
    $docker container rm -f $etest_container >/dev/null 2>&1 || true

    [[ $exitcode -eq 0 ]] || die "exitcode=$? output='$output'"
    [[ $output = 'etest 0200' ]] || die "bad output='$output'"
    echo "ok output='$output'"
}

test_build() {
    #   Unless arguments are passed in to test specific base images, all
    #   base images known to dent and viable on this platform are tested.
    header 'Builds from base images'

    #   Certain very old images don't work on very new kernels.
    kern_release=$(uname -r)
    skip_images=()
    if [[ $kern_release > 4.18 ]]; then
        #   On ≤ centos:6, bash core dumps on 4.19, though works on 4.4.
        skip_images+=(centos:5 centos:6)
        echo "Kernel $kern_release: skipping images ${skip_images[@]}"
    fi

    [[ ${#baseimages} = 0 ]] && baseimages=($(dent -L))
    for baseimage in "${baseimages[@]}"; do
        for skip in "${skip_images[@]}"; do
            [[ $baseimage = $skip ]] && {
                echo "SKIP: $baseimage broken on $kern_release"
                continue 2
            }
        done

        tag=dent-test-$$
        tag=dent-test       # DEBUG
        image="dent/${baseimage/:/.}:$tag"  # duplicates code in dent
        container="dent-test-${baseimage/:/.}"
        header "$baseimage"
        echo "    Image: $image"
        echo "Container: $container"

        #   Also removes any containers based on this image, running or not.
        echo -n "docker image rm -f $image: "
        $docker image rm -f "$image" || true

        if ! dent $force_rebuild -B $baseimage -t $tag $container true
        then
            adderror $baseimage
        else
            #   Tests on container now that we know it comes up.

            testvar() {
                VAR="$1"; shift
                VALUE="$1"; shift
                if ! dent $container bash -c "echo $VAR=\$$VAR" \
                     | grep -q "$VAR=$VALUE"
                then
                    adderror "$baseimage: expected $VAR='$VALUE'"
                    dent 1>&2 $container bash -c "echo $VAR=\$$VAR"
                fi
            }
            #   Confirm that we're ensuring $USER and $LOGNAME are set (even
            #   for non-login shells: no `-l` here!) because this is supposed
            #   to be emulating a user environment, even though login(1) or
            #   similar is not being run.
            testvar USER "$USER"
            testvar LOGNAME "$LOGNAME"
            testvar HOST_HOSTNAME "$HOSTNAME"

            #   Confirm that user can use sudo.
            dent $container sudo -v -S </dev/null \
                || adderror "$baseimage: user cannot sudo"
        fi

        echo -n "docker container rm -f $container: "
        $docker container rm -f "$container" || true
        $keep_images || {
            echo -n "docker image rm -f $image: "
            $docker image rm -f "$image" || true
        }
    done

    errcount=${#errorlist[@]}
    if [[ $errcount -gt 0 ]]; then
        echo "===== ${errcount} Errors:"
        for e in "${errorlist[@]}"; do echo "  $e"; done
        exit 1
    fi
}

all_parts=(typecheck unittest dryrun nonbuild build)

usage() {
    [[ "$@" ]] && echo 1>&2 "$@"
    sed -e 's/^        //' <<____ 1>&2
        Usage: $0 [OPTS …] [-- [PYTEST-ARGS]]
        OPTS (see doc/DEVEL.md for more details):
            -t, --test PART     run only this part (may be repeated);
                                parts: ${all_parts[@]}
            -B IMGNAME
            --keep-images
            --no-force-rebuild
____
    exit 2
}

####################################################################
#   Main

base=$(command cd "$(dirname "$0")" && pwd -P)
command cd "$base"

verbose=false
small_clean=false
parts=()                    # empty = run all parts
baseimages=()               # base images to test; default `dent -L`
keep_images=false
force_rebuild=--force-rebuild
while true; do case "$1" in
    -h|--help)          usage;;
    -v|--verbose)       shift; verbose=true;;       # does not affect pytest
    -t|--test)          shift; parts+=("$1"); shift;;
    -B)                 shift; baseimages+=("$1"); shift;;
    --keep-images)      shift; keep_images=true;;
    -f|--no-force-rebuild) shift; force_rebuild=;;  # ("fast" mode)
    -c|--small-clean)   shift; small_clean=true;;
    --)                 shift; break;;              # remaining args for pytest
    -*)                 usage "Unknown option '$1'";;
    *)                  break;;
esac; done

$small_clean && {
    #   Even an editable package needs to be re-installed to update certain
    #   things, such as pyproject.toml metadata (e.g. the version number)
    #   and the non-Python scripts installed by setup.cfg.
    rm -rf .build/virtualenv/
}
#   You can select another version of Python by using a .python link, e.g.:
#       rm -rf .build && ln -s $(pythonz locate 3.5.10) .python
. ./pactivate -q
setup_docker
header

[[ ${#parts[@]} -eq 0 ]] && parts=("${all_parts[@]}")
for part in "${parts[@]}"; do
    test_$part "$@"
done

trap '' 0
echo "OK ($(elapsed)s)"
