/**
 * Unit Tests for Encryption Utility
 * Tests AES-256-GCM encryption and decryption functions
 */

import { encrypt, decrypt, generateKey } from "@/lib/encryption";

describe("Encryption Utility", () => {
  describe("encrypt", () => {
    it("should encrypt plaintext and return hex-encoded string", () => {
      const plaintext = "sensitive-api-key-12345";
      const encrypted = encrypt(plaintext);

      // Check format: iv:authTag:encrypted (hex encoded)
      expect(encrypted).toMatch(/^[0-9a-f]+:[0-9a-f]+:[0-9a-f]+$/);

      const parts = encrypted.split(":");
      expect(parts).toHaveLength(3);

      // IV should be 32 hex chars (16 bytes)
      expect(parts[0]).toHaveLength(32);

      // Auth tag should be 32 hex chars (16 bytes)
      expect(parts[1]).toHaveLength(32);

      // Encrypted data length varies
      expect(parts[2].length).toBeGreaterThan(0);
    });

    it("should produce different ciphertexts for same plaintext (due to random IV)", () => {
      const plaintext = "test-data";
      const encrypted1 = encrypt(plaintext);
      const encrypted2 = encrypt(plaintext);

      expect(encrypted1).not.toBe(encrypted2);

      // But both should decrypt to same plaintext
      expect(decrypt(encrypted1)).toBe(plaintext);
      expect(decrypt(encrypted2)).toBe(plaintext);
    });

    it("should handle special characters", () => {
      const plaintext = 'key!@#$%^&*()_+-=[]{}|;:",.<>?/~`';
      const encrypted = encrypt(plaintext);
      expect(decrypt(encrypted)).toBe(plaintext);
    });

    it("should handle unicode characters", () => {
      const plaintext = "مرحبا بك في نظام مويدي 🔒🔑";
      const encrypted = encrypt(plaintext);
      expect(decrypt(encrypted)).toBe(plaintext);
    });

    it("should handle long strings", () => {
      const plaintext = "a".repeat(10000);
      const encrypted = encrypt(plaintext);
      expect(decrypt(encrypted)).toBe(plaintext);
    });
  });

  describe("decrypt", () => {
    it("should decrypt encrypted data correctly", () => {
      const plaintext = "my-secret-api-key";
      const encrypted = encrypt(plaintext);
      const decrypted = decrypt(encrypted);

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

    it("should handle whitespace in plaintext", () => {
      const plaintext = "  spaces  \n\t tabs \r\n newlines  ";
      const encrypted = encrypt(plaintext);
      expect(decrypt(encrypted)).toBe(plaintext);
    });

    it("should throw error for invalid format", () => {
      expect(() => decrypt("invalid-format")).toThrow(
        "Invalid encrypted data format",
      );
      expect(() => decrypt("only:two")).toThrow(
        "Invalid encrypted data format",
      );
      expect(() => decrypt(":::")).toThrow("Invalid encrypted data format");
    });

    it("should throw error for corrupted IV", () => {
      const plaintext = "test-data";
      const encrypted = encrypt(plaintext);
      const [iv, authTag, data] = encrypted.split(":");

      // Corrupt the IV
      const corrupted = `${iv.substring(0, 30)}XX:${authTag}:${data}`;

      expect(() => decrypt(corrupted)).toThrow();
    });

    it("should throw error for corrupted auth tag", () => {
      const plaintext = "test-data";
      const encrypted = encrypt(plaintext);
      const [iv, authTag, data] = encrypted.split(":");

      // Corrupt the auth tag (completely replace it)
      const corruptedAuthTag = authTag.split("").reverse().join(""); // Reverse the auth tag
      const corrupted = `${iv}:${corruptedAuthTag}:${data}`;

      expect(() => decrypt(corrupted)).toThrow();
    });

    it("should throw error for corrupted ciphertext", () => {
      const plaintext = "test-data";
      const encrypted = encrypt(plaintext);
      const [iv, authTag, data] = encrypted.split(":");

      // Corrupt the ciphertext
      const corrupted = `${iv}:${authTag}:${data.substring(0, data.length - 2)}XX`;

      expect(() => decrypt(corrupted)).toThrow();
    });

    it("should throw error for empty parts", () => {
      expect(() => decrypt("::data")).toThrow();
      expect(() => decrypt("iv::data")).toThrow();
      expect(() => decrypt("iv:tag:")).toThrow();
    });
  });

  describe("generateKey", () => {
    it("should generate 64 character hex string (32 bytes)", () => {
      const key = generateKey();
      expect(key).toHaveLength(64);
      expect(key).toMatch(/^[0-9a-f]{64}$/);
    });

    it("should generate unique keys", () => {
      const key1 = generateKey();
      const key2 = generateKey();
      const key3 = generateKey();

      expect(key1).not.toBe(key2);
      expect(key2).not.toBe(key3);
      expect(key1).not.toBe(key3);
    });

    it("should generate keys suitable for ENCRYPTION_KEY", () => {
      const key = generateKey();
      expect(key.length).toBe(64); // Required length for AES-256
    });
  });

  describe("Round-trip encryption/decryption", () => {
    const testCases = [
      { name: "API key", value: "sk-1234567890abcdef" },
      {
        name: "JSON string",
        value: JSON.stringify({ api: "key", secret: "value" }),
      },
      { name: "Email", value: "user@example.com" },
      { name: "URL", value: "https://example.com/webhook?token=abc123" },
      { name: "Arabic text", value: "مفتاح سري للنظام" },
      { name: "Mixed content", value: "User123!@#$%^مرحبا\n\ttab" },
      { name: "Numbers", value: "1234567890" },
      { name: "Base64-like", value: "YWJjZGVmZ2hpamtsbW5vcHFyc3R1dnd4eXo=" },
    ];

    testCases.forEach(({ name, value }) => {
      it(`should correctly encrypt and decrypt ${name}`, () => {
        const encrypted = encrypt(value);
        const decrypted = decrypt(encrypted);
        expect(decrypted).toBe(value);
      });
    });
  });

  describe("Security properties", () => {
    it("should use authenticated encryption (GCM)", () => {
      const plaintext = "test";
      const encrypted = encrypt(plaintext);
      const [iv, authTag, data] = encrypted.split(":");

      // Auth tag should be present and non-empty
      expect(authTag).toBeTruthy();
      expect(authTag.length).toBe(32); // 16 bytes in hex

      // Tamper detection: modifying ciphertext should fail
      const tampered = `${iv}:${authTag}:${data}ff`;
      expect(() => decrypt(tampered)).toThrow();
    });

    it("should produce unpredictable ciphertext", () => {
      const plaintext = "test";
      const encrypted1 = encrypt(plaintext);
      const encrypted2 = encrypt(plaintext);

      // Different IVs should be used
      const [iv1] = encrypted1.split(":");
      const [iv2] = encrypted2.split(":");
      expect(iv1).not.toBe(iv2);

      // Different ciphertexts
      expect(encrypted1).not.toBe(encrypted2);
    });

    it("should not leak plaintext length information excessively", () => {
      // Note: Block ciphers do leak some length info, but GCM is stream-like
      const short = encrypt("a");
      const long = encrypt("a".repeat(100));

      const shortData = short.split(":")[2];
      const longData = long.split(":")[2];

      // Length should scale with plaintext (in hex, 2 chars per byte)
      expect(longData.length).toBeGreaterThan(shortData.length);
    });
  });

  describe("Error handling", () => {
    it("should handle non-hex characters in encrypted data gracefully", () => {
      const invalidHex = "gggggggg:hhhhhhhh:iiiiiiii";
      expect(() => decrypt(invalidHex)).toThrow();
    });

    it("should handle truncated encrypted data", () => {
      const plaintext = "test";
      const encrypted = encrypt(plaintext);
      const truncated = encrypted.substring(0, 50);

      expect(() => decrypt(truncated)).toThrow();
    });
  });

  describe("Performance", () => {
    it("should encrypt/decrypt reasonably fast", () => {
      const plaintext = "performance-test-key-12345";
      const iterations = 1000;

      const startEncrypt = Date.now();
      for (let i = 0; i < iterations; i++) {
        encrypt(plaintext);
      }
      const encryptTime = Date.now() - startEncrypt;

      const encrypted = encrypt(plaintext);
      const startDecrypt = Date.now();
      for (let i = 0; i < iterations; i++) {
        decrypt(encrypted);
      }
      const decryptTime = Date.now() - startDecrypt;

      // Should complete 1000 operations in reasonable time (< 2 seconds each)
      expect(encryptTime).toBeLessThan(2000);
      expect(decryptTime).toBeLessThan(2000);
    });
  });
});
