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

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

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

describe('LoginForm component', () => {
  const onLogin = jest.fn();

  const site = { login: jest.fn() };

  let wrapper;

  beforeEach(() => {
    site.login.mockResolvedValue();
    validateEmail.mockReturnValue(true);
    wrapper = shallow(
      <LoginForm
        onLogin={ onLogin }
        site={ site }
      >
        Content
      </LoginForm>
    );
  });

  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 typePassword = (value = 'p@55w0rd') =>
    getContext('handleChange')(null, { name: 'password', value });

  /**
   * 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 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('exposes a method to receive changes from fields', () => {
    expect(getContext('handleChange')).toBeDefined();
    expect(typeof getContext('handleChange')).toBe('function');
  });

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

  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', () => {
    typePassword();
    expect(getContext('isFormValid')()).toBe(false);
  });

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

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

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

  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('invalid');
    expect(form).not.toHaveProp('onLogin');
    expect(form).not.toHaveProp('site');
  });

  it('copies propTypes from the Form', () => {
    // Check a representative prop
    expect(LoginForm.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('calls site.login() on submission', async () => {
    site.login.mockClear();
    typeEmail();
    typePassword();

    await doSubmit();

    expect(site.login).toHaveBeenCalled();
    expect(site.login).toHaveBeenCalledWith({
      email: 'test@email',
      password: 'p@55w0rd',
    });
  });

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

    doSubmit();

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

  it('adds a loader to the form when submitting', () => {
    typeEmail();
    typePassword();

    doSubmit();

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

  it('removes the loader when submission is completed', async () => {
    typeEmail();
    typePassword();

    await doSubmit();

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

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

    doSubmit();

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

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

    doSubmit();

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

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

    await doSubmit();

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

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

    doSubmit();

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

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

    doSubmit();

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

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

    doSubmit();

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

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

    doSubmit();

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

  it('trims the email address before submitting', () => {
    site.login.mockClear();
    typeEmail('  test@email  ');
    typePassword();

    doSubmit();

    expect(site.login).toHaveBeenCalled();
    expect(site.login).toHaveBeenCalledWith({
      email: 'test@email',
      password: 'p@55w0rd',
    });
  });

  it('leaves the password intact when submitting', () => {
    site.login.mockClear();
    typeEmail();
    typePassword('  p@55w0rd  ');

    doSubmit();

    expect(site.login).toHaveBeenCalled();
    expect(site.login).toHaveBeenCalledWith({
      email: 'test@email',
      password: '  p@55w0rd  ',
    });
  });

  it('calls the login event handler when submission is completed', async () => {
    onLogin.mockClear();
    typeEmail();
    typePassword();

    await doSubmit();

    expect(onLogin).toHaveBeenCalled();
  });

  it('copes if there is no login event handler', async () => {
    wrapper.setProps({ onLogin: undefined });
    typeEmail();
    typePassword();

    await doSubmit();

    // Test succeeds if no errors
  });

  it('adds an error state if an error occurs', async () => {
    site.login.mockRejectedValue();
    typeEmail();
    typePassword();

    await doSubmit();

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

  it('adds the error flag if an error occurs', async () => {
    site.login.mockRejectedValue();
    typeEmail();
    typePassword();

    await doSubmit();

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

  it('removes the loader if an error occurs', async () => {
    site.login.mockRejectedValue();
    typeEmail();
    typePassword();

    await doSubmit();

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