#!/usr/bin/env bash
#
# Contract test: Audit V2 atomic activation and head-row filtering.
#
# Exercises the Salesforce REST API contract for:
#   - <Object>History virtual view via SOQL query (C11 / §5.1)
#   - atomically active producer + outbox fan-out path
#   - Head-row suppression (Field <> '__record__')
#
# As implemented in:
#   backend/internal/features/audit/v2_hook.go
#   backend/internal/features/audit/history_soql.go
#   backend/internal/features/records/audit_v2_wiring.go
#   backend/internal/interfaces/rest/history_query.go
#
# Dimensions:
#   1. History query endpoint — SELECT … FROM <Object>History returns a
#      valid SOQL envelope (totalSize, done, records).
#   2. Unsupported predicate (WHERE Field = 'X') → HTTP 400
#      MALFORMED_QUERY (Phase 2A constraint).
#   3. History without parent filter → HTTP 200 with valid envelope (optional
#      filter per backend §5.1).
#   4. Head-row suppression — the __record__ sentinel rows are filtered
#      from the response per §5.1.
#   5. An unrelated parent with no history returns an empty valid envelope.
#
# Usage (from repo root):
#   ./tests/contract/rest_api/verify_audit_gating.sh

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

# ---------- Seed deterministic audit fixture ----------
# Dims 4 (head-row suppression) need real AccountHistory rows.
# We create a test Account, PATCH its Name to trigger history tracking,
# then verify dim 4 against that row. Cleanup restores original state.

AUDIT_SEED_IDS=()

seed_audit_fixture() {
  echo "--- seeding audit fixture (Account + field change) ---" >&2

  # Create a test Account
  local body
  body=$(authenticated_post "/services/data/${API_VER}/sobjects/Account" \
    '{"Name":"ContractTest_AuditGating","Description":"before"}') || {
    echo "  FAIL: could not seed audit Account" >&2
    return 1
  }
  local acct_id
  acct_id=$(echo "$body" | jq -r '.id // empty' 2>/dev/null)
  if [[ -z "$acct_id" ]]; then
    echo "  FAIL: no id in Account seed response" >&2
    return 1
  fi
  AUDIT_SEED_IDS+=("$acct_id")

  # PATCH the Name to generate a history row; fail hard if the HTTP layer rejects it.
  local patch_resp patch_status
  patch_resp=$(authenticated_patch "/services/data/${API_VER}/sobjects/Account/${acct_id}" \
    '{"Name":"ContractTest_AuditGating_updated"}') || {
    echo "  FAIL: PATCH network error — cannot seed history row" >&2
    return 1
  }
  patch_status=$(echo "$patch_resp" | tail -1)
  if [[ "$patch_status" != 2* ]]; then
    echo "  FAIL: PATCH returned HTTP ${patch_status} — history row will not be written" >&2
    return 1
  fi

  # Small delay to let the history row persist
  sleep 1
  echo "  seeded Account ${acct_id} with field change" >&2
}

cleanup_audit_fixture() {
  if [[ ${#AUDIT_SEED_IDS[@]} -eq 0 ]]; then return 0; fi
  echo "--- cleaning up audit fixture ---" >&2
  local id
  for id in "${AUDIT_SEED_IDS[@]}"; do
    authenticated_delete "/services/data/${API_VER}/sobjects/Account/${id}" >/dev/null 2>&1 || true
  done
  echo "  cleaned up ${#AUDIT_SEED_IDS[@]} Account(s)" >&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.
trap cleanup_audit_fixture EXIT

seed_audit_fixture || {
  echo "  FAIL: could not seed audit fixture" >&2
  exit 2
}

echo "═══ REST contract: audit gating (CON-005) ═══" >&2

# ---------- Dim 1: History query returns valid envelope ----------
test_history_envelope() {
  echo "--- dim 1: history query envelope ---" >&2

  local body status resp
  # Query AccountHistory with a dummy AccountId — standard history objects use
  # object-specific parent fields, not ParentId.
  # valid even if the parent doesn't exist or has no history.
  resp=$(authenticated_get "/services/data/${API_VER}/query?q=SELECT+Id,+AccountId,+Field,+NewValue,+CreatedDate+FROM+AccountHistory+WHERE+AccountId%3D'001xx0000000001'")
  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 "history envelope" "$body" "totalSize"; then return 1; fi
  if ! assert_has_key "history envelope" "$body" "done"; then return 1; fi
  if ! assert_has_key "history envelope" "$body" "records"; then return 1; fi

  local rec_type
  rec_type=$(echo "$body" | jq -r '.records | type')
  assert_eq "records type" "array" "$rec_type" || return 1

  echo "  ✅ history envelope OK" >&2
}

# ---------- Dim 2: Unsupported predicate → 400 MALFORMED_QUERY ----------
test_history_unsupported_predicate() {
  echo "--- dim 2: unsupported predicate → 400 ---" >&2

  local body status resp
  # Phase 2A supports only the history parent field. A predicate on any other
  # field must return HTTP 400 with Salesforce v63 MALFORMED_QUERY.
  resp=$(authenticated_get "/services/data/${API_VER}/query?q=SELECT+Id+FROM+AccountHistory+WHERE+Field%3D'Description'")
  status=$(echo "$resp" | tail -1)
  body=$(echo "$resp" | sed '$d')

  if ! assert_eq "unsupported predicate status" "400" "$status"; then return 1; fi

  # The response should contain the documented Salesforce wire code.
  if echo "$body" | grep -q "MALFORMED_QUERY"; then
    echo "  ✅ unsupported predicate → 400 (MALFORMED_QUERY) OK" >&2
  else
    echo "  FAIL unsupported predicate: body missing MALFORMED_QUERY" >&2
    return 1
  fi
}

# ---------- Dim 3: No parent filter is accepted (returns 200) ----------
test_history_optional_parent_id() {
  echo "--- dim 3: History without parent filter → 200 (optional filter) ---" >&2

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

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

  echo "  ✅ no parent filter → 200 (valid envelope) OK" >&2
}

# ---------- Dim 4: Head-row suppression ----------
test_head_row_suppression() {
  echo "--- dim 4: head-row suppression (Field ≠ __record__) ---" >&2

  # Query the seeded audit row by its AccountId. The seed step created
  # an Account and patched its Name, which generates a real history row.
  local parent_id="${AUDIT_SEED_IDS[0]}"
  local body status resp total attempt
  total=0
  for attempt in $(seq 1 20); do
    resp=$(authenticated_get "/services/data/${API_VER}/query?q=SELECT+Id,+AccountId,+Field,+NewValue+FROM+AccountHistory+WHERE+AccountId%3D'${parent_id}'")
    status=$(echo "$resp" | tail -1)
    body=$(echo "$resp" | sed '$d')
    if ! assert_eq "HTTP status" "200" "$status"; then return 1; fi
    total=$(echo "$body" | jq -r '.totalSize')
    if [[ "$total" -gt 0 ]]; then break; fi
    sleep 0.5
  done

  if [[ "$total" -eq 0 ]]; then
    echo "  FAIL: seeded Account ${parent_id} has 0 history rows. Atomic Audit V2 activation, fan-out, or history tracking is not ready." >&2
    return 1
  fi

  # No record should have Field = '__record__' (head rows are filtered
  # by the History SOQL translator per §5.1).
  local head_count
  head_count=$(echo "$body" | jq '[.records[] | select(.Field == "__record__")] | length')
  if [[ "$head_count" -gt 0 ]]; then
    echo "  FAIL: ${head_count} head row(s) leaked (Field='__record__')" >&2
    return 1
  fi

  echo "  ✅ head-row suppression OK (${total} rows, no sentinel)" >&2
}

# ---------- Dim 5: Empty audit trail keeps a valid envelope ----------
test_empty_history_envelope() {
  echo "--- dim 5: empty history envelope ---" >&2

  # An unrelated parent should return a valid SOQL envelope with totalSize=0
  # and done=true — not an error or empty body.
  local body status resp
  resp=$(authenticated_get "/services/data/${API_VER}/query?q=SELECT+Id,+UserId+FROM+UserHistory+WHERE+UserId%3D'005xx0000000001'")
  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 "totalSize" "$body" '.totalSize' "0"; then return 1; fi
  if ! assert_json_field_eq "done flag" "$body" '.done' "true"; then return 1; fi

  local total
  total=$(echo "$body" | jq -r '.totalSize')
  echo "  INFO: unrelated UserHistory totalSize=${total}" >&2

  echo "  ✅ empty history envelope OK" >&2
}

# ---------- Run ----------
test_history_envelope             || fails=$((fails+1))
test_history_unsupported_predicate || fails=$((fails+1))
test_history_optional_parent_id   || fails=$((fails+1))
test_head_row_suppression         || fails=$((fails+1))
test_empty_history_envelope       || fails=$((fails+1))

echo "" >&2
if [[ $fails -eq 0 ]]; then
  echo "════════ audit gating: ALL PASS (5/5 dimensions) ════════" >&2
else
  echo "════════ audit gating: ${fails} FAIL(S) ════════" >&2
fi
exit "$fails"
