///|
// fts5_sanitize.mbt — FTS5 query sanitization for safe use in SQLite MATCH.
//
// Strategy (mirrors hermes _sanitize_fts5_query, simplified):
//   - Tokenize on whitespace
//   - Wrap each token in double quotes
//   - Escape inner double quotes by doubling: `"` → `""`
//   - Drop tokens that are empty after escaping
//   - If no tokens remain, return "" (caller should skip the query)
//
// This approach treats every token as a literal phrase, preventing FTS5
// operator injection (AND/OR/NOT/NEAR, unbalanced parens, stray *, etc.).
// It is intentionally simpler than the hermes version (which preserves
// balanced quoted phrases); for mnemo all tokens are quoted literals.

///| Sanitize a user-supplied string for safe use in FTS5 MATCH expressions.
/// Returns an empty string when input is blank — caller must treat that as
/// "no search" and return an empty result set.
///
/// Pure MoonBit implementation — cross-target. Tokenize on any ASCII
/// whitespace (space, tab, newline, carriage return, form feed, vertical
/// tab), wrap each non-empty token in double quotes, and double any
/// inner `"` to escape it.
pub fn sanitize_fts5_query(q : String) -> String {
  let view = q.view()
  let tokens : Array[String] = []
  let buf = StringBuilder::new()
  let flush = fn() {
    let t = buf.to_string()
    if !t.is_empty() {
      tokens.push(t)
      buf.reset()
    }
  }
  let mut i = 0
  while i < view.length() {
    let c = match view.get_char(i) {
      Some(ch) => ch
      None => ' '
    }
    if c == ' ' || c == '\t' || c == '\n' || c == '\r' || c == '\u{000C}' || c == '\u{000B}' {
      flush()
    } else {
      buf.write_char(c)
    }
    i = i + 1
  }
  flush()
  if tokens.length() == 0 {
    return ""
  }
  // Escape inner `"` by doubling, then wrap.
  let out : Array[String] = []
  for t in tokens {
    let escaped = StringBuilder::new()
    let tv = t.view()
    let mut j = 0
    while j < tv.length() {
      let c = match tv.get_char(j) {
        Some(ch) => ch
        None => ' '
      }
      if c == '"' {
        escaped.write_string("\"\"")
      } else {
        escaped.write_char(c)
      }
      j = j + 1
    }
    out.push("\"" + escaped.to_string() + "\"")
  }
  out.join(" ")
}

// ── inline tests ──

///|
test "sanitize_fts5_query: normal single word" {
  assert_eq(sanitize_fts5_query("moonbit"), "\"moonbit\"")
}

///|
test "sanitize_fts5_query: multi-word wraps each token" {
  assert_eq(sanitize_fts5_query("moonbit rocks"), "\"moonbit\" \"rocks\"")
}

///|
test "sanitize_fts5_query: special chars C++ and parens quoted" {
  assert_eq(sanitize_fts5_query("C++ (fast)"), "\"C++\" \"(fast)\"")
}

///|
test "sanitize_fts5_query: inner quotes escaped by doubling" {
  // say "hi" → "say" """hi"""
  assert_eq(sanitize_fts5_query("say \"hi\""), "\"say\" \"\"\"hi\"\"\"")
}

///|
test "sanitize_fts5_query: empty string returns empty" {
  assert_eq(sanitize_fts5_query(""), "")
}

///|
test "sanitize_fts5_query: whitespace-only returns empty" {
  assert_eq(sanitize_fts5_query("   "), "")
}

///|
test "sanitize_fts5_query: unbalanced quote properly escaped" {
  // foo" → "foo""
  assert_eq(sanitize_fts5_query("foo\""), "\"foo\"\"\"")
}