import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
import { setup } from "../../__testData__/controller-test";
import { Test as SuperTest } from "supertest";
import * as ApeKeyDal from "../../../src/dal/ape-keys";
import { ObjectId } from "mongodb";
import * as Configuration from "../../../src/init/configuration";
import * as UserDal from "../../../src/dal/user";

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

describe("ApeKeyController", () => {
  const getUserMock = vi.spyOn(UserDal, "getPartialUser");

  beforeEach(async () => {
    await enableApeKeysEndpoints(true);
    getUserMock.mockResolvedValue(user(uid, {}));
    vi.useFakeTimers();
    vi.setSystemTime(1000);
  });

  afterEach(() => {
    getUserMock.mockClear();
    vi.useRealTimers();
  });

  describe("get ape keys", () => {
    const getApeKeysMock = vi.spyOn(ApeKeyDal, "getApeKeys");

    afterEach(() => {
      getApeKeysMock.mockClear();
    });

    it("should get the users config", async () => {
      //GIVEN
      const keyOne = apeKeyDb(uid);
      const keyTwo = apeKeyDb(uid);
      getApeKeysMock.mockResolvedValue([keyOne, keyTwo]);

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

      //THEN
      expect(body).toHaveProperty("message", "ApeKeys retrieved");
      expect(body.data).toHaveProperty(keyOne._id.toHexString(), {
        name: keyOne.name,
        enabled: keyOne.enabled,
        createdOn: keyOne.createdOn,
        modifiedOn: keyOne.modifiedOn,
        lastUsedOn: keyOne.lastUsedOn,
      });
      expect(body.data).toHaveProperty(keyTwo._id.toHexString(), {
        name: keyTwo.name,
        enabled: keyTwo.enabled,
        createdOn: keyTwo.createdOn,
        modifiedOn: keyTwo.modifiedOn,
        lastUsedOn: keyTwo.lastUsedOn,
      });
      expect(body.data).keys([keyOne._id, keyTwo._id]);

      expect(getApeKeysMock).toHaveBeenCalledWith(uid);
    });
    it("should fail if apeKeys endpoints are disabled", async () => {
      await expectFailForDisabledEndpoint(
        mockApp.get("/ape-keys").set("Authorization", `Bearer ${uid}`),
      );
    });
    it("should fail if user has no apeKey permissions", async () => {
      await expectFailForNoPermissions(
        mockApp.get("/ape-keys").set("Authorization", `Bearer ${uid}`),
      );
    });
  });

  describe("add ape key", () => {
    const addApeKeyMock = vi.spyOn(ApeKeyDal, "addApeKey");
    const countApeKeysMock = vi.spyOn(ApeKeyDal, "countApeKeysForUser");

    beforeEach(() => {
      countApeKeysMock.mockResolvedValue(0);
    });

    afterEach(() => {
      addApeKeyMock.mockClear();
      countApeKeysMock.mockClear();
    });

    it("should add ape key", async () => {
      //GIVEN
      addApeKeyMock.mockResolvedValue("1");

      //WHEN
      const { body } = await mockApp
        .post("/ape-keys")
        .set("Authorization", `Bearer ${uid}`)
        .send({ name: "test", enabled: true })
        .expect(200);

      expect(body.message).toEqual("ApeKey generated");
      expect(body.data).keys("apeKey", "apeKeyDetails", "apeKeyId");
      expect(body.data.apeKey).not.toBeNull();

      expect(body.data.apeKeyDetails).toStrictEqual({
        createdOn: 1000,
        enabled: true,
        lastUsedOn: -1,
        modifiedOn: 1000,
        name: "test",
      });

      expect(body.data.apeKeyId).toEqual("1");

      expect(addApeKeyMock).toHaveBeenCalledWith(
        expect.objectContaining({
          createdOn: 1000,
          enabled: true,
          lastUsedOn: -1,
          modifiedOn: 1000,
          name: "test",
          uid: uid,
          useCount: 0,
        }),
      );
    });
    it("should fail without mandatory properties", async () => {
      //WHEN
      const { body } = await mockApp
        .post("/ape-keys")
        .send({})
        .set("Authorization", `Bearer ${uid}`)
        .expect(422);

      //THEN
      expect(body).toStrictEqual({
        message: "Invalid request data schema",
        validationErrors: [`"name" Required`, `"enabled" Required`],
      });
    });
    it("should fail with extra properties", async () => {
      //WHEN
      const { body } = await mockApp
        .post("/ape-keys")
        .send({ name: "test", enabled: true, extra: "value" })
        .set("Authorization", `Bearer ${uid}`)
        .expect(422);

      //THEN
      expect(body).toStrictEqual({
        message: "Invalid request data schema",
        validationErrors: ["Unrecognized key(s) in object: 'extra'"],
      });
    });

    it("should fail if max apeKeys is reached", async () => {
      //GIVEN
      countApeKeysMock.mockResolvedValue(1);

      //WHEN
      const { body } = await mockApp
        .post("/ape-keys")
        .send({ name: "test", enabled: false })
        .set("Authorization", `Bearer ${uid}`)
        .expect(409);

      //THEN
      expect(body.message).toEqual(
        "Maximum number of ApeKeys have been generated",
      );
    });
    it("should fail if apeKeys endpoints are disabled", async () => {
      await expectFailForDisabledEndpoint(
        mockApp
          .post("/ape-keys")
          .send({ name: "test", enabled: false })
          .set("Authorization", `Bearer ${uid}`),
      );
    });
    it("should fail if user has no apeKey permissions", async () => {
      await expectFailForNoPermissions(
        mockApp
          .post("/ape-keys")
          .send({ name: "test", enabled: false })
          .set("Authorization", `Bearer ${uid}`),
      );
    });
  });

  describe("edit ape key", () => {
    const editApeKeyMock = vi.spyOn(ApeKeyDal, "editApeKey");
    const apeKeyId = new ObjectId().toHexString();

    afterEach(() => {
      editApeKeyMock.mockClear();
    });

    it("should edit ape key", async () => {
      //GIVEN
      editApeKeyMock.mockResolvedValue();

      //WHEN
      const { body } = await mockApp
        .patch(`/ape-keys/${apeKeyId}`)
        .send({ name: "new", enabled: false })
        .set("Authorization", `Bearer ${uid}`)
        .expect(200);

      //THEN
      expect(body.message).toEqual("ApeKey updated");
      expect(editApeKeyMock).toHaveBeenCalledWith(uid, apeKeyId, "new", false);
    });
    it("should edit ape key with single property", async () => {
      //GIVEN
      editApeKeyMock.mockResolvedValue();

      //WHEN
      const { body } = await mockApp
        .patch(`/ape-keys/${apeKeyId}`)
        .send({ name: "new" })
        .set("Authorization", `Bearer ${uid}`)
        .expect(200);

      //THEN
      expect(body.message).toEqual("ApeKey updated");
      expect(editApeKeyMock).toHaveBeenCalledWith(
        uid,
        apeKeyId,
        "new",
        undefined,
      );
    });
    it("should fail with missing path", async () => {
      //GIVEN

      //WHEN
      await mockApp
        .patch(`/ape-keys/`)
        .set("Authorization", `Bearer ${uid}`)
        .expect(404);
    });
    it("should fail with extra properties", async () => {
      //GIVEN

      //WHEN
      const { body } = await mockApp
        .patch(`/ape-keys/${apeKeyId}`)
        .send({ name: "new", extra: "value" })
        .set("Authorization", `Bearer ${uid}`)
        .expect(422);

      //THEN
      expect(body).toStrictEqual({
        message: "Invalid request data schema",
        validationErrors: ["Unrecognized key(s) in object: 'extra'"],
      });
    });
    it("should fail if apeKeys endpoints are disabled", async () => {
      await expectFailForDisabledEndpoint(
        mockApp
          .patch(`/ape-keys/${apeKeyId}`)
          .send({ name: "test", enabled: false })
          .set("Authorization", `Bearer ${uid}`),
      );
    });
    it("should fail if user has no apeKey permissions", async () => {
      await expectFailForNoPermissions(
        mockApp
          .patch(`/ape-keys/${apeKeyId}`)
          .send({ name: "test", enabled: false })
          .set("Authorization", `Bearer ${uid}`),
      );
    });
  });
  describe("delete ape key", () => {
    const deleteApeKeyMock = vi.spyOn(ApeKeyDal, "deleteApeKey");
    const apeKeyId = new ObjectId().toHexString();

    afterEach(() => {
      deleteApeKeyMock.mockClear();
    });

    it("should delete ape key", async () => {
      //GIVEN

      deleteApeKeyMock.mockResolvedValue();
      //WHEN
      const { body } = await mockApp
        .delete(`/ape-keys/${apeKeyId}`)
        .set("Authorization", `Bearer ${uid}`)
        .expect(200);

      //THEN
      expect(body.message).toEqual("ApeKey deleted");
      expect(deleteApeKeyMock).toHaveBeenCalledWith(uid, apeKeyId);
    });
    it("should fail with missing path", async () => {
      //GIVEN

      //WHEN
      await mockApp
        .delete(`/ape-keys/`)
        .set("Authorization", `Bearer ${uid}`)
        .expect(404);
    });
    it("should fail if apeKeys endpoints are disabled", async () => {
      await expectFailForDisabledEndpoint(
        mockApp
          .delete(`/ape-keys/${apeKeyId}`)
          .set("Authorization", `Bearer ${uid}`),
      );
    });

    it("should fail if user has no apeKey permissions", async () => {
      await expectFailForNoPermissions(
        mockApp
          .delete(`/ape-keys/${apeKeyId}`)
          .set("Authorization", `Bearer ${uid}`),
      );
    });
  });
  async function expectFailForNoPermissions(call: SuperTest): Promise<void> {
    getUserMock.mockResolvedValue(user(uid, { canManageApeKeys: false }));
    const { body } = await call.expect(403);
    expect(body.message).toEqual(
      "You have lost access to ape keys, please contact support",
    );
  }
  async function expectFailForDisabledEndpoint(call: SuperTest): Promise<void> {
    await enableApeKeysEndpoints(false);
    const { body } = await call.expect(503);
    expect(body.message).toEqual("ApeKeys are currently disabled.");
  }
});

function apeKeyDb(
  uid: string,
  data?: Partial<ApeKeyDal.DBApeKey>,
): ApeKeyDal.DBApeKey {
  return {
    _id: new ObjectId(),
    uid,
    hash: "hash",
    useCount: 1,
    name: "name",
    enabled: true,
    createdOn: Math.random() * Date.now(),
    lastUsedOn: Math.random() * Date.now(),
    modifiedOn: Math.random() * Date.now(),
    ...data,
  };
}

async function enableApeKeysEndpoints(enabled: boolean): Promise<void> {
  const mockConfig = await configuration;
  mockConfig.apeKeys = {
    ...mockConfig.apeKeys,
    endpointsEnabled: enabled,
    maxKeysPerUser: 1,
  };

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

function user(uid: string, data: Partial<UserDal.DBUser>): UserDal.DBUser {
  return {
    uid,
    ...data,
  } as UserDal.DBUser;
}
