/* test/authThrottle.test.js
 *
 * Validates that the IP-throttle path on /auth/login returns a proper
 * `message` and `retry_after` so the login page UI can render a
 * friendly message + live countdown instead of the raw `IP_COOLDOWN`
 * error code.
 */
'use strict';

const assert = require('assert');
const http = require('http');

const TESTS = [];
const test = (n, fn) => TESTS.push({ name: n, fn });

function req(opts, body) {
  return new Promise((resolve, reject) => {
    const r = http.request({hostname:'127.0.0.1', port:3000, path:opts.path, method:opts.method||'POST', headers:{'content-type':'application/json', ...(opts.headers||{})}}, (res) => {
      let d=''; res.on('data', c=>d+=c); res.on('end', () => {
        let parsed = d; try { parsed = JSON.parse(d); } catch (_) {}
        resolve({ status: res.statusCode, body: parsed, retryAfter: res.headers['retry-after'] });
      });
    });
    r.on('error', reject);
    if (body) r.write(JSON.stringify(body));
    r.end();
  });
}

test('1st failed login returns INVALID_CREDENTIALS', async () => {
  // Use a unique X-Forwarded-For so we get a fresh IP for this test
  // (the server uses trust proxy=1). This is independent of other
  // tests' state and of any in-flight cooldown.
  const ip = '10.99.' + Math.floor(Math.random() * 255) + '.' + Math.floor(Math.random() * 255);
  const r = await req({ path: '/auth/login', headers: { 'x-forwarded-for': ip } }, { email: 'throttle-test-1@nowhere.test', password: 'wrong' });
  assert.strictEqual(r.status, 401);
  assert.strictEqual(r.body.error, 'INVALID_CREDENTIALS');
});

test('2nd failed login from same IP within 60s returns IP_COOLDOWN with human message', async () => {
  const ip = '10.99.' + Math.floor(Math.random() * 255) + '.' + Math.floor(Math.random() * 255);
  await req({ path: '/auth/login', headers: { 'x-forwarded-for': ip } }, { email: 'throttle-test-2@nowhere.test', password: 'wrong' });
  const r = await req({ path: '/auth/login', headers: { 'x-forwarded-for': ip } }, { email: 'throttle-test-2@nowhere.test', password: 'wrong' });
  assert.strictEqual(r.status, 429);
  assert.strictEqual(r.body.error, 'IP_COOLDOWN');
  assert.ok(typeof r.body.retry_after === 'number' && r.body.retry_after > 0 && r.body.retry_after <= 60, 'retry_after must be a positive number <= 60');
  assert.ok(typeof r.body.message === 'string' && r.body.message.length > 10, 'message must be a human string');
  assert.ok(r.body.message.toLowerCase().includes('try again') || r.body.message.toLowerCase().includes('wait'),
    'message must contain "try again" or "wait" — not a raw code');
  assert.ok(r.retryAfter && parseInt(r.retryAfter, 10) > 0, 'Retry-After header must be set');
});

test('IP_COOLDOWN message must NOT contain the raw error code', async () => {
  const ip = '10.99.' + Math.floor(Math.random() * 255) + '.' + Math.floor(Math.random() * 255);
  await req({ path: '/auth/login', headers: { 'x-forwarded-for': ip } }, { email: 'throttle-test-3@nowhere.test', password: 'wrong' });
  const r = await req({ path: '/auth/login', headers: { 'x-forwarded-for': ip } }, { email: 'throttle-test-3@nowhere.test', password: 'wrong' });
  assert.strictEqual(r.status, 429);
  // The message field is for humans; the code lives in `error`.
  // If the message ever contains "IP_COOLDOWN" verbatim, the login
  // page would show it as raw text (the original bug).
  assert.ok(!/IP_COOLDOWN/.test(r.body.message || ''), 'message must not leak the raw code');
});

module.exports = TESTS;
module.exports.requiresServer = true;
