-- ---------------------------------------------------------------------------
--  End-to-end check of the account and character data path.
--
--  The first run creates a marker account. Every later run finds it already
--  there and verifies it came back byte-for-byte, which is what "persists
--  between restarts" actually means. Run `selftest_reset` to clear it.
-- ---------------------------------------------------------------------------

local MARKER   = 'zz_selftest'
local PASSWORD = 'correct horse 7'

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

local function hash(pw)
  local p = promise.new()
  exports.rp_core:hashPassword(pw, function(h, err) p:resolve({ h = h, err = err }) end)
  local r = Citizen.Await(p)
  return r.h, r.err
end

local function verify(pw, stored)
  local p = promise.new()
  exports.rp_core:verifyPassword(pw, stored, function(ok, err) p:resolve({ ok = ok, err = err }) end)
  return Citizen.Await(p).ok
end

CreateThread(function()
  if not DB.WaitReady(30000) then
    print('^1[selftest] database never became ready^7')
    return
  end

  print('^5[selftest]^7 ---- account + character data path ----')

  -- 1. password hashing ----------------------------------------------------
  local h, herr = hash(PASSWORD)
  check('scrypt hash produced', type(h) == 'string' and h:sub(1, 7) == 'scrypt$', tostring(herr))
  check('correct password verifies', verify(PASSWORD, h))
  check('wrong password rejected', not verify(PASSWORD .. 'x', h))
  check('empty password rejected', not verify('', h))

  local h2 = hash(PASSWORD)
  check('same password hashes differently (salted)', h ~= h2)
  check('second hash also verifies', verify(PASSWORD, h2))
  check('corrupt hash rejected safely', not verify(PASSWORD, 'not-a-hash'))

  -- 2. the account row ------------------------------------------------------
  local existing = DB.Single('SELECT id, password_hash, created_at FROM accounts WHERE username = ?', { MARKER })
  local firstRun = existing == nil
  local accountId

  if firstRun then
    accountId = DB.Insert(
      'INSERT INTO accounts (username, password_hash, license) VALUES (?, ?, ?)',
      { MARKER, h, 'selftest-license' })
    check('account created', type(accountId) == 'number' and accountId > 0)
    print('^3[selftest]^7 marker account created - restart the server to prove persistence')
  else
    accountId = existing.id
    check('account survived restart', true)
    check('stored hash still verifies after restart', verify(PASSWORD, existing.password_hash))
    print(('^5[selftest]^7 marker account has existed since %s'):format(tostring(existing.created_at)))
  end

  -- 3. the character row, including the appearance JSON ---------------------
  local appearance = Appearance.Sanitise({
    model = 'f',
    parents = { father = 12, mother = 33, shapeMix = 0.62, skinMix = 0.25 },
    hair = { style = 14, colour = 9, highlight = 2 },
    features = { ['0'] = 0.4, ['13'] = -0.75 },
    overlays = { eyebrows = { index = 5, opacity = 0.8, colour = 3 } },
    components = { jacket = { drawable = 42, texture = 3 } },
  })

  local charId = DB.Scalar('SELECT id FROM characters WHERE account_id = ? AND deleted = 0 LIMIT 1', { accountId })

  if not charId then
    charId = DB.Insert([[
      INSERT INTO characters (account_id, slot, first_name, last_name, dob, gender, backstory, appearance)
      VALUES (?, 0, ?, ?, ?, 'f', ?, ?)
    ]], { accountId, 'Zztest', 'Persistence', '1991-04-18',
          'Created by the self test.', json.encode(appearance) })
    check('character created', type(charId) == 'number' and charId > 0)
  else
    check('character survived restart', true)
  end

  local row = DB.Single('SELECT * FROM characters WHERE id = ?', { charId })
  check('character reads back', row ~= nil)

  if row then
    local back = json.decode(row.appearance)
    check('appearance JSON round trip', json.encode(Appearance.Sanitise(back)) == json.encode(appearance),
          'appearance differs after a round trip')
    check('float precision kept', math.abs(back.parents.shapeMix - 0.62) < 0.0001,
          tostring(back.parents.shapeMix))
    check('negative feature kept', math.abs(back.features['13'] + 0.75) < 0.0001,
          tostring(back.features['13']))
    check('starting balances applied', tonumber(row.cash) == Config.Money.startingCash
          and tonumber(row.bank) == Config.Money.startingBank,
          ('cash=%s bank=%s'):format(tostring(row.cash), tostring(row.bank)))
  end

  -- 4. the constraints the session code relies on ---------------------------
  local dupe, dupeErr = DB.Insert(
    'INSERT INTO accounts (username, password_hash) VALUES (?, ?)', { MARKER, h })
  check('duplicate username refused by the database', dupe == nil, tostring(dupe))

  local dupeName = DB.Insert([[
    INSERT INTO characters (account_id, slot, first_name, last_name, dob, gender)
    VALUES (?, 1, 'Zztest', 'Persistence', '1991-04-18', 'f')
  ]], { accountId })
  check('duplicate character name refused by the database', dupeName == nil)

  local orphan = DB.Insert([[
    INSERT INTO characters (account_id, slot, first_name, last_name, dob, gender)
    VALUES (999999, 0, 'Zzorphan', 'Nobody', '1991-04-18', 'm')
  ]], {})
  check('character cannot reference a missing account', orphan == nil)

  print(('^5[selftest]^7 ==== %d passed, %d failed ===='):format(passed, failed))
  if failed == 0 then
    print(firstRun and '^2[selftest] data path OK (first run)^7' or '^2[selftest] data path OK and persistent^7')
  else
    print('^1[selftest] PROBLEMS FOUND^7')
  end
end)

RegisterCommand('selftest_reset', function()
  CreateThread(function()
    DB.Update('DELETE FROM accounts WHERE username = ?', { MARKER })   -- characters cascade
    print('^3[selftest]^7 marker account removed')
  end)
end, true)
