#!/usr/bin/env bash
# next/tests/actor_state_pure.sh — m2 Step 2b test.
#
# Exercises the Erlang-fun stand-in for the actor-state projection
# fold. Activities flow:
#   Create{Person|Service|Group} -> profile registered
#   Update{Person|Service|Group, patch} -> patch deep-merged
#   Move -> :moved_to recorded
# Non-actor object Creates pass through.

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/envelope.erl\")) :name)")
(epoch 3)
(eval "(get (erlang-load-module (file-read \"next/kernel/actor_state.erl\")) :name)")

;; new/0 returns []
(epoch 10)
(eval "(get (erlang-eval-ast \"actor_state:new() =:= []\") :name)")

;; has/2 false on empty
(epoch 11)
(eval "(get (erlang-eval-ast \"actor_state:has(alice, actor_state:new()) =:= false\") :name)")

;; lookup/2 not_found on empty
(epoch 12)
(eval "(get (erlang-eval-ast \"actor_state:lookup(alice, actor_state:new()) =:= not_found\") :name)")

;; actors/1 returns [] on empty
(epoch 13)
(eval "(get (erlang-eval-ast \"actor_state:actors(actor_state:new()) =:= []\") :name)")

;; Create{Person} registers profile
(epoch 14)
(eval "(get (erlang-eval-ast \"Obj = [{type, person}, {name, alice_name}, {preferredUsername, alice_local}], Act = [{actor, alice}, {type, create}, {object, Obj}, {published, 1}], S = actor_state:fold(Act, actor_state:new()), actor_state:has(alice, S)\") :name)")

;; Profile carries :type, :name, :preferredUsername, :created
(epoch 15)
(eval "(get (erlang-eval-ast \"Obj = [{type, person}, {name, alice_name}, {preferredUsername, alice_local}], Act = [{actor, alice}, {type, create}, {object, Obj}, {published, 7}], S = actor_state:fold(Act, actor_state:new()), {ok, P} = actor_state:lookup(alice, S), {actor_state:profile_type(P), actor_state:profile_name(P), actor_state:profile_field(preferredUsername, P), actor_state:profile_field(created, P)} =:= {person, alice_name, {ok, alice_local}, {ok, 7}}\") :name)")

;; Create{Service} also registers
(epoch 16)
(eval "(get (erlang-eval-ast \"Obj = [{type, service}, {name, feedbot}], Act = [{actor, feed1}, {type, create}, {object, Obj}, {published, 1}], S = actor_state:fold(Act, actor_state:new()), {ok, P} = actor_state:lookup(feed1, S), actor_state:profile_type(P) =:= service\") :name)")

;; Create{Group} also registers
(epoch 17)
(eval "(get (erlang-eval-ast \"Obj = [{type, group}, {name, working_group}], Act = [{actor, wg1}, {type, create}, {object, Obj}, {published, 1}], S = actor_state:fold(Act, actor_state:new()), {ok, P} = actor_state:lookup(wg1, S), actor_state:profile_type(P) =:= group\") :name)")

;; Create{Note} is pass-through (non-actor object)
(epoch 18)
(eval "(get (erlang-eval-ast \"Obj = [{type, note}, {content, hi}], Act = [{actor, alice}, {type, create}, {object, Obj}, {published, 1}], actor_state:fold(Act, actor_state:new()) =:= []\") :name)")

;; Duplicate Create doesn't overwrite an existing profile
(epoch 19)
(eval "(get (erlang-eval-ast \"O1 = [{type, person}, {name, alice_v1}], O2 = [{type, person}, {name, alice_v2}], A1 = [{actor, alice}, {type, create}, {object, O1}, {published, 1}], A2 = [{actor, alice}, {type, create}, {object, O2}, {published, 2}], S1 = actor_state:fold(A1, actor_state:new()), S2 = actor_state:fold(A2, S1), {ok, P} = actor_state:lookup(alice, S2), actor_state:profile_name(P) =:= alice_v1\") :name)")

;; Two distinct actors live side by side
(epoch 20)
(eval "(get (erlang-eval-ast \"PO = [{type, person}, {name, alice_n}], SO = [{type, service}, {name, bobbot_n}], A1 = [{actor, alice}, {type, create}, {object, PO}, {published, 1}], A2 = [{actor, bobbot}, {type, create}, {object, SO}, {published, 2}], S = actor_state:fold(A2, actor_state:fold(A1, actor_state:new())), actor_state:actors(S) =:= [alice, bobbot]\") :name)")

;; Update merges patch
(epoch 21)
(eval "(get (erlang-eval-ast \"PO = [{type, person}, {name, alice_n}], A1 = [{actor, alice}, {type, create}, {object, PO}, {published, 1}], A2 = [{actor, alice}, {type, update}, {patch, [{summary, new_bio}]}, {published, 2}], S = actor_state:fold(A2, actor_state:fold(A1, actor_state:new())), {ok, P} = actor_state:lookup(alice, S), actor_state:profile_field(summary, P) =:= {ok, new_bio}\") :name)")

;; Update overwrites individual fields (last-write-wins per key)
(epoch 22)
(eval "(get (erlang-eval-ast \"PO = [{type, person}, {name, alice_v1}], A1 = [{actor, alice}, {type, create}, {object, PO}, {published, 1}], A2 = [{actor, alice}, {type, update}, {patch, [{name, alice_v2}]}, {published, 2}], S = actor_state:fold(A2, actor_state:fold(A1, actor_state:new())), {ok, P} = actor_state:lookup(alice, S), actor_state:profile_name(P) =:= alice_v2\") :name)")

;; Update for unknown actor is pass-through
(epoch 23)
(eval "(get (erlang-eval-ast \"A = [{actor, ghost}, {type, update}, {patch, [{summary, x}]}, {published, 1}], actor_state:fold(A, actor_state:new()) =:= []\") :name)")

;; Move records :moved_to
(epoch 24)
(eval "(get (erlang-eval-ast \"PO = [{type, person}, {name, alice_n}], A1 = [{actor, alice}, {type, create}, {object, PO}, {published, 1}], A2 = [{actor, alice}, {type, move}, {moved_to, new_alice}, {published, 2}], S = actor_state:fold(A2, actor_state:fold(A1, actor_state:new())), {ok, P} = actor_state:lookup(alice, S), actor_state:profile_field(moved_to, P) =:= {ok, new_alice}\") :name)")

;; fold_fn/0 is a 2-arity Erlang fun usable by projection:start_link
(epoch 25)
(eval "(get (erlang-eval-ast \"F = actor_state:fold_fn(), is_function(F, 2)\") :name)")

;; fold ignores activities with no :actor field
(epoch 26)
(eval "(get (erlang-eval-ast \"Obj = [{type, person}, {name, x}], Act = [{type, create}, {object, Obj}, {published, 1}], actor_state:fold(Act, actor_state:new()) =:= []\") :name)")

;; public_keys field is captured at Create time
(epoch 27)
(eval "(get (erlang-eval-ast \"Keys = [[{id, k1}, {value, <<1,2,3,4>>}]], Obj = [{type, person}, {name, alice_n}, {public_keys, Keys}], Act = [{actor, alice}, {type, create}, {object, Obj}, {published, 1}], S = actor_state:fold(Act, actor_state:new()), {ok, P} = actor_state:lookup(alice, S), actor_state:profile_field(public_keys, P) =:= {ok, Keys}\") :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  3  "actor_state module loaded"          "actor_state"
check 10  "new/0 -> []"                        "true"
check 11  "has/2 false on empty"               "true"
check 12  "lookup/2 not_found on empty"        "true"
check 13  "actors/1 [] on empty"               "true"
check 14  "Create{Person} registers actor"    "true"
check 15  "Profile carries type/name/created"  "true"
check 16  "Create{Service} registers actor"    "true"
check 17  "Create{Group} registers actor"      "true"
check 18  "Create{Note} pass-through"          "true"
check 19  "Duplicate Create no-overwrite"      "true"
check 20  "Two actors side by side"            "true"
check 21  "Update merges new fields"           "true"
check 22  "Update last-write-wins per key"     "true"
check 23  "Update unknown actor pass-through"  "true"
check 24  "Move records :moved_to"             "true"
check 25  "fold_fn/0 is fun/2"                 "true"
check 26  "Activity sans :actor pass-through"  "true"
check 27  "public_keys captured at Create"     "true"

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