'use strict';

Object.defineProperty(exports, '__esModule', {
  value: true
});
exports.saveInlineSnapshots = saveInlineSnapshots;
var path = _interopRequireWildcard(require('path'));
var _types = require('@babel/types');
var fs = _interopRequireWildcard(require('graceful-fs'));
var _semver = _interopRequireDefault(require('semver'));
var _utils = require('./utils');
function _interopRequireDefault(obj) {
  return obj && obj.__esModule ? obj : {default: obj};
}
function _getRequireWildcardCache(nodeInterop) {
  if (typeof WeakMap !== 'function') return null;
  var cacheBabelInterop = new WeakMap();
  var cacheNodeInterop = new WeakMap();
  return (_getRequireWildcardCache = function (nodeInterop) {
    return nodeInterop ? cacheNodeInterop : cacheBabelInterop;
  })(nodeInterop);
}
function _interopRequireWildcard(obj, nodeInterop) {
  if (!nodeInterop && obj && obj.__esModule) {
    return obj;
  }
  if (obj === null || (typeof obj !== 'object' && typeof obj !== 'function')) {
    return {default: obj};
  }
  var cache = _getRequireWildcardCache(nodeInterop);
  if (cache && cache.has(obj)) {
    return cache.get(obj);
  }
  var newObj = {};
  var hasPropertyDescriptor =
    Object.defineProperty && Object.getOwnPropertyDescriptor;
  for (var key in obj) {
    if (key !== 'default' && Object.prototype.hasOwnProperty.call(obj, key)) {
      var desc = hasPropertyDescriptor
        ? Object.getOwnPropertyDescriptor(obj, key)
        : null;
      if (desc && (desc.get || desc.set)) {
        Object.defineProperty(newObj, key, desc);
      } else {
        newObj[key] = obj[key];
      }
    }
  }
  newObj.default = obj;
  if (cache) {
    cache.set(obj, newObj);
  }
  return newObj;
}
var Symbol = globalThis['jest-symbol-do-not-touch'] || globalThis.Symbol;
var Symbol = globalThis['jest-symbol-do-not-touch'] || globalThis.Symbol;
var jestWriteFile =
  globalThis[Symbol.for('jest-native-write-file')] || fs.writeFileSync;
var Symbol = globalThis['jest-symbol-do-not-touch'] || globalThis.Symbol;
var jestReadFile =
  globalThis[Symbol.for('jest-native-read-file')] || fs.readFileSync;
// prettier-ignore
const babelTraverse = // @ts-expect-error requireOutside Babel transform
require(require.resolve('@babel/traverse', {
  [(globalThis['jest-symbol-do-not-touch'] || globalThis.Symbol).for('jest-resolve-outside-vm-option')]: true
})).default;
// prettier-ignore
const generate = // @ts-expect-error requireOutside Babel transform
require(require.resolve('@babel/generator', {
  [(globalThis['jest-symbol-do-not-touch'] || globalThis.Symbol).for('jest-resolve-outside-vm-option')]: true
})).default;
// @ts-expect-error requireOutside Babel transform
const {file, templateElement, templateLiteral} = require(require.resolve(
  '@babel/types',
  {
    [(globalThis['jest-symbol-do-not-touch'] || globalThis.Symbol).for(
      'jest-resolve-outside-vm-option'
    )]: true
  }
));
// @ts-expect-error requireOutside Babel transform
const {parseSync} = require(require.resolve('@babel/core', {
  [(globalThis['jest-symbol-do-not-touch'] || globalThis.Symbol).for(
    'jest-resolve-outside-vm-option'
  )]: true
}));
function saveInlineSnapshots(snapshots, rootDir, prettierPath) {
  let prettier = null;
  if (prettierPath) {
    try {
      // @ts-expect-error requireOutside Babel transform
      prettier = require(require.resolve(prettierPath, {
        [(globalThis['jest-symbol-do-not-touch'] || globalThis.Symbol).for(
          'jest-resolve-outside-vm-option'
        )]: true
      }));
    } catch {
      // Continue even if prettier is not installed.
    }
  }
  const snapshotsByFile = groupSnapshotsByFile(snapshots);
  for (const sourceFilePath of Object.keys(snapshotsByFile)) {
    saveSnapshotsForFile(
      snapshotsByFile[sourceFilePath],
      sourceFilePath,
      rootDir,
      prettier && _semver.default.gte(prettier.version, '1.5.0')
        ? prettier
        : undefined
    );
  }
}
const saveSnapshotsForFile = (snapshots, sourceFilePath, rootDir, prettier) => {
  const sourceFile = jestReadFile(sourceFilePath, 'utf8');

  // TypeScript projects may not have a babel config; make sure they can be parsed anyway.
  const presets = [require.resolve('babel-preset-current-node-syntax')];
  const plugins = [];
  if (/\.tsx?$/.test(sourceFilePath)) {
    plugins.push([
      require.resolve('@babel/plugin-syntax-typescript'),
      {
        isTSX: sourceFilePath.endsWith('x')
      },
      // unique name to make sure Babel does not complain about a possible duplicate plugin.
      'TypeScript syntax plugin added by Jest snapshot'
    ]);
  }

  // Record the matcher names seen during traversal and pass them down one
  // by one to formatting parser.
  const snapshotMatcherNames = [];
  let ast = null;
  try {
    ast = parseSync(sourceFile, {
      filename: sourceFilePath,
      plugins,
      presets,
      root: rootDir
    });
  } catch (error) {
    // attempt to recover from missing jsx plugin
    if (error.message.includes('@babel/plugin-syntax-jsx')) {
      try {
        const jsxSyntaxPlugin = [
          require.resolve('@babel/plugin-syntax-jsx'),
          {},
          // unique name to make sure Babel does not complain about a possible duplicate plugin.
          'JSX syntax plugin added by Jest snapshot'
        ];
        ast = parseSync(sourceFile, {
          filename: sourceFilePath,
          plugins: [...plugins, jsxSyntaxPlugin],
          presets,
          root: rootDir
        });
      } catch {
        throw error;
      }
    } else {
      throw error;
    }
  }
  if (!ast) {
    throw new Error(`jest-snapshot: Failed to parse ${sourceFilePath}`);
  }
  traverseAst(snapshots, ast, snapshotMatcherNames);

  // substitute in the snapshots in reverse order, so slice calculations aren't thrown off.
  const sourceFileWithSnapshots = snapshots.reduceRight(
    (sourceSoFar, nextSnapshot) => {
      if (
        !nextSnapshot.node ||
        typeof nextSnapshot.node.start !== 'number' ||
        typeof nextSnapshot.node.end !== 'number'
      ) {
        throw new Error('Jest: no snapshot insert location found');
      }
      return (
        sourceSoFar.slice(0, nextSnapshot.node.start) +
        generate(nextSnapshot.node, {
          retainLines: true
        }).code.trim() +
        sourceSoFar.slice(nextSnapshot.node.end)
      );
    },
    sourceFile
  );
  const newSourceFile = prettier
    ? runPrettier(
        prettier,
        sourceFilePath,
        sourceFileWithSnapshots,
        snapshotMatcherNames
      )
    : sourceFileWithSnapshots;
  if (newSourceFile !== sourceFile) {
    jestWriteFile(sourceFilePath, newSourceFile);
  }
};
const groupSnapshotsBy = createKey => snapshots =>
  snapshots.reduce((object, inlineSnapshot) => {
    const key = createKey(inlineSnapshot);
    return {
      ...object,
      [key]: (object[key] || []).concat(inlineSnapshot)
    };
  }, {});
const groupSnapshotsByFrame = groupSnapshotsBy(({frame: {line, column}}) =>
  typeof line === 'number' && typeof column === 'number'
    ? `${line}:${column - 1}`
    : ''
);
const groupSnapshotsByFile = groupSnapshotsBy(({frame: {file}}) => file);
const indent = (snapshot, numIndents, indentation) => {
  const lines = snapshot.split('\n');
  // Prevent re-indentation of inline snapshots.
  if (
    lines.length >= 2 &&
    lines[1].startsWith(indentation.repeat(numIndents + 1))
  ) {
    return snapshot;
  }
  return lines
    .map((line, index) => {
      if (index === 0) {
        // First line is either a 1-line snapshot or a blank line.
        return line;
      } else if (index !== lines.length - 1) {
        // Do not indent empty lines.
        if (line === '') {
          return line;
        }

        // Not last line, indent one level deeper than expect call.
        return indentation.repeat(numIndents + 1) + line;
      } else {
        // The last line should be placed on the same level as the expect call.
        return indentation.repeat(numIndents) + line;
      }
    })
    .join('\n');
};
const resolveAst = fileOrProgram => {
  // Flow uses a 'Program' parent node, babel expects a 'File'.
  let ast = fileOrProgram;
  if (ast.type !== 'File') {
    ast = file(ast, ast.comments, ast.tokens);
    delete ast.program.comments;
  }
  return ast;
};
const traverseAst = (snapshots, fileOrProgram, snapshotMatcherNames) => {
  const ast = resolveAst(fileOrProgram);
  const groupedSnapshots = groupSnapshotsByFrame(snapshots);
  const remainingSnapshots = new Set(snapshots.map(({snapshot}) => snapshot));
  babelTraverse(ast, {
    CallExpression({node}) {
      const {arguments: args, callee} = node;
      if (
        callee.type !== 'MemberExpression' ||
        callee.property.type !== 'Identifier' ||
        callee.property.loc == null
      ) {
        return;
      }
      const {line, column} = callee.property.loc.start;
      const snapshotsForFrame = groupedSnapshots[`${line}:${column}`];
      if (!snapshotsForFrame) {
        return;
      }
      if (snapshotsForFrame.length > 1) {
        throw new Error(
          'Jest: Multiple inline snapshots for the same call are not supported.'
        );
      }
      snapshotMatcherNames.push(callee.property.name);
      const snapshotIndex = args.findIndex(
        ({type}) => type === 'TemplateLiteral'
      );
      const values = snapshotsForFrame.map(inlineSnapshot => {
        inlineSnapshot.node = node;
        const {snapshot} = inlineSnapshot;
        remainingSnapshots.delete(snapshot);
        return templateLiteral(
          [
            templateElement({
              raw: (0, _utils.escapeBacktickString)(snapshot)
            })
          ],
          []
        );
      });
      const replacementNode = values[0];
      if (snapshotIndex > -1) {
        args[snapshotIndex] = replacementNode;
      } else {
        args.push(replacementNode);
      }
    }
  });
  if (remainingSnapshots.size) {
    throw new Error("Jest: Couldn't locate all inline snapshots.");
  }
};
const runPrettier = (
  prettier,
  sourceFilePath,
  sourceFileWithSnapshots,
  snapshotMatcherNames
) => {
  // Resolve project configuration.
  // For older versions of Prettier, do not load configuration.
  const config = prettier.resolveConfig
    ? prettier.resolveConfig.sync(sourceFilePath, {
        editorconfig: true
      })
    : null;

  // Prioritize parser found in the project config.
  // If not found detect the parser for the test file.
  // For older versions of Prettier, fallback to a simple parser detection.
  // @ts-expect-error - `inferredParser` is `string`
  const inferredParser =
    (config && typeof config.parser === 'string' && config.parser) ||
    (prettier.getFileInfo
      ? prettier.getFileInfo.sync(sourceFilePath).inferredParser
      : simpleDetectParser(sourceFilePath));
  if (!inferredParser) {
    throw new Error(
      `Could not infer Prettier parser for file ${sourceFilePath}`
    );
  }

  // Snapshots have now been inserted. Run prettier to make sure that the code is
  // formatted, except snapshot indentation. Snapshots cannot be formatted until
  // after the initial format because we don't know where the call expression
  // will be placed (specifically its indentation), so we have to do two
  // prettier.format calls back-to-back.
  return prettier.format(
    prettier.format(sourceFileWithSnapshots, {
      ...config,
      filepath: sourceFilePath
    }),
    {
      ...config,
      filepath: sourceFilePath,
      parser: createFormattingParser(snapshotMatcherNames, inferredParser)
    }
  );
};

// This parser formats snapshots to the correct indentation.
const createFormattingParser =
  (snapshotMatcherNames, inferredParser) => (text, parsers, options) => {
    // Workaround for https://github.com/prettier/prettier/issues/3150
    options.parser = inferredParser;
    const ast = resolveAst(parsers[inferredParser](text, options));
    babelTraverse(ast, {
      CallExpression({node: {arguments: args, callee}, parent}) {
        if (
          callee.type !== 'MemberExpression' ||
          callee.property.type !== 'Identifier' ||
          !snapshotMatcherNames.includes(callee.property.name) ||
          !callee.loc ||
          callee.computed
        ) {
          return;
        }
        let snapshotIndex;
        let snapshot;
        for (let i = 0; i < args.length; i++) {
          const node = args[i];
          if (node.type === 'TemplateLiteral') {
            snapshotIndex = i;
            snapshot = node.quasis[0].value.raw;
          }
        }
        if (snapshot === undefined || snapshotIndex === undefined) {
          return;
        }
        const startColumn =
          (0, _types.isAwaitExpression)(parent) && parent.loc
            ? parent.loc.start.column
            : callee.loc.start.column;
        const useSpaces = !options.useTabs;
        snapshot = indent(
          snapshot,
          Math.ceil(
            useSpaces
              ? startColumn / (options.tabWidth ?? 1)
              : // Each tab is 2 characters.
                startColumn / 2
          ),
          useSpaces ? ' '.repeat(options.tabWidth ?? 1) : '\t'
        );
        const replacementNode = templateLiteral(
          [
            templateElement({
              raw: snapshot
            })
          ],
          []
        );
        args[snapshotIndex] = replacementNode;
      }
    });
    return ast;
  };
const simpleDetectParser = filePath => {
  const extname = path.extname(filePath);
  if (/\.tsx?$/.test(extname)) {
    return 'typescript';
  }
  return 'babel';
};
