import { CellIsRule, HeaderIndex, Model } from "../../src";
import { CellComposerStore } from "../../src/components/composer/composer/cell_composer_store";
import {
  DEFAULT_CELL_HEIGHT,
  DEFAULT_CELL_WIDTH,
  FONT_SIZES,
  HEADER_HEIGHT,
  HEADER_WIDTH,
} from "../../src/constants";
import { colors, toHex } from "../../src/helpers/color";
import { toZone } from "../../src/helpers/zones";
import { ViewportsStore } from "../../src/stores/viewports_store";
import { SpreadsheetChildEnv } from "../../src/types/spreadsheet_env";
import { Store } from "../../src/types/store_engine";
import {
  activateSheet,
  addCfRule,
  addEqualCf,
  copy,
  createSheet,
  createTable,
  lockSheet,
  paste,
  renameSheet,
  resizeColumns,
  resizeRows,
  selectCell,
  setCellContent,
  setFormatting,
  setSelection,
  setViewportOffset,
  updateLocale,
} from "../test_helpers/commands_helpers";
import { FR_LOCALE } from "../test_helpers/constants";
import {
  click,
  clickCell,
  getComposerColors,
  getElComputedStyle,
  gridMouseEvent,
  keyDown,
  keyUp,
  rightClickCell,
  selectColumnByClicking,
  simulateClick,
  triggerMouseEvent,
  triggerWheelEvent,
} from "../test_helpers/dom_helper";
import {
  getActivePosition,
  getCellContent,
  getCellRawContent,
  getCellText,
  getSelectionAnchorCellXc,
  getTable,
} from "../test_helpers/getters_helpers";
import {
  getInputSelection,
  mountSpreadsheet,
  nextTick,
  startGridComposition,
  typeInComposerGrid as typeInComposerGridHelper,
  typeInComposerTopBar as typeInComposerTopBarHelper,
} from "../test_helpers/helpers";
import { addPivot } from "../test_helpers/pivot_helpers";

let fixture: HTMLElement;
let model: Model;
let composerStore: Store<CellComposerStore>;
let env: SpreadsheetChildEnv;

async function startComposition(key?: string) {
  const composerEl = await startGridComposition(key);
  return composerEl;
}

async function typeInComposerGrid(text: string, fromScratch: boolean = true) {
  const composerEl = await typeInComposerGridHelper(text, fromScratch);
  return composerEl;
}

async function typeInComposerTopBar(text: string, fromScratch: boolean = true) {
  const composerEl = await typeInComposerTopBarHelper(text, fromScratch);
  return composerEl;
}

const modelData = { sheets: [{ id: "sh1" }] };

describe("Composer interactions", () => {
  beforeEach(async () => {
    ({ model, fixture, env } = await mountSpreadsheet({ model: new Model(modelData) }));
    composerStore = env.getStore(CellComposerStore);
  });

  test("type in grid composer adds text to topbar composer", async () => {
    await keyDown({ key: "Enter" });
    const gridComposer = document.querySelector(".o-grid .o-composer");
    const topBarComposer = document.querySelector(".o-spreadsheet-topbar .o-composer");
    expect(document.activeElement).toBe(gridComposer);
    await typeInComposerGrid("text");
    expect(topBarComposer!.textContent).toBe("text");
    expect(gridComposer!.textContent).toBe("text");
  });

  test("type in topbar composer adds text to grid composer", async () => {
    await click(fixture, ".o-spreadsheet-topbar .o-composer");
    const topBarComposer = document.querySelector(".o-spreadsheet-topbar .o-composer");
    const gridComposer = document.querySelector(".o-grid .o-composer");
    expect(topBarComposer).not.toBeNull();
    expect(document.activeElement).toBe(topBarComposer);
    expect(gridComposer).not.toBeNull();
    await typeInComposerTopBar("text");
    expect(topBarComposer!.textContent).toBe("text");
    expect(gridComposer!.textContent).toBe("text");
  });

  test("start typing in topbar composer then continue in grid composer", async () => {
    await click(fixture, ".o-spreadsheet-topbar .o-composer");
    const topBarComposer = document.querySelector(".o-spreadsheet-topbar .o-composer");
    const gridComposer = document.querySelector(".o-grid .o-composer");

    // Type in top bar composer
    await typeInComposerTopBar("from topbar");
    expect(topBarComposer!.textContent).toBe("from topbar");
    expect(gridComposer!.textContent).toBe("from topbar");

    // Focus grid composer and type
    await click(fixture, ".o-grid .o-composer");
    await typeInComposerGrid("from grid", false);
    expect(topBarComposer!.textContent).toBe("from topbarfrom grid");
    expect(gridComposer!.textContent).toBe("from topbarfrom grid");
  });

  test("top bar composer display active cell content", async () => {
    setCellContent(model, "A2", "Hello");
    selectCell(model, "A2");
    await nextTick();
    const topBarComposer = document.querySelector(".o-spreadsheet-topbar .o-composer");
    expect(topBarComposer!.textContent).toBe("Hello");
  });

  test("top bar composer displays formatted date cell content", async () => {
    setCellContent(model, "A2", "10/10/2021");
    selectCell(model, "A2");
    await nextTick();
    const topBarComposer = document.querySelector(".o-spreadsheet-topbar .o-composer")!;
    expect(topBarComposer.textContent).toBe("10/10/2021");
    // Focus top bar composer
    await click(topBarComposer);
    expect(topBarComposer!.textContent).toBe("10/10/2021");
  });

  test("autocomplete disappear when grid composer is blurred", async () => {
    await keyDown({ key: "Enter" });
    const topBarComposer = document.querySelector(".o-spreadsheet-topbar .o-composer")!;
    await typeInComposerGrid("=SU");
    expect(fixture.querySelector(".o-grid .o-autocomplete-dropdown")).not.toBeNull();
    await click(topBarComposer);
    expect(fixture.querySelector(".o-grid .o-autocomplete-dropdown")).toBeNull();
  });

  test("autocomplete disappear when selecting a cell in the grid", async () => {
    setCellContent(model, "B1", "Customer");
    setCellContent(model, "B2", "Alice");

    addPivot(model, "B1:B2", {
      columns: [],
      rows: [],
      measures: [{ id: "Customer:count", fieldName: "Customer", aggregator: "count" }],
    });
    await keyDown({ key: "Enter" });
    await typeInComposerGrid("=PIVOT(");
    expect(fixture.querySelector(".o-grid .o-autocomplete-dropdown")).not.toBeNull();
    await clickCell(env, "B3");
    expect(fixture.querySelector(".o-grid .o-autocomplete-dropdown")).toBeNull();
    await keyDown({ key: "Enter" });
    expect(getCellRawContent(model, "A1")).toBe("=PIVOT(B3)");
  });

  test("autocomplete disappears when there is no match with an unknown character", async () => {
    await typeInComposerGrid("=éSUM");
    await nextTick();
    expect(fixture.querySelector(".o-grid .o-autocomplete-dropdown")).toBeNull();
  });

  test("autocomplete disappear when typing an unknown character", async () => {
    await typeInComposerGrid("=SéSUM");
    await nextTick();
    expect(fixture.querySelector(".o-grid .o-autocomplete-dropdown")).toBeNull();
  });

  test("focus top bar composer does not resize grid composer when autocomplete is displayed", async () => {
    await keyDown({ key: "Enter" });
    const topBarComposer = document.querySelector(".o-spreadsheet-topbar .o-composer")!;
    const gridComposerContainer = document.querySelector(".o-grid-composer")! as HTMLElement;
    const spy = jest.spyOn(gridComposerContainer.style, "width", "set");
    await typeInComposerGrid("=SU");
    await nextTick();
    await click(topBarComposer);
    expect(document.activeElement).toBe(topBarComposer);
    expect(spy).not.toHaveBeenCalled();
  });

  test("selecting ranges multiple times in topbar bar does not resize grid composer", async () => {
    await click(fixture, ".o-spreadsheet-topbar .o-composer");
    const gridComposerContainer = document.querySelector(".o-grid-composer")! as HTMLElement;
    // Type in top bar composer
    await typeInComposerTopBar("=");
    const spy = jest.spyOn(gridComposerContainer.style, "width", "set");
    await nextTick();
    selectCell(model, "B2");
    await nextTick();
    selectCell(model, "B2");
    await nextTick();
    expect(spy).not.toHaveBeenCalled();
  });

  test("Selecting a range should not scroll the viewport to the current Grid selection", async () => {
    const viewStore = env.getStore(ViewportsStore);
    const { top, bottom, left, right } = viewStore.activeMainViewport;
    await typeInComposerTopBar("=");
    // scroll
    triggerWheelEvent(".o-grid", { deltaY: 3 * DEFAULT_CELL_HEIGHT });
    await nextTick();
    const scrolledViewport = viewStore.activeMainViewport;
    expect(scrolledViewport).toMatchObject({
      left,
      right,
      top: top + 3,
      bottom: bottom + 3,
    });
    expect(viewStore.activeSheetScrollInfo).toMatchObject({
      scrollY: 3 * DEFAULT_CELL_HEIGHT,
    });
    await clickCell(env, "E5");
    expect(model.getters.getSelectedZones()).toEqual([toZone("A1")]);
    expect(viewStore.activeMainViewport).toMatchObject(scrolledViewport);
  });

  test("type '=' and click Cell, the cell ref should be colored", async () => {
    const composerEl = await typeInComposerGrid("=");
    expect(composerEl.textContent).toBe("=");
    expect(composerEl.querySelector(".selector-flag")).toBeTruthy();
    expect(getInputSelection()).toEqual({
      anchorNodeText: "=",
      anchorOffset: 1,
      focusNodeText: "=",
      focusOffset: 1,
    });
    expect(composerStore.editionMode).toBe("selecting");
    await clickCell(env, "C8");
    expect(composerEl.textContent).toBe("=C8");
    expect(getComposerColors(composerEl)["C8"]).toBeSameColorAs(colors[0]);
  });

  test("=+Click range, the range ref should be colored", async () => {
    const composerEl = await typeInComposerGrid("=");
    await gridMouseEvent(env, "pointerdown", "C8");
    await gridMouseEvent(env, "pointermove", "B8");
    await gridMouseEvent(env, "pointerup", "B8");
    await nextTick();
    expect(composerEl.textContent).toBe("=B8:C8");
    expect(getComposerColors(composerEl)["B8:C8"]).toBeSameColorAs(colors[0]);
  });

  test("type '=', and click a cell several times", async () => {
    const composerEl = await typeInComposerGrid("=");
    await clickCell(env, "C8");
    await clickCell(env, "C8");
    expect(composerEl.textContent).toBe("=C8");
    await clickCell(env, "C7");
    expect(composerEl.textContent).toBe("=C7");
  });

  test("grid composer is not visible when not editing", async () => {
    expect(composerStore.editionMode).toBe("inactive");
    const gridComposerEl = fixture.querySelector(".o-grid-composer") as HTMLDivElement;
    expect(gridComposerEl.style.zIndex).toBe("-1000");
    await startComposition();
    expect(gridComposerEl.style.zIndex).toBe("");
  });

  test("starting the edition with enter, the composer should have the focus", async () => {
    await startComposition();
    expect(composerStore.editionMode).toBe("editing");
    expect(getActivePosition(model)).toBe("A1");
    expect(document.activeElement).toBe(fixture.querySelector(".o-grid div.o-composer")!);
  });

  test("pressing TAB in edit mode cycles within the selection", async () => {
    setSelection(model, ["A1:B2"], { anchor: "B1" });
    await keyDown({ key: "F2" });
    expect(composerStore.editionMode).toBe("editing");
    await keyDown({ key: "Tab" });
    expect(getSelectionAnchorCellXc(model)).toBe("A2");
    expect(model.getters.getSelectedZone()).toEqual(toZone("A1:B2"));
    expect(composerStore.editionMode).toBe("inactive");
  });

  test("pressing ENTER in edit mode cycles within the selection", async () => {
    setSelection(model, ["A1:B2"], { anchor: "A2" });
    await keyDown({ key: "F2" });
    expect(composerStore.editionMode).toBe("editing");
    await keyDown({ key: "Enter" });
    expect(getSelectionAnchorCellXc(model)).toBe("B1");
    expect(model.getters.getSelectedZone()).toEqual(toZone("A1:B2"));
    expect(composerStore.editionMode).toBe("inactive");
  });

  test("Starting the edition should not display the cell reference", async () => {
    setSelection(model, ["A1:A2"]);
    await startComposition();
    expect(fixture.querySelector(".o-grid div.o-cell-reference")).toBeNull();
  });

  test("Starting the edition and scroll should display the cell reference", async () => {
    await startComposition();
    setViewportOffset(env, 0, DEFAULT_CELL_HEIGHT * 5);
    await nextTick();
    const reference = fixture.querySelector(".o-grid div.o-cell-reference");
    expect(reference).not.toBeNull();
    expect(reference!.textContent).toBe("A1");
  });

  test("Starting the edition and change sheet should display the cell reference with the sheet name", async () => {
    createSheet(model, { sheetId: "sheet2" });
    await startComposition("=");
    activateSheet(model, "sheet2");
    await nextTick();
    const reference = fixture.querySelector(".o-grid div.o-cell-reference");
    expect(reference).not.toBeNull();
    expect(reference!.textContent).toBe("Sheet1!A1");
  });

  test("Starting the edition and change sheet should display the cell reference with the sheet name, with quotes if needed", async () => {
    renameSheet(model, model.getters.getActiveSheetId(), "My beautiful name");
    createSheet(model, { sheetId: "sheet2" });
    await startComposition("=");
    activateSheet(model, "sheet2");
    await nextTick();
    const reference = fixture.querySelector(".o-grid div.o-cell-reference");
    expect(reference).not.toBeNull();
    expect(reference!.textContent).toBe("'My beautiful name'!A1");
  });

  test("Stopping the edition resets the cell reference visibility", async () => {
    await startComposition();
    setViewportOffset(env, 0, DEFAULT_CELL_HEIGHT * 5);
    await nextTick();
    const referenceSelector = ".o-grid div.o-cell-reference";
    const reference = fixture.querySelector(referenceSelector);
    expect(reference).not.toBeNull();
    expect(reference!.textContent).toBe("A1");
    composerStore.stopEdition();
    await nextTick();
    expect(fixture.querySelector(referenceSelector)).toBeNull();
    await startComposition();
    expect(fixture.querySelector(referenceSelector)).toBeNull();
  });

  test("starting the edition with a key stroke =, the composer should have the focus after the key input", async () => {
    const composerEl = await startComposition("=");
    expect(composerEl.textContent).toBe("=");
    expect(document.activeElement).toBe(composerEl);
  });

  test("starting the edition with a key stroke B, the composer should have the focus after the key input", async () => {
    const composerEl = await startComposition("b");
    expect(composerEl.textContent).toBe("b");
    expect(document.activeElement).toBe(composerEl);
  });

  test("type '=', backspace and select a cell should not add it", async () => {
    const composerEl = await typeInComposerGrid("=");
    composerStore.setCurrentContent("");
    document.getSelection()?.removeAllRanges();
    composerEl.dispatchEvent(new Event("input"));
    composerEl.dispatchEvent(new Event("keyup"));
    await clickCell(env, "C8");
    expect(getSelectionAnchorCellXc(model)).toBe("C8");
    expect(composerStore.editionMode).toBe("inactive");
  });

  test("should switch topbar composer from editing to inactive when pressing Escape on cell A1 containing '=A2'", async () => {
    setCellContent(model, "A1", "=A2");
    await click(fixture, ".o-spreadsheet-topbar .o-composer");
    expect(composerStore.editionMode).toBe("editing");
    await keyDown({ key: "Escape" });
    await keyUp({ key: "Escape" });
    expect(composerStore.editionMode).toBe("inactive");
  });

  test("ArrowKeys will move to neighbour cell, if not in contentFocus mode (left/right)", async () => {
    let composerEl: Element;
    composerEl = await startComposition("a");
    expect(composerEl.textContent).toBe("a");
    await keyDown({ key: "ArrowRight" });
    expect(getCellText(model, "A1")).toBe("a");
    expect(getActivePosition(model)).toBe("B1");

    composerEl = await startComposition("b");
    expect(composerEl.textContent).toBe("b");
    await keyDown({ key: "ArrowRight" });
    expect(getCellText(model, "B1")).toBe("b");
    expect(getActivePosition(model)).toBe("C1");

    await keyDown({ key: "ArrowLeft" });
    expect(getActivePosition(model)).toBe("B1");
    await keyDown({ key: "ArrowLeft" });
    expect(getActivePosition(model)).toBe("A1");
    composerEl = await startComposition("c");
    expect(composerEl.textContent).toBe("c");
    await keyDown({ key: "Enter" });
    expect(getCellText(model, "B1")).toBe("b");
    expect(getCellText(model, "A1")).toBe("c");
  });

  test("Arrow keys will not move to neighbor cell when a formula", async () => {
    const composerEl = await startComposition("=");
    await typeInComposerGrid(`"`, false);
    await typeInComposerGrid(`"`, false);
    expect(composerEl.textContent).toBe(`=""`);
    await keyDown({ key: "ArrowLeft" });
    expect(composerStore.editionMode).not.toBe("inactive");
  });

  test("ArrowKeys will move to neighbour cell, if not in contentFocus mode (up/down)", async () => {
    let composerEl: Element;
    composerEl = await startComposition("a");
    expect(composerEl.textContent).toBe("a");
    await keyDown({ key: "ArrowDown" });
    expect(getCellText(model, "A1")).toBe("a");
    expect(getActivePosition(model)).toBe("A2");

    composerEl = await startComposition("b");
    expect(composerEl.textContent).toBe("b");
    await keyDown({ key: "ArrowDown" });
    expect(getCellText(model, "A2")).toBe("b");
    expect(getActivePosition(model)).toBe("A3");

    await keyDown({ key: "ArrowUp" });
    expect(getActivePosition(model)).toBe("A2");
    await keyDown({ key: "ArrowUp" });
    expect(getActivePosition(model)).toBe("A1");
    composerEl = await startComposition("c");
    expect(composerEl.textContent).toBe("c");
    await keyDown({ key: "Enter" });
    expect(getCellText(model, "A2")).toBe("b");
    expect(getCellText(model, "A1")).toBe("c");
  });

  test("The composer should be closed before opening the context menu", async () => {
    await typeInComposerGrid("=");
    await rightClickCell(env, "C8");
    expect(composerStore.editionMode).toBe("inactive");
  });

  test("The composer should be closed before selecting headers", async () => {
    await typeInComposerGrid("Hello");
    expect(composerStore.editionMode).not.toBe("inactive");
    await selectColumnByClicking(env, "C");
    expect(composerStore.editionMode).toBe("inactive");
  });

  test("The content in the composer should be kept after selecting headers", async () => {
    await clickCell(env, "C8");
    await typeInComposerGrid("Hello");
    await selectColumnByClicking(env, "C");
    expect(getCellText(model, "C8")).toBe("Hello");
  });

  test("typing CTRL+C in grid does not type C in the cell", async () => {
    await keyDown({ key: "c", ctrlKey: true });
    expect(composerStore.currentContent).toBe("");
  });

  test("Hitting enter on topbar composer will properly update it", async () => {
    setCellContent(model, "A1", "I am Tabouret");
    setCellContent(model, "A2", "wooplaburg");
    await clickCell(env, "A1");
    const topbarComposerElement = fixture.querySelector(
      ".o-spreadsheet-topbar .o-composer-container .o-composer"
    )!;
    expect(topbarComposerElement.textContent).toBe("I am Tabouret");
    await simulateClick(topbarComposerElement);
    await keyDown({ key: "Enter" });
    expect(topbarComposerElement.textContent).toBe("wooplaburg");
  });

  test("Grid taking focus over topbar composer will properly update the latter", async () => {
    setCellContent(model, "A1", "I am Tabouret");
    await clickCell(env, "A1");
    const topbarComposerElement = fixture.querySelector(
      ".o-spreadsheet-topbar .o-composer-container div"
    )!;
    expect(topbarComposerElement.textContent).toBe("I am Tabouret");
    await simulateClick(topbarComposerElement); // gain focus on topbar composer
    await keyDown({ key: "ArrowLeft" });
    await simulateClick(".o-grid-overlay", 300, 200); // focus another Cell (i.e. C8)
    expect(topbarComposerElement.textContent).toBe("");
  });

  test("focus topbar composer then focus grid composer", async () => {
    const topbarComposerElement = fixture.querySelector(
      ".o-spreadsheet-topbar .o-composer-container .o-composer"
    )!;
    await simulateClick(topbarComposerElement);
    expect(document.activeElement).toBe(topbarComposerElement);
    const gridComposerElement = fixture.querySelector(".o-grid .o-composer-container .o-composer")!;
    await simulateClick(gridComposerElement);
    expect(document.activeElement).toBe(gridComposerElement);
  });

  test("=sum(sum(1,2) + click outside composer should add the missing parenthesis", async () => {
    await typeInComposerGrid("=sum(sum(1,2");
    await clickCell(env, "B2");
    expect(getCellText(model, "A1")).toBe("=sum(sum(1,2))");
  });

  test("={1,2 + click outside composer should add the missing bracket", async () => {
    await typeInComposerGrid("={1,2");
    await clickCell(env, "B2");
    expect(getCellText(model, "A1")).toBe("={1,2}");
  });

  test("=SUM(1,2,{3,4 + click outside composer should add the missing bracket then parenthesis", async () => {
    await typeInComposerGrid("=SUM(1,2,{3,4");
    await clickCell(env, "B2");
    expect(getCellText(model, "A1")).toBe("=SUM(1,2,{3,4})");
  });

  test("={1,2,SUM(3,4 + click outside composer should add the missing parenthesis then bracket", async () => {
    await typeInComposerGrid("={1,2,SUM(3,4");
    await clickCell(env, "B2");
    expect(getCellText(model, "A1")).toBe("={1,2,SUM(3,4)}");
  });

  test("Autocomplete should not appear when typing '=S', clicking outside, and editing back", async () => {
    await typeInComposerGrid("=S");
    expect(fixture.querySelectorAll(".o-autocomplete-value")).toHaveLength(10);

    await clickCell(env, "B2");
    expect(fixture.querySelector(".o-autocomplete-dropdown")).toBeFalsy();

    await startGridComposition();
    expect(fixture.querySelector(".o-autocomplete-dropdown")).toBeFalsy();
  });

  test("Confirming topbar composer ends up focusing the grid composer", async () => {
    await typeInComposerTopBar("=A1");
    const topBarComposerEl = fixture.querySelector<HTMLElement>(
      ".o-spreadsheet-topbar .o-composer"
    )!;
    topBarComposerEl.focus();
    const gridComposerEl = fixture.querySelector(".o-grid .o-composer")!;
    expect(document.activeElement === topBarComposerEl).toBe(true);
    composerStore.stopEdition();
    await nextTick();
    expect(document.activeElement === gridComposerEl).toBe(true);
  });

  test("Composer focus is not lost when selecting a range in the grid", async () => {
    await startGridComposition("=");
    await simulateClick(".o-grid-overlay", 300, 200);
    expect(composerStore.editionMode).toBe("selecting");
  });

  test("Composer focus is lost when clicking on the grid while not selecting a range", async () => {
    await startGridComposition("=1");
    await simulateClick(".o-grid-overlay", 300, 200);
    expect(composerStore.editionMode).toBe("inactive");
  });

  test("Composer focus is not lost when changing sheet, even when not selecting a range", async () => {
    createSheet(model, { sheetId: "42", name: "Sheet2" });
    await startGridComposition("=1");
    await simulateClick(".o-sheet[data-id='42']");
    expect(composerStore.editionMode).toBe("editing");
  });

  test("composer focus is not lost when changing sheet from the list sheet in the bottom bar", async () => {
    createSheet(model, { sheetId: "42", name: "Sheet2" });
    await startGridComposition("=1");
    await simulateClick(".o-list-sheets", 0, 0, { cancelable: true, bubbles: true });
    await simulateClick(".o-menu-item[data-name='42']", 0, 0, { cancelable: true, bubbles: true });
    expect(model.getters.getActiveSheetId()).toBe("42");
    expect(composerStore.editionMode).toBe("editing");
  });

  test("composer focus is lost when opening a sheet menu from the bottom bar", async () => {
    createSheet(model, { sheetId: "42", name: "Sheet2" });
    await startGridComposition("=1");
    await simulateClick(".o-sheet[data-id='42'] .o-sheet-icon");
    expect(composerStore.editionMode).toBe("inactive");
  });

  test("Composer focus is lost on blur if focusing a random element", async () => {
    await startComposition("=");
    await simulateClick(".o-spreadsheet", 300, 200);
    expect(composerStore.editionMode).toBe("inactive");
  });

  test("Pressing Enter while editing a label does not open grid composer", async () => {
    setCellContent(model, "A1", "[label](http://odoo.com)");
    await simulateClick(".o-topbar-menu[data-id='insert']");
    await simulateClick(".o-menu-item[data-name='insert_link']");
    const editor = fixture.querySelector(".o-link-editor");
    expect(editor).toBeTruthy();

    editor!.querySelectorAll("input")[0].focus();
    await keyDown({ key: "Enter" });
    expect(fixture.querySelector(".o-link-editor")).toBeFalsy();
    expect(composerStore.editionMode).toBe("inactive");
  });

  test("should create a table when a cell is double clicked in edit mode", async () => {
    selectCell(model, "A1");
    triggerMouseEvent(
      ".o-grid-overlay",
      "dblclick",
      0.5 * DEFAULT_CELL_WIDTH,
      0.5 * DEFAULT_CELL_HEIGHT
    );
    createTable(model, "A1");
    expect(getTable(model, "A1")).toBeTruthy();
  });

  test("keyboard inputs are disabled when selecting a range for the composer", async () => {
    await typeInComposerGrid("=");
    await gridMouseEvent(env, "pointerdown", "C8");
    await gridMouseEvent(env, "pointermove", "B8");
    await keyDown({ key: "A" });
    await gridMouseEvent(env, "pointerup", "B8");
    await nextTick();
    expect(composerStore.currentContent).toBe("=B8:C8");
  });
});

describe("Grid composer", () => {
  let env: SpreadsheetChildEnv;
  beforeEach(async () => {
    ({ model, env, fixture } = await mountSpreadsheet({
      model: new Model(modelData),
    }));
    composerStore = env.getStore(CellComposerStore);
  });

  test("Composer is closed when changing sheet while not editing a formula", async () => {
    const composerStore = env.getStore(CellComposerStore);
    const baseSheetId = model.getters.getActiveSheetId();
    createSheet(model, { sheetId: "42", name: "Sheet2" });
    await nextTick();

    // Editing text
    await typeInComposerGrid("hey");
    expect(composerStore.editionMode).not.toBe("inactive");
    activateSheet(model, "42");
    await nextTick();
    expect(composerStore.editionMode).toBe("inactive");

    // Editing formula
    await typeInComposerGrid("=");
    expect(composerStore.editionMode).not.toBe("inactive");
    activateSheet(model, baseSheetId);
    await nextTick();
    expect(composerStore.editionMode).not.toBe("inactive");
  });

  test("the composer should keep the focus after changing sheet", async () => {
    createSheet(model, { sheetId: "42", name: "Sheet2" });
    await nextTick();

    await startComposition("=");
    expect(document.activeElement).toBe(fixture.querySelector(".o-grid div.o-composer")!);
    await simulateClick(fixture.querySelectorAll(".o-sheet")[1]);
    expect(model.getters.getActiveSheetId()).toEqual("42");
    expect(document.activeElement).toBe(fixture.querySelector(".o-grid div.o-composer")!);
  });

  test("pressing F4 loops the references without impacting the 'redo' feature of the grid", async () => {
    setCellContent(model, "A1", "coucou");
    setCellContent(model, "A2", "coucou2");
    copy(model, "A1:A2");
    paste(model, "A3:A4");
    selectCell(model, "B1");
    await startComposition("=C4");
    await keyDown({ key: "F4" });
    expect(getCellContent(model, "B2")).toBe("");
  });

  test("Wheel event on the composer should not scroll the viewport if the composer has a scrollbar", async () => {
    const viewStore = env.getStore(ViewportsStore);
    const viewport = viewStore.activeMainViewport;
    // Describes a div that has a scrollbar - the scrollHeight is greater than the clientHeight
    jest.spyOn(HTMLDivElement.prototype, "clientHeight", "get").mockImplementation(() => 50);
    jest.spyOn(HTMLDivElement.prototype, "scrollHeight", "get").mockImplementation(() => 150);
    await startComposition("5");
    triggerWheelEvent(document.activeElement!, { deltaY: 4 * DEFAULT_CELL_HEIGHT });
    await nextTick();
    expect(viewStore.activeMainViewport).toMatchObject(viewport);

    // Describes a div without a scrollbar, the scrollHeight matches the clientheight
    jest.spyOn(HTMLDivElement.prototype, "clientHeight", "get").mockImplementation(() => 50);
    jest.spyOn(HTMLDivElement.prototype, "scrollHeight", "get").mockImplementation(() => 50);
    await nextTick();
    triggerWheelEvent(document.activeElement!, { deltaY: 4 * DEFAULT_CELL_HEIGHT });
    expect(viewStore.activeMainViewport).toMatchObject({
      ...viewport,
      top: viewport.top + 4,
      bottom: viewport.bottom + 4,
    });
  });

  describe("grid composer basic style", () => {
    const composerContainerSelector = ".o-grid .o-grid-composer";

    test("Grid composer snapshot", async () => {
      await typeInComposerGrid("A");
      expect(fixture.querySelector(composerContainerSelector)).toMatchSnapshot();
    });

    test("Grid composer container is positioned over the edited cell", async () => {
      selectCell(model, "C3");
      await typeInComposerGrid("A");

      const expectedTop = HEADER_HEIGHT + 2 * DEFAULT_CELL_HEIGHT;
      const expectedLeft = HEADER_WIDTH + 2 * DEFAULT_CELL_WIDTH - 1; //-1 to include cell border

      expect(getElComputedStyle(composerContainerSelector, "top")).toBe(expectedTop + "px");
      expect(getElComputedStyle(composerContainerSelector, "left")).toBe(expectedLeft + "px");
    });

    test("Grid Composer Position is recomputed if we change the edited cell", async () => {
      const expectedTop = (index: HeaderIndex) => HEADER_HEIGHT + index * DEFAULT_CELL_HEIGHT;
      const expectedLeft = (index: HeaderIndex) => HEADER_WIDTH + index * DEFAULT_CELL_WIDTH - 1; //-1 to include cell border
      env.model.selection.selectCell(2, 4);
      await typeInComposerGrid("coucou", true);
      expect(getElComputedStyle(composerContainerSelector, "top")).toBe(expectedTop(4) + "px");
      expect(getElComputedStyle(composerContainerSelector, "left")).toBe(expectedLeft(2) + "px");

      env.model.selection.getBackToDefault();
      env.model.selection.selectCell(1, 1);
      env.startCellEdition();
      await nextTick();
      expect(getElComputedStyle(composerContainerSelector, "top")).toBe(expectedTop(1) + "px");
      expect(getElComputedStyle(composerContainerSelector, "left")).toBe(expectedLeft(1) + "px");
    });

    test("Grid composer container have a min-height / min-width to have the same size as the edited cell ", async () => {
      resizeRows(model, [0], 40);
      resizeColumns(model, ["A"], 50);
      await typeInComposerGrid("A");

      const expectedMinHeight = 40 + 1; // +1 to include cell border
      const expectedMinWidth = 50 + 1;

      expect(getElComputedStyle(composerContainerSelector, "min-height")).toBe(
        expectedMinHeight + "px"
      );
      expect(getElComputedStyle(composerContainerSelector, "min-width")).toBe(
        expectedMinWidth + "px"
      );
    });

    test("Grid composer have a max-height / max-width to avoid overflow outside of grid", async () => {
      selectCell(model, "C3");
      await typeInComposerGrid("A");

      const sheetViewDims = env.getStore(ViewportsStore).sheetViewDimension;
      const expectedMaxHeight = sheetViewDims.height - 2 * DEFAULT_CELL_HEIGHT;
      const expectedMaxWidth = sheetViewDims.width - 2 * DEFAULT_CELL_WIDTH;

      expect(getElComputedStyle(composerContainerSelector, "max-height")).toBe(
        expectedMaxHeight + "px"
      );
      expect(getElComputedStyle(composerContainerSelector, "max-width")).toBe(
        expectedMaxWidth + "px"
      );
    });
  });

  describe("Grid composer's style depends on the style of the cell when containing text", () => {
    test("Inherits the style of the cell", async () => {
      const fontSize = FONT_SIZES[0];
      const color = "#123456";
      setFormatting(model, "A1", {
        textColor: color,
        fillColor: color,
        fontSize,
        bold: true,
        italic: true,
        strikethrough: true,
        underline: true,
        align: "right",
      });
      await typeInComposerGrid("Hello");
      const gridComposer = fixture.querySelector(".o-grid-composer")! as HTMLElement;
      expect(toHex(gridComposer.style.color)).toBe(color);
      expect(toHex(gridComposer.style.background)).toBe(color);
      expect(gridComposer.style.fontSize).toBe("8px");
      expect(gridComposer.style.fontWeight).toBe("bold");
      expect(gridComposer.style.fontStyle).toBe("italic");
      expect(gridComposer.style.textDecoration).toBe("line-through underline");
      expect(gridComposer.style.textAlign).toBe("right");
    });

    test("Inherits CF formatting of the cell", async () => {
      const style = {
        fillColor: "#0000FF",
        bold: true,
        italic: true,
        strikethrough: true,
        underline: true,
        textColor: "#FF0000",
      };
      addEqualCf(model, "A1", style, "4");
      setCellContent(model, "A1", "4");
      await typeInComposerGrid("Hello");
      const gridComposer = fixture.querySelector(".o-grid-composer")! as HTMLElement;
      expect(gridComposer.style.textDecoration).toBe("line-through underline");
      expect(gridComposer.style.fontWeight).toBe("bold");
      expect(gridComposer.style.background).toBeSameColorAs("#0000FF");
      expect(gridComposer.style.color).toBeSameColorAs("#FF0000");
    });
  });

  describe("grid composer's style does not depend on the style of the cell when containing a formula", () => {
    test("Does not inherit style of the cell", async () => {
      const fontSize = FONT_SIZES[0];
      const color = "#123456";
      setFormatting(model, "A1", {
        textColor: color,
        fillColor: color,
        fontSize,
        bold: true,
        italic: true,
        strikethrough: true,
        underline: true,
        align: "right",
      });
      await typeInComposerGrid("=");
      const gridComposer = fixture.querySelector(".o-grid-composer")! as HTMLElement;
      expect(toHex(gridComposer.style.color)).toBe("#000000");
      expect(toHex(gridComposer.style.background)).toBe("#FFFFFF");
      expect(gridComposer.style.fontSize).toBe("13px");
      expect(gridComposer.style.fontWeight).toBe("");
      expect(gridComposer.style.fontStyle).toBe("normal");
      expect(gridComposer.style.textDecoration).toBe("none");
      expect(gridComposer.style.textAlign).toBe("left");
    });

    test("Does not inherit CF formatting of the cell", async () => {
      const rule: CellIsRule = {
        values: [],
        type: "CellIsRule",
        operator: "isNotEmpty",
        style: {
          fillColor: "#0000FF",
          bold: true,
          italic: true,
          strikethrough: true,
          underline: true,
          textColor: "#FF0000",
        },
      };
      addCfRule(model, "A1", rule);
      await typeInComposerGrid("=", true);
      const gridComposer = fixture.querySelector(".o-grid-composer")! as HTMLElement;
      expect(gridComposer.style.textDecoration).toBe("none");
      expect(gridComposer.style.fontWeight).toBe("");
      expect(toHex(gridComposer.style.color)).toBe("#000000");
      expect(toHex(gridComposer.style.background)).toBe("#FFFFFF");
    });
  });

  test("Grid composer will not open if the sheet is locked", async () => {
    lockSheet(model);

    triggerMouseEvent(
      ".o-grid-overlay",
      "dblclick",
      0.5 * DEFAULT_CELL_WIDTH,
      0.5 * DEFAULT_CELL_HEIGHT
    );
    await nextTick();
    expect(composerStore.currentContent).toBe("");
    expect(composerStore.editionMode).toBe("inactive");

    await typeInComposerGrid("Hello");
    expect(composerStore.currentContent).toBe("");
    expect(composerStore.editionMode).toBe("inactive");
    expect(HTMLElement.prototype.animate).toHaveBeenCalled();
  });
});

describe("TopBar composer", () => {
  test("click on a autocomplete with multi-line topbar composer does the autocomplete", async () => {
    ({ model, fixture } = await mountSpreadsheet());
    const composerEl = await typeInComposerTopBar("=\nS");
    await click(fixture, ".o-autocomplete-dropdown > div:nth-child(2)");
    expect(composerEl.childNodes[0].textContent).toBe("=");
    expect(composerEl.childNodes[1].textContent).toBe("SIN(");
    expect(composerEl.querySelector(".selector-flag")).toBeTruthy();
    expect(getInputSelection()).toEqual({
      anchorNodeText: "(",
      anchorOffset: 1,
      focusNodeText: "(",
      focusOffset: 1,
    });
    expect(document.activeElement).toBe(composerEl);
    expect(fixture.querySelectorAll(".o-autocomplete-value")).toHaveLength(0);
  });

  test("Spreaded cell has no value in the composer but has a placeholder", async () => {
    ({ model, fixture } = await mountSpreadsheet());
    setCellContent(model, "A1", "=MUNIT(3)");
    selectCell(model, "A2");
    await nextTick();

    expect(".o-topbar-composer .o-composer").toHaveText("");
    expect(".o-topbar-composer .o-composer").toHaveAttribute("placeholder", "=MUNIT(3)");
    expect(".o-topbar-composer .o-icon").toHaveCount(0);

    await simulateClick(".o-topbar-composer .o-composer");
    expect(".o-topbar-composer .o-composer").toHaveText("");
    expect(".o-topbar-composer .o-icon").toHaveCount(0);

    await keyDown({ key: "Enter" });
    expect(".o-topbar-composer .o-composer").toHaveText("");
    expect(".o-topbar-composer .o-composer").toHaveAttribute("placeholder", "=MUNIT(3)");
    expect(".o-topbar-composer .o-icon").toHaveCount(0);
  });

  test("Fx icon is not shown in spreaded cell results", async () => {
    ({ model, fixture } = await mountSpreadsheet());
    setCellContent(model, "A1", "=MUNIT(3)");
    selectCell(model, "A2");
    await nextTick();

    expect(".o-spreadsheet-topbar .o-composer").toHaveText("");
    expect(".o-spreadsheet-topbar .o-composer").toHaveAttribute("placeholder", "=MUNIT(3)");
    expect(".o-topbar-composer .o-icon").toHaveCount(0);
  });

  test("Spreaded cell placeholder follows the current locale", async () => {
    ({ model, fixture } = await mountSpreadsheet());
    setCellContent(model, "A1", "=SEQUENCE(3,3)");
    selectCell(model, "A2");
    updateLocale(model, FR_LOCALE);
    await nextTick();

    const topBarComposer = document.querySelector(".o-spreadsheet-topbar .o-composer")!;
    expect(topBarComposer.textContent).toBe("");
    expect(topBarComposer.attributes.getNamedItem("placeholder")?.value).toEqual("=SEQUENCE(3;3)");

    await simulateClick(topBarComposer);
    expect(topBarComposer!.textContent).toBe("");

    await keyDown({ key: "Enter" });
    expect(topBarComposer!.textContent).toBe("");
    expect(topBarComposer.attributes.getNamedItem("placeholder")?.value).toEqual("=SEQUENCE(3;3)");
  });

  test("opening and closing the assistant preserves the focus on the top bar composer", async () => {
    ({ model, fixture } = await mountSpreadsheet());
    await click(fixture, ".o-spreadsheet-topbar .o-composer");
    let composerEl = document.querySelector(".o-spreadsheet-topbar .o-composer");
    expect(document.activeElement).toBe(composerEl);

    await typeInComposerTopBar("=SUM(", false);
    composerEl = document.querySelector(".o-spreadsheet-topbar .o-composer");
    expect(document.activeElement).toBe(composerEl);
    expect(fixture.querySelector(".o-formula-assistant")).toBeDefined();
    expect(fixture.querySelector(".o-spreadsheet-topbar [data-icon='help_f']")).toBe(null);
    expect(fixture.querySelector(".o-spreadsheet-topbar [data-icon='cancel_f']")).not.toBe(null);
    await simulateClick(".o-spreadsheet-topbar [data-icon='cancel_f']");
    expect(document.activeElement).toBe(composerEl);
    expect(fixture.querySelector(".o-formula-assistant")).toBe(null);
    await simulateClick(".o-spreadsheet-topbar [data-icon='help_f']");
    expect(fixture.querySelector(".o-formula-assistant")).toBeDefined();
    expect(document.activeElement).toBe(composerEl);
  });

  test("Topbar composer cannot be focused if the sheet is locked", async () => {
    let env: SpreadsheetChildEnv;
    ({ model, fixture, env } = await mountSpreadsheet());
    composerStore = env.getStore(CellComposerStore);
    lockSheet(model);
    await click(fixture, ".o-spreadsheet-topbar .o-composer");
    expect(composerStore.editionMode).toBe("inactive");
    await typeInComposerTopBar("=SUM(");

    expect(composerStore.currentContent).toBe("");
    expect(composerStore.editionMode).toBe("inactive");
    expect(HTMLElement.prototype.animate).toHaveBeenCalled();
  });
});
