import { Table, UID } from "../../src";
import { toUnboundedZone, toZone, zoneToXc } from "../../src/helpers/zones";
import {
  createTable,
  createTableStyle,
  deleteTable,
  setCellContent,
  setSelection,
  updateTableConfig,
} from "../test_helpers/commands_helpers";
import { click, setInputValueAndTrigger, simulateClick } from "../test_helpers/dom_helper";
import { getCellRawContent } from "../test_helpers/getters_helpers";
import {
  getHighlightsFromStore,
  mountComponentWithPortalTarget,
  nextTick,
  setGrid,
  toRangeData,
} from "../test_helpers/helpers";

import { Model } from "../../src";
import { FIRST_TABLE_IN_SELECTION } from "../../src/actions/menu_items_actions";
import { AutofillStore } from "../../src/components/autofill/autofill_store";
import { TableAutofillStore } from "../../src/components/autofill/table_autofill_store";
import { SidePanels } from "../../src/components/side_panel/side_panels/side_panels";
import { TableTerms } from "../../src/components/translations_terms";
import { TABLE_PRESETS } from "../../src/helpers/table_presets";
import { SpreadsheetChildEnv } from "../../src/types/spreadsheet_env";
import { addPivot } from "../test_helpers/pivot_helpers";

function getTable(model: Model, sheetId: UID): Table {
  return model.getters.getTables(sheetId)[0];
}

function getStyleElementStyleId(el: HTMLElement) {
  return el.title.split(", ")[1];
}

let model: Model;
let fixture: HTMLElement;
let sheetId: UID;
let env: SpreadsheetChildEnv;

describe("Table side panel", () => {
  beforeEach(async () => {
    model = new Model();
    sheetId = model.getters.getActiveSheetId();
    createTable(model, "A1:C3");
    ({ fixture, env } = await mountComponentWithPortalTarget(SidePanels, { model }));
    const table = FIRST_TABLE_IN_SELECTION(env);
    env.getStore(AutofillStore);
    env.getStore(TableAutofillStore);
    env.openSidePanel("TableSidePanel", { table });
    await nextTick();
  });

  test.each([
    "hasFilters",
    "totalRow",
    "firstColumn",
    "lastColumn",
    "bandedRows",
    "bandedColumns",
    "automaticAutofill",
  ])("Can change table config boolean option %s", async (configOption) => {
    const value = getTable(model, sheetId).config[configOption];
    const checkbox = fixture.querySelector(`input[name="${configOption}"]`) as HTMLInputElement;
    expect(checkbox.checked).toBe(value);
    await click(checkbox);
    expect(getTable(model, sheetId).config[configOption]).toBe(!value);
  });

  test("Cannot add filters to a table without headers", async () => {
    updateTableConfig(model, "A1:C3", { numberOfHeaders: 0 });
    await nextTick();
    const checkbox = fixture.querySelector("input[name='hasFilters']") as HTMLInputElement;
    expect(checkbox.checked).toBe(false);
    expect(checkbox.disabled).toBe(true);
    expect(checkbox.parentElement?.title).toBe(TableTerms.Tooltips.filterWithoutHeader.toString());
  });

  test("Can change number of headers", async () => {
    updateTableConfig(model, "A1:C3", { numberOfHeaders: 0 });
    await nextTick();
    expect(fixture.querySelector("input.o-table-n-of-headers")).toBeNull();
    await click(fixture, "input[name='headerRow']");

    expect(fixture.querySelector("input.o-table-n-of-headers")).not.toBeNull();

    expect(fixture.querySelector<HTMLInputElement>("input.o-table-n-of-headers")!.value).toBe("1");
    await setInputValueAndTrigger("input.o-table-n-of-headers", "2");
    expect(getTable(model, sheetId).config.numberOfHeaders).toBe(2);
  });

  test("Number of headers is reset when inputting an invalid value", async () => {
    await setInputValueAndTrigger("input.o-table-n-of-headers", "-2");
    expect(getTable(model, sheetId).config.numberOfHeaders).toBe(1);
    expect(fixture.querySelector<HTMLInputElement>("input.o-table-n-of-headers")!.value).toBe("1");
  });

  test("Disabling then re-enabling the headers saves the state of the hasFilters checkbox", async () => {
    const hasFilters = fixture.querySelector<HTMLInputElement>("input[name='hasFilters']")!;
    await click(fixture, "input[name='hasFilters']");
    expect(hasFilters.checked).toBe(true);
    await click(fixture, "input[name='headerRow']");
    expect(hasFilters.checked).toBe(false);
    await click(fixture, "input[name='headerRow']");
    expect(hasFilters.checked).toBe(true);
  });

  test("Can update table zone and it also moves the selection in the new table zone", async () => {
    await simulateClick(".o-selection input");
    await setInputValueAndTrigger(".o-selection input", "D5:E8");
    await click(fixture, ".o-selection .o-selection-ok");
    expect(zoneToXc(getTable(model, sheetId).range.zone)).toEqual("D5:E8");
    expect(model.getters.getSelectedZone()).toEqual(toZone("D5"));
  });

  test("Updating table zone does the equivalent of a ctrl+a if selecting a single cell", async () => {
    setCellContent(model, "D4", "5");
    setCellContent(model, "D5", "6");
    await simulateClick(".o-selection input");
    await setInputValueAndTrigger(".o-selection input", "D5");
    await click(fixture, ".o-selection .o-selection-ok");
    expect(zoneToXc(getTable(model, sheetId).range.zone)).toEqual("D4:D5");
    expect(model.getters.getSelectedZone()).toEqual(toZone("D4"));
  });

  test("Table is auto-filled if the new zone expands the table down", async () => {
    setCellContent(model, "A4", "=A3+1");
    await simulateClick(".o-selection input");
    await setInputValueAndTrigger(".o-selection input", "A1:C5");
    await click(fixture, ".o-selection .o-selection-ok");
    expect(getCellRawContent(model, "A5")).toEqual("=A4+1");
  });

  test("Side panel works with unbounded table zones", async () => {
    await simulateClick(".o-selection input");
    await setInputValueAndTrigger(".o-selection input", "D2:E");
    await click(fixture, ".o-selection .o-selection-ok");
    expect(fixture.querySelector<HTMLInputElement>(".o-selection input")?.value).toEqual("D2:E");
    const tableRange = getTable(model, sheetId).range;
    expect(tableRange.unboundedZone).toEqual(toUnboundedZone("D2:E"));
    expect(model.getters.getSelectedZone()).toEqual(toZone("D2"));
  });

  test("Changing the table zone to an array formula cell make the table dynamic", async () => {
    setCellContent(model, "E1", "=MUNIT(2)");
    await simulateClick(".o-selection input");
    await setInputValueAndTrigger(".o-selection input", "E1");
    await click(fixture, ".o-selection .o-selection-ok");
    expect(model.getters.getCoreTables(sheetId)[0]).toMatchObject({
      range: { zone: toZone("E1") },
      type: "dynamic",
    });
    expect(zoneToXc(getTable(model, sheetId).range.zone)).toEqual("E1:F2");
    expect(model.getters.getSelectedZone()).toEqual(toZone("E1"));
    expect(fixture.querySelector<HTMLInputElement>(".o-selection input")?.value).toBe("E1");
  });

  test("Errors messages are displayed when wrong zone is entered", async () => {
    createTable(model, "D1:D2");
    await simulateClick(".o-selection input");
    await setInputValueAndTrigger(".o-selection input", "D1:D5");
    expect(fixture.querySelector(".o-validation-error")).not.toBeNull();

    await click(fixture, ".o-selection .o-selection-ok");
    expect(fixture.querySelector(".o-validation-error")).not.toBeNull();
  });

  test("Can update a table after entering an invalid range", async () => {
    createTable(model, "D1:D2");
    await simulateClick(".o-selection input");
    await setInputValueAndTrigger(".o-selection input", "OK");
    await simulateClick(".o-checkbox");
    expect(fixture.querySelector(".o-validation-error")).not.toBeNull();
    await simulateClick(".o-selection input");
    await setInputValueAndTrigger(".o-selection input", "A1");
    await click(fixture, ".o-selection .o-selection-ok");
    expect(fixture.querySelector(".o-validation-error")).toBeNull();
    expect(getTable(model, sheetId).range.zone).toEqual(toZone("A1"));
  });

  test("the table range is highlighted when the side panel is open", async () => {
    expect(getHighlightsFromStore(env).map((h) => zoneToXc(h.range.zone))).toEqual(["A1:C3"]);
    await simulateClick(".o-sidePanelClose");
    expect(getHighlightsFromStore(env).map((h) => zoneToXc(h.range.zone))).toEqual([]);
  });

  test("The table zone displayed in the side panel is updated when the user modifies the table range with the mouse", async () => {
    const table = getTable(model, sheetId);
    expect(fixture.querySelector<HTMLInputElement>(".o-selection input")!.value).toBe("A1:C3");
    env.model.dispatch("UPDATE_TABLE", {
      sheetId,
      zone: table.range.zone,
      newTableRange: toRangeData(sheetId, "D1:D2"),
      config: { numberOfHeaders: 0 },
    });
    await nextTick();
    expect(fixture.querySelector<HTMLInputElement>(".o-selection input")!.value).toBe("D1:D2");
  });

  test("The highlighted zone is updated when the user modifies the table range with the mouse", async () => {
    const table = getTable(model, sheetId);
    expect(getHighlightsFromStore(env).map((h) => zoneToXc(h.range.zone))).toEqual(["A1:C3"]);
    env.model.dispatch("UPDATE_TABLE", {
      sheetId,
      zone: table.range.zone,
      newTableRange: toRangeData(sheetId, "D1:D2"),
      config: { numberOfHeaders: 0 },
    });
    await nextTick();
    expect(getHighlightsFromStore(env).map((h) => zoneToXc(h.range.zone))).toEqual(["D1:D2"]);
  });

  test("Changing the selection does not change the edited table", async () => {
    createTable(model, "D1:D2");
    updateTableConfig(model, "D1:D2", { numberOfHeaders: 0 });

    expect(fixture.querySelector<HTMLInputElement>("input[name='headerRow']")!.checked).toBe(true);
    expect(fixture.querySelector<HTMLInputElement>(".o-selection input")!.value).toBe("A1:C3");

    setSelection(model, ["D1"]);
    await nextTick();
    expect(fixture.querySelector<HTMLInputElement>("input[name='headerRow']")!.checked).toBe(true);
    expect(fixture.querySelector<HTMLInputElement>(".o-selection input")!.value).toBe("A1:C3");
  });

  test("Selecting a cell without a table does not close the side panel", async () => {
    setSelection(model, ["D1"]);
    await nextTick();
    expect(fixture.querySelector(".o-table-panel")).not.toBeNull();
  });

  test("Selecting a cell with a pivot table does not close the table panel", async () => {
    setGrid(model, { A1: "Header1", B1: "Header2", A2: "Data1", B2: "Data2", F1: "=PIVOT(1)" });
    addPivot(model, "A1:B2", { style: { tableStyleId: "PivotTableStyleMedium9" } });
    setSelection(model, ["F1"]);
    await nextTick();
    expect(fixture.querySelector(".o-table-panel")).not.toBeNull();
  });

  test("Can edit the table style", async () => {
    const tableStyleItems = fixture.querySelectorAll<HTMLElement>(
      ".o-table-style-list-item:not(.selected)"
    );
    await click(tableStyleItems[0]);
    expect(getTable(model, sheetId).config.styleId).toBe(
      getStyleElementStyleId(tableStyleItems[0])
    );
    expect(tableStyleItems[0].classList).toContain("selected");

    await click(tableStyleItems[1]);
    expect(getTable(model, sheetId).config.styleId).toBe(
      getStyleElementStyleId(tableStyleItems[1])
    );
    expect(tableStyleItems[1].classList).toContain("selected");
  });

  test("Table style picker only contain styles of the same category as the current one", async () => {
    updateTableConfig(model, "A1:C3", { styleId: "TableStyleMedium1" });
    await nextTick();

    const getDisplayedStyleIds = () => {
      return Array.from(fixture.querySelectorAll<HTMLElement>(".o-table-style-list-item")).map(
        (el) => el.dataset.id
      );
    };
    const expectedStyles = Object.keys(TABLE_PRESETS).filter((key) => key.includes("Medium"));
    expect(getDisplayedStyleIds()).toEqual(expectedStyles);

    createTableStyle(model, "CustomStyle");
    updateTableConfig(model, "A1:C3", { styleId: "CustomStyle" });
    await nextTick();

    expect(getDisplayedStyleIds()).toEqual(["CustomStyle"]);
  });

  test("Can toggle the table style pick popover", async () => {
    expect(fixture.querySelector(".o-popover .o-table-style-popover")).toBeNull();

    await click(fixture, ".o-table-style-picker-arrow");
    expect(fixture.querySelector(".o-popover .o-table-style-popover")).not.toBeNull();

    await click(fixture, ".o-table-style-picker-arrow");
    expect(fixture.querySelector(".o-popover .o-table-style-popover")).toBeNull();
  });

  test("Can pick a table style in the popover", async () => {
    await click(fixture, ".o-table-style-picker-arrow");
    const tableStyleItems = fixture.querySelectorAll<HTMLElement>(
      ".o-popover .o-table-style-list-item:not(.selected)"
    );
    await click(tableStyleItems[0]);
    expect(tableStyleItems[0].title).toContain(getTable(model, sheetId).config.styleId);
    expect(fixture.querySelector(".o-popover")).toBeNull();
  });

  test("Panel is closed when the table is deleted", async () => {
    deleteTable(model, "A1:C3");
    await nextTick();
    expect(fixture.querySelector(".o-table-panel")).toBeNull();
  });

  test("automaticAutofill checkbox is disabled when the table is dynamic", async () => {
    setCellContent(model, "A1", "=MUNIT(3)");
    await nextTick();
    expect(
      fixture.querySelector<HTMLInputElement>(`input[name="automaticAutofill"]`)?.disabled
    ).toBeFalsy();
    await click(fixture, "input[name='isDynamic']");
    expect(
      fixture.querySelector<HTMLInputElement>(`input[name="automaticAutofill"]`)?.disabled
    ).toBeTruthy();
  });

  test("IsDynamic checkbox is disabled when the table cannot be dynamic", async () => {
    expect(
      fixture.querySelector<HTMLInputElement>(`input[name="isDynamic"]`)?.disabled
    ).toBeTruthy();
    expect(model.getters.getCoreTable({ sheetId, col: 0, row: 0 })?.type).toBe("static");

    setCellContent(model, "A1", "=MUNIT(3)");
    await nextTick();
    expect(
      fixture.querySelector<HTMLInputElement>(`input[name="isDynamic"]`)?.disabled
    ).toBeFalsy();
  });

  test("Can change the table to be dynamic or not", async () => {
    expect(model.getters.getCoreTable({ sheetId, col: 0, row: 0 })?.type).toBe("static");
    expect(fixture.querySelector<HTMLInputElement>(".o-selection input")!.value).toBe("A1:C3");

    setCellContent(model, "A1", "=MUNIT(3)");
    await nextTick();
    const checkbox = fixture.querySelector(`input[name="isDynamic"]`) as HTMLInputElement;
    expect(checkbox.checked).toBe(false);

    await click(checkbox);
    expect(checkbox.checked).toBe(true);
    expect(model.getters.getCoreTable({ sheetId, col: 0, row: 0 })).toMatchObject({
      type: "dynamic",
    });

    expect(fixture.querySelector<HTMLInputElement>(".o-selection input")!.value).toBe("A1");
    expect(fixture.querySelector(".o-selection-input button")).toBeNull(); // check that the selection input didn't start editing

    await click(checkbox);
    expect(checkbox.checked).toBe(false);
    expect(model.getters.getCoreTable({ sheetId, col: 0, row: 0 })).toMatchObject({
      type: "forceStatic",
    });
    expect(fixture.querySelector<HTMLInputElement>(".o-selection input")!.value).toBe("A1:C3");
    expect(fixture.querySelector(".o-selection-input button")).toBeNull();
  });

  test("Deleting table range from the input does not change the table zone", async () => {
    await setInputValueAndTrigger(".o-selection input", "");
    expect(zoneToXc(model.getters.getCoreTable({ sheetId, col: 0, row: 0 })!.range.zone)).toMatch(
      "A1:C3"
    );
  });
});
