import React, { useState, useCallback, useContext, useRef } from 'react';
import { render, cleanup, fireEvent } from '@testing-library/react';
import '@testing-library/jest-dom/extend-expect';
import {
  FieldGroup,
  Form,
  FormContext,
  FormContextValue,
  Field,
  Input,
  StripeElement,
  SubmitButton,
  Checkbox,
  defaultInputValidator,
} from './index';
import {
  useValidatorState,
  Validator,
  State as ValidatorState,
  MiddlewareReducer as ValidatorMiddlewareReducer,
} from '../../lib/validator';

afterEach(cleanup);

describe('Form', () => {
  it('renders a form that provides children with a validator', () => {
    const validatorStateRef = mkValidatorStateRef();
    const { container } = render(
      <TestForm validatorStateRef={validatorStateRef}>
        <Field fieldType="input" name="foo" label="This is a label">
          <p>Hi mom</p>
        </Field>
      </TestForm>
    );
    expect(container.querySelector('form')).not.toBeNull();
    expect(validatorStateRef.current).toEqual({
      error: null,
      fields: {
        foo: {
          error: null,
          fieldType: 'input',
          required: false,
          valid: null,
          value: null,
        },
      },
    });
  });
});

describe('FieldGroup', () => {
  it('wraps children in className="input-row-group"', () => {
    const { container } = render(
      <FieldGroup>
        <div>Hi mom</div>
      </FieldGroup>
    );
    const groupEl = container.querySelector('.input-row-group');
    expect(groupEl).not.toBeNull();
  });
});

describe('Field', () => {
  it('renders a label when available', () => {
    const { container } = render(
      <TestForm>
        <Field fieldType="input" name="foo" label="This is a label">
          <p>Hi mom</p>
        </Field>
      </TestForm>
    );
    const label = container.querySelector('label .label-text');
    expect(label).not.toBeNull();
    if (label) {
      expect(label.textContent).toEqual('This is a label');
    }
  });

  it('renders a tooltip for errors', () => {
    const Subject = () => {
      const tooltipParentRef = useRef<HTMLDivElement>(null);
      return (
        <TestForm>
          <Field
            fieldType="input"
            name="foo"
            label="This is a label"
            tooltip={true}
            tooltipParentRef={tooltipParentRef}
          >
            <p ref={tooltipParentRef}>Hi mom</p>
          </Field>
          <TestValidatorFn
            fn={(validator) => {
              validator.updateField({
                name: 'foo',
                value: 'bar',
                valid: false,
                error: 'This is an error',
              });
            }}
          />
        </TestForm>
      );
    };
    const { container, queryAllByTestId } = render(<Subject />);
    queryAllByTestId('execute').forEach(fireEvent.click);
    const tooltip = container.querySelector('aside.tooltip');
    expect(tooltip).not.toBeNull();
  });

  it('omits a tooltip if tooltip=false', () => {
    const Subject = () => {
      return (
        <TestForm>
          <Field
            fieldType="input"
            name="foo"
            label="This is a label"
            tooltip={false}
          >
            <p>Hi mom</p>
          </Field>
          <TestValidatorFn
            fn={(validator) => {
              validator.updateField({
                name: 'foo',
                value: 'bar',
                valid: false,
                error: 'This is an error',
              });
            }}
          />
        </TestForm>
      );
    };
    const { container, queryAllByTestId } = render(<Subject />);
    queryAllByTestId('execute').forEach(fireEvent.click);
    const tooltip = container.querySelector('aside.tooltip');
    expect(tooltip).toBeNull();
  });

  it('registers a field with validator state', () => {
    const validatorStateRef = mkValidatorStateRef();
    render(
      <TestForm validatorStateRef={validatorStateRef}>
        <Field fieldType="input" name="foo">
          <p>Hi mom</p>
        </Field>
      </TestForm>
    );
    expect(validatorStateRef.current.fields.foo).toEqual({
      fieldType: 'input',
      required: false,
      value: null,
      valid: null,
      error: null,
    });
  });
});

describe('defaultInputValidator', () => {
  it('returns correct string when not provided `getString` prop', () => {
    const focused = false;
    const props = { name: 'my-field', required: true };

    expect(
      defaultInputValidator('', focused, props, (id: any) => id).error
    ).toEqual('default-input-error');

    expect(defaultInputValidator('', focused, props).error).toEqual(
      'This field is required'
    );
  });
});

describe('Input', () => {
  it('considers an optional field without onValidate as always valid', () => {
    const validatorStateRef = mkValidatorStateRef();
    const { getByTestId } = render(
      <TestForm validatorStateRef={validatorStateRef}>
        <Input
          data-testid="input-1"
          type="text"
          name="input-1"
          initialValue="bar"
        />
      </TestForm>
    );

    fireEvent.change(getByTestId('input-1'), { target: { value: '' } });
    fireEvent.blur(getByTestId('input-1'));
    expect(validatorStateRef.current.fields['input-1'].valid).toEqual(true);
  });

  it('enforces non-empty content in required fields', () => {
    const validatorStateRef = mkValidatorStateRef();
    const { getByTestId } = render(
      <TestForm validatorStateRef={validatorStateRef}>
        <Input
          data-testid="input-1"
          type="text"
          name="input-1"
          required
          initialValue="bar"
        />
        <Input data-testid="input-2" type="text" name="input-2" required />
      </TestForm>
    );

    fireEvent.change(getByTestId('input-1'), { target: { value: '' } });
    fireEvent.blur(getByTestId('input-1'));
    fireEvent.blur(getByTestId('input-2'));

    expect(validatorStateRef.current).toEqual({
      error: null,
      fields: {
        'input-1': {
          fieldType: 'input',
          required: true,
          value: '',
          valid: false,
          error: 'default-input-error',
        },
        'input-2': {
          fieldType: 'input',
          required: true,
          value: '',
          valid: false,
          error: 'default-input-error',
        },
      },
    });
  });

  it('accepts a function to validate on both change and blur', () => {
    const validate = jest.fn((value: string, focused: boolean) => {
      return {
        value: `bar ${value}`,
        valid: false,
        error: focused ? null : 'bad thing',
      };
    });

    const validatorStateRef = mkValidatorStateRef();
    const { getByTestId } = render(
      <TestForm validatorStateRef={validatorStateRef}>
        <Input
          data-testid="testInput"
          type="text"
          name="foo"
          onValidate={validate}
        />
      </TestForm>
    );

    fireEvent.change(getByTestId('testInput'), { target: { value: 'xyzzy' } });

    expect(validate.mock.calls.length).toBe(1);
    expect(validate.mock.calls[0][0]).toBe('xyzzy');
    expect(validate.mock.calls[0][1]).toBe(true);

    expect(validatorStateRef.current).toEqual({
      error: null,
      fields: {
        foo: {
          fieldType: 'input',
          required: false,
          // validate function can alter value
          value: 'bar xyzzy',
          // validate function can set valid state
          valid: false,
          // validate function can set error message separately from valid state
          error: null,
        },
      },
    });

    fireEvent.blur(getByTestId('testInput'));

    expect(validate.mock.calls.length).toBe(2);
    expect(validate.mock.calls[1][0]).toBe('bar xyzzy');
    expect(validate.mock.calls[1][1]).toBe(false);

    expect(validatorStateRef.current).toEqual({
      error: null,
      fields: {
        foo: {
          fieldType: 'input',
          required: false,
          // validate function can alter value, which happens twice since we validated twice
          value: 'bar bar xyzzy',
          valid: false,
          error: 'bad thing',
        },
      },
    });
  });

  it('adds an "invalid" class name when invalid', () => {
    const validate = (value: string) => ({
      value: 'bar',
      valid: false,
      error: 'bad thing',
    });
    const { container, getByTestId } = render(
      <TestForm>
        <Input
          data-testid="testInput"
          type="text"
          name="foo"
          onValidate={validate}
        />
      </TestForm>
    );
    fireEvent.change(getByTestId('testInput'), { target: { value: 'xyzzy' } });
    fireEvent.blur(getByTestId('testInput'), { target: { value: 'xyzzy' } });
    expect(container.querySelector('input.invalid')).not.toBeNull();
  });
});

describe('StripeElement', () => {
  type MockStripeElementProps = {
    id?: string;
    className?: string;
    onChange: Function;
    onBlur: Function;
    onFocus: Function;
    onReady: Function;
  };

  const buildMockStripeElement = (value: any) =>
    class extends React.Component<MockStripeElementProps> {
      constructor(props: MockStripeElementProps) {
        super(props);
      }
      handleClick = (ev: React.MouseEvent<HTMLInputElement, MouseEvent>) => {
        ev.preventDefault();
        this.props.onChange(value);
      };
      handleBlur = (ev: React.FocusEvent<HTMLInputElement>) => {
        ev.preventDefault();
        this.props.onBlur();
      };
      render() {
        return (
          <input
            type="text"
            data-testid="mockStripe"
            onClick={this.handleClick}
            onBlur={this.handleBlur}
          />
        );
      }
    };

  it('does nothing if field value is null', () => {
    const MockStripeElement = buildMockStripeElement(null);
    const validatorStateRef = mkValidatorStateRef();
    const { getByTestId } = render(
      <TestForm validatorStateRef={validatorStateRef}>
        <StripeElement name="input-1" component={MockStripeElement} />
      </TestForm>
    );
    fireEvent.click(getByTestId('mockStripe'));
    expect(validatorStateRef.current.fields['input-1'].valid).toEqual(
      undefined
    );
  });

  it('does nothing if value is not yet complete', () => {
    const MockStripeElement = buildMockStripeElement({
      complete: false,
      error: null,
    });
    const validatorStateRef = mkValidatorStateRef();
    const { getByTestId } = render(
      <TestForm validatorStateRef={validatorStateRef}>
        <StripeElement name="input-1" component={MockStripeElement} />
      </TestForm>
    );
    fireEvent.click(getByTestId('mockStripe'));
    expect(validatorStateRef.current.fields['input-1'].valid).toEqual(
      undefined
    );
  });

  it('handles error result from contained stripe element', () => {
    const MockStripeElement = buildMockStripeElement({
      error: { message: 'game over man' },
    });
    const validatorStateRef = mkValidatorStateRef();
    const { container, getByTestId } = render(
      <TestForm validatorStateRef={validatorStateRef}>
        <StripeElement name="input-1" component={MockStripeElement} />
      </TestForm>
    );

    fireEvent.click(getByTestId('mockStripe'));
    expect(container.querySelector('aside.tooltip')).toBeNull();

    fireEvent.blur(getByTestId('mockStripe'));

    const tooltipEl = container.querySelector('aside.tooltip');
    expect(tooltipEl).not.toBeNull();
    expect((tooltipEl as Element).textContent).toContain('game over man');

    expect(validatorStateRef.current).toEqual({
      error: null,
      fields: {
        'input-1': {
          fieldType: 'stripe',
          required: false,
          value: {
            error: {
              message: 'game over man',
            },
          },
          valid: false,
          error: 'game over man',
        },
      },
    });
  });

  it('runs validation for empty value if focused and blurred', () => {
    const MockStripeElement = buildMockStripeElement(undefined);
    const validatorStateRef = mkValidatorStateRef();
    const translatedIsRequired = 'IS REQUIRED TRANSLATED';
    const { container, getByTestId } = render(
      <TestForm validatorStateRef={validatorStateRef}>
        <StripeElement
          data-testid="input-1"
          label="Frobnitz"
          name="input-1"
          required={true}
          // Basic mock of getString, real version is more in depth, but has test coverage within the library
          getString={(msg, value) => {
            return value.label + ' ' + translatedIsRequired;
          }}
          component={MockStripeElement}
        />
      </TestForm>
    );

    fireEvent.click(getByTestId('mockStripe'));
    expect(container.querySelector('aside.tooltip')).toBeNull();

    fireEvent.blur(getByTestId('mockStripe'));

    const tooltipEl = container.querySelector('aside.tooltip');
    expect(tooltipEl).not.toBeNull();
    expect((tooltipEl as Element).textContent).toContain(
      'Frobnitz IS REQUIRED TRANSLATED'
    );
  });

  it('supports a custom onValidate function', () => {
    const MockStripeElement = buildMockStripeElement({
      value: 'foo',
      error: 'not this',
      complete: true,
    });
    const validatorStateRef = mkValidatorStateRef();
    const expectedError = 'My hovercraft is full of eels';
    const onValidate = jest.fn((value) => ({
      value,
      valid: false,
      error: expectedError,
    }));
    const { container, getByTestId } = render(
      <TestForm validatorStateRef={validatorStateRef}>
        <StripeElement
          data-testid="input-1"
          name="input-1"
          component={MockStripeElement}
          onValidate={onValidate}
        />
      </TestForm>
    );
    fireEvent.click(getByTestId('mockStripe'));
    expect(onValidate).toBeCalled();

    const tooltipEl = container.querySelector('aside.tooltip');
    expect(tooltipEl).not.toBeNull();
    expect((tooltipEl as Element).textContent).toContain(expectedError);
  });

  it('trims off trailing periods from error messages (issue #1718)', () => {
    const MockStripeElement = buildMockStripeElement({
      error: { message: 'period.' },
    });
    const validatorStateRef = mkValidatorStateRef();
    const { container, getByTestId } = render(
      <TestForm validatorStateRef={validatorStateRef}>
        <StripeElement name="input-1" component={MockStripeElement} />
      </TestForm>
    );

    fireEvent.click(getByTestId('mockStripe'));
    fireEvent.blur(getByTestId('mockStripe'));

    const tooltipEl = container.querySelector('aside.tooltip');
    expect(tooltipEl).not.toBeNull();
    expect((tooltipEl as Element).textContent).toContain('period');
  });

  it('handles complete result from contained stripe element', () => {
    const MockStripeElement = buildMockStripeElement({ complete: true });
    const validatorStateRef = mkValidatorStateRef();
    const { container, getByTestId } = render(
      <TestForm validatorStateRef={validatorStateRef}>
        <StripeElement name="input-1" component={MockStripeElement} />
      </TestForm>
    );

    fireEvent.click(getByTestId('mockStripe'));

    const tooltipEl = container.querySelector('aside.tooltip');
    expect(tooltipEl).toBeNull();

    expect(validatorStateRef.current).toEqual({
      error: null,
      fields: {
        'input-1': {
          fieldType: 'stripe',
          required: false,
          value: { complete: true },
          valid: true,
          error: null,
        },
      },
    });
  });
});

describe('Checkbox', () => {
  it('renders its own label with a label prop when avaiable', () => {
    const { container } = render(
      <TestForm>
        <Checkbox name="foo" label="nice label" />
      </TestForm>
    );
    const label = container.querySelector('span.label-text.checkbox');
    expect(label).not.toBeNull();
    if (label) {
      expect(label.textContent).toContain('nice label');
    }
  });

  it('renders children as a label with markup when available', () => {
    const { container } = render(
      <TestForm>
        <Checkbox name="foo">
          nice <span className="label-inner-span">label</span>
        </Checkbox>
      </TestForm>
    );
    const label = container.querySelector('span.label-text.checkbox');
    const labelInnerSpan = container.querySelector('.label-inner-span');
    expect(label).not.toBeNull();
    if (label) {
      expect(label.textContent).toEqual('nice label');
    }
    expect(labelInnerSpan).not.toBeNull();
    if (labelInnerSpan) {
      expect(labelInnerSpan.textContent).toEqual('label');
    }
  });

  it('accepts an alternate className', () => {
    const { container } = render(
      <TestForm>
        <Checkbox className="fooquux" name="foo" label="nice label" />
      </TestForm>
    );
    expect(container.querySelector('.fooquux')).toBeInTheDocument();
  });

  it('must be checked to be valid when required', () => {
    const validatorStateRef = mkValidatorStateRef();
    const { getByTestId } = render(
      <TestForm validatorStateRef={validatorStateRef}>
        <Checkbox data-testid="checkbox" name="foo" required />
      </TestForm>
    );

    const checkbox = getByTestId('checkbox');

    fireEvent.click(checkbox);
    expect(validatorStateRef.current).toEqual({
      error: null,
      fields: {
        foo: {
          fieldType: 'input',
          required: true,
          value: true,
          valid: true,
          error: null,
        },
      },
    });

    fireEvent.click(checkbox);
    expect(validatorStateRef.current).toEqual({
      error: null,
      fields: {
        foo: {
          fieldType: 'input',
          required: true,
          value: false,
          valid: false,
          error: null,
        },
      },
    });
  });
});

describe('SubmitButton', () => {
  it('is disabled if all required fields are not valid', () => {
    const Subject = () => {
      return (
        <TestForm>
          <SubmitButton data-testid="submit" name="submit">
            Submit
          </SubmitButton>
          <Input name="foo" required />
          <TestValidatorFn
            fn={(validator) => {
              validator.updateField({
                name: 'foo',
                value: 'bar',
                valid: false,
                error: 'This is an error',
              });
            }}
          />
          <TestValidatorFn
            fn={(validator) => {
              validator.updateField({ name: 'foo', value: 'baz', valid: true });
            }}
          />
        </TestForm>
      );
    };
    const { queryAllByTestId, getByTestId } = render(<Subject />);
    const validatorFns = queryAllByTestId('execute');

    fireEvent.click(validatorFns[0]);
    expect(getByTestId('submit')).toHaveAttribute('disabled');

    fireEvent.click(validatorFns[1]);
    expect(getByTestId('submit')).not.toHaveAttribute('disabled');
  });
});

const mkValidatorStateRef = () =>
  React.createRef() as React.MutableRefObject<ValidatorState>;

const TestForm = ({
  children,
  validatorStateRef,
}: {
  children: React.ReactNode;
  validatorStateRef?: React.MutableRefObject<ValidatorState>;
}) => {
  const middleware: ValidatorMiddlewareReducer = (state, action, next) => {
    const nextState = next(state, action);
    if (validatorStateRef) {
      validatorStateRef.current = nextState;
    }
    return nextState;
  };
  const validator = useValidatorState({ middleware });
  return <Form validator={validator}>{children}</Form>;
};

const TestValidatorFn = ({ fn }: { fn: (validator: Validator) => any }) => {
  const { validator } = useContext(FormContext) as FormContextValue;
  const [result, setResult] = useState('');
  const onClick = useCallback(
    (ev) => {
      ev.preventDefault();
      setResult(JSON.stringify(fn(validator)));
    },
    [setResult, fn, validator]
  );
  return (
    <div>
      <button data-testid="execute" onClick={onClick}>
        Execute
      </button>
      <pre data-testid="result">{result}</pre>
    </div>
  );
};
