import crypto from 'node:crypto';
import { describe, it, expect } from 'vitest';
import {
  encryptField,
  decryptField,
  isEncrypted,
  type FieldEncryptionConfig,
} from '../field-encryption';

const TEST_KEY = crypto.randomBytes(32).toString('hex'); // 64 hex chars
const config: FieldEncryptionConfig = { key: TEST_KEY, keyVersion: 1 };

describe('field-encryption', () => {
  describe('encryptField / decryptField round-trip', () => {
    it('encrypts and decrypts a simple object', () => {
      const original = { name: 'Nguyen Van A', idNumber: '012345678901' };
      const encrypted = encryptField(original, config);
      const decrypted = decryptField(encrypted, config);

      expect(decrypted).toEqual(original);
    });

    it('encrypts and decrypts a string value', () => {
      const original = 'sensitive-pii-data';
      const encrypted = encryptField(original, config);
      const decrypted = decryptField(encrypted, config);

      expect(decrypted).toBe(original);
    });

    it('encrypts and decrypts nested objects', () => {
      const original = {
        identity: { type: 'CCCD', number: '012345678901' },
        address: { city: 'Ho Chi Minh', district: '1' },
        documents: [{ url: 'https://example.com/front.jpg' }],
      };
      const encrypted = encryptField(original, config);
      const decrypted = decryptField(encrypted, config);

      expect(decrypted).toEqual(original);
    });

    it('encrypts and decrypts null-containing objects', () => {
      const original = { name: 'Test', optional: null };
      const encrypted = encryptField(original, config);
      const decrypted = decryptField(encrypted, config);

      expect(decrypted).toEqual(original);
    });

    it('produces different ciphertext for same input (random IV)', () => {
      const original = { name: 'Test' };
      const enc1 = encryptField(original, config);
      const enc2 = encryptField(original, config);

      expect(enc1).not.toBe(enc2);
      expect(decryptField(enc1, config)).toEqual(original);
      expect(decryptField(enc2, config)).toEqual(original);
    });
  });

  describe('encrypted format', () => {
    it('starts with enc: prefix', () => {
      const encrypted = encryptField({ test: true }, config);
      expect(encrypted).toMatch(/^enc:v\d+:[0-9a-f]+:[0-9a-f]+:[0-9a-f]+$/);
    });

    it('includes key version', () => {
      const v2Config = { ...config, keyVersion: 2 };
      const encrypted = encryptField('data', v2Config);
      expect(encrypted).toMatch(/^enc:v2:/);
    });
  });

  describe('isEncrypted', () => {
    it('returns true for encrypted values', () => {
      const encrypted = encryptField('test', config);
      expect(isEncrypted(encrypted)).toBe(true);
    });

    it('returns false for plaintext strings', () => {
      expect(isEncrypted('plain text')).toBe(false);
      expect(isEncrypted('{"name":"test"}')).toBe(false);
    });

    it('returns false for non-string values', () => {
      expect(isEncrypted(null)).toBe(false);
      expect(isEncrypted(undefined)).toBe(false);
      expect(isEncrypted(42)).toBe(false);
      expect(isEncrypted({ name: 'test' })).toBe(false);
    });
  });

  describe('decryptField — plaintext passthrough', () => {
    it('returns non-string values as-is', () => {
      const obj = { name: 'test' };
      expect(decryptField(obj, config)).toBe(obj);
      expect(decryptField(null, config)).toBe(null);
      expect(decryptField(42, config)).toBe(42);
    });

    it('returns non-encrypted strings as-is', () => {
      expect(decryptField('plain text', config)).toBe('plain text');
    });
  });

  describe('error handling', () => {
    it('rejects invalid key length', () => {
      const badConfig = { key: 'tooshort', keyVersion: 1 };
      expect(() => encryptField('test', badConfig)).toThrow('32 bytes');
    });

    it('rejects tampered ciphertext', () => {
      const encrypted = encryptField('secret', config);
      // Flip a character in the ciphertext portion
      const tampered = encrypted.slice(0, -2) + 'ff';
      expect(() => decryptField(tampered, config)).toThrow();
    });

    it('rejects decryption with wrong key', () => {
      const encrypted = encryptField('secret', config);
      const wrongKey = crypto.randomBytes(32).toString('hex');
      const wrongConfig = { key: wrongKey, keyVersion: 1 };
      expect(() => decryptField(encrypted, wrongConfig)).toThrow();
    });
  });
});
