import { describe, it, expect } from "vitest";
import { Cipher } from "./index.node";
import type { Encrypted } from "./types";

describe("Node Cipher", () => {
  const validKey =
    "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef";
  const validKey2 =
    "fedcba9876543210fedcba9876543210fedcba9876543210fedcba9876543210";
  const validKey3 =
    "abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789";

  describe("Constructor", () => {
    it("should create instance with valid key", () => {
      expect(() => new Cipher(validKey)).not.toThrow();
    });

    it("should throw on invalid hex string", () => {
      expect(() => new Cipher("not-a-hex-string")).toThrow();
    });

    it("should throw on empty string", () => {
      expect(() => new Cipher("")).toThrow();
    });

    it("should throw on non-string input", () => {
      // @ts-expect-error testing invalid input
      expect(() => new Cipher(123)).toThrow();
      // @ts-expect-error testing invalid input
      expect(() => new Cipher(null)).toThrow();
      // @ts-expect-error testing invalid input
      expect(() => new Cipher(undefined)).toThrow();
    });
  });

  describe("Encryption", () => {
    const cipher = new Cipher(validKey);

    it("should encrypt string to valid format", async () => {
      const encrypted = await cipher.encrypt("test");
      expect(encrypted).toEqual({
        iv: expect.stringMatching(/^[0-9a-f]{24}$/), // 12 bytes in hex
        content: expect.stringMatching(/^[0-9a-f]+$/), // hex string
        authTag: expect.stringMatching(/^[0-9a-f]{32}$/), // 16 bytes in hex
      });
    });

    it("should handle empty string", async () => {
      const encrypted = await cipher.encrypt("");
      expect(encrypted.content).not.toBe("");
      const decrypted = await cipher.decrypt(encrypted);
      expect(decrypted).toBe("");
    });

    it("should handle unicode characters", async () => {
      const testCases = ["Hello, 世界!", "🌍🌎🌏", "Γεια σας", "مرحبا"];
      for (const test of testCases) {
        const encrypted = await cipher.encrypt(test);
        const decrypted = await cipher.decrypt(encrypted);
        expect(decrypted).toBe(test);
      }
    });

    it("should handle long strings", async () => {
      const longString = "a".repeat(1000000); // 1MB string
      const encrypted = await cipher.encrypt(longString);
      const decrypted = await cipher.decrypt(encrypted);
      expect(decrypted).toBe(longString);
    });

    it("should produce different ciphertexts for same input", async () => {
      const input = "test";
      const encrypted1 = await cipher.encrypt(input);
      const encrypted2 = await cipher.encrypt(input);

      // IVs should be different
      expect(encrypted1.iv).not.toBe(encrypted2.iv);
      // Content should be different due to different IVs
      expect(encrypted1.content).not.toBe(encrypted2.content);
      // Auth tags should be different
      expect(encrypted1.authTag).not.toBe(encrypted2.authTag);

      // Both should decrypt to the same value
      const decrypted1 = await cipher.decrypt(encrypted1);
      const decrypted2 = await cipher.decrypt(encrypted2);
      expect(decrypted1).toBe(input);
      expect(decrypted2).toBe(input);
    });
  });

  describe("Decryption", () => {
    const cipher = new Cipher(validKey);

    it("should decrypt previously encrypted data", async () => {
      const input = "test";
      const encrypted = await cipher.encrypt(input);
      const decrypted = await cipher.decrypt(encrypted);
      expect(decrypted).toBe(input);
    });

    it("should fail with wrong key", async () => {
      const cipher1 = new Cipher(validKey);
      const cipher2 = new Cipher(validKey2);

      const encrypted = await cipher1.encrypt("test");
      await expect(cipher2.decrypt(encrypted)).rejects.toThrow();
    });

    it("should fail with tampered IV", async () => {
      const encrypted = await cipher.encrypt("test");
      const tampered: Encrypted = {
        ...encrypted,
        iv: "a".repeat(24), // same length, different value
      };
      await expect(cipher.decrypt(tampered)).rejects.toThrow();
    });

    it("should fail with tampered content", async () => {
      const encrypted = await cipher.encrypt("test");
      const tampered: Encrypted = {
        ...encrypted,
        content: encrypted.content + "00", // append extra bytes
      };
      await expect(cipher.decrypt(tampered)).rejects.toThrow();
    });

    it("should fail with tampered auth tag", async () => {
      const encrypted = await cipher.encrypt("test");
      const tampered: Encrypted = {
        ...encrypted,
        authTag: "a".repeat(32), // same length, different value
      };
      await expect(cipher.decrypt(tampered)).rejects.toThrow();
    });

    it("should fail with truncated content", async () => {
      const encrypted = await cipher.encrypt("test");
      const tampered: Encrypted = {
        ...encrypted,
        content: encrypted.content.slice(0, -2), // remove last byte
      };
      await expect(cipher.decrypt(tampered)).rejects.toThrow();
    });

    it("should fail with invalid hex in IV", async () => {
      const encrypted = await cipher.encrypt("test");
      const tampered: Encrypted = {
        ...encrypted,
        iv: "x".repeat(24), // invalid hex
      };
      await expect(cipher.decrypt(tampered)).rejects.toThrow();
    });

    it("should fail with invalid hex in content", async () => {
      const encrypted = await cipher.encrypt("test");
      const tampered: Encrypted = {
        ...encrypted,
        content: "x".repeat(encrypted.content.length), // invalid hex
      };
      await expect(cipher.decrypt(tampered)).rejects.toThrow();
    });

    it("should fail with invalid hex in auth tag", async () => {
      const encrypted = await cipher.encrypt("test");
      const tampered: Encrypted = {
        ...encrypted,
        authTag: "x".repeat(32), // invalid hex
      };
      await expect(cipher.decrypt(tampered)).rejects.toThrow();
    });
  });

  describe("Key Rotation", () => {
    const cipher1 = new Cipher(validKey);
    const cipher2 = new Cipher(validKey2);

    it("should create working rotator", async () => {
      const rotator = Cipher.createRotator(validKey, validKey2);
      const encrypted = await cipher1.encrypt("test");
      const rotated = await rotator(encrypted);

      // Should not be able to decrypt rotated data with old key
      await expect(cipher1.decrypt(rotated)).rejects.toThrow();

      // Should be able to decrypt with new key
      const decrypted = await cipher2.decrypt(rotated);
      expect(decrypted).toBe("test");
    });

    it("should handle multiple rotations", async () => {
      const cipher3 = new Cipher(validKey3);

      const rotator1 = Cipher.createRotator(validKey, validKey2);
      const rotator2 = Cipher.createRotator(validKey2, validKey3);

      const encrypted = await cipher1.encrypt("test");
      const rotated1 = await rotator1(encrypted);
      const rotated2 = await rotator2(rotated1);

      // Should not be able to decrypt with old keys
      await expect(cipher1.decrypt(rotated2)).rejects.toThrow();
      await expect(cipher2.decrypt(rotated2)).rejects.toThrow();

      // Should be able to decrypt with final key
      const decrypted = await cipher3.decrypt(rotated2);
      expect(decrypted).toBe("test");
    });

    it("should handle rotation of empty string", async () => {
      const rotator = Cipher.createRotator(validKey, validKey2);
      const encrypted = await cipher1.encrypt("");
      const rotated = await rotator(encrypted);
      const decrypted = await cipher2.decrypt(rotated);
      expect(decrypted).toBe("");
    });

    it("should handle rotation of long strings", async () => {
      const longString = "a".repeat(1000000); // 1MB string
      const rotator = Cipher.createRotator(validKey, validKey2);
      const encrypted = await cipher1.encrypt(longString);
      const rotated = await rotator(encrypted);
      const decrypted = await cipher2.decrypt(rotated);
      expect(decrypted).toBe(longString);
    });

    it("should fail rotation with invalid encrypted data", async () => {
      const rotator = Cipher.createRotator(validKey, validKey2);
      const encrypted = await cipher1.encrypt("test");
      const tampered: Encrypted = {
        ...encrypted,
        authTag: "a".repeat(32),
      };
      await expect(rotator(tampered)).rejects.toThrow();
    });
  });

  describe("Performance", () => {
    const cipher = new Cipher(validKey);

    it("should handle concurrent operations", async () => {
      const inputs = Array.from({ length: 100 }, (_, i) => `test${i}`);
      const encrypted = await Promise.all(
        inputs.map((input) => cipher.encrypt(input))
      );
      const decrypted = await Promise.all(
        encrypted.map((e) => cipher.decrypt(e))
      );
      expect(decrypted).toEqual(inputs);
    });

    it("should handle rapid sequential operations", async () => {
      const input = "test";
      for (let i = 0; i < 1000; i++) {
        const encrypted = await cipher.encrypt(input);
        const decrypted = await cipher.decrypt(encrypted);
        expect(decrypted).toBe(input);
      }
    });
  });
});
