#!/usr/bin/env bash
# Walk the write paths on the astra harness instance, as a logged-in user.
#
# This is the verification the write-enabled instance exists for. The public
# testnot demo is read-only and no-login, so every mutation path is unverified
# against a running box: the CSRF token round-tripping through a real form
# submission, the redirect a handler answers with, whether a moderation grant
# seeded before anyone logged in actually applies once a session exists. Unit
# tests cover the handlers; nothing covered the deployed surface.
#
# What it walks, which is the DONE WHEN of the task that built this instance:
# thread create, post reply, flag, and one moderation action. It logs in for
# real over OAuth against testnot's MNW (PKCE, no client secret), so a broken
# OAuth client, a missing harness account, an expired redirect registration or
# an unseeded community membership all surface here rather than in an audit run.
#
# Two accounts, because one is not enough to walk the loop: a post cannot be
# flagged by its own author, and the moderation action has to be taken by
# somebody holding the grant. `harness_fan` writes, `harness_owner` moderates
# (Owner of `rust`, Moderator of `music` — see multithreaded/src/seed.rs).
#
#   deploy/reset-astra.sh && deploy/harness-walk.sh
#
# Run the reset first when you want a clean read of the mod queue: the walk
# leaves its thread, its flag and its removal behind on purpose, so a failure is
# still there to look at afterwards.
#
# Credentials come from $MT_HARNESS_ENV (default ~/.config/mnw/mt-harness.env,
# mode 600, not in the repo). It carries MT_HARNESS_URL, MT_HARNESS_USERS and
# MT_HARNESS_PASSWORD; the password is the one seeded onto testnot by the MNW
# example seed's harness phase (server/src/seed/harness.rs).
#
# Exit 0 = a browser lens can log in and write here. Exit 1 = it cannot, and the
# FAIL line names the step.
set -uo pipefail

ENV_FILE="${MT_HARNESS_ENV:-$HOME/.config/mnw/mt-harness.env}"
COMMUNITY="${MT_HARNESS_COMMUNITY:-rust}"
CATEGORY="${MT_HARNESS_CATEGORY:-general}"

if [ ! -r "$ENV_FILE" ]; then
    echo "FAIL: no credentials at $ENV_FILE (see multithreaded/deploy/README.md)" >&2
    exit 1
fi
# shellcheck disable=SC1090
. "$ENV_FILE"

: "${MT_HARNESS_URL:?MT_HARNESS_URL not set in $ENV_FILE}"
: "${MT_HARNESS_PASSWORD:?MT_HARNESS_PASSWORD not set in $ENV_FILE}"
MT="${MT_HARNESS_URL%/}"

FAILURES=0
fail() { echo "FAIL: $*" >&2; FAILURES=$((FAILURES + 1)); }
ok()   { echo "  ok: $*"; }
step() { echo; echo "== $*"; }

WORK="$(mktemp -d)"
trap 'rm -rf "$WORK"' EXIT

# ── HTTP helpers ────────────────────────────────────────────────────────────
#
# One cookie jar per logged-in identity. The jar spans both hosts on purpose:
# the login round trip is mt -> testnot -> mt, and the mt session that holds the
# PKCE verifier has to still be there when the callback comes back.

# Value of a hidden form input, as rendered by askama.
hidden_field() { # <file> <name>
    grep -o "name=\"$2\"[^>]*value=\"[^\"]*\"" "$1" | head -1 |
        sed -E 's/.*value="([^"]*)".*/\1/'
}

# The per-session CSRF token, from the meta tag base.html renders (src/csrf.rs).
csrf_token() { # <jar> <path>
    curl -sS -b "$1" -c "$1" "$MT$2" |
        grep -o 'name="csrf-token" content="[0-9a-f]*"' | head -1 |
        sed -E 's/.*content="([0-9a-f]*)".*/\1/'
}

# POST a form to mt and print "<status> <redirect-url>". Takes the token
# explicitly rather than fetching one per call, so a caller that already has a
# page in hand does not pay for a second render.
mt_post() { # <jar> <token> <path> [--data-urlencode k=v ...]
    local jar="$1" token="$2" path="$3"
    shift 3
    curl -sS -b "$jar" -c "$jar" -o /dev/null -w '%{http_code} %{redirect_url}' \
        -H "X-CSRF-Token: $token" -X POST "$@" "$MT$path"
}

# Log in as one harness account and leave the session in <jar>.
#
# mt's /auth/login mints the PKCE pair into its session and redirects to MNW's
# /oauth/authorize, which renders a combined login-and-consent form. Posting
# that form with valid credentials answers a redirect back to mt's /auth/callback
# carrying the code, and following it is what establishes the mt session.
login() { # <jar> <username>
    local jar="$1" user="$2"
    local page="$WORK/authorize-$user.html"

    local authorize_url
    authorize_url=$(curl -sS -b "$jar" -c "$jar" -L -o "$page" -w '%{url_effective}' \
        "$MT/auth/login")
    case "$authorize_url" in
        *"/oauth/authorize"*) ;;
        *) fail "login($user): /auth/login did not reach an authorize page (landed on $authorize_url)"
           return 1 ;;
    esac

    # Everything the consent form round-trips. Losing any one of these is the
    # difference between "the client is unregistered" and "the challenge did not
    # verify", and the server reports them very differently.
    local mnw_base="${authorize_url%%/oauth/authorize*}"
    local csrf client_id redirect_uri state challenge method scope
    csrf=$(hidden_field "$page" _csrf)
    client_id=$(hidden_field "$page" client_id)
    redirect_uri=$(hidden_field "$page" redirect_uri)
    state=$(hidden_field "$page" state)
    challenge=$(hidden_field "$page" code_challenge)
    method=$(hidden_field "$page" code_challenge_method)
    scope=$(hidden_field "$page" scope)

    if [ -z "$client_id" ] || [ -z "$challenge" ]; then
        fail "login($user): authorize page carried no client_id/code_challenge — is the harness client seeded on $mnw_base?"
        return 1
    fi

    local callback
    callback=$(curl -sS -b "$jar" -c "$jar" -o "$WORK/consent-$user.html" -w '%{redirect_url}' \
        -X POST \
        --data-urlencode "login=$user" \
        --data-urlencode "password=$MT_HARNESS_PASSWORD" \
        --data-urlencode "_csrf=$csrf" \
        --data-urlencode "client_id=$client_id" \
        --data-urlencode "redirect_uri=$redirect_uri" \
        --data-urlencode "state=$state" \
        --data-urlencode "code_challenge=$challenge" \
        --data-urlencode "code_challenge_method=$method" \
        --data-urlencode "scope=$scope" \
        "$mnw_base/oauth/authorize")

    case "$callback" in
        *"/auth/callback?"*) ;;
        *) fail "login($user): authorize did not answer a callback redirect (got '${callback:-no redirect}')"
           return 1 ;;
    esac

    curl -sS -b "$jar" -c "$jar" -L -o /dev/null "$callback"

    # Proof of session, not proof of redirect: the account page renders the
    # signed-in username, and a failed login would have redirected away from it.
    if curl -sS -b "$jar" -c "$jar" "$MT/account" | grep -q "$user"; then
        ok "logged in as $user"
        return 0
    fi
    fail "login($user): no session after the callback"
    return 1
}

echo "harness walk against $MT (community: $COMMUNITY/$CATEGORY)"

FAN_JAR="$WORK/fan.jar"
OWNER_JAR="$WORK/owner.jar"

step "reachability"
health=$(curl -sS -o /dev/null -w '%{http_code}' "$MT/api/health" || echo 000)
if [ "$health" = "200" ]; then
    ok "/api/health 200"
else
    fail "/api/health returned $health — is the tailnet proxy up and mt running?"
    echo; echo "harness walk: $FAILURES failure(s)"; exit 1
fi

step "login"
login "$FAN_JAR" harness_fan || { echo; echo "harness walk: $FAILURES failure(s)"; exit 1; }
login "$OWNER_JAR" harness_owner || { echo; echo "harness walk: $FAILURES failure(s)"; exit 1; }

# ── 1. thread create ────────────────────────────────────────────────────────
step "thread create (harness_fan)"
STAMP="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
TITLE="Harness walk $STAMP"
token=$(csrf_token "$FAN_JAR" "/p/$COMMUNITY/$CATEGORY/new")
read -r code location < <(mt_post "$FAN_JAR" "$token" "/p/$COMMUNITY/$CATEGORY/new" \
    --data-urlencode "title=$TITLE" \
    --data-urlencode "body=Written by deploy/harness-walk.sh at $STAMP. Safe to delete.")

THREAD_ID=$(echo "$location" | sed -nE "s#.*/p/$COMMUNITY/$CATEGORY/([0-9a-f-]{36}).*#\1#p")
if [ -n "$THREAD_ID" ]; then
    ok "thread $THREAD_ID created (HTTP $code)"
else
    fail "thread create answered $code, redirect '$location'"
    echo; echo "harness walk: $FAILURES failure(s)"; exit 1
fi

# ── 2. reply ────────────────────────────────────────────────────────────────
step "reply (harness_fan)"
THREAD_PATH="/p/$COMMUNITY/$CATEGORY/$THREAD_ID"
token=$(csrf_token "$FAN_JAR" "$THREAD_PATH")
read -r code location < <(mt_post "$FAN_JAR" "$token" "$THREAD_PATH/reply" \
    --data-urlencode "body=Reply from the harness walk at $STAMP.")
case "$code" in
    30*) ok "reply posted (HTTP $code)" ;;
    *)   fail "reply answered $code" ;;
esac

# The reply is the second post on the thread, and it is what gets flagged: the
# OP belongs to the same account, and a post cannot be flagged by its author.
curl -sS -b "$FAN_JAR" -c "$FAN_JAR" -o "$WORK/thread.html" "$MT$THREAD_PATH"
POST_ID=$(grep -o 'data-post-id="[0-9a-f-]\{36\}"' "$WORK/thread.html" |
    sed -E 's/.*"([0-9a-f-]*)".*/\1/' | sed -n 2p)
if [ -n "$POST_ID" ]; then
    ok "reply rendered as post $POST_ID"
else
    fail "the thread page shows no second post — the reply did not land"
    echo; echo "harness walk: $FAILURES failure(s)"; exit 1
fi

# ── 3. flag ─────────────────────────────────────────────────────────────────
step "flag (harness_owner flags harness_fan's reply)"
token=$(csrf_token "$OWNER_JAR" "$THREAD_PATH")
read -r code location < <(mt_post "$OWNER_JAR" "$token" "$THREAD_PATH/posts/$POST_ID/flag" \
    --data-urlencode "reason=off_topic" \
    --data-urlencode "detail=Filed by deploy/harness-walk.sh at $STAMP.")
case "$code" in
    30*) ok "flag accepted (HTTP $code)" ;;
    *)   fail "flag answered $code" ;;
esac

# ── 4. moderation ───────────────────────────────────────────────────────────
#
# Removing the flagged post from the queue rather than pinning a thread, because
# it exercises the grant and the queue in one move: the flag has to have reached
# the moderation page for its id to be here at all.
step "moderation (harness_owner removes the flagged post)"
curl -sS -b "$OWNER_JAR" -c "$OWNER_JAR" -o "$WORK/moderation.html" "$MT/p/$COMMUNITY/moderation"
FLAG_ID=$(grep -o "moderation/flags/[0-9a-f-]\{36\}/remove" "$WORK/moderation.html" | head -1 |
    sed -E 's#.*/flags/([0-9a-f-]*)/remove#\1#')
if [ -n "$FLAG_ID" ]; then
    ok "flag $FLAG_ID is in the moderation queue"
else
    fail "the moderation queue shows no flag — either the flag did not land or the Owner grant did not apply"
    echo; echo "harness walk: $FAILURES failure(s)"; exit 1
fi

token=$(csrf_token "$OWNER_JAR" "/p/$COMMUNITY/moderation")
read -r code location < <(mt_post "$OWNER_JAR" "$token" "/p/$COMMUNITY/moderation/flags/$FLAG_ID/remove")
case "$code" in
    30*) ok "post removed through the flag (HTTP $code)" ;;
    *)   fail "flag removal answered $code" ;;
esac

# ── 5. the writes are visible ───────────────────────────────────────────────
#
# Every step above can answer a redirect without changing anything a reader
# sees, which is the whole reason the browser axis exists. Read the result back.
step "read-back"
curl -sS -b "$OWNER_JAR" -c "$OWNER_JAR" -o "$WORK/thread-after.html" "$MT$THREAD_PATH"
grep -q "Harness walk $STAMP" "$WORK/thread-after.html" &&
    ok "thread title renders" || fail "the thread page does not show the title we wrote"
grep -q "post-removed" "$WORK/thread-after.html" &&
    ok "the removed post renders as removed" || fail "the flagged post does not render as removed"

curl -sS -b "$OWNER_JAR" -c "$OWNER_JAR" -o "$WORK/modlog.html" "$MT/p/$COMMUNITY/moderation/log"
grep -qi "harness_owner" "$WORK/modlog.html" &&
    ok "the moderation log records the actor" || fail "the moderation log does not name harness_owner"

echo
if [ "$FAILURES" -eq 0 ]; then
    echo "harness walk: all steps passed — a browser lens can log in and write here"
    exit 0
fi
echo "harness walk: $FAILURES failure(s)"
exit 1
