// deno-fmt-ignore-file
// deno-lint-ignore-file

// Copyright Joyent and Node contributors. All rights reserved. MIT license.

/**
 * This file is meant as a replacement for the original common/index.js
 *
 * That file has a lot of node functionality not currently supported, so this is a lite
 * version of that file, which most tests should be able to use
 */
'use strict';
const assert = require("assert");
const { spawn } = require('child_process');
const path = require("path");
const util = require("util");
const tmpdir = require("./tmpdir");

function platformTimeout(ms) {
  return ms;
}

let localhostIPv4 = null;

let knownGlobals = [
  AbortSignal,
  addEventListener,
  alert,
  atob,
  btoa,
  Buffer,
  caches,
  clearImmediate,
  close,
  closed,
  confirm,
  console,
  createImageBitmap,
  crypto,
  Deno,
  dispatchEvent,
  EventSource,
  fetch,
  getParent,
  global,
  global.clearInterval,
  global.clearTimeout,
  global.setInterval,
  global.setTimeout,
  localStorage,
  location,
  name,
  navigator,
  onload,
  onunload,
  process,
  prompt,
  queueMicrotask,
  removeEventListener,
  reportError,
  sessionStorage,
  setImmediate,
];

if (global.AbortController)
  knownGlobals.push(global.AbortController);

if (global.gc) {
  knownGlobals.push(global.gc);
}

if (global.performance) {
  knownGlobals.push(global.performance);
}
if (global.PerformanceMark) {
  knownGlobals.push(global.PerformanceMark);
}
if (global.PerformanceMeasure) {
  knownGlobals.push(global.PerformanceMeasure);
}

if (global.structuredClone) {
  knownGlobals.push(global.structuredClone);
}

function allowGlobals(...allowlist) {
  knownGlobals = knownGlobals.concat(allowlist);
}

if (process.env.NODE_TEST_KNOWN_GLOBALS !== '0') {
  if (process.env.NODE_TEST_KNOWN_GLOBALS) {
    const knownFromEnv = process.env.NODE_TEST_KNOWN_GLOBALS.split(',').map((name) => global[name]);
    allowGlobals(...knownFromEnv);
  }

  function leakedGlobals() {
    const leaked = [];

    for (const val in global) {
      if (!knownGlobals.includes(global[val])) {
        leaked.push(val);
      }
    }

    return leaked;
  }

  process.on('exit', function() {
    const leaked = leakedGlobals();
    if (leaked.length > 0) {
      assert.fail(`Unexpected global(s) found: ${leaked.join(', ')}`);
    }
  });
}

function _expectWarning(name, expected, code) {
  if (typeof expected === 'string') {
    expected = [[expected, code]];
  } else if (!Array.isArray(expected)) {
    expected = Object.entries(expected).map(([a, b]) => [b, a]);
  } else if (!(Array.isArray(expected[0]))) {
    expected = [[expected[0], expected[1]]];
  }
  // Deprecation codes are mandatory, everything else is not.
  if (name === 'DeprecationWarning') {
    expected.forEach(([_, code]) => assert(code, expected));
  }
  return mustCall((warning) => {
    const [ message, code ] = expected.shift();
    assert.strictEqual(warning.name, name);
    if (typeof message === 'string') {
      assert.strictEqual(warning.message, message);
    } else {
      assert.match(warning.message, message);
    }
    assert.strictEqual(warning.code, code);
  }, expected.length);
}

let catchWarning;

// Accepts a warning name and description or array of descriptions or a map of
// warning names to description(s) ensures a warning is generated for each
// name/description pair.
// The expected messages have to be unique per `expectWarning()` call.
function expectWarning(nameOrMap, expected, code) {
  if (catchWarning === undefined) {
    catchWarning = {};
    process.on('warning', (warning) => {
      if (!catchWarning[warning.name]) {
        throw new TypeError(
          `"${warning.name}" was triggered without being expected.\n` +
          util.inspect(warning)
        );
      }
      catchWarning[warning.name](warning);
    });
  }
  if (typeof nameOrMap === 'string') {
    catchWarning[nameOrMap] = _expectWarning(nameOrMap, expected, code);
  } else {
    Object.keys(nameOrMap).forEach((name) => {
      catchWarning[name] = _expectWarning(name, nameOrMap[name]);
    });
  }
}

/**
 * Useful for testing expected internal/error objects
 *
 * @param {Error} error
 */
function expectsError(validator, exact) {
  /**
   * @param {Error} error
   */
  return mustCall((...args) => {
    if (args.length !== 1) {
      // Do not use `assert.strictEqual()` to prevent `inspect` from
      // always being called.
      assert.fail(`Expected one argument, got ${util.inspect(args)}`);
    }
    const error = args.pop();
    const descriptor = Object.getOwnPropertyDescriptor(error, 'message');
    // The error message should be non-enumerable
    assert.strictEqual(descriptor.enumerable, false);

    assert.throws(() => { throw error; }, validator);
    return true;
  }, exact);
}

const noop = () => {};

/**
 * @param {Function} fn
 * @param {number} exact
 */
function mustCall(fn, exact) {
  return _mustCallInner(fn, exact, "exact");
}

function mustCallAtLeast(fn, minimum) {
  return _mustCallInner(fn, minimum, 'minimum');
}

function mustSucceed(fn, exact) {
  return mustCall(function(err, ...args) {
    assert.ifError(err);
    if (typeof fn === 'function')
      return fn.apply(this, args);
  }, exact);
}

const mustCallChecks = [];
/**
 * @param {number} exitCode
 */
function runCallChecks(exitCode) {
  if (exitCode !== 0) return;

  const failed = mustCallChecks.filter(function (context) {
    if ("minimum" in context) {
      context.messageSegment = `at least ${context.minimum}`;
      return context.actual < context.minimum;
    }
    context.messageSegment = `exactly ${context.exact}`;
    return context.actual !== context.exact;
  });

  failed.forEach(function (context) {
    console.log(
      "Mismatched %s function calls. Expected %s, actual %d.",
      context.name,
      context.messageSegment,
      context.actual,
    );
    console.log(context.stack.split("\n").slice(2).join("\n"));
  });

  if (failed.length) process.exit(1);
}

/**
 * @param {Function} fn
 * @param {"exact" | "minimum"} field
 */
function _mustCallInner(fn, criteria = 1, field) {
  // @ts-ignore
  if (process._exiting) {
    throw new Error("Cannot use common.mustCall*() in process exit handler");
  }
  if (typeof fn === "number") {
    criteria = fn;
    fn = noop;
  } else if (fn === undefined) {
    fn = noop;
  }

  if (typeof criteria !== "number") {
    throw new TypeError(`Invalid ${field} value: ${criteria}`);
  }

  let context;
  if (field === "exact") {
    context = {
      exact: criteria,
      actual: 0,
      stack: util.inspect(new Error()),
      name: fn.name || "<anonymous>",
    };
  } else {
    context = {
      minimum: criteria,
      actual: 0,
      stack: util.inspect(new Error()),
      name: fn.name || "<anonymous>",
    };
  }

  // Add the exit listener only once to avoid listener leak warnings
  if (mustCallChecks.length === 0) process.on("exit", runCallChecks);

  mustCallChecks.push(context);

  return function () {
    context.actual++;
    return fn.apply(this, arguments);
  };
}

/**
 * @param {string=} msg
 */
function mustNotCall(msg) {
  /**
   * @param {any[]} args
   */
  return function mustNotCall(...args) {
    const argsInfo = args.length > 0
      ? `\ncalled with arguments: ${args.map(util.inspect).join(", ")}`
      : "";
    assert.fail(
      `${msg || "function should not have been called"} at unknown` +
        argsInfo,
    );
  };
}

const _mustNotMutateObjectDeepProxies = new WeakMap();

function mustNotMutateObjectDeep(original) {
  // Return primitives and functions directly. Primitives are immutable, and
  // proxied functions are impossible to compare against originals, e.g. with
  // `assert.deepEqual()`.
  if (original === null || typeof original !== 'object') {
    return original;
  }

  const cachedProxy = _mustNotMutateObjectDeepProxies.get(original);
  if (cachedProxy) {
    return cachedProxy;
  }

  const _mustNotMutateObjectDeepHandler = {
    __proto__: null,
    defineProperty(target, property, descriptor) {
      assert.fail(`Expected no side effects, got ${inspect(property)} ` +
                  'defined');
    },
    deleteProperty(target, property) {
      assert.fail(`Expected no side effects, got ${inspect(property)} ` +
                  'deleted');
    },
    get(target, prop, receiver) {
      return mustNotMutateObjectDeep(Reflect.get(target, prop, receiver));
    },
    preventExtensions(target) {
      assert.fail('Expected no side effects, got extensions prevented on ' +
                  inspect(target));
    },
    set(target, property, value, receiver) {
      assert.fail(`Expected no side effects, got ${inspect(value)} ` +
                  `assigned to ${inspect(property)}`);
    },
    setPrototypeOf(target, prototype) {
      assert.fail(`Expected no side effects, got set prototype to ${prototype}`);
    }
  };

  const proxy = new Proxy(original, _mustNotMutateObjectDeepHandler);
  _mustNotMutateObjectDeepProxies.set(original, proxy);
  return proxy;
}

// A helper function to simplify checking for ERR_INVALID_ARG_TYPE output.
function invalidArgTypeHelper(input) {
  if (input == null) {
    return ` Received ${input}`;
  }
  if (typeof input === "function" && input.name) {
    return ` Received function ${input.name}`;
  }
  if (typeof input === "object") {
    if (input.constructor && input.constructor.name) {
      return ` Received an instance of ${input.constructor.name}`;
    }
    return ` Received ${util.inspect(input, { depth: -1 })}`;
  }
  let inspected = util.inspect(input, { colors: false });
  if (inspected.length > 25) {
    inspected = `${inspected.slice(0, 25)}...`;
  }
  return ` Received type ${typeof input} (${inspected})`;
}

const isWindows = process.platform === 'win32';
const isAIX = process.platform === 'aix';
const isSunOS = process.platform === 'sunos';
const isFreeBSD = process.platform === 'freebsd';
const isOpenBSD = process.platform === 'openbsd';
const isLinux = process.platform === 'linux';
const isOSX = process.platform === 'darwin';

const isDumbTerminal = process.env.TERM === 'dumb';

function skipIfDumbTerminal() {
  if (isDumbTerminal) {
    skip('skipping - dumb terminal');
  }
}

function printSkipMessage(msg) {
  console.log(`1..0 # Skipped: ${msg}`);
}

function skip(msg) {
  printSkipMessage(msg);
  process.exit(0);
}

const PIPE = (() => {
  const localRelative = path.relative(process.cwd(), `${tmpdir.path}/`);
  const pipePrefix = isWindows ? "\\\\.\\pipe\\" : localRelative;
  const pipeName = `node-test.${process.pid}.sock`;
  return path.join(pipePrefix, pipeName);
})();

function getArrayBufferViews(buf) {
  const { buffer, byteOffset, byteLength } = buf;

  const out = [];

  const arrayBufferViews = [
    Int8Array,
    Uint8Array,
    Uint8ClampedArray,
    Int16Array,
    Uint16Array,
    Int32Array,
    Uint32Array,
    Float32Array,
    Float64Array,
    DataView,
  ];

  for (const type of arrayBufferViews) {
    const { BYTES_PER_ELEMENT = 1 } = type;
    if (byteLength % BYTES_PER_ELEMENT === 0) {
      out.push(new type(buffer, byteOffset, byteLength / BYTES_PER_ELEMENT));
    }
  }
  return out;
}

function getBufferSources(buf) {
  return [...getArrayBufferViews(buf), new Uint8Array(buf).buffer];
}

const pwdCommand = isWindows ?
  ['cmd.exe', ['/d', '/c', 'cd']] :
  ['pwd', []];

  function spawnPromisified(...args) {
    let stderr = '';
    let stdout = '';

    const child = spawn(...args);
    child.stderr.setEncoding('utf8');
    child.stderr.on('data', (data) => { stderr += data; });
    child.stdout.setEncoding('utf8');
    child.stdout.on('data', (data) => { stdout += data; });

    return new Promise((resolve, reject) => {
      child.on('close', (code, signal) => {
        resolve({
          code,
          signal,
          stderr,
          stdout,
        });
      });
      child.on('error', (code, signal) => {
        reject({
          code,
          signal,
          stderr,
          stdout,
        });
      });
    });
  }

module.exports = {
  allowGlobals,
  defaultAutoSelectFamilyAttemptTimeout: 2500,
  expectsError,
  expectWarning,
  getArrayBufferViews,
  getBufferSources,
  hasCrypto: true,
  hasIntl: true,
  hasMultiLocalhost() {
    return false;
  },
  invalidArgTypeHelper,
  mustCall,
  mustCallAtLeast,
  mustNotCall,
  mustNotMutateObjectDeep,
  mustSucceed,
  PIPE,
  platformTimeout,
  printSkipMessage,
  pwdCommand,
  skipIfDumbTerminal,
  spawnPromisified,
  isDumbTerminal,
  isWindows,
  isAIX,
  isSunOS,
  isFreeBSD,
  isOpenBSD,
  isLinux,
  isOSX,
  isMainThread: true, // TODO(f3n67u): replace with `worker_thread.isMainThread` when `worker_thread` implemented
  skip,
  get hasIPv6() {
    const iFaces = require('os').networkInterfaces();
    const re = isWindows ? /Loopback Pseudo-Interface/ : /lo/;
    return Object.keys(iFaces).some((name) => {
      return re.test(name) &&
             iFaces[name].some(({ family }) => family === 'IPv6');
    });
  },

  get localhostIPv4() {
    if (localhostIPv4 !== null) return localhostIPv4;
    if (localhostIPv4 === null) localhostIPv4 = '127.0.0.1';

    return localhostIPv4;
  },

  get PORT() {
    return 12346;
  },
};