/**
 * Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */

'use strict';

// This plugin exists to make sure that we use a `Promise` that has not been messed with by user code.
// Might consider extending this to other globals as well in the future

module.exports = ({template}) => {
  const promiseDeclaration = template(`
    var global = (function() {
      if (typeof globalThis !== 'undefined') {
        return globalThis;
      } else if (typeof global !== 'undefined') {
        return global;
      } else if (typeof self !== 'undefined') {
        return self;
      } else if (typeof window !== 'undefined') {
        return window;
      } else {
        return Function('return this')();
      }
    }())
    var Promise = global[Symbol.for('jest-native-promise')] || global.Promise;
  `);
  const symbolDeclaration = template(`
    var global = (function() {
      if (typeof globalThis !== 'undefined') {
        return globalThis;
      } else if (typeof global !== 'undefined') {
        return global;
      } else if (typeof self !== 'undefined') {
        return self;
      } else if (typeof window !== 'undefined') {
        return window;
      } else {
        return Function('return this')();
      }
    }())
    var Symbol = global['jest-symbol-do-not-touch'] || global.Symbol;
  `);
  const nowDeclaration = template(`
    var global = (function() {
      if (typeof globalThis !== 'undefined') {
        return globalThis;
      } else if (typeof global !== 'undefined') {
        return global;
      } else if (typeof self !== 'undefined') {
        return self;
      } else if (typeof window !== 'undefined') {
        return window;
      } else {
        return Function('return this')();
      }
    }())
    var jestNow = global[Symbol.for('jest-native-now')] || global.Date.now;
  `);
  const fsReadFileDeclaration = template(`
    var global = (function() {
      if (typeof globalThis !== 'undefined') {
        return globalThis;
      } else if (typeof global !== 'undefined') {
        return global;
      } else if (typeof self !== 'undefined') {
        return self;
      } else if (typeof window !== 'undefined') {
        return window;
      } else {
        return Function('return this')();
      }
    }())
    var jestReadFile = global[Symbol.for('jest-native-read-file')] || fs.readFileSync;
  `);
  const fsWriteFileDeclaration = template(`
    var global = (function() {
      if (typeof globalThis !== 'undefined') {
        return globalThis;
      } else if (typeof global !== 'undefined') {
        return global;
      } else if (typeof self !== 'undefined') {
        return self;
      } else if (typeof window !== 'undefined') {
        return window;
      } else {
        return Function('return this')();
      }
    }())
    var jestWriteFile = global[Symbol.for('jest-native-write-file')] || fs.writeFileSync;
  `);
  const fsExistsFileDeclaration = template(`
    var global = (function() {
      if (typeof globalThis !== 'undefined') {
        return globalThis;
      } else if (typeof global !== 'undefined') {
        return global;
      } else if (typeof self !== 'undefined') {
        return self;
      } else if (typeof window !== 'undefined') {
        return window;
      } else {
        return Function('return this')();
      }
    }())
    var jestExistsFile = global[Symbol.for('jest-native-exists-file')] || fs.existsSync;
  `);

  return {
    name: 'jest-native-globals',
    visitor: {
      ReferencedIdentifier(path, state) {
        if (path.node.name === 'Promise' && !state.jestInjectedPromise) {
          state.jestInjectedPromise = true;
          path
            .findParent(p => p.isProgram())
            .unshiftContainer('body', promiseDeclaration());
          path
            .findParent(p => p.isProgram())
            .unshiftContainer('body', symbolDeclaration());
        }
        if (path.node.name === 'Symbol' && !state.jestInjectedSymbol) {
          state.jestInjectedSymbol = true;
          path
            .findParent(p => p.isProgram())
            .unshiftContainer('body', symbolDeclaration());
        }
        if (
          path.node.name === 'Date' &&
          path.parent.property &&
          path.parent.property.name === 'now'
        ) {
          if (!state.jestInjectedNow) {
            state.jestInjectedNow = true;
            path
              .findParent(p => p.isProgram())
              .unshiftContainer('body', nowDeclaration());
            path
              .findParent(p => p.isProgram())
              .unshiftContainer('body', symbolDeclaration());
          }

          path.parentPath.replaceWithSourceString('jestNow');
        }
        if (
          path.node.name === 'fs' &&
          path.parent.property &&
          ['readFileSync', 'writeFileSync', 'existsSync'].includes(
            path.parent.property.name,
          )
        ) {
          if (
            !state.jestInjectedRead &&
            path.parent.property.name === 'readFileSync'
          ) {
            state.jestInjectedRead = true;
            path
              .findParent(p => p.isProgram())
              .unshiftContainer('body', fsReadFileDeclaration());
            path
              .findParent(p => p.isProgram())
              .unshiftContainer('body', symbolDeclaration());

            path.parentPath.replaceWithSourceString('jestReadFile');
          }
          if (
            !state.jestInjectedWrite &&
            path.parent.property.name === 'writeFileSync'
          ) {
            state.jestInjectedWrite = true;
            path
              .findParent(p => p.isProgram())
              .unshiftContainer('body', fsWriteFileDeclaration());
            path
              .findParent(p => p.isProgram())
              .unshiftContainer('body', symbolDeclaration());

            path.parentPath.replaceWithSourceString('jestWriteFile');
          }
          if (
            !state.jestInjectedExists &&
            path.parent.property.name === 'existsSync'
          ) {
            state.jestInjectedExists = true;
            path
              .findParent(p => p.isProgram())
              .unshiftContainer('body', fsExistsFileDeclaration());
            path
              .findParent(p => p.isProgram())
              .unshiftContainer('body', symbolDeclaration());

            path.parentPath.replaceWithSourceString('jestExistsFile');
          }
        }
      },
    },
  };
};
