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

// Forked from https://github.com/tribou/jest-serializer-path/blob/master/lib/index.js
// Added some project-specific handlers

import os from 'os';
import path from 'path';
import fs from 'fs';
import _ from 'lodash';
import {escapePath} from '@docusaurus/utils';
import {version} from '@docusaurus/core/package.json';
import stripAnsi from 'strip-ansi';

export function print(
  val: unknown,
  serialize: (val: unknown) => string,
): string {
  if (val instanceof Error) {
    const message = normalizePaths(val.message);
    const error = new Error(message);
    const allKeys = [
      ...Object.getOwnPropertyNames(error),
      ...Object.keys(val),
    ] as (keyof Error)[];
    allKeys.forEach((key) => {
      error[key] = normalizePaths(val[key]) as never;
    });
    return serialize(error);
  } else if (val && typeof val === 'object') {
    const normalizedValue = _.cloneDeep(val) as {[key: string]: unknown};

    Object.keys(normalizedValue).forEach((key) => {
      normalizedValue[key] = normalizePaths(normalizedValue[key]);
    });
    return serialize(normalizedValue);
  }
  return serialize(normalizePaths(val));
}

export function test(val: unknown): boolean {
  return (
    (typeof val === 'object' &&
      val &&
      Object.keys(val).some((key) =>
        shouldUpdate((val as {[key: string]: unknown})[key]),
      )) ||
    // val.message is non-enumerable in an error
    (val instanceof Error && shouldUpdate(val.message)) ||
    shouldUpdate(val)
  );
}

/**
 * Normalize paths across platforms.
 * Filters must be ran on all platforms to guard against false positives
 */
function normalizePaths<T>(value: T): T {
  if (typeof value !== 'string') {
    return value;
  }

  const cwd = process.cwd();
  const cwdReal = getRealPath(cwd);
  const tempDir = os.tmpdir();
  const tempDirReal = getRealPath(tempDir);
  const homeDir = os.homedir();
  const homeDirReal = getRealPath(homeDir);

  const homeRelativeToTemp = path.relative(tempDir, homeDir);
  const homeRelativeToTempReal = path.relative(tempDirReal, homeDir);
  const homeRealRelativeToTempReal = path.relative(tempDirReal, homeDirReal);
  const homeRealRelativeToTemp = path.relative(tempDir, homeDirReal);

  const runner: ((val: string) => string)[] = [
    (val) => (val.includes('keepAnsi') ? val : stripAnsi(val)),
    // Replace process.cwd with <PROJECT_ROOT>
    (val) => val.split(cwdReal).join('<PROJECT_ROOT>'),
    (val) => val.split(cwd).join('<PROJECT_ROOT>'),

    // Replace temp directory with <TEMP_DIR>
    (val) => val.split(tempDirReal).join('<TEMP_DIR>'),
    (val) => val.split(tempDir).join('<TEMP_DIR>'),

    // Replace home directory with <HOME_DIR>
    (val) => val.split(homeDirReal).join('<HOME_DIR>'),
    (val) => val.split(homeDir).join('<HOME_DIR>'),

    // Handle HOME_DIR nested inside TEMP_DIR
    (val) =>
      val
        .split(`<TEMP_DIR>${path.sep + homeRelativeToTemp}`)
        .join('<HOME_DIR>'),
    (val) =>
      val
        .split(`<TEMP_DIR>${path.sep + homeRelativeToTempReal}`)
        .join('<HOME_DIR>'),
    (val) =>
      val
        .split(`<TEMP_DIR>${path.sep + homeRealRelativeToTempReal}`)
        .join('<HOME_DIR>'),
    (val) =>
      val
        .split(`<TEMP_DIR>${path.sep + homeRealRelativeToTemp}`)
        .join('<HOME_DIR>'),

    // Replace the Docusaurus version with a stub
    (val) => val.split(version).join('<CURRENT_VERSION>'),

    // In case the CWD is escaped
    (val) => val.split(escapePath(cwd)).join('<PROJECT_ROOT>'),

    // Remove win32 drive letters, C:\ -> \
    (val) => val.replace(/[a-zA-Z]:\\/g, '\\'),

    // Remove duplicate backslashes created from escapePath
    (val) => val.replace(/\\\\/g, '\\'),

    // Convert win32 backslash's to forward slashes, \ -> /;
    // ignore some that look like escape sequences.
    (val) => val.replace(/\\(?!")/g, '/'),
  ];

  let result = value as string;
  runner.forEach((current) => {
    result = current(result);
  });

  return result as T & string;
}

function shouldUpdate(value: unknown) {
  return typeof value === 'string' && normalizePaths(value) !== value;
}

function getRealPath(pathname: string) {
  try {
    // eslint-disable-next-line no-restricted-properties
    const realPath = fs.realpathSync(pathname);
    return realPath;
  } catch (err) {
    return pathname;
  }
}
