const request = require('supertest');
const express = require('express');

// Mock dependencies before requiring routes
jest.mock('../../../models', () => ({
  User: {
    findByPk: jest.fn(),
  },
}));

jest.mock('../../../services/TwoFactorService', () => ({
  generateTotpSecret: jest.fn(),
  generateRecoveryCodes: jest.fn(),
}));

jest.mock('../../../services/email', () => ({
  auth: {
    sendTwoFactorEnabledEmail: jest.fn(),
    sendTwoFactorOtpEmail: jest.fn(),
    sendRecoveryCodeUsedEmail: jest.fn(),
    sendTwoFactorDisabledEmail: jest.fn(),
  },
}));

jest.mock('../../../middleware/auth', () => ({
  authenticateToken: (req, res, next) => {
    req.user = { id: 'user-123' };
    next();
  },
}));

jest.mock('../../../middleware/stepUpAuth', () => ({
  requireStepUpAuth: () => (req, res, next) => next(),
}));

jest.mock('../../../middleware/csrf', () => ({
  csrfProtection: (req, res, next) => next(),
}));

jest.mock('../../../middleware/validation', () => ({
  sanitizeInput: (req, res, next) => next(),
  validateTotpCode: (req, res, next) => next(),
  validateEmailOtp: (req, res, next) => next(),
  validateRecoveryCode: (req, res, next) => next(),
}));

jest.mock('../../../middleware/rateLimiter', () => ({
  twoFactorVerificationLimiter: (req, res, next) => next(),
  twoFactorSetupLimiter: (req, res, next) => next(),
  recoveryCodeLimiter: (req, res, next) => next(),
  emailOtpSendLimiter: (req, res, next) => next(),
}));

jest.mock('../../../utils/logger', () => ({
  info: jest.fn(),
  error: jest.fn(),
  warn: jest.fn(),
}));

const { User } = require('../../../models');
const TwoFactorService = require('../../../services/TwoFactorService');
const emailServices = require('../../../services/email');
const twoFactorRoutes = require('../../../routes/twoFactor');

describe('Two Factor Routes', () => {
  let app;

  beforeEach(() => {
    app = express();
    app.use(express.json());
    app.use('/2fa', twoFactorRoutes);
    jest.clearAllMocks();
  });

  // ============================================
  // SETUP ENDPOINTS
  // ============================================

  describe('POST /2fa/setup/totp/init', () => {
    it('should initialize TOTP setup and return QR code', async () => {
      const mockUser = {
        id: 'user-123',
        email: 'test@example.com',
        twoFactorEnabled: false,
        storePendingTotpSecret: jest.fn().mockResolvedValue(),
      };

      User.findByPk.mockResolvedValue(mockUser);
      TwoFactorService.generateTotpSecret.mockResolvedValue({
        qrCodeDataUrl: 'data:image/png;base64,test',
        encryptedSecret: 'encrypted-secret',
        encryptedSecretIv: 'iv-123',
      });

      const response = await request(app)
        .post('/2fa/setup/totp/init');

      expect(response.status).toBe(200);
      expect(response.body.qrCodeDataUrl).toBe('data:image/png;base64,test');
      expect(response.body.message).toContain('Scan the QR code');
      expect(mockUser.storePendingTotpSecret).toHaveBeenCalledWith('encrypted-secret', 'iv-123');
    });

    it('should return 404 when user not found', async () => {
      User.findByPk.mockResolvedValue(null);

      const response = await request(app)
        .post('/2fa/setup/totp/init');

      expect(response.status).toBe(404);
      expect(response.body.error).toBe('User not found');
    });

    it('should return 400 when 2FA already enabled', async () => {
      User.findByPk.mockResolvedValue({
        id: 'user-123',
        twoFactorEnabled: true,
      });

      const response = await request(app)
        .post('/2fa/setup/totp/init');

      expect(response.status).toBe(400);
      expect(response.body.error).toContain('already enabled');
    });

    it('should handle errors during setup', async () => {
      User.findByPk.mockRejectedValue(new Error('Database error'));

      const response = await request(app)
        .post('/2fa/setup/totp/init');

      expect(response.status).toBe(500);
      expect(response.body.error).toContain('Failed to initialize');
    });
  });

  describe('POST /2fa/setup/totp/verify', () => {
    it('should verify TOTP code and enable 2FA', async () => {
      const mockUser = {
        id: 'user-123',
        twoFactorEnabled: false,
        twoFactorSetupPendingSecret: 'pending-secret',
        verifyPendingTotpCode: jest.fn().mockReturnValue(true),
        enableTotp: jest.fn().mockResolvedValue(),
      };

      User.findByPk.mockResolvedValue(mockUser);
      TwoFactorService.generateRecoveryCodes.mockResolvedValue({
        codes: ['XXXX-YYYY', 'AAAA-BBBB'],
      });
      emailServices.auth.sendTwoFactorEnabledEmail.mockResolvedValue();

      const response = await request(app)
        .post('/2fa/setup/totp/verify')
        .send({ code: '123456' });

      expect(response.status).toBe(200);
      expect(response.body.message).toContain('enabled successfully');
      expect(response.body.recoveryCodes).toHaveLength(2);
      expect(response.body.warning).toContain('Save these recovery codes');
      expect(mockUser.enableTotp).toHaveBeenCalled();
    });

    it('should return 404 when user not found', async () => {
      User.findByPk.mockResolvedValue(null);

      const response = await request(app)
        .post('/2fa/setup/totp/verify')
        .send({ code: '123456' });

      expect(response.status).toBe(404);
    });

    it('should return 400 when 2FA already enabled', async () => {
      User.findByPk.mockResolvedValue({
        id: 'user-123',
        twoFactorEnabled: true,
      });

      const response = await request(app)
        .post('/2fa/setup/totp/verify')
        .send({ code: '123456' });

      expect(response.status).toBe(400);
    });

    it('should return 400 when no pending secret', async () => {
      User.findByPk.mockResolvedValue({
        id: 'user-123',
        twoFactorEnabled: false,
        twoFactorSetupPendingSecret: null,
      });

      const response = await request(app)
        .post('/2fa/setup/totp/verify')
        .send({ code: '123456' });

      expect(response.status).toBe(400);
      expect(response.body.error).toContain('No pending TOTP setup');
    });

    it('should return 400 for invalid code', async () => {
      const mockUser = {
        id: 'user-123',
        twoFactorEnabled: false,
        twoFactorSetupPendingSecret: 'pending-secret',
        verifyPendingTotpCode: jest.fn().mockReturnValue(false),
      };

      User.findByPk.mockResolvedValue(mockUser);

      const response = await request(app)
        .post('/2fa/setup/totp/verify')
        .send({ code: '123456' });

      expect(response.status).toBe(400);
      expect(response.body.error).toContain('Invalid verification code');
    });

    it('should continue even if email fails', async () => {
      const mockUser = {
        id: 'user-123',
        twoFactorEnabled: false,
        twoFactorSetupPendingSecret: 'pending-secret',
        verifyPendingTotpCode: jest.fn().mockReturnValue(true),
        enableTotp: jest.fn().mockResolvedValue(),
      };

      User.findByPk.mockResolvedValue(mockUser);
      TwoFactorService.generateRecoveryCodes.mockResolvedValue({ codes: ['XXXX-YYYY'] });
      emailServices.auth.sendTwoFactorEnabledEmail.mockRejectedValue(new Error('Email failed'));

      const response = await request(app)
        .post('/2fa/setup/totp/verify')
        .send({ code: '123456' });

      expect(response.status).toBe(200);
    });
  });

  describe('POST /2fa/setup/email/init', () => {
    it('should send email OTP for setup', async () => {
      const mockUser = {
        id: 'user-123',
        twoFactorEnabled: false,
        generateEmailOtp: jest.fn().mockResolvedValue('123456'),
      };

      User.findByPk.mockResolvedValue(mockUser);
      emailServices.auth.sendTwoFactorOtpEmail.mockResolvedValue();

      const response = await request(app)
        .post('/2fa/setup/email/init');

      expect(response.status).toBe(200);
      expect(response.body.message).toContain('Verification code sent');
      expect(emailServices.auth.sendTwoFactorOtpEmail).toHaveBeenCalled();
    });

    it('should return 404 when user not found', async () => {
      User.findByPk.mockResolvedValue(null);

      const response = await request(app)
        .post('/2fa/setup/email/init');

      expect(response.status).toBe(404);
    });

    it('should return 400 when 2FA already enabled', async () => {
      User.findByPk.mockResolvedValue({
        id: 'user-123',
        twoFactorEnabled: true,
      });

      const response = await request(app)
        .post('/2fa/setup/email/init');

      expect(response.status).toBe(400);
    });

    it('should return 500 when email fails', async () => {
      const mockUser = {
        id: 'user-123',
        twoFactorEnabled: false,
        generateEmailOtp: jest.fn().mockResolvedValue('123456'),
      };

      User.findByPk.mockResolvedValue(mockUser);
      emailServices.auth.sendTwoFactorOtpEmail.mockRejectedValue(new Error('Email failed'));

      const response = await request(app)
        .post('/2fa/setup/email/init');

      expect(response.status).toBe(500);
      expect(response.body.error).toContain('Failed to send verification email');
    });
  });

  describe('POST /2fa/setup/email/verify', () => {
    it('should verify email OTP and enable email 2FA', async () => {
      const mockUser = {
        id: 'user-123',
        twoFactorEnabled: false,
        isEmailOtpLocked: jest.fn().mockReturnValue(false),
        verifyEmailOtp: jest.fn().mockReturnValue(true),
        enableEmailTwoFactor: jest.fn().mockResolvedValue(),
        clearEmailOtp: jest.fn().mockResolvedValue(),
      };

      User.findByPk.mockResolvedValue(mockUser);
      TwoFactorService.generateRecoveryCodes.mockResolvedValue({ codes: ['XXXX-YYYY'] });
      emailServices.auth.sendTwoFactorEnabledEmail.mockResolvedValue();

      const response = await request(app)
        .post('/2fa/setup/email/verify')
        .send({ code: '123456' });

      expect(response.status).toBe(200);
      expect(response.body.recoveryCodes).toBeDefined();
      expect(mockUser.enableEmailTwoFactor).toHaveBeenCalled();
      expect(mockUser.clearEmailOtp).toHaveBeenCalled();
    });

    it('should return 429 when OTP locked', async () => {
      const mockUser = {
        id: 'user-123',
        twoFactorEnabled: false,
        isEmailOtpLocked: jest.fn().mockReturnValue(true),
      };

      User.findByPk.mockResolvedValue(mockUser);

      const response = await request(app)
        .post('/2fa/setup/email/verify')
        .send({ code: '123456' });

      expect(response.status).toBe(429);
      expect(response.body.error).toContain('Too many failed attempts');
    });

    it('should return 400 for invalid OTP', async () => {
      const mockUser = {
        id: 'user-123',
        twoFactorEnabled: false,
        isEmailOtpLocked: jest.fn().mockReturnValue(false),
        verifyEmailOtp: jest.fn().mockReturnValue(false),
        incrementEmailOtpAttempts: jest.fn().mockResolvedValue(),
      };

      User.findByPk.mockResolvedValue(mockUser);

      const response = await request(app)
        .post('/2fa/setup/email/verify')
        .send({ code: '123456' });

      expect(response.status).toBe(400);
      expect(mockUser.incrementEmailOtpAttempts).toHaveBeenCalled();
    });
  });

  // ============================================
  // VERIFICATION ENDPOINTS
  // ============================================

  describe('POST /2fa/verify/totp', () => {
    it('should verify TOTP code for step-up auth', async () => {
      const mockUser = {
        id: 'user-123',
        twoFactorEnabled: true,
        twoFactorMethod: 'totp',
        verifyTotpCode: jest.fn().mockReturnValue(true),
        markTotpCodeUsed: jest.fn().mockResolvedValue(),
        updateStepUpSession: jest.fn().mockResolvedValue(),
      };

      User.findByPk.mockResolvedValue(mockUser);

      const response = await request(app)
        .post('/2fa/verify/totp')
        .send({ code: '123456' });

      expect(response.status).toBe(200);
      expect(response.body.verified).toBe(true);
      expect(mockUser.markTotpCodeUsed).toHaveBeenCalled();
      expect(mockUser.updateStepUpSession).toHaveBeenCalled();
    });

    it('should return 400 when TOTP not enabled', async () => {
      User.findByPk.mockResolvedValue({
        id: 'user-123',
        twoFactorEnabled: false,
      });

      const response = await request(app)
        .post('/2fa/verify/totp')
        .send({ code: '123456' });

      expect(response.status).toBe(400);
    });

    it('should return 400 when wrong 2FA method', async () => {
      User.findByPk.mockResolvedValue({
        id: 'user-123',
        twoFactorEnabled: true,
        twoFactorMethod: 'email',
      });

      const response = await request(app)
        .post('/2fa/verify/totp')
        .send({ code: '123456' });

      expect(response.status).toBe(400);
    });

    it('should return 400 for invalid code', async () => {
      const mockUser = {
        id: 'user-123',
        twoFactorEnabled: true,
        twoFactorMethod: 'totp',
        verifyTotpCode: jest.fn().mockReturnValue(false),
      };

      User.findByPk.mockResolvedValue(mockUser);

      const response = await request(app)
        .post('/2fa/verify/totp')
        .send({ code: '123456' });

      expect(response.status).toBe(400);
    });
  });

  describe('POST /2fa/verify/email/send', () => {
    it('should send email OTP for step-up auth', async () => {
      const mockUser = {
        id: 'user-123',
        twoFactorEnabled: true,
        generateEmailOtp: jest.fn().mockResolvedValue('123456'),
      };

      User.findByPk.mockResolvedValue(mockUser);
      emailServices.auth.sendTwoFactorOtpEmail.mockResolvedValue();

      const response = await request(app)
        .post('/2fa/verify/email/send');

      expect(response.status).toBe(200);
      expect(response.body.message).toContain('Verification code sent');
    });

    it('should return 400 when 2FA not enabled', async () => {
      User.findByPk.mockResolvedValue({
        id: 'user-123',
        twoFactorEnabled: false,
      });

      const response = await request(app)
        .post('/2fa/verify/email/send');

      expect(response.status).toBe(400);
    });

    it('should return 500 when email fails', async () => {
      const mockUser = {
        id: 'user-123',
        twoFactorEnabled: true,
        generateEmailOtp: jest.fn().mockResolvedValue('123456'),
      };

      User.findByPk.mockResolvedValue(mockUser);
      emailServices.auth.sendTwoFactorOtpEmail.mockRejectedValue(new Error('Email failed'));

      const response = await request(app)
        .post('/2fa/verify/email/send');

      expect(response.status).toBe(500);
    });
  });

  describe('POST /2fa/verify/email', () => {
    it('should verify email OTP for step-up auth', async () => {
      const mockUser = {
        id: 'user-123',
        twoFactorEnabled: true,
        isEmailOtpLocked: jest.fn().mockReturnValue(false),
        verifyEmailOtp: jest.fn().mockReturnValue(true),
        updateStepUpSession: jest.fn().mockResolvedValue(),
        clearEmailOtp: jest.fn().mockResolvedValue(),
      };

      User.findByPk.mockResolvedValue(mockUser);

      const response = await request(app)
        .post('/2fa/verify/email')
        .send({ code: '123456' });

      expect(response.status).toBe(200);
      expect(response.body.verified).toBe(true);
    });

    it('should return 400 when 2FA not enabled', async () => {
      User.findByPk.mockResolvedValue({
        id: 'user-123',
        twoFactorEnabled: false,
      });

      const response = await request(app)
        .post('/2fa/verify/email')
        .send({ code: '123456' });

      expect(response.status).toBe(400);
    });

    it('should return 429 when locked', async () => {
      const mockUser = {
        id: 'user-123',
        twoFactorEnabled: true,
        isEmailOtpLocked: jest.fn().mockReturnValue(true),
      };

      User.findByPk.mockResolvedValue(mockUser);

      const response = await request(app)
        .post('/2fa/verify/email')
        .send({ code: '123456' });

      expect(response.status).toBe(429);
    });

    it('should return 400 and increment attempts for invalid OTP', async () => {
      const mockUser = {
        id: 'user-123',
        twoFactorEnabled: true,
        isEmailOtpLocked: jest.fn().mockReturnValue(false),
        verifyEmailOtp: jest.fn().mockReturnValue(false),
        incrementEmailOtpAttempts: jest.fn().mockResolvedValue(),
      };

      User.findByPk.mockResolvedValue(mockUser);

      const response = await request(app)
        .post('/2fa/verify/email')
        .send({ code: '123456' });

      expect(response.status).toBe(400);
      expect(mockUser.incrementEmailOtpAttempts).toHaveBeenCalled();
    });
  });

  describe('POST /2fa/verify/recovery', () => {
    it('should verify recovery code', async () => {
      const mockUser = {
        id: 'user-123',
        twoFactorEnabled: true,
        useRecoveryCode: jest.fn().mockResolvedValue({ valid: true, remainingCodes: 5 }),
      };

      User.findByPk.mockResolvedValue(mockUser);
      emailServices.auth.sendRecoveryCodeUsedEmail.mockResolvedValue();

      const response = await request(app)
        .post('/2fa/verify/recovery')
        .send({ code: 'XXXX-YYYY' });

      expect(response.status).toBe(200);
      expect(response.body.verified).toBe(true);
      expect(response.body.remainingCodes).toBe(5);
    });

    it('should warn when recovery codes are low', async () => {
      const mockUser = {
        id: 'user-123',
        twoFactorEnabled: true,
        useRecoveryCode: jest.fn().mockResolvedValue({ valid: true, remainingCodes: 2 }),
      };

      User.findByPk.mockResolvedValue(mockUser);
      emailServices.auth.sendRecoveryCodeUsedEmail.mockResolvedValue();

      const response = await request(app)
        .post('/2fa/verify/recovery')
        .send({ code: 'XXXX-YYYY' });

      expect(response.status).toBe(200);
      expect(response.body.warning).toContain('running low');
    });

    it('should return 400 when 2FA not enabled', async () => {
      User.findByPk.mockResolvedValue({
        id: 'user-123',
        twoFactorEnabled: false,
      });

      const response = await request(app)
        .post('/2fa/verify/recovery')
        .send({ code: 'XXXX-YYYY' });

      expect(response.status).toBe(400);
    });

    it('should return 400 for invalid recovery code', async () => {
      const mockUser = {
        id: 'user-123',
        twoFactorEnabled: true,
        useRecoveryCode: jest.fn().mockResolvedValue({ valid: false, remainingCodes: 0 }),
      };

      User.findByPk.mockResolvedValue(mockUser);

      const response = await request(app)
        .post('/2fa/verify/recovery')
        .send({ code: 'XXXX-YYYY' });

      expect(response.status).toBe(400);
    });
  });

  // ============================================
  // MANAGEMENT ENDPOINTS
  // ============================================

  describe('GET /2fa/status', () => {
    it('should return 2FA status', async () => {
      const mockUser = {
        id: 'user-123',
        twoFactorEnabled: true,
        twoFactorMethod: 'totp',
        getRemainingRecoveryCodes: jest.fn().mockReturnValue(5),
      };

      User.findByPk.mockResolvedValue(mockUser);

      const response = await request(app)
        .get('/2fa/status');

      expect(response.status).toBe(200);
      expect(response.body.enabled).toBe(true);
      expect(response.body.method).toBe('totp');
      expect(response.body.hasRecoveryCodes).toBe(true);
      expect(response.body.lowRecoveryCodes).toBe(false);
    });

    it('should return low recovery codes warning', async () => {
      const mockUser = {
        id: 'user-123',
        twoFactorEnabled: true,
        twoFactorMethod: 'totp',
        getRemainingRecoveryCodes: jest.fn().mockReturnValue(1),
      };

      User.findByPk.mockResolvedValue(mockUser);

      const response = await request(app)
        .get('/2fa/status');

      expect(response.status).toBe(200);
      expect(response.body.lowRecoveryCodes).toBe(true);
    });

    it('should return 404 when user not found', async () => {
      User.findByPk.mockResolvedValue(null);

      const response = await request(app)
        .get('/2fa/status');

      expect(response.status).toBe(404);
    });
  });

  describe('POST /2fa/disable', () => {
    it('should disable 2FA', async () => {
      const mockUser = {
        id: 'user-123',
        twoFactorEnabled: true,
        disableTwoFactor: jest.fn().mockResolvedValue(),
      };

      User.findByPk.mockResolvedValue(mockUser);
      emailServices.auth.sendTwoFactorDisabledEmail.mockResolvedValue();

      const response = await request(app)
        .post('/2fa/disable');

      expect(response.status).toBe(200);
      expect(response.body.message).toContain('disabled');
      expect(mockUser.disableTwoFactor).toHaveBeenCalled();
    });

    it('should return 400 when 2FA not enabled', async () => {
      User.findByPk.mockResolvedValue({
        id: 'user-123',
        twoFactorEnabled: false,
      });

      const response = await request(app)
        .post('/2fa/disable');

      expect(response.status).toBe(400);
    });

    it('should return 404 when user not found', async () => {
      User.findByPk.mockResolvedValue(null);

      const response = await request(app)
        .post('/2fa/disable');

      expect(response.status).toBe(404);
    });
  });

  describe('POST /2fa/recovery/regenerate', () => {
    it('should regenerate recovery codes', async () => {
      const mockUser = {
        id: 'user-123',
        twoFactorEnabled: true,
        regenerateRecoveryCodes: jest.fn().mockResolvedValue(['NEW1-CODE', 'NEW2-CODE']),
      };

      User.findByPk.mockResolvedValue(mockUser);

      const response = await request(app)
        .post('/2fa/recovery/regenerate');

      expect(response.status).toBe(200);
      expect(response.body.recoveryCodes).toHaveLength(2);
      expect(response.body.warning).toContain('previous codes are now invalid');
    });

    it('should return 400 when 2FA not enabled', async () => {
      User.findByPk.mockResolvedValue({
        id: 'user-123',
        twoFactorEnabled: false,
      });

      const response = await request(app)
        .post('/2fa/recovery/regenerate');

      expect(response.status).toBe(400);
    });

    it('should return 404 when user not found', async () => {
      User.findByPk.mockResolvedValue(null);

      const response = await request(app)
        .post('/2fa/recovery/regenerate');

      expect(response.status).toBe(404);
    });
  });

  describe('GET /2fa/recovery/remaining', () => {
    it('should return recovery codes status', async () => {
      const mockUser = {
        id: 'user-123',
        getRemainingRecoveryCodes: jest.fn().mockReturnValue(8),
      };

      User.findByPk.mockResolvedValue(mockUser);

      const response = await request(app)
        .get('/2fa/recovery/remaining');

      expect(response.status).toBe(200);
      expect(response.body.hasRecoveryCodes).toBe(true);
      expect(response.body.lowRecoveryCodes).toBe(false);
    });

    it('should indicate when low on recovery codes', async () => {
      const mockUser = {
        id: 'user-123',
        getRemainingRecoveryCodes: jest.fn().mockReturnValue(1),
      };

      User.findByPk.mockResolvedValue(mockUser);

      const response = await request(app)
        .get('/2fa/recovery/remaining');

      expect(response.status).toBe(200);
      expect(response.body.lowRecoveryCodes).toBe(true);
    });

    it('should return 404 when user not found', async () => {
      User.findByPk.mockResolvedValue(null);

      const response = await request(app)
        .get('/2fa/recovery/remaining');

      expect(response.status).toBe(404);
    });
  });
});
