(err, this.snapshotFile);
    }
  }

  writeFile() {
    try {
      const keys = ArrayPrototypeSort(ObjectKeys(this.snapshots));
      const snapshotStrings = ArrayPrototypeMap(keys, (key) => {
        return `exports[\`${key}\`] = \`${this.snapshots[key]}\`;\n`;
      });
      const output = ArrayPrototypeJoin(snapshotStrings, '\n');
      mkdirSync(dirname(this.snapshotFile), { __proto__: null, recursive: true });
      writeFileSync(this.snapshotFile, output, 'utf8');
    } catch (err) {
      throwWriteError(err, this.snapshotFile);
    }
  }
}

class SnapshotManager {
  constructor(updateSnapshots) {
    // A manager instance will only read or write snapshot files based on the
    // updateSnapshots argument.
    this.updateSnapshots = updateSnapshots;
    this.cache = new SafeMap();
  }

  resolveSnapshotFile(entryFile) {
    let snapshotFile = this.cache.get(entryFile);

    if (snapshotFile === undefined) {
      const resolved = resolveSnapshotPathFn(entryFile);

      if (typeof resolved !== 'string') {
        const err = new ERR_INVALID_STATE('Invalid snapshot filename.');
        err.filename = resolved;
        throw err;
      }

      snapshotFile = new SnapshotFile(resolved);
      snapshotFile.loaded = this.updateSnapshots;
      this.cache.set(entryFile, snapshotFile);
    }

    return snapshotFile;
  }

  serialize(input, serializers = serializerFns) {
    try {
      const value = serializeValue(input, serializers);
      return `\n${templateEscape(value)}\n`;
    } catch (err) {
      throwSerializationError(input, err);
    }
  }

  serializeWithoutEscape(input, serializers = serializerFns) {
    try {
      return serializeValue(input, serializers);
    } catch (err) {
      throwSerializationError(input, err);
    }
  }

  writeSnapshotFiles() {
    if (!this.updateSnapshots) {
      debug('skipping write of snapshot files');
      return;
    }

    this.cache.forEach((snapshotFile) => {
      snapshotFile.writeFile();
    });
  }

  createAssert() {
    const manager = this;

    return function snapshotAssertion(actual, options = kEmptyObject) {
      validateObject(options, 'options');
      const {
        serializers = serializerFns,
      } = options;
      validateFunctionArray(serializers, 'options.serializers');
      const { filePath, fullName } = this;
      const snapshotFile = manager.resolveSnapshotFile(filePath);
      const value = manager.serialize(actual, serializers);
      const id = snapshotFile.nextId(fullName);

      if (manager.updateSnapshots) {
        snapshotFile.setSnapshot(id, value);
      } else {
        snapshotFile.readFile();
        strictEqual(value, snapshotFile.getSnapshot(id));
      }
    };
  }

  createFileAssert() {
    const manager = this;

    return function fileSnapshotAssertion(actual, path, options = kEmptyObject) {
      validateString(path, 'path');
      validateObject(options, 'options');
      const {
        serializers = serializerFns,
      } = options;
      validateFunctionArray(serializers, 'options.serializers');
      const value = manager.serializeWithoutEscape(actual, serializers);

      if (manager.updateSnapshots) {
        try {
          mkdirSync(dirname(path), { __proto__: null, recursive: true });
          writeFileSync(path, value, 'utf8');
        } catch (err) {
          throwWriteError(err, path);
        }
      } else {
        let expected;

        try {
          expected = readFileSync(path, 'utf8');
        } catch (err) {
          throwReadError(err, path);
        }

        strictEqual(value, expected);
      }
    };
  }
}

function throwReadError(err, filename) {
  let msg = `Cannot read snapshot file '${filename}.'`;

  if (err?.code === 'ENOENT') {
    msg += ` ${kMissingSnapshotTip}`;
  }

  const error = new ERR_INVALID_STATE(msg);
  error.cause = err;
  error.filename = filename;
  throw error;
}

function throwWriteError(err, filename) {
  const msg = `Cannot write snapshot file '${filename}.'`;
  const error = new ERR_INVALID_STATE(msg);
  error.cause = err;
  error.filename = filename;
  throw error;
}

function throwSerializationError(input, err) {
  const error = new ERR_INVALID_STATE(
    'The provided serializers did not generate a string.',
  );
  error.input = input;
  error.cause = err;
  throw error;
}

function serializeValue(value, serializers) {
  let v = value;

  for (let i = 0; i < serializers.length; ++i) {
    const fn = serializers[i];
    v = fn(v);
  }

  return v;
}

function validateFunctionArray(fns, name) {
  validateArray(fns, name);
  for (let i = 0; i < fns.length; ++i) {
    validateFunction(fns[i], `${name}[${i}]`);
  }
}

function escapeSnapshotKey(str) {
  let result = JSONStringify(str, null, 2).slice(1, -1);
  result = StringPrototypeReplaceAll(result, '`', '\\`');
  result = StringPrototypeReplaceAll(result, '${', '\\${');
  return result;
}

function templateEscape(str) {
  let result = String(str);
  result = StringPrototypeReplaceAll(result, '\\', '\\\\');
  result = StringPrototypeReplaceAll(result, '`', '\\`');
  result = StringPrototypeReplaceAll(result, '${', '\\${');
  return result;
}

module.exports = {
  SnapshotManager,
  defaultResolveSnapshotPath, // Exported for testing only.
  defaultSerializers,         // Exported for testing only.
  setDefaultSnapshotSerializers,
  setResolveSnapshotPath,
};
