import { describe, it, expect } from 'vitest';
import {
  encryptField,
  decryptField,
  generateKey,
  keyFromHex,
  keyToHex,
  deriveKey,
} from './aes-gcm.js';
import { isEncryptedField } from './guards.js';
import { toHex, fromHex } from './hex.js';

describe('encryptField / decryptField', () => {
  it('roundtrip', async () => {
    const key = await generateKey();
    const encrypted = await encryptField('Hello, World!', key, 'dek_test');
    const decrypted = await decryptField(encrypted, key);
    expect(decrypted).toBe('Hello, World!');
  });

  it('empty string', async () => {
    const key = await generateKey();
    const encrypted = await encryptField('', key, 'dek_test');
    const decrypted = await decryptField(encrypted, key);
    expect(decrypted).toBe('');
  });

  it('unicode', async () => {
    const key = await generateKey();
    const text = 'こんにちは世界 🌍 مرحبا Ñoño';
    const encrypted = await encryptField(text, key, 'dek_test');
    const decrypted = await decryptField(encrypted, key);
    expect(decrypted).toBe(text);
  });

  it('large payload', async () => {
    const key = await generateKey();
    const text = 'A'.repeat(100_000);
    const encrypted = await encryptField(text, key, 'dek_test');
    const decrypted = await decryptField(encrypted, key);
    expect(decrypted).toBe(text);
  });
});

describe('EncryptedField structure', () => {
  it('has correct sentinel fields', async () => {
    const key = await generateKey();
    const encrypted = await encryptField('test', key, 'dek_test');
    expect(encrypted.__encrypted).toBe(true);
    expect(encrypted.v).toBe(1);
    expect(encrypted.alg).toBe('aes-256-gcm');
    expect(encrypted.dekId).toBe('dek_test');
  });

  it('has correct hex lengths', async () => {
    const key = await generateKey();
    const encrypted = await encryptField('test', key, 'dek_test');
    expect(encrypted.iv.length).toBe(24); // 12 bytes = 24 hex
    expect(encrypted.tag.length).toBe(32); // 16 bytes = 32 hex
    expect(encrypted.ct.length).toBeGreaterThan(0);
  });

  it('unique IVs per encryption', async () => {
    const key = await generateKey();
    const a = await encryptField('same', key, 'dek_test');
    const b = await encryptField('same', key, 'dek_test');
    expect(a.iv).not.toBe(b.iv);
    expect(a.ct).not.toBe(b.ct);
  });
});

describe('AAD (Additional Authenticated Data)', () => {
  it('roundtrip with AAD', async () => {
    const key = await generateKey();
    const encrypted = await encryptField('secret', key, 'dek_test', 'user:ctx');
    const decrypted = await decryptField(encrypted, key, 'user:ctx');
    expect(decrypted).toBe('secret');
  });

  it('wrong AAD fails', async () => {
    const key = await generateKey();
    const encrypted = await encryptField('secret', key, 'dek_test', 'correct');
    await expect(decryptField(encrypted, key, 'wrong')).rejects.toThrow();
  });

  it('missing AAD fails', async () => {
    const key = await generateKey();
    const encrypted = await encryptField('secret', key, 'dek_test', 'some-aad');
    await expect(decryptField(encrypted, key)).rejects.toThrow();
  });
});

describe('wrong key', () => {
  it('decrypt with wrong key fails', async () => {
    const key = await generateKey();
    const wrongKey = await generateKey();
    const encrypted = await encryptField('secret', key, 'dek_test');
    await expect(decryptField(encrypted, wrongKey)).rejects.toThrow();
  });
});

describe('keyFromHex / keyToHex', () => {
  it('roundtrip', async () => {
    const key = await generateKey();
    const hex = await keyToHex(key);
    expect(hex.length).toBe(64); // 32 bytes = 64 hex chars
    const restored = await keyFromHex(hex);
    const encrypted = await encryptField('test', key, 'dek_test');
    const decrypted = await decryptField(encrypted, restored);
    expect(decrypted).toBe('test');
  });

  it('rejects invalid length', async () => {
    await expect(keyFromHex('aabb')).rejects.toThrow('32-byte key');
  });
});

describe('deriveKey', () => {
  it('derives consistent key from passphrase + salt', async () => {
    const salt = new Uint8Array(16);
    globalThis.crypto.getRandomValues(salt);
    const key1 = await deriveKey('my-passphrase', salt, 1000, true);
    const key2 = await deriveKey('my-passphrase', salt, 1000, true);
    const hex1 = await keyToHex(key1);
    const hex2 = await keyToHex(key2);
    expect(hex1).toBe(hex2);
  });

  it('different passphrases produce different keys', async () => {
    const salt = new Uint8Array(16);
    globalThis.crypto.getRandomValues(salt);
    const key1 = await deriveKey('pass-1', salt, 1000, true);
    const key2 = await deriveKey('pass-2', salt, 1000, true);
    const hex1 = await keyToHex(key1);
    const hex2 = await keyToHex(key2);
    expect(hex1).not.toBe(hex2);
  });

  it('derived key can encrypt/decrypt', async () => {
    const salt = new Uint8Array(16);
    globalThis.crypto.getRandomValues(salt);
    const key = await deriveKey('test', salt, 1000, true);
    const encrypted = await encryptField('hello', key, 'dek_test');
    const decrypted = await decryptField(encrypted, key);
    expect(decrypted).toBe('hello');
  });
});

describe('isEncryptedField', () => {
  it('true for valid EncryptedField', async () => {
    const key = await generateKey();
    const encrypted = await encryptField('test', key, 'dek_test');
    expect(isEncryptedField(encrypted)).toBe(true);
  });

  it('false for plain string', () => {
    expect(isEncryptedField('just a string')).toBe(false);
  });

  it('false for null', () => {
    expect(isEncryptedField(null)).toBe(false);
  });

  it('false for incomplete object', () => {
    expect(isEncryptedField({ __encrypted: true, v: 1 })).toBe(false);
  });
});

describe('hex utilities', () => {
  it('toHex / fromHex roundtrip', () => {
    const bytes = new Uint8Array([0x00, 0x0f, 0xff, 0xab, 0xcd]);
    const hex = toHex(bytes);
    expect(hex).toBe('000fffabcd');
    const restored = fromHex(hex);
    expect(restored).toEqual(bytes);
  });

  it('fromHex rejects odd length', () => {
    expect(() => fromHex('a')).toThrow('even length');
  });
});
