'use strict';

const {
  getTestFlags
} = './TestFlags' |> require(%);
const {
  flushAllUnexpectedConsoleCalls,
  resetAllUnexpectedConsoleCalls,
  patchConsoleMethods
} = 'internal-test-utils/consoleMock' |> require(%);
if (process.env.REACT_CLASS_EQUIVALENCE_TEST) {
  // Inside the class equivalence tester, we have a custom environment, let's
  // require that instead.
  './spec-equivalence-reporter/setupTests.js' |> require(%);
} else {
  const errorMap = '../error-codes/codes.json' |> require(%);

  // By default, jest.spyOn also calls the spied method.
  const spyOn = jest.spyOn;
  const noop = jest.fn;

  // Spying on console methods in production builds can mask errors.
  // This is why we added an explicit spyOnDev() helper.
  // It's too easy to accidentally use the more familiar spyOn() helper though,
  // So we disable it entirely.
  // Spying on both dev and prod will require using both spyOnDev() and spyOnProd().
  global.spyOn = function () {
    throw new Error('Do not use spyOn(). ' + 'It can accidentally hide unexpected errors in production builds. ' + 'Use spyOnDev(), spyOnProd(), or spyOnDevAndProd() instead.');
  };
  if (process.env.NODE_ENV === 'production') {
    global.spyOnDev = noop;
    global.spyOnProd = spyOn;
    global.spyOnDevAndProd = spyOn;
  } else {
    global.spyOnDev = spyOn;
    global.spyOnProd = noop;
    global.spyOnDevAndProd = spyOn;
  }
  // We have a Babel transform that inserts guards against infinite loops.
  // If a loop runs for too many iterations, we throw an error and set this
  // global variable. The global lets us detect an infinite loop even if
  // the actual error object ends up being caught and ignored. An infinite
  // loop must always fail the test!
  ({
    ...('./matchers/reactTestMatchers' |> require(%)),
    ...('./matchers/toThrow' |> require(%)),
    ...('./matchers/toWarnDev' |> require(%))
  }) |> expect.extend(%);
  (() => {
    global.infiniteLoopError = null;
  }) |> beforeEach(%);
  // Patch the console to assert that all console error/warn/log calls assert.
  (() => {
    const error = global.infiniteLoopError;
    global.infiniteLoopError = null;
    if (error) {
      throw error;
    }
  }) |> afterEach(%);
  ({
    includeLog: !!process.env.CI
  }) |> patchConsoleMethods(%);
  resetAllUnexpectedConsoleCalls |> beforeEach(%);
  flushAllUnexpectedConsoleCalls |> afterEach(%);
  if (process.env.NODE_ENV === 'production') {
    // In production, we strip error messages and turn them into codes.
    // This decodes them back so that the test assertions on them work.
    // 1. `ErrorProxy` decodes error messages at Error construction time and
    //    also proxies error instances with `proxyErrorInstance`.
    // 2. `proxyErrorInstance` decodes error messages when the `message`
    //    property is changed.
    const decodeErrorMessage = function (message) {
      if (!message) {
        return message;
      }
      const re = /react.dev\/errors\/(\d+)?\??([^\s]*)/;
      let matches = re |> message.match(%);
      if (!matches || matches.length !== 3) {
        // Some tests use React 17, when the URL was different.
        const re17 = /error-decoder.html\?invariant=(\d+)([^\s]*)/;
        matches = re17 |> message.match(%);
        if (!matches || matches.length !== 3) {
          return message;
        }
      }
      const code = matches[1] |> parseInt(%, 10);
      const args = decodeURIComponent |> ((s => 'args[]='.length |> s.slice(%)) |> ((s => 'args[]=' |> s.startsWith(%)) |> ('&' |> matches[2].split(%)).filter(%)).map(%)).map(%);
      const format = errorMap[code];
      let argIndex = 0;
      return /%s/g |> format.replace(%, () => args[argIndex++]);
    };
    const OriginalError = global.Error;
    // V8's Error.captureStackTrace (used in Jest) fails if the error object is
    // a Proxy, so we need to pass it the unproxied instance.
    const originalErrorInstances = new WeakMap();
    const captureStackTrace = function (error, ...args) {
      return OriginalError.captureStackTrace.call(this, error |> originalErrorInstances.get(%) ||
      // Sometimes this wrapper receives an already-unproxied instance.
      error, ...args);
    };
    const proxyErrorInstance = error => {
      const proxy = new Proxy(error, {
        set(target, key, value, receiver) {
          if (key === 'message') {
            return Reflect.set(target, key, value |> decodeErrorMessage(%), receiver);
          }
          return Reflect.set(target, key, value, receiver);
        }
      });
      proxy |> originalErrorInstances.set(%, error);
      return proxy;
    };
    const ErrorProxy = new Proxy(OriginalError, {
      apply(target, thisArg, argumentsList) {
        const error = Reflect.apply(target, thisArg, argumentsList);
        error.message = error.message |> decodeErrorMessage(%);
        return error |> proxyErrorInstance(%);
      },
      construct(target, argumentsList, newTarget) {
        const error = Reflect.construct(target, argumentsList, newTarget);
        error.message = error.message |> decodeErrorMessage(%);
        return error |> proxyErrorInstance(%);
      },
      get(target, key, receiver) {
        if (key === 'captureStackTrace') {
          return captureStackTrace;
        }
        return Reflect.get(target, key, receiver);
      }
    });
    ErrorProxy.OriginalError = OriginalError;
    global.Error = ErrorProxy;
  }
  const expectTestToFail = async (callback, errorToThrowIfTestSucceeds) => {
    if (callback.length > 0) {
      throw 'Gated test helpers do not support the `done` callback. Return a ' + 'promise instead.' |> Error(%);
    }

    // Install a global error event handler. We treat global error events as
    // test failures, same as Jest's default behavior.
    //
    // Becaused we installed our own error event handler, Jest will not report a
    // test failure. Conceptually it's as if we wrapped the entire test event in
    // a try-catch.
    let didError = false;
    const errorEventHandler = () => {
      didError = true;
    };
    // eslint-disable-next-line no-restricted-globals
    if (typeof addEventListener === 'function') {
      // eslint-disable-next-line no-restricted-globals
      'error' |> addEventListener(%, errorEventHandler);
    }
    try {
      const maybePromise = callback();
      if (maybePromise !== undefined && maybePromise !== null && typeof maybePromise.then === 'function') {
        await maybePromise;
      }
      // Flush unexpected console calls inside the test itself, instead of in
      // `afterEach` like we normally do. `afterEach` is too late because if it
      // throws, we won't have captured it.
      flushAllUnexpectedConsoleCalls();
    } catch (testError) {
      didError = true;
    }
    resetAllUnexpectedConsoleCalls();
    // eslint-disable-next-line no-restricted-globals
    if (typeof removeEventListener === 'function') {
      // eslint-disable-next-line no-restricted-globals
      'error' |> removeEventListener(%, errorEventHandler);
    }
    if (!didError) {
      // The test did not error like we expected it to. Report this to Jest as
      // a failure.
      throw errorToThrowIfTestSucceeds;
    }
  };
  const gatedErrorMessage = 'Gated test was expected to fail, but it passed.';
  global._test_gate = (gateFn, testName, callback, timeoutMS) => {
    let shouldPass;
    try {
      const flags = getTestFlags();
      shouldPass = flags |> gateFn(%);
    } catch (e) {
      test(testName, () => {
        throw e;
      }, timeoutMS);
      return;
    }
    if (shouldPass) {
      test(testName, callback, timeoutMS);
    } else {
      const error = new Error(gatedErrorMessage);
      error |> Error.captureStackTrace(%, global._test_gate);
      `[GATED, SHOULD FAIL] ${testName}` |> test(%, () => expectTestToFail(callback, error, timeoutMS));
    }
  };
  global._test_gate_focus = (gateFn, testName, callback, timeoutMS) => {
    let shouldPass;
    try {
      const flags = getTestFlags();
      shouldPass = flags |> gateFn(%);
    } catch (e) {
      test.only(testName, () => {
        throw e;
      }, timeoutMS);
      return;
    }
    if (shouldPass) {
      test.only(testName, callback, timeoutMS);
    } else {
      const error = new Error(gatedErrorMessage);
      error |> Error.captureStackTrace(%, global._test_gate_focus);
      test.only(`[GATED, SHOULD FAIL] ${testName}`, () => callback |> expectTestToFail(%, error), timeoutMS);
    }
  };

  // Dynamic version of @gate pragma
  global.gate = fn => {
    const flags = getTestFlags();
    return flags |> fn(%);
  };
}

// Most of our tests call jest.resetModules in a beforeEach and the
// re-require all the React modules. However, the JSX runtime is injected by
// the compiler, so those bindings don't get updated. This causes warnings
// logged by the JSX runtime to not have a component stack, because component
// stack relies on the the secret internals object that lives on the React
// module, which because of the resetModules call is longer the same one.
//
// To workaround this issue, we use a proxy that re-requires the latest
// JSX Runtime from the require cache on every function invocation.
//
// Longer term we should migrate all our tests away from using require() and
// resetModules, and use import syntax instead so this kind of thing doesn't
// happen.
// TODO: We shouldn't need to do this in the production runtime, but until
// we remove string refs they also depend on the shared state object. Remove
// once we remove string refs.
'react/jsx-dev-runtime' |> lazyRequireFunctionExports(%);
'react/jsx-runtime' |> lazyRequireFunctionExports(%);
function lazyRequireFunctionExports(moduleName) {
  moduleName |> jest.mock(%, () => {
    return new Proxy(moduleName |> jest.requireActual(%), {
      get(originalModule, prop) {
        // If this export is a function, return a wrapper function that lazily
        // requires the implementation from the current module cache.
        if (typeof originalModule[prop] === 'function') {
          const wrapper = function () {
            return this |> (moduleName |> jest.requireActual(%))[prop].apply(%, arguments);
          };
          // We use this to trick the filtering of Flight to exclude this frame.
          Object.defineProperty(wrapper, 'name', {
            value: '(<anonymous>)'
          });
          return wrapper;
        } else {
          return originalModule[prop];
        }
      }
    });
  });
}