#!/bin/bash -eu

export LANG=C.UTF-8
export LANGUAGE=en

if command -v goctest >/dev/null; then
    goctest="goctest ./..."
else
    goctest="go test ./..."
fi
COVERMODE=${COVERMODE:-atomic}
COVERAGE_SUFFIX=${GO_BUILD_TAGS:-notags}
COVERAGE_OUT=${COVERAGE_OUT:-.coverage/coverage-$COVERAGE_SUFFIX.cov}
CHANGED_FILES=${CHANGED_FILES:-""}
TIMEOUT=${TIMEOUT:-15}

if [ -z "${GITHUB_WORKFLOW:-}" ]; then
    # when *not* running inside github, ensure we use go-1.18 by default
    export PATH=/usr/lib/go-1.18/bin:"${PATH}"
fi

# add workaround for https://github.com/golang/go/issues/24449
if [ "$(uname -m)" = "s390x" ]; then
    if go version | grep -q go1.10; then
        echo "covermode 'atomic' crashes on s390x with go1.10, reseting "
        echo "to 'set'. see https://github.com/golang/go/issues/24449"
        COVERMODE="set"
    fi
fi

# If GOPATH is set in the shell environment, the path will be reflected
# in $(go env GOPATH). If no shell path was set, Go will default the internal
# GOPATH to $HOME/go. Note that GOPATH may contain a colon delimited set
# of paths, so in order to run any binary from any of the installed GOPATHs
# we must add all the possible bin locations.
GOBINS=$(go env GOPATH | sed 's|:|/bin:|g' | sed 's|.*|\0/bin|g')
export PATH="$PATH:$GOBINS"

short=

STATIC=
UNIT=

case "${1:-all}" in
    all)
        STATIC=1
        UNIT=1
        ;;
    --static)
        STATIC=1
        ;;
    --unit)
        UNIT=1
        ;;
    --short-unit)
        UNIT=1
        short=1
        ;;
    *)
        echo "Wrong flag ${1}. To run a single suite use --static, --unit."
        exit 1
esac

CURRENTTRAP="true"
EXIT_CODE=99

store_exit_code() {
    EXIT_CODE=$?
}

exit_with_exit_code() {
    exit $EXIT_CODE
}

addtrap() {
    CURRENTTRAP="$CURRENTTRAP ; $1"
    # shellcheck disable=SC2064
    trap "store_exit_code; $CURRENTTRAP ; exit_with_exit_code" EXIT
}

goinstall() {
    pkg="$1"
    version="${2:-}"
    # go1.18+ will no longer build/install packages. Here "go install"
    # must be used but it will only fetch remote packages if the @latest
    # (or similar syntax is used). Instead of checking the version we
    # check if the "go install" help mentions this new feature.
    if go help install | grep -q @latest; then
        go install "${pkg}"@"${version:=latest}"
    else
        go get -u "${pkg}"@"${version:=latest}"
    fi
}

endmsg() {
    if [ $EXIT_CODE -eq 0 ]; then
        p="success.txt"
        m="All good, what could possibly go wrong."
    else
        p="failure.txt"
        m="Crushing failure and despair."
    fi
    echo
    if [ -t 1 ] && [ -z "$STATIC" ]; then
        cat "data/$p"
    else
        echo "$m"
    fi
}
addtrap endmsg

missing_interface_spread_test() {
    snap_yaml="tests/lib/snaps/test-snapd-policy-app-consumer/meta/snap.yaml"
    core_snap_yaml="tests/main/interfaces-many-snap-provided/test-snapd-policy-app-provider-core/meta/snap.yaml"
    classic_snap_yaml="tests/main/interfaces-many-snap-provided/test-snapd-policy-app-provider-classic/meta/snap.yaml"
    for iface in $(go run ./tests/lib/list-interfaces.go) ; do
        search="plugs: \\[ $iface \\]"
        case "$iface" in
            bool-file|gpio|pwm|dsp|netlink-driver|hidraw|i2c|iio|serial-port|spi|confdb)
                # skip gadget provided interfaces for now
                continue
                ;;
            dbus|content)
                search="interface: $iface"
                ;;
            autopilot)
                search='plugs: \[ autopilot-introspection \]'
                ;;
        esac

        # check if a standalone test already exists and that it at least
        # connects and disconnects the interface
        dedicated_test=$(find tests/main/ -maxdepth 1 -name "interfaces-$iface")
        if [ -n "$dedicated_test" ]; then
            if grep -q "$search" "$snap_yaml" ; then
                echo "Dedicated test '$dedicated_test' found for '$iface'."
                echo "Please remove '$iface' from '$snap_yaml'."
                exit 1
            fi
            # dedicated test already exists, skip high-level test check below
            continue
        fi

        # check if high-level minimal test exists for interface
        if ! grep -q "$search" "$snap_yaml" ; then
            echo "Missing high-level test for interface '$iface'. Please add to:"
            echo "* $snap_yaml"
            echo "* $core_snap_yaml (if needed)"
            echo "* $classic_snap_yaml (if needed)"
            exit 1
        fi
    done
}


if [ "$STATIC" = 1 ]; then
    ./get-deps.sh

    # Run static tests.
    echo Checking docs
    ./mdlint.py ./*.md ./**/*.md

    # XXX: remove once we can use an action, see workflows/test.yaml for
    #      details why we still use this script
    if [ -n "${GITHUB_PULL_REQUEST_TITLE:-}" ]; then
        echo Checking pull request summary
        ./check-pr-title.py "${GITHUB_PULL_REQUEST_TITLE}"
    else
        echo Skipping pull request summary check: not a pull request
    fi

    # check commit author/committer name for unicode
    ./check-commit-email.py

    if [ -z "${SKIP_GOFMT:-}" ]; then
        echo Checking formatting
        fmt=""
        for dir in $(go list -f '{{.Dir}}' ./... | grep -v '/\(c-\)\?vendor/' ); do
            # skip vendor packages
            s="$(${GOFMT:-gofmt} -s -d "$dir" || true)"
            if [ -n "$s" ]; then
                fmt="$s\\n$fmt"
            fi
        done
        if [ -n "$fmt" ]; then
            echo "Formatting wrong in following files:"
            # shellcheck disable=SC2001
            echo "$fmt" | sed -e 's/\\n/\n/g'
            exit 1
        fi
    fi

    # go vet
    echo Running vet
    go list ./... | grep -v '/vendor/' | xargs go vet

    echo 'Checking for usages of http.Status*'
    got=""
    for dir in $(go list -f '{{.Dir}}' ./... | grep -v '/vendor/' ); do
        s="$(grep -nP 'http\.Status(?!Text)' "$dir"/*.go || true)"
        if [ -n "$s" ]; then
            got="$s\\n$got"
        fi
    done

    if [ -n "$got" ]; then
        echo 'Usages of http.Status*, we prefer the numeric values directly:'
        echo "$got"
        exit 1
    fi

    echo "Checking for direct usages of math/rand"
    got=""
    for dir in $(go list -f '{{.Dir}}' ./... | grep -v '/vendor/' ); do
        # shellcheck disable=SC2063
        s="$(grep -nP --exclude '*_test.go' --exclude 'randutil/*.go' math/rand "$dir"/*.go || true)"
        if [ -n "$s" ]; then
            got="$s\\n$got"
        fi
    done

    if [ -n "$got" ]; then
        echo 'Direct usages of math/rand, we prefer randutil:'
        echo "$got"
        exit 1
    fi

    echo "Checking for usages of deprecated io/ioutil"
    got=""
    for dir in $(go list -f '{{.Dir}}' ./... | grep -v '/vendor/' ); do
        # shellcheck disable=SC2063
        s="$(grep -nP io/ioutil "$dir"/*.go || true)"
        if [ -n "$s" ]; then
            got="$s\\n$got"
        fi
    done

    if [ -n "$got" ]; then
        echo 'Found usages of deprecated io/ioutil, please use "io" or "os" equivalents'
        echo "$got"
        exit 1
    fi

    if command -v shellcheck >/dev/null; then
        exclude_tools_path=tests/lib/external/snapd-testing-tools
        echo "Checking shell scripts..."
        if [ -n "$CHANGED_FILES" ]; then
            echo "Checking just the changed bash files"
            echo "Changed files: $CHANGED_FILES"
            # shellcheck disable=SC2086
            INITIAL_FILES="$( file -N $CHANGED_FILES | awk -F": " '$2~/shell.script/{print $1}' )"
        else
            echo "Checking all the bash files"
            INITIAL_FILES="$( ( git ls-files -z 2>/dev/null || find . \( -name .git -o -name vendor -o -name c-vendor \) -prune -o -print0) | xargs -0 file -N | awk -F": " '$2~/shell.script/{print $1}' )"
        fi

        echo "Filtering files"
        FILTERED_FILES=
        for file in $INITIAL_FILES; do
            if ! echo "$file" | grep -q "$exclude_tools_path"; then
                FILTERED_FILES="$FILTERED_FILES $file"
            fi
        done
        if [ -n "$FILTERED_FILES" ]; then
            echo "Running shellcheck for files: $FILTERED_FILES"
            # shellcheck disable=SC2086
            shellcheck -x $FILTERED_FILES
        else
            echo "Skipping shellcheck, no files to check"
        fi

        regexp='GOPATH(?!%%:\*)(?!:)[^= ]*/'
        if  grep -qPr                   --exclude HACKING.md --exclude 'Makefile.*' --exclude-dir .git --exclude-dir vendor "$regexp"; then
            echo "Using GOPATH as if it were a single entry and not a list:"
            grep -PHrn -C1 --color=auto --exclude HACKING.md --exclude 'Makefile.*' --exclude-dir .git --exclude-dir vendor "$regexp"
            echo "Use GOHOME, or {GOPATH%%:*}, instead."
            exit 1
        fi
        unset regexp

        # also run spread-shellcheck
        FILTERED_FILES="spread.yaml"
        if [ -n "$CHANGED_FILES" ]; then
            # shellcheck disable=SC2086
            for changed_file in $CHANGED_FILES; do
                if [[ $changed_file == */task.yaml ]]; then
                    FILTERED_FILES="$FILTERED_FILES $(pwd)/$changed_file"
                fi
            done
        else
            FILTERED_FILES="$FILTERED_FILES tests"
        fi
        # XXX: exclude core20-preseed test as its environment block confuses shellcheck, and it's not possible to disable shellcheck there.
        # shellcheck disable=SC2086
        ./tests/lib/external/snapd-testing-tools/utils/spread-shellcheck $FILTERED_FILES --exclude "$exclude_tools_path" --exclude "tests/nested/manual/core20-preseed"
    fi

    if [ -z "${SKIP_MISSPELL:-}" ]; then
        echo "Checking spelling errors"
        if ! command -v misspell >/dev/null; then
            goinstall github.com/client9/misspell/cmd/misspell
        fi
        # FIXME: auter is only misspelled in the changelog so we should fix there
        # PROCES is used in the seccomp tests (PRIO_PROCES{,S,SS})
        # exportfs is used in the nfs-support test
        # becuase because of misspell in upstream steam rules (PR#12657)
        MISSPELL_IGNORE="auther,PROCES,PROCESSS,proces,processs,exportfs,becuase"
        git ls-files -z -- . ':!:./po' ':!:./vendor' ':!:./c-vendor' ':!:./cmd/libsnap-confine-private/bpf/vendor' ':!:./build-aux/snap/local/apparmor'|
            xargs -0 misspell -error -i "$MISSPELL_IGNORE"
    fi

    if [ -z "${SKIP_INEFFASSIGN:-}" ]; then
        if dpkg --compare-versions "$(go version | awk '$3 ~ /^go[0-9]/ {print substr($3, 3)}')" ge 1.12; then
            echo "Checking for ineffective assignments"
            if ! command -v ineffassign >/dev/null; then
                goinstall github.com/gordonklaus/ineffassign
            fi
            # ineffassign knows about ignoring vendor/ \o/
            ineffassign ./...
        fi
    fi

    echo "Checking for naked returns"
    if ! command -v nakedret >/dev/null; then
        goinstall github.com/alexkohler/nakedret v1.0.1
    fi
    got=$(go list ./... | grep -v '/osutil/udev/' | grep -v '/vendor/' | xargs nakedret 2>&1)
    if [ -n "$got" ]; then
        echo "$got"
        if [ -z "${SKIP_NAKEDRET:-}" ]; then
            exit 1
        else
            echo "Ignoring nakedret errors as requested"
        fi
    fi

    echo "Checking all interfaces have a spread test"
    missing_interface_spread_test

    echo "Checking for incorrect multiline strings in spread tests"
    badmultiline=$(find tests -name 'task.yaml' -print0 -o -name 'spread.yaml' -print0 | \
                       xargs -0 grep -R -n -E '(restore*|prepare*|execute|debug):\s*$' || true)
    if [ -n "$badmultiline" ]; then
        echo "Incorrect multiline strings at the following locations:"
        echo "$badmultiline"
        exit 1
    fi

    echo "Checking for potentially incorrect use of MATCH -v"
    badMATCH=$(find tests -name 'task.yaml' -print0 -o -name 'spread.yaml' -print0 | \
                       xargs -0 grep -R -n -E 'MATCH +-v' || true)
    if [ -n "$badMATCH" ]; then
        echo "Potentially incorrect use of MATCH -v at the following locations:"
        echo "$badMATCH"
        exit 1
    fi

    # FIXME: re-add staticcheck with a matching version for the used go-version

    if [ -z "${SKIP_TESTS_FORMAT_CHECK:-}" ] || [ "$SKIP_TESTS_FORMAT_CHECK" = 0 ]; then
        CHANGED_TESTS=""
        FILTERED_TESTS=""
        EXCLUDE_PATH=tests/lib/external/snapd-testing-tools
        if [ -n "$CHANGED_FILES" ]; then
            # shellcheck disable=SC2086
            for changed_file in $CHANGED_FILES; do
                if [[ $changed_file == */task.yaml ]]; then
                    CHANGED_TESTS="$CHANGED_TESTS $changed_file"
                fi
            done
        fi
        # shellcheck disable=SC2086
        for test in $CHANGED_TESTS; do
            if ! echo "$test" | grep -q "$EXCLUDE_PATH"; then
                if [ -z "$FILTERED_TESTS" ]; then
                    FILTERED_TESTS="$test"
                else
                    FILTERED_TESTS="$FILTERED_TESTS $test"
                fi
            fi
        done

        echo "Checking tests formatting"
        if [ -n "$FILTERED_TESTS" ]; then
            # shellcheck disable=SC2086
            ./tests/lib/external/snapd-testing-tools/utils/check-test-format --tests $FILTERED_TESTS
        fi
    fi

    echo "Checking for usages of !=, == or Equals with ErrNoState"
    if got=$(grep -n -R -E "(\!=|==|Equals,) (state\.)?ErrNoState" --include=*.go) ; then
      echo "Don't use equality checks with ErrNoState, use errors.Is() instead"
      echo "$got"
      exit 1
    fi

    if [ -z "${SKIP_GOLANGCI_LINT:-}" ]; then

        echo "Checking installation of golangci-lint"
        gcil="$(command -v golangci-lint || true)"
        if [ -z "$gcil" ]; then
            echo "ERROR: Cannot find golangci-lint. You need to first install the golangci-lint"
            exit 1
        fi

        if echo "$gcil" | grep -q '/snap/bin/' ; then
            # golangci-lint was installed from the snap
            if snap refresh --list | grep -q golangci-lint; then
                echo "WARNING: your golangci-lint snap is out of date. The CI will install a fresh version, which may differ from yours."
            fi
            if ! snap list golangci-lint | grep -q latest; then
                echo "WARNING: your golangci-lint snap is not installed from the latest/* channel."
            fi
        fi

        # Check whether golangci-lint was built with go version >= the installed go version
        gcil_go_ver=$(golangci-lint --version | grep -o 'go[0-9]*\.[0-9]*\.[0-9]*' | cut -c 3-)
        go_ver=$(go version | grep -o 'go[0-9]*\.[0-9]*\.[0-9]*' | cut -c 3-)
        if [ "$(printf '%s\n' "$go_ver" "$gcil_go_ver" | sort -V | head -n1)" != "$go_ver" ]; then
           echo "WARNING: Your go version ($go_ver) is greater than the version of go that golangci-lint was built with ($gcil_go_ver)."
        fi

        # don't run with --new-from-rev as the diff might not be enough to tell
        # the change introduces problems (e.g., removing the last call to a function)
        golangci-lint --path-prefix= run
    fi
fi

if [ "$UNIT" = 1 ]; then
    ./get-deps.sh

    echo "Show go version"
    command -v go
    go version

    tags=
    race=
    timeout="${TIMEOUT}m"
    if [ -n "${GO_BUILD_TAGS:-}" ]; then
        echo "Using build tags: $GO_BUILD_TAGS"
        tags="-tags $GO_BUILD_TAGS"
    fi
    if [ -n "${GO_TEST_RACE:-}" ]; then
        echo "Using go test -race"
        race="-race"
        timeout="$((TIMEOUT*2))m"
    fi

    echo Building
    # shellcheck disable=SC2086
    go build -v $tags $race github.com/snapcore/snapd/...

    # tests
    echo Running tests from "$PWD"
    if [ "$short" = 1 ]; then
            echo "Running without test coverage"
            # shellcheck disable=SC2046,SC2086
            GOTRACEBACK=1 $goctest $tags $race -short -timeout $timeout
    else
        coverage=""
        if [ -z "${SKIP_COVERAGE:-}" ]; then
            coverage="-coverprofile=$COVERAGE_OUT -covermode=$COVERMODE"
            # Prepare the coverage output profile.
            mkdir -p "$(dirname "$COVERAGE_OUT")"
            echo "Using coverage params: $coverage"
        else
            echo "Skipping test coverage"
        fi

        # shellcheck disable=SC2046,SC2086
        GOTRACEBACK=1 $goctest $tags $race -timeout $timeout $coverage
    fi

    # python unit test for mountinfo.query and version-compare
    command -v python2 && python2 ./tests/lib/tools/mountinfo.query --run-unit-tests
    command -v python3 && python3 ./tests/lib/tools/mountinfo.query --run-unit-tests
    command -v python2 && python2 ./tests/lib/tools/version-compare --run-unit-tests
    command -v python3 && python3 ./tests/lib/tools/version-compare --run-unit-tests
    command -v pytest-3 && PYTHONDONTWRITEBYTECODE=1 pytest-3 ./release-tools
fi

UNCLEAN="$(git status -s|grep '^??')" || true
SKIP_UNCLEAN=${SKIP_UNCLEAN=}
if [ -n "$UNCLEAN" ] && [ -z "$SKIP_UNCLEAN" ]; then
    cat <<EOF

There are files left in the git tree after the tests:

$UNCLEAN
EOF
    exit 1
fi

if [ -n "${SKIP_DIRTY_CHECK:-}" ]; then
    exit 0
fi

# XXX: re-enable after vendor/vendor.json is removed
# if git describe --always --dirty | grep -q dirty; then
#     echo "Build tree is dirty"
#     git diff
#     exit 1
# fi
