#!/usr/bin/env bash
#
# scenario-lib.sh — reusable primitives for driving the SampleApp on a connected
# Android device during a perf capture. SOURCE this from a scenario script; do
# not run it directly.
#
#   source "$(dirname "$0")/scenario-lib.sh"
#
# Design: every scenario is a short, declarative sequence of these verbs. The
# golden rule is to WAIT ON REAL VIEW-HIERARCHY testIDs, never fixed sleeps —
# Android debug navigation is slow and variable, so sleeping races the mount.
#
# Verbs:
#   relaunch                  cold-restart the app to a known state (channel list)
#   wait_for <testid> [secs]  poll uiautomator until a testID appears (default 30s)
#   tap_testid <testid>       tap the center of the first element whose resource-id
#                             starts with <testid> (prefix match, so a UUID suffix is fine)
#   tap_visible_testid <pref> [min_h] [top] [bottom]
#                             like tap_testid but skips elements clipped off-screen
#                             (needed for grid tiles: the first match is often clipped
#                             under the header)
#   tap_text <substr>         tap the center of the first node whose text= contains
#                             <substr> (e.g. open a channel by its display name)
#   tap_text_until <substr> <expect_testid> [tries]
#                             scroll the list until a node with text <substr> is visible,
#                             tap it, poll for <expect_testid>; the generic "open a
#                             specific channel by name" primitive
#   wait_for_log <pattern> [secs]
#                             poll logcat (ReactNativeJS) for <pattern> — use for states
#                             the view tree can't see (the image gallery overlay
#                             contributes NO accessibility nodes when TalkBack is off, so
#                             uiautomator cannot detect it). Clear logcat before the action.
#   tap_header_action [y_max] tap the right-most clickable node in the top header band —
#                             for header actions RN won't surface a resource-id for (e.g.
#                             the channel-screen avatar button -> Channel Details)
#   tap_header_until <expect_testid> [tries] [y_max]
#                             tap_until, but via tap_header_action instead of a testID
#   swipe_up [ms]             scroll content up / reveal older (default 250ms)
#   swipe_down [ms]           scroll content down
#   swipe_left [ms]           page forward (e.g. image gallery)
#   swipe_right [ms]          page back
#   scroll [n] [ms]           swipe_up n times (default 8) with a settle between each
#                             (for the INVERTED message list — reveals older messages)
#   scroll_down [n] [ms] [settle]
#                             swipe_down n times to paginate a NORMAL list (e.g. the
#                             Photos & Videos grid). Opposite direction from scroll();
#                             longer settle so each network page lands before the next swipe
#   count_testid <prefix>     count elements whose resource-id starts with <prefix>
#                             (uses grep -o | wc -l — NEVER grep -c: the dump is one line)
#   ui_dump                   refresh the cached view tree (most verbs call this themselves)
#
# Env: PKG overrides the app id (default: io.getstream.reactnative.sampleapp).

PKG="${PKG:-io.getstream.reactnative.sampleapp}"
UIXML="${UIXML:-/tmp/perf-scenario-ui.xml}"

ui_dump() {
  adb shell uiautomator dump /sdcard/ui.xml >/dev/null 2>&1 &&
    adb pull /sdcard/ui.xml "$UIXML" >/dev/null 2>&1
}

relaunch() {
  # force-stop alone can resume warm on some OEM ROMs (MIUI), which would leave the JS
  # context — and any cumulative perf counters — alive. force-stop + kill + a short wait
  # guarantees a true cold JS re-init.
  adb shell am force-stop "$PKG"
  adb shell am kill "$PKG" >/dev/null 2>&1
  sleep 1
  adb shell monkey -p "$PKG" -c android.intent.category.LAUNCHER 1 >/dev/null 2>&1
}

wait_for() {
  local id="$1" timeout="${2:-30}"
  for _ in $(seq 1 "$timeout"); do
    ui_dump
    if grep -q "resource-id=\"$id" "$UIXML"; then
      return 0
    fi
    sleep 1
  done
  echo "ERR: testID '$id' never appeared within ${timeout}s" >&2
  return 1
}

tap_testid() {
  local id="$1"
  ui_dump
  local box
  box=$(grep -oE "resource-id=\"$id[^\"]*\"[^>]*bounds=\"\[[0-9]+,[0-9]+\]\[[0-9]+,[0-9]+\]\"" "$UIXML" |
    grep -oE '\[[0-9]+,[0-9]+\]\[[0-9]+,[0-9]+\]' | head -1)
  if [ -z "$box" ]; then
    echo "ERR: no element found for testID '$id'" >&2
    return 1
  fi
  local cx cy
  cx=$(echo "$box" | sed -E 's/\[([0-9]+),([0-9]+)\]\[([0-9]+),([0-9]+)\]/(\1+\3)\/2/' | bc)
  cy=$(echo "$box" | sed -E 's/\[([0-9]+),([0-9]+)\]\[([0-9]+),([0-9]+)\]/(\2+\4)\/2/' | bc)
  echo "tap '$id' @ $cx,$cy"
  adb shell input tap "$cx" "$cy"
}

tap_until() {
  # tap_until <tap_testid> <expect_testid> [tries] [wait_each_s]
  # Taps <tap_testid>, then polls for <expect_testid>; retries the tap if it
  # didn't land (Android taps occasionally don't register / the list isn't
  # settled). Use this for navigation instead of a bare tap + wait_for.
  local tap_id="$1" expect_id="$2" tries="${3:-4}" each="${4:-6}"
  local i j
  for i in $(seq 1 "$tries"); do
    tap_testid "$tap_id" || return 1
    for j in $(seq 1 "$each"); do
      sleep 1
      ui_dump
      if grep -q "resource-id=\"$expect_id" "$UIXML"; then
        return 0
      fi
    done
    echo "retry $i: '$expect_id' not up after tapping '$tap_id'" >&2
  done
  echo "ERR: '$expect_id' never appeared after $tries taps of '$tap_id'" >&2
  return 1
}

swipe_up() { adb shell input swipe 540 700 540 1600 "${1:-250}"; }
swipe_down() { adb shell input swipe 540 1600 540 700 "${1:-250}"; }
# Horizontal swipes kept clear of the screen edges: a swipe that starts/ends within the
# OEM edge-gesture zone (outer ~10%) triggers the system BACK gesture instead (it will
# pop you out of the screen). 880<->200 is a ~680px central swipe, safe on a 1080px device.
swipe_left() { adb shell input swipe 880 1100 200 1100 "${1:-250}"; }
swipe_right() { adb shell input swipe 200 1100 880 1100 "${1:-250}"; }

scroll() {
  local n="${1:-8}" ms="${2:-250}"
  for _ in $(seq 1 "$n"); do
    swipe_up "$ms"
    sleep 0.6
  done
}

scroll_down() {
  # Paginate a NORMAL (non-inverted) list — e.g. the Photos & Videos media grid — by
  # scrolling down through it to reach the bottom and trigger onEndReached/loadMore.
  # This is the OPPOSITE direction from scroll() (which scrolls an inverted message list
  # up to reveal older). The settle is longer because each page is a network round-trip;
  # too short and the next swipe fires before the page lands, so N stops growing.
  local n="${1:-8}" ms="${2:-250}" settle="${3:-1.2}"
  for _ in $(seq 1 "$n"); do
    swipe_down "$ms"
    sleep "$settle"
  done
}

count_testid() {
  ui_dump
  grep -oE "resource-id=\"$1[^\"]*\"" "$UIXML" | wc -l | tr -d ' '
}

_center_of_box() {
  # echo "cx cy" for a uiautomator bounds string "[x1,y1][x2,y2]"
  echo "$1" | sed -E 's/\[([0-9]+),([0-9]+)\]\[([0-9]+),([0-9]+)\]/(\1+\3)\/2 (\2+\4)\/2/' |
    { read -r a b; echo "$(echo "$a" | bc) $(echo "$b" | bc)"; }
}

tap_visible_testid() {
  # tap_visible_testid <prefix> [min_h] [top_guard] [bottom_guard]
  # Tap the center of the first element whose resource-id starts with <prefix> that is
  # fully on-screen (height >= min_h, top >= top_guard, bottom <= bottom_guard). Grid
  # tiles' first match is usually clipped under the header, so a plain tap_testid lands
  # on a sliver and misses.
  local prefix="$1" min_h="${2:-200}" top="${3:-260}" bottom="${4:-2300}"
  ui_dump
  local center
  center=$(grep -oE "<node[^>]*resource-id=\"$prefix[^\"]*\"[^>]*bounds=\"\[[0-9]+,[0-9]+\]\[[0-9]+,[0-9]+\]\"" "$UIXML" |
    grep -oE '\[[0-9]+,[0-9]+\]\[[0-9]+,[0-9]+\]' |
    awk -F'[][,]' -v mh="$min_h" -v tg="$top" -v bg="$bottom" \
      '{x1=$2;y1=$3;x2=$5;y2=$6; h=y2-y1; if (h>=mh && y1>=tg && y2<=bg){printf "%d %d\n",(x1+x2)/2,(y1+y2)/2; exit}}')
  if [ -z "$center" ]; then
    echo "ERR: no fully-visible element for testID '$prefix'" >&2
    return 1
  fi
  echo "tap visible '$prefix' @ $center"
  # shellcheck disable=SC2086
  adb shell input tap $center
}

tap_text() {
  # tap_text <substring> : tap the center of the first node whose text= contains
  # <substring>. The tap lands on the pressable behind the text (e.g. a channel row).
  # Keep <substring> free of regex metacharacters.
  local needle="$1"
  ui_dump
  local box
  box=$(grep -oE "<node[^>]*text=\"[^\"]*${needle}[^\"]*\"[^>]*bounds=\"\[[0-9]+,[0-9]+\]\[[0-9]+,[0-9]+\]\"" "$UIXML" |
    grep -oE '\[[0-9]+,[0-9]+\]\[[0-9]+,[0-9]+\]' | head -1)
  if [ -z "$box" ]; then
    echo "ERR: no node with text matching '$needle'" >&2
    return 1
  fi
  local center
  center=$(_center_of_box "$box")
  echo "tap text '$needle' @ $center"
  # shellcheck disable=SC2086
  adb shell input tap $center
}

tap_text_until() {
  # tap_text_until <substring> <expect_testid> [tries]
  # Scroll the list until a node with text containing <substring> is visible, tap it,
  # and poll for <expect_testid>. Retries / scrolls if not found. Opens a specific
  # channel by name without hardcoding its position in the list.
  local needle="$1" expect_id="$2" tries="${3:-8}"
  local i j
  for i in $(seq 1 "$tries"); do
    ui_dump
    if grep -qE "text=\"[^\"]*${needle}[^\"]*\"" "$UIXML"; then
      tap_text "$needle" || return 1
      for j in $(seq 1 6); do
        sleep 1
        ui_dump
        if grep -q "resource-id=\"$expect_id" "$UIXML"; then
          return 0
        fi
      done
      echo "retry $i: '$expect_id' not up after tapping text '$needle'" >&2
    else
      echo "scan $i: text '$needle' not visible, scrolling" >&2
      swipe_up 250
      sleep 0.6
    fi
  done
  echo "ERR: never reached '$expect_id' via text '$needle'" >&2
  return 1
}

wait_for_log() {
  # wait_for_log <grep_pattern> [secs]
  # Poll logcat (ReactNativeJS) for a fixed-string pattern. Use for states the view tree
  # cannot see — notably the image gallery overlay, which exposes no accessibility nodes
  # when TalkBack is off, so uiautomator dump only ever sees the screen behind it.
  # Clear logcat (adb logcat -c) before the action that should emit the marker.
  local pat="$1" timeout="${2:-15}"
  for _ in $(seq 1 "$timeout"); do
    if adb logcat -d -s ReactNativeJS:V 2>/dev/null | grep -qF "$pat"; then
      return 0
    fi
    sleep 1
  done
  echo "ERR: log pattern '$pat' never appeared within ${timeout}s" >&2
  return 1
}

tap_header_action() {
  # tap_header_action [y_max]
  # Tap the right-most clickable node within the top header band (top edge < y_max).
  # Use for a header action that RN won't surface a resource-id for — e.g. the channel
  # screen's avatar button, which RN composes with the avatar into one Button node with
  # the avatar's content-desc, dropping the testID. The back button shares the band on
  # the left, so we take the right-most clickable.
  local ymax="${1:-300}"
  ui_dump
  local center
  center=$(grep -oE '<node[^>]*clickable="true"[^>]*bounds="\[[0-9]+,[0-9]+\]\[[0-9]+,[0-9]+\]"' "$UIXML" |
    grep -oE '\[[0-9]+,[0-9]+\]\[[0-9]+,[0-9]+\]' |
    awk -F'[][,]' -v ym="$ymax" \
      '{x1=$2;y1=$3;x2=$5;y2=$6; if (y1<ym){cx=int((x1+x2)/2); cy=int((y1+y2)/2); if (cx>maxx){maxx=cx; bx=cx; by=cy}}} END{if (bx!=""){printf "%d %d\n",bx,by}}')
  if [ -z "$center" ]; then
    echo "ERR: no header-band clickable node found (top < ${ymax}px)" >&2
    return 1
  fi
  echo "tap header action @ $center"
  # shellcheck disable=SC2086
  adb shell input tap $center
}

tap_header_until() {
  # tap_header_until <expect_testid> [tries] [y_max]
  # Like tap_until, but taps the right-most header action (see tap_header_action) instead
  # of a testID. Polls for <expect_testid> and retries.
  local expect_id="$1" tries="${2:-4}" ymax="${3:-300}" i j
  for i in $(seq 1 "$tries"); do
    tap_header_action "$ymax" || return 1
    for j in $(seq 1 6); do
      sleep 1
      ui_dump
      if grep -q "resource-id=\"$expect_id" "$UIXML"; then
        return 0
      fi
    done
    echo "retry $i: '$expect_id' not up after header tap" >&2
  done
  echo "ERR: '$expect_id' never appeared after $tries header taps" >&2
  return 1
}