import { shallow } from 'enzyme';
import React from 'react';
import { Form } from 'semantic-ui-react';

import { validateEmail } from '../../lib';
import Context from './Context';
import { RegisterForm } from './RegisterForm';

jest.mock('../../lib');

describe('RegisterForm component', () => {
  const onRegister = jest.fn();

  const request = {
    setParams: jest.fn().mockReturnThis(),
    promise: jest.fn().mockReturnThis(),
    create: jest.fn().mockResolvedValue({ token: 'test token' }),
  };

  const site = {
    login: jest.fn(),
    req: jest.fn().mockReturnValue(request),
  };

  let wrapper;

  beforeEach(() => {
    request.create.mockResolvedValue({ token: 'test token' });
    site.login.mockResolvedValue();
    validateEmail.mockReturnValue(true);

    wrapper = shallow(
      <RegisterForm
        onRegister={ onRegister }
        site={ site }
      >
        Content
      </RegisterForm>
    );
  });

  const getProvider = () => wrapper.find(Context.Provider);

  const getContext = (field) => getProvider().prop('value')[field];

  const typeEmail = (value = 'test@email') =>
    getContext('handleChange')(null, { name: 'email', value });

  const typeFirstName = (value = 'James') =>
    getContext('handleChange')(null, { name: 'firstName', value });

  const typeLastName = (value = 'Smith') =>
    getContext('handleChange')(null, { name: 'lastName', value });

  const typePassword = (value = 'p@55w0rd') =>
    getContext('handleChange')(null, { name: 'password', value });

  const fillInForm = () => {
    typeEmail();
    typeFirstName();
    typeLastName();
    typePassword();
  };

  /**
   * Submits the form
   * We call the instance method directly so we can await the Promise
   * @returns {Promise<void>} resolves when complete
   */
  const doSubmit = () => wrapper.instance().handleSubmit();

  it('renders', () => {
    expect(wrapper).toExist();
  });

  it('renders a Provider to pass the context', () => {
    expect(getProvider()).toExist();
    expect(getProvider()).toHaveProp('value');
  });

  it('starts out with no checkboxes', () => {
    expect(getContext('checkboxes')).toBeDefined();
    expect(getContext('checkboxes')).toEqual({ });
  });

  it('starts out with an empty email', () => {
    expect(getContext('email')).toBeDefined();
    expect(getContext('email')).toBe('');
  });

  it('starts out without any error state', () => {
    expect(getContext('error')).toBeDefined();
    expect(getContext('error')).toBeNull();
  });

  it('starts out with an empty first name', () => {
    expect(getContext('firstName')).toBeDefined();
    expect(getContext('firstName')).toBe('');
  });

  it('exposes a method to receive changes from fields', () => {
    expect(getContext('handleChange')).toBeDefined();
    expect(typeof getContext('handleChange')).toBe('function');
  });

  it('exposes a method to receive password validation information', () => {
    expect(getContext('handlePasswordValidation')).toBeDefined();
    expect(typeof getContext('handlePasswordValidation')).toBe('function');
  });

  it('allows an email address to be entered', () => {
    typeEmail();
    expect(getContext('email')).toBe('test@email');
  });

  it('allows a first name to be entered', () => {
    typeFirstName();
    expect(getContext('firstName')).toBe('James');
  });

  it('allows a last name to be entered', () => {
    typeLastName();
    expect(getContext('lastName')).toBe('Smith');
  });

  it('allows a password to be entered', () => {
    typePassword();
    expect(getContext('password')).toBe('p@55w0rd');
  });

  it('exposes a method to check whether the form is complete', () => {
    expect(getContext('isFormValid')).toBeDefined();
    expect(typeof getContext('isFormValid')).toBe('function');
  });

  it('starts out incomplete', () => {
    expect(getContext('isFormValid')()).toBe(false);
  });

  it('is still incomplete without an email address', () => {
    typeFirstName();
    typeLastName();
    typePassword();
    expect(getContext('isFormValid')()).toBe(false);
  });

  it('is still incomplete without a first name', () => {
    typeEmail();
    typeLastName();
    typePassword();
    expect(getContext('isFormValid')()).toBe(false);
  });

  it('is still incomplete without a last name', () => {
    typeEmail();
    typeFirstName();
    typePassword();
    expect(getContext('isFormValid')()).toBe(false);
  });

  it('is still incomplete without a password', () => {
    typeEmail();
    typeFirstName();
    typeLastName();
    expect(getContext('isFormValid')()).toBe(false);
  });

  it('is finally complete when all are entered', () => {
    fillInForm();
    expect(getContext('isFormValid')()).toBe(true);
  });

  it('is incomplete if the password is invalid', () => {
    fillInForm();
    getContext('handlePasswordValidation')(false);
    expect(getContext('isFormValid')()).toBe(false);
  });

  it('is incomplete if any checkboxes are unticked', () => {
    fillInForm();
    getContext('handleCheckboxChange')(null, { checked: false, name: 'one' });
    getContext('handleCheckboxChange')(null, { checked: true, name: 'two' });
    expect(getContext('isFormValid')()).toBe(false);
  });

  it('is complete again when the checkbox is ticked', () => {
    fillInForm();
    getContext('handleCheckboxChange')(null, { checked: true, name: 'one' });
    getContext('handleCheckboxChange')(null, { checked: true, name: 'two' });
    expect(getContext('isFormValid')()).toBe(true);
  });

  it('can be marked incomplete', () => {
    fillInForm();
    wrapper.setProps({ invalid: true });
    expect(getContext('isFormValid')()).toBe(false);
  });

  it('starts out with an empty last name', () => {
    expect(getContext('lastName')).toBeDefined();
    expect(getContext('lastName')).toBe('');
  });

  it('starts out with an empty password', () => {
    expect(getContext('password')).toBeDefined();
    expect(getContext('password')).toBe('');
  });

  it('renders a Form', () => {
    expect(wrapper.find('Form')).toExist();
  });

  it('passes props down to the Form', () => {
    wrapper.setProps({ size: 'small' });
    expect(wrapper.find('Form').children()).toHaveText('Content');
    expect(wrapper.find('Form')).toHaveProp('size', 'small');
  });

  it('avoids passing internal props to the Form', () => {
    wrapper.setProps({ invalid: true });
    const form = wrapper.find('Form');
    expect(form).not.toHaveProp('identityProvider');
    expect(form).not.toHaveProp('invalid');
    expect(form).not.toHaveProp('onRegister');
    expect(form).not.toHaveProp('site');
  });

  it('copies propTypes from the Form', () => {
    // Check a representative prop
    expect(RegisterForm.propTypes.size).toBe(Form.propTypes.size);
  });

  it('starts out without the Form error flag', () => {
    expect(wrapper.find('Form')).toHaveProp('error', false);
  });

  it('adds the error flag when requested to by a prop', () => {
    wrapper.setProps({ error: true });
    expect(wrapper.find('Form')).toHaveProp('error', true);
  });

  it('starts out without the Form loader', () => {
    expect(wrapper.find('Form')).toHaveProp('loading', false);
  });

  it('adds the loader when requested to by a prop', () => {
    wrapper.setProps({ loading: true });
    expect(wrapper.find('Form')).toHaveProp('loading', true);
  });

  it('listens to form submit events', () => {
    // We call the instance method directly in tests so need to check it's
    // bound to the correct event
    expect(wrapper.find('Form'))
      .toHaveProp('onSubmit', wrapper.instance().handleSubmit);
  });

  it('registers on submission', async () => {
    site.req.mockClear();
    request.setParams.mockClear();
    request.promise.mockClear();
    request.create.mockClear();
    fillInForm();

    await doSubmit();

    expect(site.req).toHaveBeenCalled();
    expect(site.req).toHaveBeenCalledWith('identityprovider/email/register');

    expect(request.setParams).toHaveBeenCalled();
    expect(request.setParams).toHaveBeenCalledWith({
      email: 'test@email',
      first_name: 'James',
      last_name: 'Smith',
      password: 'p@55w0rd',
    });
    expect(request.promise).toHaveBeenCalled();
    expect(request.create).toHaveBeenCalled();
  });

  it('does nothing if the form is marked incomplete', () => {
    site.req.mockClear();
    fillInForm();
    wrapper.setProps({ invalid: true });

    doSubmit();

    expect(site.req).not.toHaveBeenCalled();
  });

  it('uses the token from registration to login', async () => {
    site.login.mockClear();
    fillInForm();

    await doSubmit();

    expect(site.login).toHaveBeenCalled();
    expect(site.login).toHaveBeenCalledWith({ jwt: 'test token' });
  });

  it('adds a loader to the form when submitting', () => {
    fillInForm();
    doSubmit();
    expect(wrapper.find('Form')).toHaveProp('loading', true);
  });

  it('removes the loader when submission is completed', async () => {
    fillInForm();
    await doSubmit();
    expect(wrapper.find('Form')).toHaveProp('loading', false);
  });

  it('validates the email address before submitting', () => {
    validateEmail.mockClear();
    fillInForm();

    doSubmit();

    expect(validateEmail).toHaveBeenCalled();
    expect(validateEmail).toHaveBeenCalledWith('test@email');
  });

  it('trims the email address before validating', () => {
    validateEmail.mockClear();
    fillInForm();
    typeEmail('  test@email  ');

    doSubmit();

    expect(validateEmail).toHaveBeenCalledWith('test@email');
  });

  it('avoids submitting invalid email addresses', async () => {
    site.login.mockClear();
    validateEmail.mockReturnValue(false);
    fillInForm();

    await doSubmit();

    expect(site.login).not.toHaveBeenCalled();
  });

  it('adds an error state if the email is invalid', () => {
    validateEmail.mockReturnValue(false);
    fillInForm();

    doSubmit();

    expect(getContext('error')).toBe('email');
  });

  it('adds the error flag if the email is invalid', () => {
    validateEmail.mockReturnValue(false);
    fillInForm();

    doSubmit();

    expect(wrapper.find('Form')).toHaveProp('error', true);
  });

  it('removes the loader if the email is invalid', () => {
    validateEmail.mockReturnValue(false);
    fillInForm();

    doSubmit();

    expect(wrapper.find('Form')).toHaveProp('loading', false);
  });

  it('does nothing if the form is incomplete', () => {
    site.req.mockClear();
    validateEmail.mockClear();

    doSubmit();

    expect(validateEmail).not.toHaveBeenCalled();
    expect(site.req).not.toHaveBeenCalled();
  });

  it('trims the email address before submitting', () => {
    request.setParams.mockClear();
    fillInForm();
    typeEmail('  test@email  ');

    doSubmit();

    expect(request.setParams).toHaveBeenCalled();
    expect(request.setParams).toHaveBeenCalledWith({
      email: 'test@email',
      first_name: 'James',
      last_name: 'Smith',
      password: 'p@55w0rd',
    });
  });

  it('trims the first name before submitting', () => {
    request.setParams.mockClear();
    fillInForm();
    typeFirstName('  James  ');

    doSubmit();

    expect(request.setParams).toHaveBeenCalled();
    expect(request.setParams).toHaveBeenCalledWith({
      email: 'test@email',
      first_name: 'James',
      last_name: 'Smith',
      password: 'p@55w0rd',
    });
  });

  it('trims the last name before submitting', () => {
    request.setParams.mockClear();
    fillInForm();
    typeLastName('  Smith  ');

    doSubmit();

    expect(request.setParams).toHaveBeenCalled();
    expect(request.setParams).toHaveBeenCalledWith({
      email: 'test@email',
      first_name: 'James',
      last_name: 'Smith',
      password: 'p@55w0rd',
    });
  });

  it('leaves the password intact when submitting', () => {
    request.setParams.mockClear();
    fillInForm();
    typePassword('  p@55w0rd  ');

    doSubmit();

    expect(request.setParams).toHaveBeenCalled();
    expect(request.setParams).toHaveBeenCalledWith({
      email: 'test@email',
      first_name: 'James',
      last_name: 'Smith',
      password: '  p@55w0rd  ',
    });
  });

  it('allows the IDP to be customised', async () => {
    site.req.mockClear();
    wrapper.setProps({ identityProvider: 'test-idp' });
    fillInForm();

    await doSubmit();

    expect(site.req).toHaveBeenCalledWith('identityprovider/test-idp/register');
  });

  it('calls the register event handler when completed', async () => {
    onRegister.mockClear();
    fillInForm();

    await doSubmit();

    expect(onRegister).toHaveBeenCalled();
  });

  it('copes if there is no register event handler', async () => {
    wrapper.setProps({ onRegister: undefined });
    fillInForm();

    await doSubmit();

    // Test succeeds if no errors
  });

  it('adds an error state if an error occurs when registering', async () => {
    request.create.mockRejectedValue();
    fillInForm();

    await doSubmit();

    expect(getContext('error')).toBe('error');
  });

  it('adds an error state if no token is returned', async () => {
    request.create.mockResolvedValue();
    fillInForm();

    await doSubmit();

    expect(getContext('error')).toBe('error');
  });

  it('adds an error state if an error occurs when logging in', async () => {
    site.login.mockRejectedValue();
    fillInForm();

    await doSubmit();

    expect(getContext('error')).toBe('error');
  });

  it('adds a different error state if already registered', async () => {
    request.create.mockRejectedValue({
      body: {
        code: 'user_exists',
      },
    });
    fillInForm();

    await doSubmit();

    expect(getContext('error')).toBe('registered');
  });

  it('adds the error flag if an error occurs', async () => {
    request.create.mockRejectedValue();
    fillInForm();

    await doSubmit();

    expect(wrapper.find('Form')).toHaveProp('error', true);
  });

  it('removes the loader if an error occurs', async () => {
    request.create.mockRejectedValue();
    fillInForm();

    await doSubmit();

    expect(wrapper.find('Form')).toHaveProp('loading', false);
  });
});
