import {
  Align,
  BorderPosition,
  Box,
  CellIsRule,
  DataValidationCriterion,
  GridRenderingContext,
  LayerName,
  LiteralCell,
  Model,
  Zone,
} from "../../src";
import { HoveredTableStore } from "../../src/components/tables/hovered_table_store";
import {
  CANVAS_SHIFT,
  DEFAULT_CELL_HEIGHT,
  DEFAULT_CELL_WIDTH,
  HEADER_HEIGHT,
  HEADER_WIDTH,
  MIN_CELL_TEXT_MARGIN,
  MIN_CF_ICON_MARGIN,
  NEWLINE,
  SELECTION_BORDER_COLOR,
  TABLE_HOVER_BACKGROUND_COLOR,
} from "../../src/constants";
import {
  blendColors,
  colorToRGBA,
  isColorValid,
  isSameColor,
  toHex,
} from "../../src/helpers/color";
import { COLOR_THEMES } from "../../src/helpers/color_themes";
import { fontSizeInPixels, getContextFontSize } from "../../src/helpers/text_helper";
import { toZone } from "../../src/helpers/zones";
import { DependencyContainer } from "../../src/store_engine/dependency_container";
import { CellHoverOverlayStore } from "../../src/stores/cell_hover_overlay_store";
import { ClipboardStore } from "../../src/stores/clipboard_store";
import { FormulaFingerprintStore } from "../../src/stores/formula_fingerprints_store";
import { GridRenderer } from "../../src/stores/grid_renderer_store";
import { RendererStore } from "../../src/stores/renderer_store";
import { ViewportsStore } from "../../src/stores/viewports_store";
import { Mode } from "../../src/types/model";
import { MockCanvasRenderingContext2D } from "../setup/canvas.mock";
import {
  addCfRule,
  addColumns,
  addDataBarCF,
  addDataValidation,
  addEqualCf,
  addIconCF,
  copy,
  createSheet,
  createTable,
  deleteColumns,
  freezeColumns,
  freezeRows,
  merge,
  paste,
  resizeColumns,
  resizeRows,
  selectCell,
  setCellContent,
  setCellFormat,
  setFormat,
  setFormatting,
  setFormulaVisibility,
  setGridLinesVisibility,
  setSelection,
  setSheetBackground,
  setZoneBorders,
} from "../test_helpers/commands_helpers";
import { getCell } from "../test_helpers/getters_helpers";
import { createModelFromGrid, getFingerprint, target } from "../test_helpers/helpers";
import { createModelWithTestPivotDataset } from "../test_helpers/pivot_helpers";
import { MockGridRenderingContext, watchDashedOutline } from "../test_helpers/renderer_helpers";
import { makeStoreWithModel } from "../test_helpers/stores";

MockCanvasRenderingContext2D.prototype.measureText = function (text: string) {
  const fontSize = getContextFontSize(this.font);
  return {
    width: text.length,
    fontBoundingBoxAscent: fontSize / 2,
    fontBoundingBoxDescent: fontSize / 2,
  };
};

function getBoxFromText(gridRenderer: GridRenderer, text: string): Box {
  return gridRenderer["lastRenderBoxes"]
    .values()
    .find((b) => (b.content?.textLines || []).join(" ") === text)!;
}

/**
 * Cell fills are drawn with a 0.5 offset from the cell rect so they fill all of the cell and are not
 * affected with the 0.5 offset we use to make the canvas lines sharp.
 */
function removeOffsetOfFillStyles(fillStyles: any[]): any[] {
  return fillStyles.map((fs) => ({
    ...fs,
    x: fs.x + CANVAS_SHIFT,
    y: fs.y + CANVAS_SHIFT,
    w: fs.w - CANVAS_SHIFT * 2,
    h: fs.h - CANVAS_SHIFT * 2,
  }));
}

function setRenderer(model: Model = new Model(), layers: LayerName[] = ["Background"]) {
  const { container, store: gridRendererStore } = makeStoreWithModel(model, GridRenderer);
  container.get(ClipboardStore); // Instantiate a ClipboardStore
  gridRendererStore["getBoxesWithAnimations"] = function (boxes: Box[]) {
    for (const box of boxes) {
      this["lastRenderBoxes"].set(box.id, box);
    }
    return boxes;
  };
  const rendererManager = container.get(RendererStore);
  const drawGridRenderer = (ctx: GridRenderingContext) => {
    for (const layer of layers) {
      model.drawLayer(ctx, layer);
      for (const renderer of rendererManager["renderers"][layer] || []) {
        renderer.drawLayer(ctx, layer, undefined);
      }
    }
  };
  const sheetId = model.getters.getActiveSheetId();
  return { model, gridRendererStore, drawGridRenderer, container, sheetId };
}

describe("renderer", () => {
  test("snapshot for a simple grid rendering", () => {
    const { drawGridRenderer, model, container } = setRenderer(new Model(), [
      "Background",
      "Headers",
      "Selection",
    ]);

    setCellContent(model, "A1", "1");
    const ctx = new MockGridRenderingContext(model, container, 1000, 1000, {}, "nodeCanvas");
    const viewStore = container.get(ViewportsStore);
    viewStore.resizeSheetView({
      width: 1000 - HEADER_HEIGHT,
      height: 1000 - HEADER_WIDTH,
      gridOffsetX: HEADER_WIDTH,
      gridOffsetY: HEADER_HEIGHT,
    });

    drawGridRenderer(ctx);
    expect(ctx.screenshot()).toMatchImageSnapshot();
  });

  describe("Headers background color", () => {
    function getFirstRowHeaderFillColor() {
      const index = instructions.findIndex(
        (instr) =>
          instr === `ctx.fillRect(${0}, ${HEADER_HEIGHT}, ${HEADER_WIDTH}, ${DEFAULT_CELL_HEIGHT})`
      );
      let instruction = instructions[index - 1];
      instruction = instruction.replace('ctx.fillStyle="', "");
      instruction = instruction.replace('";', "");
      return instruction;
    }

    function getFirstColHeaderFillColor() {
      const index = instructions.findIndex(
        (instr) =>
          instr === `ctx.fillRect(${HEADER_WIDTH}, ${0}, ${DEFAULT_CELL_WIDTH}, ${HEADER_HEIGHT})`
      );
      let instruction = instructions[index - 1];
      instruction = instruction.replace('ctx.fillStyle="', "");
      instruction = instruction.replace('";', "");
      return instruction;
    }

    let model: Model;
    let instructions: string[];
    let ctx: MockGridRenderingContext;
    let drawGridRenderer: (ctx: GridRenderingContext) => void;
    let container: DependencyContainer;

    beforeEach(() => {
      ({ drawGridRenderer, model, container } = setRenderer(
        new Model({ sheets: [{ colNumber: 2, rowNumber: 2 }] }),
        ["Headers"]
      ));
      const viewStore = container.get(ViewportsStore);
      const { width, height } = viewStore.sheetViewDimension;
      instructions = [];
      ctx = new MockGridRenderingContext(model, container, 1000, 1000, {
        onSet: (key, value) => {
          instructions.push(`ctx.${key}=${JSON.stringify(value)};`);
        },
        onGet: (key) => {
          instructions.push(`GET:${key}`);
        },
        onFunctionCall: (key, args) => {
          instructions.push(`ctx.${key}(${args.map((a) => JSON.stringify(a)).join(", ")})`);
        },
      });
      viewStore.resizeSheetView({
        width,
        height,
        gridOffsetX: HEADER_WIDTH,
        gridOffsetY: HEADER_HEIGHT,
      });
    });

    test("Color of headers containing the selection", () => {
      setSelection(model, ["A1"]);
      drawGridRenderer(ctx);

      const fillColHeaderInstr = getFirstColHeaderFillColor();
      expect(fillColHeaderInstr).toEqual(COLOR_THEMES.light.headerSelectedBackgroundColor);
      const fillRowHeaderInstr = getFirstRowHeaderFillColor();
      expect(fillRowHeaderInstr).toEqual(COLOR_THEMES.light.headerSelectedBackgroundColor);
    });

    test("Color of active headers", () => {
      setSelection(model, ["A1:B2"]);
      drawGridRenderer(ctx);

      const fillColHeaderInstr = getFirstColHeaderFillColor();
      expect(fillColHeaderInstr).toEqual(COLOR_THEMES.light.headerActiveBackgroundColor);
      const fillRowHeaderInstr = getFirstRowHeaderFillColor();
      expect(fillRowHeaderInstr).toEqual(COLOR_THEMES.light.headerActiveBackgroundColor);
    });
  });

  test("formulas evaluating to a string are properly aligned", () => {
    const { drawGridRenderer, model, container } = setRenderer();

    setCellContent(model, "A1", "1");
    setCellContent(model, "A2", "=A1");

    let textAligns: string[] = [];
    const ctx = new MockGridRenderingContext(model, container, 1000, 1000, {
      onSet: (key, value) => {
        if (key === "textAlign") {
          textAligns.push(value);
        }
      },
    });

    drawGridRenderer(ctx);
    expect(textAligns).toEqual(["right", "right"]);

    textAligns = [];
    setCellContent(model, "A1", "asdf");
    drawGridRenderer(ctx);
    expect(textAligns).toEqual(["left", "left"]);
  });

  test("formulas referencing an empty cell are properly aligned", () => {
    const { drawGridRenderer, model, container } = setRenderer();

    setCellContent(model, "A1", "=A2");

    const textAligns: string[] = [];
    const ctx = new MockGridRenderingContext(model, container, 1000, 1000, {
      onSet: (key, value) => {
        if (key === "textAlign") {
          textAligns.push(value);
        }
      },
    });

    drawGridRenderer(ctx);

    expect(textAligns).toEqual(["right"]);
  });

  test("numbers are aligned right when overflowing vertically", () => {
    const { drawGridRenderer, model, container } = setRenderer();

    setCellContent(model, "A1", "1");
    setFormatting(model, "A1", { fontSize: 36 });

    const textAligns: string[] = [];
    const ctx = new MockGridRenderingContext(model, container, 1000, 1000, {
      onSet: (key, value) => {
        if (key === "textAlign") {
          textAligns.push(value);
        }
      },
    });

    drawGridRenderer(ctx);

    expect(textAligns).toEqual(["right"]);
  });

  test("Cells evaluating to a number are properly aligned on overflow", () => {
    const { drawGridRenderer, model, container } = setRenderer(
      new Model({
        sheets: [
          {
            id: "1",
            cols: { 0: { size: 5 }, 2: { size: 25 } },
            colNumber: 3,
            cells: {
              A1: "123456",
              A2: "=A1",
              C1: "123456",
              C2: "=C1",
            },
            conditionalFormats: [
              {
                id: "1",
                ranges: ["C1:C2"],
                rule: {
                  type: "IconSetRule",
                  upperInflectionPoint: { type: "number", value: "1000", operator: "gt" },
                  lowerInflectionPoint: { type: "number", value: "0", operator: "gt" },
                  icons: { upper: "arrowGood", middle: "arrowNeutral", lower: "arrowBad" },
                },
              },
            ],
          },
        ],
      })
    );

    let textAligns: string[] = [];
    const ctx = new MockGridRenderingContext(model, container, 1000, 1000, {
      onSet: (key, value) => {
        if (key === "textAlign") {
          textAligns.push(value);
        }
      },
    });

    drawGridRenderer(ctx);

    expect(textAligns).toEqual(["left", "left", "left", "left"]); // A1-C1-A2-C2

    textAligns = [];
    setCellContent(model, "A1", "1");
    setCellContent(model, "C1", "1");
    drawGridRenderer(ctx);

    expect(textAligns).toEqual(["right", "right", "right", "right"]); // A1-C1-A2-C2
  });

  test("fillstyle of cell will be rendered", () => {
    const { drawGridRenderer, model, container } = setRenderer(
      new Model({ sheets: [{ colNumber: 1, rowNumber: 3 }] })
    );

    setFormatting(model, "A1", { fillColor: "#DC6CDF" });

    let fillStyle: any[] = [];
    let fillStyleColor1Called = false;
    let fillStyleColor2Called = false;
    const ctx = new MockGridRenderingContext(model, container, 1000, 1000, {
      onSet: (key, value) => {
        if (key === "fillStyle" && value === "#DC6CDF") {
          fillStyleColor1Called = true;
          fillStyleColor2Called = false;
        }
        if (key === "fillStyle" && value === "#DC6CDE") {
          fillStyleColor2Called = true;
          fillStyleColor1Called = false;
        }
      },
      onFunctionCall: (val, args) => {
        if (val === "fillRect" && fillStyleColor1Called) {
          fillStyle.push({ color: "#DC6CDF", x: args[0], y: args[1], w: args[2], h: args[3] });
          fillStyleColor1Called = false;
          fillStyleColor2Called = false;
        }
        if (val === "fillRect" && fillStyleColor2Called) {
          fillStyle.push({ color: "#DC6CDE", x: args[0], y: args[1], w: args[2], h: args[3] });
          fillStyleColor1Called = false;
          fillStyleColor2Called = false;
        }
      },
    });

    drawGridRenderer(ctx);

    expect(removeOffsetOfFillStyles(fillStyle)).toEqual([
      { color: "#DC6CDF", h: 23, w: 96, x: 0, y: 0 },
    ]);

    fillStyle = [];
    setFormatting(model, "A1", { fillColor: "#DC6CDE" });
    drawGridRenderer(ctx);

    expect(removeOffsetOfFillStyles(fillStyle)).toEqual([
      { color: "#DC6CDE", h: 23, w: 96, x: 0, y: 0 },
    ]);
  });

  test("fillstyle of merge will be rendered for all cells in merge", () => {
    const { drawGridRenderer, model, container } = setRenderer(
      new Model({ sheets: [{ colNumber: 1, rowNumber: 3 }] })
    );
    setFormatting(model, "A1", { fillColor: "#DC6CDF" });
    merge(model, "A1:A3");

    let fillStyle: any[] = [];
    let fillStyleColor1Called = false;
    let fillStyleColor2Called = false;
    const ctx = new MockGridRenderingContext(model, container, 1000, 1000, {
      onSet: (key, value) => {
        if (key === "fillStyle" && value === "#DC6CDF") {
          fillStyleColor1Called = true;
          fillStyleColor2Called = false;
        }
        if (key === "fillStyle" && value === "#DC6CDE") {
          fillStyleColor2Called = true;
          fillStyleColor1Called = false;
        }
      },
      onFunctionCall: (val, args) => {
        if (val === "fillRect" && fillStyleColor1Called) {
          fillStyle.push({ color: "#DC6CDF", x: args[0], y: args[1], w: args[2], h: args[3] });
          fillStyleColor1Called = false;
          fillStyleColor2Called = false;
        }
        if (val === "fillRect" && fillStyleColor2Called) {
          fillStyle.push({ color: "#DC6CDE", x: args[0], y: args[1], w: args[2], h: args[3] });
          fillStyleColor1Called = false;
          fillStyleColor2Called = false;
        }
      },
    });

    drawGridRenderer(ctx);

    expect(removeOffsetOfFillStyles(fillStyle)).toEqual([
      { color: "#DC6CDF", h: 3 * 23, w: 96, x: 0, y: 0 },
    ]);

    fillStyle = [];
    setFormatting(model, "A1", { fillColor: "#DC6CDE" });
    drawGridRenderer(ctx);

    expect(removeOffsetOfFillStyles(fillStyle)).toEqual([
      { color: "#DC6CDE", h: 3 * 23, w: 96, x: 0, y: 0 },
    ]);
  });

  test("fillstyle of cell works with CF", () => {
    const { drawGridRenderer, model, container } = setRenderer(
      new Model({ sheets: [{ colNumber: 1, rowNumber: 3 }] })
    );
    addEqualCf(model, "A1", { fillColor: "#DC6CDF" }, "1", "1");

    let fillStyle: any[] = [];
    let fillStyleColor1Called = false;
    const ctx = new MockGridRenderingContext(model, container, 1000, 1000, {
      onSet: (key, value) => {
        if (key === "fillStyle" && value === "#DC6CDF") {
          fillStyleColor1Called = true;
        }
      },
      onFunctionCall: (val, args) => {
        if (val === "fillRect" && fillStyleColor1Called) {
          fillStyle.push({ color: "#DC6CDF", x: args[0], y: args[1], w: args[2], h: args[3] });
          fillStyleColor1Called = false;
        }
      },
    });

    drawGridRenderer(ctx);

    expect(removeOffsetOfFillStyles(fillStyle)).toEqual([]);

    fillStyle = [];
    setCellContent(model, "A1", "1");
    drawGridRenderer(ctx);

    expect(removeOffsetOfFillStyles(fillStyle)).toEqual([
      { color: "#DC6CDF", h: 23, w: 96, x: 0, y: 0 },
    ]);
  });

  test("fill style of hovered clickable cells goes over regular fill style", () => {
    const { drawGridRenderer, model, container } = setRenderer(
      new Model({ sheets: [{ colNumber: 1, rowNumber: 3 }] })
    );
    const sheetId = model.getters.getSheetIds()[0];
    const background = "#DC6CDF";
    const hoverColor = blendColors(background, TABLE_HOVER_BACKGROUND_COLOR);
    createTable(model, "A1", { numberOfHeaders: 0 });
    setFormatting(model, "A1", { fillColor: background });
    setCellContent(model, "A1", "Data");
    model.updateMode("dashboard");
    container.get(HoveredTableStore); // Instantiate hovered table store

    let fillStyle = "";
    let fillStyles: any[] = [];
    let fillStyleCalled = false;
    const ctx = new MockGridRenderingContext(model, container, 1000, 1000, {
      onSet: (key, value) => {
        if (key === "fillStyle" && [background, hoverColor].includes(value)) {
          fillStyle = value;
          fillStyleCalled = true;
        }
      },
      onFunctionCall: (val, args) => {
        if (val === "fillRect" && fillStyleCalled) {
          fillStyles.push({ color: fillStyle, x: args[0], y: args[1], w: args[2], h: args[3] });
          fillStyleCalled = false;
        }
      },
    });

    drawGridRenderer(ctx);
    expect(removeOffsetOfFillStyles(fillStyles)).toEqual([
      { color: background, h: 23, w: 96, x: 0, y: 0 },
    ]);

    fillStyles = [];
    container.get(CellHoverOverlayStore).hover({ sheetId, col: 0, row: 0 });
    drawGridRenderer(ctx);

    expect(removeOffsetOfFillStyles(fillStyles)).toEqual([
      { color: background, h: 23, w: 96, x: 0, y: 0 },
      { color: hoverColor, h: 23, w: 96, x: 0, y: 0 },
    ]);
  });

  test("hovered clickable cell without a fill colour draws the overlay colour translucent", () => {
    const { drawGridRenderer, model, container } = setRenderer(
      new Model({ sheets: [{ colNumber: 1, rowNumber: 3 }] })
    );
    const sheetId = model.getters.getSheetIds()[0];
    setCellContent(model, "A1", "Data");
    // A1 has no fill colour of its own, so the overlay has nothing to blend against.
    container
      .get(CellHoverOverlayStore)
      .register({ getHighlightedPositions: () => [{ sheetId, col: 0, row: 0 }] });

    const fillStyles: string[] = [];
    let fillStyle = "";
    const ctx = new MockGridRenderingContext(model, container, 1000, 1000, {
      onSet: (key, value) => {
        if (key === "fillStyle") {
          fillStyle = value;
        }
      },
      onFunctionCall: (val) => {
        if (val === "fillRect") {
          fillStyles.push(fillStyle);
        }
      },
    });

    container.get(CellHoverOverlayStore).hover({ sheetId, col: 0, row: 0 });
    drawGridRenderer(ctx);
    expect(fillStyles).toContain(TABLE_HOVER_BACKGROUND_COLOR);
    expect(fillStyles).not.toContain(blendColors("#FFFFFF", TABLE_HOVER_BACKGROUND_COLOR));
  });

  test("selection is drawn translucent so it does not hide the background", () => {
    const { drawGridRenderer, model, container } = setRenderer(
      new Model({ sheets: [{ colNumber: 5, rowNumber: 5 }] }),
      ["Selection"]
    );

    const fillStyles: string[] = [];
    let fillStyle = "";
    const ctx = new MockGridRenderingContext(model, container, 1000, 1000, {
      onSet: (key, value) => {
        if (key === "fillStyle") {
          fillStyle = value;
        }
      },
      onFunctionCall: (val) => {
        if (val === "fillRect") {
          fillStyles.push(fillStyle);
        }
      },
    });

    selectCell(model, "A1");
    drawGridRenderer(ctx);

    const selectionFills = fillStyles.filter((color) =>
      isSameColor(color, COLOR_THEMES.light.singleCellSelectionBackgroundColor)
    );
    expect(selectionFills).not.toHaveLength(0);
    for (const color of selectionFills) {
      expect(colorToRGBA(color).a).toBeLessThan(1);
    }
  });

  test("fillstyle of merge works with CF", () => {
    const { drawGridRenderer, model, container } = setRenderer(
      new Model({ sheets: [{ colNumber: 1, rowNumber: 3 }] })
    );
    addEqualCf(model, "A1", { fillColor: "#DC6CDF" }, "1", "1");
    merge(model, "A1:A3");
    let fillStyle: any[] = [];
    let fillStyleColor1Called = false;
    const ctx = new MockGridRenderingContext(model, container, 1000, 1000, {
      onSet: (key, value) => {
        if (key === "fillStyle" && value === "#DC6CDF") {
          fillStyleColor1Called = true;
        }
      },
      onFunctionCall: (val, args) => {
        if (val === "fillRect" && fillStyleColor1Called) {
          fillStyle.push({ color: "#DC6CDF", x: args[0], y: args[1], w: args[2], h: args[3] });
          fillStyleColor1Called = false;
        }
      },
    });

    drawGridRenderer(ctx);

    expect(removeOffsetOfFillStyles(fillStyle)).toEqual([]);

    fillStyle = [];
    setCellContent(model, "A1", "1");
    drawGridRenderer(ctx);

    expect(removeOffsetOfFillStyles(fillStyle)).toEqual([
      { color: "#DC6CDF", h: 23 * 3, w: 96, x: 0, y: 0 },
    ]);
  });

  test("formula fingerprints", () => {
    const { drawGridRenderer, model, gridRendererStore, container, sheetId } = setRenderer(
      new Model({ sheets: [{ colNumber: 1, rowNumber: 6 }] })
    );
    const fingerprints = container.get(FormulaFingerprintStore);
    const viewStore = container.get(ViewportsStore);
    fingerprints.enable();

    // a colored cell but no fingerprint (it's a string)
    setFormatting(model, "A2", { fillColor: "#DC6CDF" });
    setCellContent(model, "A2", "Hi");

    // a cell with a formula
    setCellContent(model, "A3", '="hello"');

    // a formula within a merge
    merge(model, "A4:A5");
    setCellContent(model, "A4", '="merge"');

    const renderingCtx = new MockGridRenderingContext(model, container, 1000, 1000, {});
    drawGridRenderer(renderingCtx);

    expect(getBoxFromText(gridRendererStore, "Hi")).toMatchObject({
      ...viewStore.viewports.getVisibleRect(sheetId, toZone("A2")),
      style: { fillColor: undefined },
    });
    expect(getBoxFromText(gridRendererStore, "hello")).toMatchObject({
      ...viewStore.viewports.getVisibleRect(sheetId, toZone("A3")),
      style: { fillColor: getFingerprint(fingerprints, "A3") },
    });
    expect(getBoxFromText(gridRendererStore, "merge")).toMatchObject({
      ...viewStore.viewports.getVisibleRect(sheetId, toZone("A4:A5")),
      style: { fillColor: getFingerprint(fingerprints, "A4") },
    });
  });

  test("formulas in a merge, evaluating to a string are properly aligned", () => {
    const { drawGridRenderer, model, container } = setRenderer();
    merge(model, "A2:B2");
    setCellContent(model, "A1", "1");
    setCellContent(model, "A2", "=A1");

    let textAligns: string[] = [];

    const ctx = new MockGridRenderingContext(model, container, 1000, 1000, {
      onSet: (key, value) => {
        if (key === "textAlign") {
          textAligns.push(value);
        }
      },
    });
    drawGridRenderer(ctx);

    expect(textAligns).toEqual(["right", "right"]);

    setCellContent(model, "A1", "asdf");

    textAligns = [];
    drawGridRenderer(ctx);

    expect(textAligns).toEqual(["left", "left"]);
  });

  test("formulas evaluating to a boolean are properly aligned", () => {
    const { drawGridRenderer, model, container } = setRenderer();

    setCellContent(model, "A1", "1");
    setCellContent(model, "A2", "=A1");

    let textAligns: string[] = [];
    const ctx = new MockGridRenderingContext(model, container, 1000, 1000, {
      onSet: (key, value) => {
        if (key === "textAlign") {
          textAligns.push(value);
        }
      },
    });
    drawGridRenderer(ctx);

    expect(textAligns).toEqual(["right", "right"]);

    textAligns = [];
    setCellContent(model, "A1", "true");
    drawGridRenderer(ctx);

    expect(textAligns).toEqual(["center", "center"]);
  });

  test("Cells in a merge evaluating to a number are properly aligned on overflow", () => {
    const { drawGridRenderer, model, container } = setRenderer(
      new Model({
        sheets: [
          {
            id: "1",
            colNumber: 4,
            cols: {
              0: { size: 2 + MIN_CELL_TEXT_MARGIN },
              1: { size: 2 + MIN_CELL_TEXT_MARGIN },
              2: { size: 12 + MIN_CELL_TEXT_MARGIN },
              3: { size: 12 + MIN_CELL_TEXT_MARGIN },
            },
            merges: ["A2:B2", "C2:D2"],
            cells: {
              A1: "123456789",
              A2: "=A1",
              C1: "123456891234",
              C2: "=C1",
            },
            conditionalFormats: [
              {
                id: "1",
                ranges: ["C1:D2"],
                rule: {
                  type: "IconSetRule",
                  upperInflectionPoint: { type: "number", value: "1000", operator: "gt" },
                  lowerInflectionPoint: { type: "number", value: "0", operator: "gt" },
                  icons: { upper: "arrowGood", middle: "arrowNeutral", lower: "arrowBad" },
                },
              },
            ],
          },
        ],
      })
    );

    let textAligns: string[] = [];
    const ctx = new MockGridRenderingContext(model, container, 1000, 1000, {
      onSet: (key, value) => {
        if (key === "textAlign") {
          textAligns.push(value);
        }
      },
    });

    drawGridRenderer(ctx);

    expect(textAligns).toEqual(["left", "left", "left", "left"]); // A1-C1-A2:B2-C2:D2

    textAligns = [];
    setCellContent(model, "A1", "1");
    setCellContent(model, "C1", "1");
    drawGridRenderer(ctx);

    expect(textAligns).toEqual(["right", "left", "right", "right"]); // A1-C1-A2:B2-C2:D2. C1 is still in overflow
  });

  test("formulas in a merge, evaluating to a boolean are properly aligned", () => {
    const { drawGridRenderer, model, container } = setRenderer();
    merge(model, "A2:B2");
    setCellContent(model, "A1", "1");
    setCellContent(model, "A2", "=A1");

    let textAligns: string[] = [];

    const ctx = new MockGridRenderingContext(model, container, 1000, 1000, {
      onSet: (key, value) => {
        if (key === "textAlign") {
          textAligns.push(value);
        }
      },
    });
    drawGridRenderer(ctx);

    expect(textAligns).toEqual(["right", "right"]);

    setCellContent(model, "A1", "false");

    textAligns = [];
    drawGridRenderer(ctx);

    expect(textAligns).toEqual(["center", "center"]);
  });

  test("errors are aligned to the center", () => {
    const { drawGridRenderer, model, container } = setRenderer();

    setCellContent(model, "A1", "=A1");

    const textAligns: string[] = [];

    const ctx = new MockGridRenderingContext(model, container, 1000, 1000, {
      onSet: (key, value) => {
        if (key === "textAlign") {
          textAligns.push(value);
        }
      },
    });
    drawGridRenderer(ctx);

    expect(textAligns).toEqual(["center"]);
  });

  test("dates are aligned to the right", () => {
    const { drawGridRenderer, model, container } = setRenderer();

    setCellContent(model, "A1", "03/23/2010");

    const textAligns: string[] = [];

    const ctx = new MockGridRenderingContext(model, container, 1000, 1000, {
      onSet: (key, value) => {
        if (key === "textAlign") {
          textAligns.push(value);
        }
      },
    });
    drawGridRenderer(ctx);

    expect(textAligns).toEqual(["right"]);
  });

  test("functions are aligned to the left", () => {
    const { drawGridRenderer, model, container } = setRenderer();

    setCellContent(model, "A1", "=SUM(1,2)");
    setFormulaVisibility(model, true);
    const textAligns: string[] = [];

    const ctx = new MockGridRenderingContext(model, container, 1000, 1000, {
      onSet: (key, value) => {
        if (key === "textAlign") {
          textAligns.push(value);
        }
      },
    });

    const getCellTextMock = jest.fn(() => "=SUM(1,2)");
    model.getters.getCellText = getCellTextMock;

    drawGridRenderer(ctx);

    expect(textAligns).toEqual(["left"]);
    expect(getCellTextMock).toHaveBeenLastCalledWith(
      { sheetId: expect.any(String), col: 0, row: 0 },
      { showFormula: true, availableWidth: DEFAULT_CELL_WIDTH - 2 * MIN_CELL_TEXT_MARGIN }
    );
  });

  test("functions with centered content are aligned to the left", () => {
    const { drawGridRenderer, model, container } = setRenderer();
    setFormatting(model, "A1", { align: "center" });

    setCellContent(model, "A1", "=SUM(1,2)");
    setFormulaVisibility(model, true);
    const textAligns: string[] = [];

    const ctx = new MockGridRenderingContext(model, container, 1000, 1000, {
      onSet: (key, value) => {
        if (key === "textAlign") {
          textAligns.push(value);
        }
      },
    });

    const getCellTextMock = jest.fn(() => "=SUM(1,2)");
    model.getters.getCellText = getCellTextMock;

    drawGridRenderer(ctx);

    expect(textAligns).toEqual(["left"]);
    expect(getCellTextMock).toHaveBeenLastCalledWith(
      { sheetId: expect.any(String), col: 0, row: 0 },
      { showFormula: true, availableWidth: 0 }
    );
  });

  test("CF on empty cell", () => {
    const { drawGridRenderer, model, container } = setRenderer(
      new Model({ sheets: [{ colNumber: 1, rowNumber: 1 }] })
    );
    let fillStyle: any[] = [];
    let fillStyleColor1Called = false;
    const ctx = new MockGridRenderingContext(model, container, 1000, 1000, {
      onSet: (key, value) => {
        if (key === "fillStyle" && value === "#DC6CDF") {
          fillStyleColor1Called = true;
        }
      },
      onFunctionCall: (val, args) => {
        if (val === "fillRect" && fillStyleColor1Called) {
          fillStyle.push({ color: "#DC6CDF", x: args[0], y: args[1], w: args[2], h: args[3] });
          fillStyleColor1Called = false;
        }
      },
    });

    drawGridRenderer(ctx);

    expect(removeOffsetOfFillStyles(fillStyle)).toEqual([]);
    fillStyle = [];
    const rule: CellIsRule = {
      type: "CellIsRule",
      operator: "isEmpty",
      values: [],
      style: { fillColor: "#DC6CDF" },
    };
    const result = addCfRule(model, "A1", rule, "1");
    expect(result).toBeSuccessfullyDispatched();
    drawGridRenderer(ctx);

    expect(removeOffsetOfFillStyles(fillStyle)).toEqual([
      { color: "#DC6CDF", h: 23, w: 96, x: 0, y: 0 },
    ]);
  });

  test("horizontal align on pivot header", () => {
    const pivotModel = createModelWithTestPivotDataset();
    createSheet(pivotModel, { sheetId: "2", activate: true });
    setCellContent(pivotModel, "A1", "=PIVOT(1)");
    const { drawGridRenderer, model, gridRendererStore, container } = setRenderer(pivotModel);
    setFormatting(model, "B1", { align: "right" });
    const contex2D = new MockGridRenderingContext(model, container, 1000, 1000, {});
    drawGridRenderer(contex2D);
    const box = getBoxFromText(gridRendererStore, "Alice");
    expect(box.x).toBeDefined();
    expect(box?.content?.align).toEqual("right");
  });

  test.each(["I am a very long text", "100000000000000"])(
    "Overflowing left-aligned content is correctly clipped",
    (overflowingContent) => {
      let box: Box;
      const { drawGridRenderer, model, gridRendererStore, container } = setRenderer(
        new Model({
          sheets: [
            {
              id: "sheet1",
              colNumber: 3,
              rowNumber: 1,
              cols: { 1: { size: 5 } },
              cells: { B1: overflowingContent },
              styles: { B1: 1 },
            },
          ],
          styles: { 1: { align: "left" } },
        })
      );

      const ctx = new MockGridRenderingContext(model, container, 1000, 1000, {});
      drawGridRenderer(ctx);

      box = getBoxFromText(gridRendererStore, overflowingContent);
      // no clip
      expect(box.clipRect).toBeUndefined();
      expect(box.isOverflow).toBeTruthy();

      // no clipping at the left
      setCellContent(model, "A1", "Content at the left");
      drawGridRenderer(ctx);
      box = getBoxFromText(gridRendererStore, overflowingContent);
      expect(box.clipRect).toBeUndefined();
      expect(box.isOverflow).toBeTruthy();

      // clipping at the right
      setCellContent(model, "C1", "Content at the right");
      drawGridRenderer(ctx);
      box = getBoxFromText(gridRendererStore, overflowingContent);
      expect(box.clipRect).toEqual({
        x: DEFAULT_CELL_WIDTH,
        y: 0,
        width: 5,
        height: DEFAULT_CELL_HEIGHT,
      });
    }
  );

  test.each([{ align: "left" }, { align: undefined }])(
    "Overflowing number with % align is correctly clipped",
    (style) => {
      const overflowingNumber = "100000000000000";
      let box: Box;
      const { drawGridRenderer, model, gridRendererStore, container } = setRenderer(
        new Model({
          sheets: [
            {
              id: "sheet1",
              colNumber: 3,
              rowNumber: 1,
              cols: { 1: { size: 5 } },
              cells: { B1: overflowingNumber },
              styles: { B1: 1 },
            },
          ],
          styles: { 1: style },
        })
      );

      const ctx = new MockGridRenderingContext(model, container, 1000, 1000, {});
      drawGridRenderer(ctx);

      box = getBoxFromText(gridRendererStore, overflowingNumber);
      // no clip
      expect(box.clipRect).toBeUndefined();
      expect(box.isOverflow).toBeTruthy();

      // no clipping at the left
      setCellContent(model, "A1", "Content at the left");
      drawGridRenderer(ctx);
      box = getBoxFromText(gridRendererStore, overflowingNumber);
      expect(box.clipRect).toBeUndefined();
      expect(box.isOverflow).toBeTruthy();

      // clipping at the right
      setCellContent(model, "C1", "Content at the right");
      drawGridRenderer(ctx);
      box = getBoxFromText(gridRendererStore, overflowingNumber);
      expect(box.clipRect).toEqual({
        x: DEFAULT_CELL_WIDTH,
        y: 0,
        width: 5,
        height: DEFAULT_CELL_HEIGHT,
      });
    }
  );

  test("Overflowing right-aligned text is correctly clipped", () => {
    const overflowingText = "I am a very long text";
    let box: Box;
    const { drawGridRenderer, model, gridRendererStore, container } = setRenderer(
      new Model({
        sheets: [
          {
            id: "sheet1",
            colNumber: 3,
            rowNumber: 1,
            cols: { 1: { size: 5 } },
            cells: { B1: overflowingText },
            styles: { B1: 1 },
          },
        ],
        styles: { 1: { align: "right" } },
      })
    );

    const ctx = new MockGridRenderingContext(model, container, 1000, 1000, {});
    drawGridRenderer(ctx);

    box = getBoxFromText(gridRendererStore, overflowingText);
    // no clip
    expect(box.clipRect).toBeUndefined();
    expect(box.isOverflow).toBeTruthy();

    // no clipping at the right
    setCellContent(model, "C1", "Content at the left");
    drawGridRenderer(ctx);
    box = getBoxFromText(gridRendererStore, overflowingText);
    expect(box.clipRect).toBeUndefined();
    expect(box.isOverflow).toBeTruthy();

    // clipping at the left
    setCellContent(model, "A1", "Content at the right");
    drawGridRenderer(ctx);
    box = getBoxFromText(gridRendererStore, overflowingText);
    expect(box.clipRect).toEqual({
      x: DEFAULT_CELL_WIDTH,
      y: 0,
      width: 5,
      height: DEFAULT_CELL_HEIGHT,
    });
  });

  test("Overflowing centered content is clipped on left side correctly without overlapping", () => {
    const overflowingContent = "I am a very long long long long long long text";
    // using alternative col size to clarify the computations
    const colSize = 5;
    const { drawGridRenderer, model, gridRendererStore, container } = setRenderer(
      new Model({
        sheets: [
          {
            id: "sheet1",
            colNumber: 5,
            rowNumber: 1,
            cols: { 1: { size: colSize }, 2: { size: colSize } },
            cells: { C1: overflowingContent, A1: "left" },
            styles: { C1: 1 },
          },
        ],
        styles: { 1: { align: "center" } },
      })
    );
    const ctx = new MockGridRenderingContext(model, container, 1000, 1000, {});
    drawGridRenderer(ctx);

    const centeredBox = getBoxFromText(gridRendererStore, overflowingContent);
    expect(centeredBox.clipRect).toEqual({
      x: DEFAULT_CELL_WIDTH, // clipped to the left
      y: 0,
      width: 2 * (colSize + DEFAULT_CELL_WIDTH),
      height: DEFAULT_CELL_HEIGHT,
    });
  });

  test("Overflowing centered content is clipped on right side correctly without overlapping", () => {
    const overflowingContent = "I am a very long long long long long long text";
    // using alternative col size to clarify the computations
    const colSize = 5;
    const { drawGridRenderer, model, gridRendererStore, container } = setRenderer(
      new Model({
        sheets: [
          {
            id: "sheet1",
            colNumber: 10,
            rowNumber: 1,
            cols: { 2: { size: colSize }, 3: { size: colSize } },
            cells: { C1: overflowingContent, E1: "right" },
            styles: { C1: 2 },
          },
        ],
        styles: { 2: { align: "center" } },
      })
    );
    const ctx = new MockGridRenderingContext(model, container, 1000, 1000, {});
    drawGridRenderer(ctx);

    const centeredBox = getBoxFromText(gridRendererStore, overflowingContent);
    const cell = getCell(model, "C1")! as LiteralCell;
    const contentWidth =
      model.getters.getTextWidth(cell.content, cell.style || {}) + MIN_CELL_TEXT_MARGIN;
    const expectedClipX = 2 * DEFAULT_CELL_WIDTH + colSize / 2 - contentWidth / 2;
    expect(centeredBox.clipRect).toEqual({
      x: expectedClipX,
      y: 0,
      width: 2 * DEFAULT_CELL_WIDTH + 2 * colSize - expectedClipX,
      height: DEFAULT_CELL_HEIGHT,
    });
  });

  test.each(["left", "right", "center"])(
    "Content in merge is clipped and cannot overflow",
    (align) => {
      const overflowingText = "I am a very long text";
      const { drawGridRenderer, model, gridRendererStore, container } = setRenderer(
        new Model({
          sheets: [
            {
              id: "sheet1",
              colNumber: 5,
              rowNumber: 5,
              cols: { 1: { size: 5 }, 2: { size: 5 } },
              cells: { B1: overflowingText },
              styles: { B1: 1 },
              merges: ["B1:C2"],
            },
          ],
          styles: { 1: { align } },
        })
      );

      const ctx = new MockGridRenderingContext(model, container, 1000, 1000, {});
      drawGridRenderer(ctx);

      const box = getBoxFromText(gridRendererStore, overflowingText);
      expect(box.clipRect).toEqual({
        x: DEFAULT_CELL_WIDTH,
        y: 0,
        width: 10,
        height: DEFAULT_CELL_HEIGHT * 2,
      });
    }
  );

  test.each([
    ["right", "A1:A2"],
    ["left", "C1:C2"],
    ["center", "A1:A2", "C1:C2"],
  ])("Content cannot overflow over merge with align %s", (align, ...merges) => {
    const overflowingText = "I am a very long text";
    const { drawGridRenderer, model, gridRendererStore, container } = setRenderer(
      new Model({
        sheets: [
          {
            id: "sheet1",
            colNumber: 5,
            rowNumber: 5,
            cols: { 1: { size: 5 } },
            cells: { B2: overflowingText },
            styles: { B2: 1 },
            merges,
          },
        ],
        styles: { 1: { align } },
      })
    );

    const ctx = new MockGridRenderingContext(model, container, 1000, 1000, {});
    drawGridRenderer(ctx);

    const box = getBoxFromText(gridRendererStore, overflowingText);
    expect(box.clipRect).toEqual({
      x: DEFAULT_CELL_WIDTH,
      y: DEFAULT_CELL_HEIGHT,
      width: 5,
      height: DEFAULT_CELL_HEIGHT,
    });
  });

  test.each(["left", "right", "center"])(
    'Cells with the wrapping style "wrap" cannot overflow long text content',
    (align) => {
      const overflowingText = "I am a very very very long text";
      const { drawGridRenderer, model, gridRendererStore, container } = setRenderer(
        new Model({
          sheets: [
            {
              id: "sheet1",
              colNumber: 3,
              rowNumber: 1,
              cols: { 1: { size: 20 } },
              cells: { B1: overflowingText },
              styles: { B1: 1 },
            },
          ],
          styles: { 1: { align, wrapping: "wrap" } },
        })
      );

      const ctx = new MockGridRenderingContext(model, container, 1000, 1000, {});
      drawGridRenderer(ctx);

      const box = getBoxFromText(gridRendererStore, overflowingText);
      expect(box.clipRect).toEqual({
        x: DEFAULT_CELL_WIDTH, // clipped to the left
        y: 0,
        width: 20, // clipped to the right
        height: model.getters.getRowSize("sheet1", 0),
      });
    }
  );

  test.each(["left", "right", "center"])(
    'Cells with the wrapping style "crop" cannot overflow long text content',
    (align) => {
      const overflowingText = "I am a very very very long text";
      const { drawGridRenderer, model, gridRendererStore, container } = setRenderer(
        new Model({
          sheets: [
            {
              id: "sheet1",
              colNumber: 3,
              rowNumber: 1,
              cols: { 1: { size: 20 } },
              cells: { B1: overflowingText },
              styles: { B1: 1 },
            },
          ],
          styles: { 1: { align, wrapping: "clip" } },
        })
      );

      const ctx = new MockGridRenderingContext(model, container, 1000, 1000, {});
      drawGridRenderer(ctx);

      const box = getBoxFromText(gridRendererStore, overflowingText);
      expect(box.clipRect).toEqual({
        x: DEFAULT_CELL_WIDTH, // clipped to the left
        y: 0,
        width: 20, // clipped to the right
        height: model.getters.getRowSize("sheet1", 0),
      });
    }
  );

  test("cells with a fontsize too big for the row height are clipped", () => {
    const overflowingText = "TOO HIGH";
    const fontSize = 26;
    let box: Box;
    const { drawGridRenderer, model, gridRendererStore, container } = setRenderer(
      new Model({
        sheets: [
          {
            id: "sheet1",
            colNumber: 1,
            rowNumber: 1,
            rows: { 0: { size: Math.floor(fontSizeInPixels(fontSize) + 5) } },
            cells: { A1: overflowingText },
            styles: { A1: 1 },
          },
        ],
        styles: { 1: { fontSize } },
      })
    );

    const ctx = new MockGridRenderingContext(model, container, 1000, 1000, {});
    drawGridRenderer(ctx);

    box = getBoxFromText(gridRendererStore, overflowingText);
    expect(box.clipRect).toBeUndefined();

    resizeRows(model, [0], Math.floor(fontSizeInPixels(fontSize) / 2));
    drawGridRenderer(ctx);
    box = getBoxFromText(gridRendererStore, overflowingText);
    expect(box.clipRect).toEqual({
      x: 0,
      y: 0,
      width: DEFAULT_CELL_WIDTH,
      height: Math.floor(fontSizeInPixels(fontSize) / 2),
    });
  });

  test("cells overflowing in Y have a correct clipRect", () => {
    const { drawGridRenderer, model, gridRendererStore, container } = setRenderer();
    const overflowingText = "I am a very very very long text that is also too high";
    const fontSize = 26;

    setCellContent(model, "A1", overflowingText);
    setFormatting(model, "A1", { fontSize });
    resizeRows(model, [0], Math.floor(fontSizeInPixels(fontSize) / 2));
    resizeColumns(model, ["A"], 10);

    const ctx = new MockGridRenderingContext(model, container, 1000, 1000, {});
    drawGridRenderer(ctx);

    expect(getBoxFromText(gridRendererStore, overflowingText).clipRect).toEqual({
      x: 0,
      y: 0,
      width: 1000,
      height: Math.floor(fontSizeInPixels(fontSize) / 2),
    });
  });

  test("cells with icon CF are correctly clipped", () => {
    let box: Box;
    const cellContent = "10000";
    const { drawGridRenderer, model, gridRendererStore, container } = setRenderer(
      new Model({
        sheets: [
          {
            id: "sheet1",
            colNumber: 1,
            rowNumber: 1,
            cells: { A1: "10000" },
            conditionalFormats: [
              {
                id: "1",
                ranges: ["A1"],
                rule: {
                  type: "IconSetRule",
                  upperInflectionPoint: { type: "number", value: "1000", operator: "gt" },
                  lowerInflectionPoint: { type: "number", value: "0", operator: "gt" },
                  icons: { upper: "arrowGood", middle: "arrowNeutral", lower: "arrowBad" },
                },
              },
            ],
          },
        ],
      })
    );

    let instructions: string[] = [];
    const ctx = new MockGridRenderingContext(model, container, 1000, 1000, {
      onSet: (key, value) => {
        instructions.push(`context.${key}=${JSON.stringify(value)};`);
      },
      onGet: (key) => {
        instructions.push(`GET:${key}`);
      },
      onFunctionCall: (key, args) => {
        instructions.push(`context.${key}(${args.map((a) => JSON.stringify(a)).join(", ")})`);
      },
    });
    const getCFClipRectInstruction = () => {
      const cfFillInstrIndex = instructions.findIndex((instr) => instr.includes("#6AA84F"));
      return instructions
        .slice(0, cfFillInstrIndex)
        .reverse()
        .find((instr) => instr.includes("rect"));
    };

    drawGridRenderer(ctx);
    box = getBoxFromText(gridRendererStore, cellContent);
    const maxIconBoxWidth = box.icons.left!.size + MIN_CF_ICON_MARGIN;
    expect(getCFClipRectInstruction()).toEqual(
      `context.rect(0, 0, ${DEFAULT_CELL_WIDTH}, ${DEFAULT_CELL_HEIGHT})`
    );

    expect(box.clipRect).toEqual({
      x: maxIconBoxWidth,
      y: 0,
      width: DEFAULT_CELL_WIDTH - maxIconBoxWidth,
      height: DEFAULT_CELL_HEIGHT,
    });

    resizeColumns(model, ["A"], maxIconBoxWidth - 3);
    instructions = [];
    drawGridRenderer(ctx);
    box = getBoxFromText(gridRendererStore, cellContent);
    expect(getCFClipRectInstruction()).toEqual(
      `context.rect(0, 0, ${maxIconBoxWidth - 3}, ${DEFAULT_CELL_HEIGHT})`
    );
    expect(box.clipRect).toEqual({
      x: maxIconBoxWidth,
      y: 0,
      width: 0,
      height: DEFAULT_CELL_HEIGHT,
    });
  });

  test("Cells are clipped with data validation icons", () => {
    let box: Box;
    const cellContent = "This is a long text that should be clipped";
    const { drawGridRenderer, model, gridRendererStore, container } = setRenderer();
    resizeColumns(model, ["A"], 10);
    setCellContent(model, "A1", cellContent);

    addDataValidation(model, "B1", "id", { type: "isBoolean", values: [] });

    const ctx = new MockGridRenderingContext(model, container, 1000, 1000, {});
    drawGridRenderer(ctx);
    box = getBoxFromText(gridRendererStore, cellContent);
    const expectedClipRect = { x: 0, y: 0, width: 10, height: DEFAULT_CELL_HEIGHT };
    expect(box.clipRect).toEqual(expectedClipRect);

    addDataValidation(model, "B1", "id", {
      type: "isValueInList",
      values: ["a"],
      displayStyle: "arrow",
    });
    drawGridRenderer(ctx);
    box = getBoxFromText(gridRendererStore, cellContent);
    expect(box.clipRect).toEqual(expectedClipRect);
  });

  test.each([
    ["right", ["left"], { left: 1, right: 1, top: 1, bottom: 1 }], // align right, left border => clipped on cell zone
    ["right", ["left", "right"], { left: 1, right: 1, top: 1, bottom: 1 }], // align right, left + right border => clipped on cell zone
    ["right", ["right"], undefined], // align right, right border => no clip

    ["left", ["right"], { left: 1, right: 1, top: 1, bottom: 1 }], // align left, right border => clipped on cell zone
    ["left", ["left", "right"], { left: 1, right: 1, top: 1, bottom: 1 }], // align left, left + right border => clipped on cell zone
    ["left", ["left"], undefined], // align left, left border => no clip

    ["center", ["left", "right"], { left: 1, right: 1, top: 1, bottom: 1 }], // align center, left + right border => clipped on cell zone
    ["center", ["left"], { left: 1, right: 2, top: 1, bottom: 1 }], // align center, right border => clipped left
  ])(
    "cells aligned %s with borders %s are correctly clipped",
    (align: string, borders: string[], expectedClipRectZone: Zone | undefined) => {
      const cellContent = "This is a long text larger than a cell";
      const { drawGridRenderer, model, gridRendererStore, container, sheetId } = setRenderer(
        new Model({
          sheets: [
            {
              id: "sheet1",
              colNumber: 3,
              rowNumber: 3,
              cells: { B2: cellContent },
              cols: { 1: { size: 10 } },
            },
          ],
        })
      );
      const viewStore = container.get(ViewportsStore);

      setFormatting(model, "B2", { align: align as Align });

      for (const border of borders) {
        setZoneBorders(model, { position: border as BorderPosition }, ["B2"]);
      }

      const ctx = new MockGridRenderingContext(model, container, 1000, 1000, {});
      drawGridRenderer(ctx);
      const box = getBoxFromText(gridRendererStore, cellContent);
      expect(box.clipRect).toEqual(
        expectedClipRectZone
          ? viewStore.viewports.getVisibleRect(sheetId, expectedClipRectZone)
          : undefined
      );
    }
  );

  test("Cell overflowing text centered is cut correctly when there's a border", () => {
    const cellContent = "This is a long text larger than a cell";

    const model = new Model();
    resizeColumns(model, ["B"], 10);
    setCellContent(model, "B2", cellContent);
    setFormatting(model, "B2", { align: "center" });
    setZoneBorders(model, { position: "right" }, ["B2"]);

    const { drawGridRenderer, gridRendererStore, container, sheetId } = setRenderer(model);
    const viewStore = container.get(ViewportsStore);
    const ctx = new MockGridRenderingContext(model, container, 1000, 1000, {});
    drawGridRenderer(ctx);
    const box = getBoxFromText(gridRendererStore, cellContent);
    const cell = getCell(model, "B2")! as LiteralCell;
    const textWidth =
      model.getters.getTextWidth(cell.content, cell.style || {}) + MIN_CELL_TEXT_MARGIN;
    const expectedClipRect = viewStore.viewports.getVisibleRect(sheetId, {
      left: 0,
      right: 1,
      top: 1,
      bottom: 1,
    });
    const expectedCLipX = box.x + box.width / 2 - textWidth / 2;
    expect(box.clipRect).toEqual({
      ...expectedClipRect,
      x: expectedCLipX,
      width: expectedClipRect.x + expectedClipRect.width - expectedCLipX,
    });
  });

  test.each([
    ["right", { left: 1, right: 2, top: 0, bottom: 0 }], // align right, left border => clipped on cell zone
    ["left", { left: 2, right: 3, top: 0, bottom: 0 }], // align left, left + right border => clipped on cell zone
    ["center", { left: 1, right: 3, top: 0, bottom: 0 }], // align center, right border => clipped left
  ])(
    "Cell text overflowing on multiple cells is cut as soon as it encounter a border with align %s",
    (align: string, expectedClipRectZone: Zone | undefined) => {
      const cellContent = "This is a very vey very very very very long text larger than a cell";
      const { drawGridRenderer, model, gridRendererStore, container, sheetId } = setRenderer(
        new Model({
          sheets: [
            {
              id: "sheet1",
              colNumber: 6,
              rowNumber: 6,
              cells: { C1: cellContent },
              cols: { 1: { size: 10 }, 2: { size: 10 }, 3: { size: 10 } },
            },
          ],
        })
      );
      const viewStore = container.get(ViewportsStore);

      setZoneBorders(model, { position: "right" }, ["A1"]);
      setFormatting(model, "C1", { align: align as Align });
      setZoneBorders(model, { position: "left" }, ["E1"]);

      const ctx = new MockGridRenderingContext(model, container, 1000, 1000, {});
      drawGridRenderer(ctx);
      const box = getBoxFromText(gridRendererStore, cellContent);
      expect(box.clipRect).toEqual(
        expectedClipRectZone
          ? viewStore.viewports.getVisibleRect(sheetId, expectedClipRectZone)
          : undefined
      );
    }
  );
  test("Box clip rect computation take the text margin into account", () => {
    let box: Box;
    const { drawGridRenderer, model, gridRendererStore, container } = setRenderer(
      new Model({ sheets: [{ id: "sheet1", colNumber: 1, rowNumber: 1 }] })
    );
    resizeColumns(model, ["A"], 10);

    // Text + MIN_CELL_TEXT_MARGIN  <= col size, no clip
    let ctx = new MockGridRenderingContext(model, container, 1000, 1000, {});
    let text = "a".repeat(10 - MIN_CELL_TEXT_MARGIN);
    setCellContent(model, "A1", text);
    drawGridRenderer(ctx);
    box = getBoxFromText(gridRendererStore, text);
    expect(box.clipRect).toBeUndefined();

    // Text + MIN_CELL_TEXT_MARGIN  > col size, clip text
    ctx = new MockGridRenderingContext(model, container, 1000, 1000, {});
    text = "a".repeat(10);
    setCellContent(model, "A1", text);
    drawGridRenderer(ctx);
    box = getBoxFromText(gridRendererStore, text);
    expect(box.clipRect).toEqual({ x: 0, y: 0, width: 10, height: DEFAULT_CELL_HEIGHT });
  });

  test.each(["A1", "A1:A2", "A1:A2,B1:B2", "A1,C1"])(
    "compatible copied zones %s are all outlined with dots",
    (targetXc) => {
      const { drawGridRenderer, model, container } = setRenderer(new Model(), ["Clipboard"]);
      copy(model, ...targetXc.split(","));
      const { ctx, isDotOutlined, reset } = watchDashedOutline(model, container);
      drawGridRenderer(ctx);
      const copiedTarget = target(targetXc);
      expect(isDotOutlined(copiedTarget)).toBeTruthy();
      paste(model, "A10");
      reset();
      drawGridRenderer(ctx);
      expect(isDotOutlined(copiedTarget)).toBeFalsy();
    }
  );

  test.each(["A1,A2", "A1:A2,A4:A5"])(
    "only last copied non-compatible zones %s is outlined with dots",
    (targetXc) => {
      const { drawGridRenderer, model, container } = setRenderer(new Model(), ["Clipboard"]);
      copy(model, ...targetXc.split(","));
      const { ctx, isDotOutlined, reset } = watchDashedOutline(model, container);
      drawGridRenderer(ctx);
      const copiedTarget = target(targetXc);
      const expectedOutlinedZone = copiedTarget.slice(-1);
      expect(isDotOutlined(expectedOutlinedZone)).toBeTruthy();
      paste(model, "A10");
      reset();
      drawGridRenderer(ctx);
      expect(isDotOutlined(expectedOutlinedZone)).toBeFalsy();
    }
  );

  test.each([
    (model) => setCellContent(model, "B15", "hello"),
    (model) => addColumns(model, "after", "B", 1),
    (model) => deleteColumns(model, ["K"]),
  ])("copied zone outline is removed at first change to the grid", (coreOperation) => {
    const { drawGridRenderer, model, container } = setRenderer(new Model(), ["Clipboard"]);
    copy(model, "A1:A2");
    const { ctx, isDotOutlined, reset } = watchDashedOutline(model, container);
    drawGridRenderer(ctx);

    const copiedTarget = target("A1:A2");
    expect(isDotOutlined(copiedTarget)).toBeTruthy();
    coreOperation(model);
    reset();
    drawGridRenderer(ctx);

    expect(isDotOutlined(copiedTarget)).toBeFalsy();
  });

  test.each([
    ["dashboard" as Mode, { x: 0, y: 0, width: DEFAULT_CELL_WIDTH, height: DEFAULT_CELL_HEIGHT }],
    ["normal" as Mode, { x: 0, y: 0, width: DEFAULT_CELL_WIDTH, height: DEFAULT_CELL_HEIGHT }],
  ])("A1 starts at the upper left corner with mode %s", (mode, expectedRect) => {
    const model = new Model({}, { mode });
    const sheetId = model.getters.getActiveSheetId();
    const { store: viewStore } = makeStoreWithModel(model, ViewportsStore);
    const rect = viewStore.viewports.getVisibleRect(sheetId, toZone("A1"));
    expect(rect).toEqual(expectedRect);
  });

  test("Error red triangle is correctly displayed/hidden", () => {
    /* Test if the error upper-right red triangle is correctly displayed
     * according to the kind of error
     */
    const { drawGridRenderer, model, gridRendererStore, container } = setRenderer(
      new Model({
        sheets: [
          {
            id: "sheet1",
            colNumber: 10,
            rowNumber: 1,
            cells: {
              A1: "=NA()",
              B1: "=B1",
              C1: "=A0",
              D1: "=(+",
              E1: "=5/0",
              F1: "=SQRT(-1)",
            },
            conditionalFormats: [],
          },
        ],
      })
    );

    const filled: number[][] = [];
    let current: number[] = [0, 0];

    const ctx = new MockGridRenderingContext(model, container, 1000, 1000, {
      onFunctionCall: (val, args) => {
        if (val === "moveTo") {
          current = [args[0], args[1]];
        } else if (val === "fill") {
          filled.push([current[0], current[1]]);
        }
      },
    });
    drawGridRenderer(ctx);
    const boxA1 = getBoxFromText(gridRendererStore, "#N/A"); //NotAvailableError => Shouldn't display
    expect(boxA1.isError).toBeFalsy();
    const boxB1 = getBoxFromText(gridRendererStore, "#CYCLE"); //CycleError => Should display
    expect(boxB1.isError).toBeTruthy();
    expect(filled[0][0]).toBe(boxB1.x + boxB1.width - 5);
    expect(filled[0][1]).toBe(boxB1.y);
    const boxC1 = getBoxFromText(gridRendererStore, "#REF"); //BadReferenceError => Should display
    expect(boxB1.isError).toBeTruthy();
    expect(filled[1][0]).toBe(boxC1.x + boxC1.width - 5);
    expect(filled[1][1]).toBe(boxC1.y);
    const boxD1 = getBoxFromText(gridRendererStore, "#BAD_EXPR"); //BadExpressionError => Should display
    expect(boxD1.isError).toBeTruthy();
    expect(filled[2][0]).toBe(boxD1.x + boxD1.width - 5);
    expect(filled[2][1]).toBe(boxD1.y);
    const boxE1 = getBoxFromText(gridRendererStore, "#DIV/0!"); // DivisionByZero => Should display
    expect(boxE1.isError).toBeTruthy();
    expect(filled[3][0]).toBe(boxE1.x + boxE1.width - 5);
    expect(filled[3][1]).toBe(boxE1.y);
    const boxF1 = getBoxFromText(gridRendererStore, "#ERROR"); // GeneralError => Should display
    expect(boxF1.isError).toBeTruthy();
    expect(filled[4][0]).toBe(boxF1.x + boxF1.width - 5);
    expect(filled[4][1]).toBe(boxF1.y);
  });

  test("Do not draw gridLines over colored cells in dashboard mode", () => {
    const CellFillColor = "#fe0000";
    const { drawGridRenderer, model, container } = setRenderer(
      new Model({
        sheets: [{ id: "Sheet1", name: "Sheet1", styles: { A1: 1, A2: 1 } }],
        styles: { 1: { fillColor: CellFillColor } },
      }),
      ["Selection", "Background"]
    );

    let strokeColors: string[];
    const ctx = new MockGridRenderingContext(model, container, 1000, 1000, {
      onFunctionCall: (val, _, renderingContext) => {
        if (val === "strokeRect") {
          strokeColors.push(toHex(renderingContext.ctx.strokeStyle as string));
        }
      },
    });

    // Default Model displaying grid lines
    strokeColors = [];
    drawGridRenderer(ctx);
    expect(strokeColors).toContain(toHex(SELECTION_BORDER_COLOR));
    expect(strokeColors).toContain(toHex(SELECTION_BORDER_COLOR));

    // dashboard mode
    model.updateMode("dashboard");
    strokeColors = [];
    drawGridRenderer(ctx);

    expect(strokeColors).toEqual([]);
  });

  test("Do not draw gridLines over colored cells while hiding grid lines", () => {
    const CellFillColor = "#fe0000";
    const { drawGridRenderer, model, container } = setRenderer(
      new Model({
        sheets: [{ id: "Sheet1", name: "Sheet1", styles: { A1: 1, A2: 2 } }],
        styles: { 1: { fillColor: CellFillColor } },
      }),
      ["Selection", "Background"]
    );

    let strokeColors: string[];
    const ctx = new MockGridRenderingContext(model, container, 1000, 1000, {
      onFunctionCall: (val, _, renderingContext) => {
        if (val === "strokeRect") {
          strokeColors.push(toHex(renderingContext.ctx.strokeStyle as string));
        }
      },
    });

    // Default Model displaying grid lines
    strokeColors = [];
    drawGridRenderer(ctx);
    expect(strokeColors).toContain(toHex(COLOR_THEMES.light.gridBorderColorOnDefaultBackground));
    expect(strokeColors).toContain(toHex(SELECTION_BORDER_COLOR));

    // model without grid lines
    setGridLinesVisibility(model, false);
    strokeColors = [];
    drawGridRenderer(ctx);

    expect(strokeColors).toEqual([
      toHex(SELECTION_BORDER_COLOR), // selection drawGrid
      toHex(SELECTION_BORDER_COLOR), // selection drawGrid
    ]);
  });

  test("draw text position depends on vertical align", () => {
    const { drawGridRenderer, model, container } = setRenderer(
      new Model({
        sheets: [
          {
            id: 1,
            colNumber: 1,
            rowNumber: 1,
            rows: { 0: { size: DEFAULT_CELL_HEIGHT * 2 } },
            cells: { A1: "kikou" },
          },
        ],
      })
    );

    const ctx = new MockGridRenderingContext(model, container, 1000, 1000, {
      onFunctionCall: (val, args) => {
        if (val === "fillText") {
          verticalStartPoints.push(args[2]); // args[2] corespond to "y"
        }
      },
    });

    // vertical top point
    let verticalStartPoints: any[] = [];
    setFormatting(model, "A1", { verticalAlign: "top" });
    drawGridRenderer(ctx);

    expect(verticalStartPoints[0]).toEqual(5);

    // vertical middle point
    verticalStartPoints = [];
    setFormatting(model, "A1", { verticalAlign: "middle" });
    drawGridRenderer(ctx);

    expect(verticalStartPoints[0]).toEqual(18);

    // vertical bottom point
    verticalStartPoints = [];
    setFormatting(model, "A1", { verticalAlign: "bottom" });
    drawGridRenderer(ctx);

    expect(verticalStartPoints[0]).toEqual(30);
  });

  test("keep the text vertically align to the top if not enough spaces to display it", () => {
    const { drawGridRenderer, model, container } = setRenderer(
      new Model({
        sheets: [
          {
            id: 1,
            colNumber: 1,
            rowNumber: 1,
            rows: { 0: { size: DEFAULT_CELL_HEIGHT } },
            cells: {
              A1: 'KIKOU: Interjection utilisée par les adolescents pour signifier "salut", "coucou", sur support électronique.',
            },
          },
        ],
      })
    );

    const ctx = new MockGridRenderingContext(model, container, 1000, 1000, {
      onFunctionCall: (val, args) => {
        if (val === "fillText") {
          verticalStartPoints.push(args[2]); // args[2] corespond to "y"
        }
      },
    });

    setFormatting(model, "A1", { wrapping: "wrap" });

    // with verticalAlign top
    let verticalStartPoints: any[] = [];
    setFormatting(model, "A1", { verticalAlign: "top" });
    drawGridRenderer(ctx);

    expect(verticalStartPoints[0]).toEqual(5);

    // with verticalAlign middle
    verticalStartPoints = [];
    setFormatting(model, "A1", { verticalAlign: "middle" });
    drawGridRenderer(ctx);

    expect(verticalStartPoints[0]).toEqual(5);

    // with verticalAlign bottom
    verticalStartPoints = [];
    setFormatting(model, "A1", { verticalAlign: "bottom" });
    drawGridRenderer(ctx);

    expect(verticalStartPoints[0]).toEqual(5);
  });

  describe("Overflowing cells background", () => {
    let model: Model;
    let clearRectInstructions: number[][];
    let fillRectInstructionsByColor: Record<string, number[][]>;
    let ctx: MockGridRenderingContext;
    let drawGridRenderer: (ctx: GridRenderingContext) => void;
    let gridRendererStore: GridRenderer;
    let container: DependencyContainer;

    function toDims(instruction: number[] | undefined) {
      if (!instruction) {
        return undefined;
      }
      return {
        x: instruction[0],
        y: instruction[1],
        width: instruction[2],
        height: instruction[3],
      };
    }

    function getCellOverflowingBackgroundDims() {
      // skip the first clearRect instruction, which is the whole-canvas clear done by drawGlobalBackground.
      return toDims(clearRectInstructions[1]);
    }

    beforeEach(() => {
      ({ drawGridRenderer, model, gridRendererStore, container } = setRenderer(
        new Model({ sheets: [{ colNumber: 10, rowNumber: 10 }] })
      ));
      clearRectInstructions = [];
      fillRectInstructionsByColor = {};
      let currentFillStyle: string | undefined = undefined;
      ctx = new MockGridRenderingContext(model, container, 1000, 1000, {
        onSet: (key, value) => {
          if (key !== "fillStyle") {
            return;
          }
          currentFillStyle = isColorValid(value) ? toHex(value) : undefined;
        },
        onFunctionCall: (key, args) => {
          if (key === "clearRect") {
            clearRectInstructions.push(args);
          } else if (key === "fillRect" && currentFillStyle) {
            (fillRectInstructionsByColor[currentFillStyle] ||= []).push(args);
          }
        },
      });
    });

    test("Non-overflowing cell have no overflowing background", () => {
      setCellContent(model, "A1", "Short text");
      drawGridRenderer(ctx);

      expect(getCellOverflowingBackgroundDims()).toBeUndefined();
    });

    test("Cell overflowing in x overflowing background", () => {
      const overflowingText = "Text longer than a column";
      setCellContent(model, "A1", overflowingText);
      resizeColumns(model, ["A"], 10);
      drawGridRenderer(ctx);

      const box = getBoxFromText(gridRendererStore, overflowingText);
      expect(getCellOverflowingBackgroundDims()).toMatchObject({
        x: box.x + ctx.thinLineWidth / 2,
        y: box.y + ctx.thinLineWidth / 2,
        width: box.content!.width - ctx.thinLineWidth * 2,
        height: box.height - ctx.thinLineWidth,
      });
    });

    test("Multi-line text overflowing in x overflowing background", () => {
      const longLine = "Text longer than a column";
      const longerLine = "Text longer than a column but even longer";

      setCellContent(model, "A1", longLine + NEWLINE + longerLine);
      resizeColumns(model, ["A"], 10);
      drawGridRenderer(ctx);

      const box = getBoxFromText(gridRendererStore, longLine + " " + longerLine);
      expect(getCellOverflowingBackgroundDims()).toMatchObject({
        x: box.x + ctx.thinLineWidth / 2,
        y: box.y + ctx.thinLineWidth / 2,
        width: longerLine.length + MIN_CELL_TEXT_MARGIN - ctx.thinLineWidth * 2,
        height: box.height - ctx.thinLineWidth,
      });
    });

    test("Cell overflowing in y overflowing background", () => {
      const overflowingText = "TOO HIGH";
      const fontSize = 26;
      setCellContent(model, "A1", overflowingText);
      setFormatting(model, "A1", { fontSize });
      resizeRows(model, [0], Math.floor(fontSizeInPixels(fontSize) / 2));
      drawGridRenderer(ctx);

      const box = getBoxFromText(gridRendererStore, overflowingText);
      expect(getCellOverflowingBackgroundDims()).toMatchObject({
        x: box.x + ctx.thinLineWidth / 2,
        y: box.y + ctx.thinLineWidth / 2,
        width: box.content!.width - ctx.thinLineWidth * 2,
        height: box.height - ctx.thinLineWidth,
      });
    });

    test("Overflowing background is filled with the sheet background, not cleared", () => {
      const overflowingText = "Text longer than a column";
      setSheetBackground(model, "#F2B2B7");
      setCellContent(model, "A1", overflowingText);
      resizeColumns(model, ["A"], 10);
      drawGridRenderer(ctx);

      // Clearing here would punch a transparent hole through the sheet background.
      expect(clearRectInstructions).toHaveLength(0);
      const box = getBoxFromText(gridRendererStore, overflowingText);
      // The first fill of that colour is the whole-canvas background, the second is the strip
      // repainted behind the overflowing text.
      expect(toDims(fillRectInstructionsByColor["#F2B2B7"]?.[1])).toMatchObject({
        x: box.x + ctx.thinLineWidth / 2,
        y: box.y + ctx.thinLineWidth / 2,
        width: box.content!.width - ctx.thinLineWidth * 2,
        height: box.height - ctx.thinLineWidth,
      });
    });
  });

  describe("Multi-line text rendering", () => {
    let model: Model;
    let ctx: MockGridRenderingContext;
    let renderedTexts: String[];
    let drawGridRenderer: (ctx: GridRenderingContext) => void;
    let gridRendererStore: GridRenderer;
    let container: DependencyContainer;

    beforeEach(() => {
      ({ drawGridRenderer, model, gridRendererStore, container } = setRenderer());
      renderedTexts = [];
      ctx = new MockGridRenderingContext(model, container, 1000, 1000, {
        onFunctionCall: (fn, args) => {
          if (fn === "fillText") {
            renderedTexts.push(args[0]);
          }
        },
      });
    });

    test("Wrapped text is displayed over multiple lines", () => {
      const overFlowingContent = "ThisIsAVeryVeryLongText";
      setCellContent(model, "A1", overFlowingContent);
      setFormatting(model, "A1", { wrapping: "wrap" });
      resizeColumns(model, ["A"], 14);

      // Split length = 14 - 2*MIN_CELL_TEXT_MARGIN = 6 letters (1 letter = 1px in the tests)
      const splittedText = ["ThisIs", "AVeryV", "eryLon", "gText"];

      drawGridRenderer(ctx);

      expect(renderedTexts.slice(0, 4)).toEqual(splittedText);
    });

    test.each<Align>(["left", "center", "right"])(
      "Wrapped text is displayed over multiple lines with %s align",
      (align) => {
        const overFlowingContent = "ThisIsAVeryVeryLongText";
        setCellContent(model, "A1", overFlowingContent);
        setFormatting(model, "A1", { wrapping: "wrap", align });
        resizeColumns(model, ["A"], 14);

        // Split length = 14 - 2*MIN_CELL_TEXT_MARGIN = 6 letters (1 letter = 1px in the tests)
        const splittedText = ["ThisIs", "AVeryV", "eryLon", "gText"];

        drawGridRenderer(ctx);

        expect(renderedTexts.slice(0, 4)).toEqual(splittedText);
      }
    );

    test("Wrapped text try to not split words in multiple lines if the word is small enough", () => {
      const overFlowingContent = "W Word2 W3 WordThatIsTooLong";
      setCellContent(model, "A1", overFlowingContent);
      setFormatting(model, "A1", { wrapping: "wrap" });
      resizeColumns(model, ["A"], 16);

      drawGridRenderer(ctx);

      expect(renderedTexts.slice(0, 5)).toEqual(["W Word2", "W3", "WordThat", "IsTooLon", "g"]);
    });

    test("Texts with newlines are displayed over multiple lines", () => {
      setCellContent(model, "A1", "Line1\nLine2\rLine3\r\nLine4");
      drawGridRenderer(ctx);

      expect(renderedTexts.slice(0, 4)).toEqual(["Line1", "Line2", "Line3", "Line4"]);
    });

    test("Box of Multi-line text have the width of the longest line", () => {
      const longLine = "Text longer than a column";
      const longerLine = "Text longer than a column but even longer";

      setCellContent(model, "A1", longLine + NEWLINE + longerLine);
      resizeColumns(model, ["A"], 10);
      drawGridRenderer(ctx);

      const box = getBoxFromText(gridRendererStore, longLine + " " + longerLine);
      expect(box.isOverflow).toBeTruthy();
      expect(box.content?.width).toEqual(longerLine.length + MIN_CELL_TEXT_MARGIN);
    });
  });

  test("Can render borders with different colors on the same cell", () => {
    const { drawGridRenderer, model, container } = setRenderer();
    const colors = {
      left: "#FF0000",
      right: "#888800",
      top: "#00FF00",
      bottom: "#008888",
    };
    for (const [position, color] of Object.entries(colors)) {
      setZoneBorders(
        model,
        {
          position: position as BorderPosition,
          color,
          style: "thin",
        },
        ["A1"]
      );
    }

    const renderedBorders = {};
    let currentColor = "";
    const ctx = new MockGridRenderingContext(model, container, 1000, 1000, {
      onSet: (key, value) => {
        if (key === "strokeStyle") {
          if (Object.values(colors).includes(value)) {
            currentColor = value;
          }
        }
      },
      onFunctionCall: (val, args) => {
        if (currentColor !== "" && val === "moveTo") {
          renderedBorders[currentColor] = { start: args };
        } else if (currentColor !== "" && val === "lineTo") {
          renderedBorders[currentColor].end = args;
          currentColor = "";
        }
      },
    });
    drawGridRenderer(ctx);

    expect(renderedBorders).toEqual({
      [colors.left]: {
        start: [0, 0],
        end: [0, DEFAULT_CELL_HEIGHT],
      },
      [colors.top]: {
        start: [0, 0],
        end: [DEFAULT_CELL_WIDTH, 0],
      },
      [colors.right]: {
        start: [DEFAULT_CELL_WIDTH, 0],
        end: [DEFAULT_CELL_WIDTH, DEFAULT_CELL_HEIGHT],
      },
      [colors.bottom]: {
        start: [0, DEFAULT_CELL_HEIGHT],
        end: [DEFAULT_CELL_WIDTH, DEFAULT_CELL_HEIGHT],
      },
    });
  });

  test("Thin border is correctly rendered", () => {
    const { drawGridRenderer, model, container } = setRenderer();
    setZoneBorders(
      model,
      {
        position: "left",
        style: "thin",
      },
      ["A1"]
    );

    let lineDash: any[] = [];
    let lineWidth = 1;
    const borderRenderingContext: [number, any[]][] = [];
    let isDrawing = false;
    const ctx = new MockGridRenderingContext(model, container, 1000, 1000, {
      onSet: (key, value) => {
        if (key === "lineWidth") {
          lineWidth = value;
        }
      },
      onFunctionCall: (val, args) => {
        if (val === "setLineDash") {
          lineDash = args;
        } else if (val === "moveTo" && args[0] === 0 && args[1] === 0) {
          isDrawing = true;
        } else if (
          val === "lineTo" &&
          args[0] === 0 &&
          args[1] === DEFAULT_CELL_HEIGHT &&
          isDrawing
        ) {
          borderRenderingContext.push([lineWidth, lineDash]);
          isDrawing = false;
        }
      },
    });
    drawGridRenderer(ctx);

    expect(borderRenderingContext).toEqual([[1, []]]);
  });

  test("Medium border is correctly rendered", () => {
    const { drawGridRenderer, model, container } = setRenderer();
    setZoneBorders(
      model,
      {
        position: "left",
        style: "medium",
        color: "#FF0000",
      },
      ["A1"]
    );

    let lineDash: any[] = [];
    let lineWidth = 1;
    const borderRenderingContext: [number, any[]][] = [];
    let isDrawing = false;
    const ctx = new MockGridRenderingContext(model, container, 1000, 1000, {
      onSet: (key, value) => {
        if (key === "lineWidth") {
          lineWidth = value;
        }
      },
      onFunctionCall: (val, args) => {
        if (val === "setLineDash") {
          lineDash = args;
        } else if (val === "moveTo" && args[0] === 0.5 && args[1] === -0.5) {
          isDrawing = true;
        } else if (
          val === "lineTo" &&
          args[0] === 0.5 &&
          args[1] === DEFAULT_CELL_HEIGHT + 1.5 &&
          isDrawing
        ) {
          borderRenderingContext.push([lineWidth, lineDash]);
          isDrawing = false;
        }
      },
    });
    drawGridRenderer(ctx);

    expect(borderRenderingContext).toEqual([[2, []]]);
  });

  test("Thick border is correctly rendered", () => {
    const { drawGridRenderer, model, container } = setRenderer();
    setZoneBorders(
      model,
      {
        position: "left",
        style: "thick",
      },
      ["A1"]
    );

    let lineDash: any[] = [];
    let lineWidth = 1;
    const borderRenderingContext: [number, any[]][] = [];
    let isDrawing = false;
    const ctx = new MockGridRenderingContext(model, container, 1000, 1000, {
      onSet: (key, value) => {
        if (key === "lineWidth") {
          lineWidth = value;
        }
      },
      onFunctionCall: (val, args) => {
        if (val === "setLineDash") {
          lineDash = args;
        } else if (val === "moveTo" && args[0] === 0 && args[1] === -1) {
          isDrawing = true;
        } else if (
          val === "lineTo" &&
          args[0] === 0 &&
          args[1] === DEFAULT_CELL_HEIGHT + 1 &&
          isDrawing
        ) {
          borderRenderingContext.push([lineWidth, lineDash]);
          isDrawing = false;
        }
      },
    });
    drawGridRenderer(ctx);

    expect(borderRenderingContext).toEqual([[3, []]]);
  });

  test("Dashed border is correctly rendered", () => {
    const { drawGridRenderer, model, container } = setRenderer();
    setZoneBorders(
      model,
      {
        position: "left",
        style: "dashed",
      },
      ["A1"]
    );

    let lineDash: any[] = [];
    let lineWidth = 1;
    const borderRenderingContext: [number, any[]][] = [];
    let isDrawing = false;
    const ctx = new MockGridRenderingContext(model, container, 1000, 1000, {
      onSet: (key, value) => {
        if (key === "lineWidth") {
          lineWidth = value;
        }
      },
      onFunctionCall: (val, args) => {
        if (val === "setLineDash") {
          lineDash = args;
        } else if (val === "moveTo" && args[0] === 0 && args[1] === 0) {
          isDrawing = true;
        } else if (
          val === "lineTo" &&
          args[0] === 0 &&
          args[1] === DEFAULT_CELL_HEIGHT &&
          isDrawing
        ) {
          borderRenderingContext.push([lineWidth, lineDash]);
          isDrawing = false;
        }
      },
    });
    drawGridRenderer(ctx);

    expect(borderRenderingContext).toEqual([[1, [[1, 3]]]]);
  });

  test("Dotted border is correctly rendered", () => {
    const { drawGridRenderer, model, container } = setRenderer();
    setZoneBorders(
      model,
      {
        position: "left",
        style: "dotted",
      },
      ["A1"]
    );

    let lineDash: any[] = [];
    let lineWidth = 1;
    const borderRenderingContext: [number, any[]][] = [];
    let isDrawing = false;
    const ctx = new MockGridRenderingContext(model, container, 1000, 1000, {
      onSet: (key, value) => {
        if (key === "lineWidth") {
          lineWidth = value;
        }
      },
      onFunctionCall: (val, args) => {
        if (val === "setLineDash") {
          lineDash = args;
        } else if (val === "moveTo" && args[0] === 0 && args[1] === 0.5) {
          isDrawing = true;
        } else if (
          val === "lineTo" &&
          args[0] === 0 &&
          args[1] === DEFAULT_CELL_HEIGHT + 0.5 &&
          isDrawing
        ) {
          borderRenderingContext.push([lineWidth, lineDash]);
          isDrawing = false;
        }
      },
    });
    drawGridRenderer(ctx);

    expect(borderRenderingContext).toEqual([[1, [[1, 1]]]]);
  });

  test("Cells of splilled formula are empty is we display the formulas", () => {
    const model = new Model({ sheets: [{ colNumber: 2, rowNumber: 2 }] });
    setFormulaVisibility(model, true);
    setCellContent(model, "A1", "=MUNIT(2)");
    const { drawGridRenderer, gridRendererStore, container } = setRenderer(model);
    const ctx = new MockGridRenderingContext(model, container, 1000, 1000, {});
    drawGridRenderer(ctx);
    const boxes = gridRendererStore["getGridBoxes"](ctx.viewports, ctx.sheetId, toZone("A1:B2"));
    const boxesText = boxes.map((box) => box.content?.textLines.join(""));
    expect(boxesText).toEqual(["=MUNIT(2)", "", "", ""]);
  });

  describe("boolean DataValidations are correctly rendered", () => {
    let renderedTexts: string[];
    let ctx: MockGridRenderingContext;
    let model: Model;
    let drawGridRenderer: (ctx: GridRenderingContext) => void;
    let container: DependencyContainer;

    beforeEach(() => {
      ({ drawGridRenderer, model, container } = setRenderer());
      renderedTexts = [];
      ctx = new MockGridRenderingContext(model, container, 1000, 1000, {
        onFunctionCall: (fn, args) => {
          if (fn === "fillText") {
            renderedTexts.push(args[0]);
          }
        },
      });
    });

    test("Valid checkbox value is not rendered", () => {
      addDataValidation(model, "B2", "id", { type: "isBoolean", values: [] });
      setCellContent(model, "B2", "TRUE");
      drawGridRenderer(ctx);
      expect(renderedTexts).not.toContain("TRUE");
    });

    test("Invalid checkbox value is rendered", () => {
      addDataValidation(model, "B2", "id", { type: "isBoolean", values: [] });
      setCellContent(model, "B2", "hello");
      drawGridRenderer(ctx);
      expect(renderedTexts).toContain("hello");
    });
  });

  describe("chip DataValidations are correctly rendered", () => {
    test("chip is rendered", () => {
      const { drawGridRenderer, model, container } = setRenderer();
      const roundRectArgs: any[] = [];
      let fillStyle: string = "";
      const criterion: DataValidationCriterion = {
        type: "isValueInList",
        values: ["hello"],
        colors: { hello: "#123456" },
        displayStyle: "chip",
      };
      addDataValidation(model, "A1", "id", criterion);
      const ctx = new MockGridRenderingContext(model, container, 1000, 1000, {
        onFunctionCall: (key, args) => {
          if (key === "roundRect") {
            roundRectArgs.push(args);
            expect(fillStyle).toBe("#123456");
          }
        },
        onSet: (key, value) => {
          if (key === "fillStyle") {
            fillStyle = value;
          }
        },
      });
      setCellContent(model, "A1", "hello");
      drawGridRenderer(ctx);
      expect(roundRectArgs).toHaveLength(1);
      expect(roundRectArgs[0]).toEqual([5, 5, 86, 15, 10]);
    });

    test("chip boxes are colored", () => {
      const { drawGridRenderer, model, gridRendererStore, container } = setRenderer();
      const criterion: DataValidationCriterion = {
        type: "isValueInList",
        values: ["hello"],
        displayStyle: "chip",
      };
      addDataValidation(model, "A1", "id", criterion);
      const ctx = new MockGridRenderingContext(model, container, 1000, 1000, {});
      drawGridRenderer(ctx);
      let [box] = gridRendererStore["getGridBoxes"](ctx.viewports, ctx.sheetId, toZone("A1"));
      expect(box.chip).toBeUndefined();
      setCellContent(model, "A1", "hello");
      drawGridRenderer(ctx);
      [box] = gridRendererStore["getGridBoxes"](ctx.viewports, ctx.sheetId, toZone("A1"));
      expect(box.style.textColor).toBeUndefined();
      expect(box.chip).toEqual({
        color: "#E7E9ED", // default color
        height: 15,
        width: 86,
        x: 5,
        y: 5,
      });
      addDataValidation(model, "A1", "id", {
        ...criterion,
        colors: { hello: "#FF0000" },
      });
      drawGridRenderer(ctx);
      [box] = gridRendererStore["getGridBoxes"](ctx.viewports, ctx.sheetId, toZone("A1"));
      expect(box.style.textColor).toBe("#FFE5E5");
      expect(box.chip).toEqual({
        color: "#FF0000",
        height: 15,
        width: 86,
        x: 5,
        y: 5,
      });
    });

    test("chip is rendered next to CF icon", () => {
      const { drawGridRenderer, model, gridRendererStore, container } = setRenderer();
      const criterion: DataValidationCriterion = {
        type: "isValueInList",
        values: ["1"],
        displayStyle: "chip",
      };
      addIconCF(model, "A1:A5", ["7", "7"], "arrows");
      addDataValidation(model, "A1", "id", criterion);
      setCellContent(model, "A1", "1");
      const ctx = new MockGridRenderingContext(model, container, 1000, 1000, {});
      drawGridRenderer(ctx);
      const [box] = gridRendererStore["getGridBoxes"](ctx.viewports, ctx.sheetId, toZone("A1"));
      expect(box.chip).toEqual({
        color: "#E7E9ED",
        height: 15,
        width: 69,
        x: 22,
        y: 5,
      });
    });
  });

  test("Repeated character in format is repeated to fill the column", () => {
    const { drawGridRenderer, model, gridRendererStore, container } = setRenderer();
    setCellContent(model, "A1", "1");
    setFormat(model, "A1", "* 0");
    resizeColumns(model, ["A"], 20);

    const ctx = new MockGridRenderingContext(model, container, 1000, 1000, {});
    drawGridRenderer(ctx);

    let box = gridRendererStore["getGridBoxes"](ctx.viewports, ctx.sheetId, toZone("A1")).filter(
      (box) => box.content
    )[0];
    const expectedSpaces = 20 - 2 * MIN_CELL_TEXT_MARGIN;
    expect(box.content?.textLines).toEqual(["1".padStart(expectedSpaces)]);

    setFormat(model, "A1", "0*c");
    drawGridRenderer(ctx);
    box = gridRendererStore["getGridBoxes"](ctx.viewports, ctx.sheetId, toZone("A1")).filter(
      (box) => box.content
    )[0];
    expect(box.content?.textLines).toEqual(["1".padEnd(expectedSpaces, "c")]);
  });

  test("Cells with repeated character format are aligned to the left", () => {
    const { drawGridRenderer, model, container } = setRenderer();
    setCellContent(model, "A1", "1");

    let textAligns: string[] = [];
    const ctx = new MockGridRenderingContext(model, container, 1000, 1000, {
      onSet: (key, value) => (key === "textAlign" ? textAligns.push(value) : null),
    });

    drawGridRenderer(ctx);
    expect(textAligns).toEqual(["right"]);

    textAligns = [];
    setCellFormat(model, "A1", "dd* ");
    drawGridRenderer(ctx);
    expect(textAligns).toEqual(["left"]);
  });

  test("Each frozen pane is clipped in the grid", () => {
    const model = new Model({ sheets: [{ colNumber: 7, rowNumber: 7 }] });
    // Don't account for headers for the grid, only draw the cells & frozen panes
    const { drawGridRenderer, container } = setRenderer(model, ["Background"]);
    setCellContent(model, "A1", "1");
    freezeColumns(model, 2);
    freezeRows(model, 1);
    const spyFn = jest.fn();
    const ctx = new MockGridRenderingContext(model, container, 1000, 1000, {
      onFunctionCall: (key, args) => {
        if (["rect", "clip"].includes(key)) {
          spyFn(key, args);
        }
      },
    });
    drawGridRenderer(ctx);
    expect(spyFn).toHaveBeenCalledTimes(8);
    expect(spyFn).toHaveBeenNthCalledWith(1, "rect", [
      0,
      0,
      DEFAULT_CELL_WIDTH * 2,
      DEFAULT_CELL_HEIGHT,
    ]);
    expect(spyFn).toHaveBeenNthCalledWith(2, "clip", []);
    expect(spyFn).toHaveBeenNthCalledWith(3, "rect", [
      DEFAULT_CELL_WIDTH * 2,
      0,
      1000 - DEFAULT_CELL_WIDTH * 2,
      DEFAULT_CELL_HEIGHT,
    ]);
    expect(spyFn).toHaveBeenNthCalledWith(4, "clip", []);
    expect(spyFn).toHaveBeenNthCalledWith(5, "rect", [
      0,
      DEFAULT_CELL_HEIGHT,
      DEFAULT_CELL_WIDTH * 2,
      1000 - DEFAULT_CELL_HEIGHT,
    ]);
    expect(spyFn).toHaveBeenNthCalledWith(6, "clip", []);
    expect(spyFn).toHaveBeenNthCalledWith(7, "rect", [
      DEFAULT_CELL_WIDTH * 2,
      DEFAULT_CELL_HEIGHT,
      1000 - DEFAULT_CELL_WIDTH * 2,
      1000 - DEFAULT_CELL_HEIGHT,
    ]);
    expect(spyFn).toHaveBeenNthCalledWith(8, "clip", []);
  });

  test("frozen pane border is drawn by default", () => {
    const model = new Model({ sheets: [{ colNumber: 7, rowNumber: 7 }] });
    const { drawGridRenderer, container } = setRenderer(model, ["Background"]);
    freezeColumns(model, 2);
    freezeRows(model, 1);
    const ctx = new MockGridRenderingContext(model, container, 300, 150, {}, "nodeCanvas");

    drawGridRenderer(ctx);
    expect(ctx.screenshot()).toMatchImageSnapshot();
  });

  test("frozen pane border is not drawn when hideFrozenPaneBorder is set", () => {
    const model = new Model({ sheets: [{ colNumber: 7, rowNumber: 7 }] });
    const { drawGridRenderer, container } = setRenderer(model, ["Background"]);
    freezeColumns(model, 2);
    freezeRows(model, 1);
    const ctx = new MockGridRenderingContext(model, container, 300, 150, {}, "nodeCanvas");
    (ctx as GridRenderingContext).hideFrozenPaneBorder = true;

    drawGridRenderer(ctx);
    expect(ctx.screenshot()).toMatchImageSnapshot();
  });

  test("Applying style hideGridLines on a cell skips the drawing of the grid lines for this cell", () => {
    const model = new Model();
    const { drawGridRenderer, container } = setRenderer(model);

    let strokeRectCalls: string[];
    const ctx = new MockGridRenderingContext(model, container, 1000, 1000, {
      onFunctionCall: (key, args) => {
        if (key === "strokeRect") {
          strokeRectCalls.push(`context.${key}(${args.map((a) => JSON.stringify(a)).join(", ")})`);
        }
      },
    });

    strokeRectCalls = [];
    drawGridRenderer(ctx);

    const baseNumberOfStrokeRect = strokeRectCalls.length;

    setFormatting(model, "A1:B2", { hideGridLines: true });
    strokeRectCalls = [];
    drawGridRenderer(ctx);

    expect(strokeRectCalls.length).toBe(baseNumberOfStrokeRect - 4);
  });

  test("can draw a sheet with a custom background color", () => {
    const { drawGridRenderer, model, container } = setRenderer(
      new Model({ sheets: [{ colNumber: 4, rowNumber: 8 }] }),
      ["Background", "Headers", "Selection"]
    );

    setCellContent(model, "A1", "1");
    setSheetBackground(model, "#F2B2B7", "Sheet1");
    const ctx = new MockGridRenderingContext(model, container, 500, 300, {}, "nodeCanvas");
    const viewStore = container.get(ViewportsStore);
    viewStore.resizeSheetView({
      width: 500 - HEADER_HEIGHT,
      height: 300 - HEADER_WIDTH,
      gridOffsetX: HEADER_WIDTH,
      gridOffsetY: HEADER_HEIGHT,
    });

    drawGridRenderer(ctx);
    expect(ctx.screenshot()).toMatchImageSnapshot();
  });

  test("data bar fills the whole cell height in normal mode", () => {
    const model = createModelFromGrid({ A1: "5", A2: "10" });
    const { drawGridRenderer, container } = setRenderer(model, ["Background"]);
    addDataBarCF(model, "A1:A2", "#3498db");
    const ctx = new MockGridRenderingContext(model, container, 100, 50, {}, "nodeCanvas");

    drawGridRenderer(ctx);
    expect(ctx.screenshot()).toMatchImageSnapshot();
  });

  test("data bar is vertically inset in dashboard mode", () => {
    const model = createModelFromGrid({ A1: "5", A2: "10" });
    const { drawGridRenderer, container } = setRenderer(model, ["Background"]);
    addDataBarCF(model, "A1:A2", "#3498db");
    model.updateMode("dashboard");
    const ctx = new MockGridRenderingContext(model, container, 100, 50, {}, "nodeCanvas");

    drawGridRenderer(ctx);
    expect(ctx.screenshot()).toMatchImageSnapshot();
  });
});
