import { Model, SpreadsheetPivotCoreDefinition } from "../../src";
import { toZone, zoneToXc } from "../../src/helpers/zones";
import { HighlightStore } from "../../src/stores/highlight_store";
import { SpreadsheetChildEnv } from "../../src/types/spreadsheet_env";
import { createSheet, deleteSheet } from "../test_helpers/commands_helpers";
import {
  click,
  editSelectComponent,
  setInputValueAndTrigger,
  simulateClick,
} from "../test_helpers/dom_helper";
import {
  getHighlightsFromStore,
  mountSpreadsheet,
  nextTick,
  setGrid,
} from "../test_helpers/helpers";
import { SELECTORS, addPivot, removePivot, updatePivot } from "../test_helpers/pivot_helpers";

describe("Pivot side panel", () => {
  let model: Model;
  let fixture: HTMLElement;
  let env: SpreadsheetChildEnv;

  beforeEach(async () => {
    ({ env, model, fixture } = await mountSpreadsheet(
      { model: new Model() },
      { askConfirmation: jest.fn((title, callback) => callback()) }
    ));
    addPivot(model, "A1:B2", {}, "1");
    addPivot(model, "A1:B2", {}, "2");
  });

  test("in readonly, both configuration and design panels are not clickable and greyed but remain scrollable", async () => {
    env.openSidePanel("PivotSidePanel", { pivotId: "1" });
    await nextTick();

    const sidePanel = fixture.querySelector(".o-sidePanel")!;
    let interactiveWrappers = sidePanel.querySelectorAll(".o-sidePanelBody div[inert]");
    expect(interactiveWrappers.length).toBe(0);
    model.updateMode("readonly");
    await nextTick();
    interactiveWrappers = sidePanel.querySelectorAll(".o-sidePanelBody div[inert]");
    expect(interactiveWrappers.length).toBe(2);

    const scrollableContainer = sidePanel.querySelectorAll(".overflow-y-auto")!;
    expect(scrollableContainer.length).toBe(2);

    // The [inert] wrapper with `pe-none` and `opacity-50` is placed inside the scrollable container,
    // ensuring that user interactions are blocked while still allowing vertical scrolling.
    interactiveWrappers = scrollableContainer[0].querySelectorAll("[inert]")!;
    expect(interactiveWrappers.length).toBe(1);
    expect(interactiveWrappers[0]).toBeTruthy();
    expect(interactiveWrappers[0].classList).toContain("pe-none");
    expect(interactiveWrappers[0].classList).toContain("opacity-50");
    expect(interactiveWrappers[0].getAttribute("inert")).toBe("1");

    interactiveWrappers = scrollableContainer[1].querySelectorAll("[inert]")!;
    expect(interactiveWrappers.length).toBe(1);
    expect(interactiveWrappers[0]).toBeTruthy();
    expect(interactiveWrappers[0].classList).toContain("pe-none");
    expect(interactiveWrappers[0].classList).toContain("opacity-50");
    expect(interactiveWrappers[0].getAttribute("inert")).toBe("1");

    expect(fixture.querySelector(".pivot-defer-update")).toBeNull();
  });

  test("It should open the pivot editor when pivotId is provided", async () => {
    env.openSidePanel("PivotSidePanel", { pivotId: "2" });
    await nextTick();
    expect(fixture.querySelector(".o-sidePanelTitle")?.textContent).toEqual("Pivot #2");
  });

  test("Can change the active panel tab with the panel props", async () => {
    env.openSidePanel("PivotSidePanel", { pivotId: "2", openTab: "design" });
    await nextTick();
    expect(".o-sidePanel-tab.o-panel-design").not.toHaveClass("inactive");

    env.openSidePanel("PivotSidePanel", { pivotId: "2" }); // defaults to configuration
    await nextTick();
    expect(".o-sidePanel-tab.o-panel-design").toHaveClass("inactive");

    env.openSidePanel("PivotSidePanel", { pivotId: "2", openTab: "design" });
    await nextTick();
    expect(".o-sidePanel-tab.o-panel-design").not.toHaveClass("inactive");
  });

  test("It should close the side panel when clicking on delete in the editor", async () => {
    env.openSidePanel("PivotSidePanel", { pivotId: "2" });
    await nextTick();
    expect(fixture.querySelector(".o-sidePanelTitle")?.textContent).toEqual("Pivot #2");
    await click(fixture, SELECTORS.COG_WHEEL);
    await click(fixture, SELECTORS.DELETE_PIVOT);
    expect(model.getters.getPivotIds()).toEqual(["1"]);
    expect(fixture.querySelector(".o-sidePanel")).toBeNull();
  });

  test("Sidepanel pivot definition is properly reinitialized", async () => {
    removePivot(model, "1");
    env.openSidePanel("PivotSidePanel", { pivotId: "2" });
    await nextTick();
    createSheet(model, { sheetId: "toDelete", activate: true });
    await nextTick();
    // force the invalidation of the pivot definition
    deleteSheet(model, "toDelete");
    await nextTick();
    expect(fixture.querySelector(".o-sidePanel")).not.toBeNull();
  });

  test("Side panel supports unbounded zone in definition", async () => {
    env.openSidePanel("PivotSidePanel", { pivotId: "1" });
    await nextTick();
    await setInputValueAndTrigger(SELECTORS.ZONE_INPUT, "A:A");
    await nextTick();
    await simulateClick(SELECTORS.ZONE_CONFIRM);
    expect(
      (model.getters.getPivotCoreDefinition("1") as SpreadsheetPivotCoreDefinition).dataSet
    ).toMatchObject({
      sheetId: model.getters.getActiveSheetId(),
      zone: toZone("A1:A100"),
    });
  });

  test("Pivot cells are highlighted when the panel is open on the config tab", async () => {
    // prettier-ignore
    setGrid(model, {
      A1: "Partner", B1: "Amount",
      A2: "Alice", B2: "10",
      A5: "=PIVOT(1)"
    });
    const highlightStore = env.getStore(HighlightStore);
    expect(highlightStore.highlights.map((h) => zoneToXc(h.range.zone))).toEqual([]);

    env.openSidePanel("PivotSidePanel", { pivotId: "1" });
    await nextTick();
    expect(getHighlightsFromStore(env).map((h) => zoneToXc(h.range.zone))).toEqual(["A5:A7"]);

    await click(fixture, ".o-sidePanel-tab.o-panel-design");
    expect(getHighlightsFromStore(env).map((h) => zoneToXc(h.range.zone))).toEqual([]);
  });

  test("Renaming the computed measure the pivot is sorted on keep the sorting", async () => {
    // prettier-ignore
    setGrid(model, {
      A1: "Partner", B1: "Amount",
      A2: "Alice", B2: "10",
      A5: "=PIVOT(1)"
    });

    const sheetId = model.getters.getActiveSheetId();
    updatePivot(model, "1", {
      measures: [
        { id: "Price", fieldName: "Amount", aggregator: "sum" },
        {
          id: "Amount times 2",
          fieldName: "Amount times 2",
          aggregator: "sum",
          computedBy: { formula: "=Amount*2", sheetId },
        },
      ],
      sortedColumn: { domain: [], order: "asc", measure: "Amount times 2" },
    });
    env.openSidePanel("PivotSidePanel", { pivotId: "1" });
    await nextTick();

    const measureEl = fixture.querySelectorAll(".pivot-measure")[1];
    await setInputValueAndTrigger(measureEl.querySelector("input")!, "renamed");

    const definition = model.getters.getPivotCoreDefinition("1") as SpreadsheetPivotCoreDefinition;
    expect(definition.measures[1].id).toBe("renamed:sum");
    expect(definition.sortedColumn?.measure).toBe("renamed:sum");
  });

  test("Changing pivot rows/cols filter out invalid collapsed row/cols domains", async () => {
    // prettier-ignore
    const grid = {
      A1: "Customer", B1: "Price",  C1: "Year",  D1: "Active", E1: "Client",
      A2: "Alice",    B2: "10",     C2: "2020",  D2: "FALSE",  E2: "Marc",
      A3: "Alice",    B3: "20",     C3: "2021",  D3: "TRUE",   E3: "Marc",
      A4: "Bob",      B4: "30",     C4: "2020",  D4: "FALSE",  E4: "Marc",
      A5: "Bob",      B5: "40",     C5: "2021",  D5: "TRUE",   E5: "Marc",
    };
    setGrid(model, grid);
    updatePivot(model, "1", {
      columns: [{ fieldName: "Customer" }, { fieldName: "Year" }],
      rows: [{ fieldName: "Client" }, { fieldName: "Active" }],
      measures: [{ id: "Price", fieldName: "Price", aggregator: "sum" }],
      collapsedDomains: {
        COL: [[{ field: "Customer", value: "Alice", type: "char" }]],
        ROW: [[{ field: "Client", value: "Marc", type: "char" }]],
      },
      dataSet: { sheetId: model.getters.getActiveSheetId(), zone: toZone("A1:E5") },
    });

    env.openSidePanel("PivotSidePanel", { pivotId: "1" });
    await nextTick();

    const customerDimEl = fixture.querySelectorAll(".pivot-dimension")[0];
    await click(customerDimEl, "[data-icon='delete_f']");

    let definition = model.getters.getPivotCoreDefinition("1");
    expect(definition.columns).toHaveLength(1);
    expect(definition.collapsedDomains?.COL).toHaveLength(0);
    expect(definition.collapsedDomains?.ROW).toHaveLength(1);

    const clientDimEl = fixture.querySelectorAll(".pivot-dimension")[1];
    await click(clientDimEl, "[data-icon='delete_f']");
    definition = model.getters.getPivotCoreDefinition("1");
    expect(definition.rows).toHaveLength(1);
    expect(definition.collapsedDomains?.COL).toHaveLength(0);
    expect(definition.collapsedDomains?.ROW).toHaveLength(0);
  });

  test("External update of the calulated formula impacts the side panel", async () => {
    // prettier-ignore
    setGrid(model, {
      A1: "Partner", B1: "Amount",
      A2: "Alice", B2: "10",
      A5: "=PIVOT(1)"
    });

    const sheetId = model.getters.getActiveSheetId();
    const calculatedMeasure = {
      id: "calc",
      fieldName: "calc",
      aggregator: "sum",
      computedBy: { formula: "=25", sheetId },
    };
    updatePivot(model, "1", { measures: [calculatedMeasure] });
    env.openSidePanel("PivotSidePanel", { pivotId: "1" });
    await nextTick();

    expect(".o-sidePanel .o-composer").toHaveText("=25");

    updatePivot(model, "1", {
      measures: [{ ...calculatedMeasure, computedBy: { formula: "=50", sheetId } }],
    });
    await nextTick();
    expect(".o-sidePanel .o-composer").toHaveText("=50");

    // Focus the composer. It should stay the same value
    await simulateClick(".o-sidePanel .o-composer");
    expect(".o-sidePanel .o-composer").toHaveText("=50");
  });

  test("Collapsed dimension with correct field but wrong value is not filtered out at pivot update", async () => {
    // Note: we don't want to remove those, because different users may have different value,
    // and a domain might be valid for one user and not for another

    // prettier-ignore
    const grid = {
      A1: "Customer", B1: "Price",  C1: "Year",
      A2: "Alice",    B2: "10",     C2: "2020",
      A3: "Alice",    B3: "20",     C3: "2021",
    };
    setGrid(model, grid);
    updatePivot(model, "1", {
      columns: [{ fieldName: "Customer" }, { fieldName: "Year" }],
      rows: [],
      measures: [{ id: "Price", fieldName: "Price", aggregator: "sum" }],
      collapsedDomains: {
        COL: [[{ field: "Customer", value: "NotARealPerson", type: "char" }]],
        ROW: [],
      },
      dataSet: { sheetId: model.getters.getActiveSheetId(), zone: toZone("A1:C3") },
    });

    env.openSidePanel("PivotSidePanel", { pivotId: "1" });
    await nextTick();

    const yearDimensionEl = fixture.querySelectorAll(".pivot-dimension")[1];
    await click(yearDimensionEl, "[data-icon='delete_f']");

    const definition = model.getters.getPivotCoreDefinition("1");
    expect(definition.columns).toHaveLength(1);
    expect(definition.collapsedDomains?.COL).toHaveLength(1);
  });

  test("display the description of the show as", async () => {
    //prettier-ignore
    const grid = {
      A1: "Customer", B1: "Price", C1: "Year",
      A2: "Alice",    B2: "10",    C2: "2020",
      A3: "Alice",    B3: "20",    C3: "2021",
	  };
    setGrid(model, grid);
    updatePivot(model, "1", {
      columns: [{ fieldName: "Customer" }, { fieldName: "Year" }],
      rows: [],
      measures: [
        {
          id: "Price",
          fieldName: "Price",
          aggregator: "sum",
          display: { type: "%_of", fieldNameWithGranularity: "Customer", value: "Alice" },
        },
      ],
      dataSet: { sheetId: model.getters.getActiveSheetId(), zone: toZone("A1:C3") },
    });

    env.openSidePanel("PivotSidePanel", { pivotId: "1" });
    await nextTick();
    expect(".o-measure-description").toHaveText(`Displayed as % of "Customer" : Alice`);
  });

  test(`display the description of the show as with "previous" item`, async () => {
    //prettier-ignore
    const grid = {
      A1: "Customer", B1: "Price", C1: "Year",
      A2: "Alice",    B2: "10",    C2: "2020",
      A3: "Alice",    B3: "20",    C3: "2021",
	  };
    setGrid(model, grid);
    updatePivot(model, "1", {
      columns: [{ fieldName: "Customer" }, { fieldName: "Year" }],
      rows: [],
      measures: [
        {
          id: "Price",
          fieldName: "Price",
          aggregator: "sum",
          display: { type: "%_of", fieldNameWithGranularity: "Customer", value: "(previous)" },
        },
      ],
      dataSet: { sheetId: model.getters.getActiveSheetId(), zone: toZone("A1:C3") },
    });

    env.openSidePanel("PivotSidePanel", { pivotId: "1" });
    await nextTick();
    expect(".o-measure-description").toHaveText(`Displayed as % of previous "Customer"`);
  });

  test("Changing a measure aggregator also changes the related calculated measures", async () => {
    // prettier-ignore
    setGrid(model, {
      A1: "Partner", B1: "Amount",
      A2: "Alice", B2: "10",
      A5: "=PIVOT(1)"
    });

    const sheetId = model.getters.getActiveSheetId();
    updatePivot(model, "1", {
      measures: [
        { id: "Amount:sum", fieldName: "Amount", aggregator: "sum" },
        {
          id: "m1:avg",
          fieldName: "m1",
          aggregator: "avg",
          computedBy: { formula: "='Amount:sum'*2", sheetId },
        },
        {
          id: "m2",
          fieldName: "m1",
          aggregator: "avg",
          computedBy: { formula: "='Amount:sum'+3*'m1:avg'", sheetId },
        },
      ],
    });
    env.openSidePanel("PivotSidePanel", { pivotId: "1" });
    await nextTick();

    const priceMeasureEl = fixture.querySelectorAll(".pivot-measure")[0];
    await editSelectComponent(priceMeasureEl.querySelector(".o-select"), "count");

    let definition = model.getters.getPivotCoreDefinition("1") as SpreadsheetPivotCoreDefinition;
    expect(definition.measures[0].id).toBe("Amount:count");
    expect(definition.measures[1].computedBy?.formula).toBe("='Amount:count'*2");
    expect(definition.measures[2].computedBy?.formula).toBe("='Amount:count'+3*'m1:avg'");

    const m1MeasureEl = fixture.querySelectorAll(".pivot-measure")[1];
    await editSelectComponent(m1MeasureEl.querySelector(".o-select"), "sum");

    definition = model.getters.getPivotCoreDefinition("1") as SpreadsheetPivotCoreDefinition;
    expect(definition.measures[1].id).toBe("m1:sum");
    expect(definition.measures[1].computedBy?.formula).toBe("='Amount:count'*2");
    expect(definition.measures[2].computedBy?.formula).toBe("='Amount:count'+3*'m1:sum'");
  });

  test("Changing a calculated measure name also changes the related calculated measures", async () => {
    // prettier-ignore
    setGrid(model, {
      A1: "Partner", B1: "Amount",
      A2: "Alice", B2: "10",
      A5: "=PIVOT(1)"
    });

    const sheetId = model.getters.getActiveSheetId();
    updatePivot(model, "1", {
      measures: [
        {
          id: "m1:avg",
          fieldName: "m1",
          aggregator: "avg",
          computedBy: { formula: "=6", sheetId },
        },
        {
          id: "m2",
          fieldName: "m2",
          aggregator: "avg",
          computedBy: { formula: "='m1:avg'*25", sheetId },
        },
      ],
    });
    env.openSidePanel("PivotSidePanel", { pivotId: "1" });
    await nextTick();

    const priceMeasureEl = fixture.querySelectorAll(".pivot-measure")[0];
    await setInputValueAndTrigger(priceMeasureEl.querySelector(".os-input"), "renamed");

    const definition = model.getters.getPivotCoreDefinition("1") as SpreadsheetPivotCoreDefinition;
    expect(definition.measures[0].id).toBe("renamed:avg");
    expect(definition.measures[1].computedBy?.formula).toBe("='renamed:avg'*25");
  });

  test("Can change the tabular form setting", async () => {
    setGrid(model, {
      A1: "Partner",
      B1: "Amount",
      A2: "Alice",
      B2: "10",
      A5: "=PIVOT(1)",
    });
    updatePivot(model, "1", {
      measures: [{ id: "Amount:sum", fieldName: "Amount", aggregator: "sum" }],
    });
    env.openSidePanel("PivotSidePanel", { pivotId: "1" });
    await nextTick();

    const tabularFormCheckbox = fixture.querySelector(
      ".o-pivot-design input[name='tabularForm']"
    ) as HTMLInputElement;
    expect(tabularFormCheckbox.checked).toBe(false);

    tabularFormCheckbox.click();
    await nextTick();

    const definition = model.getters.getPivotCoreDefinition("1") as SpreadsheetPivotCoreDefinition;
    expect(definition.style?.tabularForm).toBe(true);
  });
});
