/**
 * AS/PLATFORM/2026/003 T002 — XFF login-limiter hardening (paired acceptance).
 *
 * Proves the configurable trust-proxy knob (TRUST_PROXY_HOPS) makes the per-IP
 * login limiter spoof-proof on direct-exposure deployments WITHOUT breaking the
 * legitimate proxied path (Rule 7 dual-env) or the dev proof-harness.
 *
 * Method: mount the REAL `loginRateLimiter` middleware (server/lib/rate-limiter.ts)
 * on throwaway in-process Express apps, one per trust-proxy setting, and drive
 * them with controlled X-Forwarded-For headers. This exercises the actual
 * middleware + the actual Express/proxy-addr trust-proxy semantics in isolation
 * from the running dev server (whose own behaviour at the default hops=1 is
 * re-confirmed separately by re-running an existing harness battery).
 *
 * Legs (Rule 18 paired):
 *   • XFF-SPOOF-DEAD  (hops=0) — rotating X-Forwarded-For does NOT mint new
 *     buckets: req.ip stays the socket peer and the limiter trips on the real IP.
 *     (negative: spoof attempt fails; positive: enforcement fires — both in one leg.)
 *   • XFF-PROXIED-INTACT (hops=1) — five DISTINCT proxied clients each resolve to
 *     their true X-Forwarded-For IP and all pass: no false lockout.
 *   • XFF-LIMITER-ENFORCES (hops=1, positive pair for the leg above) — five hits
 *     from ONE proxied client pass and the sixth trips 429: the limiter is not
 *     globally disabled under hops=1, so the "no false lockout" above holds
 *     BECAUSE the IPs differ, not because the limiter is off.
 *   • HOPS-MAPPING — resolveTrustProxyHops maps env strings correctly (default 0
 *     fail-closed, valid integers 0..10 honoured, out-of-range / garbage → 0).
 *
 * Self-contained: best-effort cleanup of any rate_limit_events rows the 429 paths
 * persisted for the synthetic test IPs, scoped to this run's start (Rule 11).
 * Exit 0 iff ALL legs PASS.
 */

import express from "express";
import http from "http";
import { AddressInfo } from "net";
import {
  loginRateLimiter,
  resolveTrustProxyHops,
} from "../server/lib/rate-limiter";
import { ProofRun, verdict, ownerPool, endOwnerPool } from "./lib/proof-harness";

const TEST_START = new Date();

interface Hit {
  status: number;
  ip: string | null;
}

function makeApp(trustProxy: number | false): express.Express {
  const app = express();
  app.set("trust proxy", trustProxy);
  // The real login limiter, then a trivial handler that echoes the resolved IP.
  app.post("/api/auth/login", loginRateLimiter, (req, res) => {
    res.status(200).json({ ip: req.ip });
  });
  return app;
}

function listen(app: express.Express): Promise<{ server: http.Server; port: number }> {
  return new Promise((resolve) => {
    const server = http.createServer(app);
    server.listen(0, "127.0.0.1", () => {
      resolve({ server, port: (server.address() as AddressInfo).port });
    });
  });
}

async function hit(port: number, xff: string): Promise<Hit> {
  const res = await fetch(`http://127.0.0.1:${port}/api/auth/login`, {
    method: "POST",
    headers: { "content-type": "application/json", "x-forwarded-for": xff },
    body: "{}",
  });
  let ip: string | null = null;
  try {
    const body = (await res.json()) as { ip?: string };
    ip = body?.ip ?? null;
  } catch {
    ip = null;
  }
  return { status: res.status, ip };
}

async function main(): Promise<void> {
  const run = new ProofRun(
    "AS/PLATFORM/2026/003 T002 — XFF login-limiter hardening",
    "DEV",
  );

  const app0 = await listen(makeApp(false)); // hops=0 (trust proxy OFF)
  const app1 = await listen(makeApp(1)); //     hops=1 (Replit topology)

  try {
    // ── LEG 1 (hops=0 / SPOOF-DEAD) — rotating XFF, same socket ────────────────
    const rotating = ["9.9.9.1", "9.9.9.2", "9.9.9.3", "9.9.9.4", "9.9.9.5", "9.9.9.6", "9.9.9.7"];
    const spoof: Hit[] = [];
    for (const xff of rotating) spoof.push(await hit(app0.port, xff));
    const okIps = spoof.filter((h) => h.status === 200 && h.ip).map((h) => h.ip!);
    const allSameIp = okIps.length > 0 && okIps.every((ip) => ip === okIps[0]);
    const ipNotSpoofed = okIps.every((ip) => !rotating.includes(ip!) && !ip!.includes("9.9.9."));
    const tripped = spoof.some((h) => h.status === 429);
    const noPreBlock = !spoof.some((h) => h.status === 403);
    const spoofDead = allSameIp && ipNotSpoofed && tripped && noPreBlock;
    run.leg({
      id: "XFF-SPOOF-DEAD",
      scenario:
        "hops=0 (trust proxy OFF): 7 logins with a DIFFERENT X-Forwarded-For each time, all from one socket peer",
      expectation:
        "X-Forwarded-For ignored → req.ip stays the socket peer for every request → limiter trips (429) on the real IP despite the rotation",
      observed: `resolved req.ip(s)=${JSON.stringify([...new Set(okIps)])} statuses=${JSON.stringify(spoof.map((h) => h.status))}`,
      verdict: spoofDead
        ? verdict.verified(
            "rotating X-Forwarded-For did NOT mint new buckets (single resolved IP, none matching the spoofed values) and the limiter tripped",
          )
        : verdict.fail(),
      pass: spoofDead,
      signals: { allSameIp, ipNotSpoofed, tripped, noPreBlock, resolved: [...new Set(okIps)] },
    });

    // ── LEG 2 (hops=1 / PROXIED-INTACT) — 5 distinct proxied clients ───────────
    const clients = ["198.51.100.1", "198.51.100.2", "198.51.100.3", "198.51.100.4", "198.51.100.5"];
    const proxied: Hit[] = [];
    for (const xff of clients) proxied.push(await hit(app1.port, xff));
    const allOk = proxied.every((h) => h.status === 200);
    const resolvedTrue = proxied.every((h, i) => h.ip === clients[i]);
    const intact = allOk && resolvedTrue;
    run.leg({
      id: "XFF-PROXIED-INTACT",
      scenario:
        "hops=1 (Replit topology): 5 DISTINCT proxied clients, each with its own X-Forwarded-For through the single trusted hop",
      expectation:
        "each request resolves req.ip to its true client IP and all 5 pass (200) — no false lockout of legitimate distinct clients",
      observed: `statuses=${JSON.stringify(proxied.map((h) => h.status))} resolvedIps=${JSON.stringify(proxied.map((h) => h.ip))}`,
      verdict: intact
        ? verdict.verified("each distinct X-Forwarded-For resolved to its true client IP; all passed (distinct buckets)")
        : verdict.fail(),
      pass: intact,
      signals: { allOk, resolvedTrue },
    });

    // ── LEG 3 (hops=1 / LIMITER-ENFORCES) — positive pair: one client, 6 hits ──
    const sameClient = "203.0.113.50";
    const burst: Hit[] = [];
    for (let i = 0; i < 6; i++) burst.push(await hit(app1.port, sameClient));
    const firstFiveOk = burst.slice(0, 5).every((h) => h.status === 200);
    const sixthTrips = burst[5]?.status === 429;
    const enforces = firstFiveOk && sixthTrips;
    run.leg({
      id: "XFF-LIMITER-ENFORCES",
      scenario:
        "hops=1 positive pair: 6 logins from ONE proxied client (same X-Forwarded-For)",
      expectation:
        "first 5 pass, the 6th trips 429 — the limiter is genuinely enforcing per-IP under hops=1 (so PROXIED-INTACT is not hollow)",
      observed: `statuses=${JSON.stringify(burst.map((h) => h.status))}`,
      verdict: enforces
        ? verdict.verified("same-IP burst tripped on the 6th — limiter active under hops=1, not globally disabled")
        : verdict.fail(),
      pass: enforces,
      signals: { firstFiveOk, sixthTrips },
    });

    // ── LEG 4 (HOPS-MAPPING) — pure resolver ───────────────────────────────────
    const cases: Array<[string | undefined, number]> = [
      [undefined, 0],
      ["", 0],
      ["   ", 0],
      ["0", 0],
      ["1", 1],
      ["2", 2],
      ["10", 10],
      ["11", 0],
      ["-1", 0],
      ["abc", 0],
      ["1.5", 0],
    ];
    const results = cases.map(([raw, want]) => ({ raw, want, got: resolveTrustProxyHops(raw) }));
    const mappingOk = results.every((r) => r.got === r.want);
    run.leg({
      id: "HOPS-MAPPING",
      scenario: "resolveTrustProxyHops maps TRUST_PROXY_HOPS env strings",
      expectation:
        "default/blank → 0 (fail-closed); valid integers 0..10 honoured; out-of-range / non-integer / garbage → 0",
      observed: results.map((r) => `${JSON.stringify(r.raw)}→${r.got}(want ${r.want})`).join("  "),
      verdict: mappingOk
        ? verdict.verified("every env string mapped to the expected hop count, invalid → safe default")
        : verdict.fail(),
      pass: mappingOk,
      signals: { results },
    });
  } finally {
    app0.server.close();
    app1.server.close();
    // Best-effort cleanup of any rate_limit_events the 429 paths persisted for the
    // synthetic test IPs (fire-and-forget inserts; give them a moment to land).
    await new Promise((r) => setTimeout(r, 600));
    let leftover = -1;
    try {
      const o = ownerPool();
      await o.query(
        `DELETE FROM rate_limit_events WHERE window_type='login' AND ip_address IN ('127.0.0.1','203.0.113.50') AND created_at >= $1`,
        [TEST_START.toISOString()],
      );
      const res = await o.query(
        `SELECT count(*)::int AS n FROM rate_limit_events WHERE window_type='login' AND ip_address IN ('127.0.0.1','203.0.113.50') AND created_at >= $1`,
        [TEST_START.toISOString()],
      );
      leftover = res.rows[0]?.n ?? -1;
    } catch {
      /* persistence table may be absent; harmless */
    }
    console.log(`[cleanup] leftover synthetic login rate_limit_events for test IPs since run start: ${leftover}`);
  }

  run.report();
  const { allPass } = run.acceptance();
  await endOwnerPool();
  process.exit(allPass ? 0 : 1);
}

main().catch((e) => {
  console.error("FATAL", e);
  process.exit(1);
});
