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

// TODO(ry) The unit test functions in this module are too coarse. They should
// be broken up into smaller bits.

// TODO(ry) These tests currentl strip all the ANSI colors out. We don't have a
// good way to control whether we produce color output or not since
// std/fmt/colors auto determines whether to put colors in or not. We need
// better infrastructure here so we can properly test the colors.

import {
  assert,
  assertEquals,
  assertStringIncludes,
  assertThrows,
} from "./test_util.ts";
import { stripColor } from "../../../test_util/std/fmt/colors.ts";

const customInspect = Symbol.for("Deno.customInspect");
const {
  Console,
  cssToAnsi: cssToAnsi_,
  inspectArgs,
  parseCss: parseCss_,
  parseCssColor: parseCssColor_,
  // @ts-expect-error TypeScript (as of 3.7) does not support indexing namespaces by symbol
} = Deno[Deno.internal];

function stringify(...args: unknown[]): string {
  return stripColor(inspectArgs(args).replace(/\n$/, ""));
}

interface Css {
  backgroundColor: [number, number, number] | string | null;
  color: [number, number, number] | string | null;
  fontWeight: string | null;
  fontStyle: string | null;
  textDecorationColor: [number, number, number] | null;
  textDecorationLine: string[];
}

const DEFAULT_CSS: Css = {
  backgroundColor: null,
  color: null,
  fontWeight: null,
  fontStyle: null,
  textDecorationColor: null,
  textDecorationLine: [],
};

function parseCss(cssString: string): Css {
  return parseCss_(cssString);
}

function parseCssColor(colorString: string): [number, number, number] | null {
  return parseCssColor_(colorString);
}

/** ANSI-fy the CSS, replace "\x1b" with "_". */
function cssToAnsiEsc(css: Css, prevCss: Css | null = null): string {
  return cssToAnsi_(css, prevCss).replaceAll("\x1b", "_");
}

// test cases from web-platform-tests
// via https://github.com/web-platform-tests/wpt/blob/master/console/console-is-a-namespace.any.js
Deno.test(function consoleShouldBeANamespace() {
  const prototype1 = Object.getPrototypeOf(console);
  const prototype2 = Object.getPrototypeOf(prototype1);

  assertEquals(Object.getOwnPropertyNames(prototype1).length, 0);
  assertEquals(prototype2, Object.prototype);
});

Deno.test(function consoleHasRightInstance() {
  assert(console instanceof Console);
  assertEquals({} instanceof Console, false);
});

Deno.test(function consoleTestAssertShouldNotThrowError() {
  mockConsole((console) => {
    console.assert(true);
    let hasThrown = undefined;
    try {
      console.assert(false);
      hasThrown = false;
    } catch {
      hasThrown = true;
    }
    assertEquals(hasThrown, false);
  });
});

Deno.test(function consoleTestStringifyComplexObjects() {
  assertEquals(stringify("foo"), "foo");
  assertEquals(stringify(["foo", "bar"]), `[ "foo", "bar" ]`);
  assertEquals(stringify({ foo: "bar" }), `{ foo: "bar" }`);
});

Deno.test(
  function consoleTestStringifyComplexObjectsWithEscapedSequences() {
    assertEquals(
      stringify(
        ["foo\b", "foo\f", "foo\n", "foo\r", "foo\t", "foo\v", "foo\0"],
      ),
      `[
  "foo\\b",   "foo\\f",
  "foo\\n",   "foo\\r",
  "foo\\t",   "foo\\v",
  "foo\\x00"
]`,
    );
    assertEquals(
      stringify(
        [
          Symbol(),
          Symbol(""),
          Symbol("foo\b"),
          Symbol("foo\f"),
          Symbol("foo\n"),
          Symbol("foo\r"),
          Symbol("foo\t"),
          Symbol("foo\v"),
          Symbol("foo\0"),
        ],
      ),
      `[
  Symbol(),
  Symbol(""),
  Symbol("foo\\b"),
  Symbol("foo\\f"),
  Symbol("foo\\n"),
  Symbol("foo\\r"),
  Symbol("foo\\t"),
  Symbol("foo\\v"),
  Symbol("foo\\x00")
]`,
    );
    assertEquals(
      stringify(
        { "foo\b": "bar\n", "bar\r": "baz\t", "qux\0": "qux\0" },
      ),
      `{ "foo\\b": "bar\\n", "bar\\r": "baz\\t", "qux\\x00": "qux\\x00" }`,
    );
    assertEquals(
      stringify(
        {
          [Symbol("foo\b")]: `Symbol("foo\n")`,
          [Symbol("bar\n")]: `Symbol("bar\n")`,
          [Symbol("bar\r")]: `Symbol("bar\r")`,
          [Symbol("baz\t")]: `Symbol("baz\t")`,
          [Symbol("qux\0")]: `Symbol("qux\0")`,
        },
      ),
      `{
  [Symbol("foo\\b")]: 'Symbol("foo\\n\")',
  [Symbol("bar\\n")]: 'Symbol("bar\\n\")',
  [Symbol("bar\\r")]: 'Symbol("bar\\r\")',
  [Symbol("baz\\t")]: 'Symbol("baz\\t\")',
  [Symbol("qux\\x00")]: 'Symbol(\"qux\\x00")'
}`,
    );
    assertEquals(
      stringify(new Set(["foo\n", "foo\r", "foo\0"])),
      `Set { "foo\\n", "foo\\r", "foo\\x00" }`,
    );
  },
);

Deno.test(function consoleTestStringifyQuotes() {
  assertEquals(stringify(["\\"]), `[ "\\\\" ]`);
  assertEquals(stringify(['\\,"']), `[ '\\\\,"' ]`);
  assertEquals(stringify([`\\,",'`]), `[ \`\\\\,",'\` ]`);
  assertEquals(stringify(["\\,\",',`"]), `[ "\\\\,\\",',\`" ]`);
});

Deno.test(function consoleTestStringifyLongStrings() {
  const veryLongString = "a".repeat(200);
  // If we stringify an object containing the long string, it gets abbreviated.
  let actual = stringify({ veryLongString });
  assert(actual.includes("..."));
  assert(actual.length < 200);
  // However if we stringify the string itself, we get it exactly.
  actual = stringify(veryLongString);
  assertEquals(actual, veryLongString);
});

Deno.test(function consoleTestStringifyCircular() {
  class Base {
    a = 1;
    m1() {}
  }

  class Extended extends Base {
    b = 2;
    m2() {}
  }

  // deno-lint-ignore no-explicit-any
  const nestedObj: any = {
    num: 1,
    bool: true,
    str: "a",
    method() {},
    async asyncMethod() {},
    *generatorMethod() {},
    un: undefined,
    nu: null,
    arrowFunc: () => {},
    extendedClass: new Extended(),
    nFunc: new Function(),
    extendedCstr: Extended,
  };

  const circularObj = {
    num: 2,
    bool: false,
    str: "b",
    method() {},
    un: undefined,
    nu: null,
    nested: nestedObj,
    emptyObj: {},
    arr: [1, "s", false, null, nestedObj],
    baseClass: new Base(),
  };

  nestedObj.o = circularObj;
  const nestedObjExpected = `<ref *1> {
  num: 1,
  bool: true,
  str: "a",
  method: [Function: method],
  asyncMethod: [AsyncFunction: asyncMethod],
  generatorMethod: [GeneratorFunction: generatorMethod],
  un: undefined,
  nu: null,
  arrowFunc: [Function: arrowFunc],
  extendedClass: Extended { a: 1, b: 2 },
  nFunc: [Function],
  extendedCstr: [Function: Extended],
  o: {
    num: 2,
    bool: false,
    str: "b",
    method: [Function: method],
    un: undefined,
    nu: null,
    nested: [Circular *1],
    emptyObj: {},
    arr: [ 1, "s", false, null, [Circular *1] ],
    baseClass: Base { a: 1 }
  }
}`;

  assertEquals(stringify(1), "1");
  assertEquals(stringify(-0), "-0");
  assertEquals(stringify(1n), "1n");
  assertEquals(stringify("s"), "s");
  assertEquals(stringify(false), "false");
  assertEquals(stringify(new Number(1)), "[Number: 1]");
  assertEquals(stringify(new Number(-0)), "[Number: -0]");
  assertEquals(stringify(Object(1n)), "[BigInt: 1n]");
  assertEquals(stringify(new Boolean(true)), "[Boolean: true]");
  assertEquals(stringify(new String("deno")), `[String: "deno"]`);
  assertEquals(stringify(/[0-9]*/), "/[0-9]*/");
  assertEquals(
    stringify(new Date("2018-12-10T02:26:59.002Z")),
    "2018-12-10T02:26:59.002Z",
  );
  assertEquals(stringify(new Set([1, 2, 3])), "Set { 1, 2, 3 }");
  assertEquals(
    stringify(
      new Map([
        [1, "one"],
        [2, "two"],
      ]),
    ),
    `Map { 1 => "one", 2 => "two" }`,
  );
  assertEquals(stringify(new WeakSet()), "WeakSet { [items unknown] }");
  assertEquals(stringify(new WeakMap()), "WeakMap { [items unknown] }");
  assertEquals(stringify(Symbol(1)), `Symbol("1")`);
  assertEquals(stringify(Object(Symbol(1))), `[Symbol: Symbol("1")]`);
  assertEquals(stringify(null), "null");
  assertEquals(stringify(undefined), "undefined");
  assertEquals(stringify(new Extended()), "Extended { a: 1, b: 2 }");
  assertEquals(
    stringify(function f() {}),
    "[Function: f]",
  );
  assertEquals(
    stringify(async function af() {}),
    "[AsyncFunction: af]",
  );
  assertEquals(
    stringify(function* gf() {}),
    "[GeneratorFunction: gf]",
  );
  assertEquals(
    stringify(async function* agf() {}),
    "[AsyncGeneratorFunction: agf]",
  );
  assertEquals(
    stringify(new Uint8Array([1, 2, 3])),
    "Uint8Array(3) [ 1, 2, 3 ]",
  );
  assertEquals(stringify(Uint8Array.prototype), "Uint8Array {}");
  assertEquals(
    stringify({ a: { b: { c: { d: new Set([1]) } } } }),
    "{ a: { b: { c: { d: [Set] } } } }",
  );
  assertEquals(stringify(nestedObj), nestedObjExpected);
  assertEquals(
    stringify(JSON),
    "JSON {}",
  );
  assertEquals(
    stringify(new Console(() => {})),
    `console {
  log: [Function: log],
  debug: [Function: debug],
  info: [Function: info],
  dir: [Function: dir],
  dirxml: [Function: dir],
  warn: [Function: warn],
  error: [Function: error],
  assert: [Function: assert],
  count: [Function: count],
  countReset: [Function: countReset],
  table: [Function: table],
  time: [Function: time],
  timeLog: [Function: timeLog],
  timeEnd: [Function: timeEnd],
  group: [Function: group],
  groupCollapsed: [Function: group],
  groupEnd: [Function: groupEnd],
  clear: [Function: clear],
  trace: [Function: trace],
  profile: [Function: profile],
  profileEnd: [Function: profileEnd],
  timeStamp: [Function: timeStamp],
  indentLevel: 0,
  [Symbol(isConsoleInstance)]: true
}`,
  );
  assertEquals(
    stringify({ str: 1, [Symbol.for("sym")]: 2, [Symbol.toStringTag]: "TAG" }),
    'TAG { str: 1, [Symbol(sym)]: 2, [Symbol(Symbol.toStringTag)]: "TAG" }',
  );
  assertEquals(
    stringify({
      [Symbol.for("Deno.customInspect")]: function () {
        return Deno.inspect(this);
      },
    }),
    "[Circular *1]",
  );
  // test inspect is working the same
  assertEquals(stripColor(Deno.inspect(nestedObj)), nestedObjExpected);
});

Deno.test(function consoleTestStringifyMultipleCircular() {
  const y = { a: { b: {} }, foo: { bar: {} } };
  y.a.b = y.a;
  y.foo.bar = y.foo;
  console.log(y);
  assertEquals(
    stringify(y),
    "{ a: <ref *1> { b: [Circular *1] }, foo: <ref *2> { bar: [Circular *2] } }",
  );
});

Deno.test(function consoleTestStringifyFunctionWithPrototypeRemoved() {
  const f = function f() {};
  Reflect.setPrototypeOf(f, null);
  assertEquals(stringify(f), "[Function: f]");
  const af = async function af() {};
  Reflect.setPrototypeOf(af, null);
  assertEquals(stringify(af), "[Function: af]");
  const gf = function* gf() {};
  Reflect.setPrototypeOf(gf, null);
  assertEquals(stringify(gf), "[Function: gf]");
  const agf = async function* agf() {};
  Reflect.setPrototypeOf(agf, null);
  assertEquals(stringify(agf), "[Function: agf]");
});

Deno.test(function consoleTestStringifyFunctionWithProperties() {
  const f = () => "test";
  f.x = () => "foo";
  f.y = 3;
  f.z = () => "baz";
  f.b = function bar() {};
  f.a = new Map();
  assertEquals(
    stringify({ f }),
    `{
  f: [Function: f] { x: [Function], y: 3, z: [Function], b: [Function: bar], a: Map {} }
}`,
  );

  const t = () => {};
  t.x = f;
  f.s = f;
  f.t = t;
  assertEquals(
    stringify({ f }),
    `{
  f: <ref *1> [Function: f] {
    x: [Function],
    y: 3,
    z: [Function],
    b: [Function: bar],
    a: Map {},
    s: [Circular *1],
    t: [Function: t] { x: [Circular *1] }
  }
}`,
  );

  assertEquals(
    stringify(Array),
    `[Function: Array]`,
  );

  assertEquals(
    stripColor(Deno.inspect(Array, { showHidden: true })),
    `[Function: Array] { [Symbol(Symbol.species)]: [Getter] }`,
  );
});

Deno.test(function consoleTestStringifyWithDepth() {
  // deno-lint-ignore no-explicit-any
  const nestedObj: any = { a: { b: { c: { d: { e: { f: 42 } } } } } };
  assertEquals(
    stripColor(inspectArgs([nestedObj], { depth: 3 })),
    "{ a: { b: { c: [Object] } } }",
  );
  assertEquals(
    stripColor(inspectArgs([nestedObj], { depth: 4 })),
    "{ a: { b: { c: { d: [Object] } } } }",
  );
  assertEquals(stripColor(inspectArgs([nestedObj], { depth: 0 })), "[Object]");
  assertEquals(
    stripColor(inspectArgs([nestedObj])),
    "{ a: { b: { c: { d: [Object] } } } }",
  );
  // test inspect is working the same way
  assertEquals(
    stripColor(Deno.inspect(nestedObj, { depth: 4 })),
    "{ a: { b: { c: { d: [Object] } } } }",
  );
});

Deno.test(function consoleTestStringifyLargeObject() {
  const obj = {
    a: 2,
    o: {
      a: "1",
      b: "2",
      c: "3",
      d: "4",
      e: "5",
      f: "6",
      g: 10,
      asd: 2,
      asda: 3,
      x: { a: "asd", x: 3 },
    },
  };
  assertEquals(
    stringify(obj),
    `{
  a: 2,
  o: {
    a: "1",
    b: "2",
    c: "3",
    d: "4",
    e: "5",
    f: "6",
    g: 10,
    asd: 2,
    asda: 3,
    x: { a: "asd", x: 3 }
  }
}`,
  );
});

Deno.test(function consoleTestStringifyIterable() {
  const shortArray = [1, 2, 3, 4, 5];
  assertEquals(stringify(shortArray), "[ 1, 2, 3, 4, 5 ]");

  const longArray = new Array(200).fill(0);
  assertEquals(
    stringify(longArray),
    `[
  0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
  0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
  0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
  0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
  0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
  0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
  0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
  ... 100 more items
]`,
  );

  const obj = { a: "a", longArray };
  assertEquals(
    stringify(obj),
    `{
  a: "a",
  longArray: [
    0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
    0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
    0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
    0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
    0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
    0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
    0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
    ... 100 more items
  ]
}`,
  );

  const shortMap = new Map([
    ["a", 0],
    ["b", 1],
  ]);
  assertEquals(stringify(shortMap), `Map { "a" => 0, "b" => 1 }`);

  const longMap = new Map();
  for (const key of Array(200).keys()) {
    longMap.set(`${key}`, key);
  }
  assertEquals(
    stringify(longMap),
    `Map {
  "0" => 0,
  "1" => 1,
  "2" => 2,
  "3" => 3,
  "4" => 4,
  "5" => 5,
  "6" => 6,
  "7" => 7,
  "8" => 8,
  "9" => 9,
  "10" => 10,
  "11" => 11,
  "12" => 12,
  "13" => 13,
  "14" => 14,
  "15" => 15,
  "16" => 16,
  "17" => 17,
  "18" => 18,
  "19" => 19,
  "20" => 20,
  "21" => 21,
  "22" => 22,
  "23" => 23,
  "24" => 24,
  "25" => 25,
  "26" => 26,
  "27" => 27,
  "28" => 28,
  "29" => 29,
  "30" => 30,
  "31" => 31,
  "32" => 32,
  "33" => 33,
  "34" => 34,
  "35" => 35,
  "36" => 36,
  "37" => 37,
  "38" => 38,
  "39" => 39,
  "40" => 40,
  "41" => 41,
  "42" => 42,
  "43" => 43,
  "44" => 44,
  "45" => 45,
  "46" => 46,
  "47" => 47,
  "48" => 48,
  "49" => 49,
  "50" => 50,
  "51" => 51,
  "52" => 52,
  "53" => 53,
  "54" => 54,
  "55" => 55,
  "56" => 56,
  "57" => 57,
  "58" => 58,
  "59" => 59,
  "60" => 60,
  "61" => 61,
  "62" => 62,
  "63" => 63,
  "64" => 64,
  "65" => 65,
  "66" => 66,
  "67" => 67,
  "68" => 68,
  "69" => 69,
  "70" => 70,
  "71" => 71,
  "72" => 72,
  "73" => 73,
  "74" => 74,
  "75" => 75,
  "76" => 76,
  "77" => 77,
  "78" => 78,
  "79" => 79,
  "80" => 80,
  "81" => 81,
  "82" => 82,
  "83" => 83,
  "84" => 84,
  "85" => 85,
  "86" => 86,
  "87" => 87,
  "88" => 88,
  "89" => 89,
  "90" => 90,
  "91" => 91,
  "92" => 92,
  "93" => 93,
  "94" => 94,
  "95" => 95,
  "96" => 96,
  "97" => 97,
  "98" => 98,
  "99" => 99,
  ... 100 more items
}`,
  );

  const shortSet = new Set([1, 2, 3]);
  assertEquals(stringify(shortSet), `Set { 1, 2, 3 }`);
  const longSet = new Set();
  for (const key of Array(200).keys()) {
    longSet.add(key);
  }
  assertEquals(
    stringify(longSet),
    `Set {
  0,
  1,
  2,
  3,
  4,
  5,
  6,
  7,
  8,
  9,
  10,
  11,
  12,
  13,
  14,
  15,
  16,
  17,
  18,
  19,
  20,
  21,
  22,
  23,
  24,
  25,
  26,
  27,
  28,
  29,
  30,
  31,
  32,
  33,
  34,
  35,
  36,
  37,
  38,
  39,
  40,
  41,
  42,
  43,
  44,
  45,
  46,
  47,
  48,
  49,
  50,
  51,
  52,
  53,
  54,
  55,
  56,
  57,
  58,
  59,
  60,
  61,
  62,
  63,
  64,
  65,
  66,
  67,
  68,
  69,
  70,
  71,
  72,
  73,
  74,
  75,
  76,
  77,
  78,
  79,
  80,
  81,
  82,
  83,
  84,
  85,
  86,
  87,
  88,
  89,
  90,
  91,
  92,
  93,
  94,
  95,
  96,
  97,
  98,
  99,
  ... 100 more items
}`,
  );

  const withEmptyEl = Array(10);
  withEmptyEl.fill(0, 4, 6);
  assertEquals(
    stringify(withEmptyEl),
    `[ <4 empty items>, 0, 0, <4 empty items> ]`,
  );

  const emptyArray = Array(5000);
  assertEquals(
    stringify(emptyArray),
    `[ <5000 empty items> ]`,
  );

  assertEquals(
    stringify(Array(1)),
    `[ <1 empty item> ]`,
  );

  assertEquals(
    stringify([, , 1]),
    `[ <2 empty items>, 1 ]`,
  );

  assertEquals(
    stringify([1, , , 1]),
    `[ 1, <2 empty items>, 1 ]`,
  );

  const withEmptyElAndMoreItems = Array(500);
  withEmptyElAndMoreItems.fill(0, 50, 80);
  withEmptyElAndMoreItems.fill(2, 100, 120);
  withEmptyElAndMoreItems.fill(3, 140, 160);
  withEmptyElAndMoreItems.fill(4, 180);
  assertEquals(
    stringify(withEmptyElAndMoreItems),
    `[
  <50 empty items>, 0,                0, 0,
  0,                0,                0, 0,
  0,                0,                0, 0,
  0,                0,                0, 0,
  0,                0,                0, 0,
  0,                0,                0, 0,
  0,                0,                0, 0,
  0,                0,                0, <20 empty items>,
  2,                2,                2, 2,
  2,                2,                2, 2,
  2,                2,                2, 2,
  2,                2,                2, 2,
  2,                2,                2, 2,
  <20 empty items>, 3,                3, 3,
  3,                3,                3, 3,
  3,                3,                3, 3,
  3,                3,                3, 3,
  3,                3,                3, 3,
  3,                <20 empty items>, 4, 4,
  4,                4,                4, 4,
  4,                4,                4, 4,
  4,                4,                4, 4,
  4,                4,                4, 4,
  4,                4,                4, 4,
  4,                4,                4, 4,
  ... 294 more items
]`,
  );

  const lWithEmptyEl = Array(200);
  lWithEmptyEl.fill(0, 50, 80);
  assertEquals(
    stringify(lWithEmptyEl),
    `[
  <50 empty items>, 0,                 0,
  0,                0,                 0,
  0,                0,                 0,
  0,                0,                 0,
  0,                0,                 0,
  0,                0,                 0,
  0,                0,                 0,
  0,                0,                 0,
  0,                0,                 0,
  0,                0,                 0,
  0,                <120 empty items>
]`,
  );
});

Deno.test(function consoleTestStringifyIterableWhenGrouped() {
  const withOddNumberOfEls = new Float64Array(
    [
      2.1,
      2.01,
      2.001,
      2.0001,
      2.00001,
      2.000001,
      2.0000001,
      2.00000001,
      2.000000001,
      2.0000000001,
      2,
    ],
  );
  assertEquals(
    stringify(withOddNumberOfEls),
    `Float64Array(11) [
          2.1,         2.01,
        2.001,       2.0001,
      2.00001,     2.000001,
    2.0000001,   2.00000001,
  2.000000001, 2.0000000001,
            2
]`,
  );
  const withEvenNumberOfEls = new Float64Array(
    [
      2.1,
      2.01,
      2.001,
      2.0001,
      2.00001,
      2.000001,
      2.0000001,
      2.00000001,
      2.000000001,
      2.0000000001,
      2,
      2,
    ],
  );
  assertEquals(
    stringify(withEvenNumberOfEls),
    `Float64Array(12) [
          2.1,         2.01,
        2.001,       2.0001,
      2.00001,     2.000001,
    2.0000001,   2.00000001,
  2.000000001, 2.0000000001,
            2,            2
]`,
  );
  const withThreeColumns = [
    2,
    2.1,
    2.11,
    2,
    2.111,
    2.1111,
    2,
    2.1,
    2.11,
    2,
    2.1,
  ];
  assertEquals(
    stringify(withThreeColumns),
    `[
  2,   2.1,   2.11,
  2, 2.111, 2.1111,
  2,   2.1,   2.11,
  2,   2.1
]`,
  );
});

Deno.test(async function consoleTestStringifyPromises() {
  const pendingPromise = new Promise((_res, _rej) => {});
  assertEquals(stringify(pendingPromise), "Promise { <pending> }");

  const resolvedPromise = new Promise((res, _rej) => {
    res("Resolved!");
  });
  assertEquals(stringify(resolvedPromise), `Promise { "Resolved!" }`);

  let rejectedPromise;
  try {
    rejectedPromise = new Promise((_, rej) => {
      rej(Error("Whoops"));
    });
    await rejectedPromise;
  } catch (_err) {
    // pass
  }
  const strLines = stringify(rejectedPromise).split("\n");
  assertEquals(strLines[0], "Promise {");
  assertEquals(strLines[1], "  <rejected> Error: Whoops");
});

Deno.test(function consoleTestWithCustomInspector() {
  class A {
    [customInspect](
      inspect: unknown,
      options: Deno.InspectOptions,
    ): string {
      assertEquals(typeof inspect, "function");
      assertEquals(typeof options, "object");
      return "b";
    }
  }

  assertEquals(stringify(new A()), "b");
});

Deno.test(function consoleTestWithCustomInspectorUsingInspectFunc() {
  class A {
    [customInspect](
      inspect: (v: unknown, opts?: Deno.InspectOptions) => string,
    ): string {
      return "b " + inspect({ c: 1 });
    }
  }

  assertEquals(stringify(new A()), "b { c: 1 }");
});

Deno.test(function consoleTestWithCustomInspectorError() {
  class A {
    [customInspect](): never {
      throw new Error("BOOM");
    }
  }

  const a = new A();
  assertThrows(
    () => stringify(a),
    Error,
    "BOOM",
    "Custom inspect won't attempt to parse if user defined function throws",
  );
  assertThrows(
    () => stringify(a),
    Error,
    "BOOM",
    "Inpsect should fail and maintain a clear CTX_STACK",
  );
});

Deno.test(function consoleTestWithCustomInspectFunction() {
  function a() {}
  Object.assign(a, {
    [customInspect]() {
      return "b";
    },
  });

  assertEquals(stringify(a), "b");
});

Deno.test(function consoleTestWithIntegerFormatSpecifier() {
  assertEquals(stringify("%i"), "%i");
  assertEquals(stringify("%i", 42.0), "42");
  assertEquals(stringify("%i", 42), "42");
  assertEquals(stringify("%i", "42"), "NaN");
  assertEquals(stringify("%i", 1.5), "1");
  assertEquals(stringify("%i", -0.5), "0");
  assertEquals(stringify("%i", ""), "NaN");
  assertEquals(stringify("%i", Symbol()), "NaN");
  assertEquals(stringify("%i %d", 42, 43), "42 43");
  assertEquals(stringify("%d %i", 42), "42 %i");
  assertEquals(stringify("%d", 12345678901234567890123), "1");
  assertEquals(
    stringify("%i", 12345678901234567890123n),
    "12345678901234567890123n",
  );
});

Deno.test(function consoleTestWithFloatFormatSpecifier() {
  assertEquals(stringify("%f"), "%f");
  assertEquals(stringify("%f", 42.0), "42");
  assertEquals(stringify("%f", 42), "42");
  assertEquals(stringify("%f", "42"), "NaN");
  assertEquals(stringify("%f", 1.5), "1.5");
  assertEquals(stringify("%f", -0.5), "-0.5");
  assertEquals(stringify("%f", Math.PI), "3.141592653589793");
  assertEquals(stringify("%f", ""), "NaN");
  assertEquals(stringify("%f", Symbol("foo")), "NaN");
  assertEquals(stringify("%f", 5n), "NaN");
  assertEquals(stringify("%f %f", 42, 43), "42 43");
  assertEquals(stringify("%f %f", 42), "42 %f");
});

Deno.test(function consoleTestWithStringFormatSpecifier() {
  assertEquals(stringify("%s"), "%s");
  assertEquals(stringify("%s", undefined), "undefined");
  assertEquals(stringify("%s", "foo"), "foo");
  assertEquals(stringify("%s", 42), "42");
  assertEquals(stringify("%s", "42"), "42");
  assertEquals(stringify("%s %s", 42, 43), "42 43");
  assertEquals(stringify("%s %s", 42), "42 %s");
  assertEquals(stringify("%s", Symbol("foo")), "Symbol(foo)");
});

Deno.test(function consoleTestWithObjectFormatSpecifier() {
  assertEquals(stringify("%o"), "%o");
  assertEquals(stringify("%o", 42), "42");
  assertEquals(stringify("%o", "foo"), `"foo"`);
  assertEquals(stringify("o: %o, a: %O", {}, []), "o: {}, a: []");
  assertEquals(stringify("%o", { a: 42 }), "{ a: 42 }");
  assertEquals(
    stringify("%o", { a: { b: { c: { d: new Set([1]) } } } }),
    "{ a: { b: { c: { d: [Set] } } } }",
  );
});

Deno.test(function consoleTestWithStyleSpecifier() {
  assertEquals(stringify("%cfoo%cbar"), "%cfoo%cbar");
  assertEquals(stringify("%cfoo%cbar", ""), "foo%cbar");
  assertEquals(stripColor(stringify("%cfoo%cbar", "", "color: red")), "foobar");
});

Deno.test(function consoleParseCssColor() {
  assertEquals(parseCssColor("inherit"), null);
  assertEquals(parseCssColor("black"), [0, 0, 0]);
  assertEquals(parseCssColor("darkmagenta"), [139, 0, 139]);
  assertEquals(parseCssColor("slateblue"), [106, 90, 205]);
  assertEquals(parseCssColor("#ffaa00"), [255, 170, 0]);
  assertEquals(parseCssColor("#ffaa00"), [255, 170, 0]);
  assertEquals(parseCssColor("#18d"), [16, 128, 208]);
  assertEquals(parseCssColor("#18D"), [16, 128, 208]);
  assertEquals(parseCssColor("rgb(100, 200, 50)"), [100, 200, 50]);
  assertEquals(parseCssColor("rgb(+100.3, -200, .5)"), [100, 0, 1]);
  assertEquals(parseCssColor("hsl(75, 60%, 40%)"), [133, 163, 41]);

  assertEquals(parseCssColor("rgb(100,200,50)"), [100, 200, 50]);
  assertEquals(
    parseCssColor("rgb( \t\n100 \t\n, \t\n200 \t\n, \t\n50 \t\n)"),
    [100, 200, 50],
  );
});

Deno.test(function consoleParseCss() {
  assertEquals(
    parseCss("background-color: inherit"),
    { ...DEFAULT_CSS, backgroundColor: "inherit" },
  );
  assertEquals(
    parseCss("color: inherit"),
    { ...DEFAULT_CSS, color: "inherit" },
  );
  assertEquals(
    parseCss("background-color: red"),
    { ...DEFAULT_CSS, backgroundColor: "red" },
  );
  assertEquals(parseCss("color: blue"), { ...DEFAULT_CSS, color: "blue" });
  assertEquals(
    parseCss("font-weight: bold"),
    { ...DEFAULT_CSS, fontWeight: "bold" },
  );
  assertEquals(
    parseCss("font-style: italic"),
    { ...DEFAULT_CSS, fontStyle: "italic" },
  );
  assertEquals(
    parseCss("font-style: oblique"),
    { ...DEFAULT_CSS, fontStyle: "italic" },
  );
  assertEquals(
    parseCss("text-decoration-color: green"),
    { ...DEFAULT_CSS, textDecorationColor: [0, 128, 0] },
  );
  assertEquals(
    parseCss("text-decoration-line: underline overline line-through"),
    {
      ...DEFAULT_CSS,
      textDecorationLine: ["underline", "overline", "line-through"],
    },
  );
  assertEquals(
    parseCss("text-decoration: yellow underline"),
    {
      ...DEFAULT_CSS,
      textDecorationColor: [255, 255, 0],
      textDecorationLine: ["underline"],
    },
  );

  assertEquals(
    parseCss("color:red;font-weight:bold;"),
    { ...DEFAULT_CSS, color: "red", fontWeight: "bold" },
  );
  assertEquals(
    parseCss(
      " \t\ncolor \t\n: \t\nred \t\n; \t\nfont-weight \t\n: \t\nbold \t\n; \t\n",
    ),
    { ...DEFAULT_CSS, color: "red", fontWeight: "bold" },
  );
  assertEquals(
    parseCss("color: red; font-weight: bold, font-style: italic"),
    { ...DEFAULT_CSS, color: "red" },
  );
});

Deno.test(function consoleCssToAnsi() {
  assertEquals(
    cssToAnsiEsc({ ...DEFAULT_CSS, backgroundColor: "inherit" }),
    "_[49m",
  );
  assertEquals(
    cssToAnsiEsc({ ...DEFAULT_CSS, backgroundColor: "foo" }),
    "_[49m",
  );
  assertEquals(
    cssToAnsiEsc({ ...DEFAULT_CSS, backgroundColor: "black" }),
    "_[40m",
  );
  assertEquals(
    cssToAnsiEsc({ ...DEFAULT_CSS, color: "inherit" }),
    "_[39m",
  );
  assertEquals(
    cssToAnsiEsc({ ...DEFAULT_CSS, color: "blue" }),
    "_[34m",
  );
  assertEquals(
    cssToAnsiEsc({ ...DEFAULT_CSS, backgroundColor: [200, 201, 202] }),
    "_[48;2;200;201;202m",
  );
  assertEquals(
    cssToAnsiEsc({ ...DEFAULT_CSS, color: [203, 204, 205] }),
    "_[38;2;203;204;205m",
  );
  assertEquals(cssToAnsiEsc({ ...DEFAULT_CSS, fontWeight: "bold" }), "_[1m");
  assertEquals(cssToAnsiEsc({ ...DEFAULT_CSS, fontStyle: "italic" }), "_[3m");
  assertEquals(
    cssToAnsiEsc({ ...DEFAULT_CSS, textDecorationColor: [206, 207, 208] }),
    "_[58;2;206;207;208m",
  );
  assertEquals(
    cssToAnsiEsc({ ...DEFAULT_CSS, textDecorationLine: ["underline"] }),
    "_[4m",
  );
  assertEquals(
    cssToAnsiEsc(
      { ...DEFAULT_CSS, textDecorationLine: ["overline", "line-through"] },
    ),
    "_[9m_[53m",
  );
  assertEquals(
    cssToAnsiEsc(
      { ...DEFAULT_CSS, color: [203, 204, 205], fontWeight: "bold" },
    ),
    "_[38;2;203;204;205m_[1m",
  );
  assertEquals(
    cssToAnsiEsc(
      { ...DEFAULT_CSS, color: [0, 0, 0], fontWeight: "bold" },
      { ...DEFAULT_CSS, color: [203, 204, 205], fontStyle: "italic" },
    ),
    "_[38;2;0;0;0m_[1m_[23m",
  );
});

Deno.test(function consoleTestWithVariousOrInvalidFormatSpecifier() {
  assertEquals(stringify("%s:%s"), "%s:%s");
  assertEquals(stringify("%i:%i"), "%i:%i");
  assertEquals(stringify("%d:%d"), "%d:%d");
  assertEquals(stringify("%%s%s", "foo"), "%sfoo");
  assertEquals(stringify("%s:%s", undefined), "undefined:%s");
  assertEquals(stringify("%s:%s", "foo", "bar"), "foo:bar");
  assertEquals(stringify("%s:%s", "foo", "bar", "baz"), "foo:bar baz");
  assertEquals(stringify("%%%s%%", "hi"), "%hi%");
  assertEquals(stringify("%d:%d", 12), "12:%d");
  assertEquals(stringify("%i:%i", 12), "12:%i");
  assertEquals(stringify("%f:%f", 12), "12:%f");
  assertEquals(stringify("o: %o, a: %o", {}), "o: {}, a: %o");
  assertEquals(stringify("abc%", 1), "abc% 1");
});

Deno.test(function consoleTestCallToStringOnLabel() {
  const methods = ["count", "countReset", "time", "timeLog", "timeEnd"];
  mockConsole((console) => {
    for (const method of methods) {
      let hasCalled = false;
      console[method]({
        toString() {
          hasCalled = true;
        },
      });
      assertEquals(hasCalled, true);
    }
  });
});

Deno.test(function consoleTestError() {
  class MyError extends Error {
    constructor(errStr: string) {
      super(errStr);
      this.name = "MyError";
    }
  }
  try {
    throw new MyError("This is an error");
  } catch (e) {
    assert(
      stringify(e)
        .split("\n")[0] // error has been caught
        .includes("MyError: This is an error"),
    );
  }
});

Deno.test(function consoleTestClear() {
  mockConsole((console, out) => {
    console.clear();
    assertEquals(out.toString(), "\x1b[1;1H" + "\x1b[0J");
  });
});

// Test bound this issue
Deno.test(function consoleDetachedLog() {
  mockConsole((console) => {
    const log = console.log;
    const dir = console.dir;
    const dirxml = console.dirxml;
    const debug = console.debug;
    const info = console.info;
    const warn = console.warn;
    const error = console.error;
    const consoleAssert = console.assert;
    const consoleCount = console.count;
    const consoleCountReset = console.countReset;
    const consoleTable = console.table;
    const consoleTime = console.time;
    const consoleTimeLog = console.timeLog;
    const consoleTimeEnd = console.timeEnd;
    const consoleGroup = console.group;
    const consoleGroupEnd = console.groupEnd;
    const consoleClear = console.clear;
    log("Hello world");
    dir("Hello world");
    dirxml("Hello world");
    debug("Hello world");
    info("Hello world");
    warn("Hello world");
    error("Hello world");
    consoleAssert(true);
    consoleCount("Hello world");
    consoleCountReset("Hello world");
    consoleTable({ test: "Hello world" });
    consoleTime("Hello world");
    consoleTimeLog("Hello world");
    consoleTimeEnd("Hello world");
    consoleGroup("Hello world");
    consoleGroupEnd();
    consoleClear();
  });
});

class StringBuffer {
  chunks: string[] = [];
  add(x: string) {
    this.chunks.push(x);
  }
  toString(): string {
    return this.chunks.join("");
  }
}

type ConsoleExamineFunc = (
  // deno-lint-ignore no-explicit-any
  csl: any,
  out: StringBuffer,
  err?: StringBuffer,
  both?: StringBuffer,
) => void;

function mockConsole(f: ConsoleExamineFunc) {
  const out = new StringBuffer();
  const err = new StringBuffer();
  const both = new StringBuffer();
  const csl = new Console(
    (x: string, level: number, printsNewLine: boolean) => {
      const content = x + (printsNewLine ? "\n" : "");
      const buf = level > 1 ? err : out;
      buf.add(content);
      both.add(content);
    },
  );
  f(csl, out, err, both);
}

// console.group test
Deno.test(function consoleGroup() {
  mockConsole((console, out) => {
    console.group("1");
    console.log("2");
    console.group("3");
    console.log("4");
    console.groupEnd();
    console.groupEnd();
    console.log("5");
    console.log("6");

    assertEquals(
      out.toString(),
      `1
    2
    3
        4
5
6
`,
    );
  });
});

// console.group with console.warn test
Deno.test(function consoleGroupWarn() {
  mockConsole((console, _out, _err, both) => {
    assert(both);
    console.warn("1");
    console.group();
    console.warn("2");
    console.group();
    console.warn("3");
    console.groupEnd();
    console.warn("4");
    console.groupEnd();
    console.warn("5");

    console.warn("6");
    console.warn("7");
    assertEquals(
      both.toString(),
      `1
    2
        3
    4
5
6
7
`,
    );
  });
});

// console.table test
Deno.test(function consoleTable() {
  mockConsole((console, out) => {
    console.table({ a: "test", b: 1 });
    assertEquals(
      stripColor(out.toString()),
      `┌───────┬────────┐
│ (idx) │ Values │
├───────┼────────┤
│ a     │ "test" │
│ b     │ 1      │
└───────┴────────┘
`,
    );
  });
  mockConsole((console, out) => {
    console.table({ a: { b: 10 }, b: { b: 20, c: 30 } }, ["c"]);
    assertEquals(
      stripColor(out.toString()),
      `┌───────┬────┐
│ (idx) │ c  │
├───────┼────┤
│ a     │    │
│ b     │ 30 │
└───────┴────┘
`,
    );
  });
  mockConsole((console, out) => {
    console.table([1, 2, [3, [4]], [5, 6], [[7], [8]]]);
    assertEquals(
      stripColor(out.toString()),
      `┌───────┬───────┬───────┬────────┐
│ (idx) │ 0     │ 1     │ Values │
├───────┼───────┼───────┼────────┤
│     0 │       │       │      1 │
│     1 │       │       │      2 │
│     2 │ 3     │ [ 4 ] │        │
│     3 │ 5     │ 6     │        │
│     4 │ [ 7 ] │ [ 8 ] │        │
└───────┴───────┴───────┴────────┘
`,
    );
  });
  mockConsole((console, out) => {
    console.table(new Set([1, 2, 3, "test"]));
    assertEquals(
      stripColor(out.toString()),
      `┌────────────┬────────┐
│ (iter idx) │ Values │
├────────────┼────────┤
│          0 │ 1      │
│          1 │ 2      │
│          2 │ 3      │
│          3 │ "test" │
└────────────┴────────┘
`,
    );
  });
  mockConsole((console, out) => {
    console.table(
      new Map([
        [1, "one"],
        [2, "two"],
      ]),
    );
    assertEquals(
      stripColor(out.toString()),
      `┌────────────┬─────┬────────┐
│ (iter idx) │ Key │ Values │
├────────────┼─────┼────────┤
│          0 │   1 │ "one"  │
│          1 │   2 │ "two"  │
└────────────┴─────┴────────┘
`,
    );
  });
  mockConsole((console, out) => {
    console.table({
      a: true,
      b: { c: { d: 10 }, e: [1, 2, [5, 6]] },
      f: "test",
      g: new Set([1, 2, 3, "test"]),
      h: new Map([[1, "one"]]),
    });
    assertEquals(
      stripColor(out.toString()),
      `┌───────┬───────────┬───────────────────┬────────┐
│ (idx) │ c         │ e                 │ Values │
├───────┼───────────┼───────────────────┼────────┤
│ a     │           │                   │ true   │
│ b     │ { d: 10 } │ [ 1, 2, [Array] ] │        │
│ f     │           │                   │ "test" │
│ g     │           │                   │        │
│ h     │           │                   │        │
└───────┴───────────┴───────────────────┴────────┘
`,
    );
  });
  mockConsole((console, out) => {
    console.table([
      1,
      "test",
      false,
      { a: 10 },
      ["test", { b: 20, c: "test" }],
    ]);
    assertEquals(
      stripColor(out.toString()),
      `┌───────┬────────┬──────────────────────┬────┬────────┐
│ (idx) │ 0      │ 1                    │ a  │ Values │
├───────┼────────┼──────────────────────┼────┼────────┤
│     0 │        │                      │    │ 1      │
│     1 │        │                      │    │ "test" │
│     2 │        │                      │    │ false  │
│     3 │        │                      │ 10 │        │
│     4 │ "test" │ { b: 20, c: "test" } │    │        │
└───────┴────────┴──────────────────────┴────┴────────┘
`,
    );
  });
  mockConsole((console, out) => {
    console.table([]);
    assertEquals(
      stripColor(out.toString()),
      `┌───────┐
│ (idx) │
├───────┤
└───────┘
`,
    );
  });
  mockConsole((console, out) => {
    console.table({});
    assertEquals(
      stripColor(out.toString()),
      `┌───────┐
│ (idx) │
├───────┤
└───────┘
`,
    );
  });
  mockConsole((console, out) => {
    console.table(new Set());
    assertEquals(
      stripColor(out.toString()),
      `┌────────────┐
│ (iter idx) │
├────────────┤
└────────────┘
`,
    );
  });
  mockConsole((console, out) => {
    console.table(new Map());
    assertEquals(
      stripColor(out.toString()),
      `┌────────────┐
│ (iter idx) │
├────────────┤
└────────────┘
`,
    );
  });
  mockConsole((console, out) => {
    console.table("test");
    assertEquals(out.toString(), "test\n");
  });
  mockConsole((console, out) => {
    console.table(["Hello", "你好", "Amapá"]);
    assertEquals(
      stripColor(out.toString()),
      `┌───────┬─────────┐
│ (idx) │ Values  │
├───────┼─────────┤
│     0 │ "Hello" │
│     1 │ "你好"  │
│     2 │ "Amapá" │
└───────┴─────────┘
`,
    );
  });
  mockConsole((console, out) => {
    console.table([
      [1, 2],
      [3, 4],
    ]);
    assertEquals(
      stripColor(out.toString()),
      `┌───────┬───┬───┐
│ (idx) │ 0 │ 1 │
├───────┼───┼───┤
│     0 │ 1 │ 2 │
│     1 │ 3 │ 4 │
└───────┴───┴───┘
`,
    );
  });
  mockConsole((console, out) => {
    console.table({ 1: { a: 4, b: 5 }, 2: null, 3: { b: 6, c: 7 } }, ["b"]);
    assertEquals(
      stripColor(out.toString()),
      `┌───────┬───┐
│ (idx) │ b │
├───────┼───┤
│     1 │ 5 │
│     2 │   │
│     3 │ 6 │
└───────┴───┘
`,
    );
  });
  mockConsole((console, out) => {
    console.table([{ a: 0 }, { a: 1, b: 1 }, { a: 2 }, { a: 3, b: 3 }]);
    assertEquals(
      stripColor(out.toString()),
      `┌───────┬───┬───┐
│ (idx) │ a │ b │
├───────┼───┼───┤
│     0 │ 0 │   │
│     1 │ 1 │ 1 │
│     2 │ 2 │   │
│     3 │ 3 │ 3 │
└───────┴───┴───┘
`,
    );
  });
  mockConsole((console, out) => {
    console.table(
      [{ a: 0 }, { a: 1, c: 1 }, { a: 2 }, { a: 3, c: 3 }],
      ["a", "b", "c"],
    );
    assertEquals(
      stripColor(out.toString()),
      `┌───────┬───┬───┬───┐
│ (idx) │ a │ b │ c │
├───────┼───┼───┼───┤
│     0 │ 0 │   │   │
│     1 │ 1 │   │ 1 │
│     2 │ 2 │   │   │
│     3 │ 3 │   │ 3 │
└───────┴───┴───┴───┘
`,
    );
  });
});

// console.log(Error) test
Deno.test(function consoleLogShouldNotThrowError() {
  mockConsole((console) => {
    let result = 0;
    try {
      console.log(new Error("foo"));
      result = 1;
    } catch (_e) {
      result = 2;
    }
    assertEquals(result, 1);
  });

  // output errors to the console should not include "Uncaught"
  mockConsole((console, out) => {
    console.log(new Error("foo"));
    assertEquals(out.toString().includes("Uncaught"), false);
  });
});

Deno.test(function consoleLogShouldNotThrowErrorWhenInvalidCssColorsAreGiven() {
  mockConsole((console, out) => {
    console.log("%cfoo", "color: foo; background-color: bar;");
    assertEquals(stripColor(out.toString()), "foo\n");
  });
});

// console.log(Invalid Date) test
Deno.test(function consoleLogShoultNotThrowErrorWhenInvalidDateIsPassed() {
  mockConsole((console, out) => {
    const invalidDate = new Date("test");
    console.log(invalidDate);
    assertEquals(stripColor(out.toString()), "Invalid Date\n");
  });
});

// console.log(new Proxy(new Set(), {}))
Deno.test(function consoleLogShouldNotThrowErrorWhenInputIsProxiedSet() {
  mockConsole((console, out) => {
    const proxiedSet = new Proxy(new Set(), {});
    console.log(proxiedSet);
    assertEquals(stripColor(out.toString()), "Set {}\n");
  });
});

// console.log(new Proxy(new Map(), {}))
Deno.test(function consoleLogShouldNotThrowErrorWhenInputIsProxiedMap() {
  mockConsole((console, out) => {
    const proxiedMap = new Proxy(new Map(), {});
    console.log(proxiedMap);
    assertEquals(stripColor(out.toString()), "Map {}\n");
  });
});

// console.log(new Proxy(new Date(), {}))
Deno.test(function consoleLogShouldNotThrowErrorWhenInputIsProxiedDate() {
  mockConsole((console, out) => {
    const proxiedDate = new Proxy(new Date("2022-09-24T15:59:39.529Z"), {});
    console.log(proxiedDate);
    assertEquals(stripColor(out.toString()), "2022-09-24T15:59:39.529Z\n");
  });
});

// console.dir test
Deno.test(function consoleDir() {
  mockConsole((console, out) => {
    console.dir("DIR");
    assertEquals(out.toString(), "DIR\n");
  });
  mockConsole((console, out) => {
    console.dir("DIR", { indentLevel: 2 });
    assertEquals(out.toString(), "    DIR\n");
  });
});

// console.dir test
Deno.test(function consoleDirXml() {
  mockConsole((console, out) => {
    console.dirxml("DIRXML");
    assertEquals(out.toString(), "DIRXML\n");
  });
  mockConsole((console, out) => {
    console.dirxml("DIRXML", { indentLevel: 2 });
    assertEquals(out.toString(), "    DIRXML\n");
  });
});

// console.trace test
Deno.test(function consoleTrace() {
  mockConsole((console, _out, err) => {
    console.trace("%s", "custom message");
    assert(err);
    assert(err.toString().includes("Trace: custom message"));
  });
});

Deno.test(function inspectString() {
  assertEquals(
    stripColor(Deno.inspect("\0")),
    `"\\x00"`,
  );
  assertEquals(
    stripColor(Deno.inspect("\x1b[2J")),
    `"\\x1b[2J"`,
  );
});

Deno.test(function inspectGetters() {
  assertEquals(
    stripColor(Deno.inspect({
      get foo() {
        return 0;
      },
    })),
    "{ foo: [Getter] }",
  );

  assertEquals(
    stripColor(Deno.inspect({
      get foo() {
        return 0;
      },
    }, { getters: true })),
    "{ foo: 0 }",
  );

  assertEquals(
    Deno.inspect({
      get foo() {
        throw new Error("bar");
      },
    }, { getters: true }),
    "{ foo: [Thrown Error: bar] }",
  );
});

Deno.test(function inspectPrototype() {
  class A {}
  assertEquals(Deno.inspect(A.prototype), "A {}");
});

Deno.test(function inspectSorted() {
  assertEquals(
    stripColor(Deno.inspect({ b: 2, a: 1 }, { sorted: true })),
    "{ a: 1, b: 2 }",
  );
  assertEquals(
    stripColor(Deno.inspect(new Set(["b", "a"]), { sorted: true })),
    `Set { "a", "b" }`,
  );
  assertEquals(
    stripColor(Deno.inspect(
      new Map([
        ["b", 2],
        ["a", 1],
      ]),
      { sorted: true },
    )),
    `Map { "a" => 1, "b" => 2 }`,
  );
});

Deno.test(function inspectTrailingComma() {
  assertEquals(
    stripColor(Deno.inspect(
      [
        "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
        "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
      ],
      { trailingComma: true },
    )),
    `[
  "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
  "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
]`,
  );
  assertEquals(
    stripColor(Deno.inspect(
      {
        aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa: 1,
        bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb: 2,
      },
      { trailingComma: true },
    )),
    `{
  aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa: 1,
  bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb: 2,
}`,
  );
  assertEquals(
    stripColor(Deno.inspect(
      new Set([
        "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
        "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
      ]),
      { trailingComma: true },
    )),
    `Set {
  "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
  "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
}`,
  );
  assertEquals(
    stripColor(Deno.inspect(
      new Map([
        ["aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", 1],
        ["bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", 2],
      ]),
      { trailingComma: true },
    )),
    `Map {
  "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" => 1,
  "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb" => 2,
}`,
  );
});

Deno.test(function inspectCompact() {
  assertEquals(
    stripColor(Deno.inspect({ a: 1, b: 2 }, { compact: false })),
    `{
  a: 1,
  b: 2
}`,
  );
});

Deno.test(function inspectIterableLimit() {
  assertEquals(
    stripColor(Deno.inspect(["a", "b", "c"], { iterableLimit: 2 })),
    `[ "a", "b", ... 1 more items ]`,
  );
  assertEquals(
    stripColor(Deno.inspect(new Set(["a", "b", "c"]), { iterableLimit: 2 })),
    `Set { "a", "b", ... 1 more items }`,
  );
  assertEquals(
    stripColor(Deno.inspect(
      new Map([
        ["a", 1],
        ["b", 2],
        ["c", 3],
      ]),
      { iterableLimit: 2 },
    )),
    `Map { "a" => 1, "b" => 2, ... 1 more items }`,
  );
});

Deno.test(function inspectProxy() {
  assertEquals(
    stripColor(Deno.inspect(
      new Proxy([1, 2, 3], {}),
    )),
    "[ 1, 2, 3 ]",
  );
  assertEquals(
    stripColor(Deno.inspect(
      new Proxy({ key: "value" }, {}),
    )),
    `{ key: "value" }`,
  );
  assertEquals(
    stripColor(Deno.inspect(
      new Proxy({}, {
        get(_target, key) {
          if (key === Symbol.toStringTag) {
            return "MyProxy";
          } else {
            return 5;
          }
        },
        getOwnPropertyDescriptor() {
          return {
            enumerable: true,
            configurable: true,
            value: 5,
          };
        },
        ownKeys() {
          return ["prop1", "prop2"];
        },
      }),
    )),
    `MyProxy { prop1: 5, prop2: 5 }`,
  );
  assertEquals(
    stripColor(Deno.inspect(
      new Proxy([1, 2, 3], { get() {} }),
      { showProxy: true },
    )),
    "Proxy [ [ 1, 2, 3 ], { get: [Function: get] } ]",
  );
  assertEquals(
    stripColor(Deno.inspect(
      new Proxy({ a: 1 }, {
        set(): boolean {
          return false;
        },
      }),
      { showProxy: true },
    )),
    "Proxy [ { a: 1 }, { set: [Function: set] } ]",
  );
  assertEquals(
    stripColor(Deno.inspect(
      new Proxy([1, 2, 3, 4, 5, 6, 7], { get() {} }),
      { showProxy: true },
    )),
    `Proxy [ [
    1, 2, 3, 4,
    5, 6, 7
  ], { get: [Function: get] } ]`,
  );
  assertEquals(
    stripColor(Deno.inspect(
      new Proxy(function fn() {}, { get() {} }),
      { showProxy: true },
    )),
    "Proxy [ [Function: fn], { get: [Function: get] } ]",
  );
});

Deno.test(function inspectError() {
  const error1 = new Error("This is an error");
  const error2 = new Error("This is an error", {
    cause: new Error("This is a cause error"),
  });

  assertStringIncludes(
    stripColor(Deno.inspect(error1)),
    "Error: This is an error",
  );
  assertStringIncludes(
    stripColor(Deno.inspect(error2)),
    "Error: This is an error",
  );
  assertStringIncludes(
    stripColor(Deno.inspect(error2)),
    "Caused by Error: This is a cause error",
  );
});

Deno.test(function inspectErrorCircular() {
  const error1 = new Error("This is an error");
  const error2 = new Error("This is an error", {
    cause: new Error("This is a cause error"),
  });
  error1.cause = error1;
  assert(error2.cause instanceof Error);
  error2.cause.cause = error2;

  assertStringIncludes(
    stripColor(Deno.inspect(error1)),
    "Error: This is an error",
  );
  assertStringIncludes(
    stripColor(Deno.inspect(error2)),
    "<ref *1> Error: This is an error",
  );
  assertStringIncludes(
    stripColor(Deno.inspect(error2)),
    "Caused by Error: This is a cause error",
  );
  assertStringIncludes(
    stripColor(Deno.inspect(error2)),
    "Caused by [Circular *1]",
  );
});

Deno.test(function inspectColors() {
  assertEquals(Deno.inspect(1), "1");
  assertStringIncludes(Deno.inspect(1, { colors: true }), "\x1b[");
});

Deno.test(function inspectEmptyArray() {
  const arr: string[] = [];

  assertEquals(
    Deno.inspect(arr, {
      compact: false,
      trailingComma: true,
    }),
    "[\n]",
  );
});

Deno.test(function inspectDeepEmptyArray() {
  const obj = {
    arr: [],
  };

  assertEquals(
    Deno.inspect(obj, {
      compact: false,
      trailingComma: true,
    }),
    `{
  arr: [
  ],
}`,
  );
});

Deno.test(function inspectEmptyMap() {
  const map = new Map();

  assertEquals(
    Deno.inspect(map, {
      compact: false,
      trailingComma: true,
    }),
    "Map {\n}",
  );
});

Deno.test(function inspectEmptyMap() {
  const set = new Set();

  assertEquals(
    Deno.inspect(set, {
      compact: false,
      trailingComma: true,
    }),
    "Set {\n}",
  );
});

Deno.test(function inspectEmptyMap() {
  const typedArray = new Uint8Array(0);

  assertEquals(
    Deno.inspect(typedArray, {
      compact: false,
      trailingComma: true,
    }),
    "Uint8Array(0) [\n]",
  );
});

Deno.test(function inspectStringAbbreviation() {
  const LONG_STRING =
    "This is a really long string which will be abbreviated with ellipsis.";
  const obj = {
    str: LONG_STRING,
  };
  const arr = [LONG_STRING];

  assertEquals(
    Deno.inspect(obj, { strAbbreviateSize: 10 }),
    '{ str: "This is a ..." }',
  );

  assertEquals(
    Deno.inspect(arr, { strAbbreviateSize: 10 }),
    '[ "This is a ..." ]',
  );
});

Deno.test(async function inspectAggregateError() {
  try {
    await Promise.any([]);
  } catch (err) {
    assertEquals(
      Deno.inspect(err).trimEnd(),
      "AggregateError: All promises were rejected",
    );
  }
});

Deno.test(function inspectorMethods() {
  console.timeStamp("test");
  console.profile("test");
  console.profileEnd("test");
});