#!/usr/bin/env bash
#
# Unified WDIO E2E runner — one Appium session, one CEF app launch, all specs.
#
# Architecture:
#   1. Build artefacts must exist (run `pnpm test:e2e:build` first).
#   2. Clean cached app data + write a fresh E2E config.toml pointing at the
#      shared mock backend.
#   3. Launch the built CEF binary directly (cross-platform).
#   4. Wait for the CEF process to expose CDP on 127.0.0.1:19222.
#   5. Start Appium with the `chromium` driver.
#   6. Run wdio against `test/wdio.conf.ts`, which attaches to the running
#      CEF via Appium's Chromium driver. All specs share one session.
#   7. Tear everything down (Appium → CEF → workspace).
#
# Usage:
#   ./app/scripts/e2e-run-session.sh                          # whole suite
#   ./app/scripts/e2e-run-session.sh test/e2e/specs/foo.spec.ts  # single spec
#
set -euo pipefail

# Accept either:
#   - Zero args             → run the entire `specs` glob from wdio.conf.ts
#   - One spec path arg     → legacy single-spec mode (e2e-run-spec.sh shim)
#   - One spec + log suffix → legacy two-arg mode used by debug runner / CI
#   - N>1 spec paths        → multi-spec mode, one shared session
#
# To disambiguate "spec + suffix" from "two specs", we treat arg2 as a log
# suffix only when it does NOT look like a spec path (i.e. doesn't end in
# `.spec.ts` and doesn't start with `test/`).
SPEC_ARGS=()
LOG_SUFFIX="session"
if [ "$#" -ge 1 ]; then
  SPEC_ARGS+=("$1")
  if [ "$#" -eq 2 ] && [[ "$2" != *.spec.ts && "$2" != test/* ]]; then
    LOG_SUFFIX="$2"
  else
    shift
    while [ "$#" -gt 0 ]; do
      SPEC_ARGS+=("$1")
      shift
    done
  fi
fi
# Back-compat: SPEC_ARG is the first spec (only used in stale log lines below).
SPEC_ARG="${SPEC_ARGS[0]:-}"

E2E_MOCK_PORT="${E2E_MOCK_PORT:-18473}"
CEF_CDP_PORT="${CEF_CDP_PORT:-19222}"
APPIUM_PORT="${APPIUM_PORT:-4723}"
E2E_CDP_READY_TIMEOUT_SECONDS="${E2E_CDP_READY_TIMEOUT_SECONDS:-180}"
OS="$(uname)"

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
APP_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
REPO_ROOT="$(cd "$APP_DIR/.." && pwd)"
cd "$APP_DIR"

CREATED_TEMP_WORKSPACE=""
APPIUM_PID=""
APP_PID=""
E2E_CONFIG_BACKUP=""
E2E_CONFIG_FILE=""
CREATED_TEMP_CEF_CACHE=""

# ------------------------------------------------------------------------------
# Workspace + config
# ------------------------------------------------------------------------------
if [ -z "${OPENHUMAN_WORKSPACE:-}" ]; then
  OPENHUMAN_WORKSPACE="$(mktemp -d)"
  CREATED_TEMP_WORKSPACE="$OPENHUMAN_WORKSPACE"
  export OPENHUMAN_WORKSPACE
  echo "[runner] Using temporary OPENHUMAN_WORKSPACE: $OPENHUMAN_WORKSPACE"
else
  echo "[runner] Using OPENHUMAN_WORKSPACE from environment: $OPENHUMAN_WORKSPACE"
fi

# Headless Linux CI does not always have a usable Secret Service/keychain.
# Keep E2E credentials under OPENHUMAN_WORKSPACE so auth state is deterministic
# and gets cleaned up with the rest of the test workspace.
: "${OPENHUMAN_KEYRING_BACKEND:=file}"
export OPENHUMAN_KEYRING_BACKEND
echo "[runner] Using OPENHUMAN_KEYRING_BACKEND: $OPENHUMAN_KEYRING_BACKEND"

# Place the CEF cache directory OUTSIDE the workspace. By default the Tauri
# shell roots it under `$OPENHUMAN_WORKSPACE/users/<id>/cef`, but our
# `mega-flow` spec calls `openhuman.config_reset_local_data` between
# sub-scenarios — that RPC does `remove_dir_all($OPENHUMAN_WORKSPACE)`,
# which yanks CEF's cache out from under the running process and kills
# the WebDriver session (every later sub-test then fails with
# "invalid session id"). Pointing CEF at a sibling tmpdir via the
# `OPENHUMAN_CEF_CACHE_PATH` escape hatch (`cef_profile.rs:7`) keeps it
# unaffected by the reset.
if [ -z "${OPENHUMAN_CEF_CACHE_PATH:-}" ]; then
  OPENHUMAN_CEF_CACHE_PATH="$(mktemp -d)"
  CREATED_TEMP_CEF_CACHE="$OPENHUMAN_CEF_CACHE_PATH"
  export OPENHUMAN_CEF_CACHE_PATH
  echo "[runner] Using temporary OPENHUMAN_CEF_CACHE_PATH: $OPENHUMAN_CEF_CACHE_PATH"
fi

if [ "${OPENHUMAN_SERVICE_MOCK:-0}" = "1" ] && [ -z "${OPENHUMAN_SERVICE_MOCK_STATE_FILE:-}" ]; then
  OPENHUMAN_SERVICE_MOCK_STATE_FILE="$OPENHUMAN_WORKSPACE/service-mock-state.json"
  export OPENHUMAN_SERVICE_MOCK_STATE_FILE
fi

cleanup() {
  local status=$?
  set +e
  if [ -n "$APPIUM_PID" ]; then
    echo "[runner] Stopping Appium (pid $APPIUM_PID)..."
    kill "$APPIUM_PID" 2>/dev/null || true
    wait "$APPIUM_PID" 2>/dev/null || true
  fi
  if [ -n "$APP_PID" ]; then
    echo "[runner] Stopping CEF app (pid $APP_PID)..."
    # CEF spawns helper child processes (zygote, GPU, renderers) that
    # the parent does not reap on SIGTERM. If we only `kill $APP_PID`
    # the parent exits but children keep writing into the temp
    # workspace, and the `rm -rf` below races them and fails with
    # "Directory not empty" on Linux runners — even though the WDIO
    # spec itself passed. Reap the whole process tree before cleanup.
    #
    # CRITICAL: capture child PIDs **before** killing the parent.
    # The instant the parent exits, the kernel reparents its children
    # to init (PID 1). After that, `pkill -P "$APP_PID"` matches
    # nothing because no process has the dying parent as its PPID
    # anymore. Snapshot the PIDs while the relationship still exists,
    # then signal them directly by PID.
    CHILD_PIDS="$(pgrep -P "$APP_PID" 2>/dev/null || true)"
    pkill -TERM -P "$APP_PID" 2>/dev/null || true
    kill "$APP_PID" 2>/dev/null || true
    wait "$APP_PID" 2>/dev/null || true
    # Brief grace period so CEF helpers can flush their CEF/Default
    # files and exit on the SIGTERM we already sent. Anything that
    # ignored it gets SIGKILLed by the captured-PID sweep below.
    sleep 1
    if [ -n "$CHILD_PIDS" ]; then
      for pid in $CHILD_PIDS; do
        kill -KILL "$pid" 2>/dev/null || true
      done
    fi
  fi
  if [ -n "$CREATED_TEMP_WORKSPACE" ]; then
    for attempt in 1 2 3; do
      rm -rf "$CREATED_TEMP_WORKSPACE" 2>/dev/null && break
      echo "[runner] Warning: temporary workspace cleanup failed (attempt $attempt): $CREATED_TEMP_WORKSPACE" >&2
      sleep "$attempt"
    done
    if [ -e "$CREATED_TEMP_WORKSPACE" ]; then
      echo "[runner] Warning: leaving temporary workspace after cleanup retries: $CREATED_TEMP_WORKSPACE" >&2
    fi
  fi
  if [ -n "$CREATED_TEMP_CEF_CACHE" ]; then
    rm -rf "$CREATED_TEMP_CEF_CACHE" 2>/dev/null || true
  fi
  if [ -n "$E2E_CONFIG_BACKUP" ] && [ -f "$E2E_CONFIG_BACKUP" ]; then
    mv "$E2E_CONFIG_BACKUP" "$E2E_CONFIG_FILE" \
      || echo "[runner] Warning: failed to restore E2E config backup: $E2E_CONFIG_BACKUP" >&2
  elif [ -n "$E2E_CONFIG_FILE" ] && [ -f "$E2E_CONFIG_FILE" ]; then
    rm -f "$E2E_CONFIG_FILE" \
      || echo "[runner] Warning: failed to remove generated E2E config: $E2E_CONFIG_FILE" >&2
  fi
  return "$status"
}
trap cleanup EXIT

export VITE_BACKEND_URL="http://127.0.0.1:${E2E_MOCK_PORT}"
export BACKEND_URL="http://127.0.0.1:${E2E_MOCK_PORT}"
export OPENHUMAN_E2E_MODE="1"
export APPIUM_PORT
export CEF_CDP_PORT
# Redirect Telegram Bot API calls to the mock server during E2E runs.
# The mock server (WS-A) serves /bot<token>/* routes on the same port as the
# rest of the mock backend.  The core reads this at TelegramChannel::new() time,
# which runs after the config is fully loaded.
export OPENHUMAN_TELEGRAM_BOT_API_BASE="http://127.0.0.1:${E2E_MOCK_PORT}"
export OPENHUMAN_COMPOSIO_DIRECT_BASE_V2="http://127.0.0.1:${E2E_MOCK_PORT}"
export OPENHUMAN_COMPOSIO_DIRECT_BASE_V3="http://127.0.0.1:${E2E_MOCK_PORT}"

echo "[runner] Killing any running OpenHuman instances..."
case "$OS" in
  Darwin) pkill -f "OpenHuman" 2>/dev/null || true ;;
  Linux)  pkill -f "OpenHuman" 2>/dev/null || true ;;
  MINGW*|MSYS*|CYGWIN*|Windows_NT)
    taskkill //F //IM "OpenHuman.exe" 2>/dev/null || true
    ;;
esac
sleep 1

echo "[runner] Cleaning cached app data..."
case "$OS" in
  Darwin)
    rm -rf ~/Library/WebKit/com.openhuman.app
    rm -rf ~/Library/Caches/com.openhuman.app
    rm -rf "$HOME/Library/Application Support/com.openhuman.app"
    rm -rf "$HOME/Library/Saved Application State/com.openhuman.app.savedState"
    ;;
  Linux)
    rm -rf "$HOME/.local/share/com.openhuman.app" 2>/dev/null || true
    rm -rf "$HOME/.cache/com.openhuman.app" 2>/dev/null || true
    rm -rf "$HOME/.config/com.openhuman.app" 2>/dev/null || true
    ;;
  MINGW*|MSYS*|CYGWIN*|Windows_NT)
    rm -rf "${APPDATA:-$HOME/AppData/Roaming}/com.openhuman.app" 2>/dev/null || true
    rm -rf "${LOCALAPPDATA:-$HOME/AppData/Local}/com.openhuman.app" 2>/dev/null || true
    ;;
esac

# Mock URL must reach the core sidecar — XCUITest doesn't inherit env,
# and CEF child processes won't either. Pinning via config.toml works
# on every platform. The runner always sets OPENHUMAN_WORKSPACE above;
# Config::load_or_init gives that path precedence over $HOME/.openhuman.
E2E_CONFIG_DIR="${OPENHUMAN_WORKSPACE:-$HOME/.openhuman}"
E2E_CONFIG_FILE="$E2E_CONFIG_DIR/config.toml"
mkdir -p "$E2E_CONFIG_DIR"
if [ -f "$E2E_CONFIG_FILE" ]; then
  E2E_CONFIG_BACKUP="$E2E_CONFIG_FILE.e2e-backup.$$"
  cp "$E2E_CONFIG_FILE" "$E2E_CONFIG_BACKUP"
fi

# Write a complete E2E config that routes ALL LLM inference through the mock
# server via OpenAiCompatibleProvider (supports_streaming=true).
#
# WHY pre-populate cloud_providers here:
#   The unify_ai_provider_settings migration runs on first startup. If
#   cloud_providers is empty it seeds an OpenHuman entry and sets primary_cloud
#   to that entry — which routes all inference to OpenHumanBackendProvider
#   (supports_streaming=false, always returns non-streaming responses, so the
#   mock server never receives /openai/v1/chat/completions).
#
#   By pre-populating [[cloud_providers]] with a "none" auth mock entry and
#   setting primary_cloud to its id, the migration sees !is_empty() and skips
#   seeding entirely. provider_for_role() resolves unset workloads via
#   primary_cloud → slug "e2e" (non-openhuman) → returns "e2e:" →
#   make_cloud_provider_by_slug → auth_style=none → OpenAiCompatibleProvider
#   → supports_streaming=true → streams to mock at /openai/v1/chat/completions.
cat > "$E2E_CONFIG_FILE" << TOMLEOF
api_url = "http://127.0.0.1:${E2E_MOCK_PORT}"
primary_cloud = "p_e2e_mock"
default_model = "e2e-mock-model"
chat_provider = "e2e:e2e-mock-model"
reasoning_provider = "e2e:e2e-mock-model"
agentic_provider = "e2e:e2e-mock-model"
coding_provider = "e2e:e2e-mock-model"

[[cloud_providers]]
id = "p_e2e_mock"
slug = "e2e"
label = "E2E Mock"
endpoint = "http://127.0.0.1:${E2E_MOCK_PORT}/openai/v1"
auth_style = "none"
default_model = "e2e-mock-model"
TOMLEOF
echo "[runner] Wrote E2E config.toml routing inference to mock at http://127.0.0.1:${E2E_MOCK_PORT}"

DIST_JS="$(ls dist/assets/index-*.js 2>/dev/null | head -1)"
if [ -z "$DIST_JS" ]; then
  echo "ERROR: No frontend bundle found at dist/assets/index-*.js." >&2
  echo "       Run 'pnpm test:e2e:build' first." >&2
  exit 1
fi
if ! grep -q "127.0.0.1:${E2E_MOCK_PORT}" "$DIST_JS"; then
  echo "ERROR: frontend bundle does NOT contain mock server URL (127.0.0.1:${E2E_MOCK_PORT})." >&2
  echo "       Run 'pnpm test:e2e:build' to rebuild." >&2
  exit 1
fi

# ------------------------------------------------------------------------------
# Resolve the built CEF binary for this platform
# ------------------------------------------------------------------------------
resolve_app_binary() {
  case "$OS" in
    Darwin)
      for base in \
        "$APP_DIR/src-tauri/target/debug/bundle/macos/OpenHuman.app/Contents/MacOS/OpenHuman" \
        "$REPO_ROOT/target/debug/bundle/macos/OpenHuman.app/Contents/MacOS/OpenHuman"; do
        if [ -x "$base" ]; then echo "$base"; return; fi
      done
      ;;
    Linux)
      for candidate in \
        "$APP_DIR/src-tauri/target/debug/OpenHuman" \
        "$REPO_ROOT/target/debug/OpenHuman"; do
        if [ -x "$candidate" ]; then echo "$candidate"; return; fi
      done
      ;;
    MINGW*|MSYS*|CYGWIN*|Windows_NT)
      for candidate in \
        "$APP_DIR/src-tauri/target/debug/OpenHuman.exe" \
        "$REPO_ROOT/target/debug/OpenHuman.exe"; do
        if [ -x "$candidate" ]; then echo "$candidate"; return; fi
      done
      ;;
  esac
}

APP_BIN="$(resolve_app_binary)"
if [ -z "${APP_BIN:-}" ] || [ ! -x "$APP_BIN" ]; then
  echo "ERROR: built OpenHuman binary not found. Run 'pnpm test:e2e:build' first." >&2
  exit 1
fi

# ------------------------------------------------------------------------------
# Ensure a dbus session bus exists on Linux.
#
# The Tauri `single-instance` plugin (used inside OpenHuman) talks to dbus
# via zbus on Linux. If DBUS_SESSION_BUS_ADDRESS is missing or set to
# `disabled:` (which is the openhuman_ci container default), the plugin
# panics during plugin setup:
#   panicked at plugins/single-instance/src/platform_impl/linux.rs:57
#   Result::unwrap() on Err(Address("unsupported transport 'disabled'"))
# So start a real session bus with `dbus-launch` and inherit its
# DBUS_SESSION_BUS_ADDRESS for the rest of the runner.
# ------------------------------------------------------------------------------
if [ "$OS" = "Linux" ]; then
  if [ -z "${DBUS_SESSION_BUS_ADDRESS:-}" ] || \
     printf '%s' "${DBUS_SESSION_BUS_ADDRESS:-}" | grep -q '^disabled'; then
    if command -v dbus-launch >/dev/null 2>&1; then
      DBUS_LAUNCH_OUT="$(dbus-launch --sh-syntax)"
      eval "$DBUS_LAUNCH_OUT"
      echo "[runner] Started dbus session bus: $DBUS_SESSION_BUS_ADDRESS"
    else
      echo "[runner] Warning: dbus-launch not available — single-instance plugin may panic."
    fi
  fi
fi

# ------------------------------------------------------------------------------
# Make CEF runtime libraries discoverable.
#
# macOS bundles the framework into `OpenHuman.app/Contents/Frameworks/` so the
# OS resolves it automatically. On Linux + Windows we build with --no-bundle
# (faster, no .deb / .msi staging needed for tests), which means the bare
# binary has no co-located libcef.so / libcef.dll. CEF lives in the
# vendored-tauri-cli cache that `ensure-tauri-cli.sh` pinned via CEF_PATH:
#   $CEF_PATH/<cef-version>/cef_<os>_<arch>/{libcef.so,libcef.dll,Resources,…}
#
# We find the only versioned directory under CEF_PATH and prepend its CEF
# dist dir to LD_LIBRARY_PATH (Linux) / PATH (Windows). On macOS this is a
# no-op — the .app bundle already self-resolves.
# ------------------------------------------------------------------------------
CEF_PATH="${CEF_PATH:-$HOME/Library/Caches/tauri-cef}"

# Pick exactly one CEF distribution per platform. If two cached versions
# coexist (e.g. after a CEF upgrade) `head -1` silently picks an arbitrary
# one and the binary can load the wrong libcef — Linux/Windows then fail
# only on warmed runners. Error out so the failure is loud.
pick_one_cef_dist() {
  local pattern="$1"
  local matches
  mapfile -t matches < <(find "$CEF_PATH" -mindepth 2 -maxdepth 2 -type d -name "$pattern" 2>/dev/null | sort)
  if [ "${#matches[@]}" -gt 1 ]; then
    echo "ERROR: multiple CEF distributions found under $CEF_PATH matching $pattern:" >&2
    printf '  %s\n' "${matches[@]}" >&2
    echo "Set CEF_PATH to a directory containing exactly one cef_* version." >&2
    return 1
  fi
  printf '%s' "${matches[0]:-}"
}

case "$OS" in
  Linux)
    CEF_DIST_DIR="$(pick_one_cef_dist 'cef_linux_*')" || exit 1
    if [ -n "$CEF_DIST_DIR" ] && [ -d "$CEF_DIST_DIR" ]; then
      export LD_LIBRARY_PATH="$CEF_DIST_DIR:${LD_LIBRARY_PATH:-}"
      echo "[runner] LD_LIBRARY_PATH includes: $CEF_DIST_DIR"
    else
      echo "[runner] Warning: no CEF Linux distribution found under $CEF_PATH — libcef.so may fail to load."
    fi
    ;;
  MINGW*|MSYS*|CYGWIN*|Windows_NT)
    CEF_DIST_DIR="$(pick_one_cef_dist 'cef_windows_*')" || exit 1
    if [ -n "$CEF_DIST_DIR" ] && [ -d "$CEF_DIST_DIR" ]; then
      # Windows uses PATH for DLL resolution. Use the native Windows path form
      # so the CEF binary itself can find libcef.dll even though we're running
      # under git-bash.
      if command -v cygpath >/dev/null 2>&1; then
        WIN_CEF_DIST="$(cygpath -w "$CEF_DIST_DIR")"
      else
        WIN_CEF_DIST="$CEF_DIST_DIR"
      fi
      export PATH="$CEF_DIST_DIR:$PATH"
      echo "[runner] PATH includes: $CEF_DIST_DIR (win: $WIN_CEF_DIST)"
    else
      echo "[runner] Warning: no CEF Windows distribution found under $CEF_PATH — libcef.dll may fail to load."
    fi
    ;;
esac

# ------------------------------------------------------------------------------
# Launch CEF app — CDP on 19222 is already enabled in lib.rs (see CLAUDE.md).
# ------------------------------------------------------------------------------
LOG_DIR="${RUNNER_TEMP:-${TMPDIR:-/tmp}}"
APP_LOG="$LOG_DIR/openhuman-e2e-app-${LOG_SUFFIX}.log"
APP_ARGS=()
# CEF/Chromium needs extra coaxing in headless / containerized Linux runs:
#
#   --no-sandbox            crbug.com/638180 — needed only when the runner is
#                           uid 0 or the cached CEF chrome-sandbox helper is
#                           missing/misconfigured. Non-root Linux runs with a
#                           valid helper keep Chromium sandboxing.
#   --disable-dev-shm-usage docker /dev/shm is often 64 MB; Chromium
#                           assumes ≥2 GB and crashes mid-startup
#                           ("Failed global descriptor lookup: 7" in the
#                           zygote helper).
#   --disable-gpu           no GPU in the CI container.
#   --no-zygote             skips the zygote launcher that wants dbus; Chromium
#                           only permits this when sandboxing is also disabled.
#
# Apply only on Linux. macOS/Windows runners are unprivileged users with a
# real display / GPU; leaving the sandbox on there is correct.
case "$OS" in
  Linux)
    APP_ARGS+=(
      "--disable-dev-shm-usage"
      "--disable-gpu"
    )
    NO_SANDBOX_REASON=""
    if [ "$(id -u)" -eq 0 ]; then
      NO_SANDBOX_REASON="runner is uid 0"
    elif [ -n "${CEF_DIST_DIR:-}" ]; then
      SANDBOX_HELPER="$CEF_DIST_DIR/chrome-sandbox"
      SANDBOX_HELPER_MODE="$(stat -c '%u:%a' "$SANDBOX_HELPER" 2>/dev/null || true)"
      if [ "$SANDBOX_HELPER_MODE" != "0:4755" ]; then
        NO_SANDBOX_REASON="chrome-sandbox helper is not root-owned mode 4755"
      fi
    fi
    if [ -n "$NO_SANDBOX_REASON" ]; then
      APP_ARGS+=("--no-sandbox" "--no-zygote")
      echo "[runner] Linux CEF sandbox disabled: $NO_SANDBOX_REASON"
    fi
    echo "[runner] Linux CEF args: ${APP_ARGS[*]}"
    ;;
esac
echo "[runner] Launching CEF app: $APP_BIN ${APP_ARGS[*]:-}"
echo "[runner]   App logs: $APP_LOG"
# `${APP_ARGS[@]+"${APP_ARGS[@]}"}` is the idiom for expanding a possibly-
# empty array under `set -u`. On macOS APP_ARGS is empty; on Linux it has
# --no-sandbox etc. Without this guard bash errors with "APP_ARGS[@]:
# unbound variable" and the app never launches.
"$APP_BIN" ${APP_ARGS[@]+"${APP_ARGS[@]}"} > "$APP_LOG" 2>&1 &
APP_PID=$!

echo "[runner] Waiting for CDP at http://127.0.0.1:${CEF_CDP_PORT}/json/version ..."
CDP_VERSION_JSON=""
for i in $(seq 1 "$E2E_CDP_READY_TIMEOUT_SECONDS"); do
  CDP_VERSION_JSON="$(curl -sf "http://127.0.0.1:${CEF_CDP_PORT}/json/version" 2>/dev/null || true)"
  if [ -n "$CDP_VERSION_JSON" ]; then
    echo "[runner] CDP is ready."
    break
  fi
  if ! kill -0 "$APP_PID" 2>/dev/null; then
    echo "ERROR: CEF app exited before CDP came up. App log follows:" >&2
    echo "----- $APP_LOG -----" >&2
    cat "$APP_LOG" >&2 || true
    echo "----- end log -----" >&2
    exit 1
  fi
  if [ "$i" -eq "$E2E_CDP_READY_TIMEOUT_SECONDS" ]; then
    echo "ERROR: CDP did not come up within ${E2E_CDP_READY_TIMEOUT_SECONDS}s. App log follows:" >&2
    echo "----- $APP_LOG -----" >&2
    cat "$APP_LOG" >&2 || true
    echo "----- end log -----" >&2
    exit 1
  fi
  sleep 1
done

# Wait for the main app target (tauri.localhost) to be visible, then close the
# CEF prewarm child-webview slot (about:blank) so chromedriver attaches to the
# real app. Without this, debuggerAddress picks the first page target — which
# is the prewarm — and chromedriver's session is bound to a target CEF may
# garbage-collect mid-test, killing the session with "session terminated".
echo "[runner] Waiting for main app CDP target (tauri.localhost) ..."
MAIN_TARGET_ID=""
for i in $(seq 1 60); do
  TARGETS_JSON="$(curl -sf "http://127.0.0.1:${CEF_CDP_PORT}/json/list" 2>/dev/null || true)"
  MAIN_TARGET_ID="$(printf '%s' "$TARGETS_JSON" | python3 -c '
import json, sys
try:
    data = json.load(sys.stdin)
except Exception:
    sys.exit(0)
for t in data:
    if t.get("type") == "page" and "tauri.localhost" in (t.get("url") or ""):
        print(t.get("id", ""))
        break
' 2>/dev/null || true)"
  if [ -n "$MAIN_TARGET_ID" ]; then
    echo "[runner] Main app target: $MAIN_TARGET_ID"
    break
  fi
  if [ "$i" -eq 60 ]; then
    echo "[runner] Warning: main app target never appeared — chromedriver may attach to the wrong target."
    break
  fi
  sleep 1
done

if [ -n "$MAIN_TARGET_ID" ]; then
  printf '%s' "$TARGETS_JSON" | python3 -c '
import json, sys
try:
    data = json.load(sys.stdin)
except Exception:
    sys.exit(0)
for t in data:
    if t.get("type") == "page" and (t.get("url") or "").startswith("about:"):
        print(t.get("id", ""))
' 2>/dev/null | while read -r STALE_ID; do
    if [ -n "$STALE_ID" ]; then
      echo "[runner] Closing prewarm/about:blank target: $STALE_ID"
      curl -sf "http://127.0.0.1:${CEF_CDP_PORT}/json/close/$STALE_ID" > /dev/null 2>&1 || true
    fi
  done
fi

# ------------------------------------------------------------------------------
# Resolve a chromedriver whose major matches CEF's bundled Chromium.
#
# chromedriver is strict about same-major-version matching (e.g. cd148 cannot
# talk to Chrome 146). The Appium chromium driver ships its own bundled
# chromedriver but that often drifts ahead of CEF's pinned Chromium.
# Solution: parse Chromium's version from /json/version, then download the
# matching chromedriver from Chrome for Testing into a workspace-local cache.
# We pass the resulting binary to Appium via the `appium:chromedriverExecutable`
# capability (wired in wdio.conf.ts via E2E_CHROMEDRIVER_PATH).
# ------------------------------------------------------------------------------
CHROMIUM_FULL_VERSION="$(echo "$CDP_VERSION_JSON" | sed -n 's/.*"Browser": *"[^/]*\/\([^"]*\)".*/\1/p' | head -1)"
if [ -z "$CHROMIUM_FULL_VERSION" ]; then
  CHROMIUM_FULL_VERSION="$(echo "$CDP_VERSION_JSON" | sed -n 's/.*"Browser": *"\([^"]*\)".*/\1/p' | sed 's/^[^/]*\///' | head -1)"
fi
echo "[runner] CEF Chromium version: ${CHROMIUM_FULL_VERSION:-<unknown>}"

case "$OS" in
  Darwin)
    ARCH="$(uname -m)"
    case "$ARCH" in
      arm64) CFT_PLATFORM="mac-arm64" ;;
      x86_64) CFT_PLATFORM="mac-x64" ;;
      *) CFT_PLATFORM="mac-arm64" ;;
    esac
    CD_EXE_NAME="chromedriver"
    ;;
  Linux)
    CFT_PLATFORM="linux64"
    CD_EXE_NAME="chromedriver"
    ;;
  MINGW*|MSYS*|CYGWIN*|Windows_NT)
    CFT_PLATFORM="win64"
    CD_EXE_NAME="chromedriver.exe"
    ;;
esac

CD_CACHE_DIR="$APP_DIR/test/e2e/.cache/chromedriver/${CHROMIUM_FULL_VERSION:-unknown}-${CFT_PLATFORM}"
CD_BINARY="$CD_CACHE_DIR/chromedriver-${CFT_PLATFORM}/${CD_EXE_NAME}"

if [ -n "$CHROMIUM_FULL_VERSION" ] && [ ! -x "$CD_BINARY" ]; then
  CD_URL="https://storage.googleapis.com/chrome-for-testing-public/${CHROMIUM_FULL_VERSION}/${CFT_PLATFORM}/chromedriver-${CFT_PLATFORM}.zip"
  echo "[runner] Downloading matching chromedriver: $CD_URL"
  mkdir -p "$CD_CACHE_DIR"
  CD_ZIP="$CD_CACHE_DIR/chromedriver.zip"
  if curl -fSL "$CD_URL" -o "$CD_ZIP"; then
    if command -v unzip >/dev/null 2>&1; then
      (cd "$CD_CACHE_DIR" && unzip -o -q chromedriver.zip)
    else
      # The openhuman_ci docker image doesn't ship `unzip`; use Python's
      # stdlib zipfile so we don't have to add a system package install.
      python3 -c "import sys,zipfile; zipfile.ZipFile(sys.argv[1]).extractall(sys.argv[2])" \
        "$CD_ZIP" "$CD_CACHE_DIR"
    fi
    chmod +x "$CD_BINARY" 2>/dev/null || true
  else
    echo "[runner] Warning: chromedriver $CHROMIUM_FULL_VERSION not on Chrome for Testing; falling back to Appium-bundled chromedriver."
  fi
fi

if [ -x "$CD_BINARY" ]; then
  export E2E_CHROMEDRIVER_PATH="$CD_BINARY"
  echo "[runner] Using chromedriver: $E2E_CHROMEDRIVER_PATH"
fi

# ------------------------------------------------------------------------------
# Start Appium (chromium driver)
# ------------------------------------------------------------------------------
# shellcheck source=/dev/null
source "$SCRIPT_DIR/e2e-resolve-node-appium.sh"

# Make sure the chromium driver is installed. `appium driver list --installed`
# exits non-zero on parse errors in some Appium versions, so just attempt the
# install and ignore "already installed" output.
echo "[runner] Ensuring Appium chromium driver is installed..."
APPIUM_HOME_DIR="${APPIUM_HOME:-$HOME/.appium}"
if [ ! -d "$APPIUM_HOME_DIR/node_modules/appium" ]; then
  echo "[runner] Installing Appium into $APPIUM_HOME_DIR for chromium driver peer resolution..."
  npm install --prefix "$APPIUM_HOME_DIR" appium@3 >/dev/null
fi
"$APPIUM_BIN" driver install --source=npm appium-chromium-driver >/dev/null 2>&1 || true

APPIUM_LOG="$LOG_DIR/appium-e2e-${LOG_SUFFIX}.log"

# Fail fast if something else is already serving on the Appium port. Otherwise
# `curl /status` succeeds against the stale server while our just-launched
# Appium dies with EADDRINUSE — we'd silently drive the wrong instance.
if curl -sf "http://127.0.0.1:$APPIUM_PORT/status" >/dev/null 2>&1; then
  echo "ERROR: Appium is already listening on port $APPIUM_PORT. Stop the stale server or set APPIUM_PORT." >&2
  exit 1
fi

echo "[runner] Starting Appium on port $APPIUM_PORT"
echo "[runner]   Appium logs: $APPIUM_LOG"
"$APPIUM_BIN" --port "$APPIUM_PORT" --relaxed-security > "$APPIUM_LOG" 2>&1 &
APPIUM_PID=$!

for i in $(seq 1 30); do
  # If Appium crashed between forks (e.g. unhandled driver-load error), bail
  # out instead of polling /status for 30s.
  if ! kill -0 "$APPIUM_PID" 2>/dev/null; then
    echo "ERROR: Appium exited before becoming ready. Appium log follows:" >&2
    echo "----- $APPIUM_LOG -----" >&2
    cat "$APPIUM_LOG" >&2 || true
    echo "----- end log -----" >&2
    exit 1
  fi
  if curl -sf "http://127.0.0.1:$APPIUM_PORT/status" >/dev/null 2>&1; then
    echo "[runner] Appium is ready."
    break
  fi
  if [ "$i" -eq 30 ]; then
    echo "ERROR: Appium did not start within 30 seconds." >&2
    exit 1
  fi
  sleep 1
done

# ------------------------------------------------------------------------------
# Run WDIO
# ------------------------------------------------------------------------------
if [ "${#SPEC_ARGS[@]}" -gt 0 ]; then
  echo "[runner] Running ${#SPEC_ARGS[@]} spec(s) in a single shared session:"
  printf '         %s\n' "${SPEC_ARGS[@]}"
  WDIO_SPEC_ARGS=()
  for s in "${SPEC_ARGS[@]}"; do
    WDIO_SPEC_ARGS+=(--spec "$s")
  done
  pnpm exec wdio run test/wdio.conf.ts "${WDIO_SPEC_ARGS[@]}"
else
  echo "[runner] Running full E2E suite (single shared session)..."
  pnpm exec wdio run test/wdio.conf.ts
fi
