///|
pub fn compare_search(request : Value) -> Value raise {
  let old = get(request, "old")
  let next = get(request, "new")
  ignore(obj(old))
  ignore(obj(next))
  for key in ["queries", "limit", "offset", "where", "facets", "k1", "b"] {
    if get(old, key) != get(next, key) {
      raise InputError(
        "search comparison requires identical query/filter/ranking/page settings",
      )
    }
  }
  let a = arr(get(search_request(old), "results"))
  let b = arr(get(search_request(next), "results"))
  let output : Array[Value] = []
  let mut changed = 0
  for i = 0; i < a.length(); i = i + 1 {
    let left = arr(get(a[i], "hits"))
    let right = arr(get(b[i], "hits"))
    let old_ids = left.map(fn(hit) { str(get(hit, "id")) })
    let new_ids = right.map(fn(hit) { str(get(hit, "id")) })
    let added = new_ids.filter(fn(id) { !old_ids.contains(id) })
    let removed = old_ids.filter(fn(id) { !new_ids.contains(id) })
    let score_changes : Array[Value] = []
    for before in left {
      for after in right {
        if get(before, "id") == get(after, "id") &&
          get(before, "score") != get(after, "score") {
          score_changes.push(
            record([
              ("id", get(before, "id")),
              ("before", get(before, "score")),
              ("after", get(after, "score")),
            ]),
          )
        }
      }
    }
    let drift = old_ids != new_ids ||
      get(a[i], "total") != get(b[i], "total") ||
      !score_changes.is_empty() ||
      get(a[i], "facets") != get(b[i], "facets")
    if drift {
      changed += 1
    }
    output.push(
      record([
        ("query", get(a[i], "query")),
        ("before_total", get(a[i], "total")),
        ("after_total", get(b[i], "total")),
        ("added", strings(added)),
        ("removed", strings(removed)),
        ("before_order", strings(old_ids)),
        ("after_order", strings(new_ids)),
        ("score_changes", Array(score_changes)),
        ("changed", Bool(drift)),
      ]),
    )
  }
  record([
    ("queries", Array(output)),
    ("changed_count", Number(changed.to_double())),
    ("equal_on_queries", Bool(changed == 0)),
    (
      "scope",
      String(
        "returned pages and totals at fixed query settings; not a complete index equivalence proof",
      ),
    ),
  ])
}

///|
pub fn run(request : Value) -> Value raise {
  match optional_str(get(request, "operation"), "search") {
    "search" => search_request(request)
    "compare" => compare_search(request)
    _ => raise InputError("operation must be search or compare")
  }
}