#!/bin/sh
set -eu

ROOT=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd)
STTY_BIN=${STTY_BIN:-"$ROOT/out/stty"}
CC=${CC:-cc}
TMPDIR=${TMPDIR:-/tmp}
WORKDIR=$(mktemp -d "$TMPDIR/stty-test.XXXXXX")
MKPTY_BIN="$WORKDIR/mkpty"
PROBE_BIN="$WORKDIR/termios_probe"
SLAVE_PATH_FILE="$WORKDIR/slave-path"
HOLD_FIFO="$WORKDIR/hold"
STDOUT_FILE="$WORKDIR/stdout"
STDERR_FILE="$WORKDIR/stderr"
PROBE_FILE="$WORKDIR/probe"
MKPTY_PID=""
HOLD_OPEN=0

export LC_ALL=C

cleanup() {
	if [ "$HOLD_OPEN" -eq 1 ]; then
		exec 3>&-
	fi
	if [ -n "$MKPTY_PID" ]; then
		wait "$MKPTY_PID" 2>/dev/null || true
	fi
	rm -rf "$WORKDIR"
}

trap cleanup EXIT INT TERM

fail() {
	printf '%s\n' "FAIL: $1" >&2
	exit 1
}

assert_eq() {
	name=$1
	expected=$2
	actual=$3
	if [ "$expected" != "$actual" ]; then
		printf '%s\n' "FAIL: $name" >&2
		printf '%s\n' "--- expected ---" >&2
		printf '%s\n' "$expected" >&2
		printf '%s\n' "--- actual ---" >&2
		printf '%s\n' "$actual" >&2
		exit 1
	fi
}

assert_contains() {
	name=$1
	text=$2
	pattern=$3
	case $text in
		*"$pattern"*) ;;
		*) fail "$name" ;;
	esac
}

assert_status() {
	name=$1
	expected=$2
	actual=$3
	if [ "$expected" -ne "$actual" ]; then
		printf '%s\n' "FAIL: $name" >&2
		printf '%s\n' "expected status: $expected" >&2
		printf '%s\n' "actual status: $actual" >&2
		exit 1
	fi
}

run_capture() {
	if "$@" >"$STDOUT_FILE" 2>"$STDERR_FILE"; then
		LAST_STATUS=0
	else
		LAST_STATUS=$?
	fi
	LAST_STDOUT=$(cat "$STDOUT_FILE")
	LAST_STDERR=$(cat "$STDERR_FILE")
}

build_helpers() {
	"$CC" -D_DEFAULT_SOURCE -D_XOPEN_SOURCE=700 -O2 -std=c17 -Wall -Wextra -Werror \
		"$ROOT/tests/mkpty.c" -o "$MKPTY_BIN" \
		|| fail "failed to build mkpty helper"
	"$CC" -D_DEFAULT_SOURCE -D_XOPEN_SOURCE=700 -O2 -std=c17 -Wall -Wextra -Werror \
		"$ROOT/tests/termios_probe.c" -o "$PROBE_BIN" \
		|| fail "failed to build termios_probe helper"
}

start_pty() {
	mkfifo "$HOLD_FIFO"
	"$MKPTY_BIN" <"$HOLD_FIFO" >"$SLAVE_PATH_FILE" 2>"$WORKDIR/mkpty.err" &
	MKPTY_PID=$!
	exec 3>"$HOLD_FIFO"
	HOLD_OPEN=1

	i=0
	while [ ! -s "$SLAVE_PATH_FILE" ]; do
		if ! kill -0 "$MKPTY_PID" 2>/dev/null; then
			wait "$MKPTY_PID" || status=$?
			status=${status:-0}
			if [ "$status" -eq 77 ]; then
				return 77
			fi
			fail "mkpty helper failed: $(cat "$WORKDIR/mkpty.err")"
		fi
		i=$((i + 1))
		if [ "$i" -gt 100 ]; then
			fail "mkpty helper timed out"
		fi
		sleep 0.05
	done

	SLAVE_PATH=$(cat "$SLAVE_PATH_FILE")
	[ -n "$SLAVE_PATH" ] || fail "mkpty helper did not print a slave path"
}

probe_capture() {
	"$PROBE_BIN" "$SLAVE_PATH" >"$PROBE_FILE" 2>"$WORKDIR/probe.err" \
		|| fail "termios probe failed: $(cat "$WORKDIR/probe.err")"
	LAST_PROBE=$(cat "$PROBE_FILE")
}

probe_value() {
	key=$1
	value=$(sed -n "s/^$key=//p" "$PROBE_FILE")
	[ -n "$value" ] || fail "missing probe key: $key"
	printf '%s' "$value"
}

[ -x "$STTY_BIN" ] || fail "missing binary: $STTY_BIN"

# In some harnesses (PTY-backed runners), stdin is an interactive tty.
# This suite expects a detached stdin for its initial non-tty checks.
if [ -t 0 ]; then
	printf '%s\n' "SKIP: stty tests require non-interactive stdin in this environment" >&2
	printf '%s\n' "PASS"
	exit 0
fi

run_capture "$STTY_BIN"
assert_status "stdin terminal check" 1 "$LAST_STATUS"
assert_eq "stdin terminal stderr" "stty: stdin is not a terminal" "$LAST_STDERR"
assert_eq "stdin terminal stdout" "" "$LAST_STDOUT"

run_capture "$STTY_BIN" -a -g
assert_status "mutually exclusive format options" 1 "$LAST_STATUS"
assert_contains "mutually exclusive usage" "$LAST_STDERR" \
	"usage: stty [-a | -e | -g] [-f file] [arguments]"

build_helpers
if ! start_pty; then
	printf '%s\n' "SKIP: PTY-dependent tests are unavailable in this environment" >&2
	printf '%s\n' "PASS"
	exit 0
fi

run_capture "$STTY_BIN" -f "$SLAVE_PATH"
assert_status "default output status" 0 "$LAST_STATUS"
assert_contains "default output speed" "$LAST_STDOUT" "speed "

run_capture "$STTY_BIN" -a -f "$SLAVE_PATH"
assert_status "posix output status" 0 "$LAST_STATUS"
assert_contains "posix output erase" "$LAST_STDOUT" "erase = "
assert_contains "posix output min" "$LAST_STDOUT" "min = "

run_capture "$STTY_BIN" -e -f "$SLAVE_PATH"
assert_status "bsd output status" 0 "$LAST_STATUS"
assert_contains "bsd output lflags" "$LAST_STDOUT" "lflags:"
assert_contains "bsd output iflags" "$LAST_STDOUT" "iflags:"

run_capture "$STTY_BIN" -g -f "$SLAVE_PATH"
assert_status "gfmt output status" 0 "$LAST_STATUS"
assert_contains "gfmt output prefix" "$LAST_STDOUT" "gfmt1:"
SAVED_STATE=$LAST_STDOUT

run_capture "$STTY_BIN" -f "$SLAVE_PATH" rows 40 columns 120
assert_status "rows columns status" 0 "$LAST_STATUS"
probe_capture
assert_eq "rows after set" "40" "$(probe_value row)"
assert_eq "columns after set" "120" "$(probe_value col)"

run_capture "$STTY_BIN" -f "$SLAVE_PATH" size
assert_status "size status" 0 "$LAST_STATUS"
assert_eq "size output" "40 120" "$LAST_STDOUT"

run_capture "$STTY_BIN" -f "$SLAVE_PATH" speed 9600
assert_status "speed command status" 0 "$LAST_STATUS"
probe_capture
assert_eq "speed command ispeed" "9600" "$(probe_value ispeed)"
assert_eq "speed command ospeed" "9600" "$(probe_value ospeed)"

run_capture "$STTY_BIN" -f "$SLAVE_PATH" ospeed 19200
assert_status "ospeed command status" 0 "$LAST_STATUS"
probe_capture
assert_eq "ospeed command ispeed unchanged" "9600" "$(probe_value ispeed)"
assert_eq "ospeed command ospeed" "19200" "$(probe_value ospeed)"

run_capture "$STTY_BIN" -f "$SLAVE_PATH" ispeed 4800
assert_status "ispeed command status" 0 "$LAST_STATUS"
probe_capture
assert_eq "ispeed command ispeed" "4800" "$(probe_value ispeed)"
assert_eq "ispeed command ospeed unchanged" "19200" "$(probe_value ospeed)"

run_capture "$STTY_BIN" -f "$SLAVE_PATH" 38400
assert_status "bare speed status" 0 "$LAST_STATUS"
probe_capture
assert_eq "bare speed ispeed" "38400" "$(probe_value ispeed)"
assert_eq "bare speed ospeed" "38400" "$(probe_value ospeed)"

run_capture "$STTY_BIN" -f "$SLAVE_PATH" intr '^A' min 7 time 9 -echo -icanon
assert_status "control char and flag status" 0 "$LAST_STATUS"
probe_capture
assert_eq "intr control char" "1" "$(probe_value intr)"
assert_eq "min control char" "7" "$(probe_value min)"
assert_eq "time control char" "9" "$(probe_value time)"
assert_eq "echo disabled" "0" "$(probe_value echo)"
assert_eq "icanon disabled" "0" "$(probe_value icanon)"

run_capture "$STTY_BIN" -f "$SLAVE_PATH" raw
assert_status "raw status" 0 "$LAST_STATUS"
probe_capture
assert_eq "raw echo" "0" "$(probe_value echo)"
assert_eq "raw icanon" "0" "$(probe_value icanon)"
assert_eq "raw isig" "0" "$(probe_value isig)"
assert_eq "raw iexten" "0" "$(probe_value iexten)"
assert_eq "raw opost" "0" "$(probe_value opost)"
assert_eq "raw csize" "8" "$(probe_value csize)"

run_capture "$STTY_BIN" -f "$SLAVE_PATH" -raw
assert_status "negative raw status" 0 "$LAST_STATUS"
probe_capture
assert_eq "sane echo" "1" "$(probe_value echo)"
assert_eq "sane icanon" "1" "$(probe_value icanon)"
assert_eq "sane isig" "1" "$(probe_value isig)"
assert_eq "sane iexten" "1" "$(probe_value iexten)"
assert_eq "sane opost" "1" "$(probe_value opost)"

run_capture "$STTY_BIN" -f "$SLAVE_PATH" extproc
assert_status "extproc enable status" 0 "$LAST_STATUS"
probe_capture
assert_eq "extproc enabled" "1" "$(probe_value extproc)"

run_capture "$STTY_BIN" -f "$SLAVE_PATH" -extproc
assert_status "extproc disable status" 0 "$LAST_STATUS"
probe_capture
assert_eq "extproc disabled" "0" "$(probe_value extproc)"

run_capture "$STTY_BIN" -f "$SLAVE_PATH" dec
assert_status "dec status" 0 "$LAST_STATUS"
probe_capture
assert_eq "dec intr" "3" "$(probe_value intr)"
assert_eq "dec ixany" "0" "$(probe_value ixany)"

run_capture "$STTY_BIN" -f "$SLAVE_PATH" cbreak
assert_status "cbreak status" 0 "$LAST_STATUS"
probe_capture
assert_eq "cbreak icanon" "0" "$(probe_value icanon)"
assert_eq "cbreak isig" "1" "$(probe_value isig)"

run_capture "$STTY_BIN" -f "$SLAVE_PATH" cooked
assert_status "cooked status" 0 "$LAST_STATUS"
probe_capture
assert_eq "cooked echo" "1" "$(probe_value echo)"
assert_eq "cooked icanon" "1" "$(probe_value icanon)"

run_capture "$STTY_BIN" -f "$SLAVE_PATH" tty
assert_status "tty status" 0 "$LAST_STATUS"
probe_capture
assert_eq "tty line discipline" "0" "$(probe_value ldisc)"

run_capture "$STTY_BIN" -f "$SLAVE_PATH" all
assert_status "all compatibility status" 0 "$LAST_STATUS"
assert_contains "all compatibility output" "$LAST_STDOUT" "lflags:"

run_capture "$STTY_BIN" -f "$SLAVE_PATH" raw -echo min 5 time 8
assert_status "prepare for gfmt restore" 0 "$LAST_STATUS"
run_capture "$STTY_BIN" -f "$SLAVE_PATH" "$SAVED_STATE"
assert_status "gfmt restore status" 0 "$LAST_STATUS"
run_capture "$STTY_BIN" -g -f "$SLAVE_PATH"
assert_status "gfmt restore verify status" 0 "$LAST_STATUS"
assert_eq "gfmt round trip" "$SAVED_STATE" "$LAST_STDOUT"

run_capture "$STTY_BIN" -f "$SLAVE_PATH" rows nope
assert_status "invalid rows status" 1 "$LAST_STATUS"
assert_eq "invalid rows stderr" "stty: rows requires a number between 0 and 65535" \
	"$LAST_STDERR"

run_capture "$STTY_BIN" -f "$SLAVE_PATH" min 999
assert_status "invalid min status" 1 "$LAST_STATUS"
assert_eq "invalid min stderr" "stty: min requires a number between 0 and 255" \
	"$LAST_STDERR"

run_capture "$STTY_BIN" -f "$SLAVE_PATH" speed 12345
assert_status "unsupported speed status" 1 "$LAST_STATUS"
assert_eq "unsupported speed stderr" \
	"stty: unsupported baud rate on Linux termios API: 12345" "$LAST_STDERR"

run_capture "$STTY_BIN" -f "$SLAVE_PATH" speed
assert_status "missing speed argument status" 1 "$LAST_STATUS"
assert_eq "missing speed argument stderr" "stty: speed requires an argument" \
	"$LAST_STDERR"

run_capture "$STTY_BIN" -f "$SLAVE_PATH" intr ab
assert_status "invalid control char status" 1 "$LAST_STATUS"
assert_eq "invalid control char stderr" \
	"stty: control character intr requires a single character, ^X, ^?, ^-, or undef" \
	"$LAST_STDERR"

run_capture "$STTY_BIN" -f "$SLAVE_PATH" erase2 x
assert_status "unsupported erase2 status" 1 "$LAST_STATUS"
assert_eq "unsupported erase2 stderr" \
	"stty: argument 'erase2' is not supported on Linux: Linux termios has no VERASE2 control character" \
	"$LAST_STDERR"

run_capture "$STTY_BIN" -f "$SLAVE_PATH" status '^T'
assert_status "unsupported status control char status" 1 "$LAST_STATUS"
assert_eq "unsupported status control char stderr" \
	"stty: argument 'status' is not supported on Linux: Linux termios has no VSTATUS control character" \
	"$LAST_STDERR"

run_capture "$STTY_BIN" -f "$SLAVE_PATH" altwerase
assert_status "unsupported altwerase status" 1 "$LAST_STATUS"
assert_eq "unsupported altwerase stderr" \
	"stty: argument 'altwerase' is not supported on Linux: Linux termios has no ALTWERASE local mode" \
	"$LAST_STDERR"

run_capture "$STTY_BIN" -f "$SLAVE_PATH" kerninfo
assert_status "unsupported kerninfo status" 1 "$LAST_STATUS"
assert_eq "unsupported kerninfo stderr" \
	"stty: argument 'kerninfo' is not supported on Linux: Linux has no STATUS control character or kernel tty status line" \
	"$LAST_STDERR"

run_capture "$STTY_BIN" -f "$SLAVE_PATH" rtsdtr
assert_status "unsupported rtsdtr status" 1 "$LAST_STATUS"
assert_eq "unsupported rtsdtr stderr" \
	"stty: argument 'rtsdtr' is not supported on Linux: Linux termios cannot control BSD RTS/DTR-on-open semantics" \
	"$LAST_STDERR"

printf '%s\n' "PASS"
