'use strict';

const chalk = require('chalk');

if (process.env.REACT_CLASS_EQUIVALENCE_TEST) {
  // Inside the class equivalence tester, we have a custom environment, let's
  // require that instead.
  require('./spec-equivalence-reporter/setupTests.js');
} else {
  const env = jasmine.getEnv();
  const errorMap = require('../error-codes/codes.json');

  // TODO: Stop using spyOn in all the test since that seem deprecated.
  // This is a legacy upgrade path strategy from:
  // https://github.com/facebook/jest/blob/v20.0.4/packages/jest-matchers/src/spyMatchers.js#L160
  const isSpy = spy => spy.calls && typeof spy.calls.count === 'function';

  const spyOn = global.spyOn;
  const noop = function() {};

  // 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;
  }

  expect.extend({
    ...require('./matchers/toWarnDev'),
  });

  // 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!
  env.beforeEach(() => {
    global.infiniteLoopError = null;
  });
  env.afterEach(() => {
    const error = global.infiniteLoopError;
    global.infiniteLoopError = null;
    if (error) {
      throw error;
    }
  });

  ['error', 'warn'].forEach(methodName => {
    const unexpectedConsoleCallStacks = [];
    const newMethod = function(message) {
      // Capture the call stack now so we can warn about it later.
      // The call stack has helpful information for the test author.
      // Don't throw yet though b'c it might be accidentally caught and suppressed.
      const stack = new Error().stack;
      unexpectedConsoleCallStacks.push([
        stack.substr(stack.indexOf('\n') + 1),
        message,
      ]);
    };

    console[methodName] = newMethod;

    env.beforeEach(() => {
      unexpectedConsoleCallStacks.length = 0;
    });

    env.afterEach(() => {
      if (console[methodName] !== newMethod && !isSpy(console[methodName])) {
        throw new Error(
          `Test did not tear down console.${methodName} mock properly.`
        );
      }

      if (unexpectedConsoleCallStacks.length > 0) {
        const messages = unexpectedConsoleCallStacks.map(
          ([stack, message]) =>
            `${chalk.red(message)}\n` +
            `${stack
              .split('\n')
              .map(line => chalk.gray(line))
              .join('\n')}`
        );

        const message =
          `Expected test not to call ${chalk.bold(
            `console.${methodName}()`
          )}.\n\n` +
          'If the warning is expected, test for it explicitly by:\n' +
          `1. Using the ${chalk.bold('.toWarnDev()')} / ${chalk.bold(
            '.toLowPriorityWarnDev()'
          )} matchers, or...\n` +
          `2. Mock it out using ${chalk.bold(
            'spyOnDev'
          )}(console, '${methodName}') or ${chalk.bold(
            'spyOnProd'
          )}(console, '${methodName}'), and test that the warning occurs.`;

        throw new Error(`${message}\n\n${messages.join('\n\n')}`);
      }
    });
  });

  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.
    const decodeErrorMessage = function(message) {
      if (!message) {
        return message;
      }
      const re = /error-decoder.html\?invariant=(\d+)([^\s]*)/;
      const matches = message.match(re);
      if (!matches || matches.length !== 3) {
        return message;
      }
      const code = parseInt(matches[1], 10);
      const args = matches[2]
        .split('&')
        .filter(s => s.startsWith('args[]='))
        .map(s => s.substr('args[]='.length))
        .map(decodeURIComponent);
      const format = errorMap[code];
      let argIndex = 0;
      return format.replace(/%s/g, () => args[argIndex++]);
    };
    const OriginalError = global.Error;
    const ErrorProxy = new Proxy(OriginalError, {
      apply(target, thisArg, argumentsList) {
        const error = Reflect.apply(target, thisArg, argumentsList);
        error.message = decodeErrorMessage(error.message);
        return error;
      },
      construct(target, argumentsList, newTarget) {
        const error = Reflect.construct(target, argumentsList, newTarget);
        error.message = decodeErrorMessage(error.message);
        return error;
      },
    });
    ErrorProxy.OriginalError = OriginalError;
    global.Error = ErrorProxy;
  }

  require('jasmine-check').install();
}
