#!/bin/sh
# setup.sh DIR PORT
#
# Makes a fresh OCX test environment in DIR (which must not exist or be
# empty), served on 127.0.0.1:PORT, the way tree/spec/support/served_node.cr
# serves OCX to its specs, but with Fossil's own server running the staged
# program as its ext page /ext/ocx2-cgi (no proxy in between: Fossil itself
# signs the viewer in and names it in FOSSIL_USER / FOSSIL_CAPABILITIES).
#
# Scratch only. It copies the staged program (never runs it in place, since
# a program run from staged/next takes that tree's root as its release root)
# into DIR/root, laid out as scripts/start-fossil.cr lays out a release root:
#   DIR/root/current/ocx2, DIR/root/staged/next/ocx2  (copies, same hash)
#   DIR/ext/ocx2-cgi       -> DIR/root/current/ocx2
#   DIR/ext/ocx2-cgi-next  -> DIR/root/staged/next/ocx2
#   DIR/ext/who            the viewer-naming script served_node.cr writes
# Fossil runs with HOME=DIR/home. ocx2 takes its home from the account
# database instead (tools/ocx2.cr: "Fossil keeps its settings in the home the
# account database names"), so the server and DIR/bin/ocx2 run in bwrap with
# DIR/acct bound over that home: no ~/.fossil outside DIR is touched.
# OCX2=PATH tests another build than this host's staged one.
#
# What it leaves:
#   DIR/server/repo.fossil   the served repository ("fossil new -A owner")
#   DIR/server/checkout      a checkout of it, the server-side one
#   DIR/clones/ivy/clone.fossil, DIR/clones/ivy/tree
#                            a clone made over HTTP as ivy (i,o), its password
#                            saved, autosync off, and its checkout
#   DIR/passwords.txt        "user password capabilities" per line (scratch)
#   DIR/facts.txt            hashes made here (check-ins, posts, ticket, wiki)
#   DIR/env.sh               ". DIR/env.sh" sets HOME, PATH (ocx2 first), URL
#   DIR/bin/ocx2             the CLI, confined as above
#   DIR/acct                 what ocx2 sees as the account's home
#   DIR/start.sh, DIR/stop.sh, DIR/server.pid, DIR/server.log, DIR/server.err.log
# The server keeps running when this script exits; stop it with DIR/stop.sh,
# start it again on the same port with DIR/start.sh. /ext/who answers
# "viewer=USER capabilities=CAPS" (inside Fossil's skin) for whoever the
# request's cookie signs in; nobody shows as an empty viewer.
set -eu

[ $# -eq 2 ] || { echo "usage: $0 DIR PORT" >&2; exit 2; }
DIR=$1
PORT=$2
STAGED=${OCX2:-/var/lib/ocx/agents/opus-ocx2/projects/ollama/b2de8b0c2c6700eb5c686b1747b5aaa1e2d2d01370ac55f2e5a151a67e03b48a/staged/next/ocx2}
# the home the account database names, which ocx2 reads and bwrap covers
ACCT_HOME=$(getent passwd "$(id -u)" | cut -d: -f6)
NICE="nice -n 10"
FOSSIL=$(command -v fossil) || { echo "fossil is not on PATH" >&2; exit 2; }

[ -x "$STAGED" ] || { echo "no program at $STAGED; give one with OCX2=PATH" >&2; exit 2; }
command -v bwrap >/dev/null || { echo "bwrap is needed, to keep ocx2 off $ACCT_HOME" >&2; exit 2; }
[ -d "$ACCT_HOME" ] || { echo "the account's home $ACCT_HOME is not a directory" >&2; exit 2; }
case $PORT in ''|*[!0-9]*) echo "PORT must be a number" >&2; exit 2;; esac
if [ -e "$DIR" ] && [ -n "$(ls -A "$DIR" 2>/dev/null)" ]; then
  echo "$DIR exists and is not empty; give a fresh directory" >&2; exit 2
fi
if curl -s -o /dev/null --max-time 2 "http://127.0.0.1:$PORT/"; then
  echo "something already answers on 127.0.0.1:$PORT" >&2; exit 2
fi

mkdir -p "$DIR"
DIR=$(cd "$DIR" && pwd)
URL="http://127.0.0.1:$PORT"
export HOME="$DIR/home"
mkdir -p "$HOME" "$DIR/acct" "$DIR/server/checkout" "$DIR/clones" "$DIR/ext" \
  "$DIR/root/current" "$DIR/root/staged/next" "$DIR/bin"

fossil() { $NICE "$FOSSIL" "$@"; }
pw() { od -An -tx1 -N9 /dev/urandom | tr -d ' \n'; }

# --- the program: copies of the staged release, in a scratch release root
cp "$STAGED" "$DIR/root/current/ocx2"
cp "$STAGED" "$DIR/root/staged/next/ocx2"
chmod 755 "$DIR/root/current/ocx2" "$DIR/root/staged/next/ocx2"
ln -s "$DIR/root/current/ocx2" "$DIR/ext/ocx2-cgi"
ln -s "$DIR/root/staged/next/ocx2" "$DIR/ext/ocx2-cgi-next"
# the CLI, by its own name, confined so it sees DIR/acct as the account's home
printf '#!/bin/sh\nexec bwrap --dev-bind / / --bind "%s" "%s" -- "%s" "$@"\n' "$DIR/acct" "$ACCT_HOME" "$DIR/root/current/ocx2" > "$DIR/bin/ocx2"
chmod 755 "$DIR/bin/ocx2"
# served_node.cr's viewer-naming script, so a tester can ask Fossil who it signed in
printf '#!/bin/sh\nprintf '"'"'Content-Type: text/plain\\r\\n\\r\\nviewer=%%s capabilities=%%s\\n'"'"' "$FOSSIL_USER" "$FOSSIL_CAPABILITIES"\n' > "$DIR/ext/who"
chmod 755 "$DIR/ext/who"

# --- the repository, its admin, and its users
REPO="$DIR/server/repo.fossil"
CO="$DIR/server/checkout"
created=$(cd "$DIR/server" && fossil new -A owner "$REPO")
OWNER_PW=$(printf '%s\n' "$created" | sed -n 's/.*password is "\([^"]*\)".*/\1/p')
[ -n "$OWNER_PW" ] || { echo "fossil new printed no password: $created" >&2; exit 1; }
(cd "$CO" && fossil open "$REPO" >/dev/null && fossil user default owner && fossil settings autosync off)

: > "$DIR/passwords.txt"
chmod 600 "$DIR/passwords.txt"
echo "# user password capabilities   (scratch passwords for $URL)" >> "$DIR/passwords.txt"
echo "owner $OWNER_PW s   (fossil new -A)" >> "$DIR/passwords.txt"
add_user() { # login caps
  p=$(pw)
  (cd "$CO" && fossil user new "$1" "$1@scratch.invalid" "$p" >/dev/null && fossil user capabilities "$1" "$2" >/dev/null)
  echo "$1 $p $2" >> "$DIR/passwords.txt"
}
add_user sally s     # setup
add_user adam a      # admin
add_user ivy io      # check-in and read
add_user otto o      # read only
add_user rita Ri     # OCX's run sender (R) with check-in
# nobody and anonymous keep what "fossil new" gives them
password_of() { awk -v u="$1" '$1==u {print $2}' "$DIR/passwords.txt"; }

# --- check-ins by different users
commit() { # user file content comment
  (cd "$CO" && printf '%s\n' "$3" > "$2" && fossil add "$2" >/dev/null && \
   fossil commit --no-warnings --user "$1" -m "$4" >/dev/null)
}
commit owner README.md "# scratch project" "Start the scratch project."
commit ivy hello.txt "hello" "Add hello."
commit sally PLAN.md "Plan: nothing yet." "Add a plan."
commit ivy hello.txt "hello, world" "Say more in hello."
checkins=$(fossil sql -R "$REPO" "SELECT b.uuid||' '||e.user FROM event e JOIN blob b ON b.rid=e.objid WHERE e.type='ci' ORDER BY e.mtime")

# --- a wiki page, by adam
(cd "$CO" && fossil user default adam && printf '# Notes\n\nScratch notes for testers.\n' > "$DIR/server/notes.md" && \
 fossil wiki create Notes "$DIR/server/notes.md" --mimetype text/x-markdown >/dev/null)

# --- a ticket and its changes, by different users
(cd "$CO" && fossil user default owner)
tkt_out=$(cd "$CO" && fossil ticket add title "Scratch ticket" status Open type Code_Defect comment "First report.")
TICKET=$(printf '%s\n' "$tkt_out" | sed -n 's/.*for \([0-9a-f]\{40,64\}\).*/\1/p')
[ -n "$TICKET" ] || { echo "ticket add printed no hash: $tkt_out" >&2; exit 1; }
(cd "$CO" && fossil user default ivy && fossil ticket change "$TICKET" icomment "Seen it too, by ivy." >/dev/null)
(cd "$CO" && fossil user default sally && fossil ticket change "$TICKET" status Fixed icomment "Fixed, by sally." >/dev/null)
(cd "$CO" && fossil user default owner)

# --- the server
echo "$PORT" > "$DIR/server.port"
cd "$DIR"
cat > "$DIR/start.sh" <<EOF
#!/bin/sh
# Starts (again) the scratch server for $DIR on 127.0.0.1:$PORT, niced, in
# its own session; DIR/stop.sh stops it.
[ -f "$DIR/server.pid" ] && kill -0 "\$(cat "$DIR/server.pid")" 2>/dev/null && { echo "already running" >&2; exit 1; }
cd "$DIR"
HOME="$DIR/home" setsid nice -n 10 bwrap --dev-bind / / --bind "$DIR/acct" "$ACCT_HOME" -- "$FOSSIL" server --localhost --port "$PORT" --nojail \\
  --extroot "$DIR/ext" --errorlog "$DIR/server.err.log" "$REPO" >> "$DIR/server.log" 2>&1 < /dev/null &
echo \$! > "$DIR/server.pid"
i=0
until curl -s -o /dev/null --max-time 1 "$URL/"; do
  i=\$((i+1)); [ \$i -lt 50 ] || { echo "fossil server did not answer on $URL" >&2; "$DIR/stop.sh"; exit 1; }
  sleep 0.1
done
echo "serving $URL (pid \$(cat "$DIR/server.pid"))"
EOF
chmod 755 "$DIR/start.sh"
cat > "$DIR/stop.sh" <<EOF
#!/bin/sh
# Stops the scratch server setup.sh started for $DIR: its process group,
# and any process still serving this repository, such as Fossil's
# backoffice, which leaves its server's process group and may linger.
pid=\$(cat "$DIR/server.pid" 2>/dev/null)
[ -n "\$pid" ] && { kill -TERM -- -\$pid 2>/dev/null || kill -TERM \$pid 2>/dev/null; }
serving() {
  for p in /proc/[0-9]*; do
    c=\$(tr '\\0' ' ' 2>/dev/null < \$p/cmdline) || continue
    case "\$c" in *"fossil server "*"$REPO"*) echo \${p#/proc/};; esac
  done
}
for i in 1 2 3 4 5 6 7 8 9 10; do
  left=\$(serving); [ -z "\$left" ] && break
  kill -TERM \$left 2>/dev/null; sleep 0.2
done
left=\$(serving); [ -n "\$left" ] && kill -KILL \$left 2>/dev/null
rm -f "$DIR/server.pid"
[ -z "\$(serving)" ] && echo "stopped: nothing serves $REPO" || { echo "still serving: \$(serving)" >&2; exit 1; }
EOF
chmod 755 "$DIR/stop.sh"
# a failure from here on leaves no server running
trap 'st=$?; [ $st -eq 0 ] || { echo "setup failed ($st); stopping the server" >&2; "$DIR/stop.sh" >&2; }' EXIT
"$DIR/start.sh" >/dev/null

# --- a forum thread through Fossil's own form, replies by different users
JARS="$DIR/home/jars"; mkdir -p "$JARS"
login() { # user -> cookie jar path
  curl -s -o /dev/null -c "$JARS/$1" --data-urlencode "u=$1" --data-urlencode "p=$(password_of "$1")" -d in=1 "$URL/login"
  grep -q fossil- "$JARS/$1" || { echo "login as $1 set no cookie" >&2; exit 1; }
}
csrf() { sed -n 's/.*name="csrf" value="\([^"]*\)".*/\1/p' | head -n 1; }
latest_post() { fossil sql -R "$REPO" "SELECT b.uuid FROM blob b JOIN forumpost f ON b.rid=f.fpid ORDER BY f.fmtime DESC, f.fpid DESC LIMIT 1" | tr -d "'"; }
new_thread() { # user title content
  login "$1"
  t=$(curl -s -b "$JARS/$1" "$URL/forumnew" | csrf)
  curl -s -o /dev/null -b "$JARS/$1" -e "$URL/forumnew" --data-urlencode "csrf=$t" --data-urlencode "title=$2" \
    -d mimetype=text/x-markdown --data-urlencode "content=$3" -d submit=Submit "$URL/forume1"
  latest_post
}
reply() { # user to content
  login "$1"
  t=$(curl -s -b "$JARS/$1" "$URL/forumpost/$2" | csrf)
  curl -s -o /dev/null -b "$JARS/$1" -e "$URL/forume2" --data-urlencode "csrf=$t" -d "fpid=$2" -d reply=1 \
    -d mimetype=text/x-markdown --data-urlencode "content=$3" -d submit=Submit "$URL/forume2"
  latest_post
}
THREAD=$(new_thread owner "Scratch thread" "The first post, by owner.")
R1=$(reply adam "$THREAD" "A reply, by adam.")
R2=$(reply sally "$R1" "A reply to adam, by sally.")
R3=$(reply owner "$THREAD" "A second reply to the first post, by owner.")
posts=$(fossil sql -R "$REPO" "SELECT b.uuid||' '||e.user FROM forumpost f JOIN blob b ON b.rid=f.fpid JOIN event e ON e.objid=f.fpid ORDER BY f.fmtime")
[ "$(printf '%s\n' "$posts" | wc -l)" -eq 4 ] || { echo "expected 4 posts, have: $posts" >&2; "$DIR/stop.sh"; exit 1; }

# --- a clone over HTTP as ivy, with a checkout
IVY="$DIR/clones/ivy"
mkdir -p "$IVY/tree"
(cd "$IVY" && fossil clone -A ivy --save-http-password "http://ivy:$(password_of ivy)@127.0.0.1:$PORT/" "$IVY/clone.fossil" >/dev/null)
(cd "$IVY/tree" && fossil open "$IVY/clone.fossil" >/dev/null && fossil user default ivy && fossil settings autosync off)

cat > "$DIR/env.sh" <<EOF
# . $DIR/env.sh   -- for testers: the scratch HOME, ocx2 on PATH, the server's URL
export HOME="$DIR/home"
export PATH="$DIR/bin:\$PATH"
export URL="$URL"
export REPO="$REPO"
EOF

{
  echo "url $URL"
  echo "ocx_page $URL/ext/ocx2-cgi"
  echo "repository $REPO"
  echo "server_checkout $CO"
  echo "clone_ivy $IVY/clone.fossil"
  echo "checkout_ivy $IVY/tree"
  echo "program $DIR/root/current/ocx2 (copy of $STAGED, sha256 $(sha256sum "$STAGED" | cut -d' ' -f1))"
  echo "ticket $TICKET"
  echo "thread $THREAD"
  echo "reply_adam $R1"
  echo "reply_sally_to_adam $R2"
  echo "reply_owner $R3"
  echo "wiki Notes"
  printf '%s\n' "$checkins" | tr -d "'" | sed 's/^/checkin /'
  echo "nobody_caps $(fossil sql -R "$REPO" "SELECT cap FROM user WHERE login='nobody'")"
  echo "anonymous_caps $(fossil sql -R "$REPO" "SELECT cap FROM user WHERE login='anonymous'")"
} > "$DIR/facts.txt"

echo "ready: $URL (pid $(cat "$DIR/server.pid")); passwords in $DIR/passwords.txt; facts in $DIR/facts.txt; stop with $DIR/stop.sh"
