#!/usr/bin/env bash
#
# Contract test: SOQL query response envelope and error shape.
#
# Optional diagnostic: current LIMIT/OFFSET cursor smoke via /query/:locator.
# That diagnostic is intentionally not a Salesforce v63 contract assertion.
#
# Exercises the Salesforce REST API contract for /query as implemented in
# backend/internal/interfaces/rest/query_handler.go. The /query/:locator smoke
# path below is diagnostic only until native batch-size queryMore is supported.
#
# Contract dimensions:
#   1. Response envelope — HTTP 200, totalSize (number), done (bool),
#      records (array).
#   2. Pagination done=true — query with limit >= total rows.
#   3. Malformed query — HTTP 400 with SF error code.
#
# Non-contract diagnostic, opt-in with RUN_QUERYMORE_DIAGNOSTIC=true:
#   A. Current backend LIMIT/OFFSET cursor emits done=false + nextRecordsUrl.
#   B. Current backend /query/:locator round-trips that locator.
#
# Usage (from repo root):
#   ./tests/contract/rest_api/verify_query_pagination.sh
#
# Requires: running backend, TiDB with at least one standard object
# installed (e.g. after init-l0 + install-pack).

set -uo pipefail

SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
source "${SCRIPT_DIR}/fixtures.sh"

OUT_DIR="${OUT_DIR:-/tmp/sfmeta/run/contract}"
mkdir -p "$OUT_DIR"

fails=0
diagnostic_fails=0
: "${RUN_QUERYMORE_DIAGNOSTIC:=false}"

# ---------- Seed deterministic fixture for pagination ----------
# The optional diagnostic needs more records than the page size to exercise the
# current backend cursor. It deliberately uses SOQL LIMIT as an implementation
# smoke test only; Salesforce v63 queryMore pagination is driven by server batch
# size / Sforce-Query-Options, not by treating SOQL LIMIT as page size.

SEED_IDS=()

seed_pagination_fixture() {
  echo "--- seeding pagination fixture (5 Accounts) ---" >&2
  local i id_body id status
  for i in $(seq 1 5); do
    id_body=$(authenticated_post "/services/data/${API_VER}/sobjects/Account" \
      '{"Name":"ContractTest_Pagination_'$i'"}') || {
      echo "  FAIL: could not seed Account $i" >&2
      return 1
    }
    # sobjects POST returns {"id":"<sfid>","success":true}
    id=$(echo "$id_body" | jq -r '.id // empty' 2>/dev/null)
    if [[ -z "$id" ]]; then
      echo "  FAIL: no id in seed response for Account $i" >&2
      return 1
    fi
    SEED_IDS+=("$id")
  done
  echo "  seeded ${#SEED_IDS[@]} Account records" >&2
}

cleanup_pagination_fixture() {
  if [[ ${#SEED_IDS[@]} -eq 0 ]]; then return 0; fi
  echo "--- cleaning up pagination fixture ---" >&2
  local id
  for id in "${SEED_IDS[@]}"; do
    authenticated_delete "/services/data/${API_VER}/sobjects/Account/${id}" >/dev/null 2>&1 || true
  done
  echo "  cleaned up ${#SEED_IDS[@]} Account records" >&2
}

# ---------- Pre-flight: login ----------
if ! login_admin; then
  echo "  SKIP: cannot authenticate; backend may not be running at ${BASE_URL}" >&2
  exit 2
fi

# Cleanup runs on exit (even on failure) — install BEFORE seeding so
# that a partial seed is rolled up. The cleanup function guards on
# SEED_IDS being empty, so installing it even when diagnostics are disabled is
# a no-op.
trap cleanup_pagination_fixture EXIT

echo "═══ REST contract: query pagination ═══" >&2

# ---------- Dim 1: response envelope ----------
test_response_envelope() {
  echo "--- dim 1: response envelope ---" >&2

  local body status
  local resp
  resp=$(authenticated_get "/services/data/${API_VER}/query?q=SELECT+Id+FROM+User+LIMIT+1")
  status=$(echo "$resp" | tail -1)
  body=$(echo "$resp" | sed '$d')

  if ! assert_eq "HTTP status" "200" "$status"; then return 1; fi
  if ! assert_has_key "envelope" "$body" "totalSize"; then return 1; fi
  if ! assert_has_key "envelope" "$body" "done"; then return 1; fi
  if ! assert_has_key "envelope" "$body" "records"; then return 1; fi

  # totalSize must be a number, records must be an array
  local ts_type rec_type
  ts_type=$(echo "$body" | jq -r '.totalSize | type')
  rec_type=$(echo "$body" | jq -r '.records | type')
  assert_eq "totalSize type" "number" "$ts_type" || return 1
  assert_eq "records type" "array" "$rec_type" || return 1

  echo "  ✅ envelope OK" >&2
}

# ---------- Dim 2: done=true for small result set ----------
test_done_true() {
  echo "--- dim 2: done=true for small result set ---" >&2

  local resp body status
  resp=$(authenticated_get "/services/data/${API_VER}/query?q=SELECT+Id+FROM+User+LIMIT+1000")
  status=$(echo "$resp" | tail -1)
  body=$(echo "$resp" | sed '$d')

  if ! assert_eq "HTTP status" "200" "$status"; then return 1; fi
  if ! assert_json_field_eq "done flag" "$body" '.done' "true"; then return 1; fi
  if ! assert_json_field_eq "no nextRecordsUrl" "$body" '.nextRecordsUrl // "absent"' "absent"; then return 1; fi

  echo "  ✅ done=true OK" >&2
}

# ---------- Diagnostic A: current LIMIT/OFFSET cursor emits nextRecordsUrl ----------
diagnose_limit_offset_cursor() {
  echo "--- diagnostic A: LIMIT/OFFSET cursor smoke (non-contract) ---" >&2

  # First, get the total record count without LIMIT.
  local resp body status ts
  resp=$(authenticated_get "/services/data/${API_VER}/query?q=SELECT+Id+FROM+Account")
  status=$(echo "$resp" | tail -1)
  body=$(echo "$resp" | sed '$d')

  ts=$(echo "$body" | jq -r '.totalSize')
  if [[ "$ts" -lt 3 ]]; then
    echo "  FAIL: only ${ts} Account record(s); need >=3 to prove truncation" >&2
    return 1
  fi

  # Current-backend diagnostic only: LIMIT 2 presently acts as the page
  # boundary. This must not be treated as v63 native batch-size semantics.
  local resp2 body2
  resp2=$(authenticated_get "/services/data/${API_VER}/query?q=SELECT+Id+FROM+Account+LIMIT+2")
  status=$(echo "$resp2" | tail -1)
  body2=$(echo "$resp2" | sed '$d')

  if ! assert_json_field_eq "done flag" "$body2" '.done' "false"; then return 1; fi

  local next_url
  next_url=$(echo "$body2" | jq -r '.nextRecordsUrl // empty')
  if [[ -z "$next_url" ]]; then
    echo "  FAIL nextRecordsUrl: missing when done=false" >&2
    return 1
  fi

  # nextRecordsUrl must contain /query/ (locator path pattern)
  if [[ "$next_url" != *"/query/"* ]]; then
    echo "  FAIL nextRecordsUrl format: ${next_url} (expected /.../query/...)" >&2
    return 1
  fi

  echo "  ✅ diagnostic cursor smoke OK (${next_url})" >&2
}

# ---------- Diagnostic B: current /query/:locator round-trip ----------
diagnose_query_more_roundtrip() {
  echo "--- diagnostic B: QueryMore round-trip (non-contract) ---" >&2

  # First, get total count without LIMIT.
  local resp body status ts
  resp=$(authenticated_get "/services/data/${API_VER}/query?q=SELECT+Id+FROM+Account")
  status=$(echo "$resp" | tail -1)
  body=$(echo "$resp" | sed '$d')

  ts=$(echo "$body" | jq -r '.totalSize')
  if [[ "$ts" -lt 3 ]]; then
    echo "  FAIL: only ${ts} Account record(s); need >=3 for QueryMore" >&2
    return 1
  fi

  # Current-backend diagnostic only: LIMIT 2 presently produces a cursor.
  local resp2 body2
  resp2=$(authenticated_get "/services/data/${API_VER}/query?q=SELECT+Id+FROM+Account+LIMIT+2")
  status=$(echo "$resp2" | tail -1)
  body2=$(echo "$resp2" | sed '$d')

  local next_url
  next_url=$(echo "$body2" | jq -r '.nextRecordsUrl // empty')
  if [[ -z "$next_url" ]]; then
    echo "  FAIL nextRecordsUrl: missing for QueryMore test" >&2
    return 1
  fi

  # Extract locator from nextRecordsUrl path (everything after /query/)
  local locator
  locator=$(echo "$next_url" | sed 's|.*/query/||')
  if [[ -z "$locator" ]]; then
    echo "  FAIL: cannot extract locator from ${next_url}" >&2
    return 1
  fi

  # Call QueryMore
  local resp3 body3 status3
  resp3=$(authenticated_get "/services/data/${API_VER}/query/${locator}")
  status3=$(echo "$resp3" | tail -1)
  body3=$(echo "$resp3" | sed '$d')

  if ! assert_eq "QueryMore HTTP status" "200" "$status3"; then return 1; fi
  if ! assert_has_key "QueryMore envelope" "$body3" "records"; then return 1; fi
  if ! assert_has_key "QueryMore envelope" "$body3" "done"; then return 1; fi

  local rec_count done_val
  rec_count=$(echo "$body3" | jq -r '.records | length')
  done_val=$(echo "$body3" | jq -r '.done')
  if [[ "$done_val" != "true" && "$done_val" != "false" ]]; then
    echo "  FAIL QueryMore done: not a boolean (got ${done_val})" >&2
    return 1
  fi

  # Cursor correctness: page-2 IDs must be disjoint from page-1 IDs.
  local p1_ids p2_ids overlap_count
  p1_ids=$(echo "$body2" | jq -c '[.records[].Id]')
  p2_ids=$(echo "$body3" | jq -c '[.records[].Id]')
  overlap_count=$(jq -n --argjson a "$p1_ids" --argjson b "$p2_ids" \
    '[$a[], $b[]] | group_by(.) | map(select(length > 1)) | length')
  if [[ "$overlap_count" -gt 0 ]]; then
    echo "  FAIL QueryMore disjointness: ${overlap_count} ID(s) appear on both pages" >&2
    return 1
  fi

  # totalSize must be stable across pages (same result set, different window).
  local p2_total
  p2_total=$(echo "$body3" | jq -r '.totalSize // empty')
  if [[ -n "$p2_total" && "$p2_total" != "$ts" ]]; then
    echo "  FAIL QueryMore totalSize: page2=${p2_total}, expected=${ts} (must match initial query)" >&2
    return 1
  fi

  # When page 2 is the final page, record count must equal totalSize - page-1 count.
  if [[ "$done_val" == "true" ]]; then
    local expected_p2_count=$((ts - 2))
    if [[ "$rec_count" -ne "$expected_p2_count" ]]; then
      echo "  FAIL QueryMore page-2 count: got=${rec_count}, expected=${expected_p2_count} (totalSize=${ts} - diagnostic LIMIT=2)" >&2
      return 1
    fi
  fi

  echo "  ✅ diagnostic QueryMore round-trip OK (${rec_count} records, done=${done_val})" >&2
}

# ---------- Dim 3: malformed query returns 400 with SF error ----------
test_malformed_query() {
  echo "--- dim 3: malformed query → 400 ---" >&2

  local resp body status
  resp=$(authenticated_get "/services/data/${API_VER}/query?q=SELECTT+Id+FROM+User")
  status=$(echo "$resp" | tail -1)
  body=$(echo "$resp" | sed '$d')

  if ! assert_eq "malformed query status" "400" "$status"; then return 1; fi

  # Body should be an SF error array with errorCode
  local err_code
  err_code=$(echo "$body" | jq -r '.[0].errorCode // empty' 2>/dev/null)
  if [[ -z "$err_code" ]]; then
    echo "  FAIL malformed query: no SF errorCode in body" >&2
    return 1
  fi

  echo "  ✅ malformed query → 400 (${err_code}) OK" >&2
}

# ---------- Run ----------
test_response_envelope      || fails=$((fails+1))
test_done_true              || fails=$((fails+1))
test_malformed_query        || fails=$((fails+1))

if [[ "$RUN_QUERYMORE_DIAGNOSTIC" == "true" ]]; then
  seed_pagination_fixture || {
    echo "  DIAGNOSTIC FAIL: could not seed pagination fixture" >&2
    diagnostic_fails=$((diagnostic_fails+1))
  }
  if [[ $diagnostic_fails -eq 0 ]]; then
    diagnose_limit_offset_cursor || diagnostic_fails=$((diagnostic_fails+1))
    diagnose_query_more_roundtrip || diagnostic_fails=$((diagnostic_fails+1))
  fi
else
  echo "--- diagnostic: LIMIT/OFFSET queryMore smoke skipped (set RUN_QUERYMORE_DIAGNOSTIC=true) ---" >&2
fi

echo "" >&2
if [[ $fails -eq 0 ]]; then
  echo "════════ query pagination contract: ALL PASS (3/3 dimensions) ════════" >&2
else
  echo "════════ query pagination contract: ${fails} FAIL(S) ════════" >&2
fi
if [[ $diagnostic_fails -gt 0 ]]; then
  echo "════════ queryMore diagnostic: ${diagnostic_fails} FAIL(S), non-contract ════════" >&2
fi
exit "$fails"
