import { PrismaClient } from '@prisma/client';
import speakeasy from 'speakeasy';

import { MFAService } from '../mfa.service';

// Mock dependencies
jest.mock('../../../config/database.config');
jest.mock('../../../core/security/encryption.service');
jest.mock('qrcode');
jest.mock('speakeasy');
jest.mock('@goodgo/logger');

describe('MFAService', () => {
  let mfaService: MFAService;
  let mockPrisma: any;
  let mockEncryptionService: any;
  let mockQRCode: any;
  let mockSpeakeasy: any;

  beforeEach(() => {
    // Reset all mocks
    jest.clearAllMocks();

    // Setup mocks
    mockPrisma = {
      user: {
        findUnique: jest.fn(),
        update: jest.fn(),
      },
      mFADevice: {
        findMany: jest.fn(),
        create: jest.fn(),
        update: jest.fn(),
        deleteMany: jest.fn(),
        updateMany: jest.fn(),
        findFirst: jest.fn(),
      },
    } as any;

    mockEncryptionService = {
      encrypt: jest.fn().mockReturnValue('encrypted-secret'),
      decrypt: jest.fn().mockReturnValue('decrypted-secret'),
    };

    mockQRCode = {
      toDataURL: jest.fn().mockResolvedValue('data:image/png;base64,qr-code-data'),
    };

    mockSpeakeasy = {
      generateSecret: jest.fn(),
      totp: {
        verify: jest.fn(),
      },
      generateSecretASCII: jest.fn(),
    };

    // Mock the database client
    const { getPrismaClient } = require('../../../config/database.config');
    getPrismaClient.mockReturnValue(mockPrisma);

    // Mock encryption service
    const encryptionModule = require('../../../core/security/encryption.service');
    encryptionModule.encryptionService = mockEncryptionService;

    // Mock QRCode
    const qrcodeModule = require('qrcode');
    qrcodeModule.default = mockQRCode;

    // Mock speakeasy
    const speakeasyModule = require('speakeasy');
    speakeasyModule.default = mockSpeakeasy;

    mfaService = new MFAService();
  });

  describe('enableTOTP', () => {
    it('should generate TOTP secret and QR code successfully', async () => {
      // Arrange
      const userId = 'user-123';
      const serviceName = 'TestApp';

      const mockSecret = {
        base32: 'JBSWY3DPEHPK3PXP',
        otpauth_url: 'otpauth://totp/TestApp (user-123)?secret=JBSWY3DPEHPK3PXP',
      };

      mockSpeakeasy.generateSecret.mockReturnValue(mockSecret);
      mockQRCode.toDataURL.mockResolvedValue('qr-code-url');

      // Act
      const result = await mfaService.enableTOTP(userId, serviceName);

      // Assert
      expect(mockSpeakeasy.generateSecret).toHaveBeenCalledWith({
        name: 'TestApp (user-123)',
        length: 32,
      });
      expect(mockQRCode.toDataURL).toHaveBeenCalledWith(mockSecret.otpauth_url);
      expect(result).toEqual({
        secret: 'JBSWY3DPEHPK3PXP',
        qrCodeUrl: 'qr-code-url',
      });
    });
  });

  describe('verifyAndEnableTOTP', () => {
    it('should verify token and enable TOTP successfully', async () => {
      // Arrange
      const userId = 'user-123';
      const secret = 'JBSWY3DPEHPK3PXP';
      const token = '123456';

      const mockUser = {
        id: userId,
        mfaEnabled: false,
      };

      mockPrisma.user.findUnique.mockResolvedValue(mockUser);
      mockSpeakeasy.totp.verify.mockReturnValue(true);
      mockPrisma.user.update.mockResolvedValue({
        ...mockUser,
        mfaEnabled: true,
        mfaSecret: 'encrypted-secret',
      });

      // Act
      const result = await mfaService.verifyAndEnableTOTP(userId, secret, token);

      // Assert
      expect(mockSpeakeasy.totp.verify).toHaveBeenCalledWith({
        secret: secret,
        encoding: 'base32',
        token: token,
        window: 2,
      });
      expect(mockEncryptionService.encrypt).toHaveBeenCalledWith(secret);
      expect(mockPrisma.user.update).toHaveBeenCalledWith({
        where: { id: userId },
        data: {
          mfaEnabled: true,
          mfaSecret: 'encrypted-secret',
        },
      });
      expect(result).toBe(true);
    });

    it('should return false for invalid token', async () => {
      // Arrange
      const userId = 'user-123';
      const secret = 'JBSWY3DPEHPK3PXP';
      const token = 'invalid-token';

      mockPrisma.user.findUnique.mockResolvedValue({ id: userId });
      mockSpeakeasy.totp.verify.mockReturnValue(false);

      // Act
      const result = await mfaService.verifyAndEnableTOTP(userId, secret, token);

      // Assert
      expect(result).toBe(false);
      expect(mockPrisma.user.update).not.toHaveBeenCalled();
    });
  });

  describe('verifyTOTP', () => {
    it('should verify TOTP token successfully', async () => {
      // Arrange
      const userId = 'user-123';
      const token = '123456';

      const mockUser = {
        id: userId,
        mfaSecret: 'encrypted-secret',
      };

      mockPrisma.user.findUnique.mockResolvedValue(mockUser);
      mockSpeakeasy.totp.verify.mockReturnValue(true);

      // Act
      const result = await mfaService.verifyTOTP(userId, token);

      // Assert
      expect(mockEncryptionService.decrypt).toHaveBeenCalledWith('encrypted-secret');
      expect(mockSpeakeasy.totp.verify).toHaveBeenCalledWith({
        secret: 'decrypted-secret',
        encoding: 'base32',
        token: token,
        window: 2,
      });
      expect(result).toBe(true);
    });

    it('should return false for user without MFA enabled', async () => {
      // Arrange
      const userId = 'user-123';
      const token = '123456';

      const mockUser = {
        id: userId,
        mfaEnabled: false,
      };

      mockPrisma.user.findUnique.mockResolvedValue(mockUser);

      // Act
      const result = await mfaService.verifyTOTP(userId, token);

      // Assert
      expect(result).toBe(false);
      expect(mockSpeakeasy.totp.verify).not.toHaveBeenCalled();
    });
  });

  describe('disableMFA', () => {
    it('should disable MFA and clean up data successfully', async () => {
      // Arrange
      const userId = 'user-123';

      mockPrisma.mFADevice.updateMany.mockResolvedValue({ count: 2 });
      mockPrisma.user.update.mockResolvedValue(undefined);

      // Act
      await mfaService.disableMFA(userId);

      // Assert
      expect(mockPrisma.mFADevice.updateMany).toHaveBeenCalledWith({
        where: { userId },
        data: { isActive: false },
      });
      expect(mockPrisma.user.update).toHaveBeenCalledWith({
        where: { id: userId },
        data: {
          mfaEnabled: false,
          mfaSecret: null,
          mfaBackupCodes: null,
        },
      });
    });
  });

  describe('getUserMFADevices', () => {
    it('should return user MFA devices', async () => {
      // Arrange
      const userId = 'user-123';
      const mockDevices = [
        {
          id: 'device-1',
          type: 'totp',
          name: 'My Phone',
          createdAt: new Date(),
        },
        {
          id: 'device-2',
          type: 'webauthn',
          name: 'Security Key',
          createdAt: new Date(),
        },
      ];

      mockPrisma.mFADevice.findMany.mockResolvedValue(mockDevices);

      // Act
      const result = await mfaService.getUserMFADevices(userId);

      // Assert
      expect(mockPrisma.mFADevice.findMany).toHaveBeenCalledWith({
        where: {
          userId,
          isActive: true,
        },
        orderBy: { createdAt: 'desc' },
      });
      expect(result).toEqual(mockDevices);
    });
  });

  describe('storeBackupCodes', () => {
    it('should store backup codes successfully', async () => {
      // Arrange
      const userId = 'user-123';
      const codes = ['code1', 'code2', 'code3'];

      mockPrisma.user.update.mockResolvedValue(undefined);

      // Act
      await mfaService.storeBackupCodes(userId, codes);

      // Assert
      expect(mockEncryptionService.encrypt).toHaveBeenCalledWith(JSON.stringify(codes));
      expect(mockPrisma.user.update).toHaveBeenCalledWith({
        where: { id: userId },
        data: { mfaBackupCodes: 'encrypted-secret' },
      });
    });
  });

  describe('getBackupCodes', () => {
    it('should return backup codes', async () => {
      // Arrange
      const userId = 'user-123';
      const codes = ['code1', 'code3'];

      mockPrisma.user.findUnique.mockResolvedValue({
        id: userId,
        mfaBackupCodes: 'encrypted-codes',
      });
      mockEncryptionService.decrypt.mockReturnValue(JSON.stringify(codes));

      // Act
      const result = await mfaService.getBackupCodes(userId);

      // Assert
      expect(mockPrisma.user.findUnique).toHaveBeenCalledWith({
        where: { id: userId },
        select: { mfaBackupCodes: true },
      });
      expect(mockEncryptionService.decrypt).toHaveBeenCalledWith('encrypted-codes');
      expect(result).toEqual(codes);
    });
  });

  describe('validateBackupCode', () => {
    it('should validate and consume backup code', async () => {
      // Arrange
      const userId = 'user-123';
      const code = 'code1';
      const codes = ['code1', 'code2'];

      mockPrisma.user.findUnique.mockResolvedValue({
        id: userId,
        mfaBackupCodes: 'encrypted-codes',
      });
      mockEncryptionService.decrypt.mockReturnValue(JSON.stringify(codes));
      mockPrisma.user.update.mockResolvedValue(undefined);

      // Act
      const result = await mfaService.validateBackupCode(userId, code);

      // Assert
      expect(result).toBe(true);
      // Should store remaining codes
      expect(mockPrisma.user.update).toHaveBeenCalledWith({
        where: { id: userId },
        data: { mfaBackupCodes: expect.any(String) },
      });
    });

    it('should return false for invalid backup code', async () => {
      // Arrange
      const userId = 'user-123';
      const code = 'invalid-code';
      const codes = ['code1', 'code2'];

      mockPrisma.user.findUnique.mockResolvedValue({
        id: userId,
        mfaBackupCodes: 'encrypted-codes',
      });
      mockEncryptionService.decrypt.mockReturnValue(JSON.stringify(codes));

      // Act
      const result = await mfaService.validateBackupCode(userId, code);

      // Assert
      expect(result).toBe(false);
    });
  });

  describe('regenerateBackupCodes', () => {
    it('should regenerate backup codes successfully', async () => {
      // Arrange
      const userId = 'user-123';

      mockPrisma.user.update.mockResolvedValue(undefined);

      // Act
      const result = await mfaService.regenerateBackupCodes(userId);

      // Assert
      expect(mockPrisma.user.update).toHaveBeenCalled();
      expect(result).toHaveLength(10); // Default 10 codes
    });
  });

  describe('hasBackupCodes', () => {
    it('should return true when user has backup codes', async () => {
      // Arrange
      const userId = 'user-123';
      const codes = ['code1'];

      mockPrisma.user.findUnique.mockResolvedValue({
        id: userId,
        mfaBackupCodes: 'encrypted-codes',
      });
      mockEncryptionService.decrypt.mockReturnValue(JSON.stringify(codes));

      // Act
      const result = await mfaService.hasBackupCodes(userId);

      // Assert
      expect(result).toBe(true);
    });

    it('should return false when user has no backup codes', async () => {
      // Arrange
      const userId = 'user-123';

      mockPrisma.user.findUnique.mockResolvedValue({
        id: userId,
        mfaBackupCodes: null,
      });

      // Act
      const result = await mfaService.hasBackupCodes(userId);

      // Assert
      expect(result).toBe(false);
    });
  });
});