/**
 * Password Hasher Tests - CVE-2 Remediation Validation
 *
 * Tests verify:
 * - bcrypt is used instead of SHA-256
 * - 12 rounds minimum
 * - Password validation rules
 * - Timing-safe comparison
 */

import { describe, it, expect, beforeEach } from 'vitest';
import {
  PasswordHasher,
  PasswordHashError,
  createPasswordHasher,
} from '../src/password-hasher.js';

describe('PasswordHasher', () => {
  let hasher: PasswordHasher;

  beforeEach(() => {
    hasher = new PasswordHasher({ rounds: 12 });
  });

  describe('Configuration', () => {
    it('should create hasher with default 12 rounds', () => {
      const defaultHasher = new PasswordHasher();
      expect(defaultHasher.getConfig().rounds).toBe(12);
    });

    it('should reject rounds below 10', () => {
      expect(() => new PasswordHasher({ rounds: 8 })).toThrow(PasswordHashError);
    });

    it('should reject rounds above 20', () => {
      expect(() => new PasswordHasher({ rounds: 22 })).toThrow(PasswordHashError);
    });

    it('should reject minimum password length below 8', () => {
      expect(() => new PasswordHasher({ minLength: 4 })).toThrow(PasswordHashError);
    });
  });

  describe('Password Validation', () => {
    it('should reject empty password', () => {
      const result = hasher.validate('');
      expect(result.isValid).toBe(false);
      expect(result.errors).toContain('Password is required');
    });

    it('should reject password shorter than minimum', () => {
      const result = hasher.validate('short');
      expect(result.isValid).toBe(false);
      expect(result.errors.some(e => e.includes('at least'))).toBe(true);
    });

    it('should reject password without uppercase', () => {
      const result = hasher.validate('password123');
      expect(result.isValid).toBe(false);
      expect(result.errors.some(e => e.includes('uppercase'))).toBe(true);
    });

    it('should reject password without lowercase', () => {
      const result = hasher.validate('PASSWORD123');
      expect(result.isValid).toBe(false);
      expect(result.errors.some(e => e.includes('lowercase'))).toBe(true);
    });

    it('should reject password without digit', () => {
      const result = hasher.validate('PasswordNoDigit');
      expect(result.isValid).toBe(false);
      expect(result.errors.some(e => e.includes('digit'))).toBe(true);
    });

    it('should accept valid password', () => {
      const result = hasher.validate('SecurePass123');
      expect(result.isValid).toBe(true);
      expect(result.errors).toHaveLength(0);
    });

    it('should optionally require special character', () => {
      const strictHasher = new PasswordHasher({ requireSpecial: true });
      const result = strictHasher.validate('SecurePass123');
      expect(result.isValid).toBe(false);
      expect(result.errors.some(e => e.includes('special'))).toBe(true);
    });

    it('should accept password with special character when required', () => {
      const strictHasher = new PasswordHasher({ requireSpecial: true });
      const result = strictHasher.validate('SecurePass123!');
      expect(result.isValid).toBe(true);
    });
  });

  describe('Password Hashing', () => {
    it('should hash password with bcrypt', async () => {
      const hash = await hasher.hash('SecurePass123');

      // bcrypt hashes start with $2a$, $2b$, or $2y$
      expect(hash).toMatch(/^\$2[aby]\$\d{2}\$/);
    });

    it('should produce different hashes for same password', async () => {
      const hash1 = await hasher.hash('SecurePass123');
      const hash2 = await hasher.hash('SecurePass123');

      expect(hash1).not.toBe(hash2); // Different salts
    });

    it('should include rounds in hash', async () => {
      const hash = await hasher.hash('SecurePass123');

      // Hash format: $2b$12$...
      expect(hash).toContain('$12$');
    });

    it('should throw for invalid password', async () => {
      await expect(hasher.hash('short')).rejects.toThrow(PasswordHashError);
    });

    it('should throw for empty password', async () => {
      await expect(hasher.hash('')).rejects.toThrow(PasswordHashError);
    });
  });

  describe('Password Verification', () => {
    it('should verify correct password', async () => {
      const password = 'SecurePass123';
      const hash = await hasher.hash(password);

      const isValid = await hasher.verify(password, hash);
      expect(isValid).toBe(true);
    });

    it('should reject incorrect password', async () => {
      const hash = await hasher.hash('SecurePass123');

      const isValid = await hasher.verify('WrongPass123', hash);
      expect(isValid).toBe(false);
    });

    it('should return false for empty password', async () => {
      const hash = await hasher.hash('SecurePass123');

      const isValid = await hasher.verify('', hash);
      expect(isValid).toBe(false);
    });

    it('should return false for empty hash', async () => {
      const isValid = await hasher.verify('SecurePass123', '');
      expect(isValid).toBe(false);
    });

    it('should return false for invalid hash format', async () => {
      const isValid = await hasher.verify('SecurePass123', 'invalid-hash');
      expect(isValid).toBe(false);
    });

    it('should return false for SHA-256 hash (old format)', async () => {
      // SHA-256 hash format (what we're replacing)
      const sha256Hash = 'a665a45920422f9d417e4867efdc4fb8a04a1f3fff1fa07e998e86f7f7a27ae3';

      const isValid = await hasher.verify('password', sha256Hash);
      expect(isValid).toBe(false);
    });
  });

  describe('Rehash Detection', () => {
    it('should not need rehash for current rounds', async () => {
      const hash = await hasher.hash('SecurePass123');
      expect(hasher.needsRehash(hash)).toBe(false);
    });

    it('should need rehash for lower rounds', () => {
      // Hash with 10 rounds
      const lowRoundsHash = '$2b$10$N9qo8uLOickgx2ZMRZoMyeIjZAgcfl7p92ldGxad68LJZdL17lhWy';
      expect(hasher.needsRehash(lowRoundsHash)).toBe(true);
    });

    it('should need rehash for invalid format', () => {
      expect(hasher.needsRehash('invalid-hash')).toBe(true);
    });

    it('should need rehash for SHA-256 hash', () => {
      const sha256Hash = 'a665a45920422f9d417e4867efdc4fb8a04a1f3fff1fa07e998e86f7f7a27ae3';
      expect(hasher.needsRehash(sha256Hash)).toBe(true);
    });
  });

  describe('Factory Function', () => {
    it('should create hasher with specified rounds', () => {
      const hasher12 = createPasswordHasher(12);
      expect(hasher12.getConfig().rounds).toBe(12);

      const hasher14 = createPasswordHasher(14);
      expect(hasher14.getConfig().rounds).toBe(14);
    });

    it('should use default 12 rounds', () => {
      const hasher = createPasswordHasher();
      expect(hasher.getConfig().rounds).toBe(12);
    });
  });

  describe('CVE-2 Security Verification', () => {
    it('should NOT use hardcoded salt', async () => {
      // Generate multiple hashes and verify they have different salts
      const hashes = await Promise.all([
        hasher.hash('SecurePass123'),
        hasher.hash('SecurePass123'),
        hasher.hash('SecurePass123'),
      ]);

      // Extract salts (22 chars after $2b$12$)
      const salts = hashes.map(h => h.substring(7, 29));

      // All salts should be unique
      const uniqueSalts = new Set(salts);
      expect(uniqueSalts.size).toBe(3);
    });

    it('should NOT produce same hash for same input (unlike SHA-256)', async () => {
      // SHA-256 with hardcoded salt would produce identical output
      const hash1 = await hasher.hash('SecurePass123');
      const hash2 = await hasher.hash('SecurePass123');

      expect(hash1).not.toBe(hash2);
    });

    it('should produce hash that takes time to compute', async () => {
      const start = Date.now();
      await hasher.hash('SecurePass123');
      const duration = Date.now() - start;

      // bcrypt with 12 rounds should take measurable time (>10ms typically)
      expect(duration).toBeGreaterThan(5);
    });
  });
});
