#!/usr/bin/env bash

log() {
  printf '[ticket-manager-e2e] %s\n' "$*"
}

fail() {
  printf '[ticket-manager-e2e] ERROR: %s\n' "$*" >&2
  exit 1
}

require_cmd() {
  command -v "$1" >/dev/null 2>&1 || fail "missing required command: $1"
}

assert_eq() {
  local got="$1"
  local expected="$2"
  local message="$3"
  if [[ "$got" != "$expected" ]]; then
    fail "$message (expected=$expected got=$got)"
  fi
}

assert_non_empty() {
  local value="$1"
  local message="$2"
  if [[ -z "$value" || "$value" == "null" ]]; then
    fail "$message"
  fi
}

assert_ge() {
  local got="$1"
  local expected="$2"
  local message="$3"
  if (( got < expected )); then
    fail "$message (expected >= $expected got=$got)"
  fi
}

ticket_manager_e2e_usage() {
  cat <<'EOF'
ticket-manager e2e

Usage:
  bash scripts/test/ticket-manager-e2e.sh [--mode docker|local] [--config-file PATH] [--dry-run] [--cleanup] [--keep-artifacts] [case...]

Cases:
  workspace-timeline

Flags:
  --mode            Service startup mode: docker (default) or local.
  --config-file     Path to a base ticket-manager YAML config.
  --dry-run         Print resolved paths and environment, then exit.
  --cleanup         Delete the artifact directory after a successful run.
  --keep-artifacts  Keep the artifact directory after a successful run.
  --help            Show this message.
EOF
}

yq_get() {
  local file="$1"
  local path="$2"
  local value
  value="$(yq "${path} // \"\"" "${file}" 2>/dev/null || true)"
  if [[ "${value}" == "null" ]]; then
    value=""
  fi
  printf '%s' "${value}"
}

require_yq() {
  command -v yq >/dev/null 2>&1 || fail "yq not found; install with 'brew install yq' or see https://github.com/mikefarah/yq"
  if ! yq --version 2>/dev/null | grep -qE 'mikefarah.*v(4|5)'; then
    fail "yq must be the mikefarah v4+ build (got: $(yq --version 2>/dev/null || echo unknown))"
  fi
}

ticket_manager_e2e_parse_args() {
  DRY_RUN=0
  KEEP_ARTIFACTS="${KEEP_ARTIFACTS:-1}"
  E2E_MODE="${E2E_MODE:-docker}"
  CONFIG_FILE_FLAG=""

  while [[ $# -gt 0 ]]; do
    case "$1" in
      --mode)
        shift
        [[ $# -gt 0 ]] || fail "--mode requires docker or local"
        E2E_MODE="$1"
        ;;
      --config-file)
        shift
        [[ $# -gt 0 ]] || fail "--config-file requires a path"
        CONFIG_FILE_FLAG="$1"
        ;;
      --dry-run)
        DRY_RUN=1
        ;;
      --keep-artifacts)
        KEEP_ARTIFACTS=1
        ;;
      --cleanup)
        KEEP_ARTIFACTS=0
        ;;
      --help|-h)
        ticket_manager_e2e_usage
        exit 0
        ;;
      *)
        ticket_manager_e2e_usage
        fail "unknown argument: $1"
        ;;
    esac
    shift
  done

  if [[ "$E2E_MODE" != "docker" && "$E2E_MODE" != "local" ]]; then
    fail "unsupported mode: ${E2E_MODE}"
  fi
}

ticket_manager_e2e_init() {
  ticket_manager_e2e_parse_args "$@"

  SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd)"
  REPO_ROOT="$(cd -- "${SCRIPT_DIR}/../.." && pwd)"
  SERVICE_DIR="${REPO_ROOT}/services/ticket-manager"

  BASE_CONFIG=""
  if [[ -n "${CONFIG_FILE_FLAG}" ]]; then
    BASE_CONFIG="${CONFIG_FILE_FLAG}"
  elif [[ -n "${E2E_CONFIG_FILE:-}" ]]; then
    BASE_CONFIG="${E2E_CONFIG_FILE}"
  fi

  if [[ -n "${BASE_CONFIG}" ]]; then
    [[ -f "${BASE_CONFIG}" ]] || fail "config file not found: ${BASE_CONFIG}"
    require_yq
    MYSQL_HOST="$(yq_get "${BASE_CONFIG}" .database.host)"
    MYSQL_PORT="$(yq_get "${BASE_CONFIG}" .database.port)"
    MYSQL_USER="$(yq_get "${BASE_CONFIG}" .database.user)"
    MYSQL_PASSWORD="$(yq_get "${BASE_CONFIG}" .database.password)"
    MYSQL_TLS_CONFIG_KEY="$(yq_get "${BASE_CONFIG}" .database.tlsConfigKey)"
    [[ -n "${MYSQL_HOST}" ]] || fail "config file missing .database.host: ${BASE_CONFIG}"
    [[ -n "${MYSQL_PORT}" ]] || fail "config file missing .database.port: ${BASE_CONFIG}"
    [[ -n "${MYSQL_USER}" ]] || fail "config file missing .database.user: ${BASE_CONFIG}"
  elif [[ -f "${SERVICE_DIR}/config.local.yaml" ]]; then
    printf '[ticket-manager-e2e] hint: %s exists but --config-file / E2E_CONFIG_FILE was not set; falling back to env vars\n' \
      "${SERVICE_DIR}/config.local.yaml" >&2
  fi

  E2E_FAKE_AGENT_GATEWAY="${E2E_FAKE_AGENT_GATEWAY:-1}"
  AGENT_GATEWAY_ADDRESS="${AGENT_GATEWAY_ADDRESS:-127.0.0.1:9090}"
  AGENT_INSTANCE_ID="${AGENT_INSTANCE_ID:-ticket-manager-e2e}"
  AGENT_ID="${AGENT_ID:-}"
  TIMELINE_AGENT_MODEL="${TIMELINE_AGENT_MODEL:-}"
  if [[ "$E2E_MODE" == "local" ]]; then
    TICKET_MANAGER_PORT="${TICKET_MANAGER_PORT:-8080}"
  else
    TICKET_MANAGER_PORT="${TICKET_MANAGER_PORT:-$(( ( RANDOM % 10000 ) + 20000 ))}"
  fi
  BASE_URL="http://127.0.0.1:${TICKET_MANAGER_PORT}"
  INTERNAL_BASE_URL="${BASE_URL}"
  ARTIFACT_BASE_DIR="${REPO_ROOT}/.tmp/ticket-manager-e2e"
  ARTIFACT_ROOT="${ARTIFACT_BASE_DIR}/dry-run"
  HTTP_DIR="${ARTIFACT_ROOT}/http"
  E2E_IMAGE_TAG="${E2E_IMAGE_TAG:-ticket-manager:e2e}"
  E2E_DOCKERFILE="${E2E_DOCKERFILE:-${REPO_ROOT}/infra/images/ticket-manager/Dockerfile}"
  E2E_CA_CERT_FILE="${E2E_CA_CERT_FILE:-}"
  export APP_ENV="${APP_ENV:-local}"
  CONTAINER_NAME="ticket-manager-e2e-dry-run"

  MYSQL_HOST="${MYSQL_HOST:-127.0.0.1}"
  MYSQL_PORT="${MYSQL_PORT:-3306}"
  MYSQL_USER="${MYSQL_USER:-root}"
  MYSQL_PASSWORD="${MYSQL_PASSWORD:-}"
  MYSQL_TLS_CONFIG_KEY="${MYSQL_TLS_CONFIG_KEY:-}"
  if [[ -z "${MYSQL_TLS_CONFIG_KEY}" && "${MYSQL_HOST}" == *tidbcloud.com ]]; then
    MYSQL_TLS_CONFIG_KEY="true"
  fi
  E2E_DB_NAME="${E2E_DB_NAME:-ticket_manager_e2e_${RANDOM}${RANDOM}}"

  if [[ "$E2E_MODE" == "docker" ]]; then
    INTERNAL_BASE_URL="http://127.0.0.1:8080"
  fi

  if [[ "$DRY_RUN" == "1" ]]; then
    cat <<EOF
ticket-manager e2e dry-run
e2e_mode=${E2E_MODE}
base_config=${BASE_CONFIG}
repo_root=${REPO_ROOT}
service_dir=${SERVICE_DIR}
agent_gateway_address=${AGENT_GATEWAY_ADDRESS}
e2e_fake_agent_gateway=${E2E_FAKE_AGENT_GATEWAY}
app_env=${APP_ENV}
dockerfile=${E2E_DOCKERFILE}
image_tag=${E2E_IMAGE_TAG}
ticket_manager_port=${TICKET_MANAGER_PORT}
base_url=${BASE_URL}
internal_base_url=${INTERNAL_BASE_URL}
artifact_root=${ARTIFACT_ROOT}
http_dir=${HTTP_DIR}
agent_gateway_log=${ARTIFACT_ROOT}/agent-gateway.log
e2e_ca_cert_file=${E2E_CA_CERT_FILE}
keep_artifacts=${KEEP_ARTIFACTS}
mysql_host=${MYSQL_HOST}
mysql_port=${MYSQL_PORT}
mysql_user=${MYSQL_USER}
mysql_tls_config_key=${MYSQL_TLS_CONFIG_KEY}
mysql_database=${E2E_DB_NAME}
EOF
    exit 0
  fi

  require_cmd go
  require_cmd bash
  require_cmd curl
  require_cmd jq
  require_cmd mysql
  if [[ "$E2E_MODE" == "docker" ]]; then
    require_cmd docker
  else
    require_cmd go
  fi
  if [[ -n "${E2E_CA_CERT_FILE}" ]]; then
    [[ -f "${E2E_CA_CERT_FILE}" ]] || fail "E2E_CA_CERT_FILE not found: ${E2E_CA_CERT_FILE}"
  fi

  [[ -d "$SERVICE_DIR" ]] || fail "service dir not found: $SERVICE_DIR"
  if [[ "$E2E_MODE" == "docker" ]]; then
    [[ -f "$E2E_DOCKERFILE" ]] || fail "dockerfile not found: $E2E_DOCKERFILE"
  fi

  mkdir -p "${ARTIFACT_BASE_DIR}"
  ARTIFACT_ROOT="$(mktemp -d "${ARTIFACT_BASE_DIR}/run.XXXXXX")"
  HTTP_DIR="${ARTIFACT_ROOT}/http"
  mkdir -p "${HTTP_DIR}"

  SERVER_LOG="${ARTIFACT_ROOT}/ticket-manager.log"
  CONFIG_FILE="${ARTIFACT_ROOT}/ticket-manager.yaml"
  E2E_CONTAINER_CA_CERT_FILE=""
  configure_e2e_ca_cert
  SERVER_PID=""
  FAKE_GATEWAY_PID=""
  SERVER_LOG_PID=""
  CONTAINER_NAME="ticket-manager-e2e-$RANDOM$RANDOM"
  STATUS="failed"
  LAST_STATUS=""
  LAST_BODY_FILE=""
  LAST_HTTP_BASE=""
  REQUEST_SEQ=0
  DB_CREATED=0
  trap ticket_manager_e2e_cleanup EXIT
}

configure_e2e_ca_cert() {
  if [[ -z "${E2E_CA_CERT_FILE}" ]]; then
    return 0
  fi

  local ca_cert_path
  ca_cert_path="$(cd -- "$(dirname -- "${E2E_CA_CERT_FILE}")" && pwd)/$(basename -- "${E2E_CA_CERT_FILE}")"
  export SSL_CERT_FILE="${ca_cert_path}"
  export REQUESTS_CA_BUNDLE="${ca_cert_path}"
  export CURL_CA_BUNDLE="${ca_cert_path}"
  export AWS_CA_BUNDLE="${ca_cert_path}"
  export NODE_EXTRA_CA_CERTS="${ca_cert_path}"

  if [[ "${E2E_MODE}" == "docker" ]]; then
    cp "${ca_cert_path}" "${ARTIFACT_ROOT}/e2e-ca-certificates.crt"
    E2E_CONTAINER_CA_CERT_FILE="/e2e-artifacts/e2e-ca-certificates.crt"
  fi
}

mysql_client() {
  MYSQL_PWD="${MYSQL_PASSWORD}" mysql \
    --host="${MYSQL_HOST}" \
    --port="${MYSQL_PORT}" \
    --user="${MYSQL_USER}" \
    --batch --silent --skip-column-names \
    "$@"
}

create_database() {
  log "creating database ${E2E_DB_NAME} on ${MYSQL_HOST}:${MYSQL_PORT}"
  mysql_client --execute "CREATE DATABASE \`${E2E_DB_NAME}\` CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;"
  DB_CREATED=1
}

drop_database() {
  if (( DB_CREATED == 0 )); then
    return 0
  fi
  log "dropping database ${E2E_DB_NAME}"
  mysql_client --execute "DROP DATABASE IF EXISTS \`${E2E_DB_NAME}\`;" || \
    log "warning: failed to drop database ${E2E_DB_NAME}; drop manually"
}

service_mysql_host() {
  case "${MYSQL_HOST}" in
    127.0.0.1|localhost)
      if [[ "${E2E_MODE}" == "docker" ]]; then
        printf 'host.docker.internal'
        return
      fi
      ;;
  esac
  printf '%s' "${MYSQL_HOST}"
}

start_fake_agent_gateway() {
  if [[ "${E2E_FAKE_AGENT_GATEWAY}" != "1" ]]; then
    return
  fi
  local listen_address="127.0.0.1:0"
  local gateway_host="127.0.0.1"
  if [[ "${E2E_MODE}" == "docker" ]]; then
    listen_address="0.0.0.0:0"
    gateway_host="host.docker.internal"
  fi
  (cd "${SERVICE_DIR}" && go build -o "${ARTIFACT_ROOT}/fake-agent-gateway" ./e2e/fakegateway/cmd)
  "${ARTIFACT_ROOT}/fake-agent-gateway" \
    --address "${listen_address}" \
    --callback "${BASE_URL}/api/v1/ticketmanager/agent-gateway/events" \
    --ready-file "${ARTIFACT_ROOT}/gateway-port" >"${ARTIFACT_ROOT}/agent-gateway.log" 2>&1 &
  FAKE_GATEWAY_PID=$!
  local retries=50
  until [[ -s "${ARTIFACT_ROOT}/gateway-port" ]]; do
    kill -0 "${FAKE_GATEWAY_PID}" 2>/dev/null || fail "fake Agent Gateway failed to start"
    (( retries > 0 )) || fail "fake Agent Gateway did not become ready"
    retries=$((retries - 1))
    sleep 0.1
  done
  AGENT_GATEWAY_ADDRESS="${gateway_host}:$(cat "${ARTIFACT_ROOT}/gateway-port")"
}

mode_paths() {
  if [[ "$1" == "local" ]]; then
    address="127.0.0.1:${TICKET_MANAGER_PORT}"
  else
    address=":8080"
  fi
}

write_config_overrides() {
  local mode="$1"
  local out="$2"
  local address
  mode_paths "${mode}"

  cat >"${out}" <<EOF
apiServer:
  address: "${address}"

services:
  agent:
    instanceId: "${AGENT_INSTANCE_ID}"
    agentID: "${AGENT_ID}"
  workspace:
    timeline:
      enabled: true
      agentModel: "${TIMELINE_AGENT_MODEL}"
      aiDraftsEnabled: true
      agentTimeout: "30s"
      syncWarRoomInterval: "${E2E_WORKSPACE_TIMELINE_SYNC_INTERVAL:-5m}"
      syncCaseWorkspaceInterval: "${E2E_WORKSPACE_TIMELINE_SYNC_INTERVAL:-5m}"

database:
  host: "$(service_mysql_host)"
  database: "${E2E_DB_NAME}"
  tlsConfigKey: "${MYSQL_TLS_CONFIG_KEY}"

handlers:
  lark:
    botOpenID: "ou_e2e"

clients:
  jira:
    dummy: true

  agentGateway:
    address: "${AGENT_GATEWAY_ADDRESS}"
    timeout: "5s"
EOF

  if [[ -n "${DEVOPS_API_TOKEN:-}" ]]; then
    cat >>"${out}" <<EOF

  devops:
    apiToken: "${DEVOPS_API_TOKEN}"
    baseUrl: "${DEVOPS_BASE_URL:-https://prod.devops.pingcap.com}"
    frontendUrl: "${DEVOPS_FRONTEND_URL:-https://ops.tidbcloud.com}"
    timeout: "${DEVOPS_TIMEOUT:-30s}"
EOF
  fi
}

write_config_from_base() {
  local mode="$1"
  local base="$2"
  local out="$3"
  local overrides_path="${ARTIFACT_ROOT}/overrides.yaml"

  write_config_overrides "${mode}" "${overrides_path}"
  yq eval-all 'select(fileIndex==0) * select(fileIndex==1)' "${base}" "${overrides_path}" > "${out}"
}

write_config_from_env() {
  local mode="$1"
  local config_path="$2"
  local address
  mode_paths "${mode}"

  cat >"${config_path}" <<EOF
log:
  level: 0
  isJSON: false

apiServer:
  address: "${address}"

database:
  host: "$(service_mysql_host)"
  port: ${MYSQL_PORT}
  user: "${MYSQL_USER}"
  password: "${MYSQL_PASSWORD}"
  database: "${E2E_DB_NAME}"
  tlsConfigKey: "${MYSQL_TLS_CONFIG_KEY}"
  params:
    parseTime: "true"
    charset: "utf8mb4"

handlers:
  lark:
    botOpenID: "ou_e2e"

services:
  agent:
    instanceId: "${AGENT_INSTANCE_ID}"
    agentID: "${AGENT_ID}"
  workspace:
    timeline:
      enabled: true
      agentModel: "${TIMELINE_AGENT_MODEL}"
      aiDraftsEnabled: true
      agentTimeout: "30s"
      syncWarRoomInterval: "${E2E_WORKSPACE_TIMELINE_SYNC_INTERVAL:-5m}"
      syncCaseWorkspaceInterval: "${E2E_WORKSPACE_TIMELINE_SYNC_INTERVAL:-5m}"

clients:
  jira:
    dummy: true
    url: "${JIRA_URL:-}"
    username: "${JIRA_USERNAME:-}"
    password: "${JIRA_API_TOKEN:-${JIRA_PASSWORD:-}}"

  lark:
    larkBaseURL: "open.feishu.cn"
    appCode: "cli_e2e"
    appSecret: "e2e"
    encryptKey: "e2e"
    verificationToken: "e2e"

  agentGateway:
    address: "${AGENT_GATEWAY_ADDRESS}"
    timeout: "5s"

  devops:
    apiToken: "${DEVOPS_API_TOKEN:-}"
    baseUrl: "${DEVOPS_BASE_URL:-https://prod.devops.pingcap.com}"
    frontendUrl: "${DEVOPS_FRONTEND_URL:-https://ops.tidbcloud.com}"
    timeout: "${DEVOPS_TIMEOUT:-30s}"
EOF
}

write_config() {
  local mode="$1"
  local config_path="$2"
  if [[ -n "${BASE_CONFIG}" ]]; then
    write_config_from_base "${mode}" "${BASE_CONFIG}" "${config_path}"
  else
    write_config_from_env "${mode}" "${config_path}"
  fi
}

start_ticket_manager_local() {
  start_fake_agent_gateway
  INTERNAL_BASE_URL="${BASE_URL}"
  write_config "local" "${CONFIG_FILE}"
  (
    cd "$SERVICE_DIR"
    go run ./cmd/ticket-manager --config "${CONFIG_FILE}" >"${SERVER_LOG}" 2>&1
  ) &
  SERVER_PID=$!
}

start_ticket_manager_docker() {
  start_fake_agent_gateway
  (cd "${SERVICE_DIR}" && make build)
  INTERNAL_BASE_URL="http://127.0.0.1:8080"
  write_config "docker" "${CONFIG_FILE}"
  log "building ticket-manager image ${E2E_IMAGE_TAG}"
  docker build -f "${E2E_DOCKERFILE}" -t "${E2E_IMAGE_TAG}" "${REPO_ROOT}" >/dev/null
  log "starting ticket-manager container ${CONTAINER_NAME}"
  if [[ -n "${E2E_CONTAINER_CA_CERT_FILE}" ]]; then
    docker run -d \
      --name "${CONTAINER_NAME}" \
      --add-host host.docker.internal:host-gateway \
      -p "127.0.0.1:${TICKET_MANAGER_PORT}:8080" \
      -v "${ARTIFACT_ROOT}:/e2e-artifacts" \
      -e "APP_ENV=${APP_ENV}" \
      -e "SSL_CERT_FILE=${E2E_CONTAINER_CA_CERT_FILE}" \
      -e "REQUESTS_CA_BUNDLE=${E2E_CONTAINER_CA_CERT_FILE}" \
      -e "CURL_CA_BUNDLE=${E2E_CONTAINER_CA_CERT_FILE}" \
      -e "AWS_CA_BUNDLE=${E2E_CONTAINER_CA_CERT_FILE}" \
      -e "NODE_EXTRA_CA_CERTS=${E2E_CONTAINER_CA_CERT_FILE}" \
      "${E2E_IMAGE_TAG}" \
      --config /e2e-artifacts/ticket-manager.yaml > /dev/null
  else
    docker run -d \
      --name "${CONTAINER_NAME}" \
      --add-host host.docker.internal:host-gateway \
      -p "127.0.0.1:${TICKET_MANAGER_PORT}:8080" \
      -v "${ARTIFACT_ROOT}:/e2e-artifacts" \
      -e "APP_ENV=${APP_ENV}" \
      "${E2E_IMAGE_TAG}" \
      --config /e2e-artifacts/ticket-manager.yaml > /dev/null
  fi
  docker logs -f "${CONTAINER_NAME}" >"${SERVER_LOG}" 2>&1 &
  SERVER_LOG_PID=$!
}

ticket_manager_e2e_cleanup() {
  local exit_code=$?

  if [[ -n "${SERVER_LOG_PID:-}" ]]; then
    kill "$SERVER_LOG_PID" >/dev/null 2>&1 || true
    wait "$SERVER_LOG_PID" >/dev/null 2>&1 || true
  fi
  if [[ -n "${SERVER_PID:-}" ]]; then
    kill "$SERVER_PID" >/dev/null 2>&1 || true
    wait "$SERVER_PID" >/dev/null 2>&1 || true
  fi
  if [[ -n "${CONTAINER_NAME:-}" && "${E2E_MODE:-}" == "docker" ]]; then
    docker rm -f "$CONTAINER_NAME" >/dev/null 2>&1 || true
  fi

  if [[ -n "${FAKE_GATEWAY_PID:-}" ]]; then
    kill "${FAKE_GATEWAY_PID}" >/dev/null 2>&1 || true
    wait "${FAKE_GATEWAY_PID}" >/dev/null 2>&1 || true
  fi
  drop_database

  if [[ $exit_code -eq 0 ]]; then
    STATUS="passed"
  fi

  if [[ "$STATUS" == "passed" && "${KEEP_ARTIFACTS:-1}" != "1" ]]; then
    rm -rf "$ARTIFACT_ROOT"
  else
    if [[ "$STATUS" != "passed" && -f "${SERVER_LOG:-}" ]]; then
      log "server log tail:"
      tail -n 40 "$SERVER_LOG" || true
    fi
    log "artifacts kept at ${ARTIFACT_ROOT}"
    log "http artifacts: ${HTTP_DIR}"
    log "agent gateway log: ${ARTIFACT_ROOT}/agent-gateway.log"
    log "server log: ${SERVER_LOG}"
  fi
}

ticket_manager_e2e_start() {
  log "starting ticket-manager on ${BASE_URL}"
  log "artifact root: ${ARTIFACT_ROOT}"
  create_database
  if [[ "$E2E_MODE" == "docker" ]]; then
    start_ticket_manager_docker
  else
    start_ticket_manager_local
  fi
  wait_for_health
}

request_json() {
  local label="$1"
  local method="$2"
  local path="$3"
  local payload="${4:-}"
  local body_file
  REQUEST_SEQ=$((REQUEST_SEQ + 1))
  LAST_HTTP_BASE="${HTTP_DIR}/$(printf '%02d' "$REQUEST_SEQ")-${label}"
  body_file="${LAST_HTTP_BASE}.response.body"

  printf '%s %s\n' "$method" "$path" > "${LAST_HTTP_BASE}.request.txt"
  if [[ -n "$payload" ]]; then
    printf '%s\n' "$payload" > "${LAST_HTTP_BASE}.request.body"
  fi

  log "${label}: ${method} ${path}"
  if [[ -n "$payload" ]]; then
    log "${label} request body: $(cat "${LAST_HTTP_BASE}.request.body")"
  fi

  if [[ "$method" == "GET" ]]; then
    LAST_STATUS="$(curl -sS -o "$body_file" -w '%{http_code}' "${BASE_URL}${path}")"
  else
    LAST_STATUS="$(curl -sS -o "$body_file" -w '%{http_code}' -X "$method" -H 'Content-Type: application/json' -d "$payload" "${BASE_URL}${path}")"
  fi

  printf '%s\n' "$LAST_STATUS" > "${LAST_HTTP_BASE}.response.status"
  LAST_BODY_FILE="$body_file"
  log "${label}: status ${LAST_STATUS}"
  log "${label} response body: $(cat "$LAST_BODY_FILE")"
}

expect_status() {
  local expected="$1"
  if [[ "$LAST_STATUS" != "$expected" ]]; then
    fail "unexpected HTTP status ${LAST_STATUS} for body $(cat "$LAST_BODY_FILE")"
  fi
}

wait_for_health() {
  local retries=60
  while (( retries > 0 )); do
    if curl -fsS "${BASE_URL}/healthz" >/dev/null 2>&1; then
      return 0
    fi
    sleep 1
    retries=$((retries - 1))
  done

  fail "ticket-manager did not become healthy; see ${SERVER_LOG}"
}

sql_scalar() {
  local query="$1"
  mysql_client --database="${E2E_DB_NAME}" --execute "${query}"
}
