#!/usr/bin/env bash
# next/tests/http_multi_actor.sh — m2 Step 4 tests (4a: per-actor
# URL sub-paths).
#
# Per design §16.1 each actor has:
#   GET  /actors/<id>            actor doc (M1)
#   GET  /actors/<id>/outbox     outbox listing (4a: stub)
#   GET  /actors/<id>/inbox      inbox listing (4a: stub)
#   GET  /actors/<id>/followers  follower list (4a: stub)
#   GET  /actors/<id>/following  following list (4a: stub)
#   POST /actors/<id>/inbox      peer delivery (4a: 202 stub; Step 5 real)
#
# 4b-4e wire the routes to per-actor kernel state + token map.

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")
(epoch 2)
(eval "(get (erlang-load-module (file-read \"next/kernel/http_server.erl\")) :name)")
(epoch 3)
(eval "(er-load-gen-server!)")
(epoch 4)
(eval "(get (erlang-load-module (file-read \"next/kernel/envelope.erl\")) :name)")
(epoch 5)
(eval "(get (erlang-load-module (file-read \"next/kernel/log.erl\")) :name)")
(epoch 6)
(eval "(get (erlang-load-module (file-read \"next/kernel/pipeline.erl\")) :name)")
(epoch 7)
(eval "(get (erlang-load-module (file-read \"next/kernel/outbox.erl\")) :name)")
(epoch 8)
(eval "(get (erlang-load-module (file-read \"next/kernel/nx_kernel.erl\")) :name)")

;; split_first_slash sanity
(epoch 10)
(eval "(get (erlang-eval-ast \"http_server:split_first_slash(<<97,108,105,99,101>>) =:= <<97,108,105,99,101>>\") :name)")
(epoch 11)
(eval "(get (erlang-eval-ast \"http_server:split_first_slash(<<97,108,105,99,101,47,105,110,98,111,120>>) =:= {<<97,108,105,99,101>>, <<105,110,98,111,120>>}\") :name)")
(epoch 12)
(eval "(get (erlang-eval-ast \"http_server:split_first_slash(<<97,108,105,99,101,47>>) =:= {<<97,108,105,99,101>>, <<>>}\") :name)")

;; GET /actors/alice returns actor doc (regression check — M1 path)
(epoch 20)
(eval "(get (erlang-eval-ast \"Req = [{method, <<71,69,84>>}, {path, <<47,97,99,116,111,114,115,47,97,108,105,99,101>>}, {headers, []}, {body, <<>>}], R = http_server:route(Req), case R of [{status, 200}, _, {body, B}] -> http_server:match_prefix(<<97,99,116,111,114,58>>, B) =/= nomatch; _ -> false end\") :name)")

;; GET /actors/alice/outbox returns outbox stub
(epoch 21)
(eval "(get (erlang-eval-ast \"Req = [{method, <<71,69,84>>}, {path, <<47,97,99,116,111,114,115,47,97,108,105,99,101,47,111,117,116,98,111,120>>}, {headers, []}, {body, <<>>}], R = http_server:route(Req), case R of [{status, 200}, _, {body, B}] -> http_server:match_prefix(<<111,117,116,98,111,120,58>>, B) =/= nomatch; _ -> false end\") :name)")

;; GET /actors/alice/inbox returns inbox stub
(epoch 22)
(eval "(get (erlang-eval-ast \"Req = [{method, <<71,69,84>>}, {path, <<47,97,99,116,111,114,115,47,97,108,105,99,101,47,105,110,98,111,120>>}, {headers, []}, {body, <<>>}], R = http_server:route(Req), case R of [{status, 200}, _, {body, B}] -> http_server:match_prefix(<<105,110,98,111,120,58>>, B) =/= nomatch; _ -> false end\") :name)")

;; GET /actors/alice/followers returns followers stub
(epoch 23)
(eval "(get (erlang-eval-ast \"Req = [{method, <<71,69,84>>}, {path, <<47,97,99,116,111,114,115,47,97,108,105,99,101,47,102,111,108,108,111,119,101,114,115>>}, {headers, []}, {body, <<>>}], R = http_server:route(Req), case R of [{status, 200}, _, {body, B}] -> http_server:match_prefix(<<102,111,108,108,111,119,101,114,115,58>>, B) =/= nomatch; _ -> false end\") :name)")

;; GET /actors/alice/following returns following stub
(epoch 24)
(eval "(get (erlang-eval-ast \"Req = [{method, <<71,69,84>>}, {path, <<47,97,99,116,111,114,115,47,97,108,105,99,101,47,102,111,108,108,111,119,105,110,103>>}, {headers, []}, {body, <<>>}], R = http_server:route(Req), case R of [{status, 200}, _, {body, B}] -> http_server:match_prefix(<<102,111,108,108,111,119,105,110,103,58>>, B) =/= nomatch; _ -> false end\") :name)")

;; POST /actors/alice/inbox returns 202 with "accepted"
(epoch 25)
(eval "(get (erlang-eval-ast \"Req = [{method, <<80,79,83,84>>}, {path, <<47,97,99,116,111,114,115,47,97,108,105,99,101,47,105,110,98,111,120>>}, {headers, []}, {body, <<>>}], R = http_server:route(Req), case R of [{status, 202}, _, {body, B}] -> http_server:match_prefix(<<97,99,99,101,112,116,101,100>>, B) =/= nomatch; _ -> false end\") :name)")

;; GET /actors/alice/unknown returns 404
(epoch 26)
(eval "(get (erlang-eval-ast \"Req = [{method, <<71,69,84>>}, {path, <<47,97,99,116,111,114,115,47,97,108,105,99,101,47,117,110,107,110,111,119,110>>}, {headers, []}, {body, <<>>}], R = http_server:route(Req), case R of [{status, 404}, _, _] -> true; _ -> false end\") :name)")

;; POST /actors/alice/unknown returns 404
(epoch 27)
(eval "(get (erlang-eval-ast \"Req = [{method, <<80,79,83,84>>}, {path, <<47,97,99,116,111,114,115,47,97,108,105,99,101,47,117,110,107,110,111,119,110>>}, {headers, []}, {body, <<>>}], R = http_server:route(Req), case R of [{status, 404}, _, _] -> true; _ -> false end\") :name)")

;; GET /actors/ (no id) returns 404 (existing behaviour preserved)
(epoch 28)
(eval "(get (erlang-eval-ast \"Req = [{method, <<71,69,84>>}, {path, <<47,97,99,116,111,114,115,47>>}, {headers, []}, {body, <<>>}], R = http_server:route(Req), case R of [{status, 404}, _, _] -> true; _ -> false end\") :name)")

;; GET /actors/bob/outbox carries bob's id in the stub body
(epoch 29)
(eval "(get (erlang-eval-ast \"Req = [{method, <<71,69,84>>}, {path, <<47,97,99,116,111,114,115,47,98,111,98,47,111,117,116,98,111,120>>}, {headers, []}, {body, <<>>}], R = http_server:route(Req), [{status, 200}, _, {body, B}] = R, http_server:match_prefix(<<111,117,116,98,111,120,58,32,98,111,98>>, B) =/= nomatch\") :name)")

;; Accept: application/json on /actors/alice/outbox -> JSON stub
(epoch 30)
(eval "(get (erlang-eval-ast \"AcceptKey = <<97,99,99,101,112,116>>, AcceptVal = <<97,112,112,108,105,99,97,116,105,111,110,47,106,115,111,110>>, Req = [{method, <<71,69,84>>}, {path, <<47,97,99,116,111,114,115,47,97,108,105,99,101,47,111,117,116,98,111,120>>}, {headers, [{AcceptKey, AcceptVal}]}, {body, <<>>}], R = http_server:route(Req), [_, _, {body, B}] = R, http_server:match_prefix(<<123,34,111,117,116,98,111,120,34>>, B) =/= nomatch\") :name)")

;; Accept: application/sx on /actors/alice/inbox -> SX stub
(epoch 31)
(eval "(get (erlang-eval-ast \"AcceptKey = <<97,99,99,101,112,116>>, AcceptVal = <<97,112,112,108,105,99,97,116,105,111,110,47,115,120>>, Req = [{method, <<71,69,84>>}, {path, <<47,97,99,116,111,114,115,47,97,108,105,99,101,47,105,110,98,111,120>>}, {headers, [{AcceptKey, AcceptVal}]}, {body, <<>>}], R = http_server:route(Req), [_, _, {body, B}] = R, http_server:match_prefix(<<40,105,110,98,111,120,32>>, B) =/= nomatch\") :name)")

;; Accept: application/json on /actors/alice/followers -> JSON stub
(epoch 32)
(eval "(get (erlang-eval-ast \"AcceptKey = <<97,99,99,101,112,116>>, AcceptVal = <<97,112,112,108,105,99,97,116,105,111,110,47,106,115,111,110>>, Req = [{method, <<71,69,84>>}, {path, <<47,97,99,116,111,114,115,47,97,108,105,99,101,47,102,111,108,108,111,119,101,114,115>>}, {headers, [{AcceptKey, AcceptVal}]}, {body, <<>>}], R = http_server:route(Req), [_, _, {body, B}] = R, http_server:match_prefix(<<123,34,102,111,108,108,111,119,101,114,115,34>>, B) =/= nomatch\") :name)")

;; ── Step 4b: token -> ActorId map ──────────────────────────────
;; Each test inlines start_link + add_actor + Cfg with :tokens
;; proplist mapping per-actor bearer tokens. Tokens look like
;; "alice-token" = <<97,108,105,99,101,45,116,111,107,101,110>>
;; (bytes spelled) and "bob-token"   = <<98,111,98,45,116,111,107,101,110>>.

(epoch 40)
(eval "(get (erlang-eval-ast \"AK = <<1,2,3,4>>, AKS = [{key_id,k1},{algorithm,ed25519},{value,AK}], AAS = [{public_keys,[[{id,k1},{created,0},{value,AK}]]}], BK = <<5,6,7,8>>, BKS = [{key_id,k1},{algorithm,ed25519},{value,BK}], BAS = [{public_keys,[[{id,k1},{created,0},{value,BK}]]}], nx_kernel:start_link(alice, AKS, AAS), nx_kernel:add_actor(bob, BKS, BAS), AliceTok = <<97,108,105,99,101,45,116,111,107,101,110>>, BobTok = <<98,111,98,45,116,111,107,101,110>>, AuthKey = <<97,117,116,104,111,114,105,122,97,116,105,111,110>>, AliceAuth = <<66,101,97,114,101,114,32,97,108,105,99,101,45,116,111,107,101,110>>, Cfg = [{tokens, [{AliceTok, alice}, {BobTok, bob}]}], Req = [{method, <<80,79,83,84>>}, {path, http_server:activity_path()}, {headers, [{AuthKey, AliceAuth}]}, {body, <<104,105>>}], R = http_server:route(Req, Cfg), case R of [{status, 200}, _, {body, B}] -> http_server:match_prefix(<<99,105,100,58,32>>, B) =/= nomatch; _ -> false end\") :name)")

;; Alice token publishes to alice's bucket (log_tip alice = 1, bob = 0)
(epoch 41)
(eval "(get (erlang-eval-ast \"AK = <<1,2,3,4>>, AKS = [{key_id,k1},{algorithm,ed25519},{value,AK}], AAS = [{public_keys,[[{id,k1},{created,0},{value,AK}]]}], BK = <<5,6,7,8>>, BKS = [{key_id,k1},{algorithm,ed25519},{value,BK}], BAS = [{public_keys,[[{id,k1},{created,0},{value,BK}]]}], nx_kernel:start_link(alice, AKS, AAS), nx_kernel:add_actor(bob, BKS, BAS), AliceTok = <<97,108,105,99,101,45,116,111,107,101,110>>, BobTok = <<98,111,98,45,116,111,107,101,110>>, AuthKey = <<97,117,116,104,111,114,105,122,97,116,105,111,110>>, AliceAuth = <<66,101,97,114,101,114,32,97,108,105,99,101,45,116,111,107,101,110>>, Cfg = [{tokens, [{AliceTok, alice}, {BobTok, bob}]}], Req = [{method, <<80,79,83,84>>}, {path, http_server:activity_path()}, {headers, [{AuthKey, AliceAuth}]}, {body, <<104,105>>}], http_server:route(Req, Cfg), {nx_kernel:log_tip_for(alice), nx_kernel:log_tip_for(bob)} =:= {1, 0}\") :name)")

;; Bob token publishes to bob's bucket
(epoch 42)
(eval "(get (erlang-eval-ast \"AK = <<1,2,3,4>>, AKS = [{key_id,k1},{algorithm,ed25519},{value,AK}], AAS = [{public_keys,[[{id,k1},{created,0},{value,AK}]]}], BK = <<5,6,7,8>>, BKS = [{key_id,k1},{algorithm,ed25519},{value,BK}], BAS = [{public_keys,[[{id,k1},{created,0},{value,BK}]]}], nx_kernel:start_link(alice, AKS, AAS), nx_kernel:add_actor(bob, BKS, BAS), AliceTok = <<97,108,105,99,101,45,116,111,107,101,110>>, BobTok = <<98,111,98,45,116,111,107,101,110>>, AuthKey = <<97,117,116,104,111,114,105,122,97,116,105,111,110>>, BobAuth = <<66,101,97,114,101,114,32,98,111,98,45,116,111,107,101,110>>, Cfg = [{tokens, [{AliceTok, alice}, {BobTok, bob}]}], Req = [{method, <<80,79,83,84>>}, {path, http_server:activity_path()}, {headers, [{AuthKey, BobAuth}]}, {body, <<104,105>>}], http_server:route(Req, Cfg), {nx_kernel:log_tip_for(alice), nx_kernel:log_tip_for(bob)} =:= {0, 1}\") :name)")

;; Mixed token stream -> independent logs
(epoch 43)
(eval "(get (erlang-eval-ast \"AK = <<1,2,3,4>>, AKS = [{key_id,k1},{algorithm,ed25519},{value,AK}], AAS = [{public_keys,[[{id,k1},{created,0},{value,AK}]]}], BK = <<5,6,7,8>>, BKS = [{key_id,k1},{algorithm,ed25519},{value,BK}], BAS = [{public_keys,[[{id,k1},{created,0},{value,BK}]]}], nx_kernel:start_link(alice, AKS, AAS), nx_kernel:add_actor(bob, BKS, BAS), AliceTok = <<97,108,105,99,101,45,116,111,107,101,110>>, BobTok = <<98,111,98,45,116,111,107,101,110>>, AuthKey = <<97,117,116,104,111,114,105,122,97,116,105,111,110>>, AliceAuth = <<66,101,97,114,101,114,32,97,108,105,99,101,45,116,111,107,101,110>>, BobAuth = <<66,101,97,114,101,114,32,98,111,98,45,116,111,107,101,110>>, Cfg = [{tokens, [{AliceTok, alice}, {BobTok, bob}]}], AliceReq = [{method, <<80,79,83,84>>}, {path, http_server:activity_path()}, {headers, [{AuthKey, AliceAuth}]}, {body, <<104,105>>}], BobReq = [{method, <<80,79,83,84>>}, {path, http_server:activity_path()}, {headers, [{AuthKey, BobAuth}]}, {body, <<104,105>>}], http_server:route(AliceReq, Cfg), http_server:route(BobReq, Cfg), http_server:route(AliceReq, Cfg), {nx_kernel:log_tip_for(alice), nx_kernel:log_tip_for(bob)} =:= {2, 1}\") :name)")

;; Token not in :tokens map and no :publish_token -> 401
(epoch 44)
(eval "(get (erlang-eval-ast \"AK = <<1,2,3,4>>, AKS = [{key_id,k1},{algorithm,ed25519},{value,AK}], AAS = [{public_keys,[[{id,k1},{created,0},{value,AK}]]}], nx_kernel:start_link(alice, AKS, AAS), AliceTok = <<97,108,105,99,101,45,116,111,107,101,110>>, AuthKey = <<97,117,116,104,111,114,105,122,97,116,105,111,110>>, GhostAuth = <<66,101,97,114,101,114,32,103,104,111,115,116>>, Cfg = [{tokens, [{AliceTok, alice}]}], Req = [{method, <<80,79,83,84>>}, {path, http_server:activity_path()}, {headers, [{AuthKey, GhostAuth}]}, {body, <<104,105>>}], case http_server:route(Req, Cfg) of [{status, 401}, _, _] -> true; _ -> false end\") :name)")

;; Legacy :publish_token still works (M1 back-compat)
(epoch 45)
(eval "(get (erlang-eval-ast \"AK = <<1,2,3,4>>, AKS = [{key_id,k1},{algorithm,ed25519},{value,AK}], AAS = [{public_keys,[[{id,k1},{created,0},{value,AK}]]}], nx_kernel:start_link(alice, AKS, AAS), Tok = <<102,111,111>>, AuthKey = <<97,117,116,104,111,114,105,122,97,116,105,111,110>>, AuthVal = <<66,101,97,114,101,114,32,102,111,111>>, Cfg = [{publish_token, Tok}], Req = [{method, <<80,79,83,84>>}, {path, http_server:activity_path()}, {headers, [{AuthKey, AuthVal}]}, {body, <<104,105>>}], R = http_server:route(Req, Cfg), case R of [{status, 200}, _, {body, B}] -> http_server:match_prefix(<<99,105,100,58,32>>, B) =/= nomatch; _ -> false end\") :name)")

;; :tokens takes precedence; legacy :publish_token still resolved on miss
(epoch 46)
(eval "(get (erlang-eval-ast \"AK = <<1,2,3,4>>, AKS = [{key_id,k1},{algorithm,ed25519},{value,AK}], AAS = [{public_keys,[[{id,k1},{created,0},{value,AK}]]}], BK = <<5,6,7,8>>, BKS = [{key_id,k1},{algorithm,ed25519},{value,BK}], BAS = [{public_keys,[[{id,k1},{created,0},{value,BK}]]}], nx_kernel:start_link(alice, AKS, AAS), nx_kernel:add_actor(bob, BKS, BAS), AliceTok = <<97,108,105,99,101,45,116,111,107,101,110>>, LegacyTok = <<102,111,111>>, AuthKey = <<97,117,116,104,111,114,105,122,97,116,105,111,110>>, AliceAuth = <<66,101,97,114,101,114,32,97,108,105,99,101,45,116,111,107,101,110>>, LegacyAuth = <<66,101,97,114,101,114,32,102,111,111>>, Cfg = [{tokens, [{AliceTok, alice}]}, {publish_token, LegacyTok}], Req1 = [{method, <<80,79,83,84>>}, {path, http_server:activity_path()}, {headers, [{AuthKey, AliceAuth}]}, {body, <<104,105>>}], Req2 = [{method, <<80,79,83,84>>}, {path, http_server:activity_path()}, {headers, [{AuthKey, LegacyAuth}]}, {body, <<104,105>>}], http_server:route(Req1, Cfg), http_server:route(Req2, Cfg), {nx_kernel:log_tip_for(alice), nx_kernel:log_tip_for(bob)} =:= {2, 0}\") :name)")

;; Token resolution before kernel is registered -> auth-stub published response
(epoch 47)
(eval "(get (erlang-eval-ast \"AliceTok = <<97,108,105,99,101,45,116,111,107,101,110>>, AuthKey = <<97,117,116,104,111,114,105,122,97,116,105,111,110>>, AliceAuth = <<66,101,97,114,101,114,32,97,108,105,99,101,45,116,111,107,101,110>>, Cfg = [{tokens, [{AliceTok, alice}]}], Req = [{method, <<80,79,83,84>>}, {path, http_server:activity_path()}, {headers, [{AuthKey, AliceAuth}]}, {body, <<>>}], R = http_server:route(Req, Cfg), case R of [{status, 200}, _, {body, B}] -> http_server:match_prefix(<<112,117,98,108,105,115,104,101,100>>, B) =/= nomatch; _ -> false end\") :name)")
EPOCHS

OUTPUT=$(timeout 240 "$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  2  "http_server loaded"                "http_server"
check 10  "split sans slash returns bare"     "true"
check 11  "split id/sub returns {id, sub}"    "true"
check 12  "split id/ returns {id, <<>>}"      "true"
check 20  "GET /actors/<id> regression"       "true"
check 21  "GET /actors/<id>/outbox stub"      "true"
check 22  "GET /actors/<id>/inbox stub"       "true"
check 23  "GET /actors/<id>/followers stub"   "true"
check 24  "GET /actors/<id>/following stub"   "true"
check 25  "POST /actors/<id>/inbox -> 202"    "true"
check 26  "GET /actors/<id>/<bad> -> 404"     "true"
check 27  "POST /actors/<id>/<bad> -> 404"    "true"
check 28  "GET /actors/ (empty) -> 404"       "true"
check 29  "outbox body carries actor id"      "true"
check 30  "outbox JSON content negotiation"   "true"
check 31  "inbox SX content negotiation"      "true"
check 32  "followers JSON content negotiation" "true"
check 40  "two-token Cfg + Alice POST -> 200"  "true"
check 41  "Alice token publishes to alice"     "true"
check 42  "Bob token publishes to bob"         "true"
check 43  "interleaved tokens isolate logs"    "true"
check 44  "unknown token -> 401"               "true"
check 45  "legacy :publish_token still works"  "true"
check 46  "tokens map + legacy back-compat"    "true"
check 47  "no kernel + token map -> stub 200"  "true"

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