// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license.

// TODO(petamoriken): enable prefer-primordials for node polyfills
// deno-lint-ignore-file ban-types prefer-primordials

import {
  AssertionError,
  AssertionErrorConstructorOptions,
} from "ext:deno_node/assertion_error.ts";
import * as asserts from "ext:deno_node/_util/std_asserts.ts";
import { inspect } from "node:util";
import {
  ERR_AMBIGUOUS_ARGUMENT,
  ERR_INVALID_ARG_TYPE,
  ERR_INVALID_ARG_VALUE,
  ERR_INVALID_RETURN_VALUE,
  ERR_MISSING_ARGS,
} from "ext:deno_node/internal/errors.ts";
import { isDeepEqual } from "ext:deno_node/internal/util/comparisons.ts";
import { primordials } from "ext:core/mod.js";

const { ObjectPrototypeIsPrototypeOf } = primordials;

function innerFail(obj: {
  actual?: unknown;
  expected?: unknown;
  message?: string | Error;
  operator?: string;
}) {
  if (obj.message instanceof Error) {
    throw obj.message;
  }

  throw new AssertionError({
    actual: obj.actual,
    expected: obj.expected,
    message: obj.message,
    operator: obj.operator,
  });
}

interface ExtendedAssertionErrorConstructorOptions
  extends AssertionErrorConstructorOptions {
  generatedMessage?: boolean;
}

// TODO(uki00a): This function is a workaround for setting the `generatedMessage` property flexibly.
function createAssertionError(
  options: ExtendedAssertionErrorConstructorOptions,
): AssertionError {
  const error = new AssertionError(options);
  if (options.generatedMessage) {
    error.generatedMessage = true;
  }
  return error;
}

/** Converts the std assertion error to node.js assertion error */
function toNode(
  fn: () => void,
  opts?: {
    actual: unknown;
    expected: unknown;
    message?: string | Error;
    operator?: string;
  },
) {
  const { operator, message, actual, expected } = opts || {};
  try {
    fn();
  } catch (e) {
    if (e instanceof asserts.AssertionError) {
      if (typeof message === "string") {
        throw new AssertionError({
          operator,
          message,
          actual,
          expected,
        });
      } else if (message instanceof Error) {
        throw message;
      } else {
        throw new AssertionError({
          operator,
          message: e.message,
          actual,
          expected,
        });
      }
    }
    throw e;
  }
}

function assert(actual: unknown, message?: string | Error): asserts actual {
  if (arguments.length === 0) {
    throw new AssertionError({
      message: "No value argument passed to `assert.ok()`",
    });
  }
  toNode(
    () => asserts.assert(actual),
    { message, actual, expected: true },
  );
}
const ok = assert;

function throws(
  fn: () => void,
  error?: RegExp | Function | Error,
  message?: string,
) {
  // Check arg types
  if (typeof fn !== "function") {
    throw new ERR_INVALID_ARG_TYPE("fn", "function", fn);
  }
  if (
    typeof error === "object" && error !== null &&
    Object.getPrototypeOf(error) === Object.prototype &&
    Object.keys(error).length === 0
  ) {
    // error is an empty object
    throw new ERR_INVALID_ARG_VALUE(
      "error",
      error,
      "may not be an empty object",
    );
  }
  if (typeof message === "string") {
    if (
      !(error instanceof RegExp) && typeof error !== "function" &&
      !(error instanceof Error) && typeof error !== "object"
    ) {
      throw new ERR_INVALID_ARG_TYPE("error", [
        "Function",
        "Error",
        "RegExp",
        "Object",
      ], error);
    }
  } else {
    if (
      typeof error !== "undefined" && typeof error !== "string" &&
      !(error instanceof RegExp) && typeof error !== "function" &&
      !(error instanceof Error) && typeof error !== "object"
    ) {
      throw new ERR_INVALID_ARG_TYPE("error", [
        "Function",
        "Error",
        "RegExp",
        "Object",
      ], error);
    }
  }

  // Checks test function
  try {
    fn();
  } catch (e) {
    if (
      validateThrownError(e, error, message, {
        operator: throws,
      })
    ) {
      return;
    }
  }
  if (message) {
    let msg = `Missing expected exception: ${message}`;
    if (typeof error === "function" && error?.name) {
      msg = `Missing expected exception (${error.name}): ${message}`;
    }
    throw new AssertionError({
      message: msg,
      operator: "throws",
      actual: undefined,
      expected: error,
    });
  } else if (typeof error === "string") {
    // Use case of throws(fn, message)
    throw new AssertionError({
      message: `Missing expected exception: ${error}`,
      operator: "throws",
      actual: undefined,
      expected: undefined,
    });
  } else if (typeof error === "function" && error?.prototype !== undefined) {
    throw new AssertionError({
      message: `Missing expected exception (${error.name}).`,
      operator: "throws",
      actual: undefined,
      expected: error,
    });
  } else {
    throw new AssertionError({
      message: "Missing expected exception.",
      operator: "throws",
      actual: undefined,
      expected: error,
    });
  }
}

function doesNotThrow(
  fn: () => void,
  message?: string,
): void;
function doesNotThrow(
  fn: () => void,
  error?: Function,
  message?: string | Error,
): void;
function doesNotThrow(
  fn: () => void,
  error?: RegExp,
  message?: string,
): void;
function doesNotThrow(
  fn: () => void,
  expected?: Function | RegExp | string,
  message?: string | Error,
) {
  // Check arg type
  if (typeof fn !== "function") {
    throw new ERR_INVALID_ARG_TYPE("fn", "function", fn);
  } else if (
    !(expected instanceof RegExp) && typeof expected !== "function" &&
    typeof expected !== "string" && typeof expected !== "undefined"
  ) {
    throw new ERR_INVALID_ARG_TYPE("expected", ["Function", "RegExp"], fn);
  }

  // Checks test function
  try {
    fn();
  } catch (e) {
    gotUnwantedException(e, expected, message, doesNotThrow);
  }
}

function equal(
  actual: unknown,
  expected: unknown,
  message?: string | Error,
) {
  if (arguments.length < 2) {
    throw new ERR_MISSING_ARGS("actual", "expected");
  }

  if (actual == expected) {
    return;
  }

  if (Number.isNaN(actual) && Number.isNaN(expected)) {
    return;
  }

  if (typeof message === "string") {
    throw new AssertionError({
      message,
    });
  } else if (message instanceof Error) {
    throw message;
  }

  toNode(
    () => asserts.assertStrictEquals(actual, expected),
    {
      message: message || `${actual} == ${expected}`,
      operator: "==",
      actual,
      expected,
    },
  );
}
function notEqual(
  actual: unknown,
  expected: unknown,
  message?: string | Error,
) {
  if (arguments.length < 2) {
    throw new ERR_MISSING_ARGS("actual", "expected");
  }

  if (Number.isNaN(actual) && Number.isNaN(expected)) {
    throw new AssertionError({
      message: `${actual} != ${expected}`,
      operator: "!=",
      actual,
      expected,
    });
  }
  if (actual != expected) {
    return;
  }

  if (typeof message === "string") {
    throw new AssertionError({
      message,
    });
  } else if (message instanceof Error) {
    throw message;
  }

  toNode(
    () => asserts.assertNotStrictEquals(actual, expected),
    {
      message: message || `${actual} != ${expected}`,
      operator: "!=",
      actual,
      expected,
    },
  );
}
function strictEqual(
  actual: unknown,
  expected: unknown,
  message?: string | Error,
) {
  if (arguments.length < 2) {
    throw new ERR_MISSING_ARGS("actual", "expected");
  }

  toNode(
    () => asserts.assertStrictEquals(actual, expected),
    { message, operator: "strictEqual", actual, expected },
  );
}
function notStrictEqual(
  actual: unknown,
  expected: unknown,
  message?: string | Error,
) {
  if (arguments.length < 2) {
    throw new ERR_MISSING_ARGS("actual", "expected");
  }

  toNode(
    () => asserts.assertNotStrictEquals(actual, expected),
    { message, actual, expected, operator: "notStrictEqual" },
  );
}

function deepEqual(
  actual: unknown,
  expected: unknown,
  message?: string | Error,
) {
  if (arguments.length < 2) {
    throw new ERR_MISSING_ARGS("actual", "expected");
  }

  if (!isDeepEqual(actual, expected)) {
    innerFail({ actual, expected, message, operator: "deepEqual" });
  }
}
function notDeepEqual(
  actual: unknown,
  expected: unknown,
  message?: string | Error,
) {
  if (arguments.length < 2) {
    throw new ERR_MISSING_ARGS("actual", "expected");
  }

  if (isDeepEqual(actual, expected)) {
    innerFail({ actual, expected, message, operator: "notDeepEqual" });
  }
}
function deepStrictEqual(
  actual: unknown,
  expected: unknown,
  message?: string | Error,
) {
  if (arguments.length < 2) {
    throw new ERR_MISSING_ARGS("actual", "expected");
  }

  toNode(
    () => asserts.assertEquals(actual, expected),
    { message, actual, expected, operator: "deepStrictEqual" },
  );
}
function notDeepStrictEqual(
  actual: unknown,
  expected: unknown,
  message?: string | Error,
) {
  if (arguments.length < 2) {
    throw new ERR_MISSING_ARGS("actual", "expected");
  }

  toNode(
    () => asserts.assertNotEquals(actual, expected),
    { message, actual, expected, operator: "deepNotStrictEqual" },
  );
}

function fail(message?: string | Error): never {
  if (typeof message === "string" || message == null) {
    throw createAssertionError({
      message: message ?? "Failed",
      operator: "fail",
      generatedMessage: message == null,
    });
  } else {
    throw message;
  }
}
function match(actual: string, regexp: RegExp, message?: string | Error) {
  if (arguments.length < 2) {
    throw new ERR_MISSING_ARGS("actual", "regexp");
  }
  if (!(regexp instanceof RegExp)) {
    throw new ERR_INVALID_ARG_TYPE("regexp", "RegExp", regexp);
  }

  toNode(
    () => asserts.assertMatch(actual, regexp),
    { message, actual, expected: regexp, operator: "match" },
  );
}

function doesNotMatch(
  string: string,
  regexp: RegExp,
  message?: string | Error,
) {
  if (arguments.length < 2) {
    throw new ERR_MISSING_ARGS("string", "regexp");
  }
  if (!(regexp instanceof RegExp)) {
    throw new ERR_INVALID_ARG_TYPE("regexp", "RegExp", regexp);
  }
  if (typeof string !== "string") {
    if (message instanceof Error) {
      throw message;
    }
    throw new AssertionError({
      message: message ||
        `The "string" argument must be of type string. Received type ${typeof string} (${
          inspect(string)
        })`,
      actual: string,
      expected: regexp,
      operator: "doesNotMatch",
    });
  }

  toNode(
    () => asserts.assertNotMatch(string, regexp),
    { message, actual: string, expected: regexp, operator: "doesNotMatch" },
  );
}

function strict(actual: unknown, message?: string | Error): asserts actual {
  if (arguments.length === 0) {
    throw new AssertionError({
      message: "No value argument passed to `assert.ok()`",
    });
  }
  assert(actual, message);
}

function rejects(
  // deno-lint-ignore no-explicit-any
  asyncFn: Promise<any> | (() => Promise<any>),
  error?: RegExp | Function | Error,
): Promise<void>;

function rejects(
  // deno-lint-ignore no-explicit-any
  asyncFn: Promise<any> | (() => Promise<any>),
  message?: string,
): Promise<void>;

// Intentionally avoid using async/await because test-assert-async.js requires it
function rejects(
  // deno-lint-ignore no-explicit-any
  asyncFn: Promise<any> | (() => Promise<any>),
  error?: RegExp | Function | Error | string,
  message?: string,
) {
  let promise: Promise<void>;
  if (typeof asyncFn === "function") {
    try {
      promise = asyncFn();
    } catch (err) {
      // If `asyncFn` throws an error synchronously, this function returns a rejected promise.
      return Promise.reject(err);
    }

    if (!isValidThenable(promise)) {
      return Promise.reject(
        new ERR_INVALID_RETURN_VALUE(
          "instance of Promise",
          "promiseFn",
          promise,
        ),
      );
    }
  } else if (!isValidThenable(asyncFn)) {
    return Promise.reject(
      new ERR_INVALID_ARG_TYPE("promiseFn", ["function", "Promise"], asyncFn),
    );
  } else {
    promise = asyncFn;
  }

  function onFulfilled() {
    let message = "Missing expected rejection";
    if (typeof error === "string") {
      message += `: ${error}`;
    } else if (typeof error === "function" && error.prototype !== undefined) {
      message += ` (${error.name}).`;
    } else {
      message += ".";
    }
    return Promise.reject(createAssertionError({
      message,
      operator: "rejects",
      generatedMessage: true,
    }));
  }

  // deno-lint-ignore camelcase
  function rejects_onRejected(e: Error) { // TODO(uki00a): In order to `test-assert-async.js` pass, intentionally adds `rejects_` as a prefix.
    if (
      validateThrownError(e, error, message, {
        operator: rejects,
        validationFunctionName: "validate",
      })
    ) {
      return;
    }
  }

  return promise.then(onFulfilled, rejects_onRejected);
}

function doesNotReject(
  // deno-lint-ignore no-explicit-any
  asyncFn: Promise<any> | (() => Promise<any>),
  error?: RegExp | Function,
): Promise<void>;

function doesNotReject(
  // deno-lint-ignore no-explicit-any
  asyncFn: Promise<any> | (() => Promise<any>),
  message?: string,
): Promise<void>;

// Intentionally avoid using async/await because test-assert-async.js requires it
function doesNotReject(
  // deno-lint-ignore no-explicit-any
  asyncFn: Promise<any> | (() => Promise<any>),
  error?: RegExp | Function | string,
  message?: string,
) {
  // deno-lint-ignore no-explicit-any
  let promise: Promise<any>;
  if (typeof asyncFn === "function") {
    try {
      const value = asyncFn();
      if (!isValidThenable(value)) {
        return Promise.reject(
          new ERR_INVALID_RETURN_VALUE(
            "instance of Promise",
            "promiseFn",
            value,
          ),
        );
      }
      promise = value;
    } catch (e) {
      // If `asyncFn` throws an error synchronously, this function returns a rejected promise.
      return Promise.reject(e);
    }
  } else if (!isValidThenable(asyncFn)) {
    return Promise.reject(
      new ERR_INVALID_ARG_TYPE("promiseFn", ["function", "Promise"], asyncFn),
    );
  } else {
    promise = asyncFn;
  }

  return promise.then(
    () => {},
    (e) => gotUnwantedException(e, error, message, doesNotReject),
  );
}

function gotUnwantedException(
  // deno-lint-ignore no-explicit-any
  e: any,
  expected: RegExp | Function | string | null | undefined,
  message: string | Error | null | undefined,
  operator: Function,
): never {
  if (typeof expected === "string") {
    // The use case of doesNotThrow(fn, message);
    throw new AssertionError({
      message:
        `Got unwanted exception: ${expected}\nActual message: "${e.message}"`,
      operator: operator.name,
    });
  } else if (
    typeof expected === "function" && expected.prototype !== undefined
  ) {
    // The use case of doesNotThrow(fn, Error, message);
    if (e instanceof expected) {
      let msg = `Got unwanted exception: ${e.constructor?.name}`;
      if (message) {
        msg += ` ${String(message)}`;
      }
      throw new AssertionError({
        message: msg,
        operator: operator.name,
      });
    } else if (expected.prototype instanceof Error) {
      throw e;
    } else {
      const result = expected(e);
      if (result === true) {
        let msg = `Got unwanted rejection.\nActual message: "${e.message}"`;
        if (message) {
          msg += ` ${String(message)}`;
        }
        throw new AssertionError({
          message: msg,
          operator: operator.name,
        });
      }
    }
    throw e;
  } else {
    if (message) {
      throw new AssertionError({
        message: `Got unwanted exception: ${message}\nActual message: "${
          e ? e.message : String(e)
        }"`,
        operator: operator.name,
      });
    }
    throw new AssertionError({
      message: `Got unwanted exception.\nActual message: "${
        e ? e.message : String(e)
      }"`,
      operator: operator.name,
    });
  }
}

/**
 * Throws `value` if the value is not `null` or `undefined`.
 *
 * @param err
 */
// deno-lint-ignore no-explicit-any
function ifError(err: any) {
  if (err !== null && err !== undefined) {
    let message = "ifError got unwanted exception: ";

    if (typeof err === "object" && typeof err.message === "string") {
      if (err.message.length === 0 && err.constructor) {
        message += err.constructor.name;
      } else {
        message += err.message;
      }
    } else {
      message += inspect(err);
    }

    const newErr = new AssertionError({
      actual: err,
      expected: null,
      operator: "ifError",
      message,
      stackStartFn: ifError,
    });

    // Make sure we actually have a stack trace!
    const origStack = err.stack;

    if (typeof origStack === "string") {
      // This will remove any duplicated frames from the error frames taken
      // from within `ifError` and add the original error frames to the newly
      // created ones.
      const tmp2 = origStack.split("\n");
      tmp2.shift();

      // Filter all frames existing in err.stack.
      let tmp1 = newErr!.stack?.split("\n");

      for (const errFrame of tmp2) {
        // Find the first occurrence of the frame.
        const pos = tmp1?.indexOf(errFrame);

        if (pos !== -1) {
          // Only keep new frames.
          tmp1 = tmp1?.slice(0, pos);

          break;
        }
      }

      newErr.stack = `${tmp1?.join("\n")}\n${tmp2.join("\n")}`;
    }

    throw newErr;
  }
}

interface ValidateThrownErrorOptions {
  operator: Function;
  validationFunctionName?: string;
}

function validateThrownError(
  // deno-lint-ignore no-explicit-any
  e: any,
  error: RegExp | Function | Error | string | null | undefined,
  message: string | undefined | null,
  options: ValidateThrownErrorOptions,
): boolean {
  if (typeof error === "string") {
    if (message != null) {
      throw new ERR_INVALID_ARG_TYPE(
        "error",
        ["Object", "Error", "Function", "RegExp"],
        error,
      );
    } else if (typeof e === "object" && e !== null) {
      if (e.message === error) {
        throw new ERR_AMBIGUOUS_ARGUMENT(
          "error/message",
          `The error message "${e.message}" is identical to the message.`,
        );
      }
    } else if (e === error) {
      throw new ERR_AMBIGUOUS_ARGUMENT(
        "error/message",
        `The error "${e}" is identical to the message.`,
      );
    }
    message = error;
    error = undefined;
  }
  if (error?.prototype !== undefined && e instanceof error) {
    return true;
  }
  if (
    typeof error === "function" &&
    (error === Error || ObjectPrototypeIsPrototypeOf(Error, error))
  ) {
    // error is a constructor
    if (e instanceof error) {
      return true;
    }
    throw createAssertionError({
      message:
        `The error is expected to be an instance of "${error.name}". Received "${e?.constructor?.name}"\n\nError message:\n\n${e?.message}`,
      actual: e,
      expected: error,
      operator: options.operator.name,
      generatedMessage: true,
    });
  }
  if (error instanceof Function) {
    const received = error(e);
    if (received === true) {
      return true;
    }
    throw createAssertionError({
      message: `The ${
        options.validationFunctionName
          ? `"${options.validationFunctionName}" validation`
          : "validation"
      } function is expected to return "true". Received ${
        inspect(received)
      }\n\nCaught error:\n\n${e}`,
      actual: e,
      expected: error,
      operator: options.operator.name,
      generatedMessage: true,
    });
  }
  if (error instanceof RegExp) {
    if (error.test(String(e))) {
      return true;
    }
    throw createAssertionError({
      message:
        `The input did not match the regular expression ${error.toString()}. Input:\n\n'${
          String(e)
        }'\n`,
      actual: e,
      expected: error,
      operator: options.operator.name,
      generatedMessage: true,
    });
  }
  if (typeof error === "object" && error !== null) {
    const keys = Object.keys(error);
    if (error instanceof Error) {
      keys.push("name", "message");
    }
    for (const k of keys) {
      if (e == null) {
        throw createAssertionError({
          message: message || "object is expected to thrown, but got null",
          actual: e,
          expected: error,
          operator: options.operator.name,
          generatedMessage: message == null,
        });
      }

      if (typeof e === "string") {
        throw createAssertionError({
          message: message ||
            `object is expected to thrown, but got string: ${e}`,
          actual: e,
          expected: error,
          operator: options.operator.name,
          generatedMessage: message == null,
        });
      }
      if (typeof e === "number") {
        throw createAssertionError({
          message: message ||
            `object is expected to thrown, but got number: ${e}`,
          actual: e,
          expected: error,
          operator: options.operator.name,
          generatedMessage: message == null,
        });
      }
      if (!(k in e)) {
        throw createAssertionError({
          message: message || `A key in the expected object is missing: ${k}`,
          actual: e,
          expected: error,
          operator: options.operator.name,
          generatedMessage: message == null,
        });
      }
      const actual = e[k];
      // deno-lint-ignore no-explicit-any
      const expected = (error as any)[k];
      if (typeof actual === "string" && expected instanceof RegExp) {
        match(actual, expected);
      } else {
        deepStrictEqual(actual, expected);
      }
    }
    return true;
  }
  if (typeof error === "undefined") {
    return true;
  }
  throw createAssertionError({
    message: `Invalid expectation: ${error}`,
    operator: options.operator.name,
    generatedMessage: true,
  });
}

// deno-lint-ignore no-explicit-any
function isValidThenable(maybeThennable: any): boolean {
  if (!maybeThennable) {
    return false;
  }

  if (maybeThennable instanceof Promise) {
    return true;
  }

  const isThenable = typeof maybeThennable.then === "function" &&
    typeof maybeThennable.catch === "function";

  return isThenable && typeof maybeThennable !== "function";
}

Object.assign(strict, {
  AssertionError,
  deepEqual: deepStrictEqual,
  deepStrictEqual,
  doesNotMatch,
  doesNotReject,
  doesNotThrow,
  equal: strictEqual,
  fail,
  ifError,
  match,
  notDeepEqual: notDeepStrictEqual,
  notDeepStrictEqual,
  notEqual: notStrictEqual,
  notStrictEqual,
  ok,
  rejects,
  strict,
  strictEqual,
  throws,
});

export default Object.assign(assert, {
  AssertionError,
  deepEqual,
  deepStrictEqual,
  doesNotMatch,
  doesNotReject,
  doesNotThrow,
  equal,
  fail,
  ifError,
  match,
  notDeepEqual,
  notDeepStrictEqual,
  notEqual,
  notStrictEqual,
  ok,
  rejects,
  strict,
  strictEqual,
  throws,
});

export {
  AssertionError,
  deepEqual,
  deepStrictEqual,
  doesNotMatch,
  doesNotReject,
  doesNotThrow,
  equal,
  fail,
  ifError,
  match,
  notDeepEqual,
  notDeepStrictEqual,
  notEqual,
  notStrictEqual,
  ok,
  rejects,
  strict,
  strictEqual,
  throws,
};