#!/usr/bin/env node
/**
 * Verification for the canonical rate limiter at src/lib/security/rateLimit.ts.
 *
 * Runs under plain Node (>= 22.6, type-stripping) — no tsx, no build step:
 *   node scripts/verify_rate_limit.mjs
 *
 * Covers the three behaviours the security gate cares about:
 *   1. A burst over the limit is denied with a positive retryAfter (the routes
 *      map that to HTTP 429 + Retry-After — see the audit doc).
 *   2. Once the window expires, the same key is allowed again.
 *   3. Different keys are independent buckets.
 * Plus the clientIp header policy (last valid XFF entry wins; spoofed first
 * entries ignored; x-real-ip fallback; unknown fallback).
 */

import { rateLimit, clientIp, resetRateLimits } from "../src/lib/security/rateLimit.ts";

let failures = 0;
let checks = 0;

function check(name, condition, detail = "") {
  checks += 1;
  if (condition) {
    console.log(`  PASS  ${name}`);
  } else {
    failures += 1;
    console.error(`  FAIL  ${name}${detail ? ` — ${detail}` : ""}`);
  }
}

const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));

// ---------------------------------------------------------------------------
// 1. Burst over the limit
// ---------------------------------------------------------------------------
console.log("\n[1] Burst over the limit → denied with retryAfter > 0");
{
  resetRateLimits();
  const LIMIT = 3;
  const WINDOW_MS = 60_000;
  const key = "login:ip:203.0.113.9";

  const results = [];
  for (let i = 0; i < LIMIT + 2; i++) results.push(rateLimit(key, LIMIT, WINDOW_MS));

  const allowedCount = results.filter((r) => r.allowed).length;
  const blocked = results[LIMIT]; // first denial
  check(
    "exactly `limit` requests allowed before denial",
    allowedCount === LIMIT,
    `allowed ${allowedCount}, expected ${LIMIT}`
  );
  check(
    "first over-limit request is denied",
    blocked && blocked.allowed === false,
    JSON.stringify(blocked)
  );
  check(
    "denied request reports a positive retryAfter (→ Retry-After header)",
    blocked && blocked.retryAfter > 0,
    `retryAfter=${blocked?.retryAfter}`
  );
  check(
    "every further request stays denied inside the window",
    results.slice(LIMIT).every((r) => !r.allowed)
  );
  check(
    "allowed requests report retryAfter 0",
    results.slice(0, LIMIT).every((r) => r.retryAfter === 0)
  );
}

// ---------------------------------------------------------------------------
// 2. Window expiry → allowed again
// ---------------------------------------------------------------------------
console.log("\n[2] Window expiry resets the bucket");
{
  resetRateLimits();
  const key = "signup:ip:198.51.100.4";
  const WINDOW_MS = 120; // short window so the test does not stall
  for (let i = 0; i < 2; i++) rateLimit(key, 1, WINDOW_MS);
  const blockedNow = rateLimit(key, 1, WINDOW_MS);
  check("denied while inside the window", blockedNow.allowed === false);

  await sleep(WINDOW_MS + 30);
  const allowedAfter = rateLimit(key, 1, WINDOW_MS);
  check("allowed again after the window rolls over", allowedAfter.allowed === true);
}

// ---------------------------------------------------------------------------
// 3. Different keys do not interfere
// ---------------------------------------------------------------------------
console.log("\n[3] Keys are isolated buckets");
{
  resetRateLimits();
  const keyA = "reset:ip:192.0.2.1";
  const keyB = "reset:ip:192.0.2.2";
  rateLimit(keyA, 1, 60_000);
  rateLimit(keyA, 1, 60_000); // exhaust A
  check("key A is denied after its own limit", rateLimit(keyA, 1, 60_000).allowed === false);
  check("key B is still allowed", rateLimit(keyB, 1, 60_000).allowed === true);
  check(
    "route prefix keeps routes apart",
    rateLimit("login:ip:192.0.2.1", 1, 60_000).allowed === true
  );
}

// ---------------------------------------------------------------------------
// 4. clientIp header policy
// ---------------------------------------------------------------------------
console.log("\n[4] clientIp trusts the LAST valid XFF entry, never the first");
{
  const req = (headers) => new Request("http://localhost", { headers });

  check(
    "takes last valid IP when the client spoofs leading entries",
    clientIp(req({ "x-forwarded-for": "203.0.113.7, 10.0.0.1, 198.51.100.9" })) ===
      "198.51.100.9",
    clientIp(req({ "x-forwarded-for": "203.0.113.7, 10.0.0.1, 198.51.100.9" }))
  );
  check(
    "ignores a spoofed-only chain (no valid IP) and falls back to x-real-ip",
    clientIp(req({ "x-forwarded-for": "evil, not-an-ip", "x-real-ip": "10.0.0.5" })) ===
      "10.0.0.5"
  );
  check(
    "falls back to x-real-ip when XFF is absent",
    clientIp(req({ "x-real-ip": "10.0.0.6" })) === "10.0.0.6"
  );
  check(
    "never returns a spoofed string — 'unknown' when nothing validates",
    clientIp(req({ "x-forwarded-for": "spoofed-value" })) === "unknown"
  );
  check("no headers at all → 'unknown'", clientIp(req({})) === "unknown");
}

// ---------------------------------------------------------------------------
console.log(`\n${checks} checks, ${failures} failure(s)`);
if (failures > 0) {
  console.error("RATE-LIMIT VERIFICATION FAILED");
  process.exit(1);
}
console.log("RATE-LIMIT VERIFICATION PASSED");
