import { App, xml } from "@odoo/owl";
import { Model, UID } from "../../src";
import { useDragAndDropBeyondTheViewport } from "../../src/components/helpers/drag_and_drop_grid_hook";
import { DEFAULT_CELL_HEIGHT, DEFAULT_CELL_WIDTH } from "../../src/constants";
import { numberToLetters } from "../../src/helpers/coordinates";
import { Component } from "../../src/owl3_compatibility_layer";
import { ViewportsStore } from "../../src/stores/viewports_store";
import { SpreadsheetChildEnv } from "../../src/types/spreadsheet_env";
import { Store } from "../../src/types/store_engine";
import {
  addColumns,
  addRows,
  createSheet,
  freezeColumns,
  freezeRows,
  hideColumns,
  hideRows,
  setViewportOffset,
} from "../test_helpers/commands_helpers";
import {
  edgeScrollDelay,
  triggerKeyboardEvent,
  triggerMouseEvent,
} from "../test_helpers/dom_helper";
import { mountComponent, nextTick, useJestFakeTimers } from "../test_helpers/helpers";

// As we test an isolated component, grid and gridOverlay won't exist
jest.mock("../../src/components/helpers/dom_helpers", () => {
  return {
    ...jest.requireActual("../../src/components/helpers/dom_helpers"),
    ...jest.requireActual("../__mocks__/dom_helpers"),
  };
});

let model: Model;
let sheetId: UID;
let app: App;
let env: SpreadsheetChildEnv;
let viewStore: Store<ViewportsStore>;

//Test Component required
const TEMPLATE = xml/* xml */ `
  <div class="o-fake-grid" t-on-pointerdown="this.onMouseDown">
    coucou
  </div>
`;

let selectedCol: number | undefined = undefined;
let selectedRow: number | undefined = undefined;

const mouseUpFn = jest.fn();

class FakeGridComponent extends Component<SpreadsheetChildEnv> {
  static template = TEMPLATE;

  dragNDropGrid = useDragAndDropBeyondTheViewport(this.env);

  onMouseDown(ev: PointerEvent) {
    this.dragNDropGrid.start(
      ev,
      (col, row) => {
        selectedCol = col;
        selectedRow = row;
      },
      () => {
        mouseUpFn();
      }
    );
  }
}
beforeAll(() => {
  useJestFakeTimers();
});

afterAll(() => {
  useJestFakeTimers();
});

beforeEach(async () => {
  ({ model, app, env, viewStore } = await mountComponent(FakeGridComponent));
  selectedCol = selectedRow = undefined;
  sheetId = model.getters.getActiveSheetId();
  await nextTick();
});

afterEach(() => {
  mouseUpFn.mockClear();
});

describe("Drag And Drop horizontal tests", () => {
  test("Start Drag&Drop in XRight then moving to XLeft edge-scroll XRight to the left", async () => {
    freezeColumns(model, 4, sheetId);
    setViewportOffset(env, 6 * DEFAULT_CELL_WIDTH, 6 * DEFAULT_CELL_HEIGHT);
    await nextTick();
    expect(viewStore.activeMainViewport).toMatchObject({
      left: 10,
      right: 16,
    });
    const { height } = viewStore.sheetViewDimension;
    const { x: offsetCorrectionX } = viewStore.mainViewportCoordinates;
    const x = offsetCorrectionX + DEFAULT_CELL_WIDTH;
    triggerMouseEvent(".o-fake-grid", "pointerdown", x, 0.5 * height);
    triggerMouseEvent(".o-fake-grid", "pointermove", 0.5 * offsetCorrectionX, 0.5 * height);
    const advanceTimer = edgeScrollDelay(0.5 * offsetCorrectionX, 1);

    jest.advanceTimersByTime(advanceTimer);
    triggerMouseEvent(".o-fake-grid", "pointerup", 0.5 * offsetCorrectionX, 0.5 * height);
    expect(viewStore.activeMainViewport).toMatchObject({
      left: 8,
      right: 14,
    });
  });

  test("Start Drag&Drop in XLeft then moving to XRight unscroll XRight", async () => {
    freezeColumns(model, 4, sheetId);
    setViewportOffset(env, 6 * DEFAULT_CELL_WIDTH, 6 * DEFAULT_CELL_HEIGHT);
    await nextTick();
    expect(viewStore.activeMainViewport).toMatchObject({
      left: 10,
      right: 16,
    });
    const { height } = viewStore.sheetViewDimension;
    const { x: offsetCorrectionX } = viewStore.mainViewportCoordinates;
    const x = offsetCorrectionX + DEFAULT_CELL_WIDTH;
    triggerMouseEvent(".o-fake-grid", "pointerdown", 0.5 * offsetCorrectionX, 0.5 * height);
    triggerMouseEvent(".o-fake-grid", "pointermove", x, 0.5 * height);
    triggerMouseEvent(".o-fake-grid", "pointerup", 0.5 * offsetCorrectionX, 0.5 * height);
    expect(viewStore.activeMainViewport).toMatchObject({
      left: 4,
      right: 10,
    });
    expect(mouseUpFn).toHaveBeenCalled();
  });

  test("Start Drag&Drop in XRight then moving it outside right scroll XRight to the right", async () => {
    freezeColumns(model, 4, sheetId);
    await nextTick();
    expect(viewStore.activeMainViewport).toMatchObject({
      left: 4,
      right: 10,
    });
    const { width, height } = viewStore.sheetViewDimension;
    triggerMouseEvent(".o-fake-grid", "pointerdown", width, 0.5 * height);
    triggerMouseEvent(".o-fake-grid", "pointermove", 1.5 * width, 0.5 * height);
    const advanceTimer = edgeScrollDelay(0.5 * width, 4);

    jest.advanceTimersByTime(advanceTimer);
    triggerMouseEvent(".o-fake-grid", "pointerup", 1.5 * width, 0.5 * height);
    expect(viewStore.activeMainViewport).toMatchObject({
      left: 9,
      right: 15,
    });
  });

  test("Start Drag&Drop in XLeft then moving it outside right scroll XRight to the right", async () => {
    freezeColumns(model, 4, sheetId);
    await nextTick();
    expect(viewStore.activeMainViewport).toMatchObject({
      left: 4,
      right: 10,
    });
    const { width, height } = viewStore.sheetViewDimension;
    const { x: offsetCorrectionX } = viewStore.mainViewportCoordinates;
    triggerMouseEvent(".o-fake-grid", "pointerdown", 0.5 * offsetCorrectionX, 0.5 * height);
    triggerMouseEvent(".o-fake-grid", "pointermove", 1.5 * width, 0.5 * height);
    const advanceTimer = edgeScrollDelay(0.5 * width, 4);

    jest.advanceTimersByTime(advanceTimer);
    triggerMouseEvent(".o-fake-grid", "pointerup", 1.5 * width, 0.5 * height);
    expect(viewStore.activeMainViewport).toMatchObject({
      left: 9,
      right: 15,
    });
  });

  test("drag And Drop through hidden columns", async () => {
    const { right } = viewStore.activeMainViewport;
    addColumns(model, "after", "Z", 100);
    hideColumns(model, [numberToLetters(right + 1)]);
    const { width, height } = viewStore.sheetViewDimension;
    const y = height / 2;
    triggerMouseEvent(".o-fake-grid", "pointerdown", width / 2, y);
    triggerMouseEvent(".o-fake-grid", "pointermove", 1.5 * width, y);
    const advanceTimer = edgeScrollDelay(0.5 * width, 5);

    jest.advanceTimersByTime(advanceTimer);
    triggerMouseEvent(".o-fake-grid", "pointerup", 1.5 * width, y);
    expect(viewStore.activeMainViewport).toMatchObject({
      left: 6,
      right: 17,
    });
    setViewportOffset(env, (right + 2) * DEFAULT_CELL_WIDTH, 0);
    await nextTick();
    triggerMouseEvent(".o-fake-grid", "pointerdown", width / 2, y);
    triggerMouseEvent(".o-fake-grid", "pointermove", -1.5 * width, y);

    jest.advanceTimersByTime(advanceTimer);
    triggerMouseEvent(".o-fake-grid", "pointerup", -1.5 * width, y);
    expect(viewStore.activeMainViewport).toMatchObject({
      left: 6,
      right: 17,
    });
  });

  test("Drag&Drop beyond the grid: correct col value is given to the callback", async () => {
    freezeColumns(model, 4, sheetId);
    setViewportOffset(env, 6 * DEFAULT_CELL_WIDTH, 6 * DEFAULT_CELL_HEIGHT);
    await nextTick();
    const { x: offsetCorrectionX } = viewStore.mainViewportCoordinates;
    const x = offsetCorrectionX + DEFAULT_CELL_WIDTH;
    triggerMouseEvent(".o-fake-grid", "pointerdown", x, 0);
    triggerMouseEvent(".o-fake-grid", "pointermove", -50, 0);
    const advanceTimer = edgeScrollDelay(offsetCorrectionX + 50, 6);

    jest.advanceTimersByTime(advanceTimer);
    triggerMouseEvent(".o-fake-grid", "pointerup", -50, 0);
    expect(viewStore.activeMainViewport).toMatchObject({
      left: 4,
      right: 10,
    });
    expect(selectedCol).toEqual(0);
  });
});

describe("Drag And Drop vertical tests", () => {
  test("Start Drag&Drop in XBottom then moving to XTop edge-scroll XBottom way-up", async () => {
    freezeRows(model, 4, sheetId);
    setViewportOffset(env, 6 * DEFAULT_CELL_WIDTH, 6 * DEFAULT_CELL_HEIGHT);
    await nextTick();
    expect(viewStore.activeMainViewport).toMatchObject({
      top: 10,
      bottom: 49,
    });
    const { width } = viewStore.sheetViewDimension;
    const { y: offsetCorrectionY } = viewStore.mainViewportCoordinates;
    const y = offsetCorrectionY + DEFAULT_CELL_HEIGHT;
    triggerMouseEvent(".o-fake-grid", "pointerdown", 0.5 * width, y);
    triggerMouseEvent(".o-fake-grid", "pointermove", 0.5 * width, 0.5 * offsetCorrectionY);
    const advanceTimer = edgeScrollDelay(0.5 * offsetCorrectionY, 1);

    jest.advanceTimersByTime(advanceTimer);
    triggerMouseEvent(".o-fake-grid", "pointerup", 0.5 * width, 0.5 * offsetCorrectionY);
    expect(viewStore.activeMainViewport).toMatchObject({
      top: 8,
      bottom: 47,
    });
  });

  test("Start Drag&Drop in XTop then moving to XBottom unscroll XBottom", async () => {
    freezeRows(model, 4, sheetId);
    setViewportOffset(env, 6 * DEFAULT_CELL_WIDTH, 6 * DEFAULT_CELL_HEIGHT);
    await nextTick();
    expect(viewStore.activeMainViewport).toMatchObject({
      top: 10,
      bottom: 49,
    });
    const { width } = viewStore.sheetViewDimension;
    const { y: offsetCorrectionY } = viewStore.mainViewportCoordinates;
    const y = offsetCorrectionY + DEFAULT_CELL_HEIGHT;
    triggerMouseEvent(".o-fake-grid", "pointerdown", 0.5 * width, 0.5 * offsetCorrectionY);
    triggerMouseEvent(".o-fake-grid", "pointermove", 0.5 * width, y);

    triggerMouseEvent(".o-fake-grid", "pointerup", 0.5 * width, 0.5 * offsetCorrectionY);
    expect(viewStore.activeMainViewport).toMatchObject({
      top: 4,
      bottom: 43,
    });
  });

  test("Start Drag&Drop in XBottom then moving it under the viewport scroll XBottom upside-down", async () => {
    freezeRows(model, 4, sheetId);
    await nextTick();
    expect(viewStore.activeMainViewport).toMatchObject({
      top: 4,
      bottom: 43,
    });
    const { width, height } = viewStore.sheetViewDimension;
    triggerMouseEvent(".o-fake-grid", "pointerdown", 0.5 * width, height);
    triggerMouseEvent(".o-fake-grid", "pointermove", 0.5 * width, 1.5 * height);
    const advanceTimer = edgeScrollDelay(0.5 * height, 4);

    jest.advanceTimersByTime(advanceTimer);
    triggerMouseEvent(".o-fake-grid", "pointerup", 0.5 * width, 1.5 * height);
    expect(viewStore.activeMainViewport).toMatchObject({
      top: 9,
      bottom: 48,
    });
  });

  test("Start Drag&Drop in XTop then moving it under the viewport scroll XBottom upside-down", async () => {
    freezeRows(model, 4, sheetId);
    await nextTick();
    expect(viewStore.activeMainViewport).toMatchObject({
      top: 4,
      bottom: 43,
    });
    const { width, height } = viewStore.sheetViewDimension;
    const { y: offsetCorrectionY } = viewStore.mainViewportCoordinates;
    triggerMouseEvent(".o-fake-grid", "pointerdown", 0.5 * width, 0.5 * offsetCorrectionY);
    triggerMouseEvent(".o-fake-grid", "pointermove", 0.5 * width, 1.5 * height);
    const advanceTimer = edgeScrollDelay(0.5 * height, 4);

    jest.advanceTimersByTime(advanceTimer);
    triggerMouseEvent(".o-fake-grid", "pointerup", 0.5 * width, 1.5 * height);
    expect(viewStore.activeMainViewport).toMatchObject({
      top: 9,
      bottom: 48,
    });
  });

  test("Drag&Drop beyond the grid: correct row value is given to the callback", async () => {
    freezeRows(model, 4, sheetId);
    setViewportOffset(env, 6 * DEFAULT_CELL_WIDTH, 6 * DEFAULT_CELL_HEIGHT);
    await nextTick();
    const { y: offsetCorrectionY } = viewStore.mainViewportCoordinates;
    const y = offsetCorrectionY + DEFAULT_CELL_HEIGHT;
    triggerMouseEvent(".o-fake-grid", "pointerdown", 0, y);
    triggerMouseEvent(".o-fake-grid", "pointermove", 0, -50);
    const advanceTimer = edgeScrollDelay(offsetCorrectionY + 50, 6);

    jest.advanceTimersByTime(advanceTimer);
    triggerMouseEvent(".o-fake-grid", "pointerup", 0, -50);
    expect(viewStore.activeMainViewport).toMatchObject({
      top: 4,
      bottom: 43,
    });
    expect(selectedRow).toEqual(0);
  });
});

describe("Drag And Drop vertical tests without frozen panes", () => {
  test("Start Drag&Drop in viewport then moving outside-left edge-scroll the viewport to the left", () => {
    setViewportOffset(env, 6 * DEFAULT_CELL_WIDTH, 6 * DEFAULT_CELL_HEIGHT);
    expect(viewStore.activeMainViewport).toMatchObject({
      left: 6,
      right: 16,
    });
    const { height } = viewStore.sheetViewDimension;
    triggerMouseEvent(".o-fake-grid", "pointerdown", DEFAULT_CELL_WIDTH, 0.5 * height);
    triggerMouseEvent(".o-fake-grid", "pointermove", -0.5 * DEFAULT_CELL_WIDTH, 0.5 * height);
    const advanceTimer = edgeScrollDelay(0.5 * DEFAULT_CELL_WIDTH, 1);

    jest.advanceTimersByTime(advanceTimer);
    triggerMouseEvent(".o-fake-grid", "pointerup", -0.5 * DEFAULT_CELL_WIDTH, 0.5 * height);
    expect(viewStore.activeMainViewport).toMatchObject({
      left: 4,
      right: 14,
    });
  });
  test("Start Drag&Drop in viewport then moving outside-top edge-scroll the viewport to the top", () => {
    setViewportOffset(env, 6 * DEFAULT_CELL_WIDTH, 6 * DEFAULT_CELL_HEIGHT);
    expect(viewStore.activeMainViewport).toMatchObject({
      top: 6,
      bottom: 49,
    });
    const { width } = viewStore.sheetViewDimension;
    triggerMouseEvent(".o-fake-grid", "pointerdown", 0.5 * width, DEFAULT_CELL_HEIGHT);
    triggerMouseEvent(".o-fake-grid", "pointermove", 0.5 * width, -0.5 * DEFAULT_CELL_HEIGHT);
    const advanceTimer = edgeScrollDelay(0.5 * DEFAULT_CELL_HEIGHT, 1);
    jest.advanceTimersByTime(advanceTimer);
    triggerMouseEvent(".o-fake-grid", "pointerup", 0.5 * width, -0.5 * DEFAULT_CELL_HEIGHT);
    expect(viewStore.activeMainViewport).toMatchObject({
      top: 4,
      bottom: 47,
    });
  });

  test("drag And Drop through hidden rows", async () => {
    const { bottom } = viewStore.activeMainViewport;
    addRows(model, "after", 80, 100);
    hideRows(model, [bottom + 1]);
    const { width, height } = viewStore.sheetViewDimension;
    const x = width / 2;
    triggerMouseEvent(".o-fake-grid", "pointerdown", x, height / 2);
    triggerMouseEvent(".o-fake-grid", "pointermove", x, 1.5 * height);
    const advanceTimer = edgeScrollDelay(0.5 * height, 5);

    jest.advanceTimersByTime(advanceTimer);
    triggerMouseEvent(".o-fake-grid", "pointerup", x, 1.5 * height);
    expect(viewStore.activeMainViewport).toMatchObject({
      top: 6,
      bottom: 50,
    });
    setViewportOffset(env, 0, (bottom + 2) * DEFAULT_CELL_HEIGHT);
    await nextTick();
    triggerMouseEvent(".o-fake-grid", "pointerdown", x, height / 2);
    triggerMouseEvent(".o-fake-grid", "pointermove", x, -1.5 * height);

    jest.advanceTimersByTime(advanceTimer);
    triggerMouseEvent(".o-fake-grid", "pointerup", x, -1.5 * height);
    expect(viewStore.activeMainViewport).toMatchObject({
      top: 39,
      bottom: 83,
    });
  });
});

test("Drag&drop is stopped when the calling component is unmounted", async () => {
  const { x: offsetCorrectionX } = viewStore.mainViewportCoordinates;
  const x = offsetCorrectionX + DEFAULT_CELL_WIDTH;

  // move to col 1
  triggerMouseEvent(".o-fake-grid", "pointerdown", x, 0);
  triggerMouseEvent("body", "pointermove", x + DEFAULT_CELL_WIDTH / 2, 0);
  let advanceTimer = edgeScrollDelay(offsetCorrectionX + DEFAULT_CELL_WIDTH / 2, 1);
  jest.advanceTimersByTime(advanceTimer);
  expect(selectedCol).toEqual(1);
  triggerMouseEvent("body", "pointerup", x, 0);
  expect(mouseUpFn).toHaveBeenCalledTimes(1);

  const spyRemoveEventListener = jest.spyOn(window, "removeEventListener");
  // move to col 2
  triggerMouseEvent(".o-fake-grid", "pointerdown", x, 0);
  // force unmount
  app.destroy();
  triggerMouseEvent("body", "pointermove", x + DEFAULT_CELL_WIDTH * 1.5, 0);
  advanceTimer = edgeScrollDelay(offsetCorrectionX + DEFAULT_CELL_WIDTH * 1.5, 1);
  jest.advanceTimersByTime(advanceTimer);
  expect(selectedCol).toEqual(1);
  expect(mouseUpFn).toHaveBeenCalledTimes(1);
  expect(spyRemoveEventListener).toHaveBeenCalledTimes(5);
});

test("Drag&drop is stopped when mouseup is called on an element that stops pointer events", async () => {
  const div = document.createElement("div");
  div.id = "test-div";
  div.addEventListener("pointerup", (ev) => ev.stopPropagation());
  document.body.appendChild(div);
  triggerMouseEvent(".o-fake-grid", "pointerdown", 0, 0);
  expect(mouseUpFn).toHaveBeenCalledTimes(0);
  triggerMouseEvent("#test-div", "pointerup", 0, 0);
  expect(mouseUpFn).toHaveBeenCalledTimes(1);
});

test("drag And Drop is based on the current active sheet", async () => {
  setViewportOffset(env, 6 * DEFAULT_CELL_WIDTH, 6 * DEFAULT_CELL_HEIGHT);
  hideColumns(model, [numberToLetters(5)]);
  createSheet(model, { sheetId: "sh2", activate: true });
  setViewportOffset(env, 6 * DEFAULT_CELL_WIDTH, 6 * DEFAULT_CELL_HEIGHT);
  await nextTick();

  expect(viewStore.activeMainViewport).toMatchObject({
    left: 6,
    right: 16,
  });
  const { height } = viewStore.sheetViewDimension;
  triggerMouseEvent(".o-fake-grid", "pointerdown", DEFAULT_CELL_WIDTH, 0.5 * height);
  triggerMouseEvent(".o-fake-grid", "pointermove", -0.5 * DEFAULT_CELL_WIDTH, 0.5 * height);
  const advanceTimer = edgeScrollDelay(0.5 * DEFAULT_CELL_WIDTH, 1);

  jest.advanceTimersByTime(advanceTimer);
  triggerMouseEvent(".o-fake-grid", "pointerup", 0.5 * DEFAULT_CELL_WIDTH, 0.5 * height);
  expect(viewStore.activeMainViewport).toMatchObject({
    left: 4,
    right: 14,
  });
});

test("keyboard inputs are blocked during Drag And Drop", async () => {
  const target = document.querySelector(".o-fake-grid")!;
  triggerMouseEvent(".o-fake-grid", "pointerdown");
  let ev = triggerKeyboardEvent(target, "keydown", { key: "A" });
  expect(ev.defaultPrevented).toBeTruthy();
  triggerMouseEvent(".o-fake-grid", "pointermove");
  ev = triggerKeyboardEvent(target, "keydown", { key: "A" });
  expect(ev.defaultPrevented).toBeTruthy();
  triggerMouseEvent(".o-fake-grid", "pointerup");
  ev = triggerKeyboardEvent(target, "keydown", { key: "A" });
  expect(ev.defaultPrevented).toBeFalsy();
});
