import {
  describe,
  it,
  expect,
  beforeEach,
  afterEach,
  beforeAll,
  afterAll,
  vi,
} from "vitest";
import { setup } from "../../__testData__/controller-test";
import * as Configuration from "../../../src/init/configuration";
import { generateCurrentTestActivity } from "../../../src/api/controllers/user";
import * as UserDal from "../../../src/dal/user";
import * as AuthUtils from "../../../src/utils/auth";
import * as BlocklistDal from "../../../src/dal/blocklist";
import * as PresetDal from "../../../src/dal/preset";
import * as ConfigDal from "../../../src/dal/config";
import * as ResultDal from "../../../src/dal/result";
import * as ReportDal from "../../../src/dal/report";
import * as DailyLeaderboards from "../../../src/utils/daily-leaderboards";
import * as LeaderboardDal from "../../../src/dal/leaderboards";
import GeorgeQueue from "../../../src/queues/george-queue";
import * as DiscordUtils from "../../../src/utils/discord";
import * as Captcha from "../../../src/utils/captcha";
import * as FirebaseAdmin from "../../../src/init/firebase-admin";
import { FirebaseError } from "firebase-admin";
import * as ApeKeysDal from "../../../src/dal/ape-keys";
import * as LogDal from "../../../src/dal/logs";
import { ObjectId } from "mongodb";
import { PersonalBest } from "@monkeytype/schemas/shared";
import { mockAuthenticateWithApeKey } from "../../__testData__/auth";
import { randomUUID } from "node:crypto";
import { MonkeyMail, UserStreak } from "@monkeytype/schemas/users";
import MonkeyError, { isFirebaseError } from "../../../src/utils/error";
import * as WeeklyXpLeaderboard from "../../../src/services/weekly-xp-leaderboard";
import * as ConnectionsDal from "../../../src/dal/connections";
import { pb } from "../../__testData__/users";
import Test from "supertest/lib/test";

const { mockApp, uid, mockAuth } = setup();
const configuration = Configuration.getCachedConfiguration();

describe("user controller test", () => {
  describe("user signup", () => {
    const blocklistContainsMock = vi.spyOn(BlocklistDal, "contains");
    const firebaseDeleteUserMock = vi.spyOn(AuthUtils, "deleteUser");
    const usernameAvailableMock = vi.spyOn(UserDal, "isNameAvailable");
    const verifyCaptchaMock = vi.spyOn(Captcha, "verify");
    beforeEach(async () => {
      await enableSignup(true);
      usernameAvailableMock.mockResolvedValue(true);
    });
    afterEach(() => {
      [
        blocklistContainsMock,
        firebaseDeleteUserMock,
        usernameAvailableMock,
      ].forEach((it) => it.mockClear());
    });

    it("should fail if blocklisted", async () => {
      //GIVEN
      blocklistContainsMock.mockResolvedValue(true);
      firebaseDeleteUserMock.mockResolvedValue();

      const newUser = {
        name: "NewUser",
        uid: uid,
        email: "newuser@mail.com",
        captcha: "captcha",
      };

      //WHEN
      const result = await mockApp
        .post("/users/signup")
        .set("Authorization", `Bearer ${uid}`)
        .send(newUser)
        .expect(409);

      //THEN
      expect(result.body.message).toEqual("Username or email blocked");
      expect(blocklistContainsMock).toHaveBeenCalledWith({
        name: "NewUser",
        email: "newuser@mail.com",
      });

      //user will be created in firebase from the frontend, make sure we remove it
      expect(firebaseDeleteUserMock).toHaveBeenCalledWith(uid);
      expect(verifyCaptchaMock).toHaveBeenCalledWith("captcha");
    });

    it("should fail if domain is blacklisted", async () => {
      for (const domain of ["tidal.lol", "selfbot.cc"]) {
        //GIVEN
        firebaseDeleteUserMock.mockResolvedValue();
        mockAuth.modifyToken({
          email: `newuser@${domain}`,
        });

        const newUser = {
          name: "NewUser",
          uid: uid,
          email: `newuser@${domain}`,
          captcha: "captcha",
        };

        //WHEN
        const result = await mockApp
          .post("/users/signup")
          .set("Authorization", `Bearer ${uid}`)
          .send(newUser)
          .set({
            Accept: "application/json",
          })
          .expect(400);

        //THEN
        expect(result.body.message).toEqual("Invalid domain");

        //user will be created in firebase from the frontend, make sure we remove it
        expect(firebaseDeleteUserMock).toHaveBeenCalledWith(uid);
      }
    });

    it("should fail if username is taken", async () => {
      //GIVEN
      usernameAvailableMock.mockResolvedValue(false);
      firebaseDeleteUserMock.mockResolvedValue();

      const newUser = {
        name: "NewUser",
        uid: uid,
        email: "newuser@mail.com",
        captcha: "captcha",
      };

      //WHEN
      const result = await mockApp
        .post("/users/signup")
        .set("Authorization", `Bearer ${uid}`)
        .send(newUser)
        .expect(409);

      //THEN
      expect(result.body.message).toEqual("Username unavailable");
      expect(usernameAvailableMock).toHaveBeenCalledWith("NewUser", uid);

      //user will be created in firebase from the frontend, make sure we remove it
      expect(firebaseDeleteUserMock).toHaveBeenCalledWith(uid);
    });
    it("should fail if capture is invalid", async () => {
      //GIVEN
      verifyCaptchaMock.mockResolvedValue(false);

      const newUser = {
        name: "NewUser",
        uid: uid,
        email: "newuser@mail.com",
        captcha: "captcha",
      };

      //WHEN
      const { body } = await mockApp
        .post("/users/signup")
        .set("Authorization", `Bearer ${uid}`)
        .send(newUser)
        .expect(422);

      //THEN
      expect(body.message).toEqual("Captcha challenge failed");
    });
    it("should fail if username too long", async () => {
      //GIVEN
      const newUser = {
        uid: uid,
        email: "newuser@mail.com",
        captcha: "captcha",
      };

      //WHEN
      const { body } = await mockApp
        .post("/users/signup")
        .set("Authorization", `Bearer ${uid}`)
        .send({ ...newUser, name: new Array(17).fill("x").join("") })
        .expect(422);

      //THEN
      expect(body).toEqual({
        message: "Invalid request data schema",
        validationErrors: [
          '"name" String must contain at most 16 character(s)',
        ],
      });
    });
    it("should fail if username contains disallowed word", async () => {
      //GIVEN
      const newUser = {
        uid: uid,
        email: "newuser@mail.com",
        captcha: "captcha",
      };

      //WHEN
      const { body } = await mockApp
        .post("/users/signup")
        .set("Authorization", `Bearer ${uid}`)
        .send({ ...newUser, name: "miodec" })
        .expect(422);

      //THEN
      expect(body).toEqual({
        message: "Invalid request data schema",
        validationErrors: [
          '"name" Disallowed word detected. Please remove it. If you believe this is a mistake, please contact us (miodec).',
        ],
      });
    });
  });
  describe("checkName", () => {
    const userIsNameAvailableMock = vi.spyOn(UserDal, "isNameAvailable");

    beforeEach(() => {
      userIsNameAvailableMock.mockClear();
    });

    it("returns available if name is available", async () => {
      //GIVEN
      userIsNameAvailableMock.mockResolvedValue(true);

      //WHEN
      const { body } = await mockApp
        .get("/users/checkName/bob")
        //no authentication required
        .expect(200);

      //THEN
      expect(body).toEqual({
        message: "Check username",
        data: { available: true },
      });
      expect(userIsNameAvailableMock).toHaveBeenCalledWith("bob", "");
    });

    it("returns taken if name is not available", async () => {
      //GIVEN
      userIsNameAvailableMock.mockResolvedValue(false);

      //WHEN
      const { body } = await mockApp
        .get("/users/checkName/bob")
        //no authentication required
        .expect(200);

      //THEN
      expect(body).toEqual({
        message: "Check username",
        data: { available: false },
      });

      expect(userIsNameAvailableMock).toHaveBeenCalledWith("bob", "");
    });
    it("returns ok if name is our own", async () => {
      //GIVEN
      userIsNameAvailableMock.mockResolvedValue(true);

      //WHEN
      const { body } = await mockApp
        .get("/users/checkName/bob")
        .set("Authorization", `Bearer ${uid}`)
        .expect(200);

      //THEN
      expect(body).toEqual({
        message: "Check username",
        data: { available: true },
      });
      expect(userIsNameAvailableMock).toHaveBeenCalledWith("bob", uid);
    });
    it("returns 422 if username contains disallowed word", async () => {
      await mockApp
        .get("/users/checkName/newMiodec")
        //no authentication required
        .expect(422);
    });
  });
  describe("sendVerificationEmail", () => {
    const adminGetUserMock = vi.fn();
    const adminGenerateVerificationLinkMock = vi.fn();
    const getPartialUserMock = vi.spyOn(UserDal, "getPartialUser");

    vi.spyOn(FirebaseAdmin, "default").mockReturnValue({
      auth: () => ({
        getUser: adminGetUserMock,
        generateEmailVerificationLink: adminGenerateVerificationLinkMock,
      }),
    } as any);

    vi.mock("../../../src/queues/email-queue", () => ({
      __esModule: true,
      default: { sendVerificationEmail: vi.fn() },
    }));

    beforeEach(() => {
      adminGetUserMock.mockClear().mockResolvedValue({ emailVerified: false });
      getPartialUserMock.mockClear().mockResolvedValue({
        uid,
        name: "Bob",
        email: "newuser@mail.com",
      } as any);
    });

    it("should send verfification email", async () => {
      //GIVEN

      //"HEN
      const { body } = await mockApp
        .get("/users/verificationEmail")
        .set("Authorization", `Bearer ${uid}`)
        .expect(200);

      //THEN
      expect(body).toEqual({
        message: "Email sent",
        data: null,
      });

      expect(adminGetUserMock).toHaveBeenCalledWith(uid);
      expect(getPartialUserMock).toHaveBeenCalledWith(
        uid,
        "request verification email",
        ["uid", "name", "email"],
      );
      expect(adminGenerateVerificationLinkMock).toHaveBeenCalledWith(
        "newuser@mail.com",
        { url: "http://localhost:3000" },
      );
    });
    it("should fail with missing firebase user", async () => {
      //GIVEN
      adminGetUserMock.mockRejectedValue(new Error("test"));

      //WHEN
      const { body } = await mockApp
        .get("/users/verificationEmail")
        .set("Authorization", `Bearer ${uid}`)
        .expect(500);

      //THEN
      expect(body.message).toContain(
        "Auth user not found, even though the token got decoded",
      );
    });
    it("should fail with already verified email", async () => {
      //GIVEN
      adminGetUserMock.mockResolvedValue({ emailVerified: true });

      //WHEN
      const { body } = await mockApp
        .get("/users/verificationEmail")
        .set("Authorization", `Bearer ${uid}`)
        .expect(400);

      //THEN
      expect(body.message).toEqual("Email already verified");
    });
    it("should fail with email not matching the one from the authentication", async () => {
      //GIVEN
      getPartialUserMock.mockResolvedValue({
        email: "nonmatching@example.com",
      } as any);

      //WHEN
      const { body } = await mockApp
        .get("/users/verificationEmail")
        .set("Authorization", `Bearer ${uid}`)
        .expect(400);

      //THEN
      expect(body.message).toEqual(
        "Authenticated email does not match the email found in the database. This might happen if you recently changed your email. Please refresh and try again.",
      );
    });

    it("should fail with too many firebase requests", async () => {
      //GIVEN
      const mockFirebaseError = {
        code: "auth/too-many-requests",
        codePrefix: "auth",
        errorInfo: {
          code: "auth/too-many-requests",
          message: "Too many requests",
        },
      };
      adminGenerateVerificationLinkMock.mockRejectedValue(mockFirebaseError);
      expect(isFirebaseError(mockFirebaseError)).toBe(true);

      //WHEN
      const { body } = await mockApp
        .get("/users/verificationEmail")
        .set("Authorization", `Bearer ${uid}`)
        .expect(429);

      //THEN
      expect(body.message).toEqual("Too many requests. Please try again later");
    });
    it("should fail with firebase user not found", async () => {
      //GIVEN
      const mockFirebaseError = {
        code: "auth/user-not-found",
        codePrefix: "auth",
        errorInfo: {
          code: "auth/user-not-found",
          message: "User not found",
        },
      };
      adminGenerateVerificationLinkMock.mockRejectedValue(mockFirebaseError);
      expect(isFirebaseError(mockFirebaseError)).toBe(true);

      //WHEN
      const { body } = await mockApp
        .get("/users/verificationEmail")
        .set("Authorization", `Bearer ${uid}`)
        .expect(500);

      //THEN
      expect(body.message).toEqual(
        "Auth user not found when the user was found in the database. Contact support with this error message and your email\n" +
          'Stack: {"decodedTokenEmail":"newuser@mail.com","userInfoEmail":"newuser@mail.com"}',
      );
    });
    it("should fail with unknown error", async () => {
      //GIVEN
      const mockFirebaseError = {
        message: "Internal server error",
      };
      adminGenerateVerificationLinkMock.mockRejectedValue(mockFirebaseError);
      expect(isFirebaseError(mockFirebaseError)).toBe(false);

      //WHEN
      const { body } = await mockApp
        .get("/users/verificationEmail")
        .set("Authorization", `Bearer ${uid}`)
        .expect(500);

      //THEN
      expect(body.message).toEqual(
        "Failed to generate an email verification link: Internal server error",
      );
    });
  });
  describe("sendForgotPasswordEmail", () => {
    const sendForgotPasswordEmailMock = vi.spyOn(
      AuthUtils,
      "sendForgotPasswordEmail",
    );
    const verifyCaptchaMock = vi.spyOn(Captcha, "verify");

    beforeEach(() => {
      sendForgotPasswordEmailMock.mockClear().mockResolvedValue();
      verifyCaptchaMock.mockClear().mockResolvedValue(true);
    });

    it("should send forgot password email without authentication", async () => {
      //GIVEN

      //WHEN
      const { body } = await mockApp
        .post("/users/forgotPasswordEmail")
        .send({ email: "bob@example.com", captcha: "" });

      //THEN
      expect(body).toEqual({
        message:
          "Password reset request received. If the email is valid, you will receive an email shortly.",
        data: null,
      });

      expect(sendForgotPasswordEmailMock).toHaveBeenCalledWith(
        "bob@example.com",
      );
    });
    it("should fail without mandatory properties", async () => {
      //WHEN
      const { body } = await mockApp
        .post("/users/forgotPasswordEmail")
        .expect(422);

      //THEN
      expect(body).toEqual({
        message: "Invalid request data schema",
        validationErrors: ['"captcha" Required', '"email" Required'],
      });
    });
    it("should fail without unknown properties", async () => {
      //WHEN
      const { body } = await mockApp
        .post("/users/forgotPasswordEmail")
        .send({ email: "bob@example.com", extra: "value" })
        .expect(422);

      //THEN
      expect(body).toEqual({
        message: "Invalid request data schema",
        validationErrors: [
          '"captcha" Required',
          "Unrecognized key(s) in object: 'extra'",
        ],
      });
    });
  });
  describe("getTestActivity", () => {
    const getUserMock = vi.spyOn(UserDal, "getPartialUser");
    afterAll(() => {
      getUserMock.mockClear();
    });
    it("should return 503 for non premium users", async () => {
      //given
      getUserMock.mockResolvedValue({
        testActivity: { "2023": [1, 2, 3], "2024": [4, 5, 6] },
      } as Partial<UserDal.DBUser> as UserDal.DBUser);

      //when
      await mockApp
        .get("/users/testActivity")
        .set("Authorization", `Bearer ${uid}`)
        .send()
        .expect(503);
    });
    it("should send data for premium users", async () => {
      //given
      getUserMock.mockResolvedValue({
        testActivity: { "2023": [1, 2, 3], "2024": [4, 5, 6] },
      } as Partial<UserDal.DBUser> as UserDal.DBUser);
      vi.spyOn(UserDal, "checkIfUserIsPremium").mockResolvedValue(true);
      await enablePremiumFeatures(true);

      //when
      const response = await mockApp
        .get("/users/testActivity")
        .set("Authorization", `Bearer ${uid}`)
        .send()
        .expect(200);

      //%hen
      const result = response.body.data;
      expect(result["2023"]).toEqual([1, 2, 3]);
      expect(result["2024"]).toEqual([4, 5, 6]);
    });
  });

  describe("generateCurrentTestActivity", () => {
    beforeAll(() => {
      vi.useFakeTimers().setSystemTime(1712102400000);
    });
    it("without any data", () => {
      expect(generateCurrentTestActivity(undefined)).toBeUndefined();
    });
    it("with current year only", () => {
      //given
      const data = {
        "2024": fillYearWithDay(94).map((it) => 2024000 + it),
      };

      //when
      const testActivity = generateCurrentTestActivity(data);

      //then
      expect(testActivity?.lastDay).toEqual(1712102400000);

      const testsByDays = testActivity?.testsByDays ?? [];
      expect(testsByDays).toHaveLength(372);
      expect(testsByDays[6]).toEqual(undefined); //2023-04-04
      expect(testsByDays[277]).toEqual(undefined); //2023-12-31
      expect(testsByDays[278]).toEqual(2024001); //2024-01-01
      expect(testsByDays[371]).toEqual(2024094); //2024-01
    });
    it("with current and last year", () => {
      //given
      const data = {
        "2023": fillYearWithDay(365).map((it) => 2023000 + it),
        "2024": fillYearWithDay(94).map((it) => 2024000 + it),
      };

      //when
      const testActivity = generateCurrentTestActivity(data);

      //then
      expect(testActivity?.lastDay).toEqual(1712102400000);

      const testsByDays = testActivity?.testsByDays ?? [];
      expect(testsByDays).toHaveLength(372);
      expect(testsByDays[6]).toEqual(2023094); //2023-04-04
      expect(testsByDays[277]).toEqual(2023365); //2023-12-31
      expect(testsByDays[278]).toEqual(2024001); //2024-01-01
      expect(testsByDays[371]).toEqual(2024094); //2024-01
    });
    it("with current and missing days of last year", () => {
      //given
      const data = {
        "2023": fillYearWithDay(20).map((it) => 2023000 + it),
        "2024": fillYearWithDay(94).map((it) => 2024000 + it),
      };

      //when
      const testActivity = generateCurrentTestActivity(data);

      //then
      expect(testActivity?.lastDay).toEqual(1712102400000);

      const testsByDays = testActivity?.testsByDays ?? [];
      expect(testsByDays).toHaveLength(372);
      expect(testsByDays[6]).toEqual(undefined); //2023-04-04
      expect(testsByDays[277]).toEqual(undefined); //2023-12-31
      expect(testsByDays[278]).toEqual(2024001); //2024-01-01
      expect(testsByDays[371]).toEqual(2024094); //2024-01
    });
  });
  describe("delete user ", () => {
    const getUserMock = vi.spyOn(UserDal, "getPartialUser");
    const deleteUserMock = vi.spyOn(UserDal, "deleteUser");
    const firebaseDeleteUserMock = vi.spyOn(AuthUtils, "deleteUser");
    const deleteAllApeKeysMock = vi.spyOn(ApeKeysDal, "deleteAllApeKeys");
    const deleteAllPresetsMock = vi.spyOn(PresetDal, "deleteAllPresets");
    const deleteConfigMock = vi.spyOn(ConfigDal, "deleteConfig");
    const deleteAllResultMock = vi.spyOn(ResultDal, "deleteAll");
    const purgeUserFromDailyLeaderboardsMock = vi.spyOn(
      DailyLeaderboards,
      "purgeUserFromDailyLeaderboards",
    );
    const purgeUserFromXpLeaderboardsMock = vi.spyOn(
      WeeklyXpLeaderboard,
      "purgeUserFromXpLeaderboards",
    );
    const blocklistAddMock = vi.spyOn(BlocklistDal, "add");
    const connectionsDeletebyUidMock = vi.spyOn(ConnectionsDal, "deleteByUid");
    const logsDeleteUserMock = vi.spyOn(LogDal, "deleteUserLogs");

    beforeEach(() => {
      [
        firebaseDeleteUserMock,
        deleteUserMock,
        blocklistAddMock,
        deleteAllApeKeysMock,
        deleteAllPresetsMock,
        deleteConfigMock,
        purgeUserFromDailyLeaderboardsMock,
        purgeUserFromXpLeaderboardsMock,
        connectionsDeletebyUidMock,
        logsDeleteUserMock,
      ].forEach((it) => it.mockResolvedValue(undefined));

      deleteAllResultMock.mockResolvedValue({} as any);
    });

    afterEach(() => {
      [
        getUserMock,
        deleteUserMock,
        blocklistAddMock,
        firebaseDeleteUserMock,
        deleteConfigMock,
        deleteAllResultMock,
        deleteAllApeKeysMock,
        deleteAllPresetsMock,
        purgeUserFromDailyLeaderboardsMock,
        purgeUserFromXpLeaderboardsMock,
        connectionsDeletebyUidMock,
        logsDeleteUserMock,
      ].forEach((it) => it.mockClear());
    });

    it("should add user to blocklist if banned", async () => {
      //GIVEN
      const user = {
        uid,
        name: "name",
        email: "email",
        discordId: "discordId",
        banned: true,
      } as Partial<UserDal.DBUser> as UserDal.DBUser;
      await getUserMock.mockResolvedValue(user);

      //WHEN
      await mockApp
        .delete("/users/")
        .set("Authorization", `Bearer ${uid}`)
        .expect(200);

      //THEN
      expect(blocklistAddMock).toHaveBeenCalledWith(user);

      expect(deleteUserMock).toHaveBeenCalledWith(uid);
      expect(firebaseDeleteUserMock).toHaveBeenCalledWith(uid);
      expect(deleteAllApeKeysMock).toHaveBeenCalledWith(uid);
      expect(deleteAllPresetsMock).toHaveBeenCalledWith(uid);
      expect(deleteConfigMock).toHaveBeenCalledWith(uid);
      expect(deleteAllResultMock).toHaveBeenCalledWith(uid);
      expect(connectionsDeletebyUidMock).toHaveBeenCalledWith(uid);
      expect(purgeUserFromDailyLeaderboardsMock).toHaveBeenCalledWith(
        uid,
        (await configuration).dailyLeaderboards,
      );
      expect(purgeUserFromXpLeaderboardsMock).toHaveBeenCalledWith(
        uid,
        (await configuration).leaderboards.weeklyXp,
      );
      expect(logsDeleteUserMock).toHaveBeenCalledWith(uid);
    });
    it("should delete user without adding to blocklist if not banned", async () => {
      //GIVEN
      const user = {
        uid,
        name: "name",
        email: "email",
        discordId: "discordId",
      } as Partial<UserDal.DBUser> as UserDal.DBUser;
      getUserMock.mockResolvedValue(user);

      //WHEN
      await mockApp
        .delete("/users/")
        .set("Authorization", `Bearer ${uid}`)
        .expect(200);

      //THEN
      expect(blocklistAddMock).not.toHaveBeenCalled();

      expect(deleteUserMock).toHaveBeenCalledWith(uid);
      expect(firebaseDeleteUserMock).toHaveBeenCalledWith(uid);
      expect(deleteAllApeKeysMock).toHaveBeenCalledWith(uid);
      expect(deleteAllPresetsMock).toHaveBeenCalledWith(uid);
      expect(deleteConfigMock).toHaveBeenCalledWith(uid);
      expect(deleteAllResultMock).toHaveBeenCalledWith(uid);
      expect(connectionsDeletebyUidMock).toHaveBeenCalledWith(uid);
      expect(purgeUserFromDailyLeaderboardsMock).toHaveBeenCalledWith(
        uid,
        (await configuration).dailyLeaderboards,
      );
      expect(purgeUserFromXpLeaderboardsMock).toHaveBeenCalledWith(
        uid,
        (await configuration).leaderboards.weeklyXp,
      );
      expect(logsDeleteUserMock).toHaveBeenCalledWith(uid);
    });

    it("should not fail if userInfo cannot be found", async () => {
      //GIVEN
      getUserMock.mockRejectedValue(new MonkeyError(404, "user not found"));

      //WHEN
      await mockApp
        .delete("/users/")
        .set("Authorization", `Bearer ${uid}`)
        .expect(200);

      //THEN
      expect(blocklistAddMock).not.toHaveBeenCalled();

      expect(deleteUserMock).toHaveBeenCalledWith(uid);
      expect(firebaseDeleteUserMock).toHaveBeenCalledWith(uid);
      expect(deleteAllApeKeysMock).toHaveBeenCalledWith(uid);
      expect(deleteAllPresetsMock).toHaveBeenCalledWith(uid);
      expect(deleteConfigMock).toHaveBeenCalledWith(uid);
      expect(deleteAllResultMock).toHaveBeenCalledWith(uid);
      expect(connectionsDeletebyUidMock).toHaveBeenCalledWith(uid);
      expect(purgeUserFromDailyLeaderboardsMock).toHaveBeenCalledWith(
        uid,
        (await configuration).dailyLeaderboards,
      );
      expect(purgeUserFromXpLeaderboardsMock).toHaveBeenCalledWith(
        uid,
        (await configuration).leaderboards.weeklyXp,
      );
      expect(logsDeleteUserMock).toHaveBeenCalledWith(uid);
    });

    it("should fail for unknown error from UserDal", async () => {
      //GIVEN
      getUserMock.mockRejectedValue(new Error("oops"));

      //WHEN
      await mockApp
        .delete("/users/")
        .set("Authorization", `Bearer ${uid}`)
        .expect(500);

      //THEN
      expect(blocklistAddMock).not.toHaveBeenCalled();
      expect(deleteUserMock).not.toHaveBeenCalledWith(uid);
      expect(firebaseDeleteUserMock).not.toHaveBeenCalledWith(uid);
      expect(deleteAllApeKeysMock).not.toHaveBeenCalledWith(uid);
      expect(deleteAllPresetsMock).not.toHaveBeenCalledWith(uid);
      expect(deleteConfigMock).not.toHaveBeenCalledWith(uid);
      expect(deleteAllResultMock).not.toHaveBeenCalledWith(uid);
      expect(connectionsDeletebyUidMock).not.toHaveBeenCalledWith(uid);
      expect(purgeUserFromDailyLeaderboardsMock).not.toHaveBeenCalledWith(
        uid,
        (await configuration).dailyLeaderboards,
      );
      expect(purgeUserFromXpLeaderboardsMock).not.toHaveBeenCalledWith(
        uid,
        (await configuration).leaderboards.weeklyXp,
      );
      expect(logsDeleteUserMock).not.toHaveBeenCalled();
    });
    it("should not fail if firebase user cannot be found", async () => {
      //GIVEN
      const user = {
        uid,
        name: "name",
        email: "email",
        discordId: "discordId",
      } as Partial<UserDal.DBUser> as UserDal.DBUser;
      getUserMock.mockResolvedValue(user);
      firebaseDeleteUserMock.mockRejectedValue({
        code: "user-not-found",
        codePrefix: "auth",
        errorInfo: { code: "auth/user-not-found", message: "user not found" },
      });

      //WHEN
      await mockApp
        .delete("/users/")
        .set("Authorization", `Bearer ${uid}`)
        .expect(200);

      //THEN
      expect(blocklistAddMock).not.toHaveBeenCalled();

      expect(deleteUserMock).toHaveBeenCalledWith(uid);
      expect(firebaseDeleteUserMock).toHaveBeenCalledWith(uid);
      expect(deleteAllApeKeysMock).toHaveBeenCalledWith(uid);
      expect(deleteAllPresetsMock).toHaveBeenCalledWith(uid);
      expect(deleteConfigMock).toHaveBeenCalledWith(uid);
      expect(deleteAllResultMock).toHaveBeenCalledWith(uid);
      expect(connectionsDeletebyUidMock).toHaveBeenCalledWith(uid);
      expect(purgeUserFromDailyLeaderboardsMock).toHaveBeenCalledWith(
        uid,
        (await configuration).dailyLeaderboards,
      );
      expect(purgeUserFromXpLeaderboardsMock).toHaveBeenCalledWith(
        uid,
        (await configuration).leaderboards.weeklyXp,
      );
      expect(logsDeleteUserMock).toHaveBeenCalledWith(uid);
    });

    it("should fail for unknown error from firebase", async () => {
      //GIVEN
      const user = {
        uid,
        name: "name",
        email: "email",
        discordId: "discordId",
      } as Partial<UserDal.DBUser> as UserDal.DBUser;
      getUserMock.mockResolvedValue(user);
      firebaseDeleteUserMock.mockRejectedValue({
        code: "unknown",
        codePrefix: "auth",
        errorInfo: { code: "auth/unknown", message: "unknown" },
      });

      //WHEN
      await mockApp
        .delete("/users/")
        .set("Authorization", `Bearer ${uid}`)
        .expect(500);

      //THEN
      expect(blocklistAddMock).not.toHaveBeenCalled();
      expect(deleteUserMock).toHaveBeenCalledWith(uid);
      expect(firebaseDeleteUserMock).toHaveBeenCalledWith(uid);
      expect(deleteAllApeKeysMock).toHaveBeenCalledWith(uid);
      expect(deleteAllPresetsMock).toHaveBeenCalledWith(uid);
      expect(deleteConfigMock).toHaveBeenCalledWith(uid);
      expect(deleteAllResultMock).toHaveBeenCalledWith(uid);
      expect(connectionsDeletebyUidMock).toHaveBeenCalledWith(uid);
      expect(purgeUserFromDailyLeaderboardsMock).toHaveBeenCalledWith(
        uid,
        (await configuration).dailyLeaderboards,
      );
      expect(purgeUserFromXpLeaderboardsMock).toHaveBeenCalledWith(
        uid,
        (await configuration).leaderboards.weeklyXp,
      );
    });
  });
  describe("resetUser", () => {
    const getPartialUserMock = vi.spyOn(UserDal, "getPartialUser");
    const resetUserMock = vi.spyOn(UserDal, "resetUser");
    const deleteAllApeKeysMock = vi.spyOn(ApeKeysDal, "deleteAllApeKeys");
    const deleteAllPresetsMock = vi.spyOn(PresetDal, "deleteAllPresets");
    const deleteAllResultsMock = vi.spyOn(ResultDal, "deleteAll");
    const deleteConfigMock = vi.spyOn(ConfigDal, "deleteConfig");
    const purgeUserFromDailyLeaderboardsMock = vi.spyOn(
      DailyLeaderboards,
      "purgeUserFromDailyLeaderboards",
    );
    const purgeUserFromXpLeaderboardsMock = vi.spyOn(
      WeeklyXpLeaderboard,
      "purgeUserFromXpLeaderboards",
    );

    const unlinkDiscordMock = vi.spyOn(GeorgeQueue, "unlinkDiscord");
    const addImportantLogMock = vi.spyOn(LogDal, "addImportantLog");

    beforeEach(() => {
      getPartialUserMock.mockClear().mockResolvedValue({
        banned: false,
        name: "bob",
        email: "bob@example.com",
      } as any);
      deleteAllResultsMock.mockClear().mockResolvedValue(null as any);
      [
        purgeUserFromXpLeaderboardsMock,
        unlinkDiscordMock,
        addImportantLogMock,
        resetUserMock,
        deleteAllApeKeysMock,
        deleteAllPresetsMock,
        deleteConfigMock,
        purgeUserFromDailyLeaderboardsMock,
      ].forEach((it) => it.mockClear().mockResolvedValue());
    });

    it("should reset user", async () => {
      //GIVEN

      //WHEN
      const { body } = await mockApp
        .patch("/users/reset")
        .set("Authorization", `Bearer ${uid}`)
        .expect(200);

      //THEN
      expect(body).toEqual({
        message: "User reset",
        data: null,
      });

      for (const it of [
        resetUserMock,
        deleteAllApeKeysMock,
        deleteAllPresetsMock,
        deleteAllResultsMock,
        deleteConfigMock,
      ]) {
        expect(it).toHaveBeenCalledWith(uid);
      }
      expect(purgeUserFromDailyLeaderboardsMock).toHaveBeenCalledWith(
        uid,
        (await Configuration.getLiveConfiguration()).dailyLeaderboards,
      );
      expect(purgeUserFromXpLeaderboardsMock).toHaveBeenCalledWith(
        uid,
        (await configuration).leaderboards.weeklyXp,
      );
      expect(unlinkDiscordMock).not.toHaveBeenCalled();
      /*TODO
      expect(addImportantLogMock).toHaveBeenCalledWith(
        "user_reset",
        "bob@example.com bob",
        uid
      );*/
    });
    it("should unlink discord", async () => {
      //GIVEN
      getPartialUserMock.mockResolvedValue({ discordId: "discordId" } as any);

      //WHEN
      await mockApp
        .patch("/users/reset")
        .set("Authorization", `Bearer ${uid}`)
        .expect(200);

      //THEN
      //TODO
      //expect(unlinkDiscordMock).toHaveBeenCalledWith("discordId", uid);
    });
    it("should fail resetting a banned user", async () => {
      //GIVEN
      getPartialUserMock.mockResolvedValue({ banned: true } as any);

      //WHEN
      const { body } = await mockApp
        .patch("/users/reset")
        .set("Authorization", `Bearer ${uid}`)
        .expect(403);

      //THEN
      expect(body.message).toEqual("Banned users cannot reset their account");
    });
  });
  describe("update name", () => {
    const blocklistContainsMock = vi.spyOn(BlocklistDal, "contains");
    const getPartialUserMock = vi.spyOn(UserDal, "getPartialUser");
    const updateNameMock = vi.spyOn(UserDal, "updateName");
    const connectionsUpdateNameMock = vi.spyOn(ConnectionsDal, "updateName");
    const addImportantLogMock = vi.spyOn(LogDal, "addImportantLog");

    beforeEach(() => {
      [
        blocklistContainsMock,
        getPartialUserMock,
        updateNameMock,
        connectionsUpdateNameMock,
        addImportantLogMock,
      ].forEach((it) => {
        it.mockClear().mockResolvedValue(null as never);
      });
    });

    it("should update the username", async () => {
      //GIVEN
      getPartialUserMock.mockResolvedValue({
        name: "Bob",
        lastNameChange: 1000,
      } as any);
      //WHEN
      const { body } = await mockApp
        .patch("/users/name")
        .set("Authorization", `Bearer ${uid}`)
        .send({ name: "newName" })
        .expect(200);

      //THEN
      expect(body).toEqual({
        message: "User's name updated",
        data: null,
      });

      expect(updateNameMock).toHaveBeenCalledWith(uid, "newName", "Bob");
      expect(addImportantLogMock).toHaveBeenCalledWith(
        "user_name_updated",
        "changed name from Bob to newName",
        uid,
      );
      expect(connectionsUpdateNameMock).toHaveBeenCalledWith(uid, "newName");
    });

    it("should fail if username is blocked", async () => {
      //GIVEN
      blocklistContainsMock.mockResolvedValue(true);

      //WHEN
      const { body } = await mockApp
        .patch("/users/name")
        .set("Authorization", `Bearer ${uid}`)
        .send({ name: "newName" })
        .expect(409);

      //THEN
      expect(body.message).toEqual("Username blocked");
      expect(updateNameMock).not.toHaveBeenCalled();
      expect(connectionsUpdateNameMock).not.toHaveBeenCalled();
    });

    it("should fail for banned users", async () => {
      //GIVEN
      getPartialUserMock.mockResolvedValue({ banned: true } as any);

      //WHEN
      const { body } = await mockApp
        .patch("/users/name")
        .set("Authorization", `Bearer ${uid}`)
        .send({ name: "newName" })
        .expect(403);

      //THEN
      expect(body.message).toEqual("Banned users cannot change their name");
      expect(updateNameMock).not.toHaveBeenCalled();
    });
    it("should fail changing name within last 30 days", async () => {
      //GIVEN
      getPartialUserMock.mockResolvedValue({
        lastNameChange: Date.now().valueOf() - 60_000,
      } as any);

      //WHEN
      const { body } = await mockApp
        .patch("/users/name")
        .set("Authorization", `Bearer ${uid}`)
        .send({ name: "newName" })
        .expect(409);

      //THEN
      expect(body.message).toEqual(
        "You can change your name once every 30 days",
      );
      expect(updateNameMock).not.toHaveBeenCalled();
    });
    it("should update the username within 30 days if user needs to change", async () => {
      //GIVEN
      getPartialUserMock.mockResolvedValue({
        name: "Bob",
        lastNameChange: Date.now().valueOf() - 60_000,
        needsToChangeName: true,
      } as any);
      //WHEN
      const { body } = await mockApp
        .patch("/users/name")
        .set("Authorization", `Bearer ${uid}`)
        .send({ name: "newName" })
        .expect(200);

      //THEN
      expect(body).toEqual({
        message: "User's name updated",
        data: null,
      });

      expect(updateNameMock).toHaveBeenCalledWith(uid, "newName", "Bob");
    });
    it("should fail without mandatory properties", async () => {
      //WHEN
      const { body } = await mockApp
        .patch("/users/name")
        .set("Authorization", `Bearer ${uid}`)
        .expect(422);

      //THEN
      expect(body).toEqual({
        message: "Invalid request data schema",
        validationErrors: ['"name" Required'],
      });
    });
    it("should fail without unknown properties", async () => {
      //WHEN
      const { body } = await mockApp
        .patch("/users/name")
        .set("Authorization", `Bearer ${uid}`)
        .send({ name: "newName", extra: "value" })
        .expect(422);

      //THEN
      expect(body).toEqual({
        message: "Invalid request data schema",
        validationErrors: ["Unrecognized key(s) in object: 'extra'"],
      });
    });
    it("should fail if username contains disallowed word", async () => {
      //WHEN
      const { body } = await mockApp
        .patch("/users/name")
        .set("Authorization", `Bearer ${uid}`)
        .send({ name: "miodec" })
        .expect(422);

      //THEN
      expect(body).toEqual({
        message: "Invalid request data schema",
        validationErrors: [
          '"name" Disallowed word detected. Please remove it. If you believe this is a mistake, please contact us (miodec).',
        ],
      });
    });
  });
  describe("clear PBs", () => {
    const clearPbMock = vi.spyOn(UserDal, "clearPb");
    const purgeUserFromDailyLeaderboardsMock = vi.spyOn(
      DailyLeaderboards,
      "purgeUserFromDailyLeaderboards",
    );
    const addImportantLogMock = vi.spyOn(LogDal, "addImportantLog");

    beforeEach(() => {
      [
        clearPbMock,
        purgeUserFromDailyLeaderboardsMock,
        addImportantLogMock,
      ].forEach((it) => it.mockClear().mockResolvedValue());
    });

    it("should clear pb", async () => {
      //GIVEN

      //WHEN
      const { body } = await mockApp
        .delete("/users/personalBests")
        .set("Authorization", `Bearer ${uid}`)
        .expect(200);

      //THEN
      expect(body).toEqual({
        message: "User's PB cleared",
        data: null,
      });
      expect(clearPbMock).toHaveBeenCalledWith(uid);
      expect(purgeUserFromDailyLeaderboardsMock).toHaveBeenCalledWith(
        uid,
        (await Configuration.getLiveConfiguration()).dailyLeaderboards,
      );
      expect(addImportantLogMock).toHaveBeenCalledWith(
        "user_cleared_pbs",
        "",
        uid,
      );
    });
  });
  describe("opt out of leaderboard", () => {
    const optOutOfLeaderboardsMock = vi.spyOn(UserDal, "optOutOfLeaderboards");
    const purgeUserFromDailyLeaderboardsMock = vi.spyOn(
      DailyLeaderboards,
      "purgeUserFromDailyLeaderboards",
    );
    const addImportantLogMock = vi.spyOn(LogDal, "addImportantLog");

    beforeEach(() => {
      [
        optOutOfLeaderboardsMock.mockClear(),
        purgeUserFromDailyLeaderboardsMock,
        addImportantLogMock,
      ].forEach((it) => it.mockClear().mockResolvedValue());
    });
    it("should opt out", async () => {
      //GIVEN

      //WHEN
      const { body } = await mockApp
        .post("/users/optOutOfLeaderboards")
        .set("Authorization", `Bearer ${uid}`)
        .expect(200);

      //THEN
      expect(body).toEqual({
        message: "User opted out of leaderboards",
        data: null,
      });

      expect(optOutOfLeaderboardsMock).toHaveBeenCalledWith(uid);
      expect(purgeUserFromDailyLeaderboardsMock).toHaveBeenCalledWith(
        uid,
        (await Configuration.getLiveConfiguration()).dailyLeaderboards,
      );
      expect(addImportantLogMock).toHaveBeenCalledWith(
        "user_opted_out_of_leaderboards",
        "",
        uid,
      );
    });
    // it("should fail with unknown properties", async () => {
    //WHEN
    // const { body } = await mockApp
    //   .post("/users/optOutOfLeaderboards")
    //   .set("Authorization", `Bearer ${uid}`)
    //   .send({ extra: "value" });
    //TODO.expect(422);
    //THEN
    /* TODO:
        expect(body).toEqual({});
        */
    // });
  });
  describe("update email", () => {
    const authUpdateEmailMock = vi.spyOn(AuthUtils, "updateUserEmail");
    const userUpdateEmailMock = vi.spyOn(UserDal, "updateEmail");
    const addImportantLogMock = vi.spyOn(LogDal, "addImportantLog");

    beforeEach(() => {
      [authUpdateEmailMock, userUpdateEmailMock, addImportantLogMock].forEach(
        (it) => it.mockClear().mockResolvedValue(null as never),
      );
    });
    it("should update users email", async () => {
      //GIVEN
      const newEmail = "newEmail@example.com";

      //WHEN
      const { body } = await mockApp
        .patch("/users/email")
        .set("Authorization", `Bearer ${uid}`)
        .send({ newEmail, previousEmail: "previousEmail@example.com" })
        .expect(200);

      //THEN
      expect(body).toEqual({
        message: "Email updated",
        data: null,
      });

      expect(authUpdateEmailMock).toHaveBeenCalledWith(
        uid,
        newEmail.toLowerCase(),
      );
      expect(userUpdateEmailMock).toHaveBeenCalledWith(
        uid,
        newEmail.toLowerCase(),
      );
      expect(addImportantLogMock).toHaveBeenCalledWith(
        "user_email_updated",
        "changed email from previousemail@example.com to newemail@example.com",
        uid,
      );
    });
    it("should fail for duplicate email", async () => {
      //GIVEN
      const mockFirebaseError = {
        code: "auth/email-already-exists",
        codePrefix: "auth",
        errorInfo: {
          code: "auth/email-already-exists",
          message: "Email already exists",
        },
      };
      authUpdateEmailMock.mockRejectedValue(mockFirebaseError);
      expect(isFirebaseError(mockFirebaseError)).toBe(true);

      //WHEN
      const { body } = await mockApp
        .patch("/users/email")
        .set("Authorization", `Bearer ${uid}`)
        .send({
          newEmail: "newEmail@example.com",
          previousEmail: "previousEmail@example.com",
        })
        .expect(409);

      expect(body.message).toEqual(
        "The email address is already in use by another account",
      );

      expect(userUpdateEmailMock).not.toHaveBeenCalled();
    });

    it("should fail for invalid email", async () => {
      //GIVEN
      const mockFirebaseError = {
        code: "auth/invalid-email",
        codePrefix: "auth",
        errorInfo: {
          code: "auth/invalid-email",
          message: "Invalid email",
        },
      };
      authUpdateEmailMock.mockRejectedValue(mockFirebaseError);
      expect(isFirebaseError(mockFirebaseError)).toBe(true);

      //WHEN
      const { body } = await mockApp
        .patch("/users/email")
        .set("Authorization", `Bearer ${uid}`)
        .send({
          newEmail: "newEmail@example.com",
          previousEmail: "previousEmail@example.com",
        })
        .expect(400);

      expect(body.message).toEqual("Invalid email address");

      expect(userUpdateEmailMock).not.toHaveBeenCalled();
    });
    it("should fail for too many requests", async () => {
      //GIVEN
      const mockFirebaseError = {
        code: "auth/too-many-requests",
        codePrefix: "auth",
        errorInfo: {
          code: "auth/too-many-requests",
          message: "Too many requests",
        },
      };
      authUpdateEmailMock.mockRejectedValue(mockFirebaseError);
      expect(isFirebaseError(mockFirebaseError)).toBe(true);

      //WHEN
      const { body } = await mockApp
        .patch("/users/email")
        .set("Authorization", `Bearer ${uid}`)
        .send({
          newEmail: "newEmail@example.com",
          previousEmail: "previousEmail@example.com",
        })
        .expect(429);

      expect(body.message).toEqual("Too many requests. Please try again later");

      expect(userUpdateEmailMock).not.toHaveBeenCalled();
    });
    it("should fail for unknown user", async () => {
      //GIVEN
      const mockFirebaseError = {
        code: "auth/user-not-found",
        codePrefix: "auth",
        errorInfo: {
          code: "auth/user-not-found",
          message: "User not found",
        },
      };
      authUpdateEmailMock.mockRejectedValue(mockFirebaseError);
      expect(isFirebaseError(mockFirebaseError)).toBe(true);

      //WHEN
      const { body } = await mockApp
        .patch("/users/email")
        .set("Authorization", `Bearer ${uid}`)
        .send({
          newEmail: "newEmail@example.com",
          previousEmail: "previousEmail@example.com",
        })
        .expect(404);

      expect(body.message).toEqual(
        "User not found in the auth system\nStack: update email",
      );

      expect(userUpdateEmailMock).not.toHaveBeenCalled();
    });
    it("should fail for invalid user token", async () => {
      //GIVEN
      authUpdateEmailMock.mockRejectedValue({
        code: "auth/invalid-user-token",
        codePrefix: "auth",
        errorInfo: {
          code: "auth/invalid-user-token",
          message: "Invalid user token",
        },
      });

      //WHEN
      const { body } = await mockApp
        .patch("/users/email")
        .set("Authorization", `Bearer ${uid}`)
        .send({
          newEmail: "newEmail@example.com",
          previousEmail: "previousEmail@example.com",
        })
        .expect(401);

      expect(body.message).toEqual("Invalid user token\nStack: update email");

      expect(userUpdateEmailMock).not.toHaveBeenCalled();
    });
    it("should fail for unknown error", async () => {
      //GIVEN
      authUpdateEmailMock.mockRejectedValue({} as FirebaseError);

      //WHEN
      await mockApp
        .patch("/users/email")
        .set("Authorization", `Bearer ${uid}`)
        .send({
          newEmail: "newEmail@example.com",
          previousEmail: "previousEmail@example.com",
        })
        .expect(500);
    });
    it("should fail without mandatory properties", async () => {
      //WHEN
      const { body } = await mockApp
        .patch("/users/email")
        .set("Authorization", `Bearer ${uid}`)
        .expect(422);

      expect(body).toEqual({
        message: "Invalid request data schema",
        validationErrors: ['"newEmail" Required', '"previousEmail" Required'],
      });
    });
    it("should fail with unknown properties", async () => {
      //WHEN
      const { body } = await mockApp
        .patch("/users/email")
        .set("Authorization", `Bearer ${uid}`)
        .send({
          newEmail: "newEmail@example.com",
          previousEmail: "previousEmail@example.com",
          extra: "value",
        })
        .expect(422);

      expect(body).toEqual({
        message: "Invalid request data schema",
        validationErrors: ["Unrecognized key(s) in object: 'extra'"],
      });
    });
  });
  describe("update password", () => {
    const updatePasswordMock = vi.spyOn(AuthUtils, "updateUserPassword");

    beforeEach(() => {
      updatePasswordMock.mockClear().mockResolvedValue(null as never);
    });

    it("should update password", async () => {
      //WHEN
      const { body } = await mockApp
        .patch("/users/password")
        .set("Authorization", `Bearer ${uid}`)
        .send({ newPassword: "sw0rdf1sh" })
        .expect(200);

      //THEN
      expect(body).toEqual({
        message: "Password updated",
        data: null,
      });
      expect(updatePasswordMock).toHaveBeenCalledWith(uid, "sw0rdf1sh");
    });
    it("should fail without mandatory properties", async () => {
      //WHEN
      const { body } = await mockApp
        .patch("/users/password")
        .set("Authorization", `Bearer ${uid}`)
        .expect(422);

      //THEN
      expect(body).toEqual({
        message: "Invalid request data schema",
        validationErrors: ['"newPassword" Required'],
      });
    });
    it("should fail with unknown properties", async () => {
      //WHEN
      const { body } = await mockApp
        .patch("/users/password")
        .set("Authorization", `Bearer ${uid}`)
        .send({ newPassword: "sw0rdf1sh", extra: "value" })
        .expect(422);

      //THEN
      expect(body).toEqual({
        message: "Invalid request data schema",
        validationErrors: ["Unrecognized key(s) in object: 'extra'"],
      });
    });
    it("should fail with password too short", async () => {
      //WHEN
      const { body } = await mockApp
        .patch("/users/password")
        .set("Authorization", `Bearer ${uid}`)
        .send({ newPassword: "test" })
        .expect(422);

      //THEN
      expect(body).toEqual({
        message: "Invalid request data schema",
        validationErrors: [
          '"newPassword" String must contain at least 6 character(s)',
        ],
      });
    });
  });
  describe("get oauth link", () => {
    const getOauthLinkMock = vi.spyOn(DiscordUtils, "getOauthLink");
    const url = "http://example.com:1234?test";
    beforeEach(() => {
      enableDiscordIntegration(true);
      getOauthLinkMock.mockClear().mockResolvedValue(url);
    });

    it("should get oauth link", async () => {
      //WHEN
      const { body } = await mockApp
        .get("/users/discord/oauth")
        .set("Authorization", `Bearer ${uid}`)
        .expect(200);

      //THEN
      expect(body).toEqual({
        message: "Discord oauth link generated",
        data: { url },
      });
      expect(getOauthLinkMock).toHaveBeenCalledWith(uid);
    });
    it("should fail if feature is not enabled", async () => {
      //GIVEN
      enableDiscordIntegration(false);

      //WHEN
      const { body } = await mockApp
        .get("/users/discord/oauth")
        .set("Authorization", `Bearer ${uid}`)
        .expect(503);

      //THEN
      expect(body.message).toEqual(
        "Discord integration is not available at this time",
      );
    });
  });
  describe("link discord", () => {
    const getUserMock = vi.spyOn(UserDal, "getPartialUser");
    const isDiscordIdAvailableMock = vi.spyOn(UserDal, "isDiscordIdAvailable");
    const isStateValidForUserMock = vi.spyOn(
      DiscordUtils,
      "iStateValidForUser",
    );
    const getDiscordUserMock = vi.spyOn(DiscordUtils, "getDiscordUser");
    const blocklistContainsMock = vi.spyOn(BlocklistDal, "contains");
    const userLinkDiscordMock = vi.spyOn(UserDal, "linkDiscord");
    const georgeLinkDiscordMock = vi.spyOn(GeorgeQueue, "linkDiscord");
    const addImportantLogMock = vi.spyOn(LogDal, "addImportantLog");

    beforeEach(async () => {
      isStateValidForUserMock.mockResolvedValue(true);
      getUserMock.mockResolvedValue({} as any);
      getDiscordUserMock.mockResolvedValue({
        id: "discordUserId",
        avatar: "discordUserAvatar",
      });
      isDiscordIdAvailableMock.mockResolvedValue(true);
      blocklistContainsMock.mockResolvedValue(false);
      userLinkDiscordMock.mockResolvedValue();
      await enableDiscordIntegration(true);
    });
    afterEach(() => {
      [
        getUserMock,
        isStateValidForUserMock,
        isDiscordIdAvailableMock,
        getDiscordUserMock,
        blocklistContainsMock,
        userLinkDiscordMock,
        georgeLinkDiscordMock,
        addImportantLogMock,
      ].forEach((it) => it.mockClear());
    });

    it("should link discord", async () => {
      //GIVEN
      getUserMock.mockResolvedValue({} as any);

      //WHEN
      const { body } = await mockApp
        .post("/users/discord/link")
        .set("Authorization", `Bearer ${uid}`)
        .send({
          tokenType: "tokenType",
          accessToken: "accessToken",
          state: "statestatestatestate",
        })
        .expect(200);

      //THEN
      expect(body).toEqual({
        message: "Discord account linked",
        data: {
          discordId: "discordUserId",
          discordAvatar: "discordUserAvatar",
        },
      });
      expect(isStateValidForUserMock).toHaveBeenCalledWith(
        "statestatestatestate",
        uid,
      );
      expect(getUserMock).toHaveBeenCalledWith(
        uid,
        "link discord",
        expect.any(Array),
      );
      expect(getDiscordUserMock).toHaveBeenCalledWith(
        "tokenType",
        "accessToken",
      );
      expect(isDiscordIdAvailableMock).toHaveBeenCalledWith("discordUserId");
      expect(blocklistContainsMock).toHaveBeenCalledWith({
        discordId: "discordUserId",
      });
      expect(userLinkDiscordMock).toHaveBeenCalledWith(
        uid,
        "discordUserId",
        "discordUserAvatar",
      );
      expect(georgeLinkDiscordMock).toHaveBeenCalledWith(
        "discordUserId",
        uid,
        false,
      );
      expect(addImportantLogMock).toHaveBeenCalledWith(
        "user_discord_link",
        "linked to discordUserId",
        uid,
      );
    });

    it("should update existing discord avatar", async () => {
      //GIVEN
      getUserMock.mockResolvedValue({ discordId: "existingDiscordId" } as any);

      //WHEN
      const { body } = await mockApp
        .post("/users/discord/link")
        .set("Authorization", `Bearer ${uid}`)
        .send({
          tokenType: "tokenType",
          accessToken: "accessToken",
          state: "statestatestatestate",
        })
        .expect(200);

      //THEN
      expect(body).toEqual({
        message: "Discord avatar updated",
        data: {
          discordId: "discordUserId",
          discordAvatar: "discordUserAvatar",
        },
      });
      expect(userLinkDiscordMock).toHaveBeenCalledWith(
        uid,
        "existingDiscordId",
        "discordUserAvatar",
      );
      expect(isDiscordIdAvailableMock).not.toHaveBeenCalled();
      expect(blocklistContainsMock).not.toHaveBeenCalled();
      expect(georgeLinkDiscordMock).not.toHaveBeenCalled();
      expect(addImportantLogMock).not.toHaveBeenCalled();
    });
    it("should fail for user mismatch", async () => {
      //GIVEN
      isStateValidForUserMock.mockResolvedValue(false);

      //WHEN
      const { body } = await mockApp
        .post("/users/discord/link")
        .set("Authorization", `Bearer ${uid}`)
        .send({
          tokenType: "tokenType",
          accessToken: "accessToken",
          state: "statestatestatestate",
        })
        .expect(403);

      //THEN
      expect(body.message).toEqual("Invalid user token");
    });
    it("should fail for banned users", async () => {
      //GIVEN
      getUserMock.mockResolvedValue({ banned: true } as any);

      //WHEN
      const { body } = await mockApp
        .post("/users/discord/link")
        .set("Authorization", `Bearer ${uid}`)
        .send({
          tokenType: "tokenType",
          accessToken: "accessToken",
          state: "statestatestatestate",
        })
        .expect(403);

      //THEN
      expect(body.message).toEqual("Banned accounts cannot link with Discord");
    });
    it("should fail for unknown discordId", async () => {
      //GIVEN
      getDiscordUserMock.mockResolvedValue({} as any);

      //WHEN
      const { body } = await mockApp
        .post("/users/discord/link")
        .set("Authorization", `Bearer ${uid}`)
        .send({
          tokenType: "tokenType",
          accessToken: "accessToken",
          state: "statestatestatestate",
        })
        .expect(500);

      //THEN
      expect(body.message).toEqual(
        "Could not get Discord account info\nStack: discord id is undefined",
      );

      //THEN
      expect(userLinkDiscordMock).not.toHaveBeenCalled();
    });
    it("should fail for already linked discordId", async () => {
      //GIVEN
      isDiscordIdAvailableMock.mockResolvedValue(false);

      //WHEN
      const { body } = await mockApp
        .post("/users/discord/link")
        .set("Authorization", `Bearer ${uid}`)
        .send({
          tokenType: "tokenType",
          accessToken: "accessToken",
          state: "statestatestatestate",
        })
        .expect(409);

      //THEN
      expect(body.message).toEqual(
        "This Discord account is linked to a different account",
      );

      //THEN
      expect(userLinkDiscordMock).not.toHaveBeenCalled();
    });

    it("should fail if discordId is blocked", async () => {
      //GIVEN
      const user = {
        uid,
        name: "name",
        email: "email",
      } as Partial<UserDal.DBUser> as UserDal.DBUser;
      getUserMock.mockResolvedValue(user);
      blocklistContainsMock.mockResolvedValue(true);

      //WHEN
      const result = await mockApp
        .post("/users/discord/link")
        .set("Authorization", `Bearer ${uid}`)
        .send({
          tokenType: "tokenType",
          accessToken: "accessToken",
          state: "statestatestatestate",
        })
        .expect(409);

      //THEN
      expect(result.body.message).toEqual("The Discord account is blocked");

      expect(blocklistContainsMock).toHaveBeenCalledWith({
        discordId: "discordUserId",
      });
    });
    it("should fail without mandatory properties", async () => {
      //WHEN
      const { body } = await mockApp
        .post("/users/discord/link")
        .set("Authorization", `Bearer ${uid}`)
        .expect(422);

      //THEN
      expect(body).toEqual({
        message: "Invalid request data schema",
        validationErrors: [
          '"tokenType" Required',
          '"accessToken" Required',
          '"state" Required',
        ],
      });
    });
    it("should fail with unknown properties", async () => {
      //WHEN
      const { body } = await mockApp
        .post("/users/discord/link")
        .set("Authorization", `Bearer ${uid}`)
        .send({
          tokenType: "tokenType",
          accessToken: "accessToken",
          state: "statestatestatestate",
          extra: "value",
        })
        .expect(422);

      //THEN
      expect(body).toEqual({
        message: "Invalid request data schema",
        validationErrors: ["Unrecognized key(s) in object: 'extra'"],
      });
    });
  });
  describe("unlink discord", () => {
    const getPartialUserMock = vi.spyOn(UserDal, "getPartialUser");
    const userUnlinkDiscordMock = vi.spyOn(UserDal, "unlinkDiscord");
    const georgeUnlinkDiscordMock = vi.spyOn(GeorgeQueue, "unlinkDiscord");
    const addImportantLogMock = vi.spyOn(LogDal, "addImportantLog");

    beforeEach(() => {
      getPartialUserMock
        .mockClear()
        .mockResolvedValue({ discordId: "discordId" } as any);
      [
        userUnlinkDiscordMock,
        georgeUnlinkDiscordMock,
        addImportantLogMock,
      ].forEach((it) => it.mockClear().mockResolvedValue());
    });

    it("should unlink", async () => {
      //GIVEN

      //WHEN
      const { body } = await mockApp
        .post("/users/discord/unlink")
        .set("Authorization", `Bearer ${uid}`)
        .expect(200);

      //THEN
      expect(body).toEqual({
        message: "Discord account unlinked",
        data: null,
      });

      expect(userUnlinkDiscordMock).toHaveBeenCalledWith(uid);
      expect(georgeUnlinkDiscordMock).toHaveBeenCalledWith("discordId", uid);
      expect(addImportantLogMock).toHaveBeenCalledWith(
        "user_discord_unlinked",
        "discordId",
        uid,
      );
    });
    it("should fail for banned user", async () => {
      //GIVEN
      getPartialUserMock.mockResolvedValue({ banned: true } as any);

      //WHEN

      const { body } = await mockApp
        .post("/users/discord/unlink")
        .set("Authorization", `Bearer ${uid}`)
        .expect(403);

      //THEN
      expect(body.message).toEqual("Banned accounts cannot unlink Discord");
      expect(userUnlinkDiscordMock).not.toHaveBeenCalled();
      expect(georgeUnlinkDiscordMock).not.toHaveBeenCalled();
    });
    it("should fail for user without discord linked", async () => {
      //GIVEN
      getPartialUserMock.mockResolvedValue({ discordId: undefined } as any);

      //WHEN

      const { body } = await mockApp
        .post("/users/discord/unlink")
        .set("Authorization", `Bearer ${uid}`)
        .expect(404);

      //THEN
      expect(body.message).toEqual(
        "User does not have a linked Discord account",
      );
      expect(userUnlinkDiscordMock).not.toHaveBeenCalled();
      expect(georgeUnlinkDiscordMock).not.toHaveBeenCalled();
    });
  });
  describe("add result filter preset", () => {
    const validPreset = {
      _id: "66c61b7a2a65715e66a0cc95",
      name: "newPreset",
      pb: { no: true, yes: true },
      difficulty: { normal: true, expert: false, master: false },
      mode: {
        words: false,
        time: false,
        quote: true,
        zen: false,
        custom: false,
      },
      words: {
        "10": false,
        "25": false,
        "50": false,
        "100": false,
        custom: false,
      },
      time: {
        "15": false,
        "30": false,
        "60": false,
        "120": false,
        custom: false,
      },
      quoteLength: {
        short: false,
        medium: false,
        long: false,
        thicc: false,
      },
      punctuation: {
        on: false,
        off: true,
      },
      numbers: {
        on: false,
        off: true,
      },
      date: {
        last_day: false,
        last_week: false,
        last_month: false,
        last_3months: false,
        all: true,
      },
      tags: {
        none: false,
      },
      language: {
        english: true,
      },
      funbox: {
        none: true,
      },
    };
    const generatedId = new ObjectId();

    const addResultFilterPresetMock = vi.spyOn(
      UserDal,
      "addResultFilterPreset",
    );

    beforeEach(async () => {
      addResultFilterPresetMock.mockClear().mockResolvedValue(generatedId);
      await enableResultFilterPresets(true);
    });
    it("should add", async () => {
      //GIVEN

      //WHEN
      const { body } = await mockApp
        .post("/users/resultFilterPresets")
        .set("Authorization", `Bearer ${uid}`)
        .send(validPreset)
        .expect(200);

      //THEN
      expect(body).toEqual({
        message: "Result filter preset created",
        data: generatedId.toHexString(),
      });

      expect(addResultFilterPresetMock).toHaveBeenCalledWith(
        uid,
        validPreset,
        (await Configuration.getLiveConfiguration()).results.filterPresets
          .maxPresetsPerUser,
      );
    });
    it("should fail without mandatory properties", async () => {
      //WHEN
      const { body } = await mockApp
        .post("/users/resultFilterPresets")
        .set("Authorization", `Bearer ${uid}`)
        .expect(422);

      //THEN
      expect(body).toEqual({
        message: "Invalid request data schema",
        validationErrors: [
          '"_id" Required',
          '"name" Required',
          '"pb" Required',
          '"difficulty" Required',
          '"mode" Required',
          '"words" Required',
          '"time" Required',
          '"quoteLength" Required',
          '"punctuation" Required',
          '"numbers" Required',
          '"date" Required',
          '"tags" Required',
          '"language" Required',
          '"funbox" Required',
        ],
      });
    });
    it("should fail with unknown properties", async () => {
      //WHEN
      const { body } = await mockApp
        .post("/users/resultFilterPresets")
        .set("Authorization", `Bearer ${uid}`)
        .send({ ...validPreset, extra: "value" })
        .expect(422);

      //THEN
      expect(body).toEqual({
        message: "Invalid request data schema",
        validationErrors: ["Unrecognized key(s) in object: 'extra'"],
      });
    });
    it("should fail if feature is disabled", async () => {
      //GIVEN
      enableResultFilterPresets(false);
      //WHEN
      const { body } = await mockApp
        .post("/users/resultFilterPresets")
        .set("Authorization", `Bearer ${uid}`)
        .send({ validPreset })
        .expect(503);

      //THEN
      expect(body.message).toEqual(
        "Result filter presets are not available at this time.",
      );
    });
  });
  describe("remove result filter preset", () => {
    const removeResultFilterPresetMock = vi.spyOn(
      UserDal,
      "removeResultFilterPreset",
    );

    beforeEach(() => {
      enableResultFilterPresets(true);
      removeResultFilterPresetMock.mockClear().mockResolvedValue();
    });

    it("should remove filter preset", async () => {
      //WHEN
      const { body } = await mockApp
        .delete("/users/resultFilterPresets/myId")
        .set("Authorization", `Bearer ${uid}`)
        .expect(200);

      //THEN
      expect(body).toEqual({
        message: "Result filter preset deleted",
        data: null,
      });
      expect(removeResultFilterPresetMock).toHaveBeenCalledWith(uid, "myId");
    });
    it("should fail if feature is disabled", async () => {
      //GIVEN
      enableResultFilterPresets(false);

      //WHEN
      const { body } = await mockApp
        .delete("/users/resultFilterPresets/myId")
        .set("Authorization", `Bearer ${uid}`)
        .expect(503);

      //THEN
      expect(body.message).toEqual(
        "Result filter presets are not available at this time.",
      );
    });
  });
  describe("add tag", () => {
    const addTagMock = vi.spyOn(UserDal, "addTag");
    const newTag = {
      _id: new ObjectId(),
      name: "tagName",
      personalBests: {
        time: {},
        words: {},
        quote: {},
        zen: {},
        custom: {},
      },
    };

    beforeEach(() => {
      addTagMock.mockClear().mockResolvedValue(newTag);
    });

    it("should add tag", async () => {
      //GIVEN

      //WHEN
      const { body } = await mockApp
        .post("/users/tags")
        .send({ tagName: "tagName" })
        .set("Authorization", `Bearer ${uid}`)
        .expect(200);

      //THEN
      expect(body).toEqual({
        message: "Tag updated",
        data: { ...newTag, _id: newTag._id.toHexString() },
      });
      expect(addTagMock).toHaveBeenCalledWith(uid, "tagName");
    });
    it("should fail without mandatory properties", async () => {
      //GIVEN

      //WHEN
      const { body } = await mockApp
        .post("/users/tags")
        .set("Authorization", `Bearer ${uid}`)
        .expect(422);

      //THEN
      expect(body).toEqual({
        message: "Invalid request data schema",
        validationErrors: ['"tagName" Required'],
      });
    });
    it("should fail with unknown properties", async () => {
      //GIVEN

      //WHEN
      const { body } = await mockApp
        .post("/users/tags")
        .set("Authorization", `Bearer ${uid}`)
        .send({ tagName: "tagName", extra: "value" })
        .expect(422);

      //THEN
      expect(body).toEqual({
        message: "Invalid request data schema",
        validationErrors: ["Unrecognized key(s) in object: 'extra'"],
      });
    });
  });
  describe("clear tag pb", () => {
    const removeTagPbMock = vi.spyOn(UserDal, "removeTagPb");

    beforeEach(() => {
      removeTagPbMock.mockClear().mockResolvedValue();
    });

    it("should clear tag pb", async () => {
      //GIVEN
      const tagId = new ObjectId().toHexString();
      //WHEN
      const { body } = await mockApp
        .delete(`/users/tags/${tagId}/personalBest`)
        .set("Authorization", `Bearer ${uid}`)
        .expect(200);

      //THEN
      expect(body).toEqual({
        message: "Tag PB cleared",
        data: null,
      });
      expect(removeTagPbMock).toHaveBeenLastCalledWith(uid, tagId);
    });
  });

  describe("update tag", () => {
    const editTagMock = vi.spyOn(UserDal, "editTag");
    beforeEach(() => {
      editTagMock.mockClear().mockResolvedValue();
    });

    it("should update tag", async () => {
      //GIVEN
      const tagId = new ObjectId().toHexString();

      //WHEN
      const { body } = await mockApp
        .patch(`/users/tags`)
        .set("Authorization", `Bearer ${uid}`)
        .send({ tagId, newName: "newName" })
        .expect(200);

      //THEN
      expect(body).toEqual({
        message: "Tag updated",
        data: null,
      });
      expect(editTagMock).toHaveBeenCalledWith(uid, tagId, "newName");
    });
    it("should fail without mandatory properties", async () => {
      //WHEN
      const { body } = await mockApp
        .patch(`/users/tags`)
        .set("Authorization", `Bearer ${uid}`)
        .expect(422);

      //THEN
      expect(body).toEqual({
        message: "Invalid request data schema",
        validationErrors: ['"tagId" Required', '"newName" Required'],
      });
    });
    it("should fail with unknown properties", async () => {
      //WHEN
      const { body } = await mockApp
        .patch(`/users/tags`)
        .set("Authorization", `Bearer ${uid}`)
        .send({
          tagId: new ObjectId().toHexString(),
          newName: "newName",
          extra: "value",
        })
        .expect(422);

      //THEN
      expect(body).toEqual({
        message: "Invalid request data schema",
        validationErrors: ["Unrecognized key(s) in object: 'extra'"],
      });
    });
  });
  describe("remove tag", () => {
    const removeTagMock = vi.spyOn(UserDal, "removeTag");

    beforeEach(() => {
      removeTagMock.mockClear().mockResolvedValue();
    });

    it("should remove tag", async () => {
      //GIVEN
      const tagId = new ObjectId().toHexString();

      //WHEN
      const { body } = await mockApp
        .delete(`/users/tags/${tagId}`)
        .set("Authorization", `Bearer ${uid}`)
        .expect(200);

      //THEN
      expect(body).toEqual({
        message: "Tag deleted",
        data: null,
      });

      expect(removeTagMock).toHaveBeenCalledWith(uid, tagId);
    });
  });
  describe("get tags", () => {
    const getTagsMock = vi.spyOn(UserDal, "getTags");

    beforeEach(() => {
      getTagsMock.mockClear();
    });

    it("should get tags", async () => {
      //GIVEN
      const tagOne: UserDal.DBUserTag = {
        _id: new ObjectId(),
        name: "tagOne",
        personalBests: {} as any,
      };
      const tagTwo: UserDal.DBUserTag = {
        _id: new ObjectId(),
        name: "tagOne",
        personalBests: {} as any,
      };

      getTagsMock.mockResolvedValue([tagOne, tagTwo]);

      //WHEN
      const { body } = await mockApp
        .get("/users/tags")
        .set("Authorization", `Bearer ${uid}`)
        .expect(200);

      //THEN
      expect(body).toEqual({
        message: "Tags retrieved",
        data: [
          { ...tagOne, _id: tagOne._id.toHexString() },
          { ...tagTwo, _id: tagTwo._id.toHexString() },
        ],
      });
      expect(getTagsMock).toHaveBeenCalledWith(uid);
    });
  });
  describe("update lb memory", () => {
    const updateLbMemoryMock = vi.spyOn(UserDal, "updateLbMemory");
    beforeEach(() => {
      updateLbMemoryMock.mockClear().mockResolvedValue();
    });

    it("should update lb", async () => {
      //WHEN
      const { body } = await mockApp
        .patch("/users/leaderboardMemory")
        .send({
          mode: "time",
          mode2: "60",
          language: "english",
          rank: 7,
        })
        .set("Authorization", `Bearer ${uid}`)
        .expect(200);

      //THEN
      expect(body).toEqual({
        message: "Leaderboard memory updated",
        data: null,
      });

      expect(updateLbMemoryMock).toHaveBeenCalledWith(
        uid,
        "time",
        "60",
        "english",
        7,
      );
    });

    it("should fail without mandatory properties", async () => {
      //WHEN
      const { body } = await mockApp
        .patch("/users/leaderboardMemory")
        .set("Authorization", `Bearer ${uid}`)
        .expect(422);

      //THEN
      expect(body).toEqual({
        message: "Invalid request data schema",
        validationErrors: [
          '"mode" Required',
          '"mode2" Needs to be either a number, "zen" or "custom".',
          '"language" Required',
          '"rank" Required',
        ],
      });
    });
    it("should fail with unknown properties", async () => {
      //WHEN
      const { body } = await mockApp
        .patch("/users/leaderboardMemory")
        .set("Authorization", `Bearer ${uid}`)
        .send({
          mode: "time",
          mode2: "60",
          language: "english",
          rank: 7,
          extra: "value",
        })
        .expect(422);

      //THEN
      expect(body).toEqual({
        message: "Invalid request data schema",
        validationErrors: ["Unrecognized key(s) in object: 'extra'"],
      });
    });
  });
  describe("get custom themes", () => {
    const getThemesMock = vi.spyOn(UserDal, "getThemes");
    beforeEach(() => {
      getThemesMock.mockClear();
    });
    it("should get custom themes", async () => {
      //GIVEN
      const themeOne: UserDal.DBCustomTheme = {
        _id: new ObjectId(),
        name: "themeOne",
        colors: new Array(10).fill("#000000") as any,
      };
      const themeTwo: UserDal.DBCustomTheme = {
        _id: new ObjectId(),
        name: "themeTwo",
        colors: new Array(10).fill("#FFFFFF") as any,
      };
      getThemesMock.mockResolvedValue([themeOne, themeTwo]);

      //WHEN
      const { body } = await mockApp
        .get("/users/customThemes")
        .set("Authorization", `Bearer ${uid}`)
        .expect(200);

      //THEN
      expect(body).toEqual({
        message: "Custom themes retrieved",
        data: [
          { ...themeOne, _id: themeOne._id.toHexString() },
          { ...themeTwo, _id: themeTwo._id.toHexString() },
        ],
      });
    });
  });
  describe("add custom theme", () => {
    const addThemeMock = vi.spyOn(UserDal, "addTheme");
    beforeEach(() => {
      addThemeMock.mockClear();
    });

    it("should add", async () => {
      //GIVEN
      const addedTheme: UserDal.DBCustomTheme = {
        _id: new ObjectId(),
        name: "custom",
        colors: new Array(10).fill("#000000") as any,
      };
      addThemeMock.mockResolvedValue(addedTheme);

      //WHEN
      const { body } = await mockApp
        .post("/users/customThemes")
        .set("Authorization", `Bearer ${uid}`)
        .send({
          name: "customTheme",
          colors: new Array(10).fill("#000000") as any,
        })
        .expect(200);

      //THEN
      expect(body).toEqual({
        message: "Custom theme added",
        data: { ...addedTheme, _id: addedTheme._id.toHexString() },
      });
      expect(addThemeMock).toHaveBeenCalledWith(uid, {
        name: "customTheme",
        colors: new Array(10).fill("#000000") as any,
      });
    });
    it("should fail without mandatory properties", async () => {
      //WHEN
      const { body } = await mockApp
        .post("/users/customThemes")
        .set("Authorization", `Bearer ${uid}`)
        .expect(422);

      //THEN
      expect(body).toEqual({
        message: "Invalid request data schema",
        validationErrors: ['"name" Required', '"colors" Required'],
      });
    });
    it("should fail with unknown properties", async () => {
      //WHEN
      const { body } = await mockApp
        .post("/users/customThemes")
        .set("Authorization", `Bearer ${uid}`)
        .send({
          name: "customTheme",
          colors: new Array(10).fill("#000000") as any,
          extra: "value",
        })
        .expect(422);

      //THEN
      expect(body).toEqual({
        message: "Invalid request data schema",
        validationErrors: ["Unrecognized key(s) in object: 'extra'"],
      });
    });
    it("should fail with invalid properties", async () => {
      //WHEN
      const { body } = await mockApp
        .post("/users/customThemes")
        .set("Authorization", `Bearer ${uid}`)
        .send({
          name: "customThemecustomThemecustomThemecustomTheme",
          colors: new Array(9).fill("#000") as any,
        })
        .expect(422);

      //THEN
      expect(body).toEqual({
        message: "Invalid request data schema",
        validationErrors: [
          '"name" String must contain at most 16 character(s)',
          '"colors" Array must contain at least 10 element(s)',
        ],
      });
    });
  });
  describe("remove custom theme", () => {
    const removeThemeMock = vi.spyOn(UserDal, "removeTheme");

    beforeEach(() => {
      removeThemeMock.mockClear().mockResolvedValue();
    });

    it("should remove theme", async () => {
      //GIVEN
      const themeId = new ObjectId().toHexString();

      //WHEN
      const { body } = await mockApp
        .delete("/users/customThemes")
        .set("Authorization", `Bearer ${uid}`)
        .send({ themeId })
        .expect(200);

      //THEN
      expect(body).toEqual({
        message: "Custom theme removed",
        data: null,
      });
      expect(removeThemeMock).toHaveBeenCalledWith(uid, themeId);
    });
    it("should fail without mandatory properties", async () => {
      //WHEN
      const { body } = await mockApp
        .delete("/users/customThemes")
        .set("Authorization", `Bearer ${uid}`)
        .expect(422);

      //THEN
      expect(body).toEqual({
        message: "Invalid request data schema",
        validationErrors: ['"themeId" Required'],
      });
    });
    it("should fail with unknown properties", async () => {
      //WHEN
      const { body } = await mockApp
        .delete("/users/customThemes")
        .set("Authorization", `Bearer ${uid}`)
        .send({ themeId: new ObjectId().toHexString(), extra: "value" })
        .expect(422);

      //THEN
      expect(body).toEqual({
        message: "Invalid request data schema",
        validationErrors: ["Unrecognized key(s) in object: 'extra'"],
      });
    });
  });
  describe("edit custom theme", () => {
    const editThemeMock = vi.spyOn(UserDal, "editTheme");
    beforeEach(() => {
      editThemeMock.mockClear().mockResolvedValue();
    });

    it("should edit custom theme", async () => {
      //GIVEN
      const themeId = new ObjectId().toHexString();
      const theme = {
        name: "newName",
        colors: new Array(10).fill("#000000") as any,
      };

      //WHEN
      const { body } = await mockApp
        .patch("/users/customThemes")
        .set("Authorization", `Bearer ${uid}`)
        .send({
          themeId,
          theme,
        })
        .expect(200);

      //THEN
      expect(body).toEqual({
        message: "Custom theme updated",
        data: null,
      });
      expect(editThemeMock).toHaveBeenCalledWith(uid, themeId, theme);
    });
    it("should fail without mandatory properties", async () => {
      //WHEN
      const { body } = await mockApp
        .patch("/users/customThemes")
        .set("Authorization", `Bearer ${uid}`)
        .expect(422);

      //THEN
      expect(body).toEqual({
        message: "Invalid request data schema",
        validationErrors: ['"themeId" Required', '"theme" Required'],
      });
    });
    it("should fail with unknown properties", async () => {
      //WHEN
      const { body } = await mockApp
        .patch("/users/customThemes")
        .set("Authorization", `Bearer ${uid}`)
        .send({
          themeId: new ObjectId().toHexString(),
          theme: {
            name: "newName",
            colors: new Array(10).fill("#000000") as any,
            extra2: "value",
          },
          extra: "value",
        })
        .expect(422);

      //THEN
      expect(body).toEqual({
        message: "Invalid request data schema",
        validationErrors: [
          `"theme" Unrecognized key(s) in object: 'extra2'`,
          "Unrecognized key(s) in object: 'extra'",
        ],
      });
    });
  });
  describe("get personal bests", () => {
    const getPBMock = vi.spyOn(UserDal, "getPersonalBests");
    beforeEach(() => {
      getPBMock.mockClear();
    });

    it("should get pbs", async () => {
      //GIVEN
      const personalBest: PersonalBest = pb(15);
      getPBMock.mockResolvedValue(personalBest);

      //WHEN
      const { body } = await mockApp
        .get("/users/personalBests")
        .set("Authorization", `Bearer ${uid}`)
        .query({ mode: "time", mode2: "15" })
        .expect(200);

      //THEN
      expect(body).toEqual({
        message: "Personal bests retrieved",
        data: personalBest,
      });
      expect(getPBMock).toHaveBeenCalledWith(uid, "time", "15");
    });
    it("should get pbs with ape key", async () => {
      //GIVEN
      await acceptApeKeys(true);
      const apeKey = await mockAuthenticateWithApeKey(uid, await configuration);

      //WHEN
      await mockApp
        .get("/users/personalBests")
        .set("authorization", `ApeKey ${apeKey}`)
        .query({ mode: "time", mode2: "15" })
        .expect(200);
    });
    it("should fail without mandatory query parameters", async () => {
      //WHEN
      const { body } = await mockApp
        .get("/users/personalBests")
        .set("Authorization", `Bearer ${uid}`)
        .expect(422);

      //THEN
      expect(body).toEqual({
        message: "Invalid query schema",
        validationErrors: ['"mode" Required'],
      });
    });
    it("should fail with unknown query parameters", async () => {
      //WHEN
      const { body } = await mockApp
        .get("/users/personalBests")
        .set("Authorization", `Bearer ${uid}`)
        .query({ mode: "time", mode2: "15", extra: "value" })
        .expect(422);

      //THEN
      expect(body).toEqual({
        message: "Invalid query schema",
        validationErrors: ["Unrecognized key(s) in object: 'extra'"],
      });
    });
    it("should fail with invalid query parameters", async () => {
      //WHEN
      const { body } = await mockApp
        .get("/users/personalBests")
        .set("Authorization", `Bearer ${uid}`)
        .query({ mode: "mood", mode2: "happy" })

        .expect(422);

      //THEN
      expect(body).toEqual({
        message: "Invalid query schema",
        validationErrors: [
          `"mode" Invalid enum value. Expected 'time' | 'words' | 'quote' | 'custom' | 'zen', received 'mood'`,
          `"mode2" Needs to be a number or a number represented as a string e.g. "10".`,
        ],
      });
    });
  });
  describe("get stats", () => {
    const getStatsMock = vi.spyOn(UserDal, "getStats");
    beforeEach(() => {
      getStatsMock.mockClear();
    });

    it("should get stats", async () => {
      //GIVEN
      const stats: Pick<
        UserDal.DBUser,
        "startedTests" | "completedTests" | "timeTyping"
      > = {
        startedTests: 5,
        completedTests: 3,
        timeTyping: 42,
      };
      getStatsMock.mockResolvedValue(stats);

      //WHEN
      const { body } = await mockApp
        .get("/users/stats")
        .set("Authorization", `Bearer ${uid}`)
        .expect(200);

      //THEN
      expect(body).toEqual({
        message: "Personal stats retrieved",
        data: stats,
      });

      expect(getStatsMock).toHaveBeenCalledWith(uid);
    });
    it("should get stats with ape key", async () => {
      //GIVEN
      await acceptApeKeys(true);
      const apeKey = await mockAuthenticateWithApeKey(uid, await configuration);

      //WHEN
      await mockApp
        .get("/users/stats")
        .set("authorization", `ApeKey ${apeKey}`)
        .expect(200);
    });
  });
  describe("get favorite quotes", () => {
    const getFavoriteQuotesMock = vi.spyOn(UserDal, "getFavoriteQuotes");
    beforeEach(() => {
      getFavoriteQuotesMock.mockClear();
    });

    it("should get favorite quites", async () => {
      //GIVEN
      const favoriteQuotes = {
        english: ["1", "2"],
        german: ["1", "3"],
      };
      getFavoriteQuotesMock.mockResolvedValue(favoriteQuotes);

      //WHEN
      const { body } = await mockApp
        .get("/users/favoriteQuotes")
        .set("Authorization", `Bearer ${uid}`)
        .expect(200);

      //THEN
      expect(body).toEqual({
        message: "Favorite quotes retrieved",
        data: favoriteQuotes,
      });
      expect(getFavoriteQuotesMock).toHaveBeenCalledWith(uid);
    });
  });
  describe("add favorite quotes", () => {
    const addFavoriteQuoteMock = vi.spyOn(UserDal, "addFavoriteQuote");
    beforeEach(() => {
      addFavoriteQuoteMock.mockClear().mockResolvedValue();
    });
    it("should add", async () => {
      //WHEN
      const { body } = await mockApp
        .post("/users/favoriteQuotes")
        .set("Authorization", `Bearer ${uid}`)
        .send({ language: "english", quoteId: "7" })
        .expect(200);

      //THEN
      expect(body).toEqual({
        message: "Quote added to favorites",
        data: null,
      });
      expect(addFavoriteQuoteMock).toHaveBeenCalledWith(
        uid,
        "english",
        "7",
        (await Configuration.getLiveConfiguration()).quotes.maxFavorites,
      );
    });
    it("should fail without mandatory properties", async () => {
      //WHEN
      const { body } = await mockApp
        .post("/users/favoriteQuotes")
        .set("Authorization", `Bearer ${uid}`)
        .expect(422);

      //THEN
      expect(body).toEqual({
        message: "Invalid request data schema",
        validationErrors: ['"language" Required', '"quoteId" Invalid input'],
      });
    });
    it("should fail unknown properties", async () => {
      //WHEN
      const { body } = await mockApp
        .post("/users/favoriteQuotes")
        .set("Authorization", `Bearer ${uid}`)
        .send({ language: "english", quoteId: "7", extra: "value" })
        .expect(422);

      //THEN
      expect(body).toEqual({
        message: "Invalid request data schema",
        validationErrors: ["Unrecognized key(s) in object: 'extra'"],
      });
    });
  });
  describe("remove favorite quote", () => {
    const removeFavoriteQuoteMock = vi.spyOn(UserDal, "removeFavoriteQuote");
    beforeEach(() => {
      removeFavoriteQuoteMock.mockClear().mockResolvedValue();
    });

    it("should remove quote", async () => {
      //WHEN
      const { body } = await mockApp
        .delete("/users/favoriteQuotes")
        .set("Authorization", `Bearer ${uid}`)
        .send({ language: "english", quoteId: "7" })
        .expect(200);

      //THEN
      expect(body).toEqual({
        message: "Quote removed from favorites",
        data: null,
      });
      expect(removeFavoriteQuoteMock).toHaveBeenCalledWith(uid, "english", "7");
    });
    it("should fail without mandatory properties", async () => {
      //WHEN
      const { body } = await mockApp
        .delete("/users/favoriteQuotes")
        .set("Authorization", `Bearer ${uid}`)
        .expect(422);

      //THEN
      expect(body).toEqual({
        message: "Invalid request data schema",
        validationErrors: ['"language" Required', '"quoteId" Invalid input'],
      });
    });
    it("should fail unknown properties", async () => {
      //WHEN
      const { body } = await mockApp
        .delete("/users/favoriteQuotes")
        .set("Authorization", `Bearer ${uid}`)
        .send({ language: "english", quoteId: "7", extra: "value" })
        .expect(422);

      //THEN
      expect(body).toEqual({
        message: "Invalid request data schema",
        validationErrors: ["Unrecognized key(s) in object: 'extra'"],
      });
    });
  });
  describe("get profile", () => {
    const getUserMock = vi.spyOn(UserDal, "getUser");
    const getUserByNameMock = vi.spyOn(UserDal, "getUserByName");
    const checkIfUserIsPremiumMock = vi.spyOn(UserDal, "checkIfUserIsPremium");
    const leaderboardGetRankMock = vi.spyOn(LeaderboardDal, "getRank");
    const leaderboardGetCountMock = vi.spyOn(LeaderboardDal, "getCount");

    const foundUser: Partial<UserDal.DBUser> = {
      _id: new ObjectId(),
      uid: new ObjectId().toHexString(),
      name: "bob",
      banned: false,
      inventory: { badges: [{ id: 1, selected: true }, { id: 2 }] },
      profileDetails: {
        bio: "bio",
        keyboard: "keyboard",
        socialProfiles: {
          twitter: "twitter",
          github: "github",
        },
      },
      personalBests: {
        time: {
          "15": [pb(15), pb(16)],
          "30": [pb(30), pb(31)],
          "60": [pb(60), pb(61)],
          "120": [pb(120), pb(121)],
          "42": [pb(42), pb(43)],
        },
        words: {
          "10": [pb(10), pb(11)],
          "25": [pb(25), pb(26)],
          "50": [pb(50), pb(51)],
          "100": [pb(100), pb(101)],
          "42": [pb(42), pb(43)],
        },
        custom: {},
        zen: {},
        quote: {},
      },
      completedTests: 23,
      startedTests: 42,
      timeTyping: 234,
      addedAt: 1000,
      discordId: "discordId",
      discordAvatar: "discordAvatar",
      xp: 10,
      streak: { length: 2, lastResultTimestamp: 2000, maxLength: 5 },
      lbOptOut: false,
      bananas: 47, //should get removed
      testActivity: {
        "2024": fillYearWithDay(94),
      },
    };

    beforeEach(async () => {
      getUserMock.mockClear();
      getUserByNameMock.mockClear();
      checkIfUserIsPremiumMock.mockClear().mockResolvedValue(true);
      leaderboardGetRankMock.mockClear();
      leaderboardGetCountMock.mockClear();
      await enableProfiles(true);
    });

    it("should get by name without authentication", async () => {
      //GIVEN

      getUserByNameMock.mockResolvedValue(foundUser as any);

      const rank = { rank: 24 } as LeaderboardDal.DBLeaderboardEntry;
      leaderboardGetRankMock.mockResolvedValue(rank);
      leaderboardGetCountMock.mockResolvedValue(100);

      //WHEN
      const { body } = await mockApp.get("/users/bob/profile").expect(200);

      //THEN
      expect(body).toEqual({
        message: "Profile retrieved",
        data: {
          uid: foundUser.uid,
          name: "bob",
          banned: false,
          addedAt: 1000,
          typingStats: {
            completedTests: 23,
            startedTests: 42,
            timeTyping: 234,
          },
          personalBests: {
            time: {
              "15": foundUser.personalBests?.time["15"],
              "30": foundUser.personalBests?.time["30"],
              "60": foundUser.personalBests?.time["60"],
              "120": foundUser.personalBests?.time["120"],
            },
            words: {
              "10": foundUser.personalBests?.words["10"],
              "25": foundUser.personalBests?.words["25"],
              "50": foundUser.personalBests?.words["50"],
              "100": foundUser.personalBests?.words["100"],
            },
          },

          discordId: "discordId",
          discordAvatar: "discordAvatar",
          xp: 10,
          streak: 2,
          maxStreak: 5,
          lbOptOut: false,
          isPremium: true,
          allTimeLbs: {
            time: {
              "15": { english: { count: 100, rank: 24 } },
              "60": { english: { count: 100, rank: 24 } },
            },
          },
          inventory: foundUser.inventory,
          details: foundUser.profileDetails,
        },
      });
      expect(getUserByNameMock).toHaveBeenCalledWith("bob", "get user profile");
      expect(getUserMock).not.toHaveBeenCalled();
    });
    it("should get testActivity if enabled", async () => {
      //GIVEN
      vi.useFakeTimers().setSystemTime(1712102400000);
      getUserByNameMock.mockResolvedValue({
        ...foundUser,
        profileDetails: { showActivityOnPublicProfile: true },
      } as any);
      const rank = { rank: 24 } as LeaderboardDal.DBLeaderboardEntry;
      leaderboardGetRankMock.mockResolvedValue(rank);
      leaderboardGetCountMock.mockResolvedValue(100);

      //WHEN
      const { body } = await mockApp.get("/users/bob/profile").expect(200);

      //THEN
      expect(body.data.testActivity).toEqual(
        expect.objectContaining({
          lastDay: 1712102400000,
          testsByDays: expect.arrayContaining([]),
        }),
      );
    });
    it("should not get testActivity if disabled", async () => {
      //GIVEN
      vi.useFakeTimers().setSystemTime(1712102400000);
      getUserByNameMock.mockResolvedValue({
        ...foundUser,
        profileDetails: { showActivityOnPublicProfile: false },
      } as any);
      const rank = { rank: 24 } as LeaderboardDal.DBLeaderboardEntry;
      leaderboardGetRankMock.mockResolvedValue(rank);
      leaderboardGetCountMock.mockResolvedValue(100);

      //WHEN
      const { body } = await mockApp.get("/users/bob/profile").expect(200);

      //THEN
      expect(body.data.testActivity).toBeUndefined();
    });

    it("should get base profile for banned user", async () => {
      //GIVEN
      getUserByNameMock.mockResolvedValue({
        ...foundUser,
        banned: true,
      } as any);

      const rank = { rank: 24 } as LeaderboardDal.DBLeaderboardEntry;
      leaderboardGetRankMock.mockResolvedValue(rank);
      leaderboardGetCountMock.mockResolvedValue(100);

      //WHEN
      const { body } = await mockApp.get("/users/bob/profile").expect(200);

      //THEN
      expect(body).toEqual({
        message: "Profile retrived: banned user",
        data: {
          name: "bob",
          banned: true,
          addedAt: 1000,
          typingStats: {
            completedTests: 23,
            startedTests: 42,
            timeTyping: 234,
          },
          personalBests: {
            time: {
              "15": foundUser.personalBests?.time["15"],
              "30": foundUser.personalBests?.time["30"],
              "60": foundUser.personalBests?.time["60"],
              "120": foundUser.personalBests?.time["120"],
            },
            words: {
              "10": foundUser.personalBests?.words["10"],
              "25": foundUser.personalBests?.words["25"],
              "50": foundUser.personalBests?.words["50"],
              "100": foundUser.personalBests?.words["100"],
            },
          },

          discordId: "discordId",
          discordAvatar: "discordAvatar",
          xp: 10,
          streak: 2,
          maxStreak: 5,
          lbOptOut: false,
          isPremium: true,
        },
      });
      expect(getUserByNameMock).toHaveBeenCalledWith("bob", "get user profile");
      expect(getUserMock).not.toHaveBeenCalled();
    });
    it("should get by uid without authentication", async () => {
      //GIVEN
      const uid = foundUser.uid;
      getUserMock.mockResolvedValue(foundUser as any);

      const rank = { rank: 24 } as LeaderboardDal.DBLeaderboardEntry;
      leaderboardGetRankMock.mockResolvedValue(rank);
      leaderboardGetCountMock.mockResolvedValue(100);

      //WHEN
      const { body } = await mockApp
        .get(`/users/${uid}/profile`)
        .query({ isUid: "" })
        .expect(200);

      //THEN
      expect(body).toEqual({
        message: "Profile retrieved",
        data: expect.objectContaining({
          uid: foundUser.uid,
        }),
      });
      expect(getUserByNameMock).not.toHaveBeenCalled();
      expect(getUserMock).toHaveBeenCalledWith(uid, "get user profile");
    });
    it("should fail if feature is disabled", async () => {
      //GIVEN
      await enableProfiles(false);

      //WHEN
      const { body } = await mockApp.get(`/users/bob/profile`).expect(503);

      //THEN
      expect(body.message).toEqual("Profiles are not available at this time");
    });
  });
  describe("update profile", () => {
    const getPartialUserMock = vi.spyOn(UserDal, "getPartialUser");
    const updateProfileMock = vi.spyOn(UserDal, "updateProfile");

    beforeEach(async () => {
      getPartialUserMock.mockClear().mockResolvedValue({
        inventory: {
          badges: [{ id: 4, selected: true }, { id: 2 }, { id: 3 }],
        },
      } as any);
      updateProfileMock.mockClear().mockResolvedValue();
      await enableProfiles(true);
    });

    it("should update", async () => {
      //GIVEN
      const newProfile = {
        bio: "newBio",
        keyboard: "newKeyboard",

        socialProfiles: {
          github: "github",
          twitter: "twitter",
          website: "https://monkeytype.com",
        },
        showActivityOnPublicProfile: false,
      };

      //WHEN
      const { body } = await mockApp
        .patch("/users/profile")
        .set("Authorization", `Bearer ${uid}`)
        .send({
          ...newProfile,
          selectedBadgeId: 2,
        })
        .expect(200);

      //THEN
      expect(body).toEqual({
        message: "Profile updated",
        data: newProfile,
      });
      expect(updateProfileMock).toHaveBeenCalledWith(
        uid,
        {
          bio: "newBio",
          keyboard: "newKeyboard",
          socialProfiles: {
            github: "github",
            twitter: "twitter",
            website: "https://monkeytype.com",
          },
          showActivityOnPublicProfile: false,
        },
        {
          badges: [{ id: 4 }, { id: 2, selected: true }, { id: 3 }],
        },
      );
    });
    it("should update with empty strings", async () => {
      //GIVEN
      const newProfile = {
        bio: "",
        keyboard: "",

        socialProfiles: {
          github: "",
          twitter: "",
          website: "",
        },
      };

      //WHEN
      const { body } = await mockApp
        .patch("/users/profile")
        .set("Authorization", `Bearer ${uid}`)
        .send({
          ...newProfile,
          selectedBadgeId: -1,
        })
        .expect(200);

      //THEN
      expect(body).toEqual({
        message: "Profile updated",
        data: newProfile,
      });
      expect(updateProfileMock).toHaveBeenCalledWith(
        uid,
        {
          bio: "",
          keyboard: "",
          socialProfiles: {
            github: "",
            twitter: "",
            website: "",
          },
        },
        {
          badges: [{ id: 4 }, { id: 2 }, { id: 3 }],
        },
      );
    });
    it("should fail with unknown properties", async () => {
      //WHEN
      const { body } = await mockApp
        .patch("/users/profile")
        .set("Authorization", `Bearer ${uid}`)
        .send({
          extra: "value",
          socialProfiles: {
            extra2: "value",
          },
        })
        .expect(422);

      //THEN
      expect(body).toEqual({
        message: "Invalid request data schema",
        validationErrors: [
          `"socialProfiles" Unrecognized key(s) in object: 'extra2'`,
          "Unrecognized key(s) in object: 'extra'",
        ],
      });
    });
    it("should sanitize inputs", async () => {
      //WHEN
      await mockApp
        .patch("/users/profile")
        .set("Authorization", `Bearer ${uid}`)
        .send({
          bio: "Line1\n\n\nLine2\n\n\n\nLine3",
          keyboard: "  string     with      many      spaces      ",
        })
        .expect(200);

      //THEN
      expect(updateProfileMock).toHaveBeenCalledWith(
        uid,
        {
          bio: "Line1\n\nLine2\n\nLine3",
          keyboard: "string  with  many  spaces",
          socialProfiles: {},
        },
        expect.objectContaining({}),
      );
    });
    it("should fail with disallowed word", async () => {
      //WHEN
      const { body } = await mockApp
        .patch("/users/profile")
        .set("Authorization", `Bearer ${uid}`)
        .send({
          bio: "miodec",
          keyboard: "miodec",
          socialProfiles: {
            twitter: "miodec",
            github: "miodec",
            website: "https://i-luv-miodec.com",
          },
        })
        .expect(422);

      //THEN
      expect(body).toEqual({
        message: "Invalid request data schema",
        validationErrors: [
          '"bio" Disallowed word detected. Please remove it. If you believe this is a mistake, please contact us (miodec).',
          '"keyboard" Disallowed word detected. Please remove it. If you believe this is a mistake, please contact us (miodec).',
          '"socialProfiles.twitter" Disallowed word detected. Please remove it. If you believe this is a mistake, please contact us (miodec).',
          '"socialProfiles.github" Disallowed word detected. Please remove it. If you believe this is a mistake, please contact us (miodec).',
          '"socialProfiles.website" Disallowed word detected. Please remove it. If you believe this is a mistake, please contact us (https://i-luv-miodec.com).',
        ],
      });
    });
    it("should fail with properties exceeding max lengths", async () => {
      //WHEN
      const { body } = await mockApp
        .patch("/users/profile")
        .set("Authorization", `Bearer ${uid}`)
        .send({
          bio: new Array(251).fill("x").join(""),
          keyboard: new Array(76).fill("x").join(""),
          socialProfiles: {
            twitter: new Array(21).fill("x").join(""),
            github: new Array(40).fill("x").join(""),
            website:
              "https://" +
              new Array(201 - "https://".length).fill("x").join(""),
          },
        })
        .expect(422);

      //THEN
      expect(body).toEqual({
        message: "Invalid request data schema",
        validationErrors: [
          '"bio" String must contain at most 250 character(s)',
          '"keyboard" String must contain at most 75 character(s)',
          '"socialProfiles.twitter" String must contain at most 20 character(s)',
          '"socialProfiles.github" String must contain at most 39 character(s)',
          '"socialProfiles.website" String must contain at most 200 character(s)',
        ],
      });
    });
    it("should fail with website not using https", async () => {
      //WHEN
      const { body } = await mockApp
        .patch("/users/profile")
        .set("Authorization", `Bearer ${uid}`)
        .send({
          socialProfiles: {
            website: "http://monkeytype.com",
          },
        })
        .expect(422);

      //THEN
      expect(body).toEqual({
        message: "Invalid request data schema",
        validationErrors: [
          '"socialProfiles.website" Invalid input: must start with "https://"',
        ],
      });
    });
    it("should fail if feature is disabled", async () => {
      //GIVEN
      await enableProfiles(false);

      //WHEN
      const { body } = await mockApp
        .patch("/users/profile")
        .set("Authorization", `Bearer ${uid}`)
        .send({})
        .expect(503);

      //THEN
      expect(body.message).toEqual("Profiles are not available at this time");
    });
  });
  describe("get inbox", () => {
    const getInboxMock = vi.spyOn(UserDal, "getInbox");

    beforeEach(async () => {
      getInboxMock.mockClear();
      await enableInbox(true);
    });

    it("should get inbox", async () => {
      //GIVEN
      const mailOne: MonkeyMail = {
        id: randomUUID(),
        subject: "subjectOne",
        body: "bodyOne",
        timestamp: 100,
        read: false,
        rewards: [],
      };
      const mailTwo: MonkeyMail = {
        id: randomUUID(),
        subject: "subjectTwo",
        body: "bodyTwo",
        timestamp: 100,
        read: false,
        rewards: [],
      };
      getInboxMock.mockResolvedValue([mailOne, mailTwo]);

      //WHEN
      const { body } = await mockApp
        .get("/users/inbox")
        .set("Authorization", `Bearer ${uid}`)
        .expect(200);

      //THEN
      expect(body).toEqual({
        message: "Inbox retrieved",
        data: {
          inbox: [mailOne, mailTwo],
          maxMail: (await Configuration.getLiveConfiguration()).users.inbox
            .maxMail,
        },
      });
      expect(getInboxMock).toHaveBeenCalledWith(uid);
    });
    it("should fail if feature is disabled", async () => {
      //GIVEN
      await enableInbox(false);

      //WHEN
      const { body } = await mockApp
        .get("/users/inbox")
        .set("Authorization", `Bearer ${uid}`)
        .expect(503);

      //THEN
      expect(body.message).toEqual("Your inbox is not available at this time.");
    });
  });
  describe("update inbox", () => {
    const updateInboxMock = vi.spyOn(UserDal, "updateInbox");
    const mailIdOne = randomUUID();
    const mailIdTwo = randomUUID();
    beforeEach(async () => {
      updateInboxMock.mockClear().mockResolvedValue();
      await enableInbox(true);
    });

    it("should update", async () => {
      //WHEN
      const { body } = await mockApp
        .patch("/users/inbox")
        .set("Authorization", `Bearer ${uid}`)
        .send({
          mailIdsToDelete: [mailIdOne],
          mailIdsToMarkRead: [mailIdOne, mailIdTwo],
        })
        .expect(200);

      //THEN
      expect(body).toEqual({
        message: "Inbox updated",
        data: null,
      });

      expect(updateInboxMock).toHaveBeenCalledWith(
        uid,
        [mailIdOne, mailIdTwo],
        [mailIdOne],
      );
    });
    it("should update without body", async () => {
      //WHEN
      const { body } = await mockApp
        .patch("/users/inbox")
        .set("Authorization", `Bearer ${uid}`)
        .expect(200);

      //THEN
      expect(body).toEqual({
        message: "Inbox updated",
        data: null,
      });

      expect(updateInboxMock).toHaveBeenCalledWith(uid, [], []);
    });
    it("should fail with empty arrays", async () => {
      //WHEN
      const { body } = await mockApp
        .patch("/users/inbox")
        .set("Authorization", `Bearer ${uid}`)
        .send({
          mailIdsToDelete: [],
          mailIdsToMarkRead: [],
        })
        .expect(422);

      //THEN
      expect(body).toEqual({
        message: "Invalid request data schema",
        validationErrors: [
          '"mailIdsToDelete" Array must contain at least 1 element(s)',
          '"mailIdsToMarkRead" Array must contain at least 1 element(s)',
        ],
      });
    });
    it("should fail if feature is disabled", async () => {
      //GIVEN
      await enableInbox(false);

      //WHEN
      const { body } = await mockApp
        .patch("/users/inbox")
        .set("Authorization", `Bearer ${uid}`)
        .expect(503);

      //THEN
      expect(body.message).toEqual("Your inbox is not available at this time.");
    });
  });
  describe("report user", () => {
    const createReportMock = vi.spyOn(ReportDal, "createReport");
    const verifyCaptchaMock = vi.spyOn(Captcha, "verify");
    const getPartialUserMock = vi.spyOn(UserDal, "getPartialUser"); //todo replace with getPartialUser
    beforeEach(async () => {
      vi.useFakeTimers();
      vi.setSystemTime(125000);
      createReportMock.mockClear().mockResolvedValue();
      verifyCaptchaMock.mockClear().mockResolvedValue(true);
      getPartialUserMock.mockClear().mockResolvedValue({} as any);

      await enableReporting(true);
    });
    afterEach(() => {
      vi.useRealTimers();
    });

    it("should report", async () => {
      //WHEN
      const uidToReport = new ObjectId().toHexString();

      const { body } = await mockApp
        .post("/users/report")
        .set("Authorization", `Bearer ${uid}`)
        .send({
          uid: uidToReport,
          reason: "Suspected cheating",
          comment: "comment",
          captcha: "captcha",
        })
        .expect(200);

      //THEN
      expect(body).toEqual({
        message: "User reported",
        data: null,
      });
      expect(createReportMock).toHaveBeenCalledWith(
        expect.objectContaining({
          type: "user",
          timestamp: 125000,
          uid,
          contentId: uidToReport,
          reason: "Suspected cheating",
          comment: "comment",
        }),
        (await Configuration.getLiveConfiguration()).quotes.reporting
          .maxReports,
        (await Configuration.getLiveConfiguration()).quotes.reporting
          .contentReportLimit,
      );
      expect(verifyCaptchaMock).toHaveBeenCalledWith("captcha");
    });
    it("should fail without mandatory properties", async () => {
      //WHEN
      const { body } = await mockApp
        .post("/users/report")
        .set("Authorization", `Bearer ${uid}`)
        .expect(422);

      //THEN
      expect(body).toEqual({
        message: "Invalid request data schema",
        validationErrors: [
          '"uid" Required',
          '"reason" Required',
          '"captcha" Required',
        ],
      });
    });
    it("should fail with unknown properties", async () => {
      //WHEN
      const { body } = await mockApp
        .post("/users/report")
        .set("Authorization", `Bearer ${uid}`)
        .send({
          uid: new ObjectId().toHexString(),
          reason: "Suspected cheating",
          comment: "comment",
          captcha: "captcha",
          extra: "value",
        })
        .expect(422);

      //THEN
      expect(body).toEqual({
        message: "Invalid request data schema",
        validationErrors: ["Unrecognized key(s) in object: 'extra'"],
      });
    });
    it("should fail with invalid captcha", async () => {
      //GIVEN
      verifyCaptchaMock.mockResolvedValue(false);

      //WHEN
      const { body } = await mockApp
        .post("/users/report")
        .set("Authorization", `Bearer ${uid}`)
        .send({
          uid: new ObjectId().toHexString(),
          reason: "Suspected cheating",
          comment: "comment",
          captcha: "captcha",
        })
        .expect(422);

      //THEN
      expect(body.message).toEqual("Captcha challenge failed");
      /* TODO
      expect(body).toEqual({});
      */
    });
    it("should fail with invalid properties", async () => {
      //WHEN
      const { body } = await mockApp
        .post("/users/report")
        .set("Authorization", `Bearer ${uid}`)
        .send({
          uid: new Array(51).fill("x").join(""),
          reason: "unfriendly",
          comment: new Array(251).fill("x").join(""),
          captcha: "captcha",
        })
        .expect(422);

      //THEN
      expect(body).toEqual({
        message: "Invalid request data schema",
        validationErrors: [
          `"reason" Invalid enum value. Expected 'Inappropriate name' | 'Inappropriate bio' | 'Inappropriate social links' | 'Suspected cheating', received 'unfriendly'`,
          '"comment" String must contain at most 250 character(s)',
        ],
      });
    });
    it("should fail if user can not report", async () => {
      //GIVEN
      getPartialUserMock.mockResolvedValue({ canReport: false } as any);

      //WHEN
      const { body } = await mockApp
        .post("/users/report")
        .set("Authorization", `Bearer ${uid}`)
        .send({
          uid: new ObjectId().toHexString(),
          reason: "Suspected cheating",
          comment: "comment",
          captcha: "captcha",
        })
        .expect(403);

      //THEN
      expect(body.message).toEqual("You don't have permission to do this.");
    });
    it("should fail if feature is disabled", async () => {
      //GIVEN
      await enableReporting(false);

      //WHEN
      const { body } = await mockApp
        .post("/users/report")
        .set("Authorization", `Bearer ${uid}`)
        .send({
          uid: new ObjectId().toHexString(),
          reason: "Suspected cheating",
          comment: "comment",
          captcha: "captcha",
        })
        .expect(503);

      //THEN
      expect(body.message).toEqual("User reporting is unavailable.");
    });
  });
  describe("set streak hour offset", () => {
    const getPartialUserMock = vi.spyOn(UserDal, "getPartialUser");
    const setStreakHourOffsetMock = vi.spyOn(UserDal, "setStreakHourOffset");
    const addImportantLogMock = vi.spyOn(LogDal, "addImportantLog");

    beforeEach(() => {
      getPartialUserMock.mockClear().mockResolvedValue({} as any);
      setStreakHourOffsetMock.mockClear().mockResolvedValue();
      addImportantLogMock.mockClear().mockResolvedValue();
    });

    it("should set", async () => {
      //WHEN
      const { body } = await mockApp
        .post("/users/setStreakHourOffset")
        .set("Authorization", `Bearer ${uid}`)
        .send({ hourOffset: -2 })
        .expect(200);

      //THEN
      expect(body).toEqual({
        message: "Streak hour offset set",
        data: null,
      });

      expect(setStreakHourOffsetMock).toHaveBeenCalledWith(uid, -2);
      expect(addImportantLogMock).toHaveBeenCalledWith(
        "user_streak_hour_offset_set",
        { hourOffset: -2 },
        uid,
      );
    });
    it("should fail if offset already set", async () => {
      //GIVEN
      getPartialUserMock.mockResolvedValue({
        streak: { hourOffset: -2 },
      } as any);

      //WHEN
      const { body } = await mockApp
        .post("/users/setStreakHourOffset")
        .set("Authorization", `Bearer ${uid}`)
        .send({ hourOffset: -2 })
        .expect(403);

      //THEN
      expect(body.message).toEqual("Streak hour offset already set");
      expect(setStreakHourOffsetMock).not.toHaveBeenCalled();
      expect(addImportantLogMock).not.toHaveBeenCalled();
    });
    it("should fail without mandatory properties", async () => {
      //WHEN
      const { body } = await mockApp
        .post("/users/setStreakHourOffset")
        .set("Authorization", `Bearer ${uid}`)
        .expect(422);

      //THEN
      expect(body).toEqual({
        message: "Invalid request data schema",
        validationErrors: ['"hourOffset" Required'],
      });
    });
    it("should fail with invalid offset", async () => {
      await mockApp
        .post("/users/setStreakHourOffset")
        .set("Authorization", `Bearer ${uid}`)
        .send({ hourOffset: -12 })
        .expect(422);

      await mockApp
        .post("/users/setStreakHourOffset")
        .set("Authorization", `Bearer ${uid}`)
        .send({ hourOffset: 13 })
        .expect(422);

      await mockApp
        .post("/users/setStreakHourOffset")
        .set("Authorization", `Bearer ${uid}`)
        .send({ hourOffset: "UTC-8" })
        .expect(422);
    });
  });
  describe("revoke all token", () => {
    const removeTokensByUidMock = vi.spyOn(AuthUtils, "revokeTokensByUid");
    const addImportantLogMock = vi.spyOn(LogDal, "addImportantLog");

    beforeEach(() => {
      removeTokensByUidMock.mockClear().mockResolvedValue();
      addImportantLogMock.mockClear().mockResolvedValue();
    });
    it("should revoke all tokens", async () => {
      //WHEN
      const { body } = await mockApp
        .post("/users/revokeAllTokens")
        .set("Authorization", `Bearer ${uid}`)
        .expect(200);

      //THEN
      expect(body).toEqual({
        message: "All tokens revoked",
        data: null,
      });
      expect(removeTokensByUidMock).toHaveBeenCalledWith(uid);
      expect(addImportantLogMock).toHaveBeenCalledWith(
        "user_tokens_revoked",
        "",
        uid,
      );
    });
  });
  describe("getCurrentTestActivity", () => {
    const getUserMock = vi.spyOn(UserDal, "getPartialUser");

    afterEach(() => {
      getUserMock.mockClear();
    });
    it("gets", async () => {
      //GIVEN
      vi.useFakeTimers().setSystemTime(1712102400000);
      const user = {
        uid: uid,
        testActivity: {
          "2024": fillYearWithDay(94),
        },
      } as Partial<UserDal.DBUser> as UserDal.DBUser;
      getUserMock.mockResolvedValue(user);

      //WHEN
      const result = await mockApp
        .get("/users/currentTestActivity")
        .set("Authorization", `Bearer ${uid}`)
        .send()
        .expect(200);

      //THEN
      expect(result.body.data.lastDay).toEqual(1712102400000);
      const testsByDays = result.body.data.testsByDays;
      expect(testsByDays).toHaveLength(372);
      expect(testsByDays[6]).toEqual(null); //2023-04-04
      expect(testsByDays[277]).toEqual(null); //2023-12-31
      expect(testsByDays[278]).toEqual(1); //2024-01-01
      expect(testsByDays[371]).toEqual(94); //2024-01
    });
  });
  describe("getStreak", () => {
    const getUserMock = vi.spyOn(UserDal, "getPartialUser");

    afterEach(() => {
      getUserMock.mockClear();
    });
    it("gets", async () => {
      //GIVEN
      const user = {
        uid: uid,
        streak: {
          lastResultTimestamp: 1712102400000,
          length: 42,
          maxLength: 1024,
          hourOffset: 2,
        },
      } as Partial<UserDal.DBUser> as UserDal.DBUser;
      getUserMock.mockResolvedValue(user);

      //WHEN
      const result = await mockApp
        .get("/users/streak")
        .set("Authorization", `Bearer ${uid}`)
        .send()
        .expect(200);

      //THEN
      const streak: UserStreak = result.body.data;
      expect(streak).toEqual({
        lastResultTimestamp: 1712102400000,
        length: 42,
        maxLength: 1024,
        hourOffset: 2,
      });
    });
  });
  describe("get friends", () => {
    const getFriendsMock = vi.spyOn(UserDal, "getFriends");

    beforeEach(() => {
      enableConnectionsEndpoints(true);
      getFriendsMock.mockClear();
    });

    it("gets with premium enabled", async () => {
      //GIVEN
      enablePremiumFeatures(true);
      const friend: UserDal.DBFriend = {
        name: "Bob",
        isPremium: true,
      } as any;
      getFriendsMock.mockResolvedValue([friend]);

      //WHEN
      const { body } = await mockApp
        .get("/users/friends")
        .set("Authorization", `Bearer ${uid}`)
        .expect(200);

      //THEN
      expect(body.data).toEqual([{ name: "Bob", isPremium: true }]);
    });

    it("gets with premium disabled", async () => {
      //GIVEN
      enablePremiumFeatures(false);
      const friend: UserDal.DBFriend = {
        name: "Bob",
        isPremium: true,
      } as any;
      getFriendsMock.mockResolvedValue([friend]);

      //WHEN
      const { body } = await mockApp
        .get("/users/friends")
        .set("Authorization", `Bearer ${uid}`)
        .expect(200);

      //THEN
      expect(body.data).toEqual([{ name: "Bob" }]);
    });

    it("should fail if friends endpoints are disabled", async () => {
      await expectFailForDisabledEndpoint(
        mockApp.get("/users/friends").set("Authorization", `Bearer ${uid}`),
      );
    });

    it("should fail without authentication", async () => {
      await mockApp.get("/users/friends").expect(401);
    });
  });
});

function fillYearWithDay(days: number): number[] {
  const result: number[] = [];
  for (let i = 0; i < days; i++) {
    result.push(i + 1);
  }
  return result;
}

async function enablePremiumFeatures(enabled: boolean): Promise<void> {
  const mockConfig = await configuration;
  mockConfig.users.premium = { ...mockConfig.users.premium, enabled };

  vi.spyOn(Configuration, "getCachedConfiguration").mockResolvedValue(
    mockConfig,
  );
}

async function enableSignup(signUp: boolean): Promise<void> {
  const mockConfig = await configuration;
  mockConfig.users = { ...mockConfig.users, signUp };

  vi.spyOn(Configuration, "getCachedConfiguration").mockResolvedValue(
    mockConfig,
  );
}

async function enableDiscordIntegration(enabled: boolean): Promise<void> {
  const mockConfig = await configuration;
  mockConfig.users.discordIntegration = {
    ...mockConfig.users.discordIntegration,
    enabled,
  };

  vi.spyOn(Configuration, "getCachedConfiguration").mockResolvedValue(
    mockConfig,
  );
}

async function enableResultFilterPresets(enabled: boolean): Promise<void> {
  const mockConfig = await configuration;
  mockConfig.results.filterPresets = {
    ...mockConfig.results.filterPresets,
    enabled,
  };

  vi.spyOn(Configuration, "getCachedConfiguration").mockResolvedValue(
    mockConfig,
  );
}

async function acceptApeKeys(acceptKeys: boolean): Promise<void> {
  const mockConfig = await configuration;
  mockConfig.apeKeys = { ...mockConfig.apeKeys, acceptKeys };

  vi.spyOn(Configuration, "getCachedConfiguration").mockResolvedValue(
    mockConfig,
  );
}

async function enableProfiles(enabled: boolean): Promise<void> {
  const mockConfig = await configuration;
  mockConfig.users.profiles = { ...mockConfig.users.profiles, enabled };

  vi.spyOn(Configuration, "getCachedConfiguration").mockResolvedValue(
    mockConfig,
  );
}
async function enableInbox(enabled: boolean): Promise<void> {
  const mockConfig = await configuration;
  mockConfig.users.inbox = { ...mockConfig.users.inbox, enabled };

  vi.spyOn(Configuration, "getCachedConfiguration").mockResolvedValue(
    mockConfig,
  );
}

async function enableReporting(enabled: boolean): Promise<void> {
  const mockConfig = await configuration;
  mockConfig.quotes.reporting = { ...mockConfig.quotes.reporting, enabled };

  vi.spyOn(Configuration, "getCachedConfiguration").mockResolvedValue(
    mockConfig,
  );
}

async function enableConnectionsEndpoints(enabled: boolean): Promise<void> {
  const mockConfig = await configuration;
  mockConfig.connections = { ...mockConfig.connections, enabled };

  vi.spyOn(Configuration, "getCachedConfiguration").mockResolvedValue(
    mockConfig,
  );
}

async function expectFailForDisabledEndpoint(call: Test): Promise<void> {
  await enableConnectionsEndpoints(false);
  const { body } = await call.expect(503);
  expect(body.message).toEqual("Connections are not available at this time.");
}
