#!/usr/bin/env bash

# Every successful command and every expected failure is asserted explicitly.
set -o pipefail

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

SCRIPT_DIR="$(CDPATH= cd -- "$(dirname -- "$0")" && pwd -P)" \
  || fail "cannot resolve script directory"
cd "$SCRIPT_DIR" || fail "cannot enter script directory"

for option in JAVA_TOOL_OPTIONS JDK_JAVA_OPTIONS _JAVA_OPTIONS; do
  if test -n "${!option:-}"; then
    fail "$option must be unset so the declared JDK run cannot be altered"
  fi
done

test -n "${JAVA_HOME:-}" || fail "set JAVA_HOME to a complete Temurin JDK 25 or 17"
JAVA="$JAVA_HOME/bin/java"
JAVAC="$JAVA_HOME/bin/javac"
JAVAP="$JAVA_HOME/bin/javap"
for tool in "$JAVA" "$JAVAC" "$JAVAP"; do
  test -x "$tool" || fail "missing executable: $tool"
done

JAVA_VERSION="$("$JAVA" -version 2>&1)" || fail "java -version failed"
JAVAC_VERSION="$("$JAVAC" -version 2>&1)" || fail "javac -version failed"
grep -Eq 'Temurin-(25\.0\.4\+7|17\.0\.20\+8)' <<<"$JAVA_VERSION" \
  || fail "expected verified Temurin 25.0.4+7 or 17.0.20+8"
grep -Eq '^javac (25\.0\.4|17\.0\.20)$' <<<"$JAVAC_VERSION" \
  || fail "unexpected javac version"

TMP_BASE="$(CDPATH= cd -- "${TMPDIR:-/tmp}" && pwd -P)" \
  || fail "cannot resolve temporary directory"
LAB_OUT="$(mktemp -d "$TMP_BASE/classfile-linkage.XXXXXX")" \
  || fail "mktemp failed"
case "$LAB_OUT" in
  "$TMP_BASE"/classfile-linkage.*) ;;
  *) fail "unsafe temporary path: $LAB_OUT" ;;
esac
cleanup() {
  if ! rm -rf -- "$LAB_OUT"; then
    printf 'FAIL: cannot clean temporary directory: %s\n' "$LAB_OUT" >&2
    trap - EXIT
    exit 1
  fi
}
trap cleanup EXIT

for output in v1 v2-bad v2-fixed client-only init bytecode; do
  mkdir -p "$LAB_OUT/$output" || fail "cannot create output: $output"
done

"$JAVAC" --release 17 -g -Xlint:all -Werror -d "$LAB_OUT/bytecode" \
  bytecode/example/bytecode/BytecodeProbe.java || fail "bytecode compilation failed"
BYTECODE_OUTPUT="$("$JAVA" -cp "$LAB_OUT/bytecode" example.bytecode.BytecodeProbe)" \
  || fail "bytecode execution failed"
test "$BYTECODE_OUTPUT" = "$(printf '%s\n' 'total=43' 'adjust=42' 'operator=42' 'divide=-1')" \
  || fail "unexpected bytecode results"
"$JAVAP" -classpath "$LAB_OUT/bytecode" -p -v -c -s \
  example.bytecode.BytecodeProbe > "$LAB_OUT/bytecode.javap" || fail "javap bytecode failed"
for instruction in imul iadd idiv getfield invokespecial invokestatic invokevirtual invokeinterface invokedynamic; do
  grep -Eq "[[:space:]]$instruction[[:space:]]|[[:space:]]$instruction$" "$LAB_OUT/bytecode.javap" \
    || fail "missing instruction: $instruction"
done
grep -Fq 'Exception table:' "$LAB_OUT/bytecode.javap" || fail "missing exception table"
grep -Fq 'BootstrapMethods:' "$LAB_OUT/bytecode.javap" || fail "missing bootstrap methods"
printf '%s\n' "$BYTECODE_OUTPUT"

"$JAVAC" --release 17 -Xlint:all -Werror \
  -d "$LAB_OUT/v1" api-v1/example/api/Pricing.java \
  || fail "v1 API compilation failed"
"$JAVAC" --release 17 -Xlint:all -Werror \
  -d "$LAB_OUT/v2-bad" api-v2-bad/example/api/Pricing.java \
  || fail "v2-bad API compilation failed"
"$JAVAC" --release 17 -Xlint:all -Werror \
  -d "$LAB_OUT/v2-fixed" api-v2-fixed/example/api/Pricing.java \
  || fail "v2-fixed API compilation failed"

"$JAVAC" --release 17 -Xlint:all -Werror -cp "$LAB_OUT/v1" \
  -d "$LAB_OUT/client-only" client/example/client/OrderClient.java \
  || fail "client compilation against v1 failed"
CLIENT_CLASS="$LAB_OUT/client-only/example/client/OrderClient.class"
test -f "$CLIENT_CLASS" || fail "client class was not generated"

for runtime in v1 v2-bad v2-fixed; do
  mkdir -p "$LAB_OUT/$runtime/example/client" \
    || fail "cannot create client package in $runtime"
  cp "$CLIENT_CLASS" "$LAB_OUT/$runtime/example/client/OrderClient.class" \
    || fail "cannot reuse old client in $runtime"
  cmp -s "$CLIENT_CLASS" "$LAB_OUT/$runtime/example/client/OrderClient.class" \
    || fail "old client bytes changed in $runtime"
done

V1_SPECIAL="$("$JAVA" -cp "$LAB_OUT/v1" example.client.OrderClient special)" \
  || fail "old client did not run against v1"
grep -Fqx 'special=103' <<<"$V1_SPECIAL" || fail "unexpected v1 result"

BAD_REGULAR="$("$JAVA" -cp "$LAB_OUT/v2-bad" example.client.OrderClient regular)" \
  || fail "regular branch did not run against v2-bad"
grep -Fqx 'regular=accepted-without-pricing-call' <<<"$BAD_REGULAR" \
  || fail "unexpected regular-branch result"

if "$JAVA" -cp "$LAB_OUT/v2-bad" example.client.OrderClient special \
  >"$LAB_OUT/v2-bad.out" 2>"$LAB_OUT/v2-bad.err"; then
  fail "v2-bad unexpectedly satisfied the old descriptor"
fi
grep -Eq '^Exception in thread "main" java\.lang\.NoSuchMethodError:' \
  "$LAB_OUT/v2-bad.err" || fail "top-level failure is not NoSuchMethodError"
grep -Fq "'long example.api.Pricing.invoice(long, java.lang.String)'" \
  "$LAB_OUT/v2-bad.err" || fail "NoSuchMethodError does not name the old contract"
BAD_FIRST_FRAME="$(grep -m1 -E '^[[:space:]]+at ' "$LAB_OUT/v2-bad.err" \
  | sed 's/^[[:space:]]*//')" || fail "NoSuchMethodError has no frame"
case "$BAD_FIRST_FRAME" in
  'at example.client.OrderClient.specialOrder('*) ;;
  *) fail "first frame is not OrderClient.specialOrder: $BAD_FIRST_FRAME" ;;
esac

FIXED_SPECIAL="$("$JAVA" -cp "$LAB_OUT/v2-fixed" \
  example.client.OrderClient special)" || fail "v2-fixed did not restore compatibility"
grep -Fqx 'special=103' <<<"$FIXED_SPECIAL" || fail "unexpected v2-fixed result"

"$JAVAP" -classpath "$LAB_OUT/client-only" -v -c -p -s \
  example.client.OrderClient >"$LAB_OUT/client.javap" \
  || fail "javap client failed"
for runtime in v1 v2-bad v2-fixed; do
  "$JAVAP" -classpath "$LAB_OUT/$runtime" -p -s example.api.Pricing \
    >"$LAB_OUT/$runtime.javap" || fail "javap $runtime failed"
done

grep -Fq 'major version: 61' "$LAB_OUT/client.javap" \
  || fail "client class is not Java 17 classfile version 61"
OLD_REF='example/api/Pricing.invoice:(JLjava/lang/String;)J'
grep -Fq "$OLD_REF" "$LAB_OUT/client.javap" \
  || fail "caller constant pool does not contain the old Methodref"
awk '
  /^[[:space:]]+private static long specialOrder\(\);$/ { capture = 1 }
  capture { print }
  capture && /^[[:space:]]*$/ { exit }
' "$LAB_OUT/client.javap" >"$LAB_OUT/special.block"
test -s "$LAB_OUT/special.block" || fail "cannot isolate specialOrder method"
grep -Eq 'invokestatic[[:space:]]+#[0-9]+[[:space:]]+// Method example/api/Pricing\.invoice:\(JLjava/lang/String;\)J' \
  "$LAB_OUT/special.block" || fail "specialOrder does not invoke the old Methodref"
grep -Fq 'StackMapTable:' "$LAB_OUT/client.javap" \
  || fail "client control flow has no StackMapTable evidence"

OLD_DESCRIPTOR='descriptor: (JLjava/lang/String;)J'
NEW_DESCRIPTOR='descriptor: (ILjava/lang/String;)J'
grep -Fq "$OLD_DESCRIPTOR" "$LAB_OUT/v1.javap" \
  || fail "v1 is missing the old descriptor"
if grep -Fq "$OLD_DESCRIPTOR" "$LAB_OUT/v2-bad.javap"; then
  fail "v2-bad unexpectedly retains the old descriptor"
fi
grep -Fq "$NEW_DESCRIPTOR" "$LAB_OUT/v2-bad.javap" \
  || fail "v2-bad is missing its new descriptor"
grep -Fq "$OLD_DESCRIPTOR" "$LAB_OUT/v2-fixed.javap" \
  || fail "v2-fixed did not restore the old descriptor"
grep -Fq "$NEW_DESCRIPTOR" "$LAB_OUT/v2-fixed.javap" \
  || fail "v2-fixed is missing the new overload"

"$JAVAC" --release 17 -Xlint:all -Werror -d "$LAB_OUT/init" \
  init/example/init/InitializationProbe.java \
  || fail "initialization probe compilation failed"
"$JAVAP" -classpath "$LAB_OUT/init" -v -c -p \
  'example.init.InitializationProbe$Constants' >"$LAB_OUT/constants.javap" \
  || fail "javap constants class failed"
grep -Fq 'ConstantValue: int 7' "$LAB_OUT/constants.javap" \
  || fail "compile-time constant has no ConstantValue attribute"
grep -Eq 'putstatic[[:space:]]+#[0-9]+[[:space:]]+// Field RUNTIME:I' \
  "$LAB_OUT/constants.javap" \
  || fail "runtime field is not assigned by the class initializer"
if grep -Eq 'putstatic.*// Field COMPILE_TIME:I' "$LAB_OUT/constants.javap"; then
  fail "compile-time constant was unexpectedly assigned with putstatic"
fi
CONSTANTS="$("$JAVA" -cp "$LAB_OUT/init" example.init.InitializationProbe constants \
  | tr -d '\r')" \
  || fail "constants probe failed"
EXPECTED_CONSTANTS="$(printf '%s\n' \
  'compile-time=7' \
  'before-runtime' \
  'constants-clinit=entered' \
  'runtime=11')"
test "$CONSTANTS" = "$EXPECTED_CONSTANTS" \
  || fail "compile-time constant and runtime field did not show the expected init order"

FAILURE="$("$JAVA" -cp "$LAB_OUT/init" example.init.InitializationProbe failure \
  | tr -d '\r')" \
  || fail "failure probe process did not complete"
EXPECTED_FAILURE="$(printf '%s\n' \
  'broken-clinit=entered' \
  'attempt=1 error=ExceptionInInitializerError cause=IllegalStateException' \
  'attempt=2 error=NoClassDefFoundError')"
test "$FAILURE" = "$EXPECTED_FAILURE" \
  || fail "initialization failure was not remembered as expected"
test "$(grep -Fc 'broken-clinit=entered' <<<"$FAILURE")" -eq 1 \
  || fail "broken initializer ran more than once"

printf '%s\n' \
  'classfile-major=61' \
  'caller-methodref=Pricing.invoice:(JLjava/lang/String;)J' \
  'v2-bad-regular=passes-before-symbol-use' \
  'v2-bad-special=NoSuchMethodError-at-specialOrder' \
  'v2-fixed-old-client=passes-byte-identical' \
  'compile-time-constant=does-not-trigger-owner-init' \
  'runtime-static-field=triggers-owner-init' \
  'init-first-failure=ExceptionInInitializerError' \
  'init-second-failure=NoClassDefFoundError' \
  'PASS'
