import { describe, it, expect, beforeEach, vi } from "vitest";
import { Difficulty, Mode, Mode2 } from "@monkeytype/schemas/shared";
import { compressToURI } from "lz-ts";
import * as UpdateConfig from "../../src/ts/config/setters";
import * as Notifications from "../../src/ts/states/notifications";
import * as TestLogic from "../../src/ts/test/test-logic";
import * as TestState from "../../src/ts/test/test-state";
import * as Misc from "../../src/ts/utils/misc";
import { FunboxName } from "@monkeytype/schemas/configs";
import { CustomTextSettings } from "@monkeytype/schemas/results";
import { loadTestSettingsFromUrl } from "../../src/ts/controllers/url-handler";

//mock modules to avoid dependencies
vi.mock("../../src/ts/test/test-logic", () => ({
  restart: vi.fn(),
}));

describe("url-handler", () => {
  describe("loadTestSettingsFromUrl", () => {
    const findGetParameterMock = vi.spyOn(Misc, "findGetParameter");

    const setConfigMock = vi.spyOn(UpdateConfig, "setConfig");
    const setSelectedQuoteIdMock = vi.spyOn(TestState, "setSelectedQuoteId");
    const restartTestMock = vi.spyOn(TestLogic, "restart");
    const notifySuccessMock = vi.spyOn(
      Notifications,
      "showSuccessNotification",
    );
    const notifyMock = vi.spyOn(Notifications, "showNoticeNotification");

    beforeEach(() => {
      [
        setConfigMock,
        findGetParameterMock,
        setSelectedQuoteIdMock,
        restartTestMock,
        notifySuccessMock,
        notifyMock,
      ].forEach((it) => it.mockClear());

      findGetParameterMock.mockImplementation((override) => override);
    });

    it("handles null", () => {
      //GIVEN
      findGetParameterMock.mockReturnValue("null");

      //WHEN
      loadTestSettingsFromUrl("");

      //THEN
      expect(setConfigMock).not.toHaveBeenCalled();
    });
    it("handles mode2 as number", () => {
      //GIVEN
      findGetParameterMock.mockReturnValue(
        urlData({ mode: "time", mode2: 60 }),
      );

      //WHEN
      loadTestSettingsFromUrl("");

      //THEN
      expect(setConfigMock).toHaveBeenCalledWith("mode", "time", {
        nosave: true,
      });
      expect(setConfigMock).toHaveBeenCalledWith("time", 60, {
        nosave: true,
      });
      expect(restartTestMock).toHaveBeenCalled();
    });
    it("sets time", () => {
      //GIVEN
      findGetParameterMock.mockReturnValue(
        urlData({ mode: "time", mode2: "30" }),
      );

      //WHEN
      loadTestSettingsFromUrl("");

      //THEN
      expect(setConfigMock).toHaveBeenCalledWith("mode", "time", {
        nosave: true,
      });
      expect(setConfigMock).toHaveBeenCalledWith("time", 30, {
        nosave: true,
      });
      expect(restartTestMock).toHaveBeenCalled();
    });
    it("sets word count", () => {
      //GIVEN
      findGetParameterMock.mockReturnValue(
        urlData({ mode: "words", mode2: "50" }),
      );

      //WHEN
      loadTestSettingsFromUrl("");

      //THEN
      expect(setConfigMock).toHaveBeenCalledWith("mode", "words", {
        nosave: true,
      });
      expect(setConfigMock).toHaveBeenCalledWith("words", 50, {
        nosave: true,
      });
      expect(restartTestMock).toHaveBeenCalled();
    });
    it("sets quote length", () => {
      //GIVEN
      findGetParameterMock.mockReturnValue(
        urlData({ mode: "quote", mode2: "512" }),
      );

      //WHEN
      loadTestSettingsFromUrl("");

      //THEN
      expect(setConfigMock).toHaveBeenCalledWith("mode", "quote", {
        nosave: true,
      });
      expect(setConfigMock).toHaveBeenCalledWith("quoteLength", [-2]);
      expect(setSelectedQuoteIdMock).toHaveBeenCalledWith(512);
      expect(restartTestMock).toHaveBeenCalled();
    });
    it("sets punctuation", () => {
      //GIVEN
      findGetParameterMock.mockReturnValue(urlData({ punctuation: true }));

      //WHEN
      loadTestSettingsFromUrl("");

      //THEN
      expect(setConfigMock).toHaveBeenCalledWith("punctuation", true, {
        nosave: true,
      });
      expect(restartTestMock).toHaveBeenCalled();
    });
    it("sets numbers", () => {
      //GIVEN
      findGetParameterMock.mockReturnValue(urlData({ numbers: false }));

      //WHEN
      loadTestSettingsFromUrl("");

      //THEN
      expect(setConfigMock).toHaveBeenCalledWith("numbers", false, {
        nosave: true,
      });
      expect(restartTestMock).toHaveBeenCalled();
    });
    it("sets language", () => {
      //GIVEN
      findGetParameterMock.mockReturnValue(urlData({ language: "english" }));

      //WHEN
      loadTestSettingsFromUrl("");

      //THEN
      expect(setConfigMock).toHaveBeenCalledWith("language", "english", {
        nosave: true,
      });
      expect(restartTestMock).toHaveBeenCalled();
    });
    it("sets difficulty", () => {
      //GIVEN
      findGetParameterMock.mockReturnValue(urlData({ difficulty: "master" }));

      //WHEN
      loadTestSettingsFromUrl("");

      //THEN
      expect(setConfigMock).toHaveBeenCalledWith("difficulty", "master", {
        nosave: true,
      });
      expect(restartTestMock).toHaveBeenCalled();
    });
    it("sets funbox", () => {
      //GIVEN
      findGetParameterMock.mockReturnValue(
        urlData({ funbox: ["crt", "choo_choo"] }),
      );

      //WHEN
      loadTestSettingsFromUrl("");

      //THEN
      expect(setConfigMock).toHaveBeenCalledWith(
        "funbox",
        ["crt", "choo_choo"],
        {
          nosave: true,
        },
      );
      expect(restartTestMock).toHaveBeenCalled();
    });
    it("sets funbox legacy", () => {
      //GIVEN
      findGetParameterMock.mockReturnValue(
        urlData({ funbox: "crt#choo_choo" }),
      );

      //WHEN
      loadTestSettingsFromUrl("");

      //THEN
      expect(setConfigMock).toHaveBeenCalledWith(
        "funbox",
        ["crt", "choo_choo"],
        {
          nosave: true,
        },
      );
      expect(restartTestMock).toHaveBeenCalled();
    });
    it("adds notification", () => {
      //GIVEN
      findGetParameterMock.mockReturnValue(
        urlData({
          mode: "time",
          mode2: "60",
          customText: {
            text: ["abcabc"],
            limit: { value: 5, mode: "time" },
            mode: "random",
            pipeDelimiter: true,
          },
          punctuation: true,
          numbers: true,
          language: "english",
          difficulty: "master",
          funbox: ["ascii", "crt"],
        }),
      );

      //WHEN
      loadTestSettingsFromUrl("");

      //THEN
      expect(notifySuccessMock).toHaveBeenCalledWith(expect.anything(), {
        durationMs: 10000,
        useInnerHtml: true,
      });
    });
    it("rejects invalid values", () => {
      //GIVEN
      findGetParameterMock.mockReturnValue(
        urlData({
          mode: "invalidMode",
          mode2: "invalidMode2",
          customText: {
            text: "invalid",
            limit: "invalid",
            mode: "invalid",
            pipeDelimiter: "invalid",
          },
          punctuation: "invalid",
          numbers: "invalid",
          language: "invalid",
          difficulty: "invalid",
          funbox: ["invalid"],
        } as any),
      );

      //WHEN
      loadTestSettingsFromUrl("");

      //THEN
      expect(notifyMock).toHaveBeenCalledWith(
        `Failed to load test settings from URL: JSON does not match schema: "0" invalid enum value. expected 'time' | 'words' | 'quote' | 'custom' | 'zen', received 'invalidmode', "1" needs to be a number or a number represented as a string e.g. "10"., "2.mode" invalid enum value. expected 'repeat' | 'random' | 'shuffle', received 'invalid', "2.pipeDelimiter" expected boolean, received string, "2.limit" expected object, received string, "2.text" expected array, received string, "3" expected boolean, received string, "4" expected boolean, received string, "6" invalid enum value. expected 'normal' | 'expert' | 'master', received 'invalid', "7" invalid input`,
      );
    });
  });
});

const urlData = (
  data: Partial<{
    mode: Mode | undefined;
    mode2: Mode2<any> | number;
    customText: CustomTextSettings;
    punctuation: boolean;
    numbers: boolean;
    language: string;
    difficulty: Difficulty;
    funbox: FunboxName[] | string;
  }>,
): string => {
  return compressToURI(
    JSON.stringify([
      data.mode ?? null,
      data.mode2 ?? null,
      data.customText ?? null,
      data.punctuation ?? null,
      data.numbers ?? null,
      data.language ?? null,
      data.difficulty ?? null,
      data.funbox ?? null,
    ]),
  );
};
