import { describe, it, expect, vi, beforeEach } from "vitest";
import { pw_drag_drop } from "./pw_drag_drop";
import type { Locator, Page } from "playwright";

// --- Mock factories ---

function makeLocator(bbox: { x: number; y: number; width: number; height: number } | null): Locator {
  return {
    boundingBox: vi.fn().mockResolvedValue(bbox),
  } as unknown as Locator;
}

function makeFirstLocator(bbox: { x: number; y: number; width: number; height: number } | null): Locator {
  const loc = makeLocator(bbox);
  return loc;
}

function makePage(
  sourceBbox: { x: number; y: number; width: number; height: number } | null,
  targetBbox: { x: number; y: number; width: number; height: number } | null,
): Page {
  const moveStub = vi.fn().mockResolvedValue(undefined);
  const downStub = vi.fn().mockResolvedValue(undefined);
  const upStub = vi.fn().mockResolvedValue(undefined);
  const waitStub = vi.fn().mockResolvedValue(undefined);

  // locator() returns an object with .first() that returns the locator mock
  const locatorFactory = (bbox: typeof sourceBbox) => {
    const inner = makeFirstLocator(bbox);
    return { first: vi.fn().mockReturnValue(inner) };
  };

  let callCount = 0;
  const locatorMock = vi.fn().mockImplementation(() => {
    callCount++;
    if (callCount === 1) return locatorFactory(sourceBbox);
    return locatorFactory(targetBbox);
  });

  return {
    mouse: {
      move: moveStub,
      down: downStub,
      up: upStub,
    },
    locator: locatorMock,
    waitForTimeout: waitStub,
  } as unknown as Page;
}

// --- Tests ---

describe("pw_drag_drop", () => {
  describe("happy path", () => {
    it("calls mouse.down once, mouse.up once, mouse.move at least steps+2 times, waitForTimeout steps times when hoverMs=0", async () => {
      const steps = 5;
      const sourceBbox = { x: 10, y: 20, width: 100, height: 50 };
      const targetBbox = { x: 300, y: 20, width: 100, height: 50 };

      const page = makePage(sourceBbox, targetBbox);
      const sourceLocator = makeLocator(sourceBbox);
      const targetLocator = makeLocator(targetBbox);

      await pw_drag_drop(page, {
        source: sourceLocator,
        target: targetLocator,
        steps,
        delayMs: 0,
        hoverMs: 0,
      });

      expect((page.mouse.down as ReturnType<typeof vi.fn>).mock.calls.length).toBe(1);
      expect((page.mouse.up as ReturnType<typeof vi.fn>).mock.calls.length).toBe(1);

      // move(sx,sy) + move(activate, {steps:2}) + steps moves = at least steps + 2 calls
      const moveCalls = (page.mouse.move as ReturnType<typeof vi.fn>).mock.calls.length;
      expect(moveCalls).toBeGreaterThanOrEqual(steps + 2);

      // waitForTimeout called exactly `steps` times (no hoverMs)
      const waitCalls = (page.waitForTimeout as ReturnType<typeof vi.fn>).mock.calls.length;
      expect(waitCalls).toBe(steps);
    });
  });

  describe("no source bbox throws", () => {
    it("throws when source.boundingBox returns null", async () => {
      const page = makePage(null, { x: 300, y: 20, width: 100, height: 50 });
      const sourceLocator = makeLocator(null);
      const targetLocator = makeLocator({ x: 300, y: 20, width: 100, height: 50 });

      await expect(
        pw_drag_drop(page, { source: sourceLocator, target: targetLocator }),
      ).rejects.toThrow("source element has no bounding box");
    });
  });

  describe("no target bbox throws", () => {
    it("throws when target.boundingBox returns null", async () => {
      const page = makePage({ x: 10, y: 20, width: 100, height: 50 }, null);
      const sourceLocator = makeLocator({ x: 10, y: 20, width: 100, height: 50 });
      const targetLocator = makeLocator(null);

      await expect(
        pw_drag_drop(page, { source: sourceLocator, target: targetLocator }),
      ).rejects.toThrow("target element has no bounding box");
    });
  });

  describe("hoverMs adds wait", () => {
    it("calls waitForTimeout steps+1 times when hoverMs>0", async () => {
      const steps = 4;
      const sourceBbox = { x: 10, y: 20, width: 100, height: 50 };
      const targetBbox = { x: 300, y: 20, width: 100, height: 50 };

      const page = makePage(sourceBbox, targetBbox);
      const sourceLocator = makeLocator(sourceBbox);
      const targetLocator = makeLocator(targetBbox);

      await pw_drag_drop(page, {
        source: sourceLocator,
        target: targetLocator,
        steps,
        delayMs: 0,
        hoverMs: 400,
      });

      const waitCalls = (page.waitForTimeout as ReturnType<typeof vi.fn>).mock.calls.length;
      // steps waits for delayMs + 1 wait for hoverMs
      expect(waitCalls).toBe(steps + 1);
    });
  });

  describe("string selectors resolve", () => {
    it("calls page.locator(selector).first() when source/target are strings", async () => {
      const sourceBbox = { x: 10, y: 20, width: 100, height: 50 };
      const targetBbox = { x: 300, y: 20, width: 100, height: 50 };

      const page = makePage(sourceBbox, targetBbox);

      await pw_drag_drop(page, {
        source: "[data-card-id='abc']",
        target: "[data-column-id='xyz']",
        steps: 2,
        delayMs: 0,
      });

      const locatorCalls = (page.locator as ReturnType<typeof vi.fn>).mock.calls;
      expect(locatorCalls.length).toBe(2);
      expect(locatorCalls[0]?.[0]).toBe("[data-card-id='abc']");
      expect(locatorCalls[1]?.[0]).toBe("[data-column-id='xyz']");
    });
  });
});
