// Copyright 2018-2020 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, unitTest } from "./test_util.ts";
import { stripColor } from "../../../std/fmt/colors.ts";

const customInspect = Deno.customInspect;
const {
  Console,
  inspectArgs,
  // @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$/, ""));
}

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

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

unitTest(function consoleHasRightInstance(): void {
  assert(console instanceof Console);
  assertEquals({} instanceof Console, false);
});

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

unitTest(function consoleTestStringifyComplexObjects(): void {
  assertEquals(stringify("foo"), "foo");
  assertEquals(stringify(["foo", "bar"]), `[ "foo", "bar" ]`);
  assertEquals(stringify({ foo: "bar" }), `{ foo: "bar" }`);
});

unitTest(function consoleTestStringifyQuotes(): void {
  assertEquals(stringify(["\\"]), `[ "\\\\" ]`);
  assertEquals(stringify(['\\,"']), `[ '\\\\,"' ]`);
  assertEquals(stringify([`\\,",'`]), `[ \`\\\\,",'\` ]`);
  assertEquals(stringify(["\\,\",',`"]), `[ "\\\\,\\",',\`" ]`);
});

unitTest(function consoleTestStringifyLongStrings(): void {
  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);
});

/* eslint-disable @typescript-eslint/explicit-function-return-type */
unitTest(function consoleTestStringifyCircular(): void {
  class Base {
    a = 1;
    m1() {}
  }

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

  // eslint-disable-next-line @typescript-eslint/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 = `{
  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],
    emptyObj: {},
    arr: [ 1, "s", false, null, [Circular] ],
    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 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(null), "null");
  assertEquals(stringify(undefined), "undefined");
  assertEquals(stringify(new Extended()), "Extended { a: 1, b: 2 }");
  assertEquals(
    stringify(function f(): void {}),
    "[Function: f]",
  );
  assertEquals(
    stringify(async function af(): Promise<void> {}),
    "[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), "TypedArray {}");
  assertEquals(
    stringify({ a: { b: { c: { d: new Set([1]) } } } }),
    "{ a: { b: { c: { d: [Set] } } } }",
  );
  assertEquals(stringify(nestedObj), nestedObjExpected);
  assertEquals(stringify(JSON), 'JSON { Symbol(Symbol.toStringTag): "JSON" }');
  assertEquals(
    stringify(console),
    `{
  log: [Function: log],
  debug: [Function: log],
  info: [Function: log],
  dir: [Function: dir],
  dirxml: [Function: dir],
  warn: [Function: warn],
  error: [Function: warn],
  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],
  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" }',
  );
  // test inspect is working the same
  assertEquals(stripColor(Deno.inspect(nestedObj)), nestedObjExpected);
});
/* eslint-enable @typescript-eslint/explicit-function-return-type */

unitTest(function consoleTestStringifyWithDepth(): void {
  // eslint-disable-next-line @typescript-eslint/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] } } } }",
  );
});

unitTest(function consoleTestStringifyLargeObject(): void {
  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 }
  }
}`,
  );
});

unitTest(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> ]`,
  );

  /* TODO(ry) Fix this test
  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>
]`
  );
  */
});

unitTest(async function consoleTestStringifyPromises(): Promise<void> {
  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");
});

unitTest(function consoleTestWithCustomInspector(): void {
  class A {
    [customInspect](): string {
      return "b";
    }
  }

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

unitTest(function consoleTestWithCustomInspectorError(): void {
  class A {
    [customInspect](): string {
      throw new Error("BOOM");
      return "b";
    }
  }

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

  class B {
    constructor(public field: { a: string }) {}
    [customInspect](): string {
      return this.field.a;
    }
  }

  assertEquals(stringify(new B({ a: "a" })), "a");
  assertEquals(
    stringify(B.prototype),
    "{ Symbol(Deno.customInspect): [Function: [Deno.customInspect]] }",
  );
});

unitTest(function consoleTestWithIntegerFormatSpecifier(): void {
  assertEquals(stringify("%i"), "%i");
  assertEquals(stringify("%i", 42.0), "42");
  assertEquals(stringify("%i", 42), "42");
  assertEquals(stringify("%i", "42"), "42");
  assertEquals(stringify("%i", "42.0"), "42");
  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",
  );
});

unitTest(function consoleTestWithFloatFormatSpecifier(): void {
  assertEquals(stringify("%f"), "%f");
  assertEquals(stringify("%f", 42.0), "42");
  assertEquals(stringify("%f", 42), "42");
  assertEquals(stringify("%f", "42"), "42");
  assertEquals(stringify("%f", "42.0"), "42");
  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), "5");
  assertEquals(stringify("%f %f", 42, 43), "42 43");
  assertEquals(stringify("%f %f", 42), "42 %f");
});

unitTest(function consoleTestWithStringFormatSpecifier(): void {
  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)");
});

unitTest(function consoleTestWithObjectFormatSpecifier(): void {
  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] } } } }",
  );
});

unitTest(function consoleTestWithVariousOrInvalidFormatSpecifier(): void {
  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");
});

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

unitTest(function consoleTestError(): void {
  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"),
    );
  }
});

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

// Test bound this issue
unitTest(function consoleDetachedLog(): void {
  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): void {
    this.chunks.push(x);
  }
  toString(): string {
    return this.chunks.join("");
  }
}

type ConsoleExamineFunc = (
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
  csl: any,
  out: StringBuffer,
  err?: StringBuffer,
  both?: StringBuffer,
) => void;

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

// console.group test
unitTest(function consoleGroup(): void {
  mockConsole((console, out): void => {
    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
unitTest(function consoleGroupWarn(): void {
  mockConsole((console, _out, _err, both): void => {
    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
unitTest(function consoleTable(): void {
  mockConsole((console, out): void => {
    console.table({ a: "test", b: 1 });
    assertEquals(
      stripColor(out.toString()),
      `┌───────┬────────┐
│ (idx) │ Values │
├───────┼────────┤
│   a   │ "test" │
│   b   │   1    │
└───────┴────────┘
`,
    );
  });
  mockConsole((console, out): void => {
    console.table({ a: { b: 10 }, b: { b: 20, c: 30 } }, ["c"]);
    assertEquals(
      stripColor(out.toString()),
      `┌───────┬────┐
│ (idx) │ c  │
├───────┼────┤
│   a   │    │
│   b   │ 30 │
└───────┴────┘
`,
    );
  });
  mockConsole((console, out): void => {
    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): void => {
    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): void => {
    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): void => {
    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): void => {
    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): void => {
    console.table([]);
    assertEquals(
      stripColor(out.toString()),
      `┌───────┐
│ (idx) │
├───────┤
└───────┘
`,
    );
  });
  mockConsole((console, out): void => {
    console.table({});
    assertEquals(
      stripColor(out.toString()),
      `┌───────┐
│ (idx) │
├───────┤
└───────┘
`,
    );
  });
  mockConsole((console, out): void => {
    console.table(new Set());
    assertEquals(
      stripColor(out.toString()),
      `┌────────────┐
│ (iter idx) │
├────────────┤
└────────────┘
`,
    );
  });
  mockConsole((console, out): void => {
    console.table(new Map());
    assertEquals(
      stripColor(out.toString()),
      `┌────────────┐
│ (iter idx) │
├────────────┤
└────────────┘
`,
    );
  });
  mockConsole((console, out): void => {
    console.table("test");
    assertEquals(out.toString(), "test\n");
  });
  mockConsole((console, out): void => {
    console.table(["Hello", "你好", "Amapá"]);
    assertEquals(
      stripColor(out.toString()),
      `┌───────┬─────────┐
│ (idx) │ Values  │
├───────┼─────────┤
│   0   │ "Hello" │
│   1   │ "你好"  │
│   2   │ "Amapá" │
└───────┴─────────┘
`,
    );
  });
  mockConsole((console, out): void => {
    console.table([
      [1, 2],
      [3, 4],
    ]);
    assertEquals(
      stripColor(out.toString()),
      `┌───────┬───┬───┐
│ (idx) │ 0 │ 1 │
├───────┼───┼───┤
│   0   │ 1 │ 2 │
│   1   │ 3 │ 4 │
└───────┴───┴───┘
`,
    );
  });
  mockConsole((console, out): void => {
    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 │
└───────┴───┘
`,
    );
  });
});

// console.log(Error) test
unitTest(function consoleLogShouldNotThrowError(): void {
  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): void => {
    console.log(new Error("foo"));
    assertEquals(out.toString().includes("Uncaught"), false);
  });
});

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

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

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

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

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

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

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

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