import { Test, TestingModule } from '@nestjs/testing';
import { TotpService } from './totp.service';
import { PrismaService } from '../../prisma/prisma.service';

jest.mock('otplib', () => ({
  __esModule: true,
  generateSecret: jest.fn().mockReturnValue('MOCK_SECRET_BASE32'),
  generateURI: jest.fn().mockReturnValue(
    'otpauth://totp/MyFitCRM:+79001234567?secret=MOCK_SECRET_BASE32&issuer=MyFitCRM',
  ),
  verifySync: jest.fn(),
}));

jest.mock('qrcode', () => ({
  toDataURL: jest.fn().mockResolvedValue('data:image/png;base64,mockQR'),
}));

import { generateSecret, generateURI, verifySync } from 'otplib';
import * as QRCode from 'qrcode';

describe('TotpService', () => {
  let service: TotpService;
  let prisma: PrismaService;

  const mockPrisma = {
    $queryRawUnsafe: jest.fn(),
    $executeRawUnsafe: jest.fn(),
  };

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

    const module: TestingModule = await Test.createTestingModule({
      providers: [
        TotpService,
        { provide: PrismaService, useValue: mockPrisma },
      ],
    }).compile();

    service = module.get<TotpService>(TotpService);
    prisma = module.get<PrismaService>(PrismaService);
  });

  describe('setup', () => {
    it('should return secret and QR code for valid user', async () => {
      mockPrisma.$queryRawUnsafe.mockResolvedValue([
        {
          id: 'user-1',
          phone: '+79001234567',
          totpSecret: null,
        },
      ]);

      const result = await service.setup('user-1');

      expect(mockPrisma.$queryRawUnsafe).toHaveBeenCalledWith(
        expect.stringContaining('SELECT'),
        'user-1',
      );
      expect(generateSecret).toHaveBeenCalled();
      expect(result).toEqual(
        expect.objectContaining({
          secret: 'MOCK_SECRET_BASE32',
          qrCodeDataUrl: 'data:image/png;base64,mockQR',
        }),
      );
    });

    it('should throw if user not found', async () => {
      mockPrisma.$queryRawUnsafe.mockResolvedValue([]);

      await expect(service.setup('nonexistent')).rejects.toThrow();
    });

    it('should throw if 2FA is already enabled', async () => {
      mockPrisma.$queryRawUnsafe.mockResolvedValue([
        {
          id: 'user-1',
          phone: '+79001234567',
          totpSecret: 'EXISTING_SECRET',
        },
      ]);

      await expect(service.setup('user-1')).rejects.toThrow();
    });
  });

  describe('verify', () => {
    it('should save secret when TOTP code is valid', async () => {
      (verifySync as jest.Mock).mockReturnValue({ valid: true });
      mockPrisma.$executeRawUnsafe.mockResolvedValue(1);

      await service.verify('user-1', '123456', 'MOCK_SECRET_BASE32');

      expect(verifySync).toHaveBeenCalledWith({
        token: '123456',
        secret: 'MOCK_SECRET_BASE32',
      });
      expect(mockPrisma.$executeRawUnsafe).toHaveBeenCalledWith(
        expect.stringContaining('UPDATE'),
        'MOCK_SECRET_BASE32',
        'user-1',
      );
    });

    it('should throw when TOTP code is invalid', async () => {
      (verifySync as jest.Mock).mockReturnValue({ valid: false });

      await expect(
        service.verify('user-1', '000000', 'MOCK_SECRET_BASE32'),
      ).rejects.toThrow();

      expect(mockPrisma.$executeRawUnsafe).not.toHaveBeenCalled();
    });
  });

  describe('validate', () => {
    it('should return true when code is valid', async () => {
      mockPrisma.$queryRawUnsafe.mockResolvedValue([
        {
          totpSecret: 'STORED_SECRET',
        },
      ]);
      (verifySync as jest.Mock).mockReturnValue({ valid: true });

      const result = await service.validate('user-1', '123456');

      expect(result).toBe(true);
      expect(verifySync).toHaveBeenCalledWith({
        token: '123456',
        secret: 'STORED_SECRET',
      });
    });

    it('should return false when code is invalid', async () => {
      mockPrisma.$queryRawUnsafe.mockResolvedValue([
        {
          totpSecret: 'STORED_SECRET',
        },
      ]);
      (verifySync as jest.Mock).mockReturnValue({ valid: false });

      const result = await service.validate('user-1', '000000');

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

    it('should throw when 2FA is not enabled for user', async () => {
      mockPrisma.$queryRawUnsafe.mockResolvedValue([
        {
          totpSecret: null,
        },
      ]);

      await expect(service.validate('user-1', '123456')).rejects.toThrow();
    });
  });

  describe('disable', () => {
    it('should remove totpSecret when valid code is provided', async () => {
      // validate() will query for user's totpSecret
      mockPrisma.$queryRawUnsafe.mockResolvedValue([
        {
          totpSecret: 'STORED_SECRET',
        },
      ]);
      (verifySync as jest.Mock).mockReturnValue({ valid: true });
      mockPrisma.$executeRawUnsafe.mockResolvedValue(1);

      await service.disable('user-1', '123456');

      expect(verifySync).toHaveBeenCalledWith({
        token: '123456',
        secret: 'STORED_SECRET',
      });
      expect(mockPrisma.$executeRawUnsafe).toHaveBeenCalledWith(
        expect.stringContaining('UPDATE'),
        'user-1',
      );
    });

    it('should throw when invalid code is provided', async () => {
      mockPrisma.$queryRawUnsafe.mockResolvedValue([
        {
          totpSecret: 'STORED_SECRET',
        },
      ]);
      (verifySync as jest.Mock).mockReturnValue({ valid: false });

      await expect(service.disable('user-1', '000000')).rejects.toThrow();

      expect(mockPrisma.$executeRawUnsafe).not.toHaveBeenCalled();
    });

    it('should throw when 2FA is not enabled', async () => {
      mockPrisma.$queryRawUnsafe.mockResolvedValue([
        {
          totpSecret: null,
        },
      ]);

      await expect(service.disable('user-1', '123456')).rejects.toThrow();

      expect(mockPrisma.$executeRawUnsafe).not.toHaveBeenCalled();
    });
  });
});
