#!/usr/bin/env bash
# Runs one shard of the EEST spec-test workflow:
#
#     tools/run-eest-spec-test.sh <shard>
#
# Where <shard> is one of:
#
#   statetests-stable                          state tests vs. eest_stable
#   statetests-devnet                          state tests vs. eest_devnet
#   blocktests-stable-sequential               blockchain tests vs. eest_stable
#   blocktests-devnet                          blockchain tests vs. eest_devnet
#   enginextests-stable-sequential             engine-x tests vs. eest_stable
#   enginextests-benchmark-{1m,5m,10m,30m,60m,100m,150m}-sequential
#                                              engine-x benchmark fixtures per
#                                              gas-target subdir; each value
#                                              maps to one for_osaka_at_<NNNN>M/
#                                              directory under the engine_x
#                                              benchmark fixtures
#   blocktests-stable-race-{pre-cancun,cancun,prague,osaka}-sequential
#                                              race-detector variant of
#                                              blocktests-stable-sequential, split by
#                                              fork via the manifest `run` regex so
#                                              each sub-shard fits under ~30
#                                              min. Caller (Makefile / CI) must
#                                              export EVM_BIN to the race-built
#                                              binary; otherwise -race
#                                              detection doesn't fire.
#   blocktests-devnet-race-amsterdam           race-detector variant filtered
#                                              to the Amsterdam fork only.
#   zkevm-witness                              zkevm execution-witness conformance
#                                              (eest_zkevm corpus) via the zkevmtest
#                                              runner; compares debug_executionWitness
#                                              (canonical) per block.
#   zkevm-witness-race                         race-detector variant of the above over
#                                              the whole corpus (run via evm.race).
#   *-parallel                                  any of the above with "-parallel"
#                                              appended runs with
#                                              ERIGON_EXEC3_PARALLEL=true. Every
#                                              other shard runs with
#                                              ERIGON_EXEC3_PARALLEL=false so
#                                              the runtime default in
#                                              dbg.Exec3Parallel can flip without
#                                              redefining the shards.
#
# Each shard maps to one cmd/evm subcommand running with --jsonout. Pass/fail
# is decided here (not by the binary, which always exits 0): the shard fails
# if no tests ran (unexpected — fixture path bug) or if observed failures
# exceed EEST_SPEC_MAX_FAILURES.
#
# Env overrides:
#   EEST_SPEC_MAX_FAILURES   - failure budget; CI sets via matrix include:
#   EEST_SPEC_WORKERS        - parallel worker count
#   EVM_BIN                  - path to evm binary (default build/bin/evm)
#   ERIGON_EXECUTION_TESTS_TMPDIR - if set, used as TMPDIR; on Darwin we
#                                   create a ramdisk if unset

set -euo pipefail

if (( $# != 1 )); then
	echo "usage: $0 <shard>" >&2
	exit 2
fi
shard="$1"

for tool in yq jq; do
	command -v "$tool" >/dev/null 2>&1 || { echo "run-eest-spec-test: required tool '$tool' not found in PATH" >&2; exit 1; }
done

# Resolve the fixture set from the shard name. blocktests-{stable,devnet}-race-*
# inherit from the parent shard (stable/devnet).
case "$shard" in
	*zkevm*)      fixtures=eest_zkevm ;;
	*-stable*)    fixtures=eest_stable ;;
	*-devnet*)    fixtures=eest_devnet ;;
	*-benchmark*) fixtures=eest_benchmark ;;
	*) echo "cannot resolve fixtures for shard: $shard" >&2; exit 2 ;;
esac
base=test-fixtures-cache/$fixtures/fixtures

# Resolve workers + failure budget + exec3-parallel + the optional race --run
# regex from the single-source manifest. This script, the test-eest-spec.yml
# load-matrix job, and the coverage guard all read tools/eest-spec-shards.yml,
# so adding a shard / tweaking a budget / changing a fork filter is a one-file
# edit. yq converts YAML→JSON so it can be queried with jq.
manifest=tools/eest-spec-shards.yml
shard_row=$(yq -o=json '.' "$manifest" | jq -r --arg s "$shard" '.[] | select(.shard == $s) | "\(.workers)\t\(."max-allowed-failures")\t\(."exec3-parallel" // false)\t\(."no-ramdisk" // false)\t\(.run // "")"')
if [[ -z "$shard_row" ]]; then
	echo "shard $shard not found in $manifest" >&2
	exit 2
fi
IFS=$'\t' read -r default_workers default_max exec3_parallel shard_no_ramdisk run_regex <<<"$shard_row"
# Always set ERIGON_EXEC3_PARALLEL explicitly (true or false) so the shard's
# behaviour is pinned to the manifest, independent of whatever dbg.Exec3Parallel
# defaults to at runtime. If the default flips, the shards still run the mode
# they were defined for.
export ERIGON_EXEC3_PARALLEL="$exec3_parallel"

# Strip "-parallel" / "-sequential" suffix for case-arm routing — both variants
# share the same fixture path / regex as the parent shard; only the
# ERIGON_EXEC3_PARALLEL env var differs.
shard_route="${shard%-parallel}"
shard_route="${shard_route%-sequential}"

# Per-shard structural config (cmd / fixture path / extra CLI flags). Match
# against shard_route so "-parallel" variants reuse the same arm as their
# non-parallel parent.
extra=()
case "$shard_route" in
	statetests-stable | statetests-devnet)
		cmd=statetest;   path="$base/state_tests" ;;
	# race shards reuse this arm; each one's per-fork --run regex comes from the
	# manifest `run` key (appended after this case), not from a per-shard arm.
	blocktests-stable | blocktests-devnet | blocktests-stable-race-* | blocktests-devnet-race-*)
		cmd=blocktest;   path="$base/blockchain_tests" ;;
	enginextests-stable)
		cmd=enginextest; path="$base/blockchain_tests_engine_x"
		extra=(--pre-alloc-dir "$path/pre_alloc") ;;
	enginextests-benchmark-*)
		# Per-gas-target shard: "...-1m" → for_osaka_at_0001M/, etc. Use
		# shard_route (with any "-parallel" suffix stripped) so the parallel
		# variants resolve to the same gas-target subdir.
		gas="${shard_route##*-}"; gas_num="${gas%m}"
		printf -v gas_dir 'for_osaka_at_%04dM' "$gas_num"
		cmd=enginextest
		path="$base/blockchain_tests_engine_x/$gas_dir"
		extra=(--pre-alloc-dir "$base/blockchain_tests_engine_x/pre_alloc" --time) ;;
	zkevm-witness*)
		# Whole eest_zkevm blockchain_tests corpus; the "-race" variant differs
		# only in the binary (evm.race, picked by the Makefile), not the fixtures.
		cmd=zkevmtest;   path="$base/blockchain_tests" ;;
	*) echo "unknown shard: $shard (route: $shard_route)" >&2; exit 2 ;;
esac
[[ -n "$run_regex" ]] && extra+=(--run "$run_regex")

workers="${EEST_SPEC_WORKERS:-$default_workers}"
max="${EEST_SPEC_MAX_FAILURES:-$default_max}"
evm_bin="${EVM_BIN:-build/bin/evm}"

if [[ ! -x "$evm_bin" ]]; then
	echo "$evm_bin not found or not executable; run 'make evm' first" >&2
	exit 2
fi

# Provision only this shard's fixture set (no-op when already extracted) —
# all corpora together are 20G+ extracted and don't fit on the smaller CI
# runner disks.
bash tools/test-fixtures.sh test-fixtures.json test-fixtures-cache "$fixtures"

if [[ ! -d "$path" ]]; then
	echo "fixture path $path does not exist in the $fixtures corpus" >&2
	exit 2
fi

result_file=$(mktemp)
ramdisk=""
cleanup() {
	# Auto-detach Darwin ramdisk we created in this run; leave existing
	# user-provided ones (ERIGON_EXECUTION_TESTS_TMPDIR) untouched.
	if [[ -n "$ramdisk" && "$(uname -s)" == "Darwin" ]]; then
		hdiutil detach -force "$ramdisk" >/dev/null 2>&1 || true
	fi
	rm -f "$result_file" 2>/dev/null || true
}
trap cleanup EXIT

# Linux: do NOT auto-create a ramdisk. tools/create-ramdisk uses sudo mount
# and never unmounts; CI gets the env var pre-set via the setup-erigon
# action's ramdisk: true input. Local Linux users can opt in by exporting
# ERIGON_EXECUTION_TESTS_TMPDIR or TMPDIR=/dev/shm.
# Shards with no-ramdisk: true in the manifest skip the Darwin auto-ramdisk too:
# their datadirs are few and long-lived, so tmpfs buys nothing — and the 150m
# datadir (~7GB) doesn't even fit the default ramdisk size.
if [[ "$shard_no_ramdisk" != "true" && -z "${ERIGON_EXECUTION_TESTS_TMPDIR:-}" && "$(uname -s)" == "Darwin" ]]; then
	ramdisk=$(bash tools/create-ramdisk) || true
	if [[ -n "$ramdisk" ]]; then
		export ERIGON_EXECUTION_TESTS_TMPDIR="$ramdisk"
		echo "ramdisk: $ramdisk"
	fi
fi
export TMPDIR="${ERIGON_EXECUTION_TESTS_TMPDIR:-${TMPDIR:-/tmp}}"

echo "running: $evm_bin $cmd ${extra[*]:-} --workers $workers --jsonout $path"
echo "tmpdir:  $TMPDIR"
echo "max-allowed-failures: $max"

# Don't fail on most non-zero exits — the runners report all results via JSON
# regardless, and we want to inspect the JSON to drive the pass/fail decision
# (crashes surface as missing/truncated JSON below). The one exception is exit
# code 66: the Go race runtime's "data race detected" signal, emitted even when
# the run completes and the JSON parses clean, so it must be checked explicitly.
# The grep filter strips any init-time log lines (e.g. dbg.envLookup's
# "[WARN] [env]" message when ERIGON_EXEC3_PARALLEL is set fires before cmd/evm
# sets the log handler, and the default log handler writes to stdout) so jq
# sees only JSON.
raw_file=$(mktemp)
rc=0
"$evm_bin" "$cmd" --workers "$workers" --jsonout "${extra[@]}" "$path" > "$raw_file" || rc=$?
if (( rc == 66 )); then
	echo "ERROR: data race detected (race runtime exit code 66); see WARNING: DATA RACE in the log above" >&2
	exit 1
fi
grep -v '^\[[A-Z][A-Z]*\]' "$raw_file" > "$result_file"
rm -f "$raw_file"

total=$(jq 'length' "$result_file")
failed=$(jq '[.[] | select(.pass == false)] | length' "$result_file")
echo "ran $total tests, $failed failed"

if (( total == 0 )); then
	echo "ERROR: no tests were run; expected non-empty fixtures at $path" >&2
	exit 1
fi
if (( failed > max )); then
	echo "ERROR: $failed failures exceed max-allowed $max" >&2
	# Emit the cmd/evm `error` field per failing test, not just the name,
	# so transient CI flakes are diagnosable from the job log alone (the
	# raw JSON output is dropped after this script exits).
	jq -r '.[] | select(.pass == false) | "  FAIL " + .name + "\n        error: " + (.error // "<no error message>")' "$result_file" >&2
	exit 1
fi
exit 0
