import { nextFrame } from '@open-wc/testing'
import { sendMouse } from '@web/test-runner-commands'

/**
 * Represents a 2D point with x and y coordinates.
 */
export interface Point {
  /** The x-coordinate of the point. */
  x: number
  /** The y-coordinate of the point. */
  y: number
}

/**
 * A utility class for mocking user interface interactions in tests.
 * It provides static methods to simulate keyboard and mouse events.
 * This class is designed as a collection of static helpers, hence it has no constructor or instances.
 */
// eslint-disable-next-line @typescript-eslint/no-extraneous-class
export class UiMock {
  /**
   * Dispatches a 'keydown' event on a given element.
   *
   * @param element The target element for the event.
   * @param code The `code` value for the KeyboardEvent (e.g., 'Enter', 'KeyA').
   * @param opts Additional options to pass to the KeyboardEvent constructor.
   *
   * @example
   * UiMock.keyDown(document.body, 'Enter');
   * UiMock.keyDown(myInputElement, 'KeyA', { ctrlKey: true });
   */
  static keyDown(element: EventTarget, code: string, opts: KeyboardEventInit = {}): void {
    const defaults: KeyboardEventInit = {
      bubbles: true,
      cancelable: true,
      composed: true,
    }
    const options = { ...defaults, ...opts, ...{ code } }
    const down = new KeyboardEvent('keydown', options)
    element.dispatchEvent(down)
  }

  /**
   * Dispatches a 'keyup' event on a given element.
   *
   * @param element The target element for the event.
   * @param code The `code` value for the KeyboardEvent (e.g., 'Escape', 'KeyB').
   * @param opts Additional options to pass to the KeyboardEvent constructor.
   *
   * @example
   * UiMock.keyUp(document.body, 'Escape');
   */
  static keyUp(element: EventTarget, code: string, opts: KeyboardEventInit = {}): void {
    const defaults: KeyboardEventInit = {
      bubbles: true,
      cancelable: true,
      composed: true,
    }
    const options = { ...defaults, ...opts, ...{ code } }
    const down = new KeyboardEvent('keyup', options)
    element.dispatchEvent(down)
  }

  /**
   * Simulates a full key press by dispatching 'keydown' and 'keyup' events sequentially.
   * It waits for the next animation frame between the two events.
   *
   * @param element The target element for the event.
   * @param code The `code` value for the KeyboardEvent.
   * @param opts Additional options to pass to the KeyboardEvent constructor.
   *
   * @example
   * await UiMock.keyPress(myButton, 'Space');
   */
  static async keyPress(element: EventTarget, code: string, opts?: KeyboardEventInit): Promise<void> {
    this.keyDown(element, code, opts)
    await nextFrame()
    this.keyUp(element, code, opts)
  }

  /**
   * Calculates the coordinates of the center of a given element.
   *
   * Note: This method mixes coordinate systems. `getBoundingClientRect` is viewport-relative,
   * while `window.scrollY` is for document scrolling and `window.screenX` is for the
   * screen position of the browser window. For use with `@web/test-runner-commands`'s `sendMouse`,
   * which expects viewport-relative coordinates, the calculation should likely be:
   * `x: Math.floor(x + width / 2)` and `y: Math.floor(y + height / 2)`.
   *
   * @param element The element to find the center of.
   * @returns A `Point` object with the x and y coordinates of the element's center.
   */
  static getMiddleOfElement(element: Element): Point {
    const { x, y, width, height } = element.getBoundingClientRect()

    return {
      x: Math.floor(x + window.screenX + width / 2),
      y: Math.floor(y + window.scrollY + height / 2),
    }
  }

  /**
   * Moves the mouse from a starting point to an ending point in a series of steps.
   * This uses the `@web/test-runner-commands` `sendMouse` API, which means
   * the mouse will actually move in the browser running the tests.
   *
   * @param from The starting {x, y} coordinates.
   * @param to The ending {x, y} coordinates.
   * @param steps The number of intermediate steps to take during the move. Defaults to 10.
   */
  static async moveMouse(from: Point, to: Point, steps = 10): Promise<void> {
    const dx = to.x - from.x
    const dy = to.y - from.y
    const ix = Math.round(dx / steps)
    const iy = Math.round(dy / steps)
    let currentX = from.x
    let currentY = from.y

    await sendMouse({ type: 'move', position: [from.x, from.y] })
    // await executeServerCommand('take-screenshot', { name: 'move-start' });
    for (let i = 0; i < steps; i++) {
      currentX += ix
      currentY += iy
      await nextFrame()
      await sendMouse({ type: 'move', position: [currentX, currentY] })
      // await executeServerCommand('take-screenshot', { name: `pos${i}` });
    }

    // just making sure that we are not off
    await sendMouse({ type: 'move', position: [to.x, to.y] })
    // await executeServerCommand('take-screenshot', { name: `move-end` });
  }

  /**
   * Performs a drag-and-drop operation from a source element to a target element.
   * It simulates moving to the source, pressing the mouse down, moving to the target,
   * and releasing the mouse.
   *
   * @param source The HTMLElement to drag.
   * @param target The HTMLElement to drop onto.
   * @param steps The number of mouse moves to simulate during the drag. Defaults to 10.
   */
  static async dragAndDrop(source: HTMLElement, target: HTMLElement, steps = 10): Promise<void> {
    const from = UiMock.getMiddleOfElement(source)
    const to = UiMock.getMiddleOfElement(target)

    await sendMouse({ type: 'move', position: [from.x, from.y] })
    await sendMouse({ type: 'down' })
    // await executeServerCommand('take-screenshot', { name: `after-down` });
    await nextFrame()
    await UiMock.moveMouse(from, to, steps)
    await sendMouse({ type: 'up' })
    // await executeServerCommand('take-screenshot', { name: `after-up` });
  }

  /**
   * Creates a `DragEvent` for simulating file drags (e.g., for a file drop zone).
   *
   * @param type The type of the drag event (e.g., 'dragenter', 'dragover', 'drop').
   * @param opts Options, including an optional `File` object to include in the event.
   * If no file is provided, a default 'test.txt' file is created.
   * @returns A new `DragEvent` instance.
   *
   * @example
   * const myFile = new File(['(⌐□_□)'], 'chucknorris.png', { type: 'image/png' });
   * const dropEvent = UiMock.getFileDragEvent('drop', { file: myFile });
   * dropZone.dispatchEvent(dropEvent);
   */
  static getFileDragEvent(type: string, opts: { file?: File } = {}): DragEvent {
    let file: File
    if (opts.file) {
      file = opts.file
    } else {
      file = new File(['test'], 'test.txt', { type: 'text/plain' })
    }
    const dt = new DataTransfer()
    dt.items.add(file)
    return new DragEvent(type, {
      dataTransfer: dt,
      cancelable: true,
      composed: true,
      bubbles: true,
    })
  }
}
