'use strict';

const Config = require('../lib/Config');
const Definitions = require('../lib/Options/Definitions');
const request = require('../lib/request');

const loginWithWrongCredentialsShouldFail = function (username, password) {
  return new Promise((resolve, reject) => {
    Parse.User.logIn(username, password)
      .then(() => reject('login should have failed'))
      .catch(err => {
        if (err.message === 'Invalid username/password.') {
          resolve();
        } else {
          reject(err);
        }
      });
  });
};

const isAccountLockoutError = function (username, password, duration, waitTime) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      Parse.User.logIn(username, password)
        .then(() => reject('login should have failed'))
        .catch(err => {
          if (
            err.message ===
            'Your account is locked due to multiple failed login attempts. Please try again after ' +
              duration +
              ' minute(s)'
          ) {
            resolve();
          } else {
            reject(err);
          }
        });
    }, waitTime);
  });
};

describe('Account Lockout Policy: ', () => {
  it('account should not be locked even after failed login attempts if account lockout policy is not set', done => {
    reconfigureServer({
      appName: 'unlimited',
      publicServerURL: 'http://localhost:1337/1',
    })
      .then(() => {
        const user = new Parse.User();
        user.setUsername('username1');
        user.setPassword('password');
        return user.signUp(null);
      })
      .then(() => {
        return loginWithWrongCredentialsShouldFail('username1', 'incorrect password 1');
      })
      .then(() => {
        return loginWithWrongCredentialsShouldFail('username1', 'incorrect password 2');
      })
      .then(() => {
        return loginWithWrongCredentialsShouldFail('username1', 'incorrect password 3');
      })
      .then(() => done())
      .catch(err => {
        fail('allow unlimited failed login attempts failed: ' + JSON.stringify(err));
        done();
      });
  });

  it('throw error if duration is set to an invalid number', done => {
    reconfigureServer({
      appName: 'duration',
      accountLockout: {
        duration: 'invalid value',
        threshold: 5,
      },
      publicServerURL: 'https://my.public.server.com/1',
    })
      .then(() => {
        Config.get('test');
        fail('set duration to an invalid number test failed');
        done();
      })
      .catch(err => {
        if (
          err &&
          err === 'Account lockout duration should be greater than 0 and less than 100000'
        ) {
          done();
        } else {
          fail('set duration to an invalid number test failed: ' + JSON.stringify(err));
          done();
        }
      });
  });

  it('throw error if threshold is set to an invalid number', done => {
    reconfigureServer({
      appName: 'threshold',
      accountLockout: {
        duration: 5,
        threshold: 'invalid number',
      },
      publicServerURL: 'https://my.public.server.com/1',
    })
      .then(() => {
        Config.get('test');
        fail('set threshold to an invalid number test failed');
        done();
      })
      .catch(err => {
        if (
          err &&
          err === 'Account lockout threshold should be an integer greater than 0 and less than 1000'
        ) {
          done();
        } else {
          fail('set threshold to an invalid number test failed: ' + JSON.stringify(err));
          done();
        }
      });
  });

  it('throw error if threshold is < 1', done => {
    reconfigureServer({
      appName: 'threshold',
      accountLockout: {
        duration: 5,
        threshold: 0,
      },
      publicServerURL: 'https://my.public.server.com/1',
    })
      .then(() => {
        Config.get('test');
        fail('threshold value < 1 is invalid test failed');
        done();
      })
      .catch(err => {
        if (
          err &&
          err === 'Account lockout threshold should be an integer greater than 0 and less than 1000'
        ) {
          done();
        } else {
          fail('threshold value < 1 is invalid test failed: ' + JSON.stringify(err));
          done();
        }
      });
  });

  it('throw error if threshold is > 999', done => {
    reconfigureServer({
      appName: 'threshold',
      accountLockout: {
        duration: 5,
        threshold: 1000,
      },
      publicServerURL: 'https://my.public.server.com/1',
    })
      .then(() => {
        Config.get('test');
        fail('threshold value > 999 is invalid test failed');
        done();
      })
      .catch(err => {
        if (
          err &&
          err === 'Account lockout threshold should be an integer greater than 0 and less than 1000'
        ) {
          done();
        } else {
          fail('threshold value > 999 is invalid test failed: ' + JSON.stringify(err));
          done();
        }
      });
  });

  it('throw error if duration is <= 0', done => {
    reconfigureServer({
      appName: 'duration',
      accountLockout: {
        duration: 0,
        threshold: 5,
      },
      publicServerURL: 'https://my.public.server.com/1',
    })
      .then(() => {
        Config.get('test');
        fail('duration value < 1 is invalid test failed');
        done();
      })
      .catch(err => {
        if (
          err &&
          err === 'Account lockout duration should be greater than 0 and less than 100000'
        ) {
          done();
        } else {
          fail('duration value < 1 is invalid test failed: ' + JSON.stringify(err));
          done();
        }
      });
  });

  it('throw error if duration is > 99999', done => {
    reconfigureServer({
      appName: 'duration',
      accountLockout: {
        duration: 100000,
        threshold: 5,
      },
      publicServerURL: 'https://my.public.server.com/1',
    })
      .then(() => {
        Config.get('test');
        fail('duration value > 99999 is invalid test failed');
        done();
      })
      .catch(err => {
        if (
          err &&
          err === 'Account lockout duration should be greater than 0 and less than 100000'
        ) {
          done();
        } else {
          fail('duration value > 99999 is invalid test failed: ' + JSON.stringify(err));
          done();
        }
      });
  });

  it('lock account if failed login attempts are above threshold', done => {
    reconfigureServer({
      appName: 'lockout threshold',
      accountLockout: {
        duration: 1,
        threshold: 2,
      },
      publicServerURL: 'http://localhost:8378/1',
    })
      .then(() => {
        const user = new Parse.User();
        user.setUsername('username2');
        user.setPassword('failedLoginAttemptsThreshold');
        return user.signUp();
      })
      .then(() => {
        return loginWithWrongCredentialsShouldFail('username2', 'wrong password');
      })
      .then(() => {
        return loginWithWrongCredentialsShouldFail('username2', 'wrong password');
      })
      .then(() => {
        return isAccountLockoutError('username2', 'wrong password', 1, 1);
      })
      .then(() => {
        done();
      })
      .catch(err => {
        fail('lock account after failed login attempts test failed: ' + JSON.stringify(err));
        done();
      });
  });

  it('lock account for accountPolicy.duration minutes if failed login attempts are above threshold', done => {
    reconfigureServer({
      appName: 'lockout threshold',
      accountLockout: {
        duration: 0.05, // 0.05*60 = 3 secs
        threshold: 2,
      },
      publicServerURL: 'http://localhost:8378/1',
    })
      .then(() => {
        const user = new Parse.User();
        user.setUsername('username3');
        user.setPassword('failedLoginAttemptsThreshold');
        return user.signUp();
      })
      .then(() => {
        return loginWithWrongCredentialsShouldFail('username3', 'wrong password');
      })
      .then(() => {
        return loginWithWrongCredentialsShouldFail('username3', 'wrong password');
      })
      .then(() => {
        return isAccountLockoutError('username3', 'wrong password', 0.05, 1);
      })
      .then(() => {
        // account should still be locked even after 2 seconds.
        return isAccountLockoutError('username3', 'wrong password', 0.05, 2000);
      })
      .then(() => {
        done();
      })
      .catch(err => {
        fail('account should be locked for duration mins test failed: ' + JSON.stringify(err));
        done();
      });
  });

  it('allow login for locked account after accountPolicy.duration minutes', done => {
    reconfigureServer({
      appName: 'lockout threshold',
      accountLockout: {
        duration: 0.05, // 0.05*60 = 3 secs
        threshold: 2,
      },
      publicServerURL: 'http://localhost:8378/1',
    })
      .then(() => {
        const user = new Parse.User();
        user.setUsername('username4');
        user.setPassword('correct password');
        return user.signUp();
      })
      .then(() => {
        return loginWithWrongCredentialsShouldFail('username4', 'wrong password');
      })
      .then(() => {
        return loginWithWrongCredentialsShouldFail('username4', 'wrong password');
      })
      .then(() => {
        // allow locked user to login after 3 seconds with a valid userid and password
        return new Promise((resolve, reject) => {
          setTimeout(() => {
            Parse.User.logIn('username4', 'correct password')
              .then(() => resolve())
              .catch(err => reject(err));
          }, 3001);
        });
      })
      .then(() => {
        done();
      })
      .catch(err => {
        fail(
          'allow login for locked account after accountPolicy.duration minutes test failed: ' +
            JSON.stringify(err)
        );
        done();
      });
  });
});

describe('lockout with password reset option', () => {
  let sendPasswordResetEmail;

  async function setup(options = {}) {
    const accountLockout = Object.assign(
      {
        duration: 10000,
        threshold: 1,
      },
      options
    );
    const config = {
      appName: 'exampleApp',
      accountLockout: accountLockout,
      publicServerURL: 'http://localhost:8378/1',
      emailAdapter: {
        sendVerificationEmail: () => Promise.resolve(),
        sendPasswordResetEmail: () => Promise.resolve(),
        sendMail: () => {},
      },
    };
    await reconfigureServer(config);

    sendPasswordResetEmail = spyOn(config.emailAdapter, 'sendPasswordResetEmail').and.callThrough();
  }

  it('accepts valid unlockOnPasswordReset option', async () => {
    const values = [true, false];

    for (const value of values) {
      await expectAsync(setup({ unlockOnPasswordReset: value })).toBeResolved();
    }
  });

  it('rejects invalid unlockOnPasswordReset option', async () => {
    const values = ['a', 0, {}, [], null];

    for (const value of values) {
      await expectAsync(setup({ unlockOnPasswordReset: value })).toBeRejected();
    }
  });

  it('uses default value if unlockOnPasswordReset is not set', async () => {
    await expectAsync(setup({ unlockOnPasswordReset: undefined })).toBeResolved();

    const parseConfig = Config.get(Parse.applicationId);
    expect(parseConfig.accountLockout.unlockOnPasswordReset).toBe(
      Definitions.AccountLockoutOptions.unlockOnPasswordReset.default
    );
  });

  it('allow login for locked account after password reset', async () => {
    await setup({ unlockOnPasswordReset: true });
    const config = Config.get(Parse.applicationId);

    const user = new Parse.User();
    const username = 'exampleUsername';
    const password = 'examplePassword';
    user.setUsername(username);
    user.setPassword(password);
    user.setEmail('mail@example.com');
    await user.signUp();

    await expectAsync(Parse.User.logIn(username, 'incorrectPassword')).toBeRejected();
    await expectAsync(Parse.User.logIn(username, password)).toBeRejected();

    await Parse.User.requestPasswordReset(user.getEmail());
    await expectAsync(Parse.User.logIn(username, password)).toBeRejected();

    const link = sendPasswordResetEmail.calls.all()[0].args[0].link;
    const linkUrl = new URL(link);
    const token = linkUrl.searchParams.get('token');
    const newPassword = 'newPassword';
    await request({
      method: 'POST',
      url: `${config.publicServerURL}/apps/test/request_password_reset`,
      body: `new_password=${newPassword}&token=${token}`,
      headers: {
        'Content-Type': 'application/x-www-form-urlencoded',
      },
      followRedirects: false,
    });

    await expectAsync(Parse.User.logIn(username, newPassword)).toBeResolved();
  });

  it('reject login for locked account after password reset (default)', async () => {
    await setup();
    const config = Config.get(Parse.applicationId);

    const user = new Parse.User();
    const username = 'exampleUsername';
    const password = 'examplePassword';
    user.setUsername(username);
    user.setPassword(password);
    user.setEmail('mail@example.com');
    await user.signUp();

    await expectAsync(Parse.User.logIn(username, 'incorrectPassword')).toBeRejected();
    await expectAsync(Parse.User.logIn(username, password)).toBeRejected();

    await Parse.User.requestPasswordReset(user.getEmail());
    await expectAsync(Parse.User.logIn(username, password)).toBeRejected();

    const link = sendPasswordResetEmail.calls.all()[0].args[0].link;
    const linkUrl = new URL(link);
    const token = linkUrl.searchParams.get('token');
    const newPassword = 'newPassword';
    await request({
      method: 'POST',
      url: `${config.publicServerURL}/apps/test/request_password_reset`,
      body: `new_password=${newPassword}&token=${token}`,
      headers: {
        'Content-Type': 'application/x-www-form-urlencoded',
      },
      followRedirects: false,
    });

    await expectAsync(Parse.User.logIn(username, newPassword)).toBeRejected();
  });
});
