#!/usr/bin/env bash
# bootstrap-test-rig.sh — one-shot OAuth-code-with-PKCE flow to mint a test
# access token for a Mastodon or Pixelfed instance and merge it into
# `secrets/test-rigs/halflight.test.json`.
#
# Why PKCE and not password grant?
#   - Mastodon's docs: "Password Grant ... has subsequently been removed from
#     future versions of Mastodon." (docs.joinmastodon.org/spec/oauth/)
#   - Pixelfed runs Laravel Passport with `enablePasswordGrant()` NOT called
#     in app/Providers/AuthServiceProvider.php — i.e. password grant is off
#     by default.
#   - That leaves authorization-code-with-PKCE, OOB redirect form, as the
#     only universally working path.
#
# Workflow:
#   1. Prompt for instance host + protocol type.
#   2. Register an OAuth client via POST /api/v1/apps (OOB redirect).
#   3. Print the authorize URL — user signs in as the *test bot account* and
#      pastes the resulting authorization code back.
#   4. Exchange the code via POST /oauth/token. Capture access_token (and
#      refresh_token if the server issues one — Mastodon doesn't, Pixelfed
#      Passport does by default).
#   5. Call GET /api/v1/accounts/verify_credentials to capture userId,
#      username, acct, displayName.
#   6. Merge the entry into the rig JSON. Generates a fresh UUIDv4 if this
#      is a new slot; preserves the existing accountId on re-auth.
#   7. Append the host to `scripts/dev/banned-hosts.txt` so the release-zip
#      grep can refuse to ship it.
#
# Re-running for an existing (instance, userId) pair refreshes the token
# without rotating the accountId — matches the AccountStore upsert contract.

set -euo pipefail

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
REPO_ROOT="$(cd "${SCRIPT_DIR}/../.." && pwd)"
RIG_DIR="${REPO_ROOT}/secrets/test-rigs"
RIG_FILE="${RIG_DIR}/halflight.test.json"
BANNED_HOSTS_FILE="${SCRIPT_DIR}/banned-hosts.txt"

# --- preflight ---------------------------------------------------------------

for cmd in jq curl openssl python3; do
  if ! command -v "${cmd}" >/dev/null 2>&1; then
    echo "FAIL: required command not found: ${cmd}"
    exit 1
  fi
done

mkdir -p "${RIG_DIR}"

# --- prompt for slot + instance ---------------------------------------------

cat <<'BANNER'

============================================================
 Halflight test-rig bootstrap
============================================================

WARNING: Use a DEDICATED TEST BOT ACCOUNT, not your personal
account. Tests may create posts, follows, and other state on
this account. Cleanup is best-effort.

BANNER

read -r -p "Slot name (e.g. 'pixelfed', 'mastodon', 'pixelfed-secondary'): " SLOT
[ -n "${SLOT}" ] || { echo "FAIL: slot name required"; exit 1; }

read -r -p "Account type [pixelfed/mastodon]: " TYPE
case "${TYPE}" in
  pixelfed|mastodon) ;;
  *) echo "FAIL: type must be 'pixelfed' or 'mastodon'"; exit 1 ;;
esac

read -r -p "Instance host (e.g. mastodon.example, no scheme): " INSTANCE
INSTANCE=$(echo "${INSTANCE}" | sed -e 's|^https\?://||' -e 's|/$||' -e 's/[[:space:]]//g')
[ -n "${INSTANCE}" ] || { echo "FAIL: instance host required"; exit 1; }

CLIENT_NAME="Halflight Test Rig (${SLOT})"
REDIRECT_URI="urn:ietf:wg:oauth:2.0:oob"
SCOPES="read write follow push"

# --- step 1: register OAuth client ------------------------------------------

echo
echo "Registering OAuth client on ${INSTANCE} ..."
APP_RESPONSE=$(curl -fsS -X POST "https://${INSTANCE}/api/v1/apps" \
  -H "Accept: application/json" \
  --data-urlencode "client_name=${CLIENT_NAME}" \
  --data-urlencode "redirect_uris=${REDIRECT_URI}" \
  --data-urlencode "scopes=${SCOPES}" \
  --data-urlencode "website=https://halflight.app")

CLIENT_ID=$(echo "${APP_RESPONSE}" | jq -r '.client_id // empty')
CLIENT_SECRET=$(echo "${APP_RESPONSE}" | jq -r '.client_secret // empty')

if [ -z "${CLIENT_ID}" ] || [ -z "${CLIENT_SECRET}" ]; then
  echo "FAIL: /api/v1/apps did not return client_id/client_secret"
  echo "${APP_RESPONSE}"
  exit 2
fi

# --- step 2: PKCE code-verifier + code-challenge ----------------------------

CODE_VERIFIER=$(openssl rand -base64 64 | tr -d '\n=+/' | cut -c1-128)
CODE_CHALLENGE=$(printf "%s" "${CODE_VERIFIER}" \
  | openssl dgst -binary -sha256 \
  | openssl base64 \
  | tr '+/' '-_' \
  | tr -d '=\n')

ENC_SCOPES=$(python3 -c "import sys, urllib.parse; print(urllib.parse.quote(sys.argv[1]))" "${SCOPES}")
ENC_REDIRECT=$(python3 -c "import sys, urllib.parse; print(urllib.parse.quote(sys.argv[1], safe=''))" "${REDIRECT_URI}")
AUTHORIZE_URL="https://${INSTANCE}/oauth/authorize?response_type=code"
AUTHORIZE_URL+="&client_id=${CLIENT_ID}"
AUTHORIZE_URL+="&redirect_uri=${ENC_REDIRECT}"
AUTHORIZE_URL+="&scope=${ENC_SCOPES}"
AUTHORIZE_URL+="&code_challenge=${CODE_CHALLENGE}"
AUTHORIZE_URL+="&code_challenge_method=S256"

cat <<EOF

------------------------------------------------------------
 1) Open this URL in a browser:

    ${AUTHORIZE_URL}

 2) Sign in as the TEST BOT ACCOUNT (not your personal account).
 3) Click Authorize.
 4) Copy the authorization code from the resulting page and paste it below.
------------------------------------------------------------

EOF

read -r -p "Authorization code: " AUTH_CODE
[ -n "${AUTH_CODE}" ] || { echo "FAIL: code required"; exit 1; }

# --- step 3: exchange code → access token -----------------------------------

echo
echo "Exchanging code for access token ..."
TOKEN_RESPONSE=$(curl -fsS -X POST "https://${INSTANCE}/oauth/token" \
  -H "Accept: application/json" \
  --data-urlencode "grant_type=authorization_code" \
  --data-urlencode "client_id=${CLIENT_ID}" \
  --data-urlencode "client_secret=${CLIENT_SECRET}" \
  --data-urlencode "redirect_uri=${REDIRECT_URI}" \
  --data-urlencode "code=${AUTH_CODE}" \
  --data-urlencode "code_verifier=${CODE_VERIFIER}" \
  --data-urlencode "scope=${SCOPES}")

ACCESS_TOKEN=$(echo "${TOKEN_RESPONSE}" | jq -r '.access_token // empty')
REFRESH_TOKEN=$(echo "${TOKEN_RESPONSE}" | jq -r '.refresh_token // empty')

if [ -z "${ACCESS_TOKEN}" ]; then
  echo "FAIL: /oauth/token did not return access_token"
  echo "${TOKEN_RESPONSE}"
  exit 3
fi

# --- step 4: verify_credentials → identity ---------------------------------

echo "Fetching verify_credentials ..."
VC_RESPONSE=$(curl -fsS "https://${INSTANCE}/api/v1/accounts/verify_credentials" \
  -H "Authorization: Bearer ${ACCESS_TOKEN}" \
  -H "Accept: application/json")

USER_ID=$(echo "${VC_RESPONSE}" | jq -r '.id // empty')
USERNAME=$(echo "${VC_RESPONSE}" | jq -r '.username // empty')
ACCT=$(echo "${VC_RESPONSE}" | jq -r '.acct // .username // empty')
DISPLAY_NAME=$(echo "${VC_RESPONSE}" | jq -r '.display_name // .username // empty')
AVATAR_URL=$(echo "${VC_RESPONSE}" | jq -r '.avatar // empty')

if [ -z "${USER_ID}" ] || [ -z "${USERNAME}" ]; then
  echo "FAIL: verify_credentials did not return id/username"
  echo "${VC_RESPONSE}"
  exit 4
fi

# --- step 4.5: verify non-rotating refresh (Pixelfed Passport only) ----------
# Replay the SAME refresh token twice. With the [halflight-patch: oauth-non-
# rotating-refresh] server patch both succeed (the original refresh token id
# stays replayable for ~400 days). Without it, the first refresh revokes the
# refresh token and the second 400s — which is exactly the failure that
# silently strands the rig one run later. Catching it here turns a multi-run
# mystery into a one-line warning at setup. Mastodon issues no refresh token,
# so this whole block is skipped there.
if [ -n "${REFRESH_TOKEN}" ]; then
  echo
  echo "Verifying non-rotating refresh (replaying the refresh token twice) ..."

  refresh_once() {
    curl -sS -X POST "https://${INSTANCE}/oauth/token" \
      -H "Accept: application/json" \
      --data-urlencode "grant_type=refresh_token" \
      --data-urlencode "refresh_token=${REFRESH_TOKEN}" \
      --data-urlencode "client_id=${CLIENT_ID}" \
      --data-urlencode "client_secret=${CLIENT_SECRET}" \
      --data-urlencode "scope=${SCOPES}"
  }

  REFRESH_RESP_1=$(refresh_once || true)
  A_NEW_1=$(echo "${REFRESH_RESP_1}" | jq -r '.access_token // empty' 2>/dev/null || true)
  R_NEW_1=$(echo "${REFRESH_RESP_1}" | jq -r '.refresh_token // empty' 2>/dev/null || true)
  REFRESH_RESP_2=$(refresh_once || true)
  A_NEW_2=$(echo "${REFRESH_RESP_2}" | jq -r '.access_token // empty' 2>/dev/null || true)

  if [ -n "${A_NEW_1}" ] && [ -n "${A_NEW_2}" ]; then
    echo "  OK non-rotating refresh confirmed — the refresh token replays."
    # The original access token was revoked by these refreshes; store the
    # latest valid one. The refresh token (original) stays durable.
    ACCESS_TOKEN="${A_NEW_2}"
  else
    echo "  ! WARNING: the refresh token did NOT replay (rotating refresh)."
    echo "    The server is missing [halflight-patch: oauth-non-rotating-refresh]."
    echo "    The rig will burn its refresh token on each run until that patch"
    echo "    deploys; you'll need to re-bootstrap before every test session."
    # Salvage whatever is still valid so at least one run works: the first
    # refresh issued a fresh pair before the second call invalidated the old
    # refresh token.
    [ -n "${A_NEW_1}" ] && ACCESS_TOKEN="${A_NEW_1}"
    [ -n "${R_NEW_1}" ] && REFRESH_TOKEN="${R_NEW_1}"
  fi
fi

# --- step 5: merge into rig JSON --------------------------------------------

TIMESTAMP=$(date -u +%Y-%m-%dT%H:%M:%SZ)

# If the rig file is empty / placeholder / nonexistent, scaffold a fresh root.
if [ ! -s "${RIG_FILE}" ] || ! jq -e '.version == 1' "${RIG_FILE}" >/dev/null 2>&1; then
  echo '{"version":1,"createdAt":"","rigs":{}}' > "${RIG_FILE}"
fi
# Drop the placeholder marker if it's still there.
TMP=$(mktemp)
jq 'del(.placeholder) | del(._notes) | del(."$schema")' "${RIG_FILE}" > "${TMP}" && mv "${TMP}" "${RIG_FILE}"

# Preserve an existing slot's accountId if it matches (instance, userId);
# otherwise mint a fresh UUIDv4 here in the shell so the rig JSON is the
# canonical handle and the test code never has to invent one.
EXISTING_ACCOUNT_ID=$(jq -r --arg slot "${SLOT}" --arg inst "${INSTANCE}" --arg uid "${USER_ID}" \
  '.rigs[$slot] | select(.instance == $inst and .userId == $uid) | .accountId // empty' \
  "${RIG_FILE}" 2>/dev/null || true)

if [ -n "${EXISTING_ACCOUNT_ID}" ]; then
  ACCOUNT_ID="${EXISTING_ACCOUNT_ID}"
  echo "Re-auth: preserving existing accountId ${ACCOUNT_ID}"
else
  # UUIDv4 via python (portable across macOS / Linux).
  ACCOUNT_ID=$(python3 -c "import uuid; print(uuid.uuid4())")
fi

# Build the new entry with jq so values get JSON-escaped properly.
NEW_ENTRY=$(jq -n \
  --arg type "${TYPE}" \
  --arg instance "${INSTANCE}" \
  --arg accountId "${ACCOUNT_ID}" \
  --arg userId "${USER_ID}" \
  --arg username "${USERNAME}" \
  --arg acct "${ACCT}" \
  --arg displayName "${DISPLAY_NAME}" \
  --arg avatarUrl "${AVATAR_URL}" \
  --arg clientId "${CLIENT_ID}" \
  --arg clientSecret "${CLIENT_SECRET}" \
  --arg accessToken "${ACCESS_TOKEN}" \
  --arg refreshToken "${REFRESH_TOKEN}" \
  --arg tokenObtainedAt "${TIMESTAMP}" \
  --arg scopes "${SCOPES}" \
  '{
    type: $type,
    instance: $instance,
    accountId: $accountId,
    userId: $userId,
    username: $username,
    acct: $acct,
    displayName: $displayName,
    avatarUrl: (if $avatarUrl == "" then null else $avatarUrl end),
    oauthClientId: $clientId,
    oauthClientSecret: $clientSecret,
    accessToken: $accessToken,
    refreshToken: (if $refreshToken == "" then null else $refreshToken end),
    tokenObtainedAt: $tokenObtainedAt,
    scopes: $scopes
  }')

TMP=$(mktemp)
jq --arg slot "${SLOT}" \
   --argjson entry "${NEW_ENTRY}" \
   --arg createdAt "${TIMESTAMP}" \
   '
    .createdAt = (.createdAt // $createdAt) |
    (.createdAt = (if (.createdAt | length) > 0 then .createdAt else $createdAt end)) |
    .version = 1 |
    .rigs[$slot] = $entry
   ' "${RIG_FILE}" > "${TMP}"
mv "${TMP}" "${RIG_FILE}"

chmod 600 "${RIG_FILE}"

# --- step 6: append host to banned-hosts so release-zip refuses to ship it --

touch "${BANNED_HOSTS_FILE}"
if ! grep -Fxq "${INSTANCE}" "${BANNED_HOSTS_FILE}"; then
  echo "${INSTANCE}" >> "${BANNED_HOSTS_FILE}"
fi
chmod 600 "${BANNED_HOSTS_FILE}"

cat <<EOF

============================================================
 Rig entry written:
   slot:        ${SLOT}
   type:        ${TYPE}
   instance:    ${INSTANCE}
   accountId:   ${ACCOUNT_ID}
   userId:      ${USER_ID}
   username:    ${USERNAME}
   refresh tok: $([ -n "${REFRESH_TOKEN}" ] && echo "present" || echo "(none — normal on Mastodon)")
============================================================

Next:
  npm run test:integration
EOF
