import { describe, it, expect, afterAll, vi } from "vitest";
import * as Misc from "../../src/utils/misc";
import { ObjectId } from "mongodb";

describe("Misc Utils", () => {
  afterAll(() => {
    vi.useRealTimers();
  });

  describe("matchesAPattern", () => {
    const testCases = [
      {
        pattern: "eng.*",
        cases: ["english", "aenglish", "en", "eng"],
        expected: [true, false, false, true],
      },

      {
        pattern: "\\d+",
        cases: ["b", "2", "331", "1a"],
        expected: [false, true, true, false],
      },
      {
        pattern: "(hi|hello)",
        cases: ["hello", "hi", "hillo", "hi hello"],
        expected: [true, true, false, false],
      },
      {
        pattern: ".+",
        cases: ["a2", "b2", "c1", ""],
        expected: [true, true, true, false],
      },
    ];

    it.each(testCases)(
      "matchesAPattern with $pattern",
      ({ pattern, cases, expected }) => {
        cases.forEach((caseValue, index) => {
          expect(Misc.matchesAPattern(caseValue, pattern)).toBe(
            expected[index],
          );
        });
      },
    );
  });

  describe("kogascore", () => {
    const testCases = [
      {
        wpm: 214.8,
        acc: 93.04,
        timestamp: 1653586489000,
        expectedScore: 1214800930423111,
      },
      {
        wpm: 214.8,
        acc: 93.04,
        timestamp: 1653601763000,
        expectedScore: 1214800930407837,
      },
      {
        wpm: 199.37,
        acc: 97.69,
        timestamp: 1653588809000,
        expectedScore: 1199370976920791,
      },
      {
        wpm: 196.2,
        acc: 96.07,
        timestamp: 1653591901000,
        expectedScore: 1196200960717699,
      },
      {
        wpm: 196.205,
        acc: 96.075,
        timestamp: 1653591901000,
        expectedScore: 1196210960817699,
      },
      {
        // this one is particularly important - in JS 154.39 * 100 is equal to 15438.999999999998
        // thanks floating point errors!
        wpm: 154.39,
        acc: 96.14,
        timestamp: 1740333827000,
        expectedScore: 1154390961421373,
      },
    ];

    it.each(testCases)(
      "kogascore with wpm:$wpm, acc:$acc, timestamp:$timestamp = $expectedScore",
      ({ wpm, acc, timestamp, expectedScore }) => {
        expect(Misc.kogascore(wpm, acc, timestamp)).toBe(expectedScore);
      },
    );
  });

  describe("identity", () => {
    const testCases = [
      {
        input: "",
        expected: "string",
      },
      {
        input: {},
        expected: "object",
      },
      {
        input: 0,
        expected: "number",
      },
      {
        input: null,
        expected: "null",
      },
      {
        input: undefined,
        expected: "undefined",
      },
    ];
    it.each(testCases)(
      "identity with $input = $expected",
      ({ input, expected }) => {
        expect(Misc.identity(input)).toBe(expected);
      },
    );
  });

  describe("flattenObjectDeep", () => {
    const testCases = [
      {
        obj: {
          a: {
            b: {
              c: 1,
            },
          },
          d: 2,
          e: [],
        },
        expected: {
          "a.b.c": 1,
          d: 2,
          e: [],
        },
      },
      {
        obj: {
          a: {
            b: {
              c: 1,
            },
          },
          d: {
            e: {
              f: 2,
              g: 3,
            },
          },
        },
        expected: {
          "a.b.c": 1,
          "d.e.f": 2,
          "d.e.g": 3,
        },
      },
      {
        obj: {
          a: {
            b: {
              c: 1,
              d: {
                e: 2,
                f: 3,
                g: {},
              },
            },
          },
        },
        expected: {
          "a.b.c": 1,
          "a.b.d.e": 2,
          "a.b.d.f": 3,
          "a.b.d.g": {},
        },
      },
      {
        obj: {},
        expected: {},
      },
    ];

    it.each(testCases)(
      "flattenObjectDeep with $obj = $expected",
      ({ obj, expected }) => {
        expect(Misc.flattenObjectDeep(obj)).toEqual(expected);
      },
    );
  });

  it("sanitizeString", () => {
    const testCases = [
      {
        input: "h̶̼͔̭͈̏́̀́͋͜ͅe̵̺̞̦̫̫͔̋́̅̅̃̀͝͝ļ̶̬̯͚͇̺͍̞̫̟͖͋̓͛̆̒̓͜ĺ̴̗̘͇̬̆͂͌̈͊͝͝ỡ̴̡̦̩̠̞̐̃͆̚͠͝",
        expected: "hello",
      },
      {
        input: "hello",
        expected: "hello",
      },
      {
        input: "hel   lo",
        expected: "hel  lo",
      },
      {
        input: "   hel   lo   ",
        expected: "hel  lo",
      },
      {
        input: "",
        expected: "",
      },
      {
        input: "   \n\n\n",
        expected: "",
      },
      {
        input: undefined,
        expected: undefined,
      },
    ];

    testCases.forEach(({ input, expected }) => {
      expect(Misc.sanitizeString(input)).toEqual(expected);
    });
  });

  it("getOrdinalNumberString", () => {
    const testCases = [
      {
        input: 0,
        output: "0th",
      },
      {
        input: 1,
        output: "1st",
      },
      {
        input: 2,
        output: "2nd",
      },
      {
        input: 3,
        output: "3rd",
      },
      {
        input: 4,
        output: "4th",
      },
      {
        input: 10,
        output: "10th",
      },
      {
        input: 11,
        output: "11th",
      },
      {
        input: 12,
        output: "12th",
      },
      {
        input: 13,
        output: "13th",
      },
      {
        input: 100,
        output: "100th",
      },
      {
        input: 101,
        output: "101st",
      },
      {
        input: 102,
        output: "102nd",
      },
      {
        input: 103,
        output: "103rd",
      },
      {
        input: 104,
        output: "104th",
      },
      {
        input: 93589423,
        output: "93589423rd",
      },
    ];

    testCases.forEach(({ input, output }) => {
      expect(Misc.getOrdinalNumberString(input)).toEqual(output);
    });
  });
  it("formatSeconds", () => {
    const testCases = [
      {
        seconds: 5,
        expected: "5 seconds",
      },
      {
        seconds: 65,
        expected: "1.08 minutes",
      },
      {
        seconds: Misc.HOUR_IN_SECONDS,
        expected: "1 hour",
      },
      {
        seconds: Misc.DAY_IN_SECONDS,
        expected: "1 day",
      },
      {
        seconds: Misc.WEEK_IN_SECONDS,
        expected: "1 week",
      },
      {
        seconds: Misc.YEAR_IN_SECONDS,
        expected: "1 year",
      },
      {
        seconds: 2 * Misc.YEAR_IN_SECONDS,
        expected: "2 years",
      },
      {
        seconds: 4 * Misc.YEAR_IN_SECONDS,
        expected: "4 years",
      },
      {
        seconds: 3 * Misc.WEEK_IN_SECONDS,
        expected: "3 weeks",
      },
      {
        seconds: Misc.MONTH_IN_SECONDS * 4,
        expected: "4 months",
      },
      {
        seconds: Misc.MONTH_IN_SECONDS * 11,
        expected: "11 months",
      },
    ];

    testCases.forEach(({ seconds, expected }) => {
      expect(Misc.formatSeconds(seconds)).toBe(expected);
    });
  });

  describe("replaceObjectId", () => {
    it("replaces objecId with string", () => {
      const fromDatabase = {
        _id: new ObjectId(),
        test: "test",
        number: 1,
      };
      expect(Misc.replaceObjectId(fromDatabase)).toStrictEqual({
        _id: fromDatabase._id.toHexString(),
        test: "test",
        number: 1,
      });
    });
    it("ignores null values", () => {
      expect(Misc.replaceObjectId(null)).toBeNull();
    });
  });

  describe("replaceObjectIds", () => {
    it("replaces objecIds with string", () => {
      const fromDatabase = {
        _id: new ObjectId(),
        test: "test",
        number: 1,
      };
      const fromDatabase2 = {
        _id: new ObjectId(),
        test: "bob",
        number: 2,
      };
      expect(
        Misc.replaceObjectIds([fromDatabase, fromDatabase2]),
      ).toStrictEqual([
        {
          _id: fromDatabase._id.toHexString(),
          test: "test",
          number: 1,
        },
        {
          _id: fromDatabase2._id.toHexString(),
          test: "bob",
          number: 2,
        },
      ]);
    });
    it("handles undefined", () => {
      expect(Misc.replaceObjectIds(undefined as any)).toBeUndefined();
    });
  });

  describe("omit()", () => {
    it("should omit a single key", () => {
      const input = { a: 1, b: 2, c: 3 };
      const result = Misc.omit(input, ["b"]);
      expect(result).toEqual({ a: 1, c: 3 });
    });

    it("should omit multiple keys", () => {
      const input = { a: 1, b: 2, c: 3, d: 4 };
      const result = Misc.omit(input, ["a", "d"]);
      expect(result).toEqual({ b: 2, c: 3 });
    });

    it("should return the same object if no keys are omitted", () => {
      const input = { x: 1, y: 2 };
      const result = Misc.omit(input, []);
      expect(result).toEqual({ x: 1, y: 2 });
    });

    it("should not mutate the original object", () => {
      const input = { foo: "bar", baz: "qux" };
      const copy = { ...input };
      Misc.omit(input, ["baz"]);
      expect(input).toEqual(copy);
    });

    it("should ignore keys that do not exist", () => {
      const input = { a: 1, b: 2 };
      const result = Misc.omit(input, "c" as any); // allow a non-existing key
      expect(result).toEqual({ a: 1, b: 2 });
    });

    it("should work with different value types", () => {
      const input = {
        str: "hello",
        num: 123,
        bool: true,
        obj: { x: 1 },
        arr: [1, 2, 3],
      };
      const result = Misc.omit(input, ["bool", "arr"]);
      expect(result).toEqual({
        str: "hello",
        num: 123,
        obj: { x: 1 },
      });
    });
  });

  describe("isPlainObject", () => {
    it("should return true for plain objects", () => {
      expect(Misc.isPlainObject({})).toBe(true);
      expect(Misc.isPlainObject({ a: 1, b: 2 })).toBe(true);
      expect(Misc.isPlainObject(Object.create(Object.prototype))).toBe(true);
    });

    it("should return false for arrays", () => {
      expect(Misc.isPlainObject([])).toBe(false);
      expect(Misc.isPlainObject([1, 2, 3])).toBe(false);
    });

    it("should return false for null", () => {
      expect(Misc.isPlainObject(null)).toBe(false);
    });

    it("should return false for primitives", () => {
      expect(Misc.isPlainObject(123)).toBe(false);
      expect(Misc.isPlainObject("string")).toBe(false);
      expect(Misc.isPlainObject(true)).toBe(false);
      expect(Misc.isPlainObject(undefined)).toBe(false);
      expect(Misc.isPlainObject(Symbol("sym"))).toBe(false);
    });

    it("should return false for objects with different prototypes", () => {
      // oxlint-disable-next-line no-extraneous-class
      class MyClass {}
      expect(Misc.isPlainObject(new MyClass())).toBe(false);
      expect(Misc.isPlainObject(Object.create(null))).toBe(false);
      expect(Misc.isPlainObject(new Date())).toBe(false);
      expect(Misc.isPlainObject(new Map())).toBe(false);
      expect(Misc.isPlainObject(new Set())).toBe(false);
    });

    it("should return false for functions", () => {
      // oxlint-disable-next-line no-empty-function
      expect(Misc.isPlainObject(function () {})).toBe(false);
      // oxlint-disable-next-line no-empty-function
      expect(Misc.isPlainObject(() => {})).toBe(false);
    });
  });
});
