/**
 * Unit tests for API Key Management Service
 */

import { apiKeyManagementService } from "@/lib/services/api-key-management.service";
import { prisma } from "@/lib/db";
import { encrypt, decrypt } from "@/lib/encryption";

// Mock dependencies
jest.mock("@/lib/db");

jest.mock("@/lib/supabase", () => ({
  supabaseAdmin: null, // Disable Supabase in tests
}));

jest.mock("@/lib/logger", () => ({
  logger: {
    error: jest.fn(),
    info: jest.fn(),
    warn: jest.fn(),
  },
}));

// Don't mock encryption - test it for real
jest.unmock("@/lib/encryption");

describe("APIKeyManagementService", () => {
  const mockUserId = "user_123";
  const mockApiKey = "sk-test-1234567890abcdef";
  const mockService = "n8n";
  const mockKeyName = "Production API Key";

  beforeEach(() => {
    jest.clearAllMocks();
  });

  describe("createAPIKey", () => {
    it("should encrypt and store API key, returning plaintext once", async () => {
      const mockCreatedKey = {
        id: "key_123",
        userId: mockUserId,
        service: mockService,
        keyName: mockKeyName,
        encryptedKey: "encrypted_data",
        isActive: true,
        expiresAt: null,
        lastUsedAt: null,
        createdAt: new Date("2025-01-01"),
        updatedAt: new Date("2025-01-01"),
      };

      (prisma.userAPIKey.create as jest.Mock).mockResolvedValue(mockCreatedKey);

      const result = await apiKeyManagementService.createAPIKey(mockUserId, {
        service: mockService,
        keyName: mockKeyName,
        apiKey: mockApiKey,
      });

      // Verify key was created in database
      expect(prisma.userAPIKey.create).toHaveBeenCalledWith({
        data: {
          userId: mockUserId,
          service: mockService,
          keyName: mockKeyName,
          encryptedKey: expect.any(String),
          expiresAt: undefined,
          isActive: true,
        },
      });

      // Verify encrypted key is different from plaintext
      const createCall = (prisma.userAPIKey.create as jest.Mock).mock
        .calls[0][0];
      expect(createCall.data.encryptedKey).not.toBe(mockApiKey);

      // Verify plaintext is returned
      expect(result.plaintextKey).toBe(mockApiKey);

      // Verify response does not include encrypted key
      expect(result.key).toEqual({
        id: "key_123",
        service: mockService,
        keyName: mockKeyName,
        isActive: true,
        expiresAt: undefined,
        createdAt: mockCreatedKey.createdAt,
        lastUsedAt: undefined,
      });
    });

    it("should create API key with expiration date", async () => {
      const expiresAt = new Date("2025-12-31");
      const mockCreatedKey = {
        id: "key_123",
        userId: mockUserId,
        service: mockService,
        keyName: mockKeyName,
        encryptedKey: "encrypted_data",
        isActive: true,
        expiresAt,
        lastUsedAt: null,
        createdAt: new Date(),
        updatedAt: new Date(),
      };

      (prisma.userAPIKey.create as jest.Mock).mockResolvedValue(mockCreatedKey);

      const result = await apiKeyManagementService.createAPIKey(mockUserId, {
        service: mockService,
        keyName: mockKeyName,
        apiKey: mockApiKey,
        expiresAt,
      });

      expect(result.key.expiresAt).toEqual(expiresAt);
    });

    it("should enforce unique constraint on (userId, service, keyName)", async () => {
      const error = new Error("Unique constraint failed");
      (error as any).code = "P2002";
      (prisma.userAPIKey.create as jest.Mock).mockRejectedValue(error);

      await expect(
        apiKeyManagementService.createAPIKey(mockUserId, {
          service: mockService,
          keyName: mockKeyName,
          apiKey: mockApiKey,
        }),
      ).rejects.toThrow("An API key with the same name already exists");
    });
  });

  describe("getUserAPIKeys", () => {
    it("should return all API keys for a user without encrypted data", async () => {
      const mockKeys = [
        {
          id: "key_1",
          userId: mockUserId,
          service: "n8n",
          keyName: "Key 1",
          encryptedKey: "encrypted_1",
          isActive: true,
          expiresAt: null,
          lastUsedAt: new Date("2025-01-15"),
          createdAt: new Date("2025-01-01"),
          updatedAt: new Date("2025-01-01"),
        },
        {
          id: "key_2",
          userId: mockUserId,
          service: "whatsapp",
          keyName: "Key 2",
          encryptedKey: "encrypted_2",
          isActive: false,
          expiresAt: new Date("2025-12-31"),
          lastUsedAt: null,
          createdAt: new Date("2025-01-02"),
          updatedAt: new Date("2025-01-02"),
        },
      ];

      (prisma.userAPIKey.findMany as jest.Mock).mockResolvedValue(mockKeys);

      const result = await apiKeyManagementService.getUserAPIKeys(mockUserId);

      expect(prisma.userAPIKey.findMany).toHaveBeenCalledWith({
        where: { userId: mockUserId },
        orderBy: { createdAt: "desc" },
      });

      expect(result).toHaveLength(2);
      expect(result[0]).toEqual({
        id: "key_1",
        service: "n8n",
        keyName: "Key 1",
        isActive: true,
        expiresAt: undefined,
        lastUsedAt: mockKeys[0].lastUsedAt,
        createdAt: mockKeys[0].createdAt,
      });

      // Verify encrypted keys are not included
      expect((result[0] as any).encryptedKey).toBeUndefined();
      expect((result[1] as any).encryptedKey).toBeUndefined();
    });

    it("should return empty array when user has no API keys", async () => {
      (prisma.userAPIKey.findMany as jest.Mock).mockResolvedValue([]);

      const result = await apiKeyManagementService.getUserAPIKeys(mockUserId);

      expect(result).toEqual([]);
    });
  });

  describe("getDecryptedAPIKey", () => {
    it("should return decrypted key for active service", async () => {
      // Encrypt a real key for testing
      const encryptedKey = encrypt(mockApiKey);

      const mockKey = {
        id: "key_123",
        userId: mockUserId,
        service: mockService,
        keyName: mockKeyName,
        encryptedKey,
        isActive: true,
        expiresAt: null,
        lastUsedAt: null,
        createdAt: new Date(),
        updatedAt: new Date(),
      };

      (prisma.userAPIKey.findFirst as jest.Mock).mockResolvedValue(mockKey);
      (prisma.userAPIKey.update as jest.Mock).mockResolvedValue({
        ...mockKey,
        lastUsedAt: new Date(),
      });

      const result = await apiKeyManagementService.getDecryptedAPIKey(
        mockUserId,
        mockService,
      );

      expect(result).toBe(mockApiKey);
      expect(prisma.userAPIKey.findFirst).toHaveBeenCalledWith({
        where: { userId: mockUserId, service: mockService, isActive: true },
        orderBy: { createdAt: "desc" },
      });
    });

    it("should update lastUsedAt when key is retrieved", async () => {
      const encryptedKey = encrypt(mockApiKey);
      const mockKey = {
        id: "key_123",
        userId: mockUserId,
        service: mockService,
        keyName: mockKeyName,
        encryptedKey,
        isActive: true,
        expiresAt: null,
        lastUsedAt: null,
        createdAt: new Date(),
        updatedAt: new Date(),
      };

      (prisma.userAPIKey.findFirst as jest.Mock).mockResolvedValue(mockKey);
      (prisma.userAPIKey.update as jest.Mock).mockResolvedValue({
        ...mockKey,
        lastUsedAt: new Date(),
      });

      await apiKeyManagementService.getDecryptedAPIKey(mockUserId, mockService);

      // Give async update time to be called
      await new Promise((resolve) => setTimeout(resolve, 10));

      expect(prisma.userAPIKey.update).toHaveBeenCalledWith({
        where: { id: "key_123" },
        data: { lastUsedAt: expect.any(Date) },
      });
    });

    it("should return null for non-existent service", async () => {
      (prisma.userAPIKey.findFirst as jest.Mock).mockResolvedValue(null);

      const result = await apiKeyManagementService.getDecryptedAPIKey(
        mockUserId,
        "non_existent_service",
      );

      expect(result).toBeNull();
    });

    it("should return null for expired keys", async () => {
      const expiredDate = new Date("2020-01-01");
      const encryptedKey = encrypt(mockApiKey);

      const mockKey = {
        id: "key_123",
        userId: mockUserId,
        service: mockService,
        keyName: mockKeyName,
        encryptedKey,
        isActive: true,
        expiresAt: expiredDate,
        lastUsedAt: null,
        createdAt: new Date(),
        updatedAt: new Date(),
      };

      (prisma.userAPIKey.findFirst as jest.Mock).mockResolvedValue(mockKey);

      const result = await apiKeyManagementService.getDecryptedAPIKey(
        mockUserId,
        mockService,
      );

      expect(result).toBeNull();
    });

    it("should return key when expiration date is in the future", async () => {
      const futureDate = new Date("2030-01-01");
      const encryptedKey = encrypt(mockApiKey);

      const mockKey = {
        id: "key_123",
        userId: mockUserId,
        service: mockService,
        keyName: mockKeyName,
        encryptedKey,
        isActive: true,
        expiresAt: futureDate,
        lastUsedAt: null,
        createdAt: new Date(),
        updatedAt: new Date(),
      };

      (prisma.userAPIKey.findFirst as jest.Mock).mockResolvedValue(mockKey);
      (prisma.userAPIKey.update as jest.Mock).mockResolvedValue(mockKey);

      const result = await apiKeyManagementService.getDecryptedAPIKey(
        mockUserId,
        mockService,
      );

      expect(result).toBe(mockApiKey);
    });
  });

  describe("rotateAPIKey", () => {
    it("should update encrypted key with new value", async () => {
      const oldKey = {
        id: "key_123",
        userId: mockUserId,
        service: mockService,
        keyName: mockKeyName,
        encryptedKey: encrypt("old_key"),
        isActive: true,
        expiresAt: null,
        lastUsedAt: null,
        createdAt: new Date(),
        updatedAt: new Date(),
      };

      const newApiKey = "sk-new-1234567890abcdef";

      (prisma.userAPIKey.findUnique as jest.Mock).mockResolvedValue(oldKey);
      (prisma.userAPIKey.update as jest.Mock).mockResolvedValue({
        ...oldKey,
        encryptedKey: encrypt(newApiKey),
        updatedAt: new Date(),
      });

      const result = await apiKeyManagementService.rotateAPIKey(
        "key_123",
        mockUserId,
        newApiKey,
      );

      expect(prisma.userAPIKey.update).toHaveBeenCalledWith({
        where: { id: "key_123" },
        data: { encryptedKey: expect.any(String) },
      });

      // Verify the encrypted key changed
      const updateCall = (prisma.userAPIKey.update as jest.Mock).mock
        .calls[0][0];
      expect(updateCall.data.encryptedKey).not.toBe(oldKey.encryptedKey);
      expect(updateCall.data.encryptedKey).not.toBe(newApiKey);

      expect(result.id).toBe("key_123");
    });

    it("should throw error when key does not exist", async () => {
      (prisma.userAPIKey.findUnique as jest.Mock).mockResolvedValue(null);

      await expect(
        apiKeyManagementService.rotateAPIKey("key_123", mockUserId, "new_key"),
      ).rejects.toThrow("API key not found or access denied");
    });

    it("should throw error when user does not own the key", async () => {
      const otherUserKey = {
        id: "key_123",
        userId: "other_user",
        service: mockService,
        keyName: mockKeyName,
        encryptedKey: encrypt("key"),
        isActive: true,
        expiresAt: null,
        lastUsedAt: null,
        createdAt: new Date(),
        updatedAt: new Date(),
      };

      (prisma.userAPIKey.findUnique as jest.Mock).mockResolvedValue(
        otherUserKey,
      );

      await expect(
        apiKeyManagementService.rotateAPIKey("key_123", mockUserId, "new_key"),
      ).rejects.toThrow("API key not found or access denied");
    });
  });

  describe("deactivateAPIKey", () => {
    it("should set isActive to false", async () => {
      const mockKey = {
        id: "key_123",
        userId: mockUserId,
        service: mockService,
        keyName: mockKeyName,
        encryptedKey: encrypt(mockApiKey),
        isActive: true,
        expiresAt: null,
        lastUsedAt: null,
        createdAt: new Date(),
        updatedAt: new Date(),
      };

      (prisma.userAPIKey.findUnique as jest.Mock).mockResolvedValue(mockKey);
      (prisma.userAPIKey.update as jest.Mock).mockResolvedValue({
        ...mockKey,
        isActive: false,
      });

      await apiKeyManagementService.deactivateAPIKey("key_123", mockUserId);

      expect(prisma.userAPIKey.update).toHaveBeenCalledWith({
        where: { id: "key_123" },
        data: { isActive: false },
      });
    });

    it("should throw error when key does not exist", async () => {
      (prisma.userAPIKey.findUnique as jest.Mock).mockResolvedValue(null);

      await expect(
        apiKeyManagementService.deactivateAPIKey("key_123", mockUserId),
      ).rejects.toThrow("API key not found or access denied");
    });

    it("should throw error when user does not own the key", async () => {
      const otherUserKey = {
        id: "key_123",
        userId: "other_user",
        service: mockService,
        keyName: mockKeyName,
        encryptedKey: encrypt(mockApiKey),
        isActive: true,
        expiresAt: null,
        lastUsedAt: null,
        createdAt: new Date(),
        updatedAt: new Date(),
      };

      (prisma.userAPIKey.findUnique as jest.Mock).mockResolvedValue(
        otherUserKey,
      );

      await expect(
        apiKeyManagementService.deactivateAPIKey("key_123", mockUserId),
      ).rejects.toThrow("API key not found or access denied");
    });
  });

  describe("deleteAPIKey", () => {
    it("should permanently delete the API key", async () => {
      const mockKey = {
        id: "key_123",
        userId: mockUserId,
        service: mockService,
        keyName: mockKeyName,
        encryptedKey: encrypt(mockApiKey),
        isActive: true,
        expiresAt: null,
        lastUsedAt: null,
        createdAt: new Date(),
        updatedAt: new Date(),
      };

      (prisma.userAPIKey.findUnique as jest.Mock).mockResolvedValue(mockKey);
      (prisma.userAPIKey.delete as jest.Mock).mockResolvedValue(mockKey);

      await apiKeyManagementService.deleteAPIKey("key_123", mockUserId);

      expect(prisma.userAPIKey.delete).toHaveBeenCalledWith({
        where: { id: "key_123" },
      });
    });

    it("should throw error when key does not exist", async () => {
      (prisma.userAPIKey.findUnique as jest.Mock).mockResolvedValue(null);

      await expect(
        apiKeyManagementService.deleteAPIKey("key_123", mockUserId),
      ).rejects.toThrow("API key not found or access denied");
    });

    it("should throw error when user does not own the key", async () => {
      const otherUserKey = {
        id: "key_123",
        userId: "other_user",
        service: mockService,
        keyName: mockKeyName,
        encryptedKey: encrypt(mockApiKey),
        isActive: true,
        expiresAt: null,
        lastUsedAt: null,
        createdAt: new Date(),
        updatedAt: new Date(),
      };

      (prisma.userAPIKey.findUnique as jest.Mock).mockResolvedValue(
        otherUserKey,
      );

      await expect(
        apiKeyManagementService.deleteAPIKey("key_123", mockUserId),
      ).rejects.toThrow("API key not found or access denied");
    });
  });

  describe("isKeyExpired", () => {
    it("should return false when key has no expiration date", () => {
      const result = apiKeyManagementService.isKeyExpired({ expiresAt: null });
      expect(result).toBe(false);
    });

    it("should return false when expiration date is in the future", () => {
      const futureDate = new Date("2030-01-01");
      const result = apiKeyManagementService.isKeyExpired({
        expiresAt: futureDate,
      });
      expect(result).toBe(false);
    });

    it("should return true when expiration date is in the past", () => {
      const pastDate = new Date("2020-01-01");
      const result = apiKeyManagementService.isKeyExpired({
        expiresAt: pastDate,
      });
      expect(result).toBe(true);
    });
  });
});
