-- Self-test for the hand-written MySQL driver. Run with: ensure rp_dbtest

local passed, failed = 0, 0

local function check(name, ok, detail)
  if ok then
    passed = passed + 1
    print(('^2  PASS^7 %s'):format(name))
  else
    failed = failed + 1
    print(('^1  FAIL^7 %s^1 %s^7'):format(name, detail or ''))
  end
end

CreateThread(function()
  print('^5[dbtest]^7 waiting for the pool...')
  if not DB.WaitReady(30000) then
    print('^1[dbtest] database never became ready^7')
    return
  end

  -- 1. round trip through the text protocol -------------------------------
  local two = DB.Scalar('SELECT 1 + 1')
  check('scalar arithmetic', two == 2, ('got %s (%s)'):format(tostring(two), type(two)))

  local row = DB.Single('SELECT ? AS s, ? AS n, ? AS f', { 'hello', 42, 1.5 })
  check('typed columns', row and row.s == 'hello' and row.n == 42 and row.f == 1.5,
        row and json.encode(row) or 'nil')

  -- 2. hostile strings must survive verbatim ------------------------------
  DB.Update("DELETE FROM accounts WHERE username LIKE 'ztest%'")

  local nasty = [[O'Brien \ "quoted" ]] .. '\n\t' .. [[ 100% ` ; -- /* */ ?]]
  local id = DB.Insert(
    'INSERT INTO accounts (username, password_hash) VALUES (?, ?)',
    { 'ztest_a', nasty })
  check('insert returns id', type(id) == 'number' and id > 0, tostring(id))

  local back = DB.Scalar('SELECT password_hash FROM accounts WHERE id = ?', { id })
  check('hostile string round trip', back == nasty,
        ('stored %q'):format(tostring(back)))

  -- 3. a parameter can never become SQL -----------------------------------
  local inject = "'); DROP TABLE accounts; -- "
  DB.Insert('INSERT INTO accounts (username, password_hash) VALUES (?, ?)',
            { 'ztest_b', inject })
  local stillThere = DB.Scalar(
    "SELECT COUNT(*) FROM information_schema.tables WHERE table_schema = DATABASE() AND table_name = 'accounts'")
  check('injection is inert', stillThere == 1, 'accounts table missing!')
  local storedInject = DB.Scalar("SELECT password_hash FROM accounts WHERE username = 'ztest_b'")
  check('injection stored literally', storedInject == inject, tostring(storedInject))

  -- 4. question marks inside literals must not shift binding ---------------
  local q = DB.Single("SELECT 'what? really?' AS lit, ? AS bound", { 'bound-value' })
  check('literal ? not treated as placeholder',
        q and q.lit == 'what? really?' and q.bound == 'bound-value',
        q and json.encode(q) or 'nil')

  -- 5. NULL handling -------------------------------------------------------
  DB.Update('UPDATE accounts SET last_ip = ? WHERE id = ?', { DB.NULL, id })
  local isNull = DB.Scalar('SELECT last_ip IS NULL FROM accounts WHERE id = ?', { id })
  check('DB.NULL writes a real NULL', isNull == 1, tostring(isNull))
  local nilRow = DB.Single('SELECT last_ip FROM accounts WHERE id = ?', { id })
  check('NULL reads back as nil', nilRow ~= nil and nilRow.last_ip == nil,
        nilRow and json.encode(nilRow) or 'nil')

  -- 6. unicode -------------------------------------------------------------
  local uni = 'Ünïcødé — 日本語 — 🚓'
  DB.Update('UPDATE accounts SET password_hash = ? WHERE id = ?', { uni, id })
  check('utf8mb4 round trip', DB.Scalar('SELECT password_hash FROM accounts WHERE id = ?', { id }) == uni)

  -- 7. empty result sets ---------------------------------------------------
  local none = DB.Query('SELECT * FROM accounts WHERE id = ?', { -1 })
  check('empty select returns empty table', type(none) == 'table' and #none == 0)
  check('missing single returns nil', DB.Single('SELECT * FROM accounts WHERE id = ?', { -1 }) == nil)

  -- 8. transactions --------------------------------------------------------
  local ok = DB.Transaction({
    { query = 'UPDATE accounts SET role = ? WHERE id = ?', values = { 'admin', id } },
    { query = 'UPDATE accounts SET banned = ? WHERE id = ?', values = { 1, id } },
  })
  local after = DB.Single('SELECT role, banned FROM accounts WHERE id = ?', { id })
  check('transaction commits', ok and after.role == 'admin' and after.banned == 1,
        json.encode(after or {}))

  local bad, err = DB.Transaction({
    { query = 'UPDATE accounts SET role = ? WHERE id = ?', values = { 'mod', id } },
    { query = 'UPDATE accounts SET nonexistent_column = 1 WHERE id = ?', values = { id } },
  })
  local rolled = DB.Scalar('SELECT role FROM accounts WHERE id = ?', { id })
  check('failed transaction rolls back', bad == false and rolled == 'admin',
        ('role is now %s'):format(tostring(rolled)))

  -- 9. errors surface instead of failing silently --------------------------
  local res, qerr = DB.Query('SELECT * FROM table_that_does_not_exist')
  check('bad query returns an error', res == nil and type(qerr) == 'string' and #qerr > 0,
        tostring(qerr))

  -- 10. wrong parameter count is caught, not guessed -----------------------
  local _, cerr = DB.Query('SELECT ?, ?', { 1 })
  check('parameter count mismatch reported', cerr ~= nil, tostring(cerr))

  -- 11. a large payload exercises multi-packet framing in both directions --
  local big = string.rep('x', 700000)
  check('700KB payload round trip', DB.Scalar('SELECT ? AS big', { big }) == big)

  -- 12. concurrency: many queries in flight across the pool ----------------
  local done, results = 0, {}
  for i = 1, 40 do
    DB.ScalarAsync('SELECT ?', { i }, function(v) done = done + 1; results[i] = v end)
  end
  local waited = 0
  while done < 40 and waited < 10000 do Wait(50); waited = waited + 50 end
  local allGood = done == 40
  for i = 1, 40 do if results[i] ~= i then allGood = false end end
  check('40 concurrent queries all answered correctly', allGood, ('done=%d'):format(done))

  DB.Update("DELETE FROM accounts WHERE username LIKE 'ztest%'")

  print(('^5[dbtest]^7 ==== %d passed, %d failed ===='):format(passed, failed))
  if failed == 0 then print('^2[dbtest] driver OK^7') else print('^1[dbtest] DRIVER HAS PROBLEMS^7') end
end)
