#!/usr/bin/env bash
# check-make-shells — assert that apps/backend/Makefile's run/dev/start-debug
# and deadcode targets expand to the correct per-shell command line, without
# building them.
#
# The Makefile dispatches on two independent axes:
#   * EXE suffix   — keyed on $(OS): any Windows build (native cmd/PowerShell
#                    OR Git Bash) produces kandev.exe.
#   * shell syntax — keyed on $(OS)$(MSYSTEM): only native cmd/PowerShell uses
#                    `set VAR=VAL&` + backslash path + no exec; Unix and Git
#                    Bash/MSYS use POSIX `VAR=val exec`.
# Git Bash is the trap — it sets OS=Windows_NT yet runs recipes through sh, so
# it must take the POSIX branch while keeping the .exe suffix. Conflating the
# two axes (the pre-fix bug) made Git Bash emit `set VAR=VAL&` into bash, where
# the env var is never exported.
#
# Each shell is simulated deterministically with make's OS=/MSYSTEM=
# command-line overrides, so one host checks the dispatch for all three shells
# without having to run on each. The harness itself is bash (grep, sed), so it
# needs a POSIX shell — it runs in the Linux CI job and under Git Bash/WSL
# locally, not from native cmd.exe/PowerShell. Covers the Make-target dispatch
# that the backend Go CI jobs don't exercise. When cmd.exe is available, the
# script also runs an isolated mkdir probe without building any binaries.

set -euo pipefail

backend_dir="$(cd "$(dirname "$0")/../apps/backend" && pwd)"

status=0
windows_mkdir_recipe=

# expect <label> <OS> <MSYSTEM> <target> <expected recipe tail>
expect() {
	local label=$1 os=$2 msys=$3 target=$4 want=$5 marker=${6:-__backend} title_prefix=${7:-} got
	# The OS=/MSYSTEM= overrides only steer which branch the Makefile picks;
	# $(shell ...) still runs under this harness's POSIX shell, and it expands
	# even under `make -n`. So neutralise the build-stamp probes rather than let
	# the windows-native case run them: NULL_REDIR=NUL would have sh create a
	# junk ./NUL, and BUILD_TIME would spawn PowerShell per case. Blanking
	# NULL_REDIR (rather than passing /dev/null) also dodges Git Bash rewriting
	# a /dev/null argument to `nul` on its way to native make. None of this
	# affects the run/dev/start-debug recipes under assertion.
	if [ -n "$title_prefix" ]; then
		got=$(KANDEV_WEB_TITLE_PREFIX="$title_prefix" make -C "$backend_dir" OS="$os" MSYSTEM="$msys" NULL_REDIR= BUILD_TIME=simulated -n "$target" 2>/dev/null \
			| grep -F "$marker" \
			| sed 's/^[[:space:]]*//; s/[[:space:]]*$//; s/[[:space:]]\{1,\}/ /g' \
			|| true)
	else
		got=$(env -u KANDEV_WEB_TITLE_PREFIX make -C "$backend_dir" OS="$os" MSYSTEM="$msys" NULL_REDIR= BUILD_TIME=simulated -n "$target" 2>/dev/null \
			| grep -F "$marker" \
			| sed 's/^[[:space:]]*//; s/[[:space:]]*$//; s/[[:space:]]\{1,\}/ /g' \
			|| true)
	fi
	if [ "$got" = "$want" ]; then
		printf 'ok    %-16s %-12s %s\n' "$label" "$target" "$got"
		if [ "$label" = windows-native ] && [ "$target" = build-kandev ]; then
			windows_mkdir_recipe="$got"
		fi
	else
		printf 'FAIL  %-16s %-12s\n        want: %s\n        got:  %s\n' \
			"$label" "$target" "$want" "$got"
		status=1
	fi
}

# Unix — no .exe, POSIX env-prefix + exec.
expect unix '' '' run         './bin/kandev __backend'
expect unix '' '' dev         'KANDEV_DEBUG_DEV_MODE=true exec ./bin/kandev __backend'
expect unix '' '' start-debug "KANDEV_DEBUG_PPROF_ENABLED=true KANDEV_LOG_LEVEL=debug KANDEV_WEB_TITLE_PREFIX='Debug' exec ./bin/kandev __backend"
expect unix '' '' start-debug "KANDEV_DEBUG_PPROF_ENABLED=true KANDEV_LOG_LEVEL=debug KANDEV_WEB_TITLE_PREFIX='Preview' exec ./bin/kandev __backend" __backend Preview
expect unix '' '' start-debug "KANDEV_DEBUG_PPROF_ENABLED=true KANDEV_LOG_LEVEL=debug KANDEV_WEB_TITLE_PREFIX='QA Debug' exec ./bin/kandev __backend" __backend "QA Debug"

# Native Windows (cmd/PowerShell) — .exe, `set VAR=VAL&`, backslash path, no exec.
expect windows-native Windows_NT '' run         '"bin\kandev.exe" __backend'
expect windows-native Windows_NT '' dev         'set KANDEV_DEBUG_DEV_MODE=true& "bin\kandev.exe" __backend'
expect windows-native Windows_NT '' start-debug 'set KANDEV_DEBUG_PPROF_ENABLED=true& set KANDEV_LOG_LEVEL=debug& set "KANDEV_WEB_TITLE_PREFIX=Debug"& "bin\kandev.exe" __backend'
expect windows-native Windows_NT '' start-debug 'set KANDEV_DEBUG_PPROF_ENABLED=true& set KANDEV_LOG_LEVEL=debug& set "KANDEV_WEB_TITLE_PREFIX=Preview"& "bin\kandev.exe" __backend' __backend Preview
expect windows-native Windows_NT '' start-debug 'set KANDEV_DEBUG_PPROF_ENABLED=true& set KANDEV_LOG_LEVEL=debug& set "KANDEV_WEB_TITLE_PREFIX=QA Debug"& "bin\kandev.exe" __backend' __backend "QA Debug"

# Git Bash/MSYS — .exe suffix but POSIX syntax (recipes run through sh).
expect git-bash Windows_NT MINGW64 run         './bin/kandev.exe __backend'
expect git-bash Windows_NT MINGW64 dev         'KANDEV_DEBUG_DEV_MODE=true exec ./bin/kandev.exe __backend'
expect git-bash Windows_NT MINGW64 start-debug "KANDEV_DEBUG_PPROF_ENABLED=true KANDEV_LOG_LEVEL=debug KANDEV_WEB_TITLE_PREFIX='Debug' exec ./bin/kandev.exe __backend"
expect git-bash Windows_NT MINGW64 start-debug "KANDEV_DEBUG_PPROF_ENABLED=true KANDEV_LOG_LEVEL=debug KANDEV_WEB_TITLE_PREFIX='Preview' exec ./bin/kandev.exe __backend" __backend Preview
expect git-bash Windows_NT MINGW64 start-debug "KANDEV_DEBUG_PPROF_ENABLED=true KANDEV_LOG_LEVEL=debug KANDEV_WEB_TITLE_PREFIX='QA Debug' exec ./bin/kandev.exe __backend" __backend "QA Debug"

# deadcode — the analyzer is invoked through the same shell-specific env-prefix
# mechanism, but its command has no __backend marker, so match its module path.
# Git Bash additionally carries the CGO_LOCALE prefix (LC_ALL=C): deadcode compiles
# the -tags fts5 cgo packages, and this mingw gcc mis-decodes its own non-ASCII
# install path under a UTF-8 locale (see docs/public/windows-support.md). LC_ALL=C
# (not LANG=C) is used so it overrides an inherited LC_ALL/LC_CTYPE. Unix and
# native cmd stay locale-free.
expect unix '' '' deadcode \
	'GOTOOLCHAIN=go1.26.0 go run golang.org/x/tools/cmd/deadcode@v0.48.0 -tags fts5 ./...' \
	'golang.org/x/tools/cmd/deadcode'
expect windows-native Windows_NT '' deadcode \
	'set GOTOOLCHAIN=go1.26.0& go run golang.org/x/tools/cmd/deadcode@v0.48.0 -tags fts5 ./...' \
	'golang.org/x/tools/cmd/deadcode'
expect git-bash Windows_NT MINGW64 deadcode \
	'LC_ALL=C GOTOOLCHAIN=go1.26.0 go run golang.org/x/tools/cmd/deadcode@v0.48.0 -tags fts5 ./...' \
	'golang.org/x/tools/cmd/deadcode'

# test — a second CGO_LOCALE consumer, so the prefix is not proven for deadcode
# alone. The `test` recipe is deterministic (no version/timestamp ldflags), so
# assert it in full across all three shells: Git Bash carries LC_ALL=C, Unix and
# native cmd do not.
expect git-bash Windows_NT MINGW64 test \
	'LC_ALL=C CGO_ENABLED=1 go test -tags fts5 ./...' \
	'go test -tags fts5'
expect unix '' '' test \
	'CGO_ENABLED=1 go test -tags fts5 ./...' \
	'go test -tags fts5'
expect windows-native Windows_NT '' test \
	'set CGO_ENABLED=1& go test -tags fts5 ./...' \
	'go test -tags fts5'

# mkdir — the Windows branch must check for the directory before calling cmd's
# mkdir, so rebuilding over an existing bin/ stays quiet while other errors stay
# visible. Unix and Git Bash use idempotent `mkdir -p`, which is silent by design.
expect unix '' '' build-kandev 'mkdir -p "bin"' mkdir
expect windows-native Windows_NT '' build-kandev 'cmd /c if not exist "bin" mkdir "bin"' mkdir
expect git-bash Windows_NT MINGW64 build-kandev 'mkdir -p "bin"' mkdir

# When cmd.exe is available, execute the captured native-Windows recipe against
# a temporary directory. This checks that an existing directory stays quiet.
check_native_mkdir_behavior() {
	local recipe=$1 cmd_bin inner probe_name probe_dir stderr_file first_status
	if ! cmd_bin=$(command -v cmd.exe 2>/dev/null); then
		printf 'skip  %-16s cmd.exe is not available\n' "native-mkdir"
		return
	fi

	probe_name=".make-shell-check-$$"
	probe_dir="$backend_dir/$probe_name"
	stderr_file=$(mktemp)
	inner=${recipe#cmd /c }
	inner=${inner//bin/$probe_name}

	rm -rf "$probe_dir"
	first_status=0
	(cd "$backend_dir" && "$cmd_bin" /c "$inner") 2>"$stderr_file" || first_status=$?
	if [ "$first_status" -ne 0 ] || [ ! -d "$probe_dir" ] || [ -s "$stderr_file" ]; then
		printf 'FAIL  %-16s first mkdir did not create a quiet directory\n' "native-mkdir"
		status=1
		rm -rf "$probe_dir"
		rm -f "$stderr_file"
		return
	fi

	: >"$stderr_file"
	(cd "$backend_dir" && "$cmd_bin" /c "$inner") 2>"$stderr_file" || true
	if [ -s "$stderr_file" ]; then
		printf 'FAIL  %-16s existing directory wrote to stderr\n' "native-mkdir"
		status=1
	else
		printf 'ok    %-16s existing directory stayed quiet\n' "native-mkdir"
	fi

	rm -rf "$probe_dir"
	rm -f "$stderr_file"
}

if [ -n "$windows_mkdir_recipe" ]; then
	check_native_mkdir_behavior "$windows_mkdir_recipe"
fi

# Static guard: no literal /dev/null inside $(shell ...).
#
# Unlike a recipe, which only ever runs on a platform its target supports,
# $(shell ...) expands at parse time on EVERY invocation, through whatever shell
# make picked. Under native cmd there is no /dev/null: cmd resolves the
# redirection first, fails on the missing \dev directory, and hands the value to
# the fallback — so the probe silently returns its default on Windows while
# every POSIX host looks fine. Use $(NULL_REDIR), which carries the right token
# per shell. This runs on the Linux CI, where the Windows branch is otherwise
# never exercised.
# Fail closed: grep exits 1 for "no matches" but >1 for a real scan error, and
# swallowing the difference would let an unreadable or moved Makefile report ok.
grep_status=0
# `#` starts a comment anywhere in a Makefile line, so the match must not span
# one: [^#]* keeps a prose mention like `FOO = bar # $(shell x 2>/dev/null)`
# from failing CI, and cannot reach past a whole-line comment either. It matches
# empty so a bare `$(shell ...)` in column 0 still counts.
devnull_hits=$(grep -nE '^[[:space:]]*[^#]*\$\(shell[^#]*/dev/null' \
	"$backend_dir/../../Makefile" "$backend_dir/Makefile") || grep_status=$?
if [ "$grep_status" -gt 1 ]; then
	printf 'FAIL  %-16s unable to scan Makefiles (grep exit %s)\n' "static" "$grep_status"
	status=1
elif [ -n "$devnull_hits" ]; then
	printf 'FAIL  %-16s literal /dev/null inside $(shell ...) — use $(NULL_REDIR)\n' "static"
	printf '        %s\n' "$devnull_hits"
	status=1
else
	printf 'ok    %-16s %s\n' "static" 'no literal /dev/null inside $(shell ...)'
fi

# Static guard: native cgo host builds carry the $(CGO_LOCALE) prefix.
#
# kandev (-tags fts5) and agentctl both import cgo (go list -deps reports CgoFiles),
# so their native `go build` invokes gcc and fails on a non-ASCII toolchain path
# under Git Bash unless prefixed with $(CGO_LOCALE) — exactly the miss that first
# shipped build-agentctl without it. Cross builds set CGO_ENABLED=0 / GOOS= and do
# not invoke gcc, so they are excluded. Extend the cmd list here if another host
# binary starts importing cgo.
cgo_status=0
cgo_unprefixed=$(grep -nE '\$\(GO\) build' "$backend_dir/Makefile" \
	| grep -E '\./cmd/(kandev|agentctl)$' \
	| grep -vE 'CGO_LOCALE|GOOS=|\$\(GO_(LINUX|DARWIN)') || cgo_status=$?
if [ "$cgo_status" -gt 1 ]; then
	printf 'FAIL  %-16s unable to scan Makefile (grep exit %s)\n' "static-cgo" "$cgo_status"
	status=1
elif [ -n "$cgo_unprefixed" ]; then
	printf 'FAIL  %-16s native cgo build without $(CGO_LOCALE):\n' "static-cgo"
	printf '        %s\n' "$cgo_unprefixed"
	status=1
else
	printf 'ok    %-16s %s\n' "static-cgo" 'native cgo host builds carry $(CGO_LOCALE)'
fi

if [ "$status" -eq 0 ]; then
	echo "All Makefile shell-dispatch checks passed (run/dev/start-debug/deadcode/test/build-kandev across 3 shells, plus static guards)."
fi
exit "$status"
