import { UniDriver } from '@wix/wix-ui-test-utils/unidriver';
import { baseUniDriverFactory, isSled2, isSled3 } from '../../unidriver';
import { SledPage } from '../../sledPage';

// Playwright Locator shape we depend on for sled-3 dispatch. Typed locally to
// avoid pulling Playwright types into cairo's runtime surface.
type PlaywrightLocatorLike = {
  boundingBox: () => Promise<{
    x: number;
    y: number;
    width: number;
    height: number;
  } | null>;
  page: () => PlaywrightPageLike;
};

type PlaywrightPageLike = {
  mouse: {
    move: (
      x: number,
      y: number,
      options?: { steps?: number },
    ) => Promise<void>;
    down: () => Promise<void>;
    up: () => Promise<void>;
  };
};

// Both sled-2 (Puppeteer) and sled-3 (Playwright) Page objects expose `.mouse`
// with `.move/down/up`. The cairo DnD driver only needs that shape.
type AnyDriverPage = SledPage['page'] | PlaywrightPageLike;

const { fireEvent = null } =
  process.env.NODE_ENV === 'test'
    ? (() => {
        try {
          return require('@testing-library/react');
        } catch {}
        return null;
      })() ||
      (() => {
        try {
          // eslint-disable-next-line import/no-extraneous-dependencies
          return require('@testing-library/dom');
        } catch {}
        return null;
      })()
    : {};

export function DragAndDropUniDriver(base: UniDriver) {
  return {
    ...baseUniDriverFactory(base),
    escapeDrag: async (
      source: UniDriver,
      { key: code = 'Escape' }: { key?: string } = {},
    ) => {
      const sourceNative = await source.getNative();

      const { page } = sourceNative as { page?: SledPage['page'] };

      if (page?.evaluate) {
      } else if (process.env.NODE_ENV === 'test') {
        fireEvent.keyDown(sourceNative, {
          code,
        });
      }
    },
    startKeyboardDrag: async (source: UniDriver) => {
      const sourceNative = await source.getNative();

      const { page } = sourceNative as { page?: SledPage['page'] };

      if (page?.evaluate) {
      } else if (fireEvent) {
        fireEvent.keyDown(sourceNative, {
          code: 'Enter',
        });
      }
    },
    dragOverKeyboard: async (target: UniDriver) => {
      const targetNative = await target.getNative();

      const { page } = targetNative as { page?: SledPage['page'] };

      if (page?.evaluate) {
      } else if (fireEvent) {
        fireEvent.keyDown(targetNative, {
          code: 'ArrowDown',
        });
      }
    },
    endKeyboardDrag: async (target: UniDriver) => {
      const targetNative = await target.getNative();

      const { page } = targetNative as { page?: SledPage['page'] };

      if (page?.evaluate) {
      } else if (fireEvent) {
        return fireEvent.keyDown(targetNative, {
          code: 'Enter',
        });
      }
    },
    dragStart: async (
      source: UniDriver,
      options: { page?: AnyDriverPage; x?: number; y?: number } = {},
    ) => {
      const sourceNative = await source.getNative();

      if (isSled2(sourceNative)) {
        const page: SledPage['page'] =
          (options.page as SledPage['page']) ?? sourceNative.page;
        const sourceBox = await sourceNative.element.boundingBox();

        await page.mouse.move(
          options.x ?? sourceBox.x + sourceBox.width / 2,
          options.y ?? sourceBox.y + sourceBox.height / 2,
        );

        await page.mouse.down();
      } else if (isSled3(sourceNative)) {
        const locator = sourceNative as PlaywrightLocatorLike;
        const page: PlaywrightPageLike =
          (options.page as PlaywrightPageLike) ?? locator.page();
        const sourceBox = await locator.boundingBox();
        if (!sourceBox) {
          throw new Error(
            'DragAndDropUniDriver.dragStart: source.boundingBox() returned null (element detached, zero-size, or off-screen). Drag cannot start.',
          );
        }

        await page.mouse.move(
          options.x ?? sourceBox.x + sourceBox.width / 2,
          options.y ?? sourceBox.y + sourceBox.height / 2,
        );

        await page.mouse.down();
      } else if (fireEvent) {
        fireEvent.pointerDown(sourceNative);
      }
    },
    dragMove: async () => {
      await document.dispatchEvent(new Event('mousemove'));
    },
    dragEnd: async (
      target: UniDriver,
      options: { page?: AnyDriverPage } = {},
    ) => {
      const targetNative = await target.getNative();

      if (isSled2(targetNative)) {
        const page: SledPage['page'] =
          (options.page as SledPage['page']) ?? targetNative.page;
        await page.mouse.up();
      } else if (isSled3(targetNative)) {
        const locator = targetNative as PlaywrightLocatorLike;
        const page: PlaywrightPageLike =
          (options.page as PlaywrightPageLike) ?? locator.page();
        await page.mouse.up();
      } else if (fireEvent) {
        fireEvent.pointerUp(targetNative);
        await document.dispatchEvent(new Event('mouseup'));
      }
    },
    dragOver: async (
      source: UniDriver,
      target: UniDriver,
      {
        dragPointRatio = { x: 2, y: 2 },
        dropPointRatio = { x: 2, y: 2 },
        steps = 3,
      }: {
        dragPointRatio?: { x: number; y: number };
        dropPointRatio?: { x: number; y: number };
        steps?: number;
      } = {},
    ) => {
      const sourceRow = await source.getNative();
      const targetRow = await target.getNative();

      if (isSled2(sourceRow)) {
        const { page } = sourceRow as { page: SledPage['page'] };

        await source.hover();

        const sourceBox = await sourceRow.element.boundingBox();
        const targetBox = await targetRow.element.boundingBox();

        await page.mouse.move(
          sourceBox.x + sourceBox.width / dragPointRatio.x,
          sourceBox.y + sourceBox.height / dragPointRatio.y,
        );

        await page.mouse.down();
        await new Promise((resolve) => setTimeout(resolve, 100));

        await page.mouse.move(
          targetBox.x + targetBox.width / dropPointRatio.x,
          targetBox.y + targetBox.height / dropPointRatio.y,
          { steps },
        );

        await new Promise((resolve) => setTimeout(resolve, 100));
        await page.mouse.up();
        await new Promise((resolve) => setTimeout(resolve, 100)); // wait for drop animation to complete
      } else if (isSled3(sourceRow)) {
        const sourceLocator = sourceRow as PlaywrightLocatorLike;
        const targetLocator = targetRow as PlaywrightLocatorLike;
        const page = sourceLocator.page();

        await source.hover();

        const sourceBox = await sourceLocator.boundingBox();
        const targetBox = await targetLocator.boundingBox();
        if (!sourceBox || !targetBox) {
          throw new Error(
            `DragAndDropUniDriver.dragOver: ${!sourceBox ? 'source' : 'target'}.boundingBox() returned null (element detached, zero-size, or off-screen). Drag cannot proceed.`,
          );
        }

        await page.mouse.move(
          sourceBox.x + sourceBox.width / dragPointRatio.x,
          sourceBox.y + sourceBox.height / dragPointRatio.y,
        );

        await page.mouse.down();
        await new Promise((resolve) => setTimeout(resolve, 100));

        await page.mouse.move(
          targetBox.x + targetBox.width / dropPointRatio.x,
          targetBox.y + targetBox.height / dropPointRatio.y,
          { steps },
        );

        await new Promise((resolve) => setTimeout(resolve, 100));
        await page.mouse.up();
        await new Promise((resolve) => setTimeout(resolve, 100));
      } else if (fireEvent) {
        fireEvent.pointerDown(sourceRow);
        document.dispatchEvent(new Event('mousemove'));
        fireEvent.pointerUp(targetRow);
      }
    },
    moveOver: async (
      target: UniDriver,
      options: {
        page?: AnyDriverPage;
        pixels?: number;
        delta?: { x?: number };
      } = {},
    ) => {
      const targetRow = await target.getNative();
      const pixels = options.pixels ?? 0;

      if (isSled2(targetRow)) {
        const page: SledPage['page'] =
          (options.page as SledPage['page']) ?? targetRow.page;
        const targetBox = await targetRow.element.boundingBox();

        for (let i = 0; i < 2; i++) {
          await page.mouse.move(
            Math.max(options.delta?.x ?? targetBox.x - pixels, 0),
            Math.max(targetBox.y + targetBox.height - pixels, 0),
          );
        }
      } else if (isSled3(targetRow)) {
        const locator = targetRow as PlaywrightLocatorLike;
        const page: PlaywrightPageLike =
          (options.page as PlaywrightPageLike) ?? locator.page();
        const targetBox = await locator.boundingBox();
        if (!targetBox) {
          throw new Error(
            'DragAndDropUniDriver.moveOver: target.boundingBox() returned null (element detached, zero-size, or off-screen).',
          );
        }

        for (let i = 0; i < 2; i++) {
          await page.mouse.move(
            Math.max(options.delta?.x ?? targetBox.x - pixels, 0),
            Math.max(targetBox.y + targetBox.height - pixels, 0),
          );
        }
      } else if (fireEvent) {
        fireEvent.mouseMove(targetRow, {
          clientX: options.delta?.x ?? 0,
        });
      }
    },

    moveAny: async (
      source: UniDriver,
      options?: {
        page?: AnyDriverPage;
        x?: number;
        y?: number;
        steps?: number;
        delay?: number;
        vertical?: boolean;
      },
    ) => {
      const sourceNative = await source.getNative();

      if (isSled2(sourceNative)) {
        const page: SledPage['page'] =
          (options?.page as SledPage['page']) ?? sourceNative.page;
        const sourceBox = await sourceNative.element.boundingBox();
        const steps = options?.steps ?? 3;
        for (let i = steps; i >= 0; i--) {
          await page.mouse.move(
            (options?.x ?? sourceBox.x + sourceBox.width / 2) - i * 10,
            (options?.y ?? sourceBox.y + sourceBox.height / 2) -
              (options?.vertical ? i * 10 : 0),
          );
          await new Promise((resolve) =>
            setTimeout(resolve, options?.delay ?? 2000),
          );
        }
      } else if (isSled3(sourceNative)) {
        const locator = sourceNative as PlaywrightLocatorLike;
        const page: PlaywrightPageLike =
          (options?.page as PlaywrightPageLike) ?? locator.page();
        const sourceBox = await locator.boundingBox();
        if (!sourceBox) {
          throw new Error(
            'DragAndDropUniDriver.moveAny: source.boundingBox() returned null (element detached, zero-size, or off-screen).',
          );
        }
        const steps = options?.steps ?? 3;
        for (let i = steps; i >= 0; i--) {
          await page.mouse.move(
            (options?.x ?? sourceBox.x + sourceBox.width / 2) - i * 10,
            (options?.y ?? sourceBox.y + sourceBox.height / 2) -
              (options?.vertical ? i * 10 : 0),
          );
          await new Promise((resolve) =>
            setTimeout(resolve, options?.delay ?? 2000),
          );
        }
      }
    },
  };
}
