/**
 * Per-email login-attempt counter tests.
 *
 * audit3-followup (2026-05-29): the counter is now MySQL-backed via
 * BruteForceCounter. Prisma is mocked with an in-memory store
 * (_prisma-mock.ts) that mirrors the methods brute-force-counter.ts
 * uses; the original behaviour assertions (5 failures = lock, 15-min
 * window, no extension while locked, etc.) remain unchanged.
 */
import { bruteForceCounterMock, _resetMockStore } from './_prisma-mock';

jest.mock('@/lib/prisma', () => ({
    prisma: { bruteForceCounter: bruteForceCounterMock },
}));

import { isLocked, recordFailure, recordSuccess } from '../login-attempts';

beforeEach(() => {
    _resetMockStore();
    jest.useFakeTimers();
    jest.setSystemTime(new Date('2026-01-01T00:00:00Z'));
});

afterEach(() => {
    jest.useRealTimers();
});

describe('login-attempts: defaults (5 failures in 15 min -> 15 min lock)', () => {
    it('does not lock on the first four failures', async () => {
        for (let i = 0; i < 4; i++) await recordFailure('alice@example.com');
        await expect(isLocked('alice@example.com')).resolves.toEqual({ locked: false });
    });

    it('locks on the fifth failure within the window', async () => {
        for (let i = 0; i < 5; i++) await recordFailure('alice@example.com');
        const r = await isLocked('alice@example.com');
        expect(r.locked).toBe(true);
        expect(r.retryAfterSeconds).toBeGreaterThan(0);
        expect(r.retryAfterSeconds).toBeLessThanOrEqual(15 * 60);
    });

    it('does NOT lock when failures are spread outside the window', async () => {
        // Drip 5 failures spaced 4 minutes apart -> window is 15 min, so only
        // the most recent 4 are inside the window when the 5th fires.
        for (let i = 0; i < 5; i++) {
            await recordFailure('alice@example.com');
            jest.advanceTimersByTime(4 * 60 * 1000);
        }
        await expect(isLocked('alice@example.com').then((r) => r.locked)).resolves.toBe(false);
    });

    it('clears counter on recordSuccess', async () => {
        for (let i = 0; i < 4; i++) await recordFailure('alice@example.com');
        await recordSuccess('alice@example.com');
        // After success, four more failures should still be under the threshold.
        for (let i = 0; i < 4; i++) await recordFailure('alice@example.com');
        await expect(isLocked('alice@example.com').then((r) => r.locked)).resolves.toBe(false);
    });

    it('lockout expires after the configured duration', async () => {
        for (let i = 0; i < 5; i++) await recordFailure('alice@example.com');
        await expect(isLocked('alice@example.com').then((r) => r.locked)).resolves.toBe(true);
        jest.advanceTimersByTime(15 * 60 * 1000 + 1);
        await expect(isLocked('alice@example.com').then((r) => r.locked)).resolves.toBe(false);
        // Fresh start after expiry: four more failures should be fine.
        for (let i = 0; i < 4; i++) await recordFailure('alice@example.com');
        await expect(isLocked('alice@example.com').then((r) => r.locked)).resolves.toBe(false);
    });

    it('does not extend lockout when more failures arrive while locked', async () => {
        for (let i = 0; i < 5; i++) await recordFailure('alice@example.com');
        const firstLock = (await isLocked('alice@example.com')).retryAfterSeconds!;
        jest.advanceTimersByTime(5 * 60 * 1000);
        await recordFailure('alice@example.com');
        await recordFailure('alice@example.com');
        await recordFailure('alice@example.com');
        const stillLock = (await isLocked('alice@example.com')).retryAfterSeconds!;
        // The retry-after should be DECREASING (time passed), not extended by
        // the new failures.
        expect(stillLock).toBeLessThan(firstLock);
    });
});

describe('login-attempts: per-email isolation', () => {
    it('does not leak between distinct emails', async () => {
        for (let i = 0; i < 5; i++) await recordFailure('alice@example.com');
        await expect(isLocked('alice@example.com').then((r) => r.locked)).resolves.toBe(true);
        await expect(isLocked('bob@example.com').then((r) => r.locked)).resolves.toBe(false);
    });

    it('normalises email case + whitespace', async () => {
        for (let i = 0; i < 5; i++) await recordFailure('  ALICE@Example.COM  ');
        await expect(isLocked('alice@example.com').then((r) => r.locked)).resolves.toBe(true);
        await expect(isLocked('Alice@Example.com').then((r) => r.locked)).resolves.toBe(true);
    });
});

describe('login-attempts: custom config', () => {
    it('honours a tighter maxFailures and shorter window', async () => {
        const cfg = { maxFailures: 2, windowMs: 1000, lockoutMs: 5000 };
        await recordFailure('a@b', cfg);
        await expect(isLocked('a@b', cfg).then((r) => r.locked)).resolves.toBe(false);
        await recordFailure('a@b', cfg);
        await expect(isLocked('a@b', cfg).then((r) => r.locked)).resolves.toBe(true);
    });
});
