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

const lp = require('../src/middleware/loginProtection')
const { loginLimiter } = require('../src/middleware/rateLimit')
const { startApp } = require('./_helper')

beforeEach(() => lp._reset())

test('recordFailure escalates the lockout exponentially', () => {
  const ip = '198.51.100.50'
  const now = 1_000_000
  const first = lp.recordFailure(ip, now)
  const second = lp.recordFailure(ip, now)
  const third = lp.recordFailure(ip, now)
  assert.equal(first, lp.BASE_MS) // 2^0
  assert.equal(second, lp.BASE_MS * 2) // 2^1
  assert.equal(third, lp.BASE_MS * 4) // 2^2
})

test('lockout is capped at MAX_MS', () => {
  const ip = '198.51.100.51'
  const now = 1_000_000
  let last = 0
  for (let i = 0; i < 40; i++) last = lp.recordFailure(ip, now)
  assert.equal(last, lp.MAX_MS)
})

test('retryAfterMs reflects the active lockout and clears after it elapses', () => {
  const ip = '198.51.100.52'
  const now = 2_000_000
  lp.recordFailure(ip, now) // locks BASE_MS
  assert.ok(lp.retryAfterMs(ip, now) > 0)
  assert.equal(lp.retryAfterMs(ip, now + lp.BASE_MS + 1), 0)
})

test('recordSuccess clears the failure streak', () => {
  const ip = '198.51.100.53'
  const now = 2_000_000
  lp.recordFailure(ip, now)
  lp.recordSuccess(ip)
  assert.equal(lp.retryAfterMs(ip, now), 0)
})

test('streak resets after a long quiet period (does not grow forever)', () => {
  const ip = '198.51.100.54'
  const t0 = 3_000_000
  lp.recordFailure(ip, t0)
  lp.recordFailure(ip, t0)
  // Come back after RESET_MS+ of quiet: next failure starts the streak over.
  const later = t0 + lp.RESET_MS + 1
  const delay = lp.recordFailure(ip, later)
  assert.equal(delay, lp.BASE_MS) // back to 2^0
})

test('backoffGuard returns a generic 429 while locked out', async () => {
  // Pre-lock this IP, then confirm the guard blocks it with a generic message.
  const app = await startApp((a) => {
    a.set('trust proxy', 1)
    a.post('/login', lp.backoffGuard, (req, res) => res.json({ ok: true }))
  })
  try {
    // Lock the test client IP. FIVE failures, not one: the lock is
    // `BASE_MS * 2 ** (count - 1)`, so a single failure locks for exactly one
    // second and this test then races the round trip. It lost that race on CI
    // (200 instead of 429, request arriving 1,456 ms after the lock). Five
    // failures lock for sixteen seconds, which is not a race. What is under
    // test is the guard's ANSWER while locked out, and that is unchanged.
    for (let i = 0; i < 5; i += 1) lp.recordFailure('203.0.113.40')
    const res = await fetch(`${app.url}/login`, {
      method: 'POST',
      headers: { 'X-Forwarded-For': '203.0.113.40' },
    })
    assert.equal(res.status, 429)
    const body = await res.json()
    assert.match(body.message, /too many login attempts/i)
    // Message must not reveal whether username or password was the problem.
    assert.doesNotMatch(body.message, /password|username/i)
    assert.ok(res.headers.get('retry-after'))
  } finally {
    await app.close()
  }
})

test('hard rate limiter caps attempts per IP (429 after the cap)', async () => {
  const app = await startApp((a) => {
    a.set('trust proxy', 1)
    a.post('/login', loginLimiter, (req, res) => res.json({ ok: true }))
  })
  try {
    const headers = { 'X-Forwarded-For': '203.0.113.41' }
    let sawLimit = false
    // Cap is 10/15min; the 11th should be blocked.
    for (let i = 0; i < 12; i++) {
      const res = await fetch(`${app.url}/login`, { method: 'POST', headers })
      if (res.status === 429) sawLimit = true
    }
    assert.ok(sawLimit, 'expected the hard limiter to return 429 after the cap')
  } finally {
    await app.close()
  }
})
