import { describe, expect, it } from 'vitest';

import { ARGON2_OPTIONS, checkPasswordStrength, passwordHasher } from './password';

describe('Argon2id password hashing (SPEC.md §25)', () => {
  it('uses Argon2id with at least the OWASP-recommended cost', () => {
    // The algorithm id is inlined because the library declares it as an
    // ambient const enum; this test is what keeps that inlining honest.
    expect(ARGON2_OPTIONS.algorithm).toBe(2);
    expect(ARGON2_OPTIONS.memoryCost).toBeGreaterThanOrEqual(19_456);
    expect(ARGON2_OPTIONS.timeCost).toBeGreaterThanOrEqual(2);
  });

  it('produces an Argon2id hash, not Argon2i or Argon2d', async () => {
    const hash = await passwordHasher.hash('correct horse battery staple');
    expect(hash.startsWith('$argon2id$')).toBe(true);
  });

  it('salts every hash, so identical passwords hash differently', async () => {
    const [a, b] = await Promise.all([
      passwordHasher.hash('same-password-123'),
      passwordHasher.hash('same-password-123'),
    ]);
    expect(a).not.toBe(b);
  });

  it('verifies a correct password and rejects a wrong one', async () => {
    const hash = await passwordHasher.hash('Demo!Pass123');
    expect(await passwordHasher.verify(hash, 'Demo!Pass123')).toBe(true);
    expect(await passwordHasher.verify(hash, 'Demo!Pass124')).toBe(false);
  });

  it('treats a malformed stored hash as a wrong password, never as an error', async () => {
    // A corrupt row must not produce a 500 that distinguishes it from a normal
    // failed login — that difference is itself an account-existence oracle.
    expect(await passwordHasher.verify('not-a-hash', 'anything')).toBe(false);
    expect(await passwordHasher.verify('', 'anything')).toBe(false);
  });

  it('flags hashes made with weaker parameters for transparent upgrade', async () => {
    expect(passwordHasher.needsRehash('$argon2id$v=19$m=4096,t=1,p=1$abc$def')).toBe(true);
    expect(passwordHasher.needsRehash('$2b$10$somethingbcrypt')).toBe(true);

    const current = await passwordHasher.hash('Demo!Pass123');
    expect(passwordHasher.needsRehash(current)).toBe(false);
  });
});

describe('password policy', () => {
  it('accepts a sufficiently long password with a letter and a number', () => {
    expect(checkPasswordStrength('Demo!Pass123').valid).toBe(true);
  });

  it('rejects short passwords, explaining why', () => {
    const result = checkPasswordStrength('Short1');
    expect(result.valid).toBe(false);
    expect(result.problems.join(' ')).toMatch(/at least 12/);
  });

  it('requires both a letter and a number', () => {
    expect(checkPasswordStrength('123456789012').valid).toBe(false);
    expect(checkPasswordStrength('abcdefghijkl').valid).toBe(false);
  });

  it('rejects a single repeated character', () => {
    expect(checkPasswordStrength('aaaaaaaaaaaaaa').valid).toBe(false);
  });
});
