#!/usr/bin/env bash

# This script intentionally avoids `set -e`: every successful command and every
# expected failure is asserted explicitly, so the teaching path is visible.

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"

test -n "${JDK17_HOME:-}" || fail "set JDK17_HOME to a JDK 17 root"
JAVA="$JDK17_HOME/bin/java"
JAVAC="$JDK17_HOME/bin/javac"
JAVAP="$JDK17_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"
JAVAP_VERSION="$("$JAVAP" -version 2>&1)" || fail "javap -version failed"
grep -Eq 'version "17([.+-][^"]*)?"' <<<"$JAVA_VERSION" \
  || fail "java is not Java 17"
grep -Eq '^javac 17([.+-].*)?$' <<<"$JAVAC_VERSION" \
  || fail "javac is not Java 17"
grep -Eq '^17([.+-].*)?$' <<<"$JAVAP_VERSION" \
  || fail "javap is not Java 17"
printf '%s\n' "$JAVA_VERSION" "$JAVAC_VERSION" "javap $JAVAP_VERSION"

TMP_BASE="$(CDPATH= cd -- "${TMPDIR:-/tmp}" && pwd -P)" \
  || fail "cannot resolve temporary directory"
LAB_OUT="$(mktemp -d "$TMP_BASE/generics-api-evolution.XXXXXX")" \
  || fail "mktemp failed"
case "$LAB_OUT" in
  "$TMP_BASE"/generics-api-evolution.*) ;;
  *) 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-run client-only v2-bad-run v2-fixed-run \
  old-source-v2-bad old-source-v2-fixed \
  new-source-v2-bad new-source-v2-fixed bridge-run; do
  mkdir -p "$LAB_OUT/$output" || fail "cannot create $output"
done

"$JAVAC" --release 17 -Xlint:all -Werror \
  -d "$LAB_OUT/v1-run" sdk-v1/example/sdk/OrderSdk.java \
  || fail "v1 SDK compilation failed"

"$JAVAC" --release 17 -Xlint:all -XDrawDiagnostics \
  -cp "$LAB_OUT/v1-run" -d "$LAB_OUT/client-only" \
  client-v1/example/client/OldClient.java \
  >"$LAB_OUT/client-v1.lint" 2>&1 \
  || fail "old client compilation failed"
grep -Fq 'compiler.warn.raw.class.use: java.util.List, java.util.List<E>' \
  "$LAB_OUT/client-v1.lint" \
  || fail "old client did not preserve its raw List variable"
grep -Fq 'compiler.warn.prob.found.req: (compiler.misc.unchecked.assign)' \
  "$LAB_OUT/client-v1.lint" \
  || fail "old client did not expose the raw List unchecked conversion"

CLIENT_CLASS="$LAB_OUT/client-only/example/client/OldClient.class"
test -f "$CLIENT_CLASS" || fail "old client class was not generated"

mkdir -p "$LAB_OUT/v1-run/example/client" \
  || fail "cannot create v1 client package"
cp "$CLIENT_CLASS" "$LAB_OUT/v1-run/example/client/OldClient.class" \
  || fail "cannot assemble v1 runtime"
V1_VALID="$("$JAVA" -cp "$LAB_OUT/v1-run" example.client.OldClient valid)" \
  || fail "old client did not run against v1"
grep -Fqx 'client-dataset=valid ids=order-17 count=1' <<<"$V1_VALID" \
  || fail "unexpected v1 valid output"

"$JAVAC" --release 17 -Xlint:all -Werror \
  -d "$LAB_OUT/v2-bad-run" sdk-v2-bad/example/sdk/OrderSdk.java \
  || fail "v2-bad SDK compilation failed"
"$JAVAC" --release 17 -Xlint:all -Werror \
  -d "$LAB_OUT/v2-fixed-run" sdk-v2-fixed/example/sdk/OrderSdk.java \
  || fail "v2-fixed SDK compilation failed"

for version in v2-bad v2-fixed; do
  "$JAVAC" --release 17 -Xlint:all -XDrawDiagnostics \
    -cp "$LAB_OUT/${version}-run" \
    -d "$LAB_OUT/old-source-${version}" \
    client-v1/example/client/OldClient.java \
    >"$LAB_OUT/old-source-${version}.lint" 2>&1 \
    || fail "old source did not compile against $version"
  grep -Fq 'compiler.warn.raw.class.use: java.util.List, java.util.List<E>' \
    "$LAB_OUT/old-source-${version}.lint" \
    || fail "old source lost its raw List warning against $version"
  grep -Fq 'compiler.warn.prob.found.req: (compiler.misc.unchecked.assign)' \
    "$LAB_OUT/old-source-${version}.lint" \
    || fail "old source lost its unchecked warning against $version"

  "$JAVAC" --release 17 -Xlint:all -Werror \
    -cp "$LAB_OUT/${version}-run" \
    -d "$LAB_OUT/new-source-${version}" \
    client-v2/example/client/NewClient.java \
    || fail "new source did not compile strictly against $version"
done

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

"$JAVAP" -classpath "$LAB_OUT/v1-run" -v -p -s example.sdk.OrderSdk \
  >"$LAB_OUT/v1.javap" || fail "javap v1 failed"
"$JAVAP" -classpath "$LAB_OUT/v2-bad-run" -v -p -s example.sdk.OrderSdk \
  >"$LAB_OUT/v2-bad.javap" || fail "javap v2-bad failed"
"$JAVAP" -classpath "$LAB_OUT/v2-fixed-run" -v -p -s example.sdk.OrderSdk \
  >"$LAB_OUT/v2-fixed.javap" || fail "javap v2-fixed failed"

extract_load_block() {
  awk '
    /^[[:space:]]+public static .* load\(java.lang.String\);$/ { capture = 1 }
    capture { print }
    capture && /^[[:space:]]*$/ { exit }
  ' "$1"
}

for version in v1 v2-bad v2-fixed; do
  extract_load_block "$LAB_OUT/$version.javap" >"$LAB_OUT/$version.load"
  test -s "$LAB_OUT/$version.load" \
    || fail "cannot isolate public load method in $version"
done

DESCRIPTOR='descriptor: (Ljava/lang/String;)Ljava/util/List;'
GENERIC_SIGNATURE='(Ljava/lang/String;)Ljava/util/List<Lexample/sdk/OrderSdk$Order;>;'
for block in v1.load v2-bad.load v2-fixed.load; do
  grep -Fq "$DESCRIPTOR" "$LAB_OUT/$block" \
    || fail "$block does not contain the stable load descriptor"
done
grep -Fq 'public static java.util.List load(java.lang.String);' \
  "$LAB_OUT/v1.load" \
  || fail "v1 load is not exposed as a raw List"
if grep -Fq 'Signature:' "$LAB_OUT/v1.load"; then
  fail "raw v1 load unexpectedly contains the v2 generic Signature"
fi
grep -Fq "$GENERIC_SIGNATURE" "$LAB_OUT/v2-bad.load" \
  || fail "v2-bad generic Signature is missing"
grep -Fq "$GENERIC_SIGNATURE" "$LAB_OUT/v2-fixed.load" \
  || fail "v2-fixed generic Signature is missing"

V2_BAD_VALID="$("$JAVA" -cp "$LAB_OUT/v2-bad-run" \
  example.client.OldClient valid)" \
  || fail "old client did not run against v2-bad valid data"
grep -Fqx 'client-dataset=valid ids=order-17 count=1' <<<"$V2_BAD_VALID" \
  || fail "unexpected v2-bad valid output"

if "$JAVA" -cp "$LAB_OUT/v2-bad-run" example.client.OldClient bad \
  >"$LAB_OUT/v2-bad-history.out" 2>"$LAB_OUT/v2-bad-history.err"; then
  fail "v2-bad unexpectedly accepted bad historical data"
fi
grep -Eq '^Exception in thread "main" java\.lang\.ClassCastException:' \
  "$LAB_OUT/v2-bad-history.err" \
  || fail "v2-bad top-level exception is not ClassCastException"
BAD_FIRST_FRAME="$(grep -m1 -E '^[[:space:]]+at ' \
  "$LAB_OUT/v2-bad-history.err" | sed 's/^[[:space:]]*//')" \
  || fail "v2-bad stack trace has no frame"
case "$BAD_FIRST_FRAME" in
  'at example.client.OldClient.main('*) ;;
  *) fail "v2-bad first frame is not OldClient.main: $BAD_FIRST_FRAME" ;;
esac

if "$JAVA" -cp "$LAB_OUT/v2-fixed-run" example.client.OldClient bad \
  >"$LAB_OUT/v2-fixed-history.out" 2>"$LAB_OUT/v2-fixed-history.err"; then
  fail "v2-fixed unexpectedly accepted bad historical data"
fi
grep -Fxq 'Exception in thread "main" java.lang.IllegalArgumentException: history[1] expected Order but found java.lang.String' \
  "$LAB_OUT/v2-fixed-history.err" \
  || fail "v2-fixed top-level exception did not identify the invalid historical element"
FIXED_FIRST_FRAME="$(grep -m1 -E '^[[:space:]]+at ' \
  "$LAB_OUT/v2-fixed-history.err" | sed 's/^[[:space:]]*//')" \
  || fail "v2-fixed stack trace has no frame"
case "$FIXED_FIRST_FRAME" in
  'at example.sdk.OrderSdk.load('*) ;;
  *) fail "v2-fixed first frame is not OrderSdk.load: $FIXED_FIRST_FRAME" ;;
esac
if grep -Fq 'ClassCastException' "$LAB_OUT/v2-fixed-history.err"; then
  fail "v2-fixed leaked bad data to the client cast"
fi

V2_FIXED_VALID="$("$JAVA" -cp "$LAB_OUT/v2-fixed-run" \
  example.client.OldClient valid)" \
  || fail "old client did not run against v2-fixed valid data"
grep -Fqx 'client-dataset=valid ids=order-17 count=1' <<<"$V2_FIXED_VALID" \
  || fail "unexpected v2-fixed valid output"

NEW_FIXED_VALID="$("$JAVA" \
  -cp "$LAB_OUT/v2-fixed-run:$LAB_OUT/new-source-v2-fixed" \
  example.client.NewClient valid)" \
  || fail "new client did not run against v2-fixed valid data"
grep -Fqx 'new-client-dataset=valid ids=order-17 count=1' \
  <<<"$NEW_FIXED_VALID" || fail "unexpected new client valid output"

"$JAVAC" --release 17 -Xlint:all -Werror \
  -d "$LAB_OUT/bridge-run" bridge/example/bridge/BridgeProbe.java \
  || fail "bridge probe compilation failed"
BRIDGE_OUTPUT="$("$JAVA" -cp "$LAB_OUT/bridge-run" \
  example.bridge.BridgeProbe)" || fail "bridge probe run failed"
grep -Fqx 'bridge=ok' <<<"$BRIDGE_OUTPUT" \
  || fail "unexpected bridge probe output"
"$JAVAP" -classpath "$LAB_OUT/bridge-run" -v -p -s \
  'example.bridge.BridgeProbe$StringNode' >"$LAB_OUT/bridge.javap" \
  || fail "javap bridge probe failed"
grep -Fq 'descriptor: ()Ljava/lang/String;' "$LAB_OUT/bridge.javap" \
  || fail "String-returning implementation is missing"
grep -Fq 'descriptor: ()Ljava/lang/Object;' "$LAB_OUT/bridge.javap" \
  || fail "Object-returning bridge descriptor is missing"
grep -Fq 'ACC_BRIDGE, ACC_SYNTHETIC' "$LAB_OUT/bridge.javap" \
  || fail "synthetic bridge flags are missing"

printf '%s\n' \
  'descriptor-stable=true' \
  'v1-signature=raw-list' \
  'v2-bad-signature=list-of-order' \
  'old-source-v2-bad=compiles-with-unchecked' \
  'old-source-v2-fixed=compiles-with-unchecked' \
  'new-source-v2-bad=strict-compile' \
  'new-source-v2-fixed=strict-compile' \
  'old-client-v2-bad-valid=true' \
  'v2-bad-history=ClassCastException-at-old-client' \
  'v2-fixed-history=rejected-at-sdk-boundary' \
  'old-client-v2-fixed-valid=true' \
  'new-client-v2-fixed-valid=true' \
  'old-client-byte-identical=true' \
  'bridge=ACC_BRIDGE+ACC_SYNTHETIC' \
  'PASS'
