import {Component} from 'react';
import type {InjectedRouter} from 'react-router';
import {cache} from '@emotion/css'; // eslint-disable-line @emotion/no-vanilla
import {CacheProvider, ThemeProvider} from '@emotion/react';
import * as rtl from '@testing-library/react'; // eslint-disable-line no-restricted-imports
import userEvent from '@testing-library/user-event'; // eslint-disable-line no-restricted-imports

import {makeTestQueryClient} from 'sentry-test/queryClient';

import {GlobalDrawer} from 'sentry/components/globalDrawer';
import GlobalModal from 'sentry/components/globalModal';
import {SentryPropTypeValidators} from 'sentry/sentryPropTypeValidators';
import type {Organization} from 'sentry/types/organization';
import {QueryClientProvider} from 'sentry/utils/queryClient';
import {lightTheme} from 'sentry/utils/theme';
import {OrganizationContext} from 'sentry/views/organizationContext';
import {RouteContext} from 'sentry/views/routeContext';

import {instrumentUserEvent} from '../instrumentedEnv/userEventIntegration';

import {initializeOrg} from './initializeOrg';

interface ProviderOptions {
  /**
   * Sets the OrganizationContext. You may pass null to provide no organization
   */
  organization?: Partial<Organization> | null;
  /**
   * Sets the RouterContext
   */
  router?: Partial<InjectedRouter>;
}

interface Options extends ProviderOptions, rtl.RenderOptions {}

function makeAllTheProviders(providers: ProviderOptions) {
  const {organization, router} = initializeOrg({
    organization: providers.organization === null ? undefined : providers.organization,
    router: providers.router,
  });

  class LegacyRouterProvider extends Component<{children?: React.ReactNode}> {
    static childContextTypes = {
      router: SentryPropTypeValidators.isObject,
    };

    getChildContext() {
      return {router};
    }

    render() {
      return this.props.children;
    }
  }

  // In some cases we may want to not provide an organization at all
  const optionalOrganization = providers.organization === null ? null : organization;

  return function ({children}: {children?: React.ReactNode}) {
    return (
      <LegacyRouterProvider>
        <CacheProvider value={{...cache, compat: true}}>
          <ThemeProvider theme={lightTheme}>
            <QueryClientProvider client={makeTestQueryClient()}>
              <RouteContext.Provider
                value={{
                  router,
                  location: router.location,
                  params: router.params,
                  routes: router.routes,
                }}
              >
                <OrganizationContext.Provider value={optionalOrganization}>
                  <GlobalDrawer>{children}</GlobalDrawer>
                </OrganizationContext.Provider>
              </RouteContext.Provider>
            </QueryClientProvider>
          </ThemeProvider>
        </CacheProvider>
      </LegacyRouterProvider>
    );
  };
}

/**
 * Try avoiding unnecessary context and just mount your component. If it works,
 * then you dont need anything else.
 *
 * render(<TestedComponent />);
 *
 * If your component requires additional context you can pass it in the
 * options.
 */
function render(
  ui: React.ReactElement,
  {router, organization, ...rtlOptions}: Options = {}
) {
  const AllTheProviders = makeAllTheProviders({
    organization,
    router,
  });

  return rtl.render(ui, {wrapper: AllTheProviders, ...rtlOptions});
}

/**
 * @deprecated
 * Use userEvent over fireEvent where possible.
 * More details: https://kentcdodds.com/blog/common-mistakes-with-react-testing-library#not-using-testing-libraryuser-event
 */
const fireEvent = rtl.fireEvent;

function renderGlobalModal(options?: Options) {
  const result = render(<GlobalModal />, options);

  /**
   * Helper that waits for the modal to be removed from the DOM. You may need to
   * wait for the modal to be removed to avoid any act warnings.
   */
  function waitForModalToHide() {
    return rtl.waitFor(() => {
      expect(rtl.screen.queryByRole('dialog')).not.toBeInTheDocument();
    });
  }

  return {...result, waitForModalToHide};
}

/**
 * Helper that waits for the drawer to be hidden from the DOM. You may need to
 * wait for the drawer to be removed to avoid any act warnings.
 */
function waitForDrawerToHide(ariaLabel: string) {
  return rtl.waitFor(() => {
    expect(
      rtl.screen.queryByRole('complementary', {name: ariaLabel})
    ).not.toBeInTheDocument();
  });
}

/**
 * This cannot be implemented as a Sentry Integration because Jest creates an
 * isolated environment for each test suite. This means that if we were to apply
 * the monkey patching ahead of time, it would be shadowed by Jest.
 */
instrumentUserEvent();

// eslint-disable-next-line no-restricted-imports, import/export
export * from '@testing-library/react';

// eslint-disable-next-line import/export
export {render, renderGlobalModal, userEvent, fireEvent, waitForDrawerToHide};
