#!/usr/bin/env bash

# Every success 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 "${JDK17_HOME:-}" || fail "set JDK17_HOME to Temurin 17.0.20+8"
JAVA="$JDK17_HOME/bin/java"
JAVAC="$JDK17_HOME/bin/javac"
JAR="$JDK17_HOME/bin/jar"
for tool in "$JAVA" "$JAVAC" "$JAR"; 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 -Fq 'Temurin-17.0.20+8' <<<"$JAVA_VERSION" \
  || fail "java is not Temurin 17.0.20+8"
grep -Fq 'javac 17.0.20' <<<"$JAVAC_VERSION" \
  || fail "javac is not 17.0.20"

TMP_BASE="$(CDPATH= cd -- "${TMPDIR:-/tmp}" && pwd -P)" \
  || fail "cannot resolve temporary directory"
LAB_OUT="$(mktemp -d "$TMP_BASE/classloader-boundaries.XXXXXX")" \
  || fail "mktemp failed"
case "$LAB_OUT" in
  "$TMP_BASE"/classloader-boundaries.*) ;;
  *) 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

mkdir -p \
  "$LAB_OUT/platform/example/plugin" \
  "$LAB_OUT/plugin/example/plugin" \
  "$LAB_OUT/plugin/META-INF/services" \
  "$LAB_OUT/host" \
  || fail "cannot create compilation outputs"

"$JAVAC" --release 17 -Xlint:all -Werror -d "$LAB_OUT/platform" \
  platform/example/plugin/api/Greeting.java \
  platform/example/plugin/privateimpl/Version.java \
  || fail "platform compilation failed"
cp resources/platform/example/plugin/config.txt \
  "$LAB_OUT/platform/example/plugin/config.txt" \
  || fail "cannot copy platform resource"

"$JAVAC" --release 17 -Xlint:all -Werror -cp "$LAB_OUT/platform" \
  -d "$LAB_OUT/plugin" \
  plugin/example/plugin/privateimpl/Version.java \
  plugin/example/plugin/impl/GreetingPlugin.java \
  || fail "plugin compilation failed"
cp resources/plugin/example/plugin/config.txt \
  "$LAB_OUT/plugin/example/plugin/config.txt" \
  || fail "cannot copy plugin resource"
cp resources/plugin/META-INF/services/example.plugin.api.Greeting \
  "$LAB_OUT/plugin/META-INF/services/example.plugin.api.Greeting" \
  || fail "cannot copy ServiceLoader descriptor"

PLUGIN_JAR="$LAB_OUT/plugin.jar"
"$JAR" --create --file "$PLUGIN_JAR" -C "$LAB_OUT/plugin" . \
  || fail "plugin JAR creation failed"
"$JAR" --list --file "$PLUGIN_JAR" >"$LAB_OUT/plugin.entries" \
  || fail "cannot list plugin JAR"
grep -Fxq 'example/plugin/impl/GreetingPlugin.class' "$LAB_OUT/plugin.entries" \
  || fail "plugin implementation is missing"
grep -Fxq 'example/plugin/privateimpl/Version.class' "$LAB_OUT/plugin.entries" \
  || fail "plugin private dependency is missing"
grep -Fxq 'META-INF/services/example.plugin.api.Greeting' "$LAB_OUT/plugin.entries" \
  || fail "ServiceLoader descriptor is missing"
if grep -Fxq 'example/plugin/api/Greeting.class' "$LAB_OUT/plugin.entries"; then
  fail "plugin JAR must not carry the shared API"
fi

"$JAVAC" --release 17 -Xlint:all -Werror -cp "$LAB_OUT/platform" \
  -d "$LAB_OUT/host" \
  host/example/host/SelectiveChildFirstLoader.java \
  host/example/host/ContextLoaderScope.java \
  host/example/host/PluginBoundaryDemo.java \
  || fail "host compilation failed"

CLASSPATH_VALUE="$LAB_OUT/platform:$LAB_OUT/host"
run_mode() {
  "$JAVA" -cp "$CLASSPATH_VALUE" example.host.PluginBoundaryDemo "$1" "$PLUGIN_JAR" \
    | tr -d '\r'
}

IDENTITY="$(run_mode identity)" || fail "identity mode failed"
EXPECTED_IDENTITY="$(printf '%s\n' \
  'same-byte-hash=true' \
  'same-binary-name=true' \
  'same-runtime-class=false' \
  'shared-contract=true' \
  'cross-cast=ClassCastException')"
test "$IDENTITY" = "$EXPECTED_IDENTITY" || fail "unexpected identity output"

DELEGATION="$(run_mode delegation)" || fail "delegation mode failed"
EXPECTED_DELEGATION="$(printf '%s\n' \
  'parent-first-private-copy=platform-copy' \
  'selective-child-first-private-copy=plugin-private' \
  'shared-api-defining-loader=application' \
  'parallel-capable-registered=true' \
  'concurrent-first-definition=one-Class' \
  'resource-policy=parent-first-independent')"
test "$DELEGATION" = "$EXPECTED_DELEGATION" \
  || fail "unexpected delegation output"

TCCL="$(run_mode tccl)" || fail "TCCL mode failed"
EXPECTED_TCCL="$(printf '%s\n' \
  'default-provider-count=0' \
  'scoped-provider=plugin-private' \
  'tccl-restored-after-failure=true' \
  'cross-thread-close=rejected' \
  'tccl-restored-after-owner-close=true')"
test "$TCCL" = "$EXPECTED_TCCL" || fail "unexpected TCCL output"

LIFECYCLE="$("$JAVA" -Xint -Xmx64m -XX:+UseSerialGC \
  -cp "$CLASSPATH_VALUE" example.host.PluginBoundaryDemo lifecycle "$PLUGIN_JAR" \
  | tr -d '\r')" || fail "lifecycle mode failed"
test "$(wc -l <<<"$LIFECYCLE" | tr -d ' ')" -eq 4 \
  || fail "unexpected lifecycle line count"
grep -Fqx 'loader-closed=true' <<<"$LIFECYCLE" \
  || fail "loader close was not observed"
grep -Fqx 'parent-cache-retains-loader=true' <<<"$LIFECYCLE" \
  || fail "parent cache retention was not proven"
grep -Fqx 'parent-cache-cleared=true' <<<"$LIFECYCLE" \
  || fail "parent cache was not cleared"
grep -Eq '^weak-clear-observed=(true|false)$' <<<"$LIFECYCLE" \
  || fail "weak observation is malformed"
WEAK_OBSERVED="$(sed -n 's/^weak-clear-observed=//p' <<<"$LIFECYCLE")"

printf '%s\n' \
  'identity=same-bytes+different-defining-loaders' \
  'delegation=shared-api-parent+private-local' \
  'resource-lookup=independent-parent-first' \
  'concurrent-first-definition=one-Class' \
  'tccl=scoped+restored-on-failure' \
  'lifecycle=closed+parent-cache-cleared' \
  "weak-clear-observed=$WEAK_OBSERVED" \
  'PASS'
