#!/usr/bin/env bash
#
# render-smoke.sh — authenticated render smoke test for the Navagoo shop-owner portal
# =====================================================================================
#
# WHAT IT DOES
#   1. Logs in as the dev test shop owner (frontend tier, `LoginForm` + `_csrf-frontend`),
#      reusing a cookie jar so the session persists across requests.
#   2. GETs a curated list of key authenticated pages.
#   3. For each page asserts:
#        - HTTP status is 200, AND
#        - the response body contains no PHP / Yii error signature
#          (e.g. "Fatal error", "Stack trace", "Class ... not found", "... on null").
#   4. Prints a PASS/FAIL table and exits 0 only if every page is clean.
#
# USAGE
#   tests/smoke/render-smoke.sh            # run against the local dev env
#   SMOKE_VERBOSE=1 tests/smoke/render-smoke.sh   # also dump the offending snippet on FAIL
#
# CONFIG (env vars — all default to the LOCAL DEV values)
#   SMOKE_HOST      Host header the vhost matches on   (default: shop.navagoo.localhost)
#   SMOKE_BASE      Base URL / origin to actually hit  (default: http://127.0.0.1)
#   SMOKE_USER      LoginForm[username]                (default: Beautycenter123@123.com)
#   SMOKE_PASS      LoginForm[password]                (default: Nav123321)
#   SMOKE_TIMEOUT   per-request curl timeout, seconds  (default: 25)
#
# CREDENTIALS NOTE
#   The default username/password are the shared LOCAL DEV shop-owner test account
#   (see the project's dev-test-login note). They are NOT secrets and are only valid
#   against the local docker stack. Override via SMOKE_USER / SMOKE_PASS for any other
#   environment; never point this at production.
#
# REQUIREMENTS
#   bash, curl, grep, mktemp, sed — all standard on macOS and Linux. No GNU-only flags.
#
# EXIT CODES
#   0  all pages rendered cleanly
#   1  one or more pages failed (non-200 or error signature found)
#   2  login / preflight failure (could not even start the sweep)
#
set -u

# --- configuration ----------------------------------------------------------------
HOST="${SMOKE_HOST:-shop.navagoo.localhost}"
BASE="${SMOKE_BASE:-http://127.0.0.1}"
USER_NAME="${SMOKE_USER:-Beautycenter123@123.com}"
USER_PASS="${SMOKE_PASS:-Nav123321}"
TIMEOUT="${SMOKE_TIMEOUT:-25}"
VERBOSE="${SMOKE_VERBOSE:-0}"

LOGIN_PATH="/sign-in/login"
CSRF_PARAM="_csrf-frontend"   # frontend/config/web.php → 'csrfParam'

# Curated list of key authenticated pages to render-check.
#
# Each entry is "<path>" or "<path>|<expected_http_status>". When the status is
# omitted it defaults to 200. A non-200 expectation lets us positively assert a
# route that is *supposed* to deny/redirect — e.g. an admin-gated feature that
# 404s until enabled — instead of treating that as a failure. Error-signature
# scanning only runs on pages expected to return 200 (a deliberate error page
# must not be scanned for error signatures).
PAGES=(
  "/"
  "/agents-bookings"
  "/booking-calendar?view=day&date=2025-12-10"
  "/booking-calendar?view=list&date=2025-12-10"
  "/booking-calendar?view=month&date=2025-12-10"
  "/earnings?tab=earnings"
  "/earnings?tab=charges"
  "/earnings?tab=invoices"
  "/earnings?tab=settlement"
  "/navagoo-plans"
  "/shop-analytics"
  "/settings?tab=scheduling"
  "/settings?tab=notifications"
  "/agents"
  "/agents?tab=payroll"
  "/shop-service"
  "/promo-code/index"
  "/rate"
  # /booking/* legacy CRUD views were removed (db318a6) — the daily calendar is
  # now /booking-calendar (covered above). The old /booking/index route is gone.
  "/notifications"
  # --- controller landing pages (added to broaden render coverage) ---
  "/agents-wallet"
  "/agent-slots"
  "/branch"
  "/customers"
  "/customer-invitations"
  "/package"
  "/payment-settings"
  "/shop-settings"
  "/technical-support"
  "/page"
  # --- gated feature: asserts the admin gate holds (404 until enabled) ---
  # SocialMediaController::resolveShopAndSettings() throws NotFoundHttpException
  # when ShopSocialSettings.enabled != 1 for the shop. A 200 here would mean the
  # gate regressed, so we positively assert the 404.
  "/social-media|404"
)

# PHP / Yii runtime error signatures to grep the body for. These are extended-regex
# alternatives. The JS idiom "lang || undefined" is explicitly NOT in this list and is
# additionally filtered out below so it never trips "Undefined ...".
ERROR_SIGNATURES='Class .* not found|Exception|Fatal error|Undefined variable|Undefined array key|on null|Stack trace|Call to a member'

# --- shared curl plumbing ---------------------------------------------------------
COOKIE_JAR="$(mktemp -t navagoo-smoke-cookies.XXXXXX)"

cleanup() { rm -f "$COOKIE_JAR" 2>/dev/null || true; }
trap cleanup EXIT

# curl wrapped with the constant flags we always want:
#   -s            silent
#   -S            still show hard errors
#   --max-time    bound every request
#   -H Host:      route to the right vhost on the shared IP
#   -b/-c jar     read + write the session cookie jar
#   -L            follow redirects (a deauthed page bounces to /sign-in/login)
ccurl() {
  curl -sS --max-time "$TIMEOUT" \
    -H "Host: ${HOST}" \
    -b "$COOKIE_JAR" -c "$COOKIE_JAR" \
    "$@"
}

# Strip the known-safe JS idiom before scanning, so 'lang || undefined' (which contains
# the substring "undefined") can never match the "Undefined ..." signatures.
scrub_body() {
  sed 's/lang || undefined//g'
}

# --- step 1: authenticate ---------------------------------------------------------
log_preflight_fail() {
  echo "PREFLIGHT FAILURE: $1" >&2
  echo "  Host=${HOST}  Base=${BASE}" >&2
  echo "  Is the local docker stack up and serving ${BASE} (Host: ${HOST})?" >&2
}

# 1a. GET the login page to obtain a CSRF token + seed the cookie jar.
LOGIN_PAGE="$(ccurl "${BASE}${LOGIN_PATH}")"
if [ -z "$LOGIN_PAGE" ]; then
  log_preflight_fail "empty response from GET ${LOGIN_PATH} (server unreachable?)"
  exit 2
fi

# Prefer the hidden form input (exact token the form would POST); fall back to the
# <meta name="csrf-token"> tag. Both carry the masked CSRF token Yii accepts.
extract_token() {
  # hidden input: <input type="hidden" name="_csrf-frontend" value="TOKEN">
  printf '%s' "$1" \
    | grep -oE "name=\"${CSRF_PARAM}\"[^>]*value=\"[^\"]*\"" \
    | head -n 1 \
    | sed -E 's/.*value="([^"]*)".*/\1/'
}
extract_token_meta() {
  # meta tag: <meta name="csrf-token" content="TOKEN">
  printf '%s' "$1" \
    | grep -oE 'name="csrf-token"[^>]*content="[^"]*"' \
    | head -n 1 \
    | sed -E 's/.*content="([^"]*)".*/\1/'
}

CSRF_TOKEN="$(extract_token "$LOGIN_PAGE")"
[ -z "$CSRF_TOKEN" ] && CSRF_TOKEN="$(extract_token_meta "$LOGIN_PAGE")"

if [ -z "$CSRF_TOKEN" ]; then
  log_preflight_fail "could not extract a ${CSRF_PARAM} token from the login page"
  exit 2
fi

# 1b. POST credentials. -i so we can see the redirect; a successful login 302s away
#     from /sign-in/login. --data-urlencode keeps the '@' and special chars intact.
LOGIN_RESULT="$(ccurl -i \
  --data-urlencode "${CSRF_PARAM}=${CSRF_TOKEN}" \
  --data-urlencode "LoginForm[username]=${USER_NAME}" \
  --data-urlencode "LoginForm[password]=${USER_PASS}" \
  --data-urlencode "LoginForm[rememberMe]=1" \
  "${BASE}${LOGIN_PATH}")"

# Confirm we actually have a session: re-fetch the login route; an authenticated user
# is redirected to the portal instead of being shown the username/password form.
POST_LOGIN="$(ccurl -L "${BASE}${LOGIN_PATH}")"
if printf '%s' "$POST_LOGIN" | grep -qE "name=\"LoginForm\[password\]\""; then
  log_preflight_fail "login did not establish a session (still shown the login form)"
  if [ "$VERBOSE" = "1" ]; then
    echo "---- login response headers ----" >&2
    printf '%s\n' "$LOGIN_RESULT" | sed -n '1,15p' >&2
  fi
  exit 2
fi

echo "Authenticated as ${USER_NAME} @ ${HOST} (cookie jar: ${COOKIE_JAR})"
echo

# --- step 2: render-sweep each page -----------------------------------------------
FAILS=0
PASSES=0
ROWS=""

for entry in "${PAGES[@]}"; do
  # Split "<path>|<expected_status>"; default the expectation to 200.
  path="${entry%%|*}"
  expected="200"
  [ "$entry" != "$path" ] && expected="${entry##*|}"
  url="${BASE}${path}"

  # Capture body + trailing HTTP status code in one request (no second round-trip).
  # The body is everything before the final line; the final line is the status code.
  resp="$(ccurl -L -w $'\n%{http_code}' "$url")"
  status="$(printf '%s' "$resp" | tail -n 1)"
  body="$(printf '%s' "$resp" | sed '$d')"

  reason=""
  ok=1

  if [ "$status" != "$expected" ]; then
    ok=0
    reason="HTTP ${status:-???} (expected ${expected})"
  elif [ "$expected" = "200" ]; then
    # Scan the scrubbed body for any PHP/Yii error signature. Only meaningful for
    # pages expected to render — a deliberately-gated error page is not scanned.
    hit="$(printf '%s' "$body" | scrub_body | grep -oE "$ERROR_SIGNATURES" | head -n 1)"
    if [ -n "$hit" ]; then
      ok=0
      reason="error sig: ${hit}"
    fi
  fi

  if [ "$ok" = "1" ]; then
    PASSES=$((PASSES + 1))
    ROWS="${ROWS}PASS|${status}|${path}|"$'\n'
  else
    FAILS=$((FAILS + 1))
    ROWS="${ROWS}FAIL|${status:-???}|${path}|${reason}"$'\n'
    if [ "$VERBOSE" = "1" ] && [ "$status" = "200" ]; then
      echo "  ↳ ${path}: offending snippet:" >&2
      printf '%s' "$body" | scrub_body | grep -nE "$ERROR_SIGNATURES" | head -n 3 >&2
    fi
  fi
done

# --- step 3: report ---------------------------------------------------------------
printf '%-6s %-6s %s\n' "RESULT" "HTTP" "PAGE"
printf '%-6s %-6s %s\n' "------" "----" "----------------------------------------"
# Render the accumulated rows into an aligned table.
printf '%s' "$ROWS" | while IFS='|' read -r result code page note; do
  [ -z "$result" ] && continue
  if [ -n "$note" ]; then
    printf '%-6s %-6s %s  (%s)\n' "$result" "$code" "$page" "$note"
  else
    printf '%-6s %-6s %s\n' "$result" "$code" "$page"
  fi
done

echo
echo "Summary: ${PASSES} passed, ${FAILS} failed, $(( PASSES + FAILS )) total."

[ "$FAILS" -eq 0 ] || exit 1
exit 0
