// Point the DB at a closed port BEFORE requiring the controller (its models build
// the pool). Every model/mailer call is monkeypatched, so no query runs;
// db.close() at the end releases the pool so the process exits cleanly.
process.env.DB_HOST = '127.0.0.1'
process.env.DB_PORT = '59999'

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

// Unit-test the self-service password reset controller. The security invariants:
//   - requestReset NEVER reveals whether an email exists — empty, unmatched,
//     matched, and even an internal error all return the same generic 200;
//   - one account's mail failure does not abort the others or change the answer;
//   - confirmReset consumes the token atomically (a lost double-submit race is a
//     404) and, on success, rotates the password AND revokes mobile sessions,
//     without auto-logging the user in.
const ctrl = require('../src/router/v1/auth/passwordReset.controller')
const passwordResets = require('../src/model/passwordResets/passwordResets.model')
const users = require('../src/model/users/users.model')
const mobileSessions = require('../src/model/mobileSessions/mobileSessions.model')
const trustedDevices = require('../src/model/trustedDevices/trustedDevices.model')
const recoveryCodes = require('../src/model/recoveryCodes/recoveryCodes.model')
const activity = require('../src/model/activity/activity.model')
const mailer = require('../src/utils/mailer')
const db = require('../src/utils/db')

after(() => db.close())

function mockRes() {
  return {
    statusCode: 200,
    body: null,
    status(c) {
      this.statusCode = c
      return this
    },
    json(b) {
      this.body = b
      return this
    },
  }
}

const GENERIC_MATCH = /if an account exists/i

const orig = {}
beforeEach(() => {
  for (const [mod, name] of [
    [users, 'getActiveByEmail'], [users, 'getById'], [users, 'update'],
    [passwordResets, 'create'], [passwordResets, 'findValidByToken'], [passwordResets, 'consume'], [passwordResets, 'invalidatePendingForUser'],
    [mobileSessions, 'revokeAllForUser'], [trustedDevices, 'revokeAllForUser'], [recoveryCodes, 'clearForUser'],
    [activity, 'log'], [mailer, 'sendPasswordReset'],
  ]) {
    orig[name] = { mod, val: mod[name] }
  }
  activity.log = async () => {}
  mailer.sendPasswordReset = async () => ({ sent: true })
  passwordResets.create = async () => ({ token: 'opaque-token' })
  passwordResets.invalidatePendingForUser = async () => {}
  mobileSessions.revokeAllForUser = async () => {}
  trustedDevices.revokeAllForUser = async () => {}
  recoveryCodes.clearForUser = async () => {}
})
afterEach(() => {
  for (const key of Object.keys(orig)) {
    orig[key].mod[key] = orig[key].val
    delete orig[key]
  }
})

const req = (body = {}) => ({ body, ip: '10.0.0.1' })

// ── requestReset never enumerates ───────────────────────────────────────
test('requestReset returns the generic OK for an empty email without any lookup', async () => {
  let lookedUp = false
  users.getActiveByEmail = async () => {
    lookedUp = true
    return []
  }
  const res = mockRes()
  await ctrl.requestReset(req({ email: '   ' }), res)
  assert.equal(res.statusCode, 200)
  assert.match(res.body.message, GENERIC_MATCH)
  assert.equal(lookedUp, false, 'a blank email is short-circuited before the DB')
})

test('requestReset returns the SAME generic OK whether or not the email matched', async () => {
  users.getActiveByEmail = async () => [] // no account
  const resNone = mockRes()
  await ctrl.requestReset(req({ email: 'ghost@x.io' }), resNone)

  users.getActiveByEmail = async () => [{ id: 1, email: 'real@x.io', username: 'real' }]
  const resHit = mockRes()
  await ctrl.requestReset(req({ email: 'real@x.io' }), resHit)

  assert.deepEqual(resNone.body, resHit.body) // indistinguishable
  assert.equal(resHit.statusCode, 200)
})

test('requestReset emails every account matching a (non-unique) address', async () => {
  users.getActiveByEmail = async () => [
    { id: 1, email: 'shared@x.io', username: 'alpha' },
    { id: 2, email: 'shared@x.io', username: 'beta' },
  ]
  const sent = []
  mailer.sendPasswordReset = async ({ username }) => {
    sent.push(username)
    return { sent: true }
  }
  await ctrl.requestReset(req({ email: 'shared@x.io' }), mockRes())
  assert.deepEqual(sent.sort(), ['alpha', 'beta'])
})

test("requestReset: one account's mail failure does not abort the others or change the response", async () => {
  users.getActiveByEmail = async () => [
    { id: 1, email: 'a@x.io', username: 'alpha' },
    { id: 2, email: 'b@x.io', username: 'beta' },
  ]
  const sent = []
  mailer.sendPasswordReset = async ({ username }) => {
    if (username === 'alpha') throw new Error('smtp reject')
    sent.push(username)
    return { sent: true }
  }
  const res = mockRes()
  await ctrl.requestReset(req({ email: 'a@x.io' }), res)
  assert.deepEqual(sent, ['beta'], 'beta still got its link after alpha failed')
  assert.match(res.body.message, GENERIC_MATCH)
})

test('requestReset stays generic even when the account lookup itself throws', async () => {
  users.getActiveByEmail = async () => {
    throw new Error('pool down')
  }
  const res = mockRes()
  await ctrl.requestReset(req({ email: 'x@x.io' }), res)
  assert.equal(res.statusCode, 200) // internal error is not an enumeration oracle
  assert.match(res.body.message, GENERIC_MATCH)
})

// ── lookupReset ─────────────────────────────────────────────────────────
test('lookupReset 404s an invalid/expired token and returns only the username on success', async () => {
  passwordResets.findValidByToken = async () => null
  const res404 = mockRes()
  await ctrl.lookupReset({ params: { token: 'bad' } }, res404)
  assert.equal(res404.statusCode, 404)

  passwordResets.findValidByToken = async () => ({ user_id: 7 })
  users.getById = async () => ({ id: 7, username: 'target', email: 'secret@x.io' })
  const resOk = mockRes()
  await ctrl.lookupReset({ params: { token: 'good' } }, resOk)
  assert.deepEqual(resOk.body, { username: 'target' }) // email/token never surfaced
})

// ── confirmReset consume race + revoke-everywhere ───────────────────────
test('confirmReset 404s when the token is not valid', async () => {
  passwordResets.findValidByToken = async () => null
  const res = mockRes()
  await ctrl.confirmReset({ params: { token: 'bad' }, body: { password: 'new' } }, res)
  assert.equal(res.statusCode, 404)
})

test('confirmReset 404s the loser of a double-submit race and never rotates the password', async () => {
  passwordResets.findValidByToken = async () => ({ id: 3, user_id: 7 })
  passwordResets.consume = async () => false // lost the race
  let rotated = false
  users.update = async () => {
    rotated = true
  }
  const res = mockRes()
  await ctrl.confirmReset({ params: { token: 't' }, body: { password: 'new' } }, res)
  assert.equal(res.statusCode, 404)
  assert.equal(rotated, false)
})

test('confirmReset rotates the password, revokes mobile sessions, and retires other links — no auto-login', async () => {
  passwordResets.findValidByToken = async () => ({ id: 3, user_id: 7 })
  passwordResets.consume = async () => true
  const calls = { update: null, revoke: null, invalidate: null }
  users.update = async (id, patch) => {
    calls.update = { id, patch }
  }
  mobileSessions.revokeAllForUser = async (id) => {
    calls.revoke = id
  }
  passwordResets.invalidatePendingForUser = async (id) => {
    calls.invalidate = id
  }
  const res = mockRes()
  await ctrl.confirmReset({ params: { token: 't' }, body: { password: 'brand-new' } }, res)
  assert.deepEqual(calls.update, { id: 7, patch: { password: 'brand-new' } })
  assert.equal(calls.revoke, 7)
  assert.equal(calls.invalidate, 7)
  assert.equal(res.body.ok, true)
  assert.equal(res.body.user, undefined, 'no session/user is returned — the user signs in fresh')
})
