#!/bin/bash -e
set -o pipefail

# Helper script to develop/debug mini-buildd.
#
# Quickstart: Enter your development system (preferably sid). sudo should be configured.
#  $ ./devel prepare-system
#  $ ./devel update
#
# Run full testuite w/ default setup (leaves you with a mini-buildd fully set up):
#  $ ./devel updatetestall
#
# Local setup to test remotes (on two systems w/ same network stack like chroots):
#  $ ./devel profile prof_hostname updatetestall  # chroot 0
#  $ ./devel profile prof_localhost updatetestall # chtoot 1

# Profiles
declare -A MBD_PROFILES=(
	[prof_all]="export MBD_SETUP+=' --sources-from-vendor-with-all Debian Ubuntu';"                                                                             # All codenames from dist.py should work
	[prof_ssl]="export MBD_HTTPD_SSL=true;"                                                                                                                     # Encrypted
	[prof_hostname]="export MBD_SETUP+=' --remotes tcp:host=localhost:port=8068';"                                                                              # Hostname: With remote localhost
	[prof_localhost]="export MBD_SETUP+=' --remotes tcp:host=$(hostname -f):port=8066'; export MBD_HOSTNAME=localhost; export MBD_HTTPD_PORT=8068;"             # Localhost: With remote hostname
	[prof_buster]="export MBD_SKIP='mbd_run:115:pyflakes mbd_run:201:sphinx-build mbd_run:615:tidy mbd_run:620:build-ourselves' export MBD_PIPINSTALL=pylint;"  # Hacks to run under buster
)

mbd_profile()  # [<prof0> <prof1> ...] <target>
{
	local prof
	for prof in "${@:1:$#-1}"; do  # eval all args but 1
		eval "${MBD_PROFILES[${prof}]}"
	done
	./devel "${@:$#:1}"  # Run with last arg
}

mbd_supertestall()
{
	./devel profile prof_all          updatetestall | tee ../supertestall_ssl.log 2>&1
	./devel profile prof_all prof_ssl updatetestall | tee ../supertestall.log 2>&1
}

_bf_=$(tput bold) || true
_red_=$(tput setaf 1) || true
_yellow_=$(tput setaf 3) || true
_blue_=$(tput setaf 4) || true
_reset_=$(tput sgr0) || true

log()
{
	local running=""
	[ -z "${MBD_RUNNING}" ] || running="${_bf_}${MBD_RUNNING}${_reset_}: "
	local -A color=(["I"]="${_blue_}" ["W"]="${_yellow_}" ["E"]="${_red_}")
	printf "$(date --rfc-3339=ns) ${color[${1}]}${1}${_reset_}: ${running}${2}\n" "${@:3}" >&2;
}
logI() { log "I" "${@}"; }
logW() { log "W" "${@}"; }
logE() { log "E" "${@}"; }

# don't accidentially run this on your host system.
check_devsys()
{
	if ! (ischroot || systemd-detect-virt --quiet); then
		printf "W: $0\n" >&2
		printf "W: Never use this script in production systems\n" >&2
		printf "W: This may be not a developer system (not chroot, not systemd container)\n" >&2
		read -p"Sure to continue (Ctrl-c to abort)?" DUMMY
	fi
}

# python local install helpers
mbd_pip_uninstall()
{
	local packages=${1:-$(pip3 freeze --user)}
	[ -z "${packages}" ] || pip3 uninstall --yes ${packages}
}

mbd_pip_install()
{
	local package
	for package in "${@}"; do
		pip3 install --upgrade --ignore-installed "${package}"
	done
}

declare -g -a _ON_EXIT=()
on_exit_run()
{
	local line
	for line in "${_ON_EXIT[@]}"; do
		if ${MBD_KEEP}; then
			logW "MBD_KEEP: Ignoring On Exit: %s" "${line}"
		else
			logI "On Exit: %s" "${line}"
			${line}
		fi
	done
	[ -z "${MBD_RUNNING}" ] || logE "Failed: $?"
}
on_exit()
{
	_ON_EXIT+=("${*}")
}
trap "on_exit_run" EXIT
# Avoid some esoteric cases where on_exit is not run (certain emacs compilation buffer kills)
trap "exit 2" INT
trap "exit 3" TERM

_check_prog()
{
	local path
	for path in $(printf "${PATH}" | tr ":" " "); do
		local prog="${path}/${1}"
		if [ -x "${prog}" ]; then
			logI "Found: ${prog}."
			return 0
		fi
	done
	logE "'${1}' not found in path; please install."
	logI "'./devel prepare-system' should install all deps needed."
	exit 1
}

# Extra python as pip user install (f.e., use 'pylint' here to use it instead of the (usually older) Debian version).
: ${MBD_PIPINSTALL:=""}

MBD_PJPATH="$(readlink -f $(dirname $0))"
MBD_PYPATH="${MBD_PJPATH}/src"
export PYTHONPATH="${MBD_PYPATH}"
MBD_SETUP_CFG="${MBD_PJPATH}/setup.cfg"
MBD_LINTIANRC="${MBD_PJPATH}/.lintianrc"
MBD_CODENAME=$(lsb_release --codename --short)
declare -A _MBD_CODEVERSIONS=([buster]=10 [bullseye]=11 [bullseye]=11 [bookworm]=~BOOKWORM [sid]=~SID)
MBD_CODEVERSION="${_MBD_CODEVERSIONS[${MBD_CODENAME}]}"

# Autogenerated by us
MBD_DPUT_CF="${MBD_PJPATH}/devel.dput.cf"
MBD_LAST_CHANGES="${MBD_PJPATH}/devel.last_changes"

#
# Configurable variables
#
MBD_HTTPD_SSL_DESC="Toggle 'https mode' (ssl key is managed via 'mini-buildd-self-signed-certificate'. Should just work after './devel prepare-system')."
: ${MBD_HTTPD_SSL:=false}
MBD_CONFIG+="MBD_HTTPD_SSL "

MBD_HTTPD_PORT_DESC="Custom port -- run a second instance on same network stack (i.e, use '8068' and configure ftp to be at '8069')."
: ${MBD_HTTPD_PORT:=8066}
MBD_CONFIG+="MBD_HTTPD_PORT "

MBD_DEBUG_DESC="Debug options (for 'mini-buildd --debug=xxx')."
: ${MBD_DEBUG:=warnings,exception,http,webapp}
MBD_CONFIG+="MBD_DEBUG "

MBD_HOSTNAME_DESC="Custom hostname -- for a second instance in ssl mode, you must have a different hostname for the certificate."
: ${MBD_HOSTNAME:=$(hostname -f)}
MBD_CONFIG+="MBD_HOSTNAME "

MBD_SETUP_COMMON_DESC="Common args for API call 'setup', needed to complete testsuite. Rather don't touch."
: ${MBD_SETUP_COMMON:=--save --archives-from-proxy --sources ${MBD_CODENAME} --chroots-from-sources --repositories test/Test}
MBD_CONFIG+="MBD_SETUP_COMMON "

MBD_SETUP_DESC="Extra args for API call 'setup'. Usually you just add more sources here."
: ${MBD_SETUP:=}
MBD_CONFIG+="MBD_SETUP "

MBD_DEBIAN_FRONTEND_DESC="debonf frontend for package install and removal/purge. 'noninteractive' will free you from interruptions in test runs (will then purge the package without asking)."
: ${MBD_DEBIAN_FRONTEND:=noninteractive}
MBD_CONFIG+="MBD_DEBIAN_FRONTEND "

MBD_KEEP_DESC="Don't run 'on exit' cleanups."
: ${MBD_KEEP:=false}
MBD_CONFIG+="MBD_KEEP "

if ${MBD_HTTPD_SSL}; then
	MBD_HTTPD_ENDPOINT="ssl:port=${MBD_HTTPD_PORT}:privateKey=/etc/ssl/mini-buildd/private/mini-buildd.key:certKey=/etc/ssl/mini-buildd/certs/mini-buildd.crt"
	MBD_HTTPD_PROTO="https"
else
	MBD_HTTPD_ENDPOINT="tcp6:port=${MBD_HTTPD_PORT}"
	MBD_HTTPD_PROTO="http"
fi
MBD_HTTPD_URL="${MBD_HTTPD_PROTO}://${MBD_HOSTNAME}:${MBD_HTTPD_PORT}"
MBD_DEBCONF="mini-buildd mini-buildd/admin_password password admin"
MBD_DEFAULT="MINI_BUILDD_OPTIONS='--verbose --verbose --debug=${MBD_DEBUG} --hostname=${MBD_HOSTNAME} --http-endpoint=${MBD_HTTPD_ENDPOINT}'"

mbd_api()    { ${MBD_PYPATH}/mini-buildd-api -vv --auto-confirm ${1} ${MBD_HTTPD_PROTO}://admin@${MBD_HOSTNAME}:${MBD_HTTPD_PORT} "${@:2}"; }
mbd_events() { ${MBD_PYPATH}/mini-buildd-events -vv ${MBD_HTTPD_PROTO}://admin@${MBD_HOSTNAME}:${MBD_HTTPD_PORT} "${@:1}"; }
mbd_dput()
{
	mbd_api dput_conf >${MBD_DPUT_CF}
	${MBD_PYPATH}/mini-buildd-dput -vv --config=${MBD_DPUT_CF} --force mini-buildd-${MBD_HOSTNAME%%.*} "${@:1}"
}

# Shortcuts to build one test/keyring package
mbd_testpackage() { mbd_api test_packages --sources="${1}" --distributions="${MBD_CODENAME}-test-unstable" "${@:2}"; }
mbd_mbd-test-cpp()     { mbd_testpackage "mbd-test-cpp" "${@}"; }
mbd_mbd-test-archall() { mbd_testpackage "mbd-test-archall" "${@}"; }
mbd_mbd-test-ftbfs()   { mbd_testpackage "mbd-test-ftbfs" "${@}"; }

mbd_archive-keyring()  { mbd_api keyring_packages --distributions="${MBD_CODENAME}-test-unstable" "${@}"; }

# Shortcuts for pid and kill
mbd_pid()
{
	if cat /var/lib/mini-buildd/.mini-buildd.pid; then
		logI "PID from pidfile"
	elif pgrep --uid=mini-buildd mini-buildd; then
		logI "PID from pgrep"
	fi
}

mbd_signal()
{
	sudo kill -${1} $(mbd_pid)
}

# Find files with python code
declare -a MBD_PYFINDPARAMS=(-not -wholename './debian/*' -not -wholename './.git/*' -not -wholename './build/*' -not -wholename './.pybuild/*' -type f)
mbd_pyscripts()
{
	local f
	for f in $(find \( "${MBD_PYFINDPARAMS[@]}" \) -a -executable); do
		if head -1 "${f}" | grep --quiet "bin/python"; then
			printf "%s\n" "${f}"
		fi
	done
}

mbd_pymodules()
{
	local -a exceptions=(-true)
	[ -z "${*}" ] || exceptions=($(printf " -not -wholename %s" "${@}"))
	find -name "*.py" -a \( "${MBD_PYFINDPARAMS[@]}" \) -a \( "${exceptions[@]}" \)
}

mbd_pysources()
{
	mbd_pyscripts
	mbd_pymodules
}

mbd_service()
{
	if sudo ischroot && [ -d /run/systemd/system ]; then
		# Seems we are in a chroot, and host is running systemd
		# The service will not be started in that case
		# For now, we really want to start the service anyway, so this is still usable in "traditional" chroots
		# (Rather use a container-based environment to test)
		sudo mv /lib/lsb/init-functions.d/40-systemd /lib/lsb/init-functions.d/40-systemd.DISABLED || true
		sudo /etc/init.d/mini-buildd ${1}
		sudo mv /lib/lsb/init-functions.d/40-systemd.DISABLED /lib/lsb/init-functions.d/40-systemd || true
	else
		sudo service mini-buildd "${1}"
	fi
}

# Create "big files" for HTTPD tests and benchamrking.
# POST: MBD_HTTPD_TESTFILES (fileItem: sha1sum) will be available globally.
mbd_gen_httpd_testfiles()
{
	declare -g -A MBD_HTTPD_TESTFILES=()
	local fileSize relPath="repositories/test/pool/testfiles"
	local absPath="/var/lib/mini-buildd/${relPath}"
	sudo mkdir -p "${absPath}"
	sudo chown mini-buildd:mini-buildd "${absPath}"

	for fileSize in 5 50 100; do
		local id="file${fileSize}M"
		local of="/var/lib/mini-buildd/${relPath}/${id}.bin"
		[ -e "${of}" ] || sudo dd if="/dev/urandom" of="${of}" count="${fileSize}" bs="1M" >/dev/null 2>&1
		sudo chown mini-buildd:mini-buildd "${of}"
		MBD_HTTPD_TESTFILES["${id}:${MBD_HTTPD_URL}/${relPath}/${id}.bin"]="$(sha1sum < "${of}")"
	done
}

mbd_changes() # [<arch>]
{
	local arch=${1-$(dpkg-architecture --query=DEB_BUILD_ARCH)}
	printf "../mini-buildd_$(dpkg-parsechangelog --show-field=version)_${arch}.changes"
}

mbd_curl-admin()
{
	local url="${MBD_HTTPD_URL}/accounts/login/"
	local cookies=./devel.curl_cookies
	local curl="curl --silent --cookie-jar ${cookies} --cookie ${cookies} --referer ${url}"
	rm -v -f "${cookies}"

	# Generate initial cookies file
	${curl} "${url}" >/dev/null
	local csrf="csrfmiddlewaretoken=$(grep csrftoken "${cookies}" | cut -f7)"

	# Login. Returns 302 w/ empty body on successful login. We test for the empty body.
	local login_body="./devel.curl_login"
	${curl} --data "${csrf}&username=admin&password=admin" --output "${login_body}" "${url}"
	if [ -s "${login_body}" ]; then
		logE "Curl login failed."
		return 1
	fi
	time ${curl} "${@}"
}

mbd_icons()
{
	logI "Icon names that need to be supported in used icon theme."
	logI "Naming oriented on: https://specifications.freedesktop.org/icon-naming-spec/icon-naming-spec-latest.html"
	grep --recursive --no-filename --only-matching 'mbd_icon[[:space:]]*[^[:space:]]*' src/mini_buildd/templates/ | sort --unique
}

# A very bad hack to hack HTML without having to install package
mbd_hack-html()
{
	local static_path="/usr/lib/python3/dist-packages/mini_buildd/static"
	local templates_path="/usr/lib/python3/dist-packages/mini_buildd/templates"
	case $1 in
		start)
			sudo adduser mini-buildd "$(id --group --name)"
			local path
			for path in ${static_path} ${templates_path}; do
				sudo rm -rfv "${path}"
				(cd $(dirname "${path}") && sudo ln -s -v "${MBD_PYPATH}/mini_buildd/$(basename "${path}")" .)
			done
			(
				cd "${MBD_PYPATH}/mini_buildd/static/"
				ln -s -v "/usr/share/javascript" . || true
				ln -s -v /usr/lib/python3/dist-packages/django/contrib/admin/static/admin . || true
				ln -s -v /usr/share/icons/Numix/48 icons || true
			)
			./devel service restart
			;;
		stop)
			for path in ${static_path} ${templates_path}; do
				sudo rm -v -f "${path}"
			done
			rm -v "${MBD_PYPATH}/mini_buildd/static/javascript" || true
			rm -v "${MBD_PYPATH}/mini_buildd/static/admin" || true
			rm -v "${MBD_PYPATH}/mini_buildd/static/icons" || true
			./devel update
			;;
		check)
			if [ -L "${static_path}" -o -L "${templates_path}" ]; then
				logE "HTML hack active!"
				return 1
			fi
			;;
	esac
}

#
# Runner functions: mbd_run:<sequence>:<name>
#
# <sequence>: Three-digit sequence number; this number also determines in which order to run.
# All run in sequence order is the whole testsuite (which, in turn, may be run with different profiles, see supertestall as example).
#  0..: system prepare
#  1..: static code tests
#  2..: dynamic code tests
#  3..: build
#  4..: static build artefact tests
#  5..: deploy
#  6..: live tests

mbd_run:000:prepare-system()
{
	mbd_installdeps()
	{
		sudo apt-get update
		sudo apt-get --yes upgrade

		# Build-Deps
		mk-build-deps --install --remove --root-cmd=sudo --tool="apt-get --no-install-recommends"
		# WORKAROUND: https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=989696
		rm -v -f *.changes *.buildinfo

		# Extras
		sudo apt-get install \
				 --no-install-recommends \
				 --yes \
				 \
				 git \
				 git-buildpackage \
				 devscripts \
				 disorderfs \
				 diffoscope \
				 equivs \
				 pydocstyle \
				 pycodestyle \
				 pylint \
				 pyflakes3 \
				 python3-apt \
				 python3-pip \
				 python3-wheel \
				 devscripts \
				 piuparts \
				 autopkgtest \
				 faketime \
				 python3-bs4 \
				 python3-keyrings.alt \
				 tidy \
				 codespell \
				 wget \
				 curl \
				 apache2-utils \
				 ssl-cert \
				 libnss3-tools \
				 libdistro-info-perl

		# Extras: ftp client, prefering ftp-ssl.
		sudo apt-get install ftp-ssl || sudo apt-get install ftp
	}

	# Hack to be able to do authorized non-interactive API calls
	mbd_pythonkeyringtestconfig()
	{
		sudo apt-get install python3-keyrings.alt || true   # for the PlainTextKeyring (newer versions only)
		local configDir
		# python-keyring 21.3.0 fixes config file location to '~/.config/python_keyring'; we need to support the old path for some time still.
		for configDir in "${HOME}/.config/python_keyring" "${HOME}/.local/share/python_keyring"; do
			mkdir -p "${configDir}"
			local configFile="${configDir}/keyringrc.cfg"
			cat <<EOF >"${configFile}"
# suitable for python3-keyring > 8
[backend]
default-keyring=keyrings.alt.file.PlaintextKeyring
EOF
			logI "Written ${configFile}"
		done
	}

	# Add custom self-signed cert
	mbd_setupcert()
	{
		sudo ${MBD_PJPATH}/src/mini-buildd-self-signed-certificate create ${MBD_HOSTNAME}

		# Update some cert stores: system, chrome, firefox
		local cert="/etc/ssl/mini-buildd/certs/mini-buildd.crt"
		( cd "/usr/local/share/ca-certificates/" && sudo ln -s -f "${cert}" .)
		sudo update-ca-certificates --fresh

		local pkiDir="${HOME}/.pki" trust="C,,"
		[ "${MBD_CODENAME}" != "buster" ] || trust="P,,"
		local certDB=$(find  ${HOME}/.mozilla/ -name "cert*.db")
		local c
		for c in /usr/local/share/ca-certificates/*.crt; do
			logI "Adding ${c} to chromium..."
			[ ! -e "${pkiDir}/nssdb" ] || certutil -d sql:${pkiDir}/nssdb -A -t "${trust}" -n"$(basename ${c} .pem)" -i "${c}"
			logI "Adding ${c} to firefox..."
			[ ! -e "${certDB}" ] || certutil -d $(dirname ${certDB}) -A -t "${trust}" -n"$(basename ${c} .pem)" -i "${c}"
		done
	}

	mbd_installdeps
	mbd_pythonkeyringtestconfig
	mbd_setupcert
	mbd_pip_uninstall
	mbd_pip_install ${MBD_PIPINSTALL}
}

mbd_run:100:build_py()  # Needed once to get (at least) version.py setup
{
	python3 ./setup.py build_py
}

mbd_run:105:pycodestyle()  # See setup.cfg
{
	_check_prog pycodestyle
	pycodestyle $(mbd_pysources)
}

mbd_run:106:pydocstyle()  # See setup.cfg
{
	_check_prog pydocstyle
	pydocstyle $(mbd_pysources)
}

mbd_run:110:pylint()  # See .pylintrc
{
	_check_prog pylint
	pylint $(mbd_pysources)
}

mbd_run:115:pyflakes()  # See setup.cfg
{
	_check_prog pyflakes3
	pyflakes3 $(mbd_pysources)
}

mbd_run:125:codespell()
{
	local ups=$(codespell --ignore-words-list='referer,fpr,stati,ser' --quiet-level=2 $(mbd_pysources) manual/*.rst)
	if [ -n "${ups}" ]; then
		logE "codespell failed:\n${ups}"
		return 1
	fi
}

mbd_run:200:pydoctests()
{
	for m in $(mbd_pymodules ./setup.py ./manual/conf.py); do  # ./setup.py && ./manual/conf.py can't be used for doctests
		local module="$(basename $(tr '/' '.' <<< ${m:4}) '.py' | cut -d. -f2-)"
		logI "=> Doctest on %s (%s)" "${m}" "${module}"
		( cd ./src/ && ./run-doctest "${module}" )
	done
	python3 -m doctest -v src/mini-buildd src/mini-buildd-api
}

# "fixme"s (W0511) should not fail general pylint check, but we want a to show them anyway, and we want other warning checks still to fail our general check.
# It seems the only way to achieve this currently is to disable 'fixme' in general check, and do this extra run.
mbd_run:210:pylint-fixme()
{
	_check_prog pylint
	pylint --disable=all --enable=fixme $(mbd_pysources) || true
}

# 'locally disabled' pylint checks should be verified from time to time if still needed.
mbd_run:211:pylint-locally-disabled()
{
	_check_prog pylint
	pylint --disable=all --enable=locally-disabled $(mbd_pysources) || true
}

# Same call as in debian/rules, but warnings are errors
mbd_run:201:sphinx-build()
{
	sphinx-build -N -bhtml -W ./manual/ ./build/sphinx/html/
}

mbd_run:300:changelog()
{
	# Checking changelog (must be unchanged)...
	git diff-index --exit-code HEAD debian/changelog
	on_exit git checkout debian/changelog

	## Variant w/: --snapshot --distribution=xxx working:
	# gbp dch --snapshot --auto \
	#		--new-version="$(dpkg-parsechangelog --show-field=Version)~test${MBD_CODEVERSION}+0" --dch-opt="--force-bad-version" \
	#		--distribution="${MBD_CODENAME}-test-experimental" --force-distribution

	# Try to get snapshot changelog so package might be uploaded to testsuite mini-buildd later for testing
	# gbp dch --auto   # This gets pretty slow if your are in a longer dev battle stint with many commits
	gbp dch --since HEAD~~~~ \
			--new-version="$(dpkg-parsechangelog --show-field=Version)~test${MBD_CODEVERSION}+0~$(date --utc +'%Y%m%d%H%M%S')" --dch-opt="--force-bad-version" \
			--distribution=${MBD_CODENAME}-test-experimental --force-distribution
	mbd_changes >"${MBD_LAST_CHANGES}"
}

mbd_run:301:build()
{
	DEB_BUILD_OPTIONS+="nocheck" debuild --no-lintian -us -uc
}

mbd_run:400:lintian()
{
	local changes=$(cat "${MBD_LAST_CHANGES}" || mbd_changes)
	logI "Lintian-checking: %s" "${changes}"
	local result=$(lintian --cfg="${MBD_LINTIANRC}" "${@}" "${changes}")
	printf "%s\n---\n" "${result}"
	if grep "^[EW]:" <<< ${result}; then
		logE "Lintian FAILED"
		if [ -n "${MBD_IGNORE_LINTIAN}" ]; then
			return 0
		else
			read -p "RETURN to ignore" DUMMY
			return 0
		fi
		return 1
	fi
}

# This also checks "full" package building (with doc and check)
mbd_run:401:debrepro()
{
	debrepro
}

mbd_run:500:remove()
{
	mbd_service stop || true
	[ -z "${MBD_DEBIAN_FRONTEND}" ] || export DEBIAN_FRONTEND="${MBD_DEBIAN_FRONTEND}"
	sudo --preserve-env=DEBIAN_FRONTEND dpkg --${1:-remove} mini-buildd mini-buildd-utils mini-buildd-doc python3-mini-buildd python-mini-buildd mini-buildd-common
}

mbd_run:501:purge()
{
	mbd_run:500:remove purge
}

mbd_run:502:install()
{
	mbd_hack-html check
	printf "%s" "${MBD_DEBCONF}" | sudo debconf-set-selections --verbose -
	[ -z "${MBD_DEBIAN_FRONTEND}" ] || export DEBIAN_FRONTEND="${MBD_DEBIAN_FRONTEND}"
	sudo --preserve-env=DEBIAN_FRONTEND apt-get --yes --allow-downgrades install $(mbd_changes)
	printf "%s" "${MBD_DEFAULT}" | sudo tee /etc/default/mini-buildd
}

mbd_run:503:restart()
{
	mbd_service restart

	sleep 0.8  # Try to avoid "connection refused" warnings in most cases.
	local status=false try
	for try in {1..5}; do
		if mbd_api status; then
			status=true
			break
		fi
		sleep 1
	done
	${status}
}

mbd_run:600:setup()
{
	mbd_api setup ${MBD_SETUP_COMMON} ${MBD_SETUP}
}

mbd_run:605:keyring-packages()
{
	mbd_api keyring_packages
}

mbd_run:610:test-packages()
{
	mbd_api test_packages --with-check
}

mbd_run:615:tidy()
{
	local url html="./devel.tidy.html"
	for uri in \
		/mini_buildd/ \
		/mini_buildd/api/ \
		/mini_buildd/api/status/?output=page \
		/mini_buildd/api/ls/?repository=test\&codename=${MBD_CODENAME}\&source=mbd-test-cpp\&output=page \
		$(for route in events builds repositories builders crontab manual api setup log sitemap; do printf "/mini_buildd/%s/ " "${route}"; done) \
		$(for route in events builds repositories; do printf "/mini_buildd/%s-dir/ " "${route}"; done) \
		/accounts/login/ \
		/admin/ \
		/admin/mini_buildd/source/ \
		/static/manual/index.html \
		/static/manual/abstract.html \
		/static/manual/administrator.html \
		/static/manual/consumer.html \
		/static/manual/developer.html \
		/static/manual/roadmap.html \
		/static/manual/auto_admonitions.html \
		; do
		logI "Testing HTML: ${html} from '${MBD_HTTPD_URL}${uri}'"
		# wget --quiet --output-document="${html}" "${MBD_HTTPD_URL}${uri}"
		mbd_curl-admin --fail --silent --output "${html}" "${MBD_HTTPD_URL}${uri}"

		local mute=""
		mute+="PROPRIETARY_ATTRIBUTE,"                                       # With django 3, tidy complains about 'proprietary attribute "autocapitalize"', but this seems to be valid html5.
		mute+="BACKSLASH_IN_URI,ESCAPED_ILLEGAL_URI,ILLEGAL_URI_CODEPOINT,"  # False-positives due to django "escapejs" tag.
		mute+="TRIM_EMPTY_ELEMENT,"                                          # Just too picky.
		mute+="TAG_NOT_ALLOWED_IN,"                                          # Cries about "<script> isn't allowed in <table>", but this isn't true for HTML5 afaik. And me wants!
		if grep --quiet "/manual/" <<<${uri}; then
			mute+="MISSING_ATTR_VALUE,"                                        # HTML created by sphinx (manual) has this a lot, and seems nothing we can do about it.
		fi

		local result=$(tidy --quiet yes --gnu-emacs yes --mute-id yes -output /dev/null --mute ${mute} "${html}" 2>&1)

		if [ -n "${result}" ]; then
			printf "%s" "${result}"
			return 1
		fi
	done
}

mbd_run:620:build-ourselves()
{
	local since=$(date --rfc-3339=ns)
	local changes=$(cat "${MBD_LAST_CHANGES}" || mbd_changes)
	mbd_dput ${changes}
	mbd_events --source="mini-buildd" --distribution="${MBD_CODENAME}-test-experimental" --since="${since}" --exit-on INSTALLED --fail-on FAILED REJECTED
}

mbd_run:625:build-migrate()
{
	local since=$(date --rfc-3339=ns)
	mbd_mbd-test-cpp
	mbd_events --source="mbd-test-cpp" --distribution="${MBD_CODENAME}-test-unstable" --since="${since}" --exit-on INSTALLED --fail-on FAILED REJECTED

	mbd_api migrate --full "mbd-test-cpp" "${MBD_CODENAME}-test-unstable"
	mbd_events --source="mbd-test-cpp" --distribution="${MBD_CODENAME}-test-testing" --since="${since}" --exit-on MIGRATED
	mbd_events --source="mbd-test-cpp" --distribution="${MBD_CODENAME}-test-stable" --since="${since}" --exit-on MIGRATED
}

mbd_run:630:api-getters()
{
	# Consume
	mbd_api status
	mbd_api pub_key
	mbd_api dput_conf
	mbd_api sources_list
	mbd_api ls "mbd-test-cpp" --repositories "test" --codenames "${MBD_CODENAME}"
	mbd_api list "mbd-test-cpp" --distributions "${MBD_CODENAME}-test-unstable"
	mbd_api find "mbd-test-cpp"

	# Maintain
	mbd_api power ON
	mbd_api uploaders
	mbd_api snapshot_ls "${MBD_CODENAME}-test-stable"
}

_test_install()
{
	on_exit sudo dpkg --purge "${1}"
	sudo apt-get "${@:2}" install "${1}"
}

mbd_run:635:apt-tofu-bootstrap()
{
	# Setup cleanup
	local sources_list="/etc/apt/sources.list.d/mbd-testsuite.list"
	on_exit sudo rm -v -f "${sources_list}"
	on_exit sudo apt-get update

	# Install keyring package via a forcibly trusted stable apt line
	mbd_api sources_list --codenames "${MBD_CODENAME}" --suites "stable" --options="trusted=yes" | sudo tee "${sources_list}"
	sudo apt-get update
	_test_install ${MBD_HOSTNAME%%.*}-archive-keyring

	# Re-instate 'normal' apt line and test apt update
	mbd_api sources_list --codenames "${MBD_CODENAME}" --suites "stable" | sudo tee "${sources_list}"
	sudo apt-get update
}

mbd_run:640:apt-snapshot()
{
	# Test snapshot gen, del and sources.list
	local snap="TESTSUITE_$(date --iso-8601=seconds)"
	mbd_api snapshot_create "${MBD_CODENAME}-test-stable" "${snap}"
	mbd_api snapshot_delete "${MBD_CODENAME}-test-stable" "${snap}"
	mbd_api snapshot_create "${MBD_CODENAME}-test-stable" "${snap}"
	mbd_api snapshot_ls "${MBD_CODENAME}-test-stable"
	mbd_api sources_list --codenames "${MBD_CODENAME}" --suites "stable" --snapshot=${snap}
}

mbd_run:645:apt-install()
{
	# install previously built test package
	_test_install mbd-test-cpp
}

mbd_run:650:testsuite-packages()
{
	local location="./share/testsuite-packages/" dst changes

	# Clean out possible debris
	(cd "${location}" && git clean -x -d -f)

	# Rebuild DSCs when needed
	for dst in $(find ${location} -maxdepth 1 -mindepth 1 -type d); do
		sed --in-place --expression="1 s/sid/${MBD_CODENAME}/" --expression="1 s/~SID/${MBD_CODEVERSION}/" "${dst}/debian/changelog"
		(
			cd ${dst}
			dpkg-buildpackage -us -uc
		)
		sed --in-place --expression="1 s/${MBD_CODENAME}/sid/" --expression="1 s/${MBD_CODEVERSION}/~SID/" "${dst}/debian/changelog"
	done

	# Tests: Upload and expect event
	for changes in ${location}/*${1}*.changes; do
		local since=$(date --rfc-3339=ns)
		local source=$(basename ${changes} | cut -d'_' -f1)
		local expect=$(cut -d'-' -f1 <<<${source} | tr '[:lower:]' '[:upper:]')
		local failOn="FAILED REJECTED"
		case ${expect} in
			REJECTED)
				failOn="INSTALLED BUILT"
				;;
		esac
		logI "Testing ${source} (expect=${expect})"

		case ${source} in
			installed-portext)
				cp "${location}/installed-portext_1.0.0.dsc" "${location}/installed-portext_1.0.0.tar.gz" "/tmp/"
				mbd_api port_ext --allow-unauthenticated "file:///tmp/installed-portext_1.0.0.dsc" "${MBD_CODENAME}-test-unstable"
				;;
			*)
				# Standard dput, dput-ng can't do ftps.
				mbd_dput "${changes}"
				;;
		esac
		mbd_events --source="${source}" --since="${since}" --exit-on ${expect} --fail-on ${failOn}
		# We should remove an installed package so the test can be repeated on same instance:
		if [ "${expect}" == "INSTALLED" ]; then
			mbd_api remove "${source}" "${MBD_CODENAME}-test-unstable" --without-rollback
		fi
	done
}

mbd_run:655:bogus-ftp-uploads()
{
	local -i ftp_port=$((MBD_HTTPD_PORT+1))
	# test packager.py bogus upload handling
	_upload()
	{
		ftp -inv ${MBD_HOSTNAME} ${ftp_port} <<EOF
user "anonymous" "dummypass"
cd incoming/
binary
put "${1}" "$(basename "${1}")"
bye
EOF
	}

	# Broken but basically acceptable changes. This should log event REJECTED.
	local bogc="/tmp/bogus.changes"
	cat <<EOF >"${bogc}"
Distribution: kaputt
Version: 1.2.3
Source: kaputt
Architecture: kaputt
EOF
	local since=$(date --rfc-3339=ns)
	_upload "${bogc}"
	mbd_events --source="kaputt" --since="${since}" --exit-on REJECTED

	# Completely unparsable changes. This file should just be removed by mini-buildd (no feasable way to test that automatically, though).
	cat <<EOF >"${bogc}"
kaputt
EOF
	_upload "${bogc}"
}

mbd_run:660:httpd-testfiles()
{
	mbd_gen_httpd_testfiles
	local item
	for item in ${!MBD_HTTPD_TESTFILES[@]}; do
		local id=$(cut -d: -f1 <<<${item}) url=$(cut -d: -f2- <<<${item})
		local of="$(mktemp)"
		wget --output-document="${of}" "${url}"
		local sha1sum="$(sha1sum < "${of}")"
		if [ "${sha1sum}" = "${MBD_HTTPD_TESTFILES[${item}]}" ]; then
			logI "HTTP download OK: %s=%s %s" "${id}" "${of}" "${sha1sum}"
		else
			logE "HTTP download FAILED: %s=%s %s != %s" "${id}" "${of}" "${sha1sum}" "${MBD_HTTPD_TESTFILES[${item}]}"
			return 1
		fi
	done
}

mbd_run:665:httpd-benchmark()
{
	mbd_gen_httpd_testfiles

	_ab_val() { cut -d: -f2- | cut -d[ -f1 | tr -d '[:space:]'; }

	local abRequests=50 item

	for item in django:${MBD_HTTPD_URL}/mini_buildd/repositories/test/ index:${MBD_HTTPD_URL}/repositories/test/pool/main/m/mbd-test-cpp/ ${!MBD_HTTPD_TESTFILES[@]}; do
		local id=$(cut -d: -f1 <<<${item})
		local url=$(cut -d: -f2- <<<${item})
		local c
		for c in 1 4; do
			local abResult=$(ab -n "${abRequests}" -c "${c}" "${url}")
			local server=$(grep "^Server Software:.*" <<<${abResult} | _ab_val)
			if (( c > 1 )); then
				local tpr=$(grep "^Time per request:.*(mean, across all concurrent requests)" <<<${abResult} | _ab_val)
			else
				local tpr=$(grep "^Time per request:.*(mean)" <<<${abResult} | _ab_val)
			fi
			{
				if grep "^Non-2xx responses:" <<<${abResult}; then
					printf "%s" "${abResult}"
					printf "\n\n%s\n" "E: Above ab call has non-200 responses!"
					tpr="-1.0"
				fi
			} >&2
			# Note: decimal separator in "ab" is ".", so e need to set LANG for printf
			LANG="C.UTF-8" printf "[%20s] %-12s: % 8.3f\n" "${server}" "${id} c=${c}" "${tpr}"
		done
	done
}

mbd_sequence()  # [<levelRegex>=00] [<name>] <hr>
{
	local levelRegex="${1:-00}" name="${2}" hr="${3}"
	for func in $(declare -F | cut -d" " -f3- | grep "^mbd_run:${levelRegex}:${name}" | sort || true); do
		if [ -n "${hr}" ]; then
			printf "%s " "$(cut -d: -f3 <<<${func})"
		else
			printf "%s " "${func}"
		fi
	done
}

mbd_run()  # [<levelRegex>=00] [<name>] [<customArgs>...]
{
	local levelRegex="${1}" name="${2}"
	local -a info=()
	local func totalStartStamp=$(date +%s)
	local -i count=0
	for func in $(mbd_sequence "${levelRegex}" "${name}"); do
		MBD_RUNNING="${func}"
		local startStamp=$(date +%s)
		if grep --quiet "${func}" <<<${MBD_SKIP}; then
			logI "SKIPPED."
		else
			logI "Starting..."
			${func} "${@:3}"
			logI "Completed."
		fi
		MBD_RUNNING=""

		count+=1
		info+=("$(printf "OK (%03d seconds): %s" "$(($(date +%s) - startStamp))" "${func}")")
	done
	if ((count <= 0)); then
		logE "No runs for this sequence filter: ${levelRegex} ${name}"
		return 1
	fi
	logI "Sequence results (%03d seconds):" "$(($(date +%s) - totalStartStamp))"
	logI "%s" "${info[@]}"
	logI "OK, %s runs succeeded ($(date))." "${count}"
}

# Shortcuts
declare -A MBD_RUN_SHORTCUTS=(
	["check"]='1..'
	["update"]='\(300\|301\|502\|503\)'
	["updatecheck"]='\(1..\|300\|301\|502\|503\)'
	["updatetest"]='\([12]..\|300\|301\|502\|503\|4..\)'
	["updatetestall"]='.*'
)
# We can't iterate through the associative array in the given order later, so we at least want a sorted key list as helper
MBD_RUN_SHORTCUTS_SORTED="$(printf '%s\n' "${!MBD_RUN_SHORTCUTS[@]}" | sort -n)"

# Some extra targets
mbd_daemon.log()
{
	# less --follow-name +F /var/lib/mini-buildd/var/log/daemon.log  # tends to hog CPU
	tail --follow=name --retry /var/lib/mini-buildd/var/log/daemon.log
}

mbd_access.log()
{
	# less --follow-name +F /var/lib/mini-buildd/var/log/access.log  # tends to hog CPU
	tail --follow=name --retry /var/lib/mini-buildd/var/log/access.log
}

mbd_pygrep()
{
	[ -n "${*}" ] || return 0  # Return immediately on empty args, else grep will stall
	local p
	for p in $(mbd_pysources); do
		grep "${@}" "${p}" || true
	done
}

mbd_pylintgeneratedmembers()
{
	# Hack to update identifiers with false-positive "has no xxx member" error (mostly django).
	# Implies you already have a this as _last_ option in ``setup.cfg``.
	{
		for o in $(pylint src/mini_buildd/ | grep "has no.*member" | cut -d"'" -f4 | sort | uniq); do
			printf "\t${o},\n"
		done
	} >>setup.cfg
}

# Try to reproduce deadlock
# This try w/ events does not yet reproduce the deadlock; saving for later.
mbd_deadlock()
{
	local -i count=0
	while mbd_events --source mbd-test-cpp --stop >/dev/null; do
		# echo ${count}; count+=1;
		sleep 0;
	done
}

mbd_bash-completion()
{
	local non_runners=$(declare -F | cut -d" " -f3 | grep "^mbd_" | grep --invert-match "^mbd_run" | cut -b5-)
	local profiles="${!MBD_PROFILES[@]}"
	printf "complete -W '%s' ./devel\n" "$(printf '%s ' "${!MBD_RUN_SHORTCUTS[@]}") $(mbd_sequence "..." "" "hr") ${non_runners} ${profiles}"
}

mbd_run-lintian()
{
	mbd_run:300:changelog
	mbd_run:301:build
	mbd_run:400:lintian
}

main()
{
	if [ -z "${1}" ]; then
		local p="./$(basename "${0}")" b=$(tput bold) i=$(tput sitm) r=$(tput sgr0)
		cat <<EOF
Usage: [<env>] ${p} <shortcut-or-runner-or-special> | run <groupRegex><levelRegex>${r} [<customArgs>...]

mini-buildd development helper.

Environment (prefix to command or export):

$(for VAR in ${MBD_CONFIG}; do VAR_DESC="${VAR}_DESC"; printf "%s\n  %s\n" "${VAR}='${!VAR}'" "${!VAR_DESC}"; done)

${b}Sequence filter shortcuts${r}:
$(for s in ${MBD_RUN_SHORTCUTS_SORTED}; do printf "  ${i}${p} %-15s${r}: %s(%s)\n" "${s}" "$(mbd_sequence "${MBD_RUN_SHORTCUTS[${s}]}" "" "hr")" "${MBD_RUN_SHORTCUTS[${s}]}"; done)

${b}Runners${r}:
 ${i}${p} $(mbd_sequence ".." "" "hr" | tr " " "|")

${b}Special (non-runner) targets${r}:
 ${i}${p} supertestall: Iterates 'updatetestall' through all test profiles (currently normal+ssl).
 ${i}${p} daemon.log  : Follow daemon logs.
 ...-> Check source for other possible esoteric calls.
EOF
	else
		check_devsys
		local shortcut="${MBD_RUN_SHORTCUTS[${1}]}"
		if [ -n "${shortcut}" ]; then
			mbd_run "${shortcut}" "" "${@:2}"
		else
			local func="mbd_${1}"  # direct function
			if [ "$(type -t "${func}")" = "function" ]; then
				${func} "${@:2}"
			else
				mbd_run '.*' "${1}" "${@:2}"
			fi
		fi
	fi
}

main "${@}"
