const crypto = require('crypto');

// Mock crypto module
jest.mock('crypto');

// Mock the entire models module
jest.mock('../../../models', () => {
  const mockUser = {
    update: jest.fn(),
    verificationToken: null,
    verificationTokenExpiry: null,
    isVerified: false,
    verifiedAt: null
  };

  return {
    User: mockUser,
    sequelize: {
      models: {
        User: mockUser
      }
    }
  };
});

// Import User model methods - we'll test them directly
const User = require('../../../models/User');

describe('User Model - Email Verification', () => {
  let mockUser;

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

    // Create a fresh mock user for each test
    mockUser = {
      id: 1,
      email: 'test@example.com',
      firstName: 'Test',
      lastName: 'User',
      verificationToken: null,
      verificationTokenExpiry: null,
      verificationAttempts: 0,
      isVerified: false,
      verifiedAt: null,
      update: jest.fn().mockImplementation(function(updates) {
        Object.assign(this, updates);
        return Promise.resolve(this);
      })
    };

    // Add the prototype methods to mockUser
    Object.setPrototypeOf(mockUser, User.prototype);
  });

  describe('generateVerificationToken', () => {
    it('should generate a 6-digit code and set 24-hour expiry', async () => {
      const mockCode = 123456;
      crypto.randomInt.mockReturnValue(mockCode);

      await User.prototype.generateVerificationToken.call(mockUser);

      expect(crypto.randomInt).toHaveBeenCalledWith(100000, 999999);
      expect(mockUser.update).toHaveBeenCalledWith(
        expect.objectContaining({
          verificationToken: '123456',
          verificationAttempts: 0,
        })
      );

      // Check that expiry is approximately 24 hours from now
      const updateCall = mockUser.update.mock.calls[0][0];
      const expiryTime = updateCall.verificationTokenExpiry.getTime();
      const expectedExpiry = Date.now() + 24 * 60 * 60 * 1000;

      expect(expiryTime).toBeGreaterThan(expectedExpiry - 1000);
      expect(expiryTime).toBeLessThan(expectedExpiry + 1000);
    });

    it('should update the user with code and expiry', async () => {
      const mockCode = 654321;
      crypto.randomInt.mockReturnValue(mockCode);

      const result = await User.prototype.generateVerificationToken.call(mockUser);

      expect(mockUser.update).toHaveBeenCalledTimes(1);
      expect(result.verificationToken).toBe('654321');
      expect(result.verificationTokenExpiry).toBeInstanceOf(Date);
    });

    it('should generate unique codes on multiple calls', async () => {
      crypto.randomInt
        .mockReturnValueOnce(111111)
        .mockReturnValueOnce(222222);

      await User.prototype.generateVerificationToken.call(mockUser);
      const firstCode = mockUser.update.mock.calls[0][0].verificationToken;

      await User.prototype.generateVerificationToken.call(mockUser);
      const secondCode = mockUser.update.mock.calls[1][0].verificationToken;

      expect(firstCode).not.toBe(secondCode);
    });
  });

  describe('isVerificationTokenValid', () => {
    beforeEach(() => {
      // Mock timingSafeEqual to do a simple comparison
      crypto.timingSafeEqual = jest.fn((a, b) => a.equals(b));
    });

    it('should return true for valid token and non-expired time', () => {
      const validToken = '123456';
      const futureExpiry = new Date(Date.now() + 60 * 60 * 1000); // 1 hour from now

      mockUser.verificationToken = validToken;
      mockUser.verificationTokenExpiry = futureExpiry;

      const result = User.prototype.isVerificationTokenValid.call(mockUser, validToken);

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

    it('should return false for missing token', () => {
      mockUser.verificationToken = null;
      mockUser.verificationTokenExpiry = new Date(Date.now() + 60 * 60 * 1000);

      const result = User.prototype.isVerificationTokenValid.call(mockUser, 'any-token');

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

    it('should return false for missing expiry', () => {
      mockUser.verificationToken = '123456';
      mockUser.verificationTokenExpiry = null;

      const result = User.prototype.isVerificationTokenValid.call(mockUser, '123456');

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

    it('should return false for mismatched token', () => {
      mockUser.verificationToken = '123456';
      mockUser.verificationTokenExpiry = new Date(Date.now() + 60 * 60 * 1000);

      const result = User.prototype.isVerificationTokenValid.call(mockUser, '654321');

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

    it('should return false for expired token', () => {
      const validToken = '123456';
      const pastExpiry = new Date(Date.now() - 60 * 60 * 1000); // 1 hour ago

      mockUser.verificationToken = validToken;
      mockUser.verificationTokenExpiry = pastExpiry;

      const result = User.prototype.isVerificationTokenValid.call(mockUser, validToken);

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

    it('should return false for token expiring in the past by 1 second', () => {
      const validToken = '123456';
      const pastExpiry = new Date(Date.now() - 1000); // 1 second ago

      mockUser.verificationToken = validToken;
      mockUser.verificationTokenExpiry = pastExpiry;

      const result = User.prototype.isVerificationTokenValid.call(mockUser, validToken);

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

    it('should handle edge case of token expiring exactly now', () => {
      const validToken = '123456';
      // Set expiry 1ms in the future to handle timing precision
      const nowExpiry = new Date(Date.now() + 1);

      mockUser.verificationToken = validToken;
      mockUser.verificationTokenExpiry = nowExpiry;

      // This should be true because expiry is slightly in the future
      const result = User.prototype.isVerificationTokenValid.call(mockUser, validToken);

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

    it('should handle string dates correctly', () => {
      const validToken = '123456';
      const futureExpiry = new Date(Date.now() + 60 * 60 * 1000).toISOString(); // String date

      mockUser.verificationToken = validToken;
      mockUser.verificationTokenExpiry = futureExpiry;

      const result = User.prototype.isVerificationTokenValid.call(mockUser, validToken);

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

  describe('verifyEmail', () => {
    it('should mark user as verified and clear token fields', async () => {
      mockUser.verificationToken = '123456';
      mockUser.verificationTokenExpiry = new Date();

      await User.prototype.verifyEmail.call(mockUser);

      expect(mockUser.update).toHaveBeenCalledWith(
        expect.objectContaining({
          isVerified: true,
          verificationToken: null,
          verificationTokenExpiry: null
        })
      );
    });

    it('should set verifiedAt timestamp', async () => {
      const beforeTime = Date.now();

      await User.prototype.verifyEmail.call(mockUser);

      const updateCall = mockUser.update.mock.calls[0][0];
      const verifiedAtTime = updateCall.verifiedAt.getTime();
      const afterTime = Date.now();

      expect(verifiedAtTime).toBeGreaterThanOrEqual(beforeTime);
      expect(verifiedAtTime).toBeLessThanOrEqual(afterTime);
    });

    it('should return updated user object', async () => {
      const result = await User.prototype.verifyEmail.call(mockUser);

      expect(result.isVerified).toBe(true);
      expect(result.verificationToken).toBe(null);
      expect(result.verificationTokenExpiry).toBe(null);
      expect(result.verifiedAt).toBeInstanceOf(Date);
    });

    it('should call update only once', async () => {
      await User.prototype.verifyEmail.call(mockUser);

      expect(mockUser.update).toHaveBeenCalledTimes(1);
    });
  });

  describe('Complete verification flow', () => {
    beforeEach(() => {
      crypto.timingSafeEqual = jest.fn((a, b) => a.equals(b));
    });

    it('should complete full verification flow successfully', async () => {
      // Step 1: Generate verification code
      const mockCode = 999888;
      crypto.randomInt.mockReturnValue(mockCode);

      await User.prototype.generateVerificationToken.call(mockUser);

      expect(mockUser.verificationToken).toBe('999888');
      expect(mockUser.verificationTokenExpiry).toBeInstanceOf(Date);

      // Step 2: Validate code
      const isValid = User.prototype.isVerificationTokenValid.call(mockUser, '999888');
      expect(isValid).toBe(true);

      // Step 3: Verify email
      await User.prototype.verifyEmail.call(mockUser);

      expect(mockUser.isVerified).toBe(true);
      expect(mockUser.verificationToken).toBe(null);
      expect(mockUser.verificationTokenExpiry).toBe(null);
      expect(mockUser.verifiedAt).toBeInstanceOf(Date);
    });

    it('should fail verification with wrong token', async () => {
      // Generate code
      crypto.randomInt.mockReturnValue(123456);

      await User.prototype.generateVerificationToken.call(mockUser);

      // Try to validate with wrong code
      const isValid = User.prototype.isVerificationTokenValid.call(mockUser, '654321');

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

    it('should fail verification with expired token', async () => {
      // Manually set an expired token
      mockUser.verificationToken = '123456';
      mockUser.verificationTokenExpiry = new Date(Date.now() - 25 * 60 * 60 * 1000); // 25 hours ago

      const isValid = User.prototype.isVerificationTokenValid.call(mockUser, '123456');

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