import type { NoteBlockModel, NoteDisplayMode } from '@blocks/index.js';
import type { IPoint, IVec } from '@blocksuite/global/utils';
import type { Locator, Page } from '@playwright/test';

import { assertExists, sleep } from '@blocksuite/global/utils';
import { expect } from '@playwright/test';

import type { Bound } from '../asserts.js';

import '../declare-test-window.js';
import { clickView } from './click.js';
import { dragBetweenCoords } from './drag.js';
import {
  pressBackspace,
  pressEnter,
  pressEscape,
  selectAllByKeyboard,
  SHIFT_KEY,
  SHORT_KEY,
  type,
} from './keyboard.js';
import {
  enterPlaygroundRoom,
  getEditorLocator,
  initEmptyEdgelessState,
  resetHistory,
  waitNextFrame,
} from './misc.js';

const rotWith = (A: number[], C: number[], r = 0): number[] => {
  if (r === 0) return A;

  const s = Math.sin(r);
  const c = Math.cos(r);

  const px = A[0] - C[0];
  const py = A[1] - C[1];

  const nx = px * c - py * s;
  const ny = px * s + py * c;

  return [nx + C[0], ny + C[1]];
};

const AWAIT_TIMEOUT = 500;
export const ZOOM_BAR_RESPONSIVE_SCREEN_WIDTH = 1200;
export type Point = { x: number; y: number };
export enum Shape {
  Diamond = 'Diamond',
  Ellipse = 'Ellipse',
  'Rounded rectangle' = 'Rounded rectangle',
  Square = 'Square',
  Triangle = 'Triangle',
}

export enum LassoMode {
  FreeHand = 'freehand',
  Polygonal = 'polygonal',
}

export enum ConnectorMode {
  Straight,
  Orthogonal,
  Curve,
}

export async function getNoteRect(page: Page, noteId: string) {
  const xywh: string | null = await page.evaluate(
    ([noteId]) => {
      const doc = window.collection.getDoc('doc:home');
      const block = doc?.getBlockById(noteId);
      if (block?.flavour === 'affine:note') {
        return (block as NoteBlockModel).xywh;
      } else {
        return null;
      }
    },
    [noteId] as const
  );
  expect(xywh).not.toBeNull();
  const [x, y, w, h] = JSON.parse(xywh as string);
  return { x, y, w, h };
}

export async function getNoteProps(page: Page, noteId: string) {
  const props = await page.evaluate(
    ([id]) => {
      const doc = window.collection.getDoc('doc:home');
      const block = doc?.getBlockById(id);
      if (block?.flavour === 'affine:note') {
        return (block as NoteBlockModel).keys.reduce(
          (pre, key) => {
            pre[key] = block[key as keyof typeof block] as string;
            return pre;
          },
          {} as Record<string, string | number>
        );
      } else {
        return null;
      }
    },
    [noteId] as const
  );
  return props;
}

export async function extendFormatBar(page: Page) {
  await page.click('sl-button:text("Test Operations")');
  await page.click('sl-menu-item:text("Extend Format Bar")');
  await waitNextFrame(page);
}

export async function toggleFramePanel(page: Page) {
  await page.click('sl-button:text("Test Operations")');
  await page.click('sl-menu-item:text("Toggle Frame Panel")');
  await waitNextFrame(page);
}

export async function toggleMultipleEditors(page: Page) {
  await page.click('sl-button:text("Test Operations")');
  await page.click('sl-menu-item:text("Toggle Multiple Editors")');
  await waitNextFrame(page);
}

export async function switchEditorMode(page: Page) {
  await page.click('sl-tooltip[content="Switch Editor"]');
  // FIXME: listen to editor loaded event
  await waitNextFrame(page);
}

export async function switchMultipleEditorsMode(page: Page) {
  await page.evaluate(() => {
    const containers = document.querySelectorAll('affine-editor-container');
    const mode = containers[0].mode === 'edgeless' ? 'page' : 'edgeless';

    containers.forEach(container => {
      container.mode = mode;
    });
  });
}

export async function switchEditorEmbedMode(page: Page) {
  await page.click('sl-button:text("Test Operations")');
  await page.click('sl-menu-item:text("Switch Offset Mode")');
}

export async function enterPresentationMode(page: Page) {
  await page.click('sl-tooltip[content="Enter presentation mode"]');
  await waitNextFrame(page);
}

export async function toggleEditorReadonly(page: Page) {
  await page.click('sl-button:text("Test Operations")');
  await page.click('sl-menu-item:text("Toggle Readonly")');
  await waitNextFrame(page);
}

type EdgelessTool =
  | 'default'
  | 'pan'
  | 'note'
  | 'shape'
  | 'brush'
  | 'eraser'
  | 'text'
  | 'connector'
  | 'frame'
  | 'frameNavigator'
  | 'lasso';
type ZoomToolType = 'zoomIn' | 'zoomOut' | 'fitToScreen';
type ComponentToolType = 'shape' | 'thin' | 'thick' | 'brush' | 'more';

type PresentationToolType = 'previous' | 'next';

const locatorEdgelessToolButtonSenior = async (
  page: Page,
  selector: string
): Promise<Locator> => {
  const target = page.locator(selector);
  const visible = await target.isVisible();
  if (visible) return target;
  // try to click next page
  const nextButton = page.locator(
    '.senior-nav-button-wrapper.next > icon-button'
  );
  const nextExists = await nextButton.count();
  const isDisabled =
    (await nextButton.getAttribute('data-test-disabled')) === 'true';
  if (!nextExists || isDisabled) return target;
  await nextButton.click();
  await page.waitForTimeout(200);
  return locatorEdgelessToolButtonSenior(page, selector);
};

export async function locatorEdgelessToolButton(
  page: Page,
  type: EdgelessTool,
  innerContainer = true
) {
  const selector = {
    default: '.edgeless-default-button',
    pan: '.edgeless-default-button',
    shape: '.edgeless-shape-button',
    brush: '.edgeless-brush-button',
    eraser: '.edgeless-eraser-button',
    text: '.edgeless-mindmap-button',
    connector: '.edgeless-connector-button',
    note: '.edgeless-note-button',
    frame: '.edgeless-frame-button',
    frameNavigator: '.edgeless-frame-navigator-button',
    lasso: '.edgeless-lasso-button',
  }[type];

  let buttonType;
  switch (type) {
    case 'brush':
    case 'text':
    case 'eraser':
    case 'shape':
    case 'note':
      buttonType = 'edgeless-toolbar-button';
      break;
    default:
      buttonType = 'edgeless-tool-icon-button';
  }
  // TODO: quickTool locator is different
  const button = await locatorEdgelessToolButtonSenior(
    page,
    `edgeless-toolbar-widget ${buttonType}${selector}`
  );

  return innerContainer ? button.locator('.icon-container') : button;
}

export async function toggleZoomBarWhenSmallScreenWidth(page: Page) {
  const toggleZoomBarButton = page.locator(
    '.toggle-button edgeless-tool-icon-button'
  );
  const isClosed = (await toggleZoomBarButton.count()) === 1;
  if (isClosed) {
    await toggleZoomBarButton.click();
    await page.waitForTimeout(200);
  }
}

export async function locatorEdgelessZoomToolButton(
  page: Page,
  type: ZoomToolType,
  innerContainer = true
) {
  const text = {
    zoomIn: 'Zoom in',
    zoomOut: 'Zoom out',
    fitToScreen: 'Fit to screen',
  }[type];

  const screenWidth = page.viewportSize()?.width;
  assertExists(screenWidth);
  let zoomBarClass = 'horizontal';
  if (screenWidth < ZOOM_BAR_RESPONSIVE_SCREEN_WIDTH) {
    await toggleZoomBarWhenSmallScreenWidth(page);
    zoomBarClass = 'vertical';
  }

  const button = page
    .locator(
      `.edgeless-zoom-toolbar-container.${zoomBarClass} edgeless-tool-icon-button`
    )
    .filter({
      hasText: text,
    });

  return innerContainer ? button.locator('.icon-container') : button;
}

export function locatorEdgelessComponentToolButton(
  page: Page,
  type: ComponentToolType,
  innerContainer = true
) {
  const text = {
    shape: 'Shape',
    brush: 'Color',
    thin: 'Thin',
    thick: 'Thick',
    more: 'More',
  }[type];
  const button = page
    .locator('edgeless-element-toolbar-widget editor-icon-button')
    .filter({
      hasText: text,
    });

  return innerContainer ? button.locator('.icon-container') : button;
}

export function locatorPresentationToolbarButton(
  page: Page,
  type: PresentationToolType
) {
  const text = {
    previous: 'Previous',
    next: 'Next',
  }[type];
  const button = page
    .locator('presentation-toolbar edgeless-tool-icon-button')
    .filter({
      hasText: text,
    });

  return button;
}

export async function setEdgelessTool(
  page: Page,
  mode: EdgelessTool,
  shape = Shape.Square
) {
  switch (mode) {
    // text tool is removed, use shortcut to trigger
    case 'text':
      await page.keyboard.press('t', { delay: 100 });
      break;
    case 'default': {
      const button = await locatorEdgelessToolButton(page, 'default', false);
      const classes = (await button.getAttribute('class'))?.split(' ');
      if (!classes?.includes('default')) {
        await button.click();
        await sleep(100);
      }
      break;
    }
    case 'pan': {
      const button = await locatorEdgelessToolButton(page, 'default', false);
      const classes = (await button.getAttribute('class'))?.split(' ');
      if (classes?.includes('default')) {
        await button.click();
        await sleep(100);
      } else if (classes?.includes('pan')) {
        await button.click(); // change to default
        await sleep(100);
        await button.click(); // change to pan
        await sleep(100);
      }
      break;
    }
    case 'lasso':
    case 'note':
    case 'brush':
    case 'eraser':
    case 'frame':
    case 'connector': {
      const button = await locatorEdgelessToolButton(page, mode, false);
      await button.click();
      break;
    }
    case 'shape': {
      const shapeToolButton = await locatorEdgelessToolButton(
        page,
        'shape',
        false
      );
      // Avoid clicking on the shape-element (will trigger dragging mode)
      await shapeToolButton.click({ position: { x: 5, y: 5 } });

      const squareShapeButton = page
        .locator('edgeless-slide-menu edgeless-tool-icon-button')
        .filter({ hasText: shape });
      await squareShapeButton.click();
      break;
    }
  }
}
export type ShapeName =
  | 'rect'
  | 'ellipse'
  | 'diamond'
  | 'triangle'
  | 'roundedRect';

export async function assertEdgelessShapeType(page: Page, type: ShapeName) {
  const curType = await page.evaluate(() => {
    const container = document.querySelector('affine-edgeless-root');
    if (!container) {
      throw new Error('Missing edgeless page');
    }
    const tool = container.gfx.tool.currentToolOption$.peek();
    if (tool.type !== 'shape') throw new Error('Expected shape tool');

    return tool.shapeName;
  });

  expect(type).toEqual(curType);
}

export async function assertEdgelessTool(page: Page, mode: EdgelessTool) {
  const type = await page.evaluate(() => {
    const container = document.querySelector('affine-edgeless-root');
    if (!container) {
      throw new Error('Missing edgeless page');
    }
    return container.gfx.tool.currentToolOption$.peek().type;
  });
  expect(type).toEqual(mode);
}

export async function assertEdgelessConnectorToolMode(
  page: Page,
  mode: ConnectorMode
) {
  const tool = await page.evaluate(() => {
    const container = document.querySelector('affine-edgeless-root');
    if (!container) {
      throw new Error('Missing edgeless page');
    }
    return container.gfx.tool.currentToolOption$.peek();
  });
  if (tool.type !== 'connector') {
    throw new Error('Expected connector tool');
  }
  expect(tool.mode).toEqual(mode);
}

export async function assertEdgelessLassoToolMode(page: Page, mode: LassoMode) {
  const tool = await page.evaluate(() => {
    const container = document.querySelector('affine-edgeless-root');
    if (!container) {
      throw new Error('Missing edgeless page');
    }
    return container.gfx.tool.currentToolOption$.peek();
  });
  if (tool.type !== 'lasso') {
    throw new Error('Expected lasso tool');
  }
  expect(tool.mode).toEqual(mode === LassoMode.FreeHand ? 0 : 1);
}

export async function getEdgelessBlockChild(page: Page) {
  const block = page.locator('affine-edgeless-note');
  const blockBox = await block.boundingBox();
  if (blockBox === null) throw new Error('Missing edgeless block child rect');
  return blockBox;
}

export async function getEdgelessSelectedRect(page: Page) {
  const selectedBox = await page.evaluate(() => {
    const selected = document
      .querySelector('edgeless-selected-rect')
      ?.shadowRoot?.querySelector('.affine-edgeless-selected-rect');
    if (!selected) {
      throw new Error('Missing edgeless selected rect');
    }
    return selected.getBoundingClientRect();
  });
  return selectedBox;
}

export async function getEdgelessSelectedRectModel(page: Page): Promise<Bound> {
  return page.evaluate(() => {
    const container = document.querySelector('affine-edgeless-root');
    if (!container) throw new Error('container not found');
    const bound = container.service.selection.selectedBound;
    return [bound.x, bound.y, bound.w, bound.h];
  });
}

export async function decreaseZoomLevel(page: Page) {
  const btn = await locatorEdgelessZoomToolButton(page, 'zoomOut', false);
  await btn.click();
  await sleep(AWAIT_TIMEOUT);
}

export async function increaseZoomLevel(page: Page) {
  const btn = await locatorEdgelessZoomToolButton(page, 'zoomIn', false);
  await btn.click();
  await sleep(AWAIT_TIMEOUT);
}

export async function autoFit(page: Page) {
  const btn = await locatorEdgelessZoomToolButton(page, 'fitToScreen', false);
  await btn.click();
  await sleep(AWAIT_TIMEOUT);
}

export async function addBasicBrushElement(
  page: Page,
  start: Point,
  end: Point,
  auto = true
) {
  await setEdgelessTool(page, 'brush');
  await dragBetweenCoords(page, start, end, { steps: 100 });
  auto && (await setEdgelessTool(page, 'default'));
}

export async function addBasicRectShapeElement(
  page: Page,
  start: Point,
  end: Point
) {
  await setEdgelessTool(page, 'shape');
  await dragBetweenCoords(page, start, end, { steps: 50 });
}

export async function addBasicShapeElement(
  page: Page,
  start: Point,
  end: Point,
  shape: Shape
) {
  await setEdgelessTool(page, 'shape', shape);
  await dragBetweenCoords(page, start, end, { steps: 50 });
  return (await getSelectedIds(page))[0];
}

export async function addBasicConnectorElement(
  page: Page,
  start: Point,
  end: Point
) {
  await setEdgelessTool(page, 'connector');
  await dragBetweenCoords(page, start, end, { steps: 100 });
}

export async function addBasicFrameElement(
  page: Page,
  start: Point,
  end: Point
) {
  await setEdgelessTool(page, 'frame');
  await dragBetweenCoords(page, start, end, { steps: 50 });
}

export async function addBasicEdgelessText(
  page: Page,
  text: string,
  x: number,
  y: number
) {
  await setEdgelessTool(page, 'text');
  await page.mouse.click(x, y);
  await page.locator('affine-edgeless-text').waitFor({ state: 'visible' });
  await waitNextFrame(page, 100);
  await type(page, text, 20);
  await pressEscape(page, 2);
  await setEdgelessTool(page, 'default');
}

export async function addNote(page: Page, text: string, x: number, y: number) {
  await setEdgelessTool(page, 'note');
  await page.mouse.click(x, y);
  await waitNextFrame(page);

  const paragraphs = text.split('\n');
  let i = 0;
  for (const paragraph of paragraphs) {
    ++i;
    await type(page, paragraph, 20);

    if (i < paragraphs.length) {
      await pressEnter(page);
    }
  }

  const { id } = await page.evaluate(() => {
    const container = document.querySelector('affine-edgeless-root');
    if (!container) throw new Error('container not found');

    return {
      id: container.service.selection.selectedIds[0],
    };
  });

  return id;
}

export async function exitEditing(page: Page) {
  await page.evaluate(() => {
    const container = document.querySelector('affine-edgeless-root');
    if (!container) throw new Error('container not found');

    container.service.selection.set({
      elements: [],
      editing: false,
    });
  });
}

export async function resizeElementByHandle(
  page: Page,
  delta: Point,
  corner:
    | 'top-left'
    | 'top-right'
    | 'bottom-right'
    | 'bottom-left' = 'top-left',
  steps = 1
) {
  const handle = page.locator(`.handle[aria-label="${corner}"] .resize`);
  const box = await handle.boundingBox();
  if (box === null) throw new Error();
  const offset = 5;
  await dragBetweenCoords(
    page,
    { x: box.x + offset, y: box.y + offset },
    { x: box.x + delta.x + offset, y: box.y + delta.y + offset },
    {
      steps,
    }
  );
}

export async function rotateElementByHandle(
  page: Page,
  deg = 0,
  corner:
    | 'top-left'
    | 'top-right'
    | 'bottom-right'
    | 'bottom-left' = 'top-left',
  steps = 1
) {
  const rect = await page
    .locator('.affine-edgeless-selected-rect')
    .boundingBox();
  if (rect === null) throw new Error();
  const box = await page
    .locator(`.handle[aria-label="${corner}"] .rotate`)
    .boundingBox();
  if (box === null) throw new Error();

  const cx = rect.x + rect.width / 2;
  const cy = rect.y + rect.height / 2;
  const x = box.x + box.width / 2;
  const y = box.y + box.height / 2;

  const t = rotWith([x, y], [cx, cy], (deg * Math.PI) / 180);

  await dragBetweenCoords(
    page,
    { x, y },
    { x: t[0], y: t[1] },
    {
      steps,
    }
  );
}

export async function selectBrushColor(page: Page, color: string) {
  const colorButton = page.locator(
    `edgeless-brush-menu .color-unit[aria-label="${color.toLowerCase()}"]`
  );
  await colorButton.click();
}

export async function selectBrushSize(page: Page, size: string) {
  const sizeIndexMap: Record<string, number> = {
    two: 1,
    four: 2,
    six: 3,
    eight: 4,
    ten: 5,
    twelve: 6,
  };
  const sizeButton = page.locator(
    `edgeless-brush-menu .line-width-panel .line-width-button:nth-child(${sizeIndexMap[size]})`
  );
  await sizeButton.click();
}

export async function pickColorAtPoints(page: Page, points: number[][]) {
  const pickedColors: `#${string}`[] = await page.evaluate(points => {
    const node = document.querySelector(
      '.affine-edgeless-surface-block-container canvas'
    ) as HTMLCanvasElement;
    const w = node.width;
    const h = node.height;
    const ctx = node?.getContext('2d');
    if (!ctx) throw new Error('Cannot get canvas context');
    const pixelData = ctx.getImageData(0, 0, w, h).data;

    const colors = points.map(([x, y]) => {
      const startPosition = (y * w + x) * 4;
      return ('#' +
        (
          (1 << 24) +
          (pixelData[startPosition] << 16) +
          (pixelData[startPosition + 1] << 8) +
          pixelData[startPosition + 2]
        )
          .toString(16)
          .slice(1)) as `#${string}`;
    });
    return colors;
  }, points);
  return pickedColors;
}

export async function getNoteBoundBoxInEdgeless(page: Page, noteId: string) {
  const editor = getEditorLocator(page);
  const note = editor.locator(
    `affine-edgeless-note[data-block-id="${noteId}"]`
  );
  const bound = await note.boundingBox();
  if (!bound) {
    throw new Error(`Missing note: ${noteId}`);
  }
  return bound;
}

export async function getAllNoteIds(page: Page) {
  return page.evaluate(() => {
    return Array.from(document.querySelectorAll('affine-note')).map(
      note => note.model.id
    );
  });
}

export async function getAllEdgelessNoteIds(page: Page) {
  return page.evaluate(() => {
    return Array.from(document.querySelectorAll('affine-edgeless-note')).map(
      note => note.model.id
    );
  });
}

export async function getAllEdgelessTextIds(page: Page) {
  return page.evaluate(() => {
    return Array.from(document.querySelectorAll('affine-edgeless-text')).map(
      text => text.model.id
    );
  });
}

export async function countBlock(page: Page, flavour: string) {
  return page.evaluate(
    ([flavour]) => {
      return Array.from(document.querySelectorAll(flavour)).length;
    },
    [flavour]
  );
}

export async function activeNoteInEdgeless(page: Page, noteId: string) {
  const bound = await getNoteBoundBoxInEdgeless(page, noteId);
  await page.mouse.dblclick(
    bound.x + bound.width / 2,
    bound.y + bound.height / 2
  );
}

export async function selectNoteInEdgeless(page: Page, noteId: string) {
  const bound = await getNoteBoundBoxInEdgeless(page, noteId);
  await page.mouse.click(bound.x + bound.width / 2, bound.y + bound.height / 2);
}

export function locatorNoteDisplayModeButton(
  page: Page,
  mode: NoteDisplayMode
) {
  return page
    .locator('edgeless-change-note-button')
    .locator('note-display-mode-panel')
    .locator(`.item.${mode}`);
}

export function locatorScalePanelButton(page: Page, scale: number) {
  return page.locator('edgeless-scale-panel').locator(`.scale-${scale}`);
}

export async function changeNoteDisplayMode(page: Page, mode: NoteDisplayMode) {
  const button = locatorNoteDisplayModeButton(page, mode);
  await button.click();
}

export async function changeNoteDisplayModeWithId(
  page: Page,
  noteId: string,
  mode: NoteDisplayMode
) {
  await selectNoteInEdgeless(page, noteId);
  await triggerComponentToolbarAction(page, 'changeNoteDisplayMode');
  await waitNextFrame(page);
  await changeNoteDisplayMode(page, mode);
}

export async function updateExistedBrushElementSize(
  page: Page,
  nthSizeButton: 1 | 2 | 3 | 4 | 5 | 6
) {
  // get the nth brush size button
  const btn = page.locator(
    `.line-width-panel > div:nth-child(${nthSizeButton})`
  );

  await btn.click();
}

export async function openComponentToolbarMoreMenu(page: Page) {
  const btn = page.locator(
    'edgeless-element-toolbar-widget edgeless-more-button editor-menu-button'
  );

  await btn.click();
}

export async function clickComponentToolbarMoreMenuButton(
  page: Page,
  button: 'delete'
) {
  const text = {
    delete: 'Delete',
  }[button];

  const btn = locatorComponentToolbarMoreButton(page)
    .locator('editor-menu-action')
    .filter({ hasText: text });

  await btn.click();
}

// stepX/Y may not equal to wheel event delta.
// Chromium reports deltaX/deltaY scaled by host device scale factor.
// https://bugs.chromium.org/p/chromium/issues/detail?id=1324819
export async function zoomByMouseWheel(
  page: Page,
  stepX: number,
  stepY: number
) {
  await page.keyboard.down(SHORT_KEY);
  await page.mouse.wheel(stepX, stepY);
  await page.keyboard.up(SHORT_KEY);
}

// touch screen is not supported by Playwright now
// use pointer event mock instead
// https://github.com/microsoft/playwright/issues/2903
export async function multiTouchDown(page: Page, points: Point[]) {
  await page.evaluate(points => {
    const target = document.querySelector('affine-edgeless-root');
    if (!target) {
      throw new Error('Missing edgeless page');
    }
    points.forEach((point, index) => {
      const clientX = point.x;
      const clientY = point.y;

      target.dispatchEvent(
        new PointerEvent('pointerdown', {
          clientX,
          clientY,
          bubbles: true,
          pointerType: 'touch',
          pointerId: index,
          isPrimary: index === 0,
        })
      );
    });
  }, points);
}

export async function multiTouchMove(
  page: Page,
  from: Point[],
  to: Point[],
  step = 5
) {
  await page.evaluate(
    async ({ from, to, step }) => {
      const target = document.querySelector('affine-edgeless-root');
      if (!target) {
        throw new Error('Missing edgeless page');
      }

      if (from.length !== to.length) {
        throw new Error('from and to should have the same length');
      }

      if (step !== 0) {
        for (const [i] of Array.from({ length: step }).entries()) {
          from.forEach((point, index) => {
            const clientX =
              point.x + ((to[index].x - point.x) / step) * (i + 1);
            const clientY =
              point.y + ((to[index].y - point.y) / step) * (i + 1);

            target.dispatchEvent(
              new PointerEvent('pointermove', {
                clientX,
                clientY,
                bubbles: true,
                pointerType: 'touch',
                pointerId: index,
                isPrimary: index === 0,
              })
            );
          });
          await new Promise(resolve => setTimeout(resolve, 16));
        }
      }
    },
    { from, to, step }
  );
}

export async function multiTouchUp(page: Page, points: Point[]) {
  await page.evaluate(points => {
    const target = document.querySelector('affine-edgeless-root');
    if (!target) {
      throw new Error('Missing edgeless page');
    }
    points.forEach((point, index) => {
      const clientX = point.x;
      const clientY = point.y;

      target.dispatchEvent(
        new PointerEvent('pointerup', {
          clientX,
          clientY,
          bubbles: true,
          pointerType: 'touch',
          pointerId: index,
          isPrimary: index === 0,
        })
      );
    });
  }, points);
}

export async function zoomFitByKeyboard(page: Page) {
  await page.keyboard.press(`${SHORT_KEY}+1`, { delay: 100 });
  await waitNextFrame(page, 300);
}

export async function zoomOutByKeyboard(page: Page) {
  await page.keyboard.press(`${SHORT_KEY}+-`, { delay: 100 });
  await waitNextFrame(page, 300);
}

export async function zoomResetByKeyboard(page: Page) {
  await page.keyboard.press(`${SHORT_KEY}+0`, { delay: 50 });
  // Wait for animation
  await waitNextFrame(page, 300);
}

export async function zoomInByKeyboard(page: Page) {
  await page.keyboard.press(`${SHORT_KEY}+=`, { delay: 50 });
  await waitNextFrame(page, 300);
}

export async function getZoomLevel(page: Page) {
  const screenWidth = page.viewportSize()?.width;
  assertExists(screenWidth);
  let zoomBarClass = 'horizontal';
  if (screenWidth < ZOOM_BAR_RESPONSIVE_SCREEN_WIDTH) {
    await toggleZoomBarWhenSmallScreenWidth(page);
    zoomBarClass = 'vertical';
  }
  const span = page.locator(
    `.edgeless-zoom-toolbar-container.${zoomBarClass} .zoom-percent`
  );
  await waitNextFrame(page);
  const text = await span.textContent();
  if (!text) {
    throw new Error('Missing .zoom-percent');
  }
  return Number(text.replace('%', ''));
}

export async function optionMouseDrag(
  page: Page,
  start: number[],
  end: number[]
) {
  start = await toViewCoord(page, start);
  end = await toViewCoord(page, end);
  await page.keyboard.down('Alt');
  await dragBetweenCoords(
    page,
    { x: start[0], y: start[1] },
    { x: end[0], y: end[1] },
    { steps: 30 }
  );
  await page.keyboard.up('Alt');
}

export async function shiftClick(page: Page, point: IPoint) {
  await page.keyboard.down(SHIFT_KEY);
  await page.mouse.click(point.x, point.y);
  await page.keyboard.up(SHIFT_KEY);
}

export async function shiftClickView(page: Page, point: [number, number]) {
  await page.keyboard.down(SHIFT_KEY);
  await clickView(page, point);
  await page.keyboard.up(SHIFT_KEY);
}

export async function deleteAll(page: Page) {
  await clickView(page, [0, 0]);
  await selectAllByKeyboard(page);
  await pressBackspace(page);
}

export async function deleteAllConnectors(page: Page) {
  return page.evaluate(() => {
    const container = document.querySelector('affine-edgeless-root');
    if (!container) throw new Error('container not found');
    container.service.getElementsByType('connector').forEach(c => {
      container.service.removeElement(c.id);
    });
  });
}

export function locatorComponentToolbar(page: Page) {
  return page.locator('edgeless-element-toolbar-widget');
}

export function locatorComponentToolbarMoreButton(page: Page) {
  const moreButton = locatorComponentToolbar(page).locator(
    'edgeless-more-button'
  );
  return moreButton;
}
type Action =
  | 'bringToFront'
  | 'bringForward'
  | 'sendBackward'
  | 'sendToBack'
  | 'copyAsPng'
  | 'changeNoteColor'
  | 'changeShapeStyle'
  | 'changeShapeFillColor'
  | 'changeShapeStrokeColor'
  | 'changeShapeStrokeStyles'
  | 'changeConnectorStrokeColor'
  | 'changeConnectorStrokeStyles'
  | 'changeConnectorShape'
  | 'addFrame'
  | 'addGroup'
  | 'addMindmap'
  | 'createGroupOnMoreOption'
  | 'ungroup'
  | 'releaseFromGroup'
  | 'createFrameOnMoreOption'
  | 'duplicate'
  | 'renameGroup'
  | 'autoSize'
  | 'changeNoteDisplayMode'
  | 'changeNoteSlicerSetting'
  | 'changeNoteScale'
  | 'addText'
  | 'quickConnect'
  | 'turnIntoLinkedDoc'
  | 'createLinkedDoc'
  | 'openLinkedDoc'
  | 'toCardView'
  | 'toEmbedView'
  | 'autoArrange'
  | 'autoResize';

export async function triggerComponentToolbarAction(
  page: Page,
  action: Action
) {
  switch (action) {
    case 'bringToFront': {
      const moreButton = locatorComponentToolbarMoreButton(page);
      await moreButton.click();

      const actionButton = moreButton
        .locator('.more-actions-container editor-menu-action')
        .filter({
          hasText: 'Bring to Front',
        });
      await actionButton.click();
      break;
    }
    case 'bringForward': {
      const moreButton = locatorComponentToolbarMoreButton(page);
      await moreButton.click();

      const actionButton = moreButton
        .locator('.more-actions-container editor-menu-action')
        .filter({
          hasText: 'Bring Forward',
        });
      await actionButton.click();
      break;
    }
    case 'sendBackward': {
      const moreButton = locatorComponentToolbarMoreButton(page);
      await moreButton.click();

      const actionButton = moreButton
        .locator('.more-actions-container editor-menu-action')
        .filter({
          hasText: 'Send Backward',
        });
      await actionButton.click();
      break;
    }
    case 'sendToBack': {
      const moreButton = locatorComponentToolbarMoreButton(page);
      await moreButton.click();

      const actionButton = moreButton
        .locator('.more-actions-container editor-menu-action')
        .filter({
          hasText: 'Send to Back',
        });
      await actionButton.click();
      break;
    }
    case 'copyAsPng': {
      const moreButton = locatorComponentToolbarMoreButton(page);
      await moreButton.click();

      const actionButton = moreButton
        .locator('.more-actions-container editor-menu-action')
        .filter({
          hasText: 'Copy as PNG',
        });
      await actionButton.click();
      break;
    }
    case 'createFrameOnMoreOption': {
      const moreButton = locatorComponentToolbarMoreButton(page);
      await moreButton.click();

      const actionButton = moreButton
        .locator('.more-actions-container editor-menu-action')
        .filter({
          hasText: 'Frame Section',
        });
      await actionButton.click();
      break;
    }
    case 'duplicate': {
      const moreButton = locatorComponentToolbarMoreButton(page);
      await moreButton.click();

      const actionButton = moreButton
        .locator('.more-actions-container editor-menu-action')
        .filter({
          hasText: 'Duplicate',
        });
      await actionButton.click();
      break;
    }
    case 'changeShapeFillColor': {
      const button = locatorComponentToolbar(page)
        .locator('edgeless-change-shape-button')
        .getByRole('button', { name: 'Fill color' });
      await button.click();
      break;
    }
    case 'changeShapeStrokeColor': {
      const button = locatorComponentToolbar(page)
        .locator('edgeless-change-shape-button')
        .getByRole('button', { name: 'Border style' });
      await button.click();
      break;
    }
    case 'changeShapeStrokeStyles': {
      const button = locatorComponentToolbar(page)
        .locator('edgeless-change-shape-button')
        .getByRole('button', { name: 'Border style' });
      await button.click();
      break;
    }
    case 'changeShapeStyle': {
      const button = locatorComponentToolbar(page)
        .locator('edgeless-change-shape-button')
        .getByRole('button', { name: /^Style$/ });
      await button.click();
      break;
    }
    case 'changeConnectorStrokeColor': {
      const button = page
        .locator('edgeless-change-connector-button')
        .getByRole('button', { name: 'Stroke style' });
      await button.click();
      break;
    }
    case 'changeConnectorStrokeStyles': {
      const button = locatorComponentToolbar(page)
        .locator('edgeless-change-connector-button')
        .getByRole('button', { name: 'Stroke style' });
      await button.click();
      break;
    }
    case 'changeConnectorShape': {
      const button = locatorComponentToolbar(page)
        .locator('edgeless-change-connector-button')
        .getByRole('button', { name: 'Shape' });
      await button.click();
      break;
    }
    case 'addFrame': {
      const button = locatorComponentToolbar(page).locator(
        'edgeless-add-frame-button'
      );
      await button.click();
      break;
    }
    case 'addGroup': {
      const button = locatorComponentToolbar(page).locator(
        'edgeless-add-group-button'
      );
      await button.click();
      break;
    }
    case 'addMindmap': {
      const button = page.locator('edgeless-mindmap-tool-button');
      await button.click();
      await page.mouse.move(400, 400);
      await page.mouse.click(400, 400);
      break;
    }
    case 'createGroupOnMoreOption': {
      const moreButton = locatorComponentToolbarMoreButton(page);
      await moreButton.click();

      const actionButton = moreButton
        .locator('.more-actions-container editor-menu-action')
        .filter({
          hasText: 'Group Section',
        });
      await actionButton.click();
      break;
    }
    case 'ungroup': {
      const button = locatorComponentToolbar(page)
        .locator('edgeless-change-group-button')
        .getByRole('button', { name: 'Ungroup' });
      await button.click();
      break;
    }
    case 'renameGroup': {
      const button = locatorComponentToolbar(page)
        .locator('edgeless-change-group-button')
        .getByRole('button', { name: 'Rename' });
      await button.click();
      break;
    }
    case 'releaseFromGroup': {
      const button = locatorComponentToolbar(page).locator(
        'edgeless-release-from-group-button'
      );
      await button.click();
      break;
    }
    case 'changeNoteColor': {
      const button = locatorComponentToolbar(page)
        .locator('edgeless-change-note-button')
        .getByRole('button', { name: 'Background' });
      await button.click();
      break;
    }
    case 'changeNoteDisplayMode': {
      const button = locatorComponentToolbar(page)
        .locator('edgeless-change-note-button')
        .getByRole('button', { name: 'Mode' });
      await button.click();
      break;
    }
    case 'changeNoteSlicerSetting': {
      const button = locatorComponentToolbar(page)
        .locator('edgeless-change-note-button')
        .getByRole('button', { name: 'Slicer' });
      await button.click();
      break;
    }
    case 'changeNoteScale': {
      const button = locatorComponentToolbar(page)
        .locator('edgeless-change-note-button')
        .getByRole('button', { name: 'Scale' });
      await button.click();
      break;
    }
    case 'autoSize': {
      const button = locatorComponentToolbar(page)
        .locator('edgeless-change-note-button')
        .getByRole('button', { name: 'Size' });
      await button.click();
      break;
    }
    case 'addText': {
      const button = locatorComponentToolbar(page).getByRole('button', {
        name: 'Add text',
      });
      await button.click();
      break;
    }
    case 'quickConnect': {
      const button = locatorComponentToolbar(page).getByRole('button', {
        name: 'Draw connector',
      });
      await button.click();
      break;
    }
    case 'turnIntoLinkedDoc': {
      const moreButton = locatorComponentToolbarMoreButton(page);
      await moreButton.click();

      const actionButton = moreButton
        .locator('.more-actions-container editor-menu-action')
        .filter({
          hasText: 'Turn into linked doc',
        });
      await actionButton.click();
      break;
    }
    case 'createLinkedDoc': {
      const moreButton = locatorComponentToolbarMoreButton(page);
      await moreButton.click();

      const actionButton = moreButton
        .locator('.more-actions-container editor-menu-action')
        .filter({
          hasText: 'Create linked doc',
        });
      await actionButton.click();
      break;
    }
    case 'openLinkedDoc': {
      const openButton = locatorComponentToolbar(page).getByRole('button', {
        name: 'Open',
      });
      await openButton.click();

      const button = locatorComponentToolbar(page).getByRole('button', {
        name: 'Open this doc',
      });
      await button.click();
      break;
    }
    case 'toCardView': {
      const button = locatorComponentToolbar(page)
        .locator('edgeless-tool-icon-button')
        .filter({
          hasText: 'Card view',
        });
      await button.click();
      break;
    }
    case 'toEmbedView': {
      const button = locatorComponentToolbar(page)
        .locator('edgeless-tool-icon-button')
        .filter({
          hasText: 'Embed view',
        });
      await button.click();
      break;
    }
    case 'autoArrange': {
      const button = locatorComponentToolbar(page).locator(
        'edgeless-align-button'
      );
      await button.click();
      const arrange = button.locator('editor-icon-button').filter({
        hasText: 'Auto arrange',
      });
      await arrange.click();
      break;
    }
    case 'autoResize': {
      const button = locatorComponentToolbar(page).locator(
        'edgeless-align-button'
      );
      await button.click();
      const resize = button.locator('editor-icon-button').filter({
        hasText: 'Resize & Align',
      });
      await resize.click();
      break;
    }
  }
}

export async function changeEdgelessNoteBackground(page: Page, color: string) {
  const colorButton = page
    .locator('edgeless-change-note-button')
    .locator(`.color-unit[aria-label="${color}"]`);
  await colorButton.click();
}

export async function changeShapeFillColor(page: Page, color: string) {
  const colorButton = page
    .locator('edgeless-change-shape-button')
    .locator('edgeless-color-picker-button.fill-color')
    .locator('edgeless-color-panel')
    .locator(`.color-unit[aria-label="${color}"]`);
  await colorButton.click({ force: true });
}

export async function changeShapeFillColorToTransparent(page: Page) {
  const colorButton = page
    .locator('edgeless-change-shape-button')
    .locator('edgeless-color-picker-button.fill-color')
    .locator('edgeless-color-panel')
    .locator('edgeless-color-custom-button');
  await colorButton.click({ force: true });

  const input = page.locator('edgeless-color-picker').locator('label.alpha');
  await input.focus();
  await input.press('ArrowRight');
  await input.press('ArrowRight');
  await input.press('ArrowRight');
  await input.press('Backspace');
  await input.press('Backspace');
  await input.press('Backspace');
}

export async function changeShapeStrokeColor(page: Page, color: string) {
  const colorButton = page
    .locator('edgeless-change-shape-button')
    .locator('edgeless-color-picker-button.border-style')
    .locator(`.color-unit[aria-label="${color}"]`);
  await colorButton.click();
}

export async function resizeConnectorByStartCapitalHandler(
  page: Page,
  delta: { x: number; y: number },
  steps = 1
) {
  const handler = page.locator(
    '.affine-edgeless-selected-rect .line-controller.line-start'
  );
  const box = await handler.boundingBox();
  if (box === null) throw new Error();
  const offset = 5;
  await dragBetweenCoords(
    page,
    { x: box.x + offset, y: box.y + offset },
    { x: box.x + delta.x + offset, y: box.y + delta.y + offset },
    {
      steps,
    }
  );
}

export function getEdgelessLineWidthPanel(page: Page) {
  return page
    .locator('edgeless-change-shape-button')
    .locator('edgeless-line-width-panel')
    .locator('.line-width-panel');
}
export async function changeShapeStrokeWidth(page: Page) {
  const lineWidthPanel = getEdgelessLineWidthPanel(page);
  const lineWidthPanelRect = await lineWidthPanel.boundingBox();
  assertExists(lineWidthPanelRect);
  // click line width panel by position
  const x = lineWidthPanelRect.x + 40;
  const y = lineWidthPanelRect.y + 10;
  await page.mouse.click(x, y);
}

export function locatorShapeStrokeStyleButton(
  page: Page,
  mode: 'solid' | 'dash' | 'none'
) {
  return page
    .locator('edgeless-change-shape-button')
    .locator(`.line-style-button.mode-${mode}`);
}

export async function changeShapeStrokeStyle(
  page: Page,
  mode: 'solid' | 'dash' | 'none'
) {
  const button = locatorShapeStrokeStyleButton(page, mode);
  await button.click();
}

export function locatorShapeStyleButton(
  page: Page,
  style: 'general' | 'scribbled'
) {
  return page
    .locator('edgeless-change-shape-button')
    .locator('edgeless-shape-style-panel')
    .getByRole('button', { name: style });
}

export async function changeShapeStyle(
  page: Page,
  style: 'general' | 'scribbled'
) {
  const button = locatorShapeStyleButton(page, style);
  await button.click();
}

export async function changeConnectorStrokeColor(page: Page, color: string) {
  const colorButton = page
    .locator('edgeless-change-connector-button')
    .locator('edgeless-color-panel')
    .getByLabel(color);
  await colorButton.click();
}

export function locatorConnectorStrokeWidthButton(
  page: Page,
  buttonPosition: number
) {
  return page
    .locator('edgeless-change-connector-button')
    .locator(`edgeless-line-width-panel`)
    .locator(`.line-width-button:nth-child(${buttonPosition})`);
}
export async function changeConnectorStrokeWidth(
  page: Page,
  buttonPosition: number
) {
  const button = locatorConnectorStrokeWidthButton(page, buttonPosition);
  await button.click();
}

export function locatorConnectorStrokeStyleButton(
  page: Page,
  mode: 'solid' | 'dash' | 'none'
) {
  return page
    .locator('edgeless-change-connector-button')
    .locator(`.line-style-button.mode-${mode}`);
}
export async function changeConnectorStrokeStyle(
  page: Page,
  mode: 'solid' | 'dash' | 'none'
) {
  const button = locatorConnectorStrokeStyleButton(page, mode);
  await button.click();
}

export async function initThreeOverlapFilledShapes(page: Page) {
  const rect0 = {
    start: { x: 100, y: 100 },
    end: { x: 200, y: 200 },
  };
  await addBasicRectShapeElement(page, rect0.start, rect0.end);
  await page.mouse.click(rect0.start.x + 5, rect0.start.y + 5);
  await triggerComponentToolbarAction(page, 'changeShapeFillColor');
  await changeShapeFillColor(page, '--affine-palette-shape-teal');

  const rect1 = {
    start: { x: 130, y: 130 },
    end: { x: 230, y: 230 },
  };
  await addBasicRectShapeElement(page, rect1.start, rect1.end);
  await page.mouse.click(rect1.start.x + 5, rect1.start.y + 5);
  await triggerComponentToolbarAction(page, 'changeShapeFillColor');
  await changeShapeFillColor(page, '--affine-palette-shape-black');

  const rect2 = {
    start: { x: 160, y: 160 },
    end: { x: 260, y: 260 },
  };
  await addBasicRectShapeElement(page, rect2.start, rect2.end);
  await page.mouse.click(rect2.start.x + 5, rect2.start.y + 5);
  await triggerComponentToolbarAction(page, 'changeShapeFillColor');
  await changeShapeFillColor(page, '--affine-palette-shape-white');
}

export async function initThreeOverlapNotes(page: Page, x = 130, y = 140) {
  await addNote(page, 'abc', x, y);
  await addNote(page, 'efg', x + 30, y);
  await addNote(page, 'hij', x + 60, y);
}

export async function initThreeNotes(page: Page) {
  await addNote(page, 'abc', 30 + 100, 40 + 100);
  await addNote(page, 'efg', 30 + 130, 40 + 200);
  await addNote(page, 'hij', 30 + 160, 40 + 300);
}

export async function toViewCoord(page: Page, point: number[]) {
  return page.evaluate(point => {
    const container = document.querySelector('affine-edgeless-root');
    if (!container) throw new Error('container not found');
    return container.service.viewport.toViewCoord(point[0], point[1]);
  }, point);
}

export async function dragBetweenViewCoords(
  page: Page,
  start: number[],
  end: number[],
  options?: Parameters<typeof dragBetweenCoords>[3]
) {
  const [startX, startY] = await toViewCoord(page, start);
  const [endX, endY] = await toViewCoord(page, end);
  await dragBetweenCoords(
    page,
    { x: startX, y: startY },
    { x: endX, y: endY },
    options
  );
}

export async function toModelCoord(page: Page, point: number[]) {
  return page.evaluate(point => {
    const container = document.querySelector('affine-edgeless-root');
    if (!container) throw new Error('container not found');
    return container.service.viewport.toModelCoord(point[0], point[1]);
  }, point);
}

export async function getConnectorSourceConnection(page: Page) {
  return page.evaluate(() => {
    const container = document.querySelector('affine-edgeless-root');
    if (!container) throw new Error('container not found');
    return container.service.getElementsByType('connector')[0].source;
  });
}

export async function getConnectorPath(page: Page, index = 0): Promise<IVec[]> {
  return page.evaluate(
    ([index]) => {
      const container = document.querySelector('affine-edgeless-root');
      if (!container) throw new Error('container not found');
      const connectors = container.service.getElementsByType('connector');
      return connectors[index].absolutePath;
    },
    [index]
  );
}

export async function getEdgelessElementBound(
  page: Page,
  elementId: string
): Promise<[number, number, number, number]> {
  return page.evaluate(
    ([elementId]) => {
      const container = document.querySelector('affine-edgeless-root');
      if (!container) throw new Error('container not found');
      const element = container.service.getElementById(elementId);
      if (!element) throw new Error(`element not found: ${elementId}`);
      return JSON.parse(element.xywh);
    },
    [elementId]
  );
}

export async function getSelectedIds(page: Page) {
  return page.evaluate(() => {
    const container = document.querySelector('affine-edgeless-root');
    if (!container) throw new Error('container not found');
    return container.service.selection.selectedElements.map(e => e.id);
  });
}

export async function getSelectedBoundCount(page: Page) {
  return page.evaluate(() => {
    const container = document.querySelector('affine-edgeless-root');
    if (!container) throw new Error('container not found');
    return container.service.selection.selectedElements.length;
  });
}

export async function getSelectedBound(
  page: Page,
  index = 0
): Promise<[number, number, number, number]> {
  return page.evaluate(
    ([index]) => {
      const container = document.querySelector('affine-edgeless-root');
      if (!container) throw new Error('container not found');
      const selected = container.service.selection.selectedElements[index];
      return JSON.parse(selected.xywh);
    },
    [index]
  );
}

export async function getContainerOfElements(page: Page, ids: string[]) {
  return page.evaluate(
    ([ids]) => {
      const container = document.querySelector('affine-edgeless-root');
      if (!container) throw new Error('container not found');

      return ids.map(id => container.service.surface.getGroup(id)?.id ?? null);
    },
    [ids]
  );
}

export async function getContainerIds(page: Page) {
  return page.evaluate(() => {
    const container = document.querySelector('affine-edgeless-root');
    if (!container) throw new Error('container not found');
    return container.service.elements.map(el => el.group?.id ?? 'null');
  });
}

export async function getContainerChildIds(page: Page, id: string) {
  return page.evaluate(
    ([id]) => {
      const container = document.querySelector('affine-edgeless-root');
      if (!container) throw new Error('container not found');
      const gfxModel = container.service.getElementById(id);

      return gfxModel && container.service.surface.isGroup(gfxModel)
        ? gfxModel.childIds
        : [];
    },
    [id]
  );
}

export async function getCanvasElementsCount(page: Page) {
  return page.evaluate(() => {
    const container = document.querySelector('affine-edgeless-root');
    if (!container) throw new Error('container not found');
    return container.service.elements.length;
  });
}

export async function getSortedIds(page: Page) {
  return page.evaluate(() => {
    const container = document.querySelector('affine-edgeless-root');
    if (!container) throw new Error('container not found');
    return container.service.layer.canvasElements.map(e => e.id);
  });
}

export async function getAllSortedIds(page: Page) {
  return page.evaluate(() => {
    const container = document.querySelector('affine-edgeless-root');
    if (!container) throw new Error('container not found');
    return container.service.edgelessElements.map(e => e.id);
  });
}

export async function getTypeById(page: Page, id: string) {
  return page.evaluate(
    ([id]) => {
      const container = document.querySelector('affine-edgeless-root');
      if (!container) throw new Error('container not found');
      const element = container.service.getElementById(id)!;
      return 'flavour' in element ? element.flavour : element.type;
    },
    [id]
  );
}

export async function getIds(page: Page, filterGroup = false) {
  return page.evaluate(
    ([filterGroup]) => {
      const container = document.querySelector('affine-edgeless-root');
      if (!container) throw new Error('container not found');
      return container.service.elements
        .filter(el => !filterGroup || el.type !== 'group')
        .map(e => e.id);
    },
    [filterGroup]
  );
}

export async function getFirstContainerId(page: Page, exclude: string[] = []) {
  return page.evaluate(
    ([exclude]) => {
      const container = document.querySelector('affine-edgeless-root');
      if (!container) throw new Error('container not found');
      return (
        container.service.edgelessElements.find(
          e => container.service.surface.isGroup(e) && !exclude.includes(e.id)
        )?.id ?? ''
      );
    },
    [exclude]
  );
}

export async function getIndexes(page: Page) {
  return page.evaluate(() => {
    const container = document.querySelector('affine-edgeless-root');
    if (!container) throw new Error('container not found');
    return container.service.elements.map(e => e.index);
  });
}

export async function getSortedIdsInViewport(page: Page) {
  return page.evaluate(() => {
    const container = document.querySelector('affine-edgeless-root');
    if (!container) throw new Error('container not found');
    const { service } = container;
    return service.gfx.grid
      .search(service.viewport.viewportBounds, {
        filter: ['canvas'],
      })
      .map(e => e.id);
  });
}

export async function edgelessCommonSetup(page: Page) {
  await enterPlaygroundRoom(page);
  await initEmptyEdgelessState(page);
  await switchEditorMode(page);
  await deleteAll(page);
  await resetHistory(page);
}

export async function createFrame(
  page: Page,
  coord1: [number, number],
  coord2: [number, number]
) {
  await page.keyboard.press('f');
  await dragBetweenViewCoords(page, coord1, coord2);
  const id = (await getSelectedIds(page))[0];
  await page.keyboard.press('Escape');
  return id;
}

export async function createShapeElement(
  page: Page,
  coord1: number[],
  coord2: number[],
  shape = Shape.Square
) {
  const start = await toViewCoord(page, coord1);
  const end = await toViewCoord(page, coord2);
  const shapeId = await addBasicShapeElement(
    page,
    { x: start[0], y: start[1] },
    { x: end[0], y: end[1] },
    shape
  );
  return shapeId;
}

export async function createConnectorElement(
  page: Page,
  coord1: number[],
  coord2: number[]
) {
  const start = await toViewCoord(page, coord1);
  const end = await toViewCoord(page, coord2);
  await addBasicConnectorElement(
    page,
    { x: start[0], y: start[1] },
    { x: end[0], y: end[1] }
  );
}

export async function createFrameElement(
  page: Page,
  coord1: number[],
  coord2: number[]
) {
  const start = await toViewCoord(page, coord1);
  const end = await toViewCoord(page, coord2);
  await addBasicFrameElement(
    page,
    { x: start[0], y: start[1] },
    { x: end[0], y: end[1] }
  );
}

export async function createBrushElement(
  page: Page,
  coord1: number[],
  coord2: number[],
  auto = true
) {
  const start = await toViewCoord(page, coord1);
  const end = await toViewCoord(page, coord2);
  await addBasicBrushElement(
    page,
    { x: start[0], y: start[1] },
    { x: end[0], y: end[1] },
    auto
  );
}

export async function createEdgelessText(
  page: Page,
  coord: number[],
  text = 'text'
) {
  const position = await toViewCoord(page, coord);
  await addBasicEdgelessText(page, text, position[0], position[1]);
}

export async function createMindmap(page: Page, coord: number[]) {
  const position = await toViewCoord(page, coord);
  await page.keyboard.press('m');
  await page.mouse.click(position[0], position[1]);
}

export async function createNote(
  page: Page,
  coord1: number[],
  content?: string
) {
  const start = await toViewCoord(page, coord1);
  return addNote(page, content || 'note', start[0], start[1]);
}

export async function hoverOnNote(page: Page, id: string, offset = [0, 0]) {
  const blockRect = await page.locator(`[data-block-id="${id}"]`).boundingBox();

  assertExists(blockRect);

  await page.mouse.move(
    blockRect.x + blockRect.width / 2 + offset[0],
    blockRect.y + blockRect.height / 2 + offset[1]
  );
}

export function toIdCountMap(ids: string[]) {
  return ids.reduce(
    (pre, cur) => {
      pre[cur] = (pre[cur] ?? 0) + 1;
      return pre;
    },
    {} as Record<string, number>
  );
}

export function getFrameTitle(page: Page, frame: string) {
  return page.locator(`affine-frame-title[data-id="${frame}"]`);
}
