import { randomBytes } from 'node:crypto';
import { HashUserIdService } from './hash-user-id.service';

const STRONG_SALT = randomBytes(32).toString('base64url');

function withSalt<T>(salt: string, fn: () => T): T {
  const original = process.env['LOG_USER_ID_SALT'];
  process.env['LOG_USER_ID_SALT'] = salt;
  try {
    return fn();
  } finally {
    if (original === undefined) {
      delete process.env['LOG_USER_ID_SALT'];
    } else {
      process.env['LOG_USER_ID_SALT'] = original;
    }
  }
}

describe('HashUserIdService', () => {
  it('hashes the same input to the same output (stable join key)', () => {
    withSalt(STRONG_SALT, () => {
      const service = new HashUserIdService();
      expect(service.hash('user-oid-42')).toBe(service.hash('user-oid-42'));
    });
  });

  it('returns a 16-hex-char digest', () => {
    withSalt(STRONG_SALT, () => {
      const service = new HashUserIdService();
      const hash = service.hash('user-oid-42');
      expect(hash).toMatch(/^[0-9a-f]{16}$/);
    });
  });

  it('produces different outputs for different inputs', () => {
    withSalt(STRONG_SALT, () => {
      const service = new HashUserIdService();
      expect(service.hash('alice')).not.toBe(service.hash('bob'));
    });
  });

  it('produces different outputs across salts (rotation invalidates the join key)', () => {
    const a = withSalt(STRONG_SALT, () => new HashUserIdService().hash('alice'));
    const b = withSalt(randomBytes(32).toString('base64url'), () =>
      new HashUserIdService().hash('alice'),
    );
    expect(a).not.toBe(b);
  });

  it('refuses to construct when LOG_USER_ID_SALT is unset', () => {
    const original = process.env['LOG_USER_ID_SALT'];
    delete process.env['LOG_USER_ID_SALT'];
    try {
      expect(() => new HashUserIdService()).toThrow(/LOG_USER_ID_SALT is not set/);
    } finally {
      if (original !== undefined) {
        process.env['LOG_USER_ID_SALT'] = original;
      }
    }
  });
});
