import { Color, Model } from "../../src";
import { ColorPicker } from "../../src/components/color_picker/color_picker";
import { toHex } from "../../src/helpers/color";
import { PropsOf } from "../../src/types/props_of";
import { registerCleanup } from "../setup/jest.setup";
import { setFormatting, updateColorScheme } from "../test_helpers/commands_helpers";
import {
  getElComputedStyle,
  setInputValueAndTrigger,
  simulateClick,
} from "../test_helpers/dom_helper";
import { mountComponentWithPortalTarget, nextTick } from "../test_helpers/helpers";

let fixture: HTMLElement;

function addMockEyeDropperSupport(mockColor: Color) {
  globalThis.EyeDropper = class {
    open() {
      return Promise.resolve({ sRGBHex: mockColor });
    }
  };
  registerCleanup(() => delete globalThis.EyeDropper);
}

async function mountColorPicker(
  partialProps: Partial<PropsOf<ColorPicker>> = {},
  model = new Model()
) {
  const props = {
    onColorPicked: partialProps.onColorPicked || (() => {}),
    currentColor: partialProps.currentColor || "#000000",
    maxHeight: partialProps.maxHeight !== undefined ? partialProps.maxHeight : 1000,
    anchorRect: partialProps.anchorRect || { x: 0, y: 0, width: 0, height: 0 },
    disableNoColor: partialProps.disableNoColor || false,
  };
  ({ fixture } = await mountComponentWithPortalTarget(ColorPicker, { model, props }));
  return model;
}

test("Color picker is correctly positioned", async () => {
  await mountColorPicker({ anchorRect: { x: 100, y: 100, width: 50, height: 50 } });
  expect(getElComputedStyle(".o-popover", "left")).toEqual("100px");
  expect(getElComputedStyle(".o-popover", "top")).toEqual("150px");
});

describe("Color Picker buttons", () => {
  test("Click on 'reset' button reset the cell style", async () => {
    const onColorPicked = jest.fn();
    await mountColorPicker({ currentColor: "#45818e", onColorPicked });
    await simulateClick(".o-cancel");
    expect(onColorPicked).toHaveBeenCalledWith("");
  });

  test("Clicking on '+', custom section except custom added colors must toggle the custom color picker", async () => {
    await mountColorPicker();
    const elements = fixture.querySelectorAll(".o-color-picker-toggler");
    for (const element of elements) {
      await simulateClick(element);
      expect(fixture.querySelector(".o-custom-selector")).toBeDefined();
      await simulateClick(element);
      expect(fixture.querySelector(".o-custom-selector")).toBeNull();
    }
  });

  test("Full component rendering", async () => {
    addMockEyeDropperSupport("#123456");
    await mountColorPicker();
    await simulateClick(".o-color-picker-toggler-sign");
    expect(fixture.querySelector(".o-color-picker")).toMatchSnapshot();
  });

  test("Can pick a standard color", async () => {
    const onColorPicked = jest.fn();
    await mountColorPicker({ onColorPicked });
    await simulateClick("div[data-color='#FF9900']");
    expect(onColorPicked).toHaveBeenCalledWith("#FF9900");
  });

  test("Clicking a custom color picks it without opening the gradient", async () => {
    const model = new Model();
    setFormatting(model, "A1", { fillColor: "#123456" });
    const onColorPicked = jest.fn();
    await mountColorPicker({ onColorPicked }, model);
    await simulateClick("div[data-color='#123456']");
    expect(onColorPicked).toHaveBeenCalledWith("#123456");
    expect(fixture.querySelector(".o-custom-selector")).toBeNull();
  });

  test("Can pick a custom color in the gradient", async () => {
    const onColorPicked = jest.fn();
    await mountColorPicker({ onColorPicked });
    await simulateClick(".o-color-picker-toggler");
    await simulateClick(".o-gradient");
    const inputCodeEl = fixture.querySelector(".o-custom-input-preview input") as HTMLInputElement;
    const previewColor = toHex(getElComputedStyle(".o-color-preview", "backgroundColor"));
    const inputColorCode = inputCodeEl.value;
    expect(previewColor).toBeSameColorAs(inputColorCode);
    await simulateClick(".o-add-button");
    expect(onColorPicked).toHaveBeenCalledWith(inputColorCode);
  });

  test("Can choose a custom color with the input", async () => {
    const onColorPicked = jest.fn();
    await mountColorPicker({ onColorPicked });
    await simulateClick(".o-color-picker-toggler");
    await simulateClick(".o-gradient");
    const color = "#12EF78";
    await setInputValueAndTrigger(".o-custom-input-preview input", color);
    const previewColor = toHex(getElComputedStyle(".o-color-preview", "backgroundColor"));
    // hex <-> hsla is not a bijection, this specific color
    // is not exactly the same when processed
    expect(previewColor).toBeSameColorAs(color, 0.01);
    await simulateClick(".o-add-button");
    expect(onColorPicked).toHaveBeenCalledWith(color);
  });

  test("Color from the input is sanitized", async () => {
    const onColorPicked = jest.fn();
    await mountColorPicker({ onColorPicked });
    await simulateClick(".o-color-picker-toggler");
    await simulateClick(".o-gradient");
    const color = "12ef78";
    await setInputValueAndTrigger(".o-custom-input-preview input", color);
    await simulateClick(".o-add-button");
    expect(onColorPicked).toHaveBeenCalledWith(toHex(color));
  });

  test("Cannot input an invalid color code", async () => {
    const onColorPicked = jest.fn();
    await mountColorPicker({ onColorPicked });
    await simulateClick(".o-color-picker-toggler");
    const target = document.querySelector(".o-custom-input-preview input");
    await setInputValueAndTrigger(target, "this is not a color");

    expect(fixture.querySelector(".o-wrong-color")).not.toBeNull();
    const addButton = fixture.querySelector(".o-add-button")!;
    expect(addButton.classList).toContain("o-disabled");
    await simulateClick(addButton);
    expect(onColorPicked).not.toHaveBeenCalled();
  });

  test("warning border of invalid color disappears when selecting on the gradient", async () => {
    const onColorPicked = jest.fn();
    await mountColorPicker({ onColorPicked });
    await simulateClick(".o-color-picker-toggler");
    await setInputValueAndTrigger(".o-custom-input-preview input", "this is not a color");
    await simulateClick(".o-add-button");
    expect(fixture.querySelector(".o-wrong-color")).not.toBeNull();
    await simulateClick(".o-gradient");
    expect(fixture.querySelector(".o-wrong-color")).toBeNull();
    await simulateClick(".o-add-button");
    expect(onColorPicked).toHaveBeenCalled();
  });

  test("initial standard color", async () => {
    await mountColorPicker({ currentColor: "#45818e" });
    const color = fixture.querySelector("div[data-color='#45818E']") as HTMLElement;
    expect(color?.textContent).toBe(" ✓ ");
  });

  test("initial custom color", async () => {
    const model = new Model();
    setFormatting(model, "A1", { fillColor: "#123456" });
    await mountColorPicker({ currentColor: "#123456" }, model);
    const color = fixture.querySelector("div[data-color='#123456']") as HTMLElement;
    expect(color?.textContent).toBe(" ✓ ");
  });

  test("wrong initial color", async () => {
    await mountColorPicker({ currentColor: "azeazeaze" });
    await simulateClick(".o-color-picker-toggler");
    const inputCodeEl = fixture.querySelector(".o-custom-input-preview input") as HTMLInputElement;
    expect(inputCodeEl.value).toBe("");
  });

  test("color picker disappears when maxHeight is 0", async () => {
    await mountColorPicker({ currentColor: "#45818e", maxHeight: 0 });
    const picker = fixture.querySelector<HTMLElement>(".o-color-picker")!;
    expect(picker.style["display"]).toEqual("none");
  });

  test("Hides the 'No Color' button when disableNoColor prop is set to true", async () => {
    await mountColorPicker({ disableNoColor: true });
    expect(fixture.querySelector(".o-buttons .o-cancel")).toBeNull();
  });

  test.each([
    "#fff",
    "##fff",
    "fff",
    "#ffffff",
    "##ffffff",
    "ffffff",
    "#FFFFFF00", // Hex + alpha
  ])("Can input a custom HEX code, alpha is ignored", async (hexCode) => {
    await mountColorPicker();
    await simulateClick(".o-color-picker-toggler");

    const inputTarget = fixture.querySelector(".o-custom-input-preview input")!;
    await setInputValueAndTrigger(inputTarget, hexCode as Color);
    expect((inputTarget as HTMLInputElement).value).toBeSameColorAs(
      hexCode.replace("##", "#").slice(0, 7)
    );
    const addButton = fixture.querySelector(".o-add-button")!;
    expect(addButton.classList).not.toContain("o-disabled");
  });

  test.each([
    "#fff#000",
    "rgb(1,1,1)", // rgb
    "rgb(1,1,1,0.5)", // rgba
  ])("refuse non strictly HEX codes", async (hexCode) => {
    await mountColorPicker();
    await simulateClick(".o-color-picker-toggler");

    const inputTarget = fixture.querySelector(".o-custom-input-preview input")!;
    await setInputValueAndTrigger(inputTarget, hexCode as Color);
    expect((inputTarget as HTMLInputElement).value).toBe(hexCode.slice(0, 7));
    const addButton = fixture.querySelector(".o-add-button")!;
    expect(addButton.classList).toContain("o-disabled");
  });

  test("Eye dropper button is not there if the EyeDropper API is not present", async () => {
    await mountColorPicker();
    expect(".eyedropper").toHaveCount(0);
  });

  test("Eye dropper button is not there in dark mode", async () => {
    // In dark mode we'd have to do the opposite color inversion than then dark mode color inversion to get the correct color.
    // But this mathematically cannot always be done, because our color inversion lose information with clipping and rounding.
    addMockEyeDropperSupport("#abc123");
    const model = await mountColorPicker({});
    expect(".eyedropper").toHaveCount(1);

    updateColorScheme(model, "dark");
    await nextTick();
    expect(".eyedropper").toHaveCount(0);
  });

  test("Can pick a custom color with the eye dropper", async () => {
    addMockEyeDropperSupport("#abc123");
    const onColorPicked = jest.fn();
    await mountColorPicker({ onColorPicked });
    await simulateClick(".eyedropper");
    expect(onColorPicked).toHaveBeenCalledWith("#ABC123");
  });
});
