#!/usr/bin/env bash
# Drive the Maestro E2E tier (Tier 3) end-to-end from a clean machine.
#
#   scripts/dev/run-e2e.sh ios       # iPhone sim (P5)
#   scripts/dev/run-e2e.sh android   # Pixel emulator (P6)
#   scripts/dev/run-e2e.sh ios e2e/flows/cold-boot-seeded.yaml   # one flow
#
# What it owns, in order:
#   1. Resolve the rig (HALFLIGHT_TEST_RIG or secrets/test-rigs/halflight.test.json).
#   2. Mint a fresh Pixelfed access token (the E2E tier has no jest globalSetup,
#      so the stored token would otherwise be stale) — shared mint script.
#   3. Build + install the Debug app and boot the sim/emulator.
#   4. Ensure Metro is up on :8081 (the app has no dev-client; it fetches the
#      bundle from Metro at launch). Started here if absent, torn down on exit.
#   5. For each flow: wipe the app's MMKV state (so it boots to login → the seed
#      subflow's readiness gate is deterministic), place the rig payload file,
#      then run it. The rig is delivered as a file (Documents/devnet/e2e-rig.json
#      on iOS, files/devnet/e2e-rig.json on Android), read by the __DEV__-only
#      `__dev_seed` deep-link handler inside the app.
#
# No hostname or token is ever passed on argv or written to a flow file — the
# rig travels only as the on-device payload file the wrapper places per flow.

set -uo pipefail

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
REPO_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
# shellcheck source=/dev/null
source "$SCRIPT_DIR/sim.env"
cd "$REPO_ROOT"

PLATFORM="${1:-ios}"
FLOW_TARGET="${2:-e2e/flows}"
MAESTRO="$HOME/.maestro/bin/maestro"
export MAESTRO_CLI_NO_ANALYTICS=1
# Set to a device serial for Android (so Maestro targets our E2E AVD, never the
# dev's day-to-day emulator); stays empty on iOS (single booted sim).
MAESTRO_DEVICE=""

# Android SDK tool paths (used to auto-provision the pinned E2E AVD).
SDK_ROOT="${ANDROID_HOME:-$HOME/Library/Android/sdk}"
EMU="$SDK_ROOT/emulator/emulator"
SDKMGR="$SDK_ROOT/cmdline-tools/latest/bin/sdkmanager"
AVDMGR="$SDK_ROOT/cmdline-tools/latest/bin/avdmanager"
case "$(uname -m)" in
  arm64) E2E_ANDROID_ABI="arm64-v8a" ;;
  *) E2E_ANDROID_ABI="x86_64" ;;
esac

note() { echo "[run-e2e] $*"; }
fail() { echo "[run-e2e] ✘ $*" >&2; exit 1; }

usage() {
  cat <<'EOF'
Usage: run-e2e.sh <ios|android> [flow-or-dir]

  run-e2e.sh ios                                   # all flows, iPhone sim
  run-e2e.sh android                               # all flows, pinned API 34 AVD
  run-e2e.sh ios e2e/flows/account-switch.yaml     # a single flow
  run-e2e.sh android e2e/flows/compose-publish-delete.yaml

Owns the whole lifecycle: mint rig token → ensure list fixtures → build +
install → ensure Metro → per-flow wipe + place rig + run → sweep leftover test
posts. The compose flow seeds its own picker photo in-flow via Maestro addMedia.
The Android emulator (a dedicated API 34 AVD, see sim.env) is auto-provisioned.
EOF
}

case "$PLATFORM" in
  -h | --help | help) usage; exit 0 ;;
esac

[ -x "$MAESTRO" ] || fail "Maestro not found at $MAESTRO — install it first (https://maestro.dev)."

# ---- 1. Resolve rig ----------------------------------------------------------
RIG="${HALFLIGHT_TEST_RIG:-$REPO_ROOT/secrets/test-rigs/halflight.test.json}"
[ -f "$RIG" ] || fail "no rig at $RIG — run scripts/dev/bootstrap-test-rig.sh first."
note "rig: $RIG"

# ---- 2. Mint a fresh Pixelfed token (shared single-source script) -----------
note "minting fresh rig token(s)…"
node "$SCRIPT_DIR/refresh-rig-tokens.mjs" "$RIG" || note "token mint hit a snag — relying on in-app 401 self-heal."

# ---- 2b. Ensure organizer fixtures (idempotent, best-effort) ----------------
# The lists-organize flow needs the mastodon bot to have ≥1 follow and ≥1 list,
# or the organizer renders only empty states. Idempotent — a no-op once seeded.
note "ensuring list-organizer fixtures…"
node "$SCRIPT_DIR/ensure-list-fixtures.mjs" "$RIG" || note "fixture seed hit a snag — organizer flow may see empty states."

# ---- Metro lifecycle ---------------------------------------------------------
METRO_PID=""
METRO_LOG="$(mktemp -t halflight-e2e-metro.XXXXXX.log)"
cleanup() {
  if [ -n "$METRO_PID" ]; then
    note "stopping Metro ($METRO_PID) that this run started…"
    kill "$METRO_PID" 2>/dev/null || true
  fi
}
trap cleanup EXIT

metro_up() {
  curl -s --max-time 2 http://localhost:8081/status 2>/dev/null | grep -q 'packager-status:running'
}

ensure_metro() {
  if metro_up; then
    note "Metro already running on :8081 — reusing it."
    return
  fi
  note "starting Metro (logs: $METRO_LOG)…"
  # `< /dev/null` so the Expo CLI doesn't self-terminate on an inherited closed stdin.
  ( npx expo start --dev-client < /dev/null > "$METRO_LOG" 2>&1 ) &
  METRO_PID=$!
  local tries=0
  until metro_up; do
    tries=$((tries + 1))
    [ "$tries" -gt 60 ] && { tail -20 "$METRO_LOG" || true; fail "Metro did not come up on :8081 within ~60s."; }
    sleep 1
  done
  note "Metro is up."
  # Pre-warm the JS bundle so the first cold app launch doesn't pay the full
  # compile inside the flow's readiness wait. Best-effort; the app's real
  # bundle request will compile it anyway if this path drifts.
  local platform_param="ios"; [ "$PLATFORM" = "android" ] && platform_param="android"
  note "pre-warming the $platform_param bundle…"
  curl -s --max-time 180 \
    "http://localhost:8081/.expo/.virtual-metro-entry.bundle?platform=${platform_param}&dev=true" \
    > /dev/null 2>&1 || note "bundle pre-warm skipped (endpoint drift) — first launch will compile."
}

# ---- Platform setup (build / boot / metro / fixture) -------------------------
setup_ios() {
  note "building + installing iOS app (start-sim.sh --ios-only)…"
  "$SCRIPT_DIR/start-sim.sh" --ios-only || fail "iOS build/install failed."
  ensure_metro
  # Fixture photo is seeded in-flow via Maestro's `addMedia` (see
  # e2e/flows/compose-publish-delete.yaml) — no host-side preload needed.
}

# Serial of a *booted* emulator running our pinned E2E AVD (empty if none).
e2e_android_serial() {
  local s name
  for s in $(adb devices | awk '/emulator-[0-9]+[[:space:]]+device/{print $1}'); do
    name=$(adb -s "$s" emu avd name 2>/dev/null | head -1 | tr -d '\r')
    [ "$name" = "$E2E_ANDROID_AVD" ] && { echo "$s"; return 0; }
  done
  return 1
}

# Create the pinned E2E AVD from the system image (one-time). If the image isn't
# installed, print the exact sdkmanager command rather than silently pulling
# ~1.5 GB mid-run.
ensure_android_avd() {
  "$EMU" -list-avds 2>/dev/null | grep -qx "$E2E_ANDROID_AVD" && return 0
  local img="system-images;android-${E2E_ANDROID_API};google_apis;${E2E_ANDROID_ABI}"
  note "E2E AVD '$E2E_ANDROID_AVD' is missing — creating it (one-time)…"
  if ! "$SDKMGR" --list_installed 2>/dev/null | grep -q "android-${E2E_ANDROID_API};google_apis;${E2E_ANDROID_ABI}"; then
    fail "Android ${E2E_ANDROID_API} system image not installed. Install it once:
    \"$SDKMGR\" \"$img\"
  then re-run this script. (The E2E rig pins API ${E2E_ANDROID_API}; Maestro's text
  input hangs on Android 16 / API 36 — see scripts/dev/sim.env.)"
  fi
  echo no | "$AVDMGR" create avd -n "$E2E_ANDROID_AVD" -k "$img" -d pixel_7 --force \
    || fail "could not create AVD '$E2E_ANDROID_AVD'."
}

setup_android() {
  command -v adb >/dev/null || fail "adb not on PATH (set up Android SDK / ANDROID_HOME)."
  [ -x "$EMU" ] || fail "Android emulator not found at $EMU (set ANDROID_HOME)."
  ensure_android_avd

  local serial
  serial=$(e2e_android_serial || true)
  if [ -z "$serial" ]; then
    note "booting E2E emulator ($E2E_ANDROID_AVD)…"
    "$EMU" -avd "$E2E_ANDROID_AVD" -no-snapshot-save -no-boot-anim >/dev/null 2>&1 &
    local tries=0
    until serial=$(e2e_android_serial); do
      tries=$((tries + 1))
      [ "$tries" -gt 120 ] && fail "E2E emulator did not register within ~4min."
      sleep 2
    done
    until [ "$(adb -s "$serial" shell getprop sys.boot_completed 2>/dev/null | tr -d '\r')" = "1" ]; do sleep 2; done
  fi
  # Point every later adb/gradle/Maestro call at this exact device, so a second
  # (day-to-day) emulator running alongside it is never touched.
  export ANDROID_SERIAL="$serial"
  MAESTRO_DEVICE="$serial"
  note "E2E emulator: $serial ($E2E_ANDROID_AVD)"

  note "wiring adb reverse :8081 so the app reaches host Metro…"
  adb reverse tcp:8081 tcp:8081 || true
  ensure_metro
  # Build + install the Debug APK straight to the running emulator via gradle.
  # We avoid `expo run:android` here: it insists on "opening" an emulator by AVD
  # name and chokes on an already-booted one. android/ is prebuilt (JS-only
  # changes don't invalidate it); Maestro's launchApp starts the app.
  note "building + installing Android app (gradle installDebug → $serial)…"
  ( cd android && ./gradlew installDebug ) || fail "Android build/install failed."
  # Fixture photo is seeded in-flow via Maestro's `addMedia` (see
  # e2e/flows/compose-publish-delete.yaml) — no host-side push/scan needed.
}

# ---- Per-flow state wipe (deterministic clean boot → login screen) ----------
# MMKV stores (incl. the `accounts` registry the auth gate reads) live in the
# shared App Group container, which Maestro's clearState does not reliably clear.
# Wiping the store files between flows guarantees each flow starts unseeded.
wipe_ios() {
  xcrun simctl terminate "$IPHONE_UDID" "$IOS_BUNDLE_ID" 2>/dev/null || true
  local dir
  dir=$(xcrun simctl get_app_container "$IPHONE_UDID" "$IOS_BUNDLE_ID" groups 2>/dev/null \
    | awk -F'\t' 'NF>1{print $NF; exit} NF==1{print $1; exit}')
  [ -n "$dir" ] && [ -d "$dir" ] || return 0  # never launched yet → already clean
  local id
  for id in react-query-cache user app accounts; do
    rm -f "$dir/$id" "$dir/$id.crc" 2>/dev/null || true
  done
}

wipe_android() {
  # pm clear wipes app data (incl. MMKV); the fixture in shared /sdcard/Pictures
  # survives, and the privacy photo-picker needs no runtime permission.
  adb shell pm clear "$IOS_BUNDLE_ID" >/dev/null 2>&1 || true
}

# ---- Place the rig payload file the __dev_seed handler reads -----------------
# The app reads Documents/devnet/e2e-rig.json (Paths.document/devnet). Written
# AFTER the wipe (which leaves the data container intact) and BEFORE the flow's
# launchApp. The handler deletes it post-seed so tokens don't linger on disk.
place_rig_ios() {
  local data dir
  data=$(xcrun simctl get_app_container "$IPHONE_UDID" "$IOS_BUNDLE_ID" data 2>/dev/null) \
    || { note "could not resolve app data container (app installed?)"; return 1; }
  dir="$data/Documents/devnet"
  mkdir -p "$dir" && cp "$RIG" "$dir/e2e-rig.json"
}

place_rig_android() {
  # Paths.document on Android = the app's internal files/ dir; reach it via
  # run-as (Debug builds are debuggable).
  adb push "$RIG" /data/local/tmp/e2e-rig.json >/dev/null 2>&1 || return 1
  adb shell run-as "$IOS_BUNDLE_ID" mkdir -p files/devnet >/dev/null 2>&1
  adb shell run-as "$IOS_BUNDLE_ID" cp /data/local/tmp/e2e-rig.json files/devnet/e2e-rig.json >/dev/null 2>&1
  adb shell rm /data/local/tmp/e2e-rig.json >/dev/null 2>&1 || true
}

case "$PLATFORM" in
  ios) setup_ios ;;
  android) setup_android ;;
  *) fail "unknown platform '$PLATFORM' (expected: ios | android)" ;;
esac

# ---- 6. Run each flow: wipe registry → place rig file → run -----------------
# Expand the target into a flow list (a directory → its top-level *.yaml).
FLOWS=()
if [ -d "$FLOW_TARGET" ]; then
  while IFS= read -r f; do FLOWS+=("$f"); done < <(find "$FLOW_TARGET" -maxdepth 1 -name '*.yaml' | sort)
else
  FLOWS=("$FLOW_TARGET")
fi
[ "${#FLOWS[@]}" -gt 0 ] || fail "no flow files found under $FLOW_TARGET"

OVERALL=0
PASSED=()
FAILED=()
for flow in "${FLOWS[@]}"; do
  note "──────────────────────────────────────────────"
  note "wiping app state + placing rig file for a clean boot…"
  case "$PLATFORM" in
    ios) wipe_ios; place_rig_ios || note "rig file placement failed — seed will no-op." ;;
    android) wipe_android; place_rig_android || note "rig file placement failed — seed will no-op." ;;
  esac
  note "running flow: $flow"
  if "$MAESTRO" test ${MAESTRO_DEVICE:+--udid="$MAESTRO_DEVICE"} "$flow"; then
    PASSED+=("$flow")
  else
    FAILED+=("$flow")
    OVERALL=1
  fi
done

note "──────────────────────────────────────────────"
# §10 safety-net: sweep any "halflight e2e" post a crashed/aborted compose flow
# left on the bot. The flow deletes its own post via the UI on a clean run; this
# catches the rest. Best-effort — never fails the run.
note "sweeping any leftover test posts…"
node "$SCRIPT_DIR/cleanup-test-posts.mjs" "$RIG" || true

note "──────────────────────────────────────────────"
note "passed: ${#PASSED[@]} | failed: ${#FAILED[@]}"
for f in "${FAILED[@]:-}"; do [ -n "$f" ] && note "  ✘ $f"; done
[ "$OVERALL" -eq 0 ] && note "✓ all flows passed." || note "✘ some flows failed."
exit "$OVERALL"
