import { test, expect } from "@playwright/test";
import { pw_kanban_login } from "../../../../frontend/functions/browser/pw_kanban_login";
import { pw_drag_drop } from "../../../../frontend/functions/browser/pw_drag_drop";
import { pw_wait_predicate } from "../../../../frontend/functions/browser/pw_wait_predicate";

const USER = process.env.KANBAN_USER || "e2e_user";
const PWD = process.env.KANBAN_PWD || "e2e_test_pw_2026";

interface BoardColumn {
  id: string;
  name: string;
  location: "board" | "sidebar";
  position: number;
}

interface BoardCard {
  id: string;
  column_id: string;
  title: string;
}

interface BoardResponse {
  columns: BoardColumn[];
  cards: BoardCard[];
}

test.describe("Issue 0091 — sidebar drag dropzone", () => {
  test("drag near left edge opens sidebar and drop moves card to sidebar column", async ({ page }) => {
    await page.goto("/");
    await pw_kanban_login(page, { username: USER, password: PWD });

    // Pre-req: ensure there is at least one sidebar column and a card on the board.
    const initialBoard: BoardResponse = await page.request
      .get("/api/board")
      .then((r) => r.json());

    let sidebarCol = initialBoard.columns.find((c) => c.location === "sidebar");
    if (!sidebarCol) {
      const created = await page.request
        .post("/api/columns", {
          data: { name: "E2E Sidebar", location: "sidebar" },
        })
        .then((r) => r.json());
      sidebarCol = created as BoardColumn;
    }

    const boardCol = initialBoard.columns.find((c) => c.location !== "sidebar");
    if (!boardCol) {
      test.skip(true, "no board column to drag a card from");
      return;
    }

    // Ensure at least one card exists in a board column we can drag.
    let card = initialBoard.cards.find((c) => c.column_id === boardCol.id);
    if (!card) {
      const created = await page.request
        .post("/api/cards", {
          data: {
            column_id: boardCol.id,
            title: `e2e dropzone card ${Date.now()}`,
            requester: "e2e",
          },
        })
        .then((r) => r.json());
      card = created as BoardCard;
      // Reload UI so the new card appears.
      await page.reload();
      await page.waitForLoadState("networkidle");
    }

    // Sanity: side bar should start closed. The toggle button has aria-label="Toggle sidebar".
    const toggleBtn = page.locator('button[aria-label="Toggle sidebar"]');
    await expect(toggleBtn).toBeVisible();

    // The Mantine Navbar has a known data attribute (data-mantine-component=AppShellNavbar)
    // but the simplest check is: when collapsed, the desktop navbar is hidden via display:none.
    // We use the strip element's visibility too.
    const strip = page.locator('[data-test="kanban-drag-edge"]');
    await expect(strip).toHaveCount(1);
    // While not dragging, strip is_active=0.
    await expect(strip).toHaveAttribute("data-active", "0");

    const cardLocator = page.locator(`[data-card-id="${card!.id}"]`);
    await expect(cardLocator).toBeVisible();

    // Build a "left edge" target by creating a 1x100 box near x=10 to drop on.
    // pw_drag_drop expects a Locator for the target; we use the strip itself
    // even though pointer-events:none — page.mouse.move works against the
    // viewport so its bounding box only drives where the pointer goes.
    // We override hoverMs=700 so the 400ms timer fires well within the hover.

    // Get the card bounding box.
    const cardBox = await cardLocator.boundingBox();
    if (!cardBox) throw new Error("card has no bounding box");

    // Manually drive the pointer: press down on card, drag to x=10, dwell 700ms,
    // assert sidebar opened (via predicate on toggle button aria-pressed OR the
    // strip's data-active attribute observed), then drop on sidebar column.
    const sx = cardBox.x + cardBox.width / 2;
    const sy = cardBox.y + cardBox.height / 2;

    await page.mouse.move(sx, sy);
    await page.mouse.down();
    // Cross dnd-kit's 5px activation threshold (we configured PointerSensor distance:5).
    await page.mouse.move(sx + 15, sy, { steps: 4 });

    // Glide towards x=10 (inside the 32px strip).
    const edgeX = 10;
    const edgeY = sy; // keep vertical, change horizontal.
    const steps = 25;
    for (let i = 1; i <= steps; i++) {
      const t = i / steps;
      const xi = (sx + 15) + (edgeX - (sx + 15)) * t;
      const yi = sy + (edgeY - sy) * t;
      await page.mouse.move(xi, yi);
      await page.waitForTimeout(16);
    }

    // Now dwell inside the strip — the 400ms timer should fire.
    // While dwelling, every ~50ms we nudge the mouse 1px to keep dnd-kit pointer events alive
    // but stay inside the strip.
    const dwellMs = 700;
    const nudgeStart = Date.now();
    while (Date.now() - nudgeStart < dwellMs) {
      await page.mouse.move(edgeX + ((Date.now() / 50) % 2), edgeY);
      await page.waitForTimeout(50);
    }

    // Assert: the strip is now armed AND the sidebar opened.
    await expect(strip).toHaveAttribute("data-armed", "1");

    // Wait for sidebar column header text to appear (sidebar opened).
    await pw_wait_predicate(
      page,
      (sidebarName: string) => {
        const els = Array.from(document.querySelectorAll('[data-column-location="sidebar"]'));
        // Element must be visible (offsetParent != null is a good proxy for display!=none).
        return els.some((el) => (el as HTMLElement).offsetParent !== null);
      },
      {
        arg: sidebarCol!.name,
        timeoutMs: 3000,
        pollMs: 100,
        message: "sidebar column did not become visible after dwell",
      }
    );

    // Now move pointer to the sidebar column and release.
    const sidebarColLoc = page.locator(`[data-column-id="${sidebarCol!.id}"]`).first();
    await expect(sidebarColLoc).toBeVisible();
    const sbBox = await sidebarColLoc.boundingBox();
    if (!sbBox) throw new Error("sidebar column has no bounding box");
    const tx = sbBox.x + sbBox.width / 2;
    const ty = sbBox.y + sbBox.height / 2;
    const dropSteps = 15;
    let lastX = edgeX;
    let lastY = edgeY;
    for (let i = 1; i <= dropSteps; i++) {
      const t = i / dropSteps;
      const xi = lastX + (tx - lastX) * t;
      const yi = lastY + (ty - lastY) * t;
      await page.mouse.move(xi, yi);
      await page.waitForTimeout(20);
    }
    await page.waitForTimeout(150);
    await page.mouse.up();

    // Validate via API the card moved to the sidebar column.
    await pw_wait_predicate(
      page,
      async (args: { id: string; col: string }) => {
        const res = await fetch("/api/board", { credentials: "same-origin" });
        const b = await res.json();
        const c = (b.cards as BoardCard[]).find((x) => x.id === args.id);
        return c?.column_id === args.col;
      },
      {
        arg: { id: card!.id, col: sidebarCol!.id },
        timeoutMs: 5000,
        pollMs: 200,
        message: "card did not land in sidebar column after drop",
      }
    );
  });

  test("strip stays inactive when there is no drag", async ({ page }) => {
    await page.goto("/");
    await pw_kanban_login(page, { username: USER, password: PWD });
    const strip = page.locator('[data-test="kanban-drag-edge"]');
    await expect(strip).toHaveCount(1);
    await expect(strip).toHaveAttribute("data-active", "0");
    await expect(strip).toHaveAttribute("data-armed", "0");
    // Move the pointer over the left edge — without a drag, strip must stay disarmed.
    await page.mouse.move(10, 200);
    await page.waitForTimeout(600);
    await expect(strip).toHaveAttribute("data-armed", "0");
  });
});
