const crypto = require('crypto');

// Mock crypto module
jest.mock('crypto', () => ({
  randomBytes: jest.fn(),
  createHash: jest.fn(() => ({
    update: jest.fn().mockReturnThis(),
    digest: jest.fn((encoding) => {
      if (encoding === 'hex') {
        return 'a'.repeat(64); // Deterministic hash for testing
      }
      return Buffer.from('a'.repeat(32));
    })
  })),
  timingSafeEqual: jest.fn((a, b) => {
    if (a.length !== b.length) return false;
    return a.equals(b);
  })
}));

// Mock the entire models module
jest.mock('../../../models', () => {
  const mockUser = {
    update: jest.fn(),
    passwordResetToken: null,
    passwordResetTokenExpiry: 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 - Password Reset', () => {
  let mockUser;

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

    // Create a fresh mock user for each test
    mockUser = {
      id: 1,
      email: 'test@example.com',
      firstName: 'Test',
      lastName: 'User',
      password: 'hashedPassword123',
      passwordResetToken: null,
      passwordResetTokenExpiry: null,
      jwtVersion: 0,
      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('generatePasswordResetToken', () => {
    it('should generate a random token and set 1-hour expiry', async () => {
      const mockRandomBytes = Buffer.from('a'.repeat(32));
      const hashedToken = 'a'.repeat(64); // Hashed version stored in DB

      crypto.randomBytes.mockReturnValue(mockRandomBytes);

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

      expect(crypto.randomBytes).toHaveBeenCalledWith(32);
      expect(mockUser.update).toHaveBeenCalledWith(
        expect.objectContaining({
          passwordResetToken: hashedToken // Expect hashed token in DB
        })
      );

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

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

    it('should update the user with token and expiry', async () => {
      const mockRandomBytes = Buffer.from('b'.repeat(32));
      const plainToken = mockRandomBytes.toString('hex');

      crypto.randomBytes.mockReturnValue(mockRandomBytes);

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

      expect(mockUser.update).toHaveBeenCalledTimes(1);
      expect(result).toBe(plainToken); // Method returns plain token
      expect(mockUser.passwordResetToken).toBe('a'.repeat(64)); // DB has hashed token
      expect(mockUser.passwordResetTokenExpiry).toBeInstanceOf(Date);
    });

    it('should generate unique tokens on multiple calls', async () => {
      const mockRandomBytes1 = Buffer.from('a'.repeat(32));
      const mockRandomBytes2 = Buffer.from('b'.repeat(32));
      const plainToken1 = mockRandomBytes1.toString('hex');
      const plainToken2 = mockRandomBytes2.toString('hex');

      crypto.randomBytes
        .mockReturnValueOnce(mockRandomBytes1)
        .mockReturnValueOnce(mockRandomBytes2);

      const result1 = await User.prototype.generatePasswordResetToken.call(mockUser);
      const result2 = await User.prototype.generatePasswordResetToken.call(mockUser);

      // Plain tokens returned should be different
      expect(result1).toBe(plainToken1);
      expect(result2).toBe(plainToken2);
      expect(plainToken1).not.toBe(plainToken2);
    });
  });

  describe('isPasswordResetTokenValid', () => {
    it('should return true for valid token and non-expired time', () => {
      const plainToken = 'valid-token-123';
      const hashedToken = 'a'.repeat(64); // Mocked hash result
      const futureExpiry = new Date(Date.now() + 30 * 60 * 1000); // 30 minutes from now

      mockUser.passwordResetToken = hashedToken; // Store hashed token
      mockUser.passwordResetTokenExpiry = futureExpiry;

      const result = User.prototype.isPasswordResetTokenValid.call(mockUser, plainToken);

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

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

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

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

    it('should return false for missing expiry', () => {
      const hashedToken = 'a'.repeat(64);
      mockUser.passwordResetToken = hashedToken;
      mockUser.passwordResetTokenExpiry = null;

      const result = User.prototype.isPasswordResetTokenValid.call(mockUser, 'valid-token');

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

    it('should return false for mismatched token', () => {
      const hashedToken = 'a'.repeat(64);
      mockUser.passwordResetToken = hashedToken;
      mockUser.passwordResetTokenExpiry = new Date(Date.now() + 30 * 60 * 1000);

      // Mock createHash to return different hash for wrong token
      crypto.createHash.mockReturnValueOnce({
        update: jest.fn().mockReturnThis(),
        digest: jest.fn(() => 'b'.repeat(64)) // Different hash
      });

      const result = User.prototype.isPasswordResetTokenValid.call(mockUser, 'wrong-token');

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

    it('should return false for expired token', () => {
      const plainToken = 'valid-token-123';
      const hashedToken = 'a'.repeat(64);
      const pastExpiry = new Date(Date.now() - 10 * 60 * 1000); // 10 minutes ago

      mockUser.passwordResetToken = hashedToken;
      mockUser.passwordResetTokenExpiry = pastExpiry;

      const result = User.prototype.isPasswordResetTokenValid.call(mockUser, plainToken);

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

    it('should return false for token expiring in the past by 1 second', () => {
      const plainToken = 'valid-token-123';
      const hashedToken = 'a'.repeat(64);
      const pastExpiry = new Date(Date.now() - 1000); // 1 second ago

      mockUser.passwordResetToken = hashedToken;
      mockUser.passwordResetTokenExpiry = pastExpiry;

      const result = User.prototype.isPasswordResetTokenValid.call(mockUser, plainToken);

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

    it('should handle edge case of token expiring exactly now', () => {
      const plainToken = 'valid-token-123';
      const hashedToken = 'a'.repeat(64);
      // Set expiry 1ms in the future to handle timing precision
      const nowExpiry = new Date(Date.now() + 1);

      mockUser.passwordResetToken = hashedToken;
      mockUser.passwordResetTokenExpiry = nowExpiry;

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

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

    it('should handle string dates correctly', () => {
      const plainToken = 'valid-token-123';
      const hashedToken = 'a'.repeat(64);
      const futureExpiry = new Date(Date.now() + 30 * 60 * 1000).toISOString(); // String date

      mockUser.passwordResetToken = hashedToken;
      mockUser.passwordResetTokenExpiry = futureExpiry;

      const result = User.prototype.isPasswordResetTokenValid.call(mockUser, plainToken);

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

  describe('resetPassword', () => {
    it('should update password and clear token fields', async () => {
      mockUser.passwordResetToken = 'some-token';
      mockUser.passwordResetTokenExpiry = new Date();
      mockUser.jwtVersion = 0;

      await User.prototype.resetPassword.call(mockUser, 'newSecurePassword123!');

      expect(mockUser.update).toHaveBeenCalledWith({
        password: 'newSecurePassword123!',
        passwordResetToken: null,
        passwordResetTokenExpiry: null,
        jwtVersion: 1
      });
    });

    it('should return updated user object', async () => {
      mockUser.jwtVersion = 0;
      const result = await User.prototype.resetPassword.call(mockUser, 'newPassword123!');

      expect(result.password).toBe('newPassword123!');
      expect(result.passwordResetToken).toBe(null);
      expect(result.passwordResetTokenExpiry).toBe(null);
      expect(result.jwtVersion).toBe(1);
    });

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

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

  describe('Complete password reset flow', () => {
    it('should complete full password reset flow successfully', async () => {
      // Step 1: Generate password reset token
      const mockRandomBytes = Buffer.from('c'.repeat(32));
      const plainToken = mockRandomBytes.toString('hex');
      const hashedToken = 'a'.repeat(64);
      crypto.randomBytes.mockReturnValue(mockRandomBytes);

      const returnedToken = await User.prototype.generatePasswordResetToken.call(mockUser);

      expect(returnedToken).toBe(plainToken); // Returns plain token
      expect(mockUser.passwordResetToken).toBe(hashedToken); // Stores hashed token
      expect(mockUser.passwordResetTokenExpiry).toBeInstanceOf(Date);

      // Step 2: Validate token (pass plain token, compares with hashed)
      const isValid = User.prototype.isPasswordResetTokenValid.call(mockUser, plainToken);
      expect(isValid).toBe(true);

      // Step 3: Reset password
      mockUser.jwtVersion = 0;
      await User.prototype.resetPassword.call(mockUser, 'newPassword123!');

      expect(mockUser.password).toBe('newPassword123!');
      expect(mockUser.passwordResetToken).toBe(null);
      expect(mockUser.passwordResetTokenExpiry).toBe(null);
      expect(mockUser.jwtVersion).toBe(1);
    });

    it('should fail password reset with wrong token', async () => {
      // Generate token
      const mockRandomBytes = Buffer.from('d'.repeat(32));
      const plainToken = mockRandomBytes.toString('hex');
      const hashedToken = 'a'.repeat(64);
      crypto.randomBytes.mockReturnValue(mockRandomBytes);

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

      // Mock createHash to return different hash for wrong token
      crypto.createHash.mockReturnValueOnce({
        update: jest.fn().mockReturnThis(),
        digest: jest.fn(() => 'b'.repeat(64)) // Different hash
      });

      // Try to validate with wrong token
      const isValid = User.prototype.isPasswordResetTokenValid.call(mockUser, 'wrong-token');

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

    it('should fail password reset with expired token', async () => {
      // Manually set an expired token
      const hashedToken = 'a'.repeat(64);
      mockUser.passwordResetToken = hashedToken;
      mockUser.passwordResetTokenExpiry = new Date(Date.now() - 2 * 60 * 60 * 1000); // 2 hours ago

      const isValid = User.prototype.isPasswordResetTokenValid.call(mockUser, 'expired-token');

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

    it('should not allow password reset after token has been used', async () => {
      // Generate token
      const mockRandomBytes = Buffer.from('e'.repeat(32));
      const plainToken = mockRandomBytes.toString('hex');
      crypto.randomBytes.mockReturnValue(mockRandomBytes);

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

      // Reset password (clears token)
      mockUser.jwtVersion = 0;
      await User.prototype.resetPassword.call(mockUser, 'newPassword123!');

      // Try to validate same token again (should fail because it's cleared)
      const isValid = User.prototype.isPasswordResetTokenValid.call(mockUser, plainToken);

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