import { describe, it, expect, beforeEach, vi } from "vitest";
import { Test, TestingModule } from "@nestjs/testing";
import { CredentialsService } from "./credentials.service";
import { PrismaService } from "../prisma/prisma.service";
import { ActivityService } from "../activity/activity.service";
import { VaultService } from "../vault/vault.service";
import { CredentialType, CredentialScope } from "@prisma/client";
import { ActivityAction, EntityType } from "@prisma/client";
import { NotFoundException, BadRequestException } from "@nestjs/common";
import { TransitKey } from "../vault/vault.constants";

describe("CredentialsService", () => {
  let service: CredentialsService;
  let prisma: PrismaService;
  let activityService: ActivityService;
  let vaultService: VaultService;

  const mockPrismaService = {
    userCredential: {
      create: vi.fn(),
      findMany: vi.fn(),
      count: vi.fn(),
      findUnique: vi.fn(),
      update: vi.fn(),
      delete: vi.fn(),
    },
    activityLog: {
      findMany: vi.fn(),
      count: vi.fn(),
    },
  };

  const mockActivityService = {
    logActivity: vi.fn(),
  };

  const mockVaultService = {
    encrypt: vi.fn(),
    decrypt: vi.fn(),
  };

  const mockWorkspaceId = "550e8400-e29b-41d4-a716-446655440001";
  const mockUserId = "550e8400-e29b-41d4-a716-446655440002";
  const mockCredentialId = "550e8400-e29b-41d4-a716-446655440003";

  const mockCredential = {
    id: mockCredentialId,
    userId: mockUserId,
    workspaceId: mockWorkspaceId,
    name: "GitHub Token",
    provider: "github",
    type: CredentialType.API_KEY,
    scope: CredentialScope.USER,
    encryptedValue: "vault:v1:encrypted-data-here",
    maskedValue: "****3456",
    description: "My GitHub API key",
    expiresAt: null,
    lastUsedAt: null,
    metadata: {},
    isActive: true,
    rotatedAt: null,
    createdAt: new Date(),
    updatedAt: new Date(),
  };

  beforeEach(async () => {
    const module: TestingModule = await Test.createTestingModule({
      providers: [
        CredentialsService,
        {
          provide: PrismaService,
          useValue: mockPrismaService,
        },
        {
          provide: ActivityService,
          useValue: mockActivityService,
        },
        {
          provide: VaultService,
          useValue: mockVaultService,
        },
      ],
    }).compile();

    service = module.get<CredentialsService>(CredentialsService);
    prisma = module.get<PrismaService>(PrismaService);
    activityService = module.get<ActivityService>(ActivityService);
    vaultService = module.get<VaultService>(VaultService);

    // Clear all mocks before each test
    vi.clearAllMocks();
  });

  it("should be defined", () => {
    expect(service).toBeDefined();
  });

  describe("create", () => {
    it("should create a credential with encrypted value and log activity", async () => {
      const createDto = {
        name: "GitHub Token",
        provider: "github",
        type: CredentialType.API_KEY,
        value: "ghp_1234567890abcdef",
        description: "My GitHub API key",
      };

      mockVaultService.encrypt.mockResolvedValue("vault:v1:encrypted-data-here");
      mockPrismaService.userCredential.create.mockResolvedValue(mockCredential);
      mockActivityService.logActivity.mockResolvedValue({});

      const result = await service.create(mockWorkspaceId, mockUserId, createDto);

      expect(result).toEqual(mockCredential);
      expect(vaultService.encrypt).toHaveBeenCalledWith(createDto.value, TransitKey.CREDENTIALS);
      expect(prisma.userCredential.create).toHaveBeenCalledWith({
        data: {
          userId: mockUserId,
          workspaceId: mockWorkspaceId,
          name: createDto.name,
          provider: createDto.provider,
          type: createDto.type,
          scope: CredentialScope.USER,
          encryptedValue: "vault:v1:encrypted-data-here",
          maskedValue: "****cdef",
          description: createDto.description,
          expiresAt: null,
          metadata: {},
        },
        select: expect.any(Object),
      });
      expect(activityService.logActivity).toHaveBeenCalledWith({
        workspaceId: mockWorkspaceId,
        userId: mockUserId,
        action: ActivityAction.CREDENTIAL_CREATED,
        entityType: EntityType.CREDENTIAL,
        entityId: mockCredential.id,
        details: {
          name: createDto.name,
          provider: createDto.provider,
          type: createDto.type,
        },
      });
    });

    it("should create credential with custom scope", async () => {
      const createDto = {
        name: "Workspace API Key",
        provider: "custom",
        type: CredentialType.API_KEY,
        scope: CredentialScope.WORKSPACE,
        value: "ws_key_123",
      };

      mockVaultService.encrypt.mockResolvedValue("vault:v1:encrypted");
      mockPrismaService.userCredential.create.mockResolvedValue({
        ...mockCredential,
        scope: CredentialScope.WORKSPACE,
      });

      await service.create(mockWorkspaceId, mockUserId, createDto);

      expect(prisma.userCredential.create).toHaveBeenCalledWith(
        expect.objectContaining({
          data: expect.objectContaining({
            scope: CredentialScope.WORKSPACE,
          }),
        })
      );
    });

    it("should generate maskedValue from last 4 characters", async () => {
      const createDto = {
        name: "Short",
        provider: "test",
        type: CredentialType.SECRET,
        value: "abc",
      };

      mockVaultService.encrypt.mockResolvedValue("vault:v1:encrypted");
      mockPrismaService.userCredential.create.mockResolvedValue(mockCredential);

      await service.create(mockWorkspaceId, mockUserId, createDto);

      expect(prisma.userCredential.create).toHaveBeenCalledWith(
        expect.objectContaining({
          data: expect.objectContaining({
            maskedValue: "****abc",
          }),
        })
      );
    });
  });

  describe("findAll", () => {
    it("should return paginated credentials without encryptedValue", async () => {
      const query = { page: 1, limit: 10 };
      const credentials = [mockCredential];

      mockPrismaService.userCredential.findMany.mockResolvedValue(credentials);
      mockPrismaService.userCredential.count.mockResolvedValue(1);

      const result = await service.findAll(mockWorkspaceId, mockUserId, query);

      expect(result).toEqual({
        data: credentials,
        meta: {
          total: 1,
          page: 1,
          limit: 10,
          totalPages: 1,
        },
      });
      expect(prisma.userCredential.findMany).toHaveBeenCalledWith({
        where: {
          userId: mockUserId,
          workspaceId: mockWorkspaceId,
        },
        select: {
          id: true,
          userId: true,
          workspaceId: true,
          name: true,
          provider: true,
          type: true,
          scope: true,
          maskedValue: true,
          description: true,
          expiresAt: true,
          lastUsedAt: true,
          metadata: true,
          isActive: true,
          rotatedAt: true,
          createdAt: true,
          updatedAt: true,
        },
        orderBy: { createdAt: "desc" },
        skip: 0,
        take: 10,
      });
    });

    it("should filter by type", async () => {
      const query = { type: CredentialType.API_KEY };

      mockPrismaService.userCredential.findMany.mockResolvedValue([]);
      mockPrismaService.userCredential.count.mockResolvedValue(0);

      await service.findAll(mockWorkspaceId, mockUserId, query);

      expect(prisma.userCredential.findMany).toHaveBeenCalledWith(
        expect.objectContaining({
          where: expect.objectContaining({
            type: { in: [CredentialType.API_KEY] },
          }),
        })
      );
    });

    it("should filter by isActive", async () => {
      const query = { isActive: true };

      mockPrismaService.userCredential.findMany.mockResolvedValue([]);
      mockPrismaService.userCredential.count.mockResolvedValue(0);

      await service.findAll(mockWorkspaceId, mockUserId, query);

      expect(prisma.userCredential.findMany).toHaveBeenCalledWith(
        expect.objectContaining({
          where: expect.objectContaining({
            isActive: true,
          }),
        })
      );
    });
  });

  describe("findOne", () => {
    it("should return a single credential without encryptedValue", async () => {
      mockPrismaService.userCredential.findUnique.mockResolvedValue(mockCredential);

      const result = await service.findOne(mockCredentialId, mockWorkspaceId, mockUserId);

      expect(result).toEqual(mockCredential);
      expect(prisma.userCredential.findUnique).toHaveBeenCalledWith({
        where: {
          id: mockCredentialId,
          userId: mockUserId,
          workspaceId: mockWorkspaceId,
        },
        select: expect.objectContaining({
          id: true,
          maskedValue: true,
        }),
      });
    });

    it("should throw NotFoundException when credential not found", async () => {
      mockPrismaService.userCredential.findUnique.mockResolvedValue(null);

      await expect(service.findOne(mockCredentialId, mockWorkspaceId, mockUserId)).rejects.toThrow(
        NotFoundException
      );
    });
  });

  describe("getValue", () => {
    it("should decrypt and return credential value, update lastUsedAt, and log access", async () => {
      mockPrismaService.userCredential.findUnique.mockResolvedValue(mockCredential);
      mockVaultService.decrypt.mockResolvedValue("ghp_1234567890abcdef");
      mockPrismaService.userCredential.update.mockResolvedValue(mockCredential);
      mockActivityService.logActivity.mockResolvedValue({});

      const result = await service.getValue(mockCredentialId, mockWorkspaceId, mockUserId);

      expect(result).toEqual({ value: "ghp_1234567890abcdef" });
      expect(vaultService.decrypt).toHaveBeenCalledWith(
        mockCredential.encryptedValue,
        TransitKey.CREDENTIALS
      );
      expect(prisma.userCredential.update).toHaveBeenCalledWith({
        where: { id: mockCredentialId },
        data: { lastUsedAt: expect.any(Date) },
      });
      expect(activityService.logActivity).toHaveBeenCalledWith({
        workspaceId: mockWorkspaceId,
        userId: mockUserId,
        action: ActivityAction.CREDENTIAL_ACCESSED,
        entityType: EntityType.CREDENTIAL,
        entityId: mockCredentialId,
        details: {
          name: mockCredential.name,
          provider: mockCredential.provider,
        },
      });
    });

    it("should throw NotFoundException when credential not found", async () => {
      mockPrismaService.userCredential.findUnique.mockResolvedValue(null);

      await expect(service.getValue(mockCredentialId, mockWorkspaceId, mockUserId)).rejects.toThrow(
        NotFoundException
      );
    });

    it("should throw BadRequestException when credential is not active", async () => {
      mockPrismaService.userCredential.findUnique.mockResolvedValue({
        ...mockCredential,
        isActive: false,
      });

      await expect(service.getValue(mockCredentialId, mockWorkspaceId, mockUserId)).rejects.toThrow(
        BadRequestException
      );
    });
  });

  describe("update", () => {
    it("should update credential metadata and log activity", async () => {
      const updateDto = {
        description: "Updated description",
      };

      mockPrismaService.userCredential.findUnique.mockResolvedValue(mockCredential);
      mockPrismaService.userCredential.update.mockResolvedValue({
        ...mockCredential,
        ...updateDto,
      });
      mockActivityService.logActivity.mockResolvedValue({});

      const result = await service.update(mockCredentialId, mockWorkspaceId, mockUserId, updateDto);

      expect(result.description).toBe(updateDto.description);
      expect(prisma.userCredential.update).toHaveBeenCalledWith({
        where: { id: mockCredentialId },
        data: updateDto,
        select: expect.any(Object),
      });
      expect(activityService.logActivity).toHaveBeenCalledWith({
        workspaceId: mockWorkspaceId,
        userId: mockUserId,
        action: ActivityAction.UPDATED,
        entityType: EntityType.CREDENTIAL,
        entityId: mockCredentialId,
        details: {
          description: updateDto.description,
          expiresAt: undefined,
          isActive: undefined,
        },
      });
    });

    it("should throw NotFoundException when credential not found", async () => {
      mockPrismaService.userCredential.findUnique.mockResolvedValue(null);

      await expect(
        service.update(mockCredentialId, mockWorkspaceId, mockUserId, { description: "test" })
      ).rejects.toThrow(NotFoundException);
    });
  });

  describe("rotate", () => {
    it("should rotate credential value and log activity", async () => {
      const newValue = "ghp_new_token_12345";

      mockPrismaService.userCredential.findUnique.mockResolvedValue(mockCredential);
      mockVaultService.encrypt.mockResolvedValue("vault:v1:new-encrypted");
      mockPrismaService.userCredential.update.mockResolvedValue({
        ...mockCredential,
        encryptedValue: "vault:v1:new-encrypted",
        maskedValue: "****2345",
        rotatedAt: new Date(),
      });
      mockActivityService.logActivity.mockResolvedValue({});

      const result = await service.rotate(mockCredentialId, mockWorkspaceId, mockUserId, newValue);

      expect(vaultService.encrypt).toHaveBeenCalledWith(newValue, TransitKey.CREDENTIALS);
      expect(prisma.userCredential.update).toHaveBeenCalledWith({
        where: { id: mockCredentialId },
        data: {
          encryptedValue: "vault:v1:new-encrypted",
          maskedValue: "****2345",
          rotatedAt: expect.any(Date),
        },
        select: expect.any(Object),
      });
      expect(activityService.logActivity).toHaveBeenCalledWith({
        workspaceId: mockWorkspaceId,
        userId: mockUserId,
        action: ActivityAction.CREDENTIAL_ROTATED,
        entityType: EntityType.CREDENTIAL,
        entityId: mockCredentialId,
        details: {
          name: mockCredential.name,
          provider: mockCredential.provider,
        },
      });
    });

    it("should throw NotFoundException when credential not found", async () => {
      mockPrismaService.userCredential.findUnique.mockResolvedValue(null);

      await expect(
        service.rotate(mockCredentialId, mockWorkspaceId, mockUserId, "new_value")
      ).rejects.toThrow(NotFoundException);
    });
  });

  describe("remove", () => {
    it("should soft-delete credential and log activity", async () => {
      mockPrismaService.userCredential.findUnique.mockResolvedValue(mockCredential);
      mockPrismaService.userCredential.update.mockResolvedValue({
        ...mockCredential,
        isActive: false,
      });
      mockActivityService.logActivity.mockResolvedValue({});

      await service.remove(mockCredentialId, mockWorkspaceId, mockUserId);

      expect(prisma.userCredential.update).toHaveBeenCalledWith({
        where: { id: mockCredentialId },
        data: { isActive: false },
      });
      expect(activityService.logActivity).toHaveBeenCalledWith({
        workspaceId: mockWorkspaceId,
        userId: mockUserId,
        action: ActivityAction.CREDENTIAL_REVOKED,
        entityType: EntityType.CREDENTIAL,
        entityId: mockCredentialId,
        details: {
          name: mockCredential.name,
          provider: mockCredential.provider,
        },
      });
    });

    it("should throw NotFoundException when credential not found", async () => {
      mockPrismaService.userCredential.findUnique.mockResolvedValue(null);

      await expect(service.remove(mockCredentialId, mockWorkspaceId, mockUserId)).rejects.toThrow(
        NotFoundException
      );
    });
  });

  describe("getAuditLog", () => {
    const mockAuditLogs = [
      {
        id: "log-1",
        action: ActivityAction.CREDENTIAL_ACCESSED,
        entityId: mockCredentialId,
        createdAt: new Date("2024-01-15T10:00:00Z"),
        details: { name: "GitHub Token", provider: "github" },
        user: {
          id: mockUserId,
          name: "John Doe",
          email: "john@example.com",
        },
      },
      {
        id: "log-2",
        action: ActivityAction.CREDENTIAL_CREATED,
        entityId: mockCredentialId,
        createdAt: new Date("2024-01-10T09:00:00Z"),
        details: { name: "GitHub Token", provider: "github" },
        user: {
          id: mockUserId,
          name: "John Doe",
          email: "john@example.com",
        },
      },
    ];

    it("should return paginated audit logs", async () => {
      mockPrismaService.activityLog.findMany.mockResolvedValue(mockAuditLogs);
      mockPrismaService.activityLog.count.mockResolvedValue(2);

      const result = await service.getAuditLog(mockWorkspaceId, {
        page: 1,
        limit: 20,
      });

      expect(result.data).toHaveLength(2);
      expect(result.meta.total).toBe(2);
      expect(result.meta.page).toBe(1);
      expect(result.meta.limit).toBe(20);
      expect(result.meta.totalPages).toBe(1);
    });

    it("should filter by credentialId", async () => {
      mockPrismaService.activityLog.findMany.mockResolvedValue([mockAuditLogs[0]]);
      mockPrismaService.activityLog.count.mockResolvedValue(1);

      await service.getAuditLog(mockWorkspaceId, {
        credentialId: mockCredentialId,
        page: 1,
        limit: 20,
      });

      const callArgs = mockPrismaService.activityLog.findMany.mock.calls[0][0];
      expect(callArgs.where.entityId).toBe(mockCredentialId);
    });

    it("should filter by action type", async () => {
      mockPrismaService.activityLog.findMany.mockResolvedValue([mockAuditLogs[0]]);
      mockPrismaService.activityLog.count.mockResolvedValue(1);

      await service.getAuditLog(mockWorkspaceId, {
        action: ActivityAction.CREDENTIAL_ACCESSED,
        page: 1,
        limit: 20,
      });

      const callArgs = mockPrismaService.activityLog.findMany.mock.calls[0][0];
      expect(callArgs.where.action).toBe(ActivityAction.CREDENTIAL_ACCESSED);
    });

    it("should filter by date range", async () => {
      const startDate = new Date("2024-01-10T00:00:00Z");
      const endDate = new Date("2024-01-15T23:59:59Z");

      mockPrismaService.activityLog.findMany.mockResolvedValue(mockAuditLogs);
      mockPrismaService.activityLog.count.mockResolvedValue(2);

      await service.getAuditLog(mockWorkspaceId, {
        startDate,
        endDate,
        page: 1,
        limit: 20,
      });

      const callArgs = mockPrismaService.activityLog.findMany.mock.calls[0][0];
      expect(callArgs.where.createdAt.gte).toBe(startDate);
      expect(callArgs.where.createdAt.lte).toBe(endDate);
    });

    it("should handle pagination correctly", async () => {
      mockPrismaService.activityLog.findMany.mockResolvedValue([mockAuditLogs[0]]);
      mockPrismaService.activityLog.count.mockResolvedValue(25);

      const result = await service.getAuditLog(mockWorkspaceId, {
        page: 2,
        limit: 20,
      });

      expect(result.meta.page).toBe(2);
      expect(result.meta.limit).toBe(20);
      expect(result.meta.totalPages).toBe(2);

      const callArgs = mockPrismaService.activityLog.findMany.mock.calls[0][0];
      expect(callArgs.skip).toBe(20); // (2 - 1) * 20
      expect(callArgs.take).toBe(20);
    });

    it("should order by createdAt descending", async () => {
      mockPrismaService.activityLog.findMany.mockResolvedValue(mockAuditLogs);
      mockPrismaService.activityLog.count.mockResolvedValue(2);

      await service.getAuditLog(mockWorkspaceId, {
        page: 1,
        limit: 20,
      });

      const callArgs = mockPrismaService.activityLog.findMany.mock.calls[0][0];
      expect(callArgs.orderBy).toEqual({ createdAt: "desc" });
    });

    it("should always filter by CREDENTIAL entityType", async () => {
      mockPrismaService.activityLog.findMany.mockResolvedValue([]);
      mockPrismaService.activityLog.count.mockResolvedValue(0);

      await service.getAuditLog(mockWorkspaceId, {
        page: 1,
        limit: 20,
      });

      const callArgs = mockPrismaService.activityLog.findMany.mock.calls[0][0];
      expect(callArgs.where.entityType).toBe(EntityType.CREDENTIAL);
      expect(callArgs.where.workspaceId).toBe(mockWorkspaceId);
    });
  });
});
