#!/usr/bin/env bash
# Integration test for the development environment.
#
# It drives `frogue-dev`, the same entry point `nix run .#dev` runs, with the
# same config and the same OTLP environment; drives a request through the
# server; and then asserts that the trace actually reached Jaeger. Nothing here
# re-implements the environment, so it covers the topology *and* the way the app
# starts process-compose -- an app that binds something fixed fails here rather
# than only on whoever's machine happens to have that port taken.
#
# Run it with `nix run .#dev-test`. It needs podman, it really starts the
# container, and it uses the development ports, so nothing else may be holding
# 6565, 4318 or 16686 while it runs.

set -euo pipefail

if ! command -v frogue-dev >/dev/null; then
	echo "dev-test: frogue-dev is not on PATH" >&2
	echo "dev-test: run me through \`nix run .#dev-test\`" >&2
	exit 1
fi

root=${FROGUE_DEV_ROOT:-$(git rev-parse --show-toplevel 2>/dev/null || pwd)}
cd "$root"

# A connection attempt is the cheapest protocol-agnostic liveness probe, and
# unlike curl it is not fooled by a listener that speaks something other than
# HTTP -- which is exactly what the OTLP port is.
port_open() {
	(exec 3<>"/dev/tcp/127.0.0.1/$1") 2>/dev/null
}

busy=""
for port in 6565 4318 16686; do
	if port_open "$port"; then
		busy="$busy $port"
	fi
done
if [ -n "$busy" ]; then
	echo "dev-test: something is already listening on$busy; stop it first" >&2
	exit 1
fi

log_dir=${TMPDIR:-/tmp}/frogue-dev-test
mkdir -p "$log_dir"
log=$log_dir/process-compose.log
# Kept apart from the stack's console output: pointing both at one file would
# leave two writers truncating each other.
console=$log_dir/console.log

echo "dev-test: starting the development stack (logs: $log_dir)"
frogue-dev --tui=false --log-file "$log" >"$console" 2>&1 &
pc_pid=$!

cleanup() {
	kill "$pc_pid" 2>/dev/null || true
	wait "$pc_pid" 2>/dev/null || true
	# process-compose stops what it started, but if it was killed outright the
	# container can outlive it and hold the ports.
	podman rm --force frogue-dev-jaeger >/dev/null 2>&1 || true
}
trap cleanup EXIT

wait_for_http() {
	local what=$1 url=$2 timeout=$3
	local deadline=$((SECONDS + timeout))
	while [ "$SECONDS" -lt "$deadline" ]; do
		if curl --silent --fail --output /dev/null "$url"; then
			echo "dev-test: $what is up"
			return 0
		fi
		sleep 1
	done
	echo "dev-test: $what did not answer at $url within ${timeout}s" >&2
	echo "dev-test: last lines of $console:" >&2
	tail --lines=20 "$console" >&2 || true
	return 1
}

# Jaeger 2 dropped the v1 query routes: /api/services is a 404 there, and
# probing it would report a healthy collector as dead.
wait_for_http jaeger "http://127.0.0.1:16686/api/v3/services" 60

# Generous: the watcher compiles the whole workspace before it can serve
# anything, and that is minutes from cold.
wait_for_http frogue "http://127.0.0.1:6565/" 900

echo "dev-test: driving a request through the server"
curl --silent --output /dev/null "http://127.0.0.1:6565/"

# The export is batched, with a five second schedule delay, so this is expected
# to take a few seconds rather than being immediate.
deadline=$((SECONDS + 60))
services=""
while [ "$SECONDS" -lt "$deadline" ]; do
	services=$(curl --silent "http://127.0.0.1:16686/api/v3/services" || true)
	if printf '%s' "$services" | jq --exit-status '.services | index("frogue")' >/dev/null 2>&1; then
		break
	fi
	sleep 1
done

if ! printf '%s' "$services" | jq --exit-status '.services | index("frogue")' >/dev/null 2>&1; then
	echo "dev-test: frogue never reached jaeger; services were: $services" >&2
	echo "dev-test: last lines of $console:" >&2
	tail --lines=20 "$console" >&2 || true
	exit 1
fi

echo "dev-test: jaeger knows about frogue"

# Auto-reload is the point of the watcher, so it is asserted rather than
# assumed: a change under either watched directory has to end in the server
# being restarted. The files are only touched, never modified -- only the mtime
# matters, so this leaves no trace in the working copy -- and the restart is
# seen through cargo, which prints its build line on every run. Watching that
# rather than the server's own startup line keeps the assertion independent of
# RUST_LOG, which the environment deliberately leaves to the caller.
expect_restart() {
	local path=$1
	# The dots stand in for the backticks cargo puts around `dev`: spelling
	# them out would trip shellcheck's "expressions don't expand in single
	# quotes" warning.
	local pattern='Finished .dev. profile'
	local before
	before=$(grep --extended-regexp --count "$pattern" "$console" || true)
	touch "$path"

	local deadline=$((SECONDS + 300))
	while [ "$SECONDS" -lt "$deadline" ]; do
		if [ "$(grep --extended-regexp --count "$pattern" "$console" || true)" -gt "$before" ]; then
			echo "dev-test: touching $path restarted the server"
			return 0
		fi
		sleep 1
	done

	echo "dev-test: touching $path did not restart the server" >&2
	echo "dev-test: last lines of $console:" >&2
	tail --lines=20 "$console" >&2 || true
	return 1
}

expect_restart frogue/src/main.rs
expect_restart frogue/src/views.rs

wait_for_http "frogue after reloading" "http://127.0.0.1:6565/" 120

cleanup
trap - EXIT

# A leak here would be invisible in the happy path but would break the next
# run, so it is part of the test.
for _ in $(seq 1 20); do
	busy=""
	for port in 6565 4318 16686; do
		if port_open "$port"; then
			busy="$busy $port"
		fi
	done
	[ -z "$busy" ] && break
	sleep 0.5
done
if [ -n "$busy" ]; then
	echo "dev-test: ports still bound after shutdown:$busy" >&2
	exit 1
fi

echo "dev-test: OK, traces flow to jaeger and the stack shuts down cleanly"
