/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this file,
 * You can obtain one at https://mozilla.org/MPL/2.0/. */

import { act, renderHook } from "@testing-library/react";
import { useWidgetDnD } from "content-src/components/Widgets/useWidgetDnD.jsx";
import { cursorToSlot } from "content-src/components/Widgets/useMouseDnD.jsx";

const DEFAULT_ORDER = [
  "lists",
  "focusTimer",
  "weather",
  "sportsWidget",
  "clocks",
];

function makeProps(overrides = {}) {
  return {
    widgetOrder: DEFAULT_ORDER,
    prefs: {},
    dispatch: jest.fn(),
    ...overrides,
  };
}

function setup(overrides = {}) {
  const props = makeProps(overrides);
  const { result } = renderHook(() => useWidgetDnD(props));
  return { result, dispatch: props.dispatch, props };
}

function dataTransferStub(types = ["text/widget-id"], data = {}) {
  return {
    types,
    setData: jest.fn((k, v) => {
      data[k] = v;
    }),
    getData: jest.fn(k => data[k] || ""),
    setDragImage: jest.fn(),
    effectAllowed: "",
    dropEffect: "",
  };
}

function makeArticle(widgetId) {
  const el = document.createElement("article");
  el.setAttribute("data-widget-id", widgetId);
  return el;
}

function dragEvent(opts = {}) {
  const types = opts.types ?? ["text/widget-id"];
  const data = opts.data ?? { "text/widget-id": opts.sourceId || "" };
  const target = opts.target || opts.currentTarget;
  const currentTarget = opts.currentTarget || opts.target;
  return {
    dataTransfer: dataTransferStub(types, data),
    clientX: opts.clientX ?? 0,
    clientY: opts.clientY ?? 0,
    target,
    currentTarget,
    preventDefault: jest.fn(),
    stopPropagation: jest.fn(),
  };
}

describe("cursorToSlot", () => {
  const slotRects = [
    { left: 0, right: 200, top: 0, bottom: 100, width: 200, height: 100 },
    { left: 200, right: 400, top: 0, bottom: 100, width: 200, height: 100 },
    { left: 400, right: 600, top: 0, bottom: 100, width: 200, height: 100 },
  ];

  it("returns the slot whose rect contains the cursor", () => {
    expect(cursorToSlot(slotRects, 100, 50)).toBe(0);
    expect(cursorToSlot(slotRects, 300, 50)).toBe(1);
    expect(cursorToSlot(slotRects, 500, 50)).toBe(2);
  });

  it("returns null when the cursor is outside every rect (gaps/empty regions)", () => {
    // Above all rects
    expect(cursorToSlot(slotRects, 100, -10)).toBe(null);
    // Below all rects
    expect(cursorToSlot(slotRects, 100, 200)).toBe(null);
    // Past the rightmost rect
    expect(cursorToSlot(slotRects, 700, 50)).toBe(null);
  });

  it("treats rect edges as inclusive (right and bottom edges hit the slot)", () => {
    expect(cursorToSlot(slotRects, 200, 50)).toBe(0); // first match wins on shared edge
    expect(cursorToSlot(slotRects, 100, 100)).toBe(0);
  });

  it("returns null when slotRects is null (no in-flight drag)", () => {
    expect(cursorToSlot(null, 100, 50)).toBe(null);
  });

  it("skips holes in slotRects", () => {
    const rectsWithHole = [
      { left: 0, right: 200, top: 0, bottom: 100, width: 200, height: 100 },
      null,
      { left: 400, right: 600, top: 0, bottom: 100, width: 200, height: 100 },
    ];
    expect(cursorToSlot(rectsWithHole, 100, 50)).toBe(0);
    expect(cursorToSlot(rectsWithHole, 300, 50)).toBe(null); // hole, no match
    expect(cursorToSlot(rectsWithHole, 500, 50)).toBe(2);
  });
});

describe("useWidgetDnD - mouse drag", () => {
  it("foreign drop (no text/widget-id) does not dispatch", () => {
    const { result, dispatch } = setup();
    const e = dragEvent({ types: ["text/plain"] });
    act(() => {
      result.current.handleDrop(e);
    });
    expect(dispatch).not.toHaveBeenCalled();
    expect(e.preventDefault).not.toHaveBeenCalled();
  });

  it("drop without an active drag does not dispatch", () => {
    const { result, dispatch } = setup();
    const e = dragEvent({
      types: ["text/widget-id"],
      data: { "text/widget-id": "lists" },
    });
    // No prior dragstart — sourceIdx is fine but targetSlot is null.
    act(() => {
      result.current.handleDrop(e);
    });
    expect(dispatch).not.toHaveBeenCalled();
  });

  it("foreign dragover (no text/widget-id) is left alone", () => {
    const { result } = setup();
    const e = dragEvent({ types: ["text/plain"] });
    act(() => {
      result.current.handleDragOver(e);
    });
    expect(e.preventDefault).not.toHaveBeenCalled();
    expect(e.stopPropagation).not.toHaveBeenCalled();
  });
});

describe("useWidgetDnD - interactive descendant guard", () => {
  it("aborts the drag when mousedown was on a button inside the widget", () => {
    const { result } = setup();
    const article = makeArticle("lists");
    const button = document.createElement("button");
    article.appendChild(button);
    document.body.appendChild(article);

    act(() => {
      result.current.handleMouseDown({ target: button });
    });
    const e = dragEvent({ currentTarget: article, target: article });
    act(() => {
      result.current.handleDragStart(e, "lists");
    });
    expect(e.preventDefault).toHaveBeenCalled();
    expect(result.current.draggedId).toBe(null);

    document.body.removeChild(article);
  });

  it("allows the drag when mousedown was on a non-interactive area", () => {
    const { result } = setup();
    const article = makeArticle("lists");
    const inner = document.createElement("div");
    article.appendChild(inner);
    document.body.appendChild(article);

    act(() => {
      result.current.handleMouseDown({ target: inner });
    });
    const e = dragEvent({ currentTarget: article, target: article });
    act(() => {
      result.current.handleDragStart(e, "lists");
    });
    expect(e.preventDefault).not.toHaveBeenCalled();
    expect(result.current.draggedId).toBe("lists");

    document.body.removeChild(article);
  });

  it("aborts the drag when mousedown was inside an open dialog (modal should not drag the widget)", () => {
    const { result } = setup();
    const article = makeArticle("sportsWidget");
    const dialog = document.createElement("dialog");
    dialog.setAttribute("open", "");
    const dialogContent = document.createElement("div");
    dialog.appendChild(dialogContent);
    article.appendChild(dialog);
    document.body.appendChild(article);

    act(() => {
      result.current.handleMouseDown({ target: dialogContent });
    });
    const e = dragEvent({ currentTarget: article, target: article });
    act(() => {
      result.current.handleDragStart(e, "sportsWidget");
    });
    expect(e.preventDefault).toHaveBeenCalled();
    expect(result.current.draggedId).toBe(null);

    document.body.removeChild(article);
  });

  it("aborts the drag when mousedown was directly on the dialog backdrop", () => {
    const { result } = setup();
    const article = makeArticle("sportsWidget");
    const dialog = document.createElement("dialog");
    dialog.setAttribute("open", "");
    article.appendChild(dialog);
    document.body.appendChild(article);

    act(() => {
      result.current.handleMouseDown({ target: dialog });
    });
    const e = dragEvent({ currentTarget: article, target: article });
    act(() => {
      result.current.handleDragStart(e, "sportsWidget");
    });
    expect(e.preventDefault).toHaveBeenCalled();
    expect(result.current.draggedId).toBe(null);

    document.body.removeChild(article);
  });

  it("does not abort the drag when mousedown was on an anchor (anchors should drag the widget)", () => {
    const { result } = setup();
    const article = makeArticle("weather");
    const anchor = document.createElement("a");
    article.appendChild(anchor);
    document.body.appendChild(article);

    act(() => {
      result.current.handleMouseDown({ target: anchor });
    });
    const e = dragEvent({ currentTarget: article, target: article });
    act(() => {
      result.current.handleDragStart(e, "weather");
    });
    expect(e.preventDefault).not.toHaveBeenCalled();
    expect(result.current.draggedId).toBe("weather");

    document.body.removeChild(article);
  });
});
