/**
 * Security: AES-256-GCM Encryption Properties
 *
 * Verifies the security properties of @/lib/utils/encryption:
 *   - Ciphertext never contains plaintext
 *   - Random IVs produce different ciphertexts for identical plaintexts
 *   - Tampered ciphertext or auth tag throws (GCM authentication)
 *   - Decryption round-trips correctly
 *   - Missing / malformed key is rejected at runtime
 *
 * Note: tests/unit/encryption.test.ts covers basic round-trip and IV
 * randomness.  This file focuses on the *security boundary* properties
 * (plaintext non-exposure, authenticated encryption, key validation).
 *
 * SECURITY-GUIDELINES.md: credentials_enc uses AES-256-GCM.
 */
import { beforeAll, describe, expect, it } from 'vitest';

const VALID_KEY = 'a'.repeat(64); // 64 hex chars = 32 bytes

beforeAll(() => {
  process.env.EMAIL_CREDENTIAL_KEY = VALID_KEY;
});

// ─────────────────────────────────────────────────────────────────────────────

describe('AES-256-GCM - plaintext non-exposure', () => {
  it('encrypted output does not contain the plaintext', async () => {
    const { encrypt } = await import('@/lib/utils/encryption');
    const plaintext = 'my-secret-password';
    const encrypted = encrypt(plaintext);
    expect(encrypted).not.toContain(plaintext);
  });

  it('encrypted output does not contain plaintext even for short values', async () => {
    const { encrypt } = await import('@/lib/utils/encryption');
    // Pick a 2-char plaintext using *non-hex* characters so the assertion can't
    // false-positive: random hex bytes routinely contain pairs like 'ab' or 'cd'
    // by chance (~1 in 256 byte positions). Using 'XY' (neither is a hex digit)
    // means a passing assertion actually proves the plaintext didn't leak.
    const plaintext = 'XY';
    const encrypted = encrypt(plaintext);
    expect(encrypted).not.toContain(plaintext);
  });

  it('encrypted output is a JSON object with iv, tag, data fields', async () => {
    const { encrypt } = await import('@/lib/utils/encryption');
    const encrypted = encrypt('test-payload');
    const parsed = JSON.parse(encrypted) as Record<string, unknown>;
    expect(typeof parsed.iv).toBe('string');
    expect(typeof parsed.tag).toBe('string');
    expect(typeof parsed.data).toBe('string');
  });

  it('IV is 12 bytes (24 hex chars)', async () => {
    const { encrypt } = await import('@/lib/utils/encryption');
    const parsed = JSON.parse(encrypt('hello')) as { iv: string };
    expect(parsed.iv).toHaveLength(24); // 12 bytes × 2 hex chars/byte
  });

  it('GCM auth tag is 16 bytes (32 hex chars)', async () => {
    const { encrypt } = await import('@/lib/utils/encryption');
    const parsed = JSON.parse(encrypt('hello')) as { tag: string };
    expect(parsed.tag).toHaveLength(32); // 16 bytes × 2 hex chars/byte
  });
});

describe('AES-256-GCM - IV randomness (semantic security)', () => {
  it('different plaintexts produce different ciphertexts', async () => {
    const { encrypt } = await import('@/lib/utils/encryption');
    const enc1 = encrypt('password1');
    const enc2 = encrypt('password2');
    expect(enc1).not.toBe(enc2);
  });

  it('same plaintext produces different ciphertexts (random IV)', async () => {
    const { encrypt } = await import('@/lib/utils/encryption');
    const enc1 = encrypt('same-password');
    const enc2 = encrypt('same-password');
    // IVs differ, so ciphertexts differ - prevents ciphertext comparison attacks
    expect(enc1).not.toBe(enc2);
  });

  it('IVs are unique across repeated encryptions of identical plaintext', async () => {
    const { encrypt } = await import('@/lib/utils/encryption');
    const ivs = Array.from({ length: 10 }, () => {
      const parsed = JSON.parse(encrypt('repeated')) as { iv: string };
      return parsed.iv;
    });
    const uniqueIvs = new Set(ivs);
    // All 10 IVs must be unique (birthday probability is negligible for 12-byte random)
    expect(uniqueIvs.size).toBe(10);
  });
});

describe('AES-256-GCM - authenticated encryption (tamper detection)', () => {
  it('tampered data field throws on decrypt', async () => {
    const { encrypt, decrypt } = await import('@/lib/utils/encryption');
    const encrypted = encrypt('test');
    const parsed = JSON.parse(encrypted) as { iv: string; tag: string; data: string };
    // Flip the first byte of ciphertext
    const flipped = parsed.data.slice(0, 2) === 'ff' ? '00' : 'ff';
    parsed.data = flipped + parsed.data.slice(2);
    expect(() => decrypt(JSON.stringify(parsed))).toThrow();
  });

  it('tampered auth tag throws on decrypt', async () => {
    const { encrypt, decrypt } = await import('@/lib/utils/encryption');
    const encrypted = encrypt('test-auth-tag');
    const parsed = JSON.parse(encrypted) as { iv: string; tag: string; data: string };
    // Corrupt the auth tag
    const flipped = parsed.tag.slice(0, 2) === 'ff' ? '00' : 'ff';
    parsed.tag = flipped + parsed.tag.slice(2);
    expect(() => decrypt(JSON.stringify(parsed))).toThrow();
  });

  it('tampered IV throws on decrypt', async () => {
    const { encrypt, decrypt } = await import('@/lib/utils/encryption');
    const encrypted = encrypt('test-iv-tamper');
    const parsed = JSON.parse(encrypted) as { iv: string; tag: string; data: string };
    // Replace IV with a different random 12-byte value
    parsed.iv = 'b'.repeat(24);
    expect(() => decrypt(JSON.stringify(parsed))).toThrow();
  });

  it('completely different ciphertext throws on decrypt', async () => {
    const { decrypt } = await import('@/lib/utils/encryption');
    const fake = JSON.stringify({
      iv: 'c'.repeat(24),
      tag: 'd'.repeat(32),
      data: 'e'.repeat(32),
    });
    expect(() => decrypt(fake)).toThrow();
  });
});

describe('AES-256-GCM - decryption correctness', () => {
  it('decrypt recovers original plaintext', async () => {
    const { encrypt, decrypt } = await import('@/lib/utils/encryption');
    const plaintext = 'my-secret-credentials';
    const encrypted = encrypt(plaintext);
    const decrypted = decrypt(encrypted);
    expect(decrypted).toBe(plaintext);
  });

  it('round-trips an empty string', async () => {
    const { encrypt, decrypt } = await import('@/lib/utils/encryption');
    expect(decrypt(encrypt(''))).toBe('');
  });

  it('round-trips unicode and emoji', async () => {
    const { encrypt, decrypt } = await import('@/lib/utils/encryption');
    const unicode = 'γεια σου 🚢 日本語';
    expect(decrypt(encrypt(unicode))).toBe(unicode);
  });

  it('round-trips a long credential string', async () => {
    const { encrypt, decrypt } = await import('@/lib/utils/encryption');
    const longCred = 'smtp_password=' + 'x'.repeat(256);
    expect(decrypt(encrypt(longCred))).toBe(longCred);
  });
});

describe('AES-256-GCM - key validation', () => {
  it('throws when EMAIL_CREDENTIAL_KEY is not set', async () => {
    const { encrypt } = await import('@/lib/utils/encryption');
    const saved = process.env.EMAIL_CREDENTIAL_KEY;
    delete process.env.EMAIL_CREDENTIAL_KEY;
    expect(() => encrypt('test')).toThrow('EMAIL_CREDENTIAL_KEY');
    process.env.EMAIL_CREDENTIAL_KEY = saved;
  });

  it('throws when EMAIL_CREDENTIAL_KEY is too short', async () => {
    const { encrypt } = await import('@/lib/utils/encryption');
    const saved = process.env.EMAIL_CREDENTIAL_KEY;
    process.env.EMAIL_CREDENTIAL_KEY = 'tooshort';
    expect(() => encrypt('test')).toThrow('EMAIL_CREDENTIAL_KEY');
    process.env.EMAIL_CREDENTIAL_KEY = saved;
  });

  it('throws when EMAIL_CREDENTIAL_KEY is too long', async () => {
    const { encrypt } = await import('@/lib/utils/encryption');
    const saved = process.env.EMAIL_CREDENTIAL_KEY;
    process.env.EMAIL_CREDENTIAL_KEY = 'a'.repeat(65);
    expect(() => encrypt('test')).toThrow('EMAIL_CREDENTIAL_KEY');
    process.env.EMAIL_CREDENTIAL_KEY = saved;
  });
});
