import type { Locator, Page } from "playwright";

/** Options for pw_drag_drop. */
export interface PwDragDropOptions {
  /** Selector string or Playwright Locator for the element to drag from. */
  source: string | Locator;
  /** Selector string or Playwright Locator for the drop target. */
  target: string | Locator;
  /** Number of intermediate mousemove steps from source to target. Default: 20. */
  steps?: number;
  /** Pause between each step in ms. Default: 16. */
  delayMs?: number;
  /** Pause over the target before releasing in ms. Useful for time-based dropzones. Default: 0. */
  hoverMs?: number;
  /** Initial offset after mousedown to cross dnd-kit's activation threshold (default 8px). Default: {x: 12, y: 0}. */
  activateOffset?: { x: number; y: number };
}

/**
 * pw_drag_drop — simulates a human pointer drag compatible with dnd-kit.
 *
 * Playwright's built-in dragTo uses HTML5 drag events which dnd-kit ignores
 * (it listens to PointerEvents). This helper uses raw mouse API with a
 * small activation move to cross dnd-kit's default 8px threshold, then
 * interpolates in configurable steps toward the target.
 */
export async function pw_drag_drop(page: Page, opts: PwDragDropOptions): Promise<void> {
  const steps = opts.steps ?? 20;
  const delayMs = opts.delayMs ?? 16;
  const hoverMs = opts.hoverMs ?? 0;
  const activateOffset = opts.activateOffset ?? { x: 12, y: 0 };

  // Resolve source and target to Locators.
  const sourceLocator: Locator =
    typeof opts.source === "string" ? page.locator(opts.source).first() : opts.source;
  const targetLocator: Locator =
    typeof opts.target === "string" ? page.locator(opts.target).first() : opts.target;

  // Get bounding boxes.
  const sourceBbox = await sourceLocator.boundingBox();
  if (sourceBbox === null) {
    throw new Error("pw_drag_drop: source element has no bounding box (not visible or detached)");
  }

  const targetBbox = await targetLocator.boundingBox();
  if (targetBbox === null) {
    throw new Error("pw_drag_drop: target element has no bounding box (not visible or detached)");
  }

  // Compute centers.
  const sx = sourceBbox.x + sourceBbox.width / 2;
  const sy = sourceBbox.y + sourceBbox.height / 2;
  const tx = targetBbox.x + targetBbox.width / 2;
  const ty = targetBbox.y + targetBbox.height / 2;

  // Move to source and press down.
  await page.mouse.move(sx, sy);
  await page.mouse.down();

  // Small move to cross dnd-kit activation threshold (default 8px).
  await page.mouse.move(sx + activateOffset.x, sy + activateOffset.y, { steps: 2 });

  // Interpolate from activation position toward target.
  const startX = sx + activateOffset.x;
  const startY = sy + activateOffset.y;
  for (let i = 1; i <= steps; i++) {
    const t = i / steps;
    const xi = startX + (tx - startX) * t;
    const yi = startY + (ty - startY) * t;
    await page.mouse.move(xi, yi);
    await page.waitForTimeout(delayMs);
  }

  // Optional hover pause before release (useful for sidebar auto-open, etc.).
  if (hoverMs > 0) {
    await page.waitForTimeout(hoverMs);
  }

  // Release.
  await page.mouse.up();
}
