/*
 * Tests for the password-policy validation and lockout helpers. These exercise the pure logic
 * (complexity checks, identifier detection, lockout math) against an explicit policy object so
 * they do not depend on the database-backed settings store.
 */

const { checkPassword, describe: describePolicy, DEFAULT_POLICY, isLocked, minAgeRemainingMinutes, passwordExpired } = require('./passwordPolicy');

const strictPolicy = {
  ...DEFAULT_POLICY,
  min_length: 12,
  require_upper: true,
  require_lower: true,
  require_number: true,
  require_symbol: true,
  disallow_identifiers: true,
  history_count: 0 // disable DB-backed history for these pure tests
};

describe('password policy', () => {
  test('a compliant password passes every rule', () => {
    expect(checkPassword('Str0ng!Passphrase', null, strictPolicy)).toEqual([]);
  });

  test('each missing character class is reported', () => {
    expect(checkPassword('short', null, strictPolicy).length).toBeGreaterThan(0);
    expect(checkPassword('alllowercase1!', null, strictPolicy)).toContain('Add an uppercase letter');
    expect(checkPassword('ALLUPPERCASE1!', null, strictPolicy)).toContain('Add a lowercase letter');
    expect(checkPassword('NoNumbersHere!', null, strictPolicy)).toContain('Add a number');
    expect(checkPassword('NoSymbolsHere1', null, strictPolicy)).toContain('Add a symbol');
  });

  test('minimum length is enforced', () => {
    expect(checkPassword('Ab1!', null, strictPolicy)).toContain('Use at least 12 characters');
  });

  test('the user name or email cannot appear in the password', () => {
    const user = { name: 'Jordan Banks', email: 'jbanks@example.com' };
    expect(checkPassword('Jordan!Secret99', user, strictPolicy)).toContain('Must not contain your name or email');
    expect(checkPassword('jbanks!Secret99', user, strictPolicy)).toContain('Must not contain your name or email');
    // A password without the identifiers is fine.
    expect(checkPassword('Unrelated!Secret99', user, strictPolicy)).toEqual([]);
  });

  test('identifier check is skipped when the policy disables it', () => {
    const policy = { ...strictPolicy, disallow_identifiers: false };
    const user = { name: 'Jordan Banks', email: 'jbanks@example.com' };
    expect(checkPassword('Jordan!Secret99', user, policy)).toEqual([]);
  });

  test('describe lists the active rules', () => {
    const rules = describePolicy(strictPolicy);
    expect(rules.some((r) => /12 characters/.test(r))).toBe(true);
    expect(rules.some((r) => /uppercase/.test(r))).toBe(true);
  });

  test('isLocked reflects the locked_until timestamp', () => {
    expect(isLocked({ locked_until: new Date(Date.now() + 60000).toISOString() })).toBe(true);
    expect(isLocked({ locked_until: new Date(Date.now() - 60000).toISOString() })).toBe(false);
    expect(isLocked({ locked_until: null })).toBe(false);
  });

  test('passwordExpired compares age against expiry_days', () => {
    const policy = { ...DEFAULT_POLICY, expiry_days: 30 };
    const old = new Date(Date.now() - 40 * 86400000).toISOString();
    const recent = new Date(Date.now() - 5 * 86400000).toISOString();
    expect(passwordExpired({ password_changed_at: old }, policy)).toBe(true);
    expect(passwordExpired({ password_changed_at: recent }, policy)).toBe(false);
    // expiry_days 0 means passwords never expire.
    expect(passwordExpired({ password_changed_at: old }, { ...policy, expiry_days: 0 })).toBe(false);
  });

  test('minAgeRemainingMinutes blocks changes that are too soon', () => {
    const policy = { ...DEFAULT_POLICY, min_age_hours: 24 };
    const justChanged = new Date(Date.now() - 60000).toISOString();
    const longAgo = new Date(Date.now() - 48 * 3600000).toISOString();
    expect(minAgeRemainingMinutes({ password_changed_at: justChanged }, policy)).toBeGreaterThan(0);
    expect(minAgeRemainingMinutes({ password_changed_at: longAgo }, policy)).toBe(0);
  });
});
