// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license.
import { AssertionError, assertObjectMatch, assertThrows } from "./mod.ts";

const sym = Symbol("foo");
const a = { foo: true, bar: false };
const b = { ...a, baz: a };
const c = { ...b, qux: b };
const d = { corge: c, grault: c };
const e = { foo: true } as { [key: string]: unknown };
e.bar = e;
const f = { [sym]: true, bar: false };
interface r {
  foo: boolean;
  bar: boolean;
}
const g: r = { foo: true, bar: false };
const h = { foo: [1, 2, 3], bar: true };
const i = { foo: [a, e], bar: true };
const j = { foo: [[1, 2, 3]], bar: true };
const k = { foo: [[1, [2, [3]]]], bar: true };
const l = { foo: [[1, [2, [a, e, j, k]]]], bar: true };
const m = { foo: /abc+/i, bar: [/abc/g, /abc/m] };
const n = {
  foo: new Set(["foo", "bar"]),
  bar: new Map([
    ["foo", 1],
    ["bar", 2],
  ]),
  baz: new Map([
    ["a", a],
    ["b", b],
  ]),
};

Deno.test("assertObjectMatch() matches simple subset", () => {
  assertObjectMatch(a, {
    foo: true,
  });
});

Deno.test("assertObjectMatch() matches subset with another subset", () => {
  assertObjectMatch(b, {
    foo: true,
    baz: { bar: false },
  });
});

Deno.test("assertObjectMatch() matches subset with multiple subsets", () => {
  assertObjectMatch(c, {
    foo: true,
    baz: { bar: false },
    qux: {
      baz: { foo: true },
    },
  });
});

Deno.test("assertObjectMatch() matches subset with same object reference as subset", () => {
  assertObjectMatch(d, {
    corge: {
      foo: true,
      qux: { bar: false },
    },
    grault: {
      bar: false,
      qux: { foo: true },
    },
  });
});

Deno.test("assertObjectMatch() matches subset with circular reference", () => {
  assertObjectMatch(e, {
    foo: true,
    bar: {
      bar: {
        bar: {
          foo: true,
        },
      },
    },
  });
});

Deno.test("assertObjectMatch() matches subset with interface", () => {
  assertObjectMatch(g, { bar: false });
});

Deno.test("assertObjectMatch() matches subset with same symbol", () => {
  assertObjectMatch(f, {
    [sym]: true,
  });
});

Deno.test("assertObjectMatch() matches subset with array inside", () => {
  assertObjectMatch(h, { foo: [] });
  assertObjectMatch(h, { foo: [1, 2] });
  assertObjectMatch(h, { foo: [1, 2, 3] });
  assertObjectMatch(i, { foo: [{ bar: false }] });
  assertObjectMatch(i, {
    foo: [{ bar: false }, { bar: { bar: { bar: { foo: true } } } }],
  });
});

Deno.test("assertObjectMatch() matches subset with nested array inside", () => {
  assertObjectMatch(j, { foo: [[1, 2, 3]] });
  assertObjectMatch(k, { foo: [[1, [2, [3]]]] });
  assertObjectMatch(l, { foo: [[1, [2, [a, e, j, k]]]] });
});

Deno.test("assertObjectMatch() matches subset with regexp", () => {
  assertObjectMatch(m, { foo: /abc+/i });
  assertObjectMatch(m, { bar: [/abc/g, /abc/m] });
});

Deno.test("assertObjectMatch() matches subset with built-in data structures", () => {
  assertObjectMatch(n, { foo: new Set(["foo"]) });
  assertObjectMatch(n, { bar: new Map([["bar", 2]]) });
  assertObjectMatch(n, { baz: new Map([["b", b]]) });
  assertObjectMatch(n, { baz: new Map([["b", { foo: true }]]) });
});

Deno.test("assertObjectMatch() throws when a key is missing from subset", () => {
  assertThrows(
    () =>
      assertObjectMatch(
        {
          foo: true,
        },
        {
          foo: true,
          bar: false,
        },
      ),
    AssertionError,
  );
});

Deno.test("assertObjectMatch() throws when simple subset mismatches", () => {
  assertThrows(
    () =>
      assertObjectMatch(a, {
        foo: false,
      }),
    AssertionError,
  );
});

Deno.test("assertObjectMatch() throws when subset with another subset mismatches", () => {
  assertThrows(
    () =>
      assertObjectMatch(b, {
        foo: true,
        baz: { bar: true },
      }),
    AssertionError,
  );
});

Deno.test("assertObjectMatch() throws when subset with multiple subsets mismatches", () => {
  assertThrows(
    () =>
      assertObjectMatch(c, {
        foo: true,
        baz: { bar: false },
        qux: {
          baz: { foo: false },
        },
      }),
    AssertionError,
  );
});

Deno.test("assertObjectMatch() throws when subset with same object reference as subset mismatches", () => {
  assertThrows(
    () =>
      assertObjectMatch(d, {
        corge: {
          foo: true,
          qux: { bar: true },
        },
        grault: {
          bar: false,
          qux: { foo: false },
        },
      }),
    AssertionError,
  );
});

Deno.test("assertObjectMatch() throws when subset with circular reference mismatches", () => {
  assertThrows(
    () =>
      assertObjectMatch(e, {
        foo: true,
        bar: {
          bar: {
            bar: {
              foo: false,
            },
          },
        },
      }),
    AssertionError,
  );
});

Deno.test("assertObjectMatch() throws when subset with symbol key mismatches other string key", () => {
  assertThrows(
    () =>
      assertObjectMatch(f, {
        [sym]: false,
      }),
    AssertionError,
  );
});

Deno.test("assertObjectMatch() throws when subset with array inside mismatches", () => {
  assertThrows(
    () =>
      assertObjectMatch(i, {
        foo: [1, 2, 3, 4],
      }),
    AssertionError,
  );
  assertThrows(
    () =>
      assertObjectMatch(i, {
        foo: [{ bar: true }, { foo: false }],
      }),
    AssertionError,
  );
});

Deno.test("assertObjectMatch() matches when actual/expected value as instance of class mismatches", () => {
  class A {
    a: number;
    constructor(a: number) {
      this.a = a;
    }
  }
  assertObjectMatch({ test: new A(1) }, { test: { a: 1 } });
  assertObjectMatch({ test: { a: 1 } }, { test: { a: 1 } });
  assertObjectMatch({ test: { a: 1 } }, { test: new A(1) });
  assertObjectMatch({ test: new A(1) }, { test: new A(1) });
});

Deno.test("assertObjectMatch() matches when actual/expected contains same instance of Map/TypedArray/etc", () => {
  const body = new Uint8Array([0, 1, 2]);
  assertObjectMatch({ body, foo: "foo" }, { body });
});

Deno.test("assertObjectMatch() matches subsets of arrays", () => {
  assertObjectMatch(
    { positions: [[1, 2, 3, 4]] },
    {
      positions: [[1, 2, 3]],
    },
  );
});

Deno.test("assertObjectMatch() throws when regexp mismatches", () => {
  assertThrows(() => assertObjectMatch(m, { foo: /abc+/ }), AssertionError);
  assertThrows(() => assertObjectMatch(m, { foo: /abc*/i }), AssertionError);
  assertThrows(
    () => assertObjectMatch(m, { bar: [/abc/m, /abc/g] }),
    AssertionError,
  );
});

Deno.test("assertObjectMatch() throws when built-in data structures mismatches", () => {
  assertThrows(
    () => assertObjectMatch(n, { foo: new Set(["baz"]) }),
    AssertionError,
  );
  assertThrows(
    () => assertObjectMatch(n, { bar: new Map([["bar", 3]]) }),
    AssertionError,
  );
  assertThrows(
    () => assertObjectMatch(n, { baz: new Map([["a", { baz: true }]]) }),
    AssertionError,
  );
});

Deno.test("assertObjectMatch() throws assertion error when in the first argument mismatches, rather than a TypeError: Invalid value used as weak map key", () => {
  assertThrows(
    () => assertObjectMatch({ foo: null }, { foo: { bar: 42 } }),
    AssertionError,
  );
  assertObjectMatch({ foo: null, bar: null }, { foo: null });
  assertObjectMatch({ foo: undefined, bar: null }, { foo: undefined });
  assertThrows(
    () => assertObjectMatch({ foo: undefined, bar: null }, { foo: null }),
    AssertionError,
  );
});

Deno.test("assertObjectMatch() throws readable type error for non mapable primative types", () => {
  assertThrows(
    // @ts-expect-error Argument of type 'null' is not assignable to parameter of type 'Record<PropertyKey, any>'
    () => assertObjectMatch(null, { foo: 42 }),
    TypeError,
    "assertObjectMatch",
  );
  // @ts-expect-error Argument of type 'null' is not assignable to parameter of type 'Record<PropertyKey, any>'
  assertThrows(() => assertObjectMatch(null, { foo: 42 }), TypeError, "null"); // since typeof null is "object", want to make sure user knows the bad value is "null"
  assertThrows(
    // @ts-expect-error Argument of type 'undefined' is not assignable to parameter of type 'Record<PropertyKey, any>'
    () => assertObjectMatch(undefined, { foo: 42 }),
    TypeError,
    "assertObjectMatch",
  );
  // @ts-expect-error Argument of type 'number' is not assignable to parameter of type 'Record<PropertyKey, any>'
  assertThrows(() => assertObjectMatch(21, 42), TypeError, "assertObjectMatch");
  assertThrows(
    // @ts-expect-error Argument of type 'string' is not assignable to parameter of type 'Record<PropertyKey, any>'
    () => assertObjectMatch("string", "string"),
    TypeError,
    "assertObjectMatch",
  );
});
