/**
 * Lockout Security Suite
 *
 * Pure-logic tests for the progressive-delay lockout tracker
 * (`src/lib/auth/lockout.ts`): escalating tiers, reset on success,
 * sweep/eviction, the injectable clock, and the guarantee that no amount of
 * failures ever produces a permanent lockout. No database required.
 *
 * Self-executing: run with `node --import tsx tests/e2e/lockout_security.test.ts`.
 */

import { describe, test, expect, runAllTests } from "./runner";
import {
  createLockoutTracker,
  delaySecondsForFailures,
  MAX_LOCKOUT_SECONDS,
} from "../../src/lib/auth/lockout";

describe("Lockout — escalating tiers", () => {
  test("the first four failures never lock the key", () => {
    const clock = { value: 0 };
    const tracker = createLockoutTracker({ now: () => clock.value });
    for (let failures = 1; failures <= 4; failures += 1) {
      const state = tracker.recordFailure("email:a@b.de");
      expect(state.locked).toBe(false);
      expect(state.retryAfterSeconds).toBe(0);
      expect(state.failures).toBe(failures);
    }
  });

  test("the fifth failure escalates to a 30-second delay", () => {
    const clock = { value: 0 };
    const tracker = createLockoutTracker({ now: () => clock.value });
    for (let i = 0; i < 4; i += 1) tracker.recordFailure("email:a@b.de");
    const state = tracker.recordFailure("email:a@b.de");
    expect(state.locked).toBe(true);
    expect(state.retryAfterSeconds).toBe(30);
    expect(state.failures).toBe(5);
  });

  test("later tiers escalate to 2 minutes, 15 minutes and a 1-hour cap", () => {
    const clock = { value: 0 };
    const tracker = createLockoutTracker({ now: () => clock.value });
    const expected: Record<number, number> = { 8: 120, 11: 900, 15: 3600, 50: 3600 };
    for (let failures = 1; failures <= 50; failures += 1) {
      const state = tracker.recordFailure("ip:203.0.113.7");
      if (failures in expected) {
        expect(state.locked).toBe(true);
        expect(state.retryAfterSeconds).toBe(expected[failures]);
      }
    }
  });

  test("tier boundaries hold: 7 stays 30s, 10 stays 120s, 14 stays 900s", () => {
    const clock = { value: 0 };
    const tracker = createLockoutTracker({ now: () => clock.value });
    const boundaries: Record<number, number> = { 7: 30, 10: 120, 14: 900 };
    for (let failures = 1; failures <= 14; failures += 1) {
      const state = tracker.recordFailure("email:a@b.de");
      if (failures in boundaries) {
        expect(state.retryAfterSeconds).toBe(boundaries[failures]);
      }
    }
  });
});

describe("Lockout — reset on success", () => {
  test("a successful login clears the counter and any pending delay", () => {
    const clock = { value: 0 };
    const tracker = createLockoutTracker({ now: () => clock.value });
    for (let i = 0; i < 15; i += 1) tracker.recordFailure("email:a@b.de");
    expect(tracker.checkLockout("email:a@b.de").locked).toBe(true);

    tracker.recordSuccess("email:a@b.de");

    const after = tracker.checkLockout("email:a@b.de");
    expect(after.locked).toBe(false);
    expect(after.failures).toBe(0);
    // No escalation memory: the next failure starts over from one.
    expect(tracker.recordFailure("email:a@b.de").failures).toBe(1);
  });

  test("resetting one key leaves the other key untouched", () => {
    const clock = { value: 0 };
    const tracker = createLockoutTracker({ now: () => clock.value });
    for (let i = 0; i < 8; i += 1) {
      tracker.recordFailure("email:a@b.de");
      tracker.recordFailure("ip:203.0.113.7");
    }
    tracker.recordSuccess("email:a@b.de");
    expect(tracker.checkLockout("email:a@b.de").locked).toBe(false);
    expect(tracker.checkLockout("ip:203.0.113.7").locked).toBe(true);
  });
});

describe("Lockout — injectable clock and delay expiry", () => {
  test("the delay counts down and releases after the full window", () => {
    const clock = { value: 0 };
    const tracker = createLockoutTracker({ now: () => clock.value });
    for (let i = 0; i < 5; i += 1) tracker.recordFailure("email:a@b.de"); // locked for 30s

    clock.value += 29_000;
    const nearly = tracker.checkLockout("email:a@b.de");
    expect(nearly.locked).toBe(true);
    expect(nearly.retryAfterSeconds).toBe(1);

    clock.value += 1_000;
    expect(tracker.checkLockout("email:a@b.de").locked).toBe(false);
  });

  test("an unknown key checks as unlocked with zero failures", () => {
    const clock = { value: 0 };
    const tracker = createLockoutTracker({ now: () => clock.value });
    const state = tracker.checkLockout("email:never-touched@b.de");
    expect(state.locked).toBe(false);
    expect(state.retryAfterSeconds).toBe(0);
    expect(state.failures).toBe(0);
  });
});

describe("Lockout — per-key isolation", () => {
  test("email and ip counters escalate independently", () => {
    const clock = { value: 0 };
    const tracker = createLockoutTracker({ now: () => clock.value });
    for (let i = 0; i < 8; i += 1) tracker.recordFailure("email:a@b.de");
    for (let i = 0; i < 2; i += 1) tracker.recordFailure("ip:203.0.113.9");

    const email = tracker.checkLockout("email:a@b.de");
    expect(email.locked).toBe(true);
    expect(email.retryAfterSeconds).toBe(120);

    const ip = tracker.checkLockout("ip:203.0.113.9");
    expect(ip.locked).toBe(false);
    expect(ip.failures).toBe(2);

    // A different IP is completely unaffected.
    expect(tracker.checkLockout("ip:203.0.113.5").failures).toBe(0);
  });
});

describe("Lockout — no permanent lockout (DoS safety)", () => {
  test("even 100 failures cap at one hour, never more", () => {
    const clock = { value: 0 };
    const tracker = createLockoutTracker({ now: () => clock.value });
    let state = tracker.recordFailure("email:a@b.de");
    for (let i = 1; i < 100; i += 1) state = tracker.recordFailure("email:a@b.de");

    expect(state.failures).toBe(100);
    expect(state.locked).toBe(true);
    expect(state.retryAfterSeconds).toBe(3600);

    // The delay always releases: after an hour the account is usable again.
    clock.value += 3_600_000;
    const after = tracker.checkLockout("email:a@b.de");
    expect(after.locked).toBe(false);
    expect(after.failures).toBe(100);
  });

  test("the cap constant matches the final tier for any failure count", () => {
    expect(MAX_LOCKOUT_SECONDS).toBe(3600);
    expect(delaySecondsForFailures(15)).toBe(3600);
    expect(delaySecondsForFailures(1000)).toBe(3600);
    expect(delaySecondsForFailures(0)).toBe(0);
  });
});

describe("Lockout — sweep bounds the map", () => {
  test("stale unlocked entries are evicted once the map grows", () => {
    const clock = { value: 0 };
    const tracker = createLockoutTracker({
      now: () => clock.value,
      maxEntries: 5,
      staleMs: 0,
    });
    for (let i = 0; i < 5; i += 1) tracker.recordFailure(`email:user${i}@b.de`);
    expect(tracker.size()).toBe(5);

    // All five carry a single failure (no lock) and are instantly stale, so the
    // next insert triggers a sweep that evicts them.
    tracker.recordFailure("email:new@b.de");
    expect(tracker.size()).toBe(1);
    expect(tracker.checkLockout("email:new@b.de").failures).toBe(1);
    expect(tracker.checkLockout("email:user0@b.de").failures).toBe(0);
  });

  test("still-locked entries survive a sweep", () => {
    const clock = { value: 0 };
    const tracker = createLockoutTracker({
      now: () => clock.value,
      maxEntries: 3,
      staleMs: 1,
    });
    for (let i = 0; i < 3; i += 1) {
      for (let f = 0; f < 5; f += 1) tracker.recordFailure(`email:user${i}@b.de`);
    }
    expect(tracker.size()).toBe(3);

    // Fast-forward past the idle threshold so any unlocked entry would be
    // evictable — all three here are mid-delay, so the sweep triggered by the
    // next insert must keep them. An attacker's escalation is never silently
    // forgotten while its delay is still running.
    clock.value += 2;
    tracker.recordFailure("email:new@b.de");
    expect(tracker.size()).toBe(4);
    expect(tracker.checkLockout("email:user0@b.de").locked).toBe(true);
    expect(tracker.checkLockout("email:user1@b.de").locked).toBe(true);
    expect(tracker.checkLockout("email:user2@b.de").locked).toBe(true);
  });

  test("clear empties the tracker", () => {
    const clock = { value: 0 };
    const tracker = createLockoutTracker({ now: () => clock.value });
    tracker.recordFailure("email:a@b.de");
    tracker.recordFailure("ip:203.0.113.7");
    tracker.clear();
    expect(tracker.size()).toBe(0);
    expect(tracker.checkLockout("email:a@b.de").failures).toBe(0);
  });
});

async function main() {
  const passed = await runAllTests();
  if (!passed) process.exit(1);
}

main().catch((error) => {
  console.error("Lockout suite crashed:", error);
  process.exit(1);
});
