import { describe, expect, it } from 'vitest';
import {
  describeWait,
  LOGIN_ACCOUNT_DELAY_MS,
  LOGIN_MAX_FAILURES,
  LOGIN_WINDOW_MS,
  LoginThrottle,
  maskEmail,
  throttleKeys,
} from './login-throttle';

describe('LoginThrottle', () => {
  it('blocks one address guessing one account after 10 failures in 15 minutes, even for the right password', () => {
    const t = new LoginThrottle();
    const keys = throttleKeys('host', '203.0.113.7', 'Owner@Bar.test');
    for (let i = 0; i < LOGIN_MAX_FAILURES; i++) {
      expect(t.check(keys, i * 1000).allowed).toBe(true);
      t.recordFailure(keys, i * 1000);
    }
    const blocked = t.check(keys, 10_000);
    expect(blocked.allowed).toBe(false);
    // Blocked for a full window from the 10th failure (at 9 s).
    expect(blocked.retryAfterMs).toBe(LOGIN_WINDOW_MS - 1_000);
  });

  it('unblocks once the window has passed', () => {
    const t = new LoginThrottle({ maxFailures: 2, windowMs: 60_000 });
    const keys = throttleKeys('host', '203.0.113.7', 'a@x.test');
    t.recordFailure(keys, 0);
    t.recordFailure(keys, 1_000);
    expect(t.check(keys, 30_000).allowed).toBe(false);
    expect(t.check(keys, 61_001).allowed).toBe(true);
  });

  it('never locks an account out for other networks: failures from many addresses only slow it down', () => {
    const t = new LoginThrottle({ maxFailures: 3, accountSlowAfter: 3, windowMs: 60_000 });
    const tripped: boolean[] = [];
    // Spraying one account from many addresses (the S3 attack).
    for (let i = 0; i < 20; i++) tripped.push(t.recordFailure(throttleKeys('admin', `198.51.100.${i}`, 'admin@x.test'), i).accountTripped);
    // The owner, from an address with no failures, is never refused…
    const owner = t.check(throttleKeys('admin', '192.0.2.50', 'ADMIN@x.test'), 100);
    expect(owner.allowed).toBe(true);
    // …only slowed down while the account is under attack.
    expect(owner.delayMs).toBe(LOGIN_ACCOUNT_DELAY_MS);
    // The trip is reported exactly once per window, for logging.
    expect(tripped.filter(Boolean)).toHaveLength(1);
    expect(tripped.indexOf(true)).toBe(2);
    // Other accounts are not slowed.
    expect(t.check(throttleKeys('admin', '192.0.2.50', 'other@x.test'), 100).delayMs).toBe(0);
    // After the window the slowdown ends.
    expect(t.check(throttleKeys('admin', '192.0.2.50', 'admin@x.test'), 60_100).delayMs).toBe(0);
  });

  it('blocks one address spraying many accounts at the per-address limit', () => {
    const t = new LoginThrottle({ maxFailures: 3, maxIpFailures: 5, windowMs: 60_000 });
    for (let i = 0; i < 4; i++) t.recordFailure(throttleKeys('admin', '192.0.2.1', `u${i}@x.test`), i);
    expect(t.check(throttleKeys('admin', '192.0.2.1', 'fresh@x.test'), 10).allowed).toBe(true);
    t.recordFailure(throttleKeys('admin', '192.0.2.1', 'u4@x.test'), 5);
    expect(t.check(throttleKeys('admin', '192.0.2.1', 'fresh@x.test'), 10).allowed).toBe(false);
    // Another address is unaffected; host and admin counters are separate.
    expect(t.check(throttleKeys('admin', '192.0.2.2', 'u0@x.test'), 10).allowed).toBe(true);
    expect(t.check(throttleKeys('host', '192.0.2.1', 'u0@x.test'), 10).allowed).toBe(true);
  });

  it('a blocked pair does not block the same account from another address', () => {
    const t = new LoginThrottle({ maxFailures: 2, windowMs: 60_000 });
    const keys = throttleKeys('host', '203.0.113.20', 'owner@bar.test');
    t.recordFailure(keys, 0);
    t.recordFailure(keys, 1);
    expect(t.check(keys, 2).allowed).toBe(false);
    expect(t.check(throttleKeys('host', '203.0.113.21', 'owner@bar.test'), 2).allowed).toBe(true);
  });

  it('does not count checks or successes', () => {
    const t = new LoginThrottle({ maxFailures: 1, windowMs: 60_000 });
    const keys = throttleKeys('host', '203.0.113.7', 'a@x.test');
    for (let i = 0; i < 50; i++) expect(t.check(keys, i)).toEqual({ allowed: true, retryAfterMs: 0, delayMs: 0 });
  });
});

describe('throttle keys', () => {
  it('builds address, pair and account keys with a normalised e-mail', () => {
    expect(throttleKeys('host', '203.0.113.7', ' Owner@Bar.test ')).toEqual({
      ip: 'host:ip:203.0.113.7',
      pair: 'host:pair:owner@bar.test|203.0.113.7',
      account: 'host:email:owner@bar.test',
    });
  });

  it('masks e-mails for logs and describes waits in minutes', () => {
    expect(maskEmail('Owner@Bar.test')).toBe('o***@bar.test');
    expect(describeWait(10_000)).toBe('about a minute');
    expect(describeWait(14 * 60_000 + 1)).toBe('about 15 minutes');
  });
});
