#!/usr/bin/env bash
# next/tests/http_marshal.sh — Step 8b-start unit test for the
# dict↔proplist marshaling helpers added to lib/erlang/runtime.sx.
#
# Exercises:
#   er-request-dict-to-proplist  — http-listen request dict shape
#   er-of-sx-deep                — recursive marshaling
#   er-dict-to-header-proplist   — headers (binary keys)
#   er-proplist-to-dict          — handler-response inverse
#   er-to-sx-deep                — recursive marshaling on the way out
#
# These helpers underpin the http_server:start/1 process so an
# Erlang route/1 handler can pattern-match on a real proplist
# instead of an opaque SX dict.

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

VERBOSE="${1:-}"
PASS=0; FAIL=0; ERRORS=""
TMPFILE=$(mktemp); trap "rm -f $TMPFILE" EXIT

cat > "$TMPFILE" <<'EPOCHS'
(epoch 1)
(load "lib/erlang/tokenizer.sx")
(load "lib/erlang/parser.sx")
(load "lib/erlang/parser-core.sx")
(load "lib/erlang/parser-expr.sx")
(load "lib/erlang/parser-module.sx")
(load "lib/erlang/transpile.sx")
(load "lib/erlang/runtime.sx")
(load "lib/erlang/vm/dispatcher.sx")

;; Local helper: walk an Erlang proplist (cons of {Key, Value}) and
;; return the value for the first matching key. Key can be an atom
;; name (string) or a binary as bytes-list.
(epoch 9)
(eval "(define test-pl-find (fn (pl key-name) (cond (er-nil? pl) nil (er-cons? pl) (let ((head (get pl :head))) (cond (er-tuple? head) (let ((kv (get head :elements))) (cond (and (er-atom? (nth kv 0)) (= (get (nth kv 0) :name) key-name)) (nth kv 1) :else (test-pl-find (get pl :tail) key-name))) :else (test-pl-find (get pl :tail) key-name))) :else nil)))")

;; --- helpers exist ---
(epoch 10)
(eval "(if (= (type-of er-request-dict-to-proplist) \"lambda\") 'ok 'missing)")
(epoch 11)
(eval "(if (= (type-of er-proplist-to-dict) \"lambda\") 'ok 'missing)")

;; --- request dict -> proplist with atom keys + binary values ---
(epoch 20)
(eval "(let ((d (dict :method \"GET\" :path \"/foo\" :query \"\" :headers (dict) :body \"\"))) (let ((pl (er-request-dict-to-proplist d))) (er-cons? pl)))")

;; method maps to atom 'method' with binary value <<"GET">> — verify via SX-side proplist walker
(epoch 21)
(eval "(let ((d (dict :method \"GET\" :path \"/foo\" :query \"\" :headers (dict) :body \"\"))) (let ((pl (er-request-dict-to-proplist d))) (get (test-pl-find pl \"method\") :bytes)))")

;; path roundtrip
(epoch 22)
(eval "(let ((d (dict :method \"POST\" :path \"/activity\" :query \"x=1\" :headers (dict) :body \"hi\"))) (let ((pl (er-request-dict-to-proplist d))) (let ((v (test-pl-find pl \"path\"))) (list->string (map integer->char (get v :bytes))))))")

;; --- headers nested as proplist with binary keys ---
;; Build a dict with a headers sub-dict, fetch headers field, find a header by binary key.
;; Local helper for binary-keyed proplist lookup.
(epoch 23)
(eval "(define test-pl-find-bin (fn (pl key-bytes) (cond (er-nil? pl) nil (er-cons? pl) (let ((head (get pl :head))) (cond (er-tuple? head) (let ((kv (get head :elements))) (cond (and (er-binary? (nth kv 0)) (= (get (nth kv 0) :bytes) key-bytes)) (nth kv 1) :else (test-pl-find-bin (get pl :tail) key-bytes))) :else (test-pl-find-bin (get pl :tail) key-bytes))) :else nil)))")
(epoch 30)
(eval "(let ((h (dict \"content-type\" \"text/plain\")) (d (dict :method \"GET\" :path \"/\" :query \"\" :body \"\"))) (dict-set! d :headers h) (let ((pl (er-request-dict-to-proplist d))) (let ((hpl (test-pl-find pl \"headers\"))) (let ((key-bytes (map char->integer (string->list \"content-type\")))) (let ((ct (test-pl-find-bin hpl key-bytes))) (list->string (map integer->char (get ct :bytes))))))))")

;; --- inverse: proplist response -> SX dict ---
;; Build an Erlang [{status, 200}, {headers, [...]}, {body, <<...>>}] proplist via SX
;; and verify er-proplist-to-dict returns an SX dict with status=200 and body string.
(epoch 40)
(eval "(let ((resp (er-mk-cons (er-mk-tuple (list (er-mk-atom \"status\") 200)) (er-mk-cons (er-mk-tuple (list (er-mk-atom \"headers\") (er-mk-nil))) (er-mk-cons (er-mk-tuple (list (er-mk-atom \"body\") (er-mk-binary (map char->integer (string->list \"hello\")))))  (er-mk-nil)))))) (let ((d (er-proplist-to-dict resp))) (get d \"status\")))")
(epoch 41)
(eval "(let ((resp (er-mk-cons (er-mk-tuple (list (er-mk-atom \"status\") 200)) (er-mk-cons (er-mk-tuple (list (er-mk-atom \"headers\") (er-mk-nil))) (er-mk-cons (er-mk-tuple (list (er-mk-atom \"body\") (er-mk-binary (map char->integer (string->list \"hello\")))))  (er-mk-nil)))))) (let ((d (er-proplist-to-dict resp))) (get d \"body\")))")

;; --- inverse: nested headers proplist -> nested SX dict ---
(epoch 42)
(eval "(let ((hpl (er-mk-cons (er-mk-tuple (list (er-mk-binary (map char->integer (string->list \"content-type\"))) (er-mk-binary (map char->integer (string->list \"text/plain\"))))) (er-mk-nil)))) (let ((resp (er-mk-cons (er-mk-tuple (list (er-mk-atom \"status\") 200)) (er-mk-cons (er-mk-tuple (list (er-mk-atom \"headers\") hpl)) (er-mk-cons (er-mk-tuple (list (er-mk-atom \"body\") (er-mk-binary (map char->integer (string->list \"ok\"))))) (er-mk-nil)))))) (let ((d (er-proplist-to-dict resp))) (let ((h (get d \"headers\"))) (get h \"content-type\")))))")

;; --- round-trip: handler eats a dict via proplist, returns a dict ---
;; Simulate: request dict -> proplist -> Erlang handler builds reply proplist
;; -> dict. Verify final dict has the keys the native http-listen expects.
(epoch 50)
(eval "(let ((req-dict (dict :method \"GET\" :path \"/echo\" :query \"\" :headers (dict) :body \"\"))) (let ((req-pl (er-request-dict-to-proplist req-dict))) (let ((resp (er-mk-cons (er-mk-tuple (list (er-mk-atom \"status\") 200)) (er-mk-cons (er-mk-tuple (list (er-mk-atom \"headers\") (er-mk-nil))) (er-mk-cons (er-mk-tuple (list (er-mk-atom \"body\") (er-mk-binary (map char->integer (string->list \"echoed\")))))  (er-mk-nil)))))) (let ((d (er-proplist-to-dict resp))) (get d \"status\"))))) ")

EPOCHS

OUTPUT=$(timeout 60 "$SX_SERVER" < "$TMPFILE" 2>/dev/null)

check() {
  local epoch="$1" desc="$2" expected="$3"
  local actual
  actual=$(echo "$OUTPUT" | awk -v e="$epoch" '
    $0 ~ "^\\(ok-len " e " " { getline; print; exit }
    $0 ~ "^\\(ok " e " "     { print; exit }
    $0 ~ "^\\(error " e " "  { print; exit }
  ')
  [ -z "$actual" ] && actual="<no output for epoch $epoch>"
  if echo "$actual" | grep -qF -- "$expected"; then
    PASS=$((PASS+1))
    [ "$VERBOSE" = "-v" ] && echo "  ok $desc"
  else
    FAIL=$((FAIL+1))
    ERRORS+="  FAIL [$desc] (epoch $epoch) expected: $expected | actual: $actual
"
  fi
}

check 10 "er-request-dict-to-proplist defined" "ok"
check 11 "er-proplist-to-dict defined"        "ok"
check 20 "request dict -> cons proplist"      "true"
check 21 "method value is <<\"GET\">>"        "(71 69 84)"
check 22 "path value as string"               "/activity"
check 30 "header value reachable as binary"   "text/plain"
check 40 "response status field = 200"        "200"
check 41 "response body present as string"    "hello"
check 42 "nested headers reconstructed dict"  "text/plain"
check 50 "full round-trip status preserved"   "200"

TOTAL=$((PASS+FAIL))
if [ $FAIL -eq 0 ]; then
  echo "ok $PASS/$TOTAL http_marshal tests passed"
else
  echo "FAIL $PASS/$TOTAL passed, $FAIL failed:"
  echo "$ERRORS"
fi
[ $FAIL -eq 0 ]
