import { Test, TestingModule } from '@nestjs/testing';
import { getRepositoryToken } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { BadRequestException, NotFoundException } from '@nestjs/common';
import { MFAService } from './mfa.service';
import { User } from '../../database/entities';
import { EmailService } from '../../common/services/email.service';
import { ConfigService } from '@nestjs/config';
import { authenticator } from 'otplib';
import * as bcrypt from 'bcrypt';

describe('MFAService', () => {
  let service: MFAService;
  let userRepository: Repository<User>;
  let emailService: EmailService;
  let configService: ConfigService;

  const mockUser = {
    id: 'user_123',
    email: 'test@example.com',
    name: 'Test User',
    mfaEnabled: false,
    mfaMethod: null,
    totpSecret: null,
    mfaBackupCodes: [],
    emailMfaCode: null,
    emailMfaCodeExpiresAt: null,
  };

  const mockUserRepository = {
    findOne: jest.fn(),
    update: jest.fn(),
  };

  const mockEmailService = {
    sendEmail: jest.fn().mockResolvedValue(true),
  };

  const mockConfigService = {
    get: jest.fn().mockReturnValue('Maternal App'),
  };

  beforeEach(async () => {
    const module: TestingModule = await Test.createTestingModule({
      providers: [
        MFAService,
        {
          provide: getRepositoryToken(User),
          useValue: mockUserRepository,
        },
        {
          provide: EmailService,
          useValue: mockEmailService,
        },
        {
          provide: ConfigService,
          useValue: mockConfigService,
        },
      ],
    }).compile();

    service = module.get<MFAService>(MFAService);
    userRepository = module.get<Repository<User>>(getRepositoryToken(User));
    emailService = module.get<EmailService>(EmailService);
    configService = module.get<ConfigService>(ConfigService);

    // Reset mocks
    jest.clearAllMocks();
  });

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

  describe('getMFAStatus', () => {
    it('should return MFA status for a user', async () => {
      mockUserRepository.findOne.mockResolvedValue({
        ...mockUser,
        mfaEnabled: true,
        mfaMethod: 'totp',
        mfaBackupCodes: ['hashed_code_1', 'hashed_code_2'],
      });

      const result = await service.getMFAStatus('user_123');

      expect(result).toEqual({
        enabled: true,
        method: 'totp',
        hasBackupCodes: true,
      });
      expect(userRepository.findOne).toHaveBeenCalledWith({
        where: { id: 'user_123' },
        select: ['id', 'mfaEnabled', 'mfaMethod', 'mfaBackupCodes'],
      });
    });

    it('should return disabled status when MFA is not enabled', async () => {
      mockUserRepository.findOne.mockResolvedValue(mockUser);

      const result = await service.getMFAStatus('user_123');

      expect(result).toEqual({
        enabled: false,
        method: undefined,
        hasBackupCodes: false,
      });
    });

    it('should throw NotFoundException if user not found', async () => {
      mockUserRepository.findOne.mockResolvedValue(null);

      await expect(service.getMFAStatus('user_123')).rejects.toThrow(
        NotFoundException,
      );
    });
  });

  describe('setupTOTP', () => {
    it('should generate TOTP secret, QR code, and backup codes', async () => {
      mockUserRepository.findOne.mockResolvedValue(mockUser);
      mockUserRepository.update.mockResolvedValue({ affected: 1 });

      const result = await service.setupTOTP('user_123');

      expect(result).toHaveProperty('secret');
      expect(result).toHaveProperty('qrCodeUrl');
      expect(result).toHaveProperty('backupCodes');
      expect(result.secret).toBeTruthy();
      expect(result.qrCodeUrl).toContain('data:image/png;base64');
      expect(result.backupCodes).toHaveLength(10);
      expect(userRepository.update).toHaveBeenCalledWith(
        'user_123',
        expect.objectContaining({
          totpSecret: expect.any(String),
          mfaBackupCodes: expect.any(Array),
        }),
      );
    });

    it('should throw NotFoundException if user not found', async () => {
      mockUserRepository.findOne.mockResolvedValue(null);

      await expect(service.setupTOTP('user_123')).rejects.toThrow(
        NotFoundException,
      );
    });
  });

  describe('verifyAndEnableTOTP', () => {
    it('should enable TOTP MFA with valid code', async () => {
      const totpSecret = authenticator.generateSecret();
      const validCode = authenticator.generate(totpSecret);

      mockUserRepository.findOne.mockResolvedValue({
        ...mockUser,
        totpSecret,
      });
      mockUserRepository.update.mockResolvedValue({ affected: 1 });

      const result = await service.verifyAndEnableTOTP('user_123', validCode);

      expect(result.success).toBe(true);
      expect(result.message).toContain('enabled successfully');
      expect(userRepository.update).toHaveBeenCalledWith('user_123', {
        mfaEnabled: true,
        mfaMethod: 'totp',
      });
    });

    it('should throw BadRequestException for invalid TOTP code', async () => {
      const totpSecret = authenticator.generateSecret();

      mockUserRepository.findOne.mockResolvedValue({
        ...mockUser,
        totpSecret,
      });

      await expect(
        service.verifyAndEnableTOTP('user_123', '000000'),
      ).rejects.toThrow(BadRequestException);
    });

    it('should throw BadRequestException if TOTP not set up', async () => {
      mockUserRepository.findOne.mockResolvedValue(mockUser);

      await expect(
        service.verifyAndEnableTOTP('user_123', '123456'),
      ).rejects.toThrow(BadRequestException);
    });

    it('should throw NotFoundException if user not found', async () => {
      mockUserRepository.findOne.mockResolvedValue(null);

      await expect(
        service.verifyAndEnableTOTP('user_123', '123456'),
      ).rejects.toThrow(NotFoundException);
    });
  });

  describe('setupEmailMFA', () => {
    it('should enable Email MFA and send confirmation email', async () => {
      mockUserRepository.findOne.mockResolvedValue(mockUser);
      mockUserRepository.update.mockResolvedValue({ affected: 1 });

      const result = await service.setupEmailMFA('user_123');

      expect(result.success).toBe(true);
      expect(result.message).toContain('enabled successfully');
      expect(userRepository.update).toHaveBeenCalledWith(
        'user_123',
        expect.objectContaining({
          mfaEnabled: true,
          mfaMethod: 'email',
          mfaBackupCodes: expect.any(Array),
        }),
      );
      expect(emailService.sendEmail).toHaveBeenCalledWith(
        expect.objectContaining({
          to: mockUser.email,
          subject: expect.stringContaining('Two-Factor Authentication'),
        }),
      );
    });

    it('should throw NotFoundException if user not found', async () => {
      mockUserRepository.findOne.mockResolvedValue(null);

      await expect(service.setupEmailMFA('user_123')).rejects.toThrow(
        NotFoundException,
      );
    });

    it('should still succeed if email sending fails', async () => {
      mockUserRepository.findOne.mockResolvedValue(mockUser);
      mockUserRepository.update.mockResolvedValue({ affected: 1 });
      mockEmailService.sendEmail.mockRejectedValue(
        new Error('Email service down'),
      );

      const result = await service.setupEmailMFA('user_123');

      expect(result.success).toBe(true);
      // MFA should still be enabled even if email fails
      expect(userRepository.update).toHaveBeenCalled();
    });
  });

  describe('sendEmailMFACode', () => {
    it('should generate and send 6-digit code via email', async () => {
      mockUserRepository.findOne.mockResolvedValue({
        ...mockUser,
        mfaEnabled: true,
        mfaMethod: 'email',
      });
      mockUserRepository.update.mockResolvedValue({ affected: 1 });

      const result = await service.sendEmailMFACode('user_123');

      expect(result.success).toBe(true);
      expect(result.message).toContain('Verification code sent');
      expect(userRepository.update).toHaveBeenCalledWith(
        'user_123',
        expect.objectContaining({
          emailMfaCode: expect.any(String),
          emailMfaCodeExpiresAt: expect.any(Date),
        }),
      );
      expect(emailService.sendEmail).toHaveBeenCalledWith(
        expect.objectContaining({
          to: mockUser.email,
          subject: expect.stringContaining('verification code'),
        }),
      );
    });

    it('should throw BadRequestException if Email MFA not enabled', async () => {
      mockUserRepository.findOne.mockResolvedValue(mockUser);

      await expect(service.sendEmailMFACode('user_123')).rejects.toThrow(
        BadRequestException,
      );
    });

    it('should throw NotFoundException if user not found', async () => {
      mockUserRepository.findOne.mockResolvedValue(null);

      await expect(service.sendEmailMFACode('user_123')).rejects.toThrow(
        NotFoundException,
      );
    });
  });

  describe('verifyMFACode', () => {
    it('should verify valid TOTP code', async () => {
      const totpSecret = authenticator.generateSecret();
      const validCode = authenticator.generate(totpSecret);

      mockUserRepository.findOne.mockResolvedValue({
        ...mockUser,
        mfaEnabled: true,
        mfaMethod: 'totp',
        totpSecret,
      });

      const result = await service.verifyMFACode('user_123', validCode);

      expect(result.success).toBe(true);
    });

    it('should reject invalid TOTP code', async () => {
      const totpSecret = authenticator.generateSecret();

      mockUserRepository.findOne.mockResolvedValue({
        ...mockUser,
        mfaEnabled: true,
        mfaMethod: 'totp',
        totpSecret,
      });

      await expect(service.verifyMFACode('user_123', '000000')).rejects.toThrow(
        BadRequestException,
      );
    });

    it('should verify valid email MFA code', async () => {
      const code = '123456';
      const expiresAt = new Date();
      expiresAt.setMinutes(expiresAt.getMinutes() + 5);

      mockUserRepository.findOne.mockResolvedValue({
        ...mockUser,
        mfaEnabled: true,
        mfaMethod: 'email',
        emailMfaCode: code,
        emailMfaCodeExpiresAt: expiresAt,
      });
      mockUserRepository.update.mockResolvedValue({ affected: 1 });

      const result = await service.verifyMFACode('user_123', code);

      expect(result.success).toBe(true);
      // Code should be cleared after use
      expect(userRepository.update).toHaveBeenCalledWith('user_123', {
        emailMfaCode: null,
        emailMfaCodeExpiresAt: null,
      });
    });

    it('should reject expired email MFA code', async () => {
      const code = '123456';
      const expiresAt = new Date();
      expiresAt.setMinutes(expiresAt.getMinutes() - 1); // Expired 1 minute ago

      mockUserRepository.findOne.mockResolvedValue({
        ...mockUser,
        mfaEnabled: true,
        mfaMethod: 'email',
        emailMfaCode: code,
        emailMfaCodeExpiresAt: expiresAt,
      });

      await expect(service.verifyMFACode('user_123', code)).rejects.toThrow(
        BadRequestException,
      );
    });

    it('should verify valid backup code and mark it as used', async () => {
      const backupCode = 'BACKUP-123456';
      const hashedBackupCode = await bcrypt.hash(backupCode, 10);

      mockUserRepository.findOne.mockResolvedValue({
        ...mockUser,
        mfaEnabled: true,
        mfaMethod: 'totp',
        mfaBackupCodes: [hashedBackupCode, 'other_hashed_code'],
      });
      mockUserRepository.update.mockResolvedValue({ affected: 1 });

      const result = await service.verifyMFACode('user_123', backupCode);

      expect(result.success).toBe(true);
      // Backup code should be removed after use
      expect(userRepository.update).toHaveBeenCalledWith(
        'user_123',
        expect.objectContaining({
          mfaBackupCodes: expect.any(Array),
        }),
      );
    });

    it('should throw BadRequestException if MFA not enabled', async () => {
      mockUserRepository.findOne.mockResolvedValue(mockUser);

      await expect(service.verifyMFACode('user_123', '123456')).rejects.toThrow(
        BadRequestException,
      );
    });
  });

  describe('disableMFA', () => {
    it('should disable MFA and clear all MFA data', async () => {
      mockUserRepository.findOne.mockResolvedValue({
        ...mockUser,
        mfaEnabled: true,
        mfaMethod: 'totp',
      });
      mockUserRepository.update.mockResolvedValue({ affected: 1 });

      const result = await service.disableMFA('user_123');

      expect(result.success).toBe(true);
      expect(result.message).toContain('disabled successfully');
      expect(userRepository.update).toHaveBeenCalledWith('user_123', {
        mfaEnabled: false,
        mfaMethod: null,
        totpSecret: null,
        emailMfaCode: null,
        emailMfaCodeExpiresAt: null,
        mfaBackupCodes: [],
      });
    });

    it('should throw BadRequestException if MFA not enabled', async () => {
      mockUserRepository.findOne.mockResolvedValue(mockUser);

      await expect(service.disableMFA('user_123')).rejects.toThrow(
        BadRequestException,
      );
    });

    it('should throw NotFoundException if user not found', async () => {
      mockUserRepository.findOne.mockResolvedValue(null);

      await expect(service.disableMFA('user_123')).rejects.toThrow(
        NotFoundException,
      );
    });
  });

  describe('regenerateBackupCodes', () => {
    it('should generate new backup codes', async () => {
      mockUserRepository.findOne.mockResolvedValue({
        ...mockUser,
        mfaEnabled: true,
        mfaMethod: 'totp',
      });
      mockUserRepository.update.mockResolvedValue({ affected: 1 });

      const result = await service.regenerateBackupCodes('user_123');

      expect(result).toHaveLength(10);
      expect(result[0]).toMatch(/^BACKUP-[A-Z0-9]{6}$/);
      expect(userRepository.update).toHaveBeenCalledWith(
        'user_123',
        expect.objectContaining({
          mfaBackupCodes: expect.any(Array),
        }),
      );
    });

    it('should throw BadRequestException if MFA not enabled', async () => {
      mockUserRepository.findOne.mockResolvedValue(mockUser);

      await expect(service.regenerateBackupCodes('user_123')).rejects.toThrow(
        BadRequestException,
      );
    });

    it('should throw NotFoundException if user not found', async () => {
      mockUserRepository.findOne.mockResolvedValue(null);

      await expect(service.regenerateBackupCodes('user_123')).rejects.toThrow(
        NotFoundException,
      );
    });
  });
});
