// @flow
import React, { type Node } from 'react';
import ReactDOM from 'react-dom';
import { act, create } from 'react-test-renderer';
import { IntlProvider, addLocaleData } from 'react-intl';
import { ThemeProvider } from 'styled-components';
import { shallow, mount, type ShallowWrapper } from 'enzyme';
import en from 'react-intl/locale-data/en';

import { themes } from '../../../src/themes';

/**
 * Utilities for testing patchwork components. They make sure the
 * correct providers are set up for theme, intl, etc.
 */

addLocaleData(en);

export const currentTheme = themes.wealthsimple;

export type MessageTable = {
  [messageName: string]: string,
};

type WrappingComponentProps = {
  children: Node,
};
export const WrappingComponent = (messages: MessageTable) => ({
  children,
}: WrappingComponentProps) => (
  <IntlProvider locale="en" messages={messages}>
    <ThemeProvider theme={currentTheme}>{children}</ThemeProvider>
  </IntlProvider>
);

/**
 * Mounts some JSX, including its descendants. Handles setting up
 * react-intl and patchwork theme contexts.
 *
 * Generally used for
 * behavioural testing. Use .simulate() to interact via events, and .find()
 * to verify outcomes.
 *
 * @param {Node} jsx the JSX tree under test.
 * @param {Object} messages optional, custom set of string table messages.
 */
export const mountWrappedComponent = (jsx: Node, messages: MessageTable = {}) =>
  mount<*>(
    // $FlowFixMe - Enzyme typedefs call for Element, but it should be Node
    jsx,
    { wrappingComponent: WrappingComponent(messages) },
  );

/**
 * Shallow mounts some JSX, using stubs for any chilren.
 * react-intl and patchwork theme contexts. In most cases
 * you probably want to use mountWrappedComponent instead.
 *
 * @param {Node} jsx the JSX tree under test.
 * @param {Object} messages optional, custom set of string table messages.
 */
export const shallowWrappedComponent = (
  jsx: Node,
  messages: MessageTable = {},
): ShallowWrapper<*> =>
  shallow(
    // $FlowFixMe - Enzyme typedefs call for Element, but it should be Node
    jsx,
    {
      wrappingComponent: WrappingComponent(messages),
    },
  );

/**
 * Enzyme + styled-components results in horribly noisy snapshots.
 * For jest snapshot tests, this method is preferred since it uses
 * react-test-renderer to produce simple DOM/CSS snapshots without
 * all the noise.
 *
 * Note however that you can't use find, simulate, etc. on the return
 * value of this function; if you need those, you're probably better
 * off writing a behavioural test with mountWrappedComponent instead.
 */
export const renderSnapshot = (jsx: Node, messages?: MessageTable = {}) => {
  const Wrapper = WrappingComponent(messages);
  let root;

  // Monkey patch ReactDom.createPortal because react-test-render doesn't
  // support it, but unfortunately our modals use this API.
  const originalCreatePortal = ReactDOM.createPortal;
  // $FlowFixMe flow doesn't like the monkeypatching.
  ReactDOM.createPortal = (node) => node;

  act(() => {
    root = create(<Wrapper>{jsx}</Wrapper>);
  });

  // Undo the monkey patch when done.
  // $FlowFixMe flow doesn't like the unmonkeypatching.
  ReactDOM.createPortal = originalCreatePortal;

  return root ? root.toJSON() : undefined;
};

/**
 * Executes callback function inside the context of a React render() function.
 * This allows for calling React hooks inside callback function.
 */
export const testHook = (callback: () => void) =>
  mountWrappedComponent(
    React.createElement(() => {
      callback();
      return null;
    }),
  );
