import { randomBytes } from 'node:crypto';
import { SessionDecryptError, decrypt, encrypt } from './session-crypto';

const KEY = randomBytes(32);
const OTHER_KEY = randomBytes(32);
const PLAINTEXT = JSON.stringify({ user: { oid: 'abc', tid: 'def' }, createdAt: 1714000000000 });

describe('session-crypto', () => {
  it('round-trips a payload', () => {
    const ciphertext = encrypt(PLAINTEXT, KEY);
    expect(decrypt(ciphertext, KEY)).toBe(PLAINTEXT);
  });

  it('produces the v1 envelope shape', () => {
    const ciphertext = encrypt(PLAINTEXT, KEY);
    const parts = ciphertext.split('.');
    expect(parts).toHaveLength(4);
    expect(parts[0]).toBe('v1');
  });

  it('uses a fresh IV per encryption', () => {
    const a = encrypt(PLAINTEXT, KEY);
    const b = encrypt(PLAINTEXT, KEY);
    expect(a).not.toBe(b);
    // Same plaintext + key + different IV ⇒ different ciphertexts but
    // both decrypt back to the same value.
    expect(decrypt(a, KEY)).toBe(PLAINTEXT);
    expect(decrypt(b, KEY)).toBe(PLAINTEXT);
  });

  it('rejects ciphertext encrypted under a different key', () => {
    const ciphertext = encrypt(PLAINTEXT, KEY);
    expect(() => decrypt(ciphertext, OTHER_KEY)).toThrow(SessionDecryptError);
  });

  it('rejects tampered ciphertext (auth tag verification)', () => {
    const ciphertext = encrypt(PLAINTEXT, KEY);
    const parts = ciphertext.split('.');
    // Flip a byte in the ciphertext segment.
    const ct = Buffer.from(parts[3] as string, 'base64url');
    ct[0] = (ct[0] ?? 0) ^ 0xff;
    parts[3] = ct.toString('base64url');
    const tampered = parts.join('.');
    expect(() => decrypt(tampered, KEY)).toThrow(SessionDecryptError);
  });

  it('rejects a tampered auth tag', () => {
    const ciphertext = encrypt(PLAINTEXT, KEY);
    const parts = ciphertext.split('.');
    const tag = Buffer.from(parts[2] as string, 'base64url');
    tag[0] = (tag[0] ?? 0) ^ 0xff;
    parts[2] = tag.toString('base64url');
    const tampered = parts.join('.');
    expect(() => decrypt(tampered, KEY)).toThrow(SessionDecryptError);
  });

  it('rejects an unknown version', () => {
    const ciphertext = encrypt(PLAINTEXT, KEY);
    const parts = ciphertext.split('.');
    parts[0] = 'v999';
    expect(() => decrypt(parts.join('.'), KEY)).toThrow(/unsupported version "v999"/);
  });

  it('rejects a malformed envelope (wrong segment count)', () => {
    expect(() => decrypt('not-even-close', KEY)).toThrow(/malformed envelope/);
    expect(() => decrypt('v1.a.b', KEY)).toThrow(/malformed envelope/);
  });

  it('rejects an IV of the wrong length', () => {
    const ciphertext = encrypt(PLAINTEXT, KEY);
    const parts = ciphertext.split('.');
    parts[1] = Buffer.alloc(8).toString('base64url');
    expect(() => decrypt(parts.join('.'), KEY)).toThrow(/iv length 8/);
  });

  it('rejects a tag of the wrong length', () => {
    const ciphertext = encrypt(PLAINTEXT, KEY);
    const parts = ciphertext.split('.');
    parts[2] = Buffer.alloc(8).toString('base64url');
    expect(() => decrypt(parts.join('.'), KEY)).toThrow(/tag length 8/);
  });
});
