const { test, beforeEach, afterEach } = require('node:test')
const assert = require('node:assert/strict')

// Exercise invite create/lookup/single-use accept against an in-memory fake by
// monkeypatching the shared db module the model require()s. No DB.
const db = require('../src/model/invites/invites.db')
const invites = require('../src/model/invites/invites.model')

let rows
let nextId
const saved = {}

beforeEach(() => {
  rows = []
  nextId = 1
  for (const k of ['insert', 'getById', 'findByTokenHash', 'markAccepted', 'revoke']) saved[k] = db[k]
  db.insert = async ({ tokenHash, email, role, invitedBy, expiresAt }) => {
    const id = nextId++
    rows.push({ id, token_hash: tokenHash, email, role, status: 'pending', invited_by: invitedBy ?? null, accepted_user_id: null, expires_at: expiresAt, created_at: new Date(), accepted_at: null })
    return id
  }
  db.getById = async (id) => rows.find((r) => r.id === id) || null
  db.findByTokenHash = async (h) => rows.find((r) => r.token_hash === h) || null
  db.markAccepted = async (id, userId) => {
    const row = rows.find((r) => r.id === id && r.status === 'pending')
    if (!row) return 0
    row.status = 'accepted'
    row.accepted_user_id = userId
    return 1
  }
  db.revoke = async (id) => {
    const row = rows.find((r) => r.id === id && r.status === 'pending')
    if (!row) return 0
    row.status = 'revoked'
    return 1
  }
})

afterEach(() => {
  for (const k of Object.keys(saved)) db[k] = saved[k]
})

test('create stores only the token hash, never the plaintext token', async () => {
  const { invite, token } = await invites.create({ email: 'a@b.com', role: 'player', invitedBy: 1 })
  assert.ok(token && token.length >= 20)
  assert.equal(rows[0].token_hash, invites.hashToken(token))
  assert.notEqual(rows[0].token_hash, token) // hash, not the raw token
  assert.equal(invite.email, 'a@b.com')
  assert.equal(invite.role, 'player')
  assert.equal(invite.status, 'pending')
})

test('findValidByToken resolves a pending token and rejects a wrong/used one', async () => {
  const { token } = await invites.create({ email: 'a@b.com', role: 'moderator', invitedBy: 1 })
  assert.ok(await invites.findValidByToken(token))
  assert.equal(await invites.findValidByToken('not-a-real-token'), null)
})

test('accept is single-use — the second accept loses the race', async () => {
  const { token } = await invites.create({ email: 'a@b.com', role: 'player', invitedBy: 1 })
  const row = await invites.findValidByToken(token)
  assert.equal(await invites.accept(row.id, 55), true)
  assert.equal(await invites.accept(row.id, 66), false) // already consumed
  assert.equal(await invites.findValidByToken(token), null) // no longer pending
})

test('an expired invite is not valid (exercises the expiry branch, not a bad token)', async () => {
  const { token } = await invites.create({ email: 'a@b.com', role: 'player', invitedBy: 1, ttlDays: -1 })
  // The token itself is correct and the row is pending — only expires_at rejects it.
  assert.ok(rows[0] && rows[0].status === 'pending')
  assert.equal(await invites.findValidByToken(token), null)
})

test('revoke makes a pending invite unusable', async () => {
  const { invite, token } = await invites.create({ email: 'a@b.com', role: 'player', invitedBy: 1 })
  assert.equal(await invites.revoke(invite.id), 1)
  assert.equal(await invites.findValidByToken(token), null)
})
