/**
 * Crypto Service Tests
 */

import { describe, it, expect, beforeEach, vi } from "vitest";
import { Test, TestingModule } from "@nestjs/testing";
import { ConfigService } from "@nestjs/config";
import { CryptoService } from "./crypto.service";
import { Logger } from "@nestjs/common";

describe("CryptoService", () => {
  let service: CryptoService;

  // Valid 32-byte hex key for testing
  const testEncryptionKey = "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef";

  beforeEach(async () => {
    const module: TestingModule = await Test.createTestingModule({
      providers: [
        CryptoService,
        {
          provide: ConfigService,
          useValue: {
            get: (key: string) => {
              if (key === "ENCRYPTION_KEY") return testEncryptionKey;
              return undefined;
            },
          },
        },
      ],
    }).compile();

    service = module.get<CryptoService>(CryptoService);
  });

  describe("initialization", () => {
    it("should throw error if ENCRYPTION_KEY is missing", () => {
      expect(() => {
        new CryptoService({
          get: () => undefined,
        } as ConfigService);
      }).toThrow("ENCRYPTION_KEY environment variable is required");
    });

    it("should throw error if ENCRYPTION_KEY is invalid length", () => {
      expect(() => {
        new CryptoService({
          get: () => "invalid",
        } as ConfigService);
      }).toThrow("ENCRYPTION_KEY must be 64 hexadecimal characters");
    });

    it("should initialize successfully with valid key", () => {
      expect(service).toBeDefined();
    });
  });

  describe("encrypt", () => {
    it("should encrypt plaintext data", () => {
      // Arrange
      const plaintext = "sensitive data";

      // Act
      const encrypted = service.encrypt(plaintext);

      // Assert
      expect(encrypted).toBeDefined();
      expect(encrypted).not.toEqual(plaintext);
      expect(encrypted.split(":")).toHaveLength(3); // iv:authTag:encrypted
    });

    it("should produce different ciphertext for same plaintext", () => {
      // Arrange
      const plaintext = "sensitive data";

      // Act
      const encrypted1 = service.encrypt(plaintext);
      const encrypted2 = service.encrypt(plaintext);

      // Assert
      expect(encrypted1).not.toEqual(encrypted2); // Different IVs
    });

    it("should encrypt long data (RSA private key)", () => {
      // Arrange
      const longData =
        "-----BEGIN PRIVATE KEY-----\n" + "a".repeat(1000) + "\n-----END PRIVATE KEY-----";

      // Act
      const encrypted = service.encrypt(longData);

      // Assert
      expect(encrypted).toBeDefined();
      expect(encrypted.length).toBeGreaterThan(0);
    });
  });

  describe("decrypt", () => {
    it("should decrypt encrypted data", () => {
      // Arrange
      const plaintext = "sensitive data";
      const encrypted = service.encrypt(plaintext);

      // Act
      const decrypted = service.decrypt(encrypted);

      // Assert
      expect(decrypted).toEqual(plaintext);
    });

    it("should decrypt long data", () => {
      // Arrange
      const longData =
        "-----BEGIN PRIVATE KEY-----\n" + "a".repeat(1000) + "\n-----END PRIVATE KEY-----";
      const encrypted = service.encrypt(longData);

      // Act
      const decrypted = service.decrypt(encrypted);

      // Assert
      expect(decrypted).toEqual(longData);
    });

    it("should throw error for invalid encrypted data format", () => {
      // Arrange
      const invalidData = "invalid:format";

      // Act & Assert
      expect(() => service.decrypt(invalidData)).toThrow("Failed to decrypt data");
    });

    it("should throw error for corrupted data", () => {
      // Arrange
      const plaintext = "sensitive data";
      const encrypted = service.encrypt(plaintext);
      const corrupted = encrypted.replace(/[0-9a-f]/, "x"); // Corrupt one character

      // Act & Assert
      expect(() => service.decrypt(corrupted)).toThrow("Failed to decrypt data");
    });

    it("should not log sensitive data in error messages", () => {
      // Arrange
      const loggerErrorSpy = vi.spyOn(Logger.prototype, "error");
      const corruptedData = "corrupted:data:here";

      // Act & Assert
      expect(() => service.decrypt(corruptedData)).toThrow("Failed to decrypt data");

      // Verify logger was called with safe message
      expect(loggerErrorSpy).toHaveBeenCalled();
      const logCall = loggerErrorSpy.mock.calls[0];

      // First argument should contain error type but not sensitive data
      expect(logCall[0]).toMatch(/Decryption failed:/);

      // Should NOT log the actual error object with stack traces
      expect(logCall.length).toBe(1); // Only one argument (the message)

      // Verify the corrupted data is not in the log
      const logMessage = logCall[0] as string;
      expect(logMessage).not.toContain(corruptedData);

      loggerErrorSpy.mockRestore();
    });
  });

  describe("encrypt/decrypt round-trip", () => {
    it("should maintain data integrity through encrypt-decrypt cycle", () => {
      // Arrange
      const testCases = [
        "short",
        "medium length string with special chars !@#$%",
        "-----BEGIN PRIVATE KEY-----\nMIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQC\n-----END PRIVATE KEY-----",
        JSON.stringify({ complex: "object", with: ["arrays", 123] }),
      ];

      testCases.forEach((plaintext) => {
        // Act
        const encrypted = service.encrypt(plaintext);
        const decrypted = service.decrypt(encrypted);

        // Assert
        expect(decrypted).toEqual(plaintext);
      });
    });
  });
});
