#!/usr/bin/env bash
# host-on-sx conformance runner — loads the kernel stdlib, the subsystem
# libraries the host wires to, the host modules, and the host test suites in one
# sx_server process, then reports pass/fail per suite. Mirrors lib/dream's runner.
#
# Usage:
#   bash lib/host/conformance.sh            # run all suites
#   bash lib/host/conformance.sh sxtp       # run ONLY the sxtp suite (fast — skips
#                                           # the Datalog-heavy blog suite)
#   bash lib/host/conformance.sh blog -v    # one suite, verbose
#   bash lib/host/conformance.sh -v         # all suites, verbose

set -uo pipefail
cd "$(git rev-parse --show-toplevel)"

SX_SERVER="${SX_SERVER:-hosts/ocaml/_build/default/bin/sx_server.exe}"
if [ ! -x "$SX_SERVER" ]; then
  SX_SERVER="/root/rose-ash/hosts/ocaml/_build/default/bin/sx_server.exe"
fi
if [ ! -x "$SX_SERVER" ]; then
  echo "ERROR: sx_server.exe not found." >&2
  exit 1
fi

# Args: an optional suite NAME runs just that suite (fast); -v is verbose per-suite.
VERBOSE=""
SUITE_FILTER=""
for arg in "$@"; do
  case "$arg" in
    -v|--verbose) VERBOSE="-v" ;;
    *) SUITE_FILTER="$arg" ;;
  esac
done

# Kernel + subsystem dependencies, then the host modules. Order matters:
# stdlib/r7rs first; the Datalog engine + ACL subsystem (authorisation); the feed
# subsystem (the first migrated domain); Dream (types/json/auth/error/router) the
# host builds on; then the host layer itself.
MODULES=(
  "spec/stdlib.sx"
  "lib/r7rs.sx"
  "lib/apl/runtime.sx"
  "lib/datalog/tokenizer.sx"
  "lib/datalog/parser.sx"
  "lib/datalog/unify.sx"
  "lib/datalog/db.sx"
  "lib/datalog/builtins.sx"
  "lib/datalog/aggregates.sx"
  "lib/datalog/strata.sx"
  "lib/datalog/eval.sx"
  "lib/datalog/api.sx"
  "lib/datalog/magic.sx"
  "lib/acl/schema.sx"
  "lib/acl/facts.sx"
  "lib/acl/engine.sx"
  "lib/acl/explain.sx"
  "lib/acl/audit.sx"
  "lib/acl/federation.sx"
  "lib/acl/api.sx"
  "lib/relations/schema.sx"
  "lib/relations/engine.sx"
  "lib/relations/api.sx"
  "lib/relations/explain.sx"
  "lib/relations/federation.sx"
  "lib/relations/tree.sx"
  "lib/feed/normalize.sx"
  "lib/feed/stream.sx"
  "lib/feed/api.sx"
  "lib/persist/event.sx"
  "lib/persist/backend.sx"
  "lib/persist/log.sx"
  "lib/persist/kv.sx"
  "lib/persist/api.sx"
  "lib/persist/durable.sx"
  "spec/render.sx"
  "web/adapter-html.sx"
  "lib/dream/types.sx"
  "lib/dream/json.sx"
  "lib/dream/auth.sx"
  "lib/dream/error.sx"
  "lib/dream/form.sx"
  "lib/dream/session.sx"
  "lib/dream/router.sx"
  "lib/host/handler.sx"
  "lib/host/middleware.sx"
  "lib/host/session.sx"
  "lib/host/auth.sx"
  "lib/host/sxtp.sx"
  "lib/host/router.sx"
  "lib/host/static.sx"
  "lib/host/sx/relate-picker.sx"
  "lib/host/sx/kg-cards.sx"
  "lib/host/feed.sx"
  "lib/host/relations.sx"
  "lib/host/compose.sx"
  "lib/host/execute.sx"
  "lib/host/blog.sx"
  "lib/host/page.sx"
  "lib/host/server.sx"
  "lib/host/ledger.sx"
)

# Suites: NAME RUNNER-FN PATH
SUITES=(
  "handler     host-hd-tests-run!  lib/host/tests/handler.sx"
  "middleware  host-mw-tests-run!  lib/host/tests/middleware.sx"
  "sxtp        host-sx-tests-run!  lib/host/tests/sxtp.sx"
  "router      host-rt-tests-run!  lib/host/tests/router.sx"
  "feed        host-fd-tests-run!  lib/host/tests/feed.sx"
  "relations   host-rl-tests-run!  lib/host/tests/relations.sx"
  "blog        host-bl-tests-run!  lib/host/tests/blog.sx"
  "compose     host-cp-tests-run!  lib/host/tests/compose.sx"
  "execute     host-ex-tests-run!  lib/host/tests/execute.sx"
  "session     host-se-tests-run!  lib/host/tests/session.sx"
  "page        host-pg-tests-run!  lib/host/tests/page.sx"
  "server      host-sv-tests-run!  lib/host/tests/server.sx"
  "ledger      host-lg-tests-run!  lib/host/tests/ledger.sx"
)

# Filter to a single suite if a name was given (filter the array itself so its
# indices stay aligned with the result-parsing loop below). All MODULES still load
# — the host modules are interdependent; only the TEST suites are narrowed.
if [ -n "$SUITE_FILTER" ]; then
  _FILTERED=()
  for SUITE in "${SUITES[@]}"; do
    [ "$(echo "$SUITE" | awk '{print $1}')" = "$SUITE_FILTER" ] && _FILTERED+=("$SUITE")
  done
  if [ "${#_FILTERED[@]}" -eq 0 ]; then
    echo "ERROR: no suite named '$SUITE_FILTER'. Valid names:" >&2
    for SUITE in "${SUITES[@]}"; do echo "  $(echo "$SUITE" | awk '{print $1}')" >&2; done
    exit 1
  fi
  SUITES=("${_FILTERED[@]}")
fi

TMPFILE=$(mktemp); trap "rm -f $TMPFILE" EXIT
EPOCH=1
emit_load () { echo "(epoch $EPOCH)"; echo "(load \"$1\")"; EPOCH=$((EPOCH+1)); }
emit_eval () { echo "(epoch $EPOCH)"; echo "(eval \"$1\")"; EPOCH=$((EPOCH+1)); }

{
  for M in "${MODULES[@]}"; do emit_load "$M"; done
  for SUITE in "${SUITES[@]}"; do
    read -r _NAME _RUNNER FILE <<< "$SUITE"
    emit_load "$FILE"
    emit_eval "($_RUNNER)"
  done
} > "$TMPFILE"

# 1200s: the blog suite drives the relations graph hard (every is-a/types-of/
# instances-of query re-saturates the Datalog db), so it's CPU-bound and much slower
# under shared-box contention (a sibling loop at load ~6 pushed it past 600s -> false
# "no suite results parsed" truncation). Override with SX_CONF_TIMEOUT for a tighter cap.
OUTPUT=$(timeout "${SX_CONF_TIMEOUT:-1200}" "$SX_SERVER" < "$TMPFILE" 2>&1 || true)

# Fail LOUD on any load/eval error. A test file that errors mid-load silently
# truncates its suite — the runner returns only the tests that ran before the
# error, so the suite reports a false green (e.g. "blog 13 passed, 0 failed"
# when 16 CRUD tests never ran). Catch the error markers and abort before the
# pass/fail tally can hide them.
if echo "$OUTPUT" | grep -qE 'Undefined symbol|Unhandled exception|\[load\][^|]*[Ee]rror|expected list, got|: error '; then
  echo "FAIL: load/eval error detected — a suite may be silently truncated:" >&2
  echo "$OUTPUT" | grep -nE 'Undefined symbol|Unhandled exception|\[load\]|expected list, got|: error ' | head -20 >&2
  exit 1
fi

TOTAL_PASS=0
TOTAL_FAIL=0
FAILED_SUITES=()
LAST_DICT_LINES=$(echo "$OUTPUT" | grep -E '^\{:' || true)

I=0
while read -r LINE; do
  [ -z "$LINE" ] && continue
  P=$(echo "$LINE" | grep -oE ':passed [0-9]+' | awk '{print $2}')
  F=$(echo "$LINE" | grep -oE ':failed [0-9]+' | awk '{print $2}')
  [ -z "$P" ] && P=0
  [ -z "$F" ] && F=0
  SUITE_INFO="${SUITES[$I]}"
  SUITE_NAME=$(echo "$SUITE_INFO" | awk '{print $1}')
  TOTAL_PASS=$((TOTAL_PASS + P))
  TOTAL_FAIL=$((TOTAL_FAIL + F))
  if [ "$F" -gt 0 ]; then
    FAILED_SUITES+=("$SUITE_NAME: $P/$((P+F))")
    printf 'X %-12s %d/%d\n' "$SUITE_NAME" "$P" "$((P+F))"
    echo "$LINE" | grep -oE ':name "[^"]*"' | sed 's/:name /    fail: /'
  elif [ "$VERBOSE" = "-v" ]; then
    printf 'ok %-12s %d passed\n' "$SUITE_NAME" "$P"
  fi
  I=$((I+1))
done <<< "$LAST_DICT_LINES"

TOTAL=$((TOTAL_PASS + TOTAL_FAIL))
if [ "$TOTAL" -eq 0 ]; then
  echo "ERROR: no suite results parsed. Raw output:" >&2
  echo "$OUTPUT" >&2
  exit 1
fi
if [ $TOTAL_FAIL -eq 0 ]; then
  echo "ok $TOTAL_PASS/$TOTAL host-on-sx tests passed (${#SUITES[@]} suites)"
else
  echo "FAIL $TOTAL_PASS/$TOTAL passed, $TOTAL_FAIL failed:"
  for S in "${FAILED_SUITES[@]}"; do echo "  $S"; done
  exit 1
fi
