/**
 * Tests for useMultiSelectIntersection.
 *
 * The hook is a pure side-effect hook (useEffect only) that computes which
 * allocations/resources fall within a drag-selection rectangle and then calls
 * either setMultiSelectState or clearMultiSelect.
 *
 * Strategy:
 *  - Supply a real canvasRef backed by a jsdom div with child elements that
 *    have the expected data-* attributes.
 *  - Mock getBoundingClientRect on individual row elements to simulate viewport
 *    positions.
 *  - Drive the hook by changing multiSelectState (isSelecting, startX/Y, etc.).
 */
import { describe, expect, it, vi, beforeEach } from "vitest";
import { renderHook } from "@testing-library/react";
import { useRef } from "react";
import { useMultiSelectIntersection } from "./useMultiSelectIntersection.js";
import type { MultiSelectState } from "~/hooks/useTimelineDrag.js";

// ---------------------------------------------------------------------------
// Module mocks
// ---------------------------------------------------------------------------

// The hook imports LABEL_WIDTH from timelineConstants – mock the whole module
// so we can control the value and avoid file-system resolution issues.
vi.mock("~/components/timeline/timelineConstants.js", () => ({
  LABEL_WIDTH: 256,
}));

// ---------------------------------------------------------------------------
// Shared test data / types
// ---------------------------------------------------------------------------

type AllocationLike = {
  id: string;
  startDate: string;
  endDate: string;
};

const CELL_WIDTH = 40;
const LABEL_WIDTH_VALUE = 256;

/** Build a MultiSelectState with sensible defaults. */
function makeState(overrides: Partial<MultiSelectState> = {}): MultiSelectState {
  return {
    isSelecting: false,
    startX: 300,
    startY: 100,
    currentX: 500,
    currentY: 200,
    selectedAllocationIds: [],
    selectedResourceIds: [],
    dateRange: null,
    multiDragDaysDelta: 0,
    isMultiDragging: false,
    multiDragMode: "move",
    ...overrides,
  };
}

/** Create a minimal allocation-like object. */
function makeAlloc(id: string, startDate: string, endDate: string): AllocationLike {
  return { id, startDate, endDate };
}

/**
 * Create a <div> element with data-* attributes and a mocked
 * getBoundingClientRect that returns the given DOMRect values.
 */
function makeRowElement(
  attrs: Record<string, string>,
  rect: { top: number; bottom: number; left: number; right: number },
): HTMLElement {
  const el = document.createElement("div");
  for (const [k, v] of Object.entries(attrs)) {
    el.dataset[k] = v;
  }
  el.getBoundingClientRect = vi.fn(() => ({
    top: rect.top,
    bottom: rect.bottom,
    left: rect.left,
    right: rect.right,
    width: rect.right - rect.left,
    height: rect.bottom - rect.top,
    x: rect.left,
    y: rect.top,
    toJSON: () => ({}),
  }));
  return el;
}

/** toLeft / toWidth helpers that mirror the real timeline calculation. */
const baseDate = new Date("2025-01-01");
function toLeft(d: Date): number {
  const days = Math.floor((d.getTime() - baseDate.getTime()) / 86_400_000);
  return days * CELL_WIDTH;
}
function toWidth(s: Date, e: Date): number {
  const days = Math.ceil((e.getTime() - s.getTime()) / 86_400_000);
  return days * CELL_WIDTH;
}

/** Build a dates array covering n days from baseDate. */
function buildDates(n: number): Date[] {
  return Array.from({ length: n }, (_, i) => {
    const d = new Date(baseDate);
    d.setDate(d.getDate() + i);
    return d;
  });
}

// ---------------------------------------------------------------------------
// Hook wrapper that gives us a stable canvasRef pointing to a live DOM node
// ---------------------------------------------------------------------------

function useCanvasRef() {
  return useRef<HTMLDivElement | null>(document.createElement("div"));
}

// ---------------------------------------------------------------------------
// Tests
// ---------------------------------------------------------------------------

describe("useMultiSelectIntersection", () => {
  let setMultiSelectState: ReturnType<typeof vi.fn>;
  let clearMultiSelect: ReturnType<typeof vi.fn>;
  let canvasDom: HTMLDivElement;

  beforeEach(() => {
    setMultiSelectState = vi.fn();
    clearMultiSelect = vi.fn();
    canvasDom = document.createElement("div");
    // Default bounding rect for the canvas container
    canvasDom.getBoundingClientRect = vi.fn(() => ({
      left: 0,
      top: 0,
      right: 1200,
      bottom: 800,
      width: 1200,
      height: 800,
      x: 0,
      y: 0,
      toJSON: () => ({}),
    }));
  });

  // -------------------------------------------------------------------------
  // Early-exit conditions
  // -------------------------------------------------------------------------

  it("does nothing while isSelecting is true", () => {
    const state = makeState({ isSelecting: true });
    renderHook(() => {
      const canvasRef = useRef<HTMLDivElement | null>(canvasDom);
      useMultiSelectIntersection({
        multiSelectState: state,
        setMultiSelectState,
        clearMultiSelect,
        canvasRef,
        viewMode: "resource",
        resources: [],
        allocsByResource: new Map(),
        projectGroups: [],
        openDemandsByProject: new Map(),
        dates: buildDates(30),
        today: baseDate,
        CELL_WIDTH,
        toLeft,
        toWidth,
      });
    });
    expect(setMultiSelectState).not.toHaveBeenCalled();
    expect(clearMultiSelect).not.toHaveBeenCalled();
  });

  it("does nothing when start coordinates are both 0 (no drag happened)", () => {
    const state = makeState({ isSelecting: false, startX: 0, startY: 0 });
    renderHook(() => {
      const canvasRef = useRef<HTMLDivElement | null>(canvasDom);
      useMultiSelectIntersection({
        multiSelectState: state,
        setMultiSelectState,
        clearMultiSelect,
        canvasRef,
        viewMode: "resource",
        resources: [],
        allocsByResource: new Map(),
        projectGroups: [],
        openDemandsByProject: new Map(),
        dates: buildDates(30),
        today: baseDate,
        CELL_WIDTH,
        toLeft,
        toWidth,
      });
    });
    expect(setMultiSelectState).not.toHaveBeenCalled();
    expect(clearMultiSelect).not.toHaveBeenCalled();
  });

  it("does nothing when selectedAllocationIds is already populated", () => {
    const state = makeState({ selectedAllocationIds: ["existing-alloc"] });
    renderHook(() => {
      const canvasRef = useRef<HTMLDivElement | null>(canvasDom);
      useMultiSelectIntersection({
        multiSelectState: state,
        setMultiSelectState,
        clearMultiSelect,
        canvasRef,
        viewMode: "resource",
        resources: [],
        allocsByResource: new Map(),
        projectGroups: [],
        openDemandsByProject: new Map(),
        dates: buildDates(30),
        today: baseDate,
        CELL_WIDTH,
        toLeft,
        toWidth,
      });
    });
    expect(setMultiSelectState).not.toHaveBeenCalled();
    expect(clearMultiSelect).not.toHaveBeenCalled();
  });

  it("does nothing when selectedResourceIds is already populated", () => {
    const state = makeState({ selectedResourceIds: ["existing-res"] });
    renderHook(() => {
      const canvasRef = useRef<HTMLDivElement | null>(canvasDom);
      useMultiSelectIntersection({
        multiSelectState: state,
        setMultiSelectState,
        clearMultiSelect,
        canvasRef,
        viewMode: "resource",
        resources: [],
        allocsByResource: new Map(),
        projectGroups: [],
        openDemandsByProject: new Map(),
        dates: buildDates(30),
        today: baseDate,
        CELL_WIDTH,
        toLeft,
        toWidth,
      });
    });
    expect(setMultiSelectState).not.toHaveBeenCalled();
    expect(clearMultiSelect).not.toHaveBeenCalled();
  });

  it("does nothing when canvasRef.current is null", () => {
    const state = makeState();
    renderHook(() => {
      const canvasRef = useRef<HTMLDivElement | null>(null);
      useMultiSelectIntersection({
        multiSelectState: state,
        setMultiSelectState,
        clearMultiSelect,
        canvasRef,
        viewMode: "resource",
        resources: [],
        allocsByResource: new Map(),
        projectGroups: [],
        openDemandsByProject: new Map(),
        dates: buildDates(30),
        today: baseDate,
        CELL_WIDTH,
        toLeft,
        toWidth,
      });
    });
    expect(setMultiSelectState).not.toHaveBeenCalled();
    expect(clearMultiSelect).not.toHaveBeenCalled();
  });

  // -------------------------------------------------------------------------
  // resource view — no hits → clearMultiSelect
  // -------------------------------------------------------------------------

  it("calls clearMultiSelect when no rows intersect the selection rect", () => {
    // Row is completely above the selection band
    const rowEl = makeRowElement(
      { index: "0" },
      { top: 0, bottom: 10, left: 0, right: 200 }, // above selTop=100
    );
    canvasDom.appendChild(rowEl);

    const resource = { id: "r1" } as { id: string; name: string };
    const state = makeState({ startY: 100, currentY: 200 });

    renderHook(() => {
      const canvasRef = useRef<HTMLDivElement | null>(canvasDom);
      useMultiSelectIntersection({
        multiSelectState: state,
        setMultiSelectState,
        clearMultiSelect,
        canvasRef,
        viewMode: "resource",
        resources: [resource as never],
        allocsByResource: new Map([["r1", []]]),
        projectGroups: [],
        openDemandsByProject: new Map(),
        dates: buildDates(30),
        today: baseDate,
        CELL_WIDTH,
        toLeft,
        toWidth,
      });
    });

    expect(clearMultiSelect).toHaveBeenCalledOnce();
    expect(setMultiSelectState).not.toHaveBeenCalled();
  });

  // -------------------------------------------------------------------------
  // resource view — row + alloc hit
  // -------------------------------------------------------------------------

  it("selects resource and allocation when both intersect the selection rect", () => {
    /**
     * Canvas left = 0, LABEL_WIDTH = 256.
     * canvasXOffset = 0 + 256 = 256.
     * Selection: startX=300, currentX=500.
     * selLeftCanvas  = 300 - 256 = 44  → day index 1  (col 1)
     * selRightCanvas = 500 - 256 = 244 → day index 6  (col 6)
     *
     * Allocation: starts on day 2 (left=80), 3 days long (width=120), ends at 200.
     * 200 >= 44 (✓) AND 80 <= 244 (✓) → should be selected.
     */
    const allocStart = new Date(baseDate);
    allocStart.setDate(allocStart.getDate() + 2); // day index 2, left=80
    const allocEnd = new Date(allocStart);
    allocEnd.setDate(allocEnd.getDate() + 3); // 3 days, width=120, right=200

    const alloc = makeAlloc("alloc-1", allocStart.toISOString(), allocEnd.toISOString());

    const rowEl = makeRowElement(
      { index: "0" },
      { top: 120, bottom: 160, left: 0, right: 1200 }, // intersects selY 100–200
    );
    canvasDom.appendChild(rowEl);

    const resource = { id: "r1" } as { id: string; name: string };
    const state = makeState({ startX: 300, currentX: 500, startY: 100, currentY: 200 });

    renderHook(() => {
      const canvasRef = useRef<HTMLDivElement | null>(canvasDom);
      useMultiSelectIntersection({
        multiSelectState: state,
        setMultiSelectState,
        clearMultiSelect,
        canvasRef,
        viewMode: "resource",
        resources: [resource as never],
        allocsByResource: new Map([["r1", [alloc as never]]]),
        projectGroups: [],
        openDemandsByProject: new Map(),
        dates: buildDates(30),
        today: baseDate,
        CELL_WIDTH,
        toLeft,
        toWidth,
      });
    });

    expect(setMultiSelectState).toHaveBeenCalledOnce();
    const updaterFn = setMultiSelectState.mock.calls[0]![0] as (
      prev: MultiSelectState,
    ) => MultiSelectState;
    const prevState = makeState();
    const nextState = updaterFn(prevState);
    expect(nextState.selectedAllocationIds).toContain("alloc-1");
    expect(nextState.selectedResourceIds).toContain("r1");
  });

  // -------------------------------------------------------------------------
  // resource view — row hit but alloc outside X range
  // -------------------------------------------------------------------------

  it("selects resource but not allocation when alloc is outside the X range", () => {
    /**
     * selLeftCanvas = 44, selRightCanvas = 244
     * Allocation on day 10: left=400, right=480 → outside X range
     */
    const allocStart = new Date(baseDate);
    allocStart.setDate(allocStart.getDate() + 10); // left=400
    const allocEnd = new Date(allocStart);
    allocEnd.setDate(allocEnd.getDate() + 2); // right=480

    const alloc = makeAlloc("alloc-outside", allocStart.toISOString(), allocEnd.toISOString());

    const rowEl = makeRowElement({ index: "0" }, { top: 120, bottom: 160, left: 0, right: 1200 });
    canvasDom.appendChild(rowEl);

    const resource = { id: "r1" } as { id: string; name: string };
    const state = makeState({ startX: 300, currentX: 500, startY: 100, currentY: 200 });

    renderHook(() => {
      const canvasRef = useRef<HTMLDivElement | null>(canvasDom);
      useMultiSelectIntersection({
        multiSelectState: state,
        setMultiSelectState,
        clearMultiSelect,
        canvasRef,
        viewMode: "resource",
        resources: [resource as never],
        allocsByResource: new Map([["r1", [alloc as never]]]),
        projectGroups: [],
        openDemandsByProject: new Map(),
        dates: buildDates(30),
        today: baseDate,
        CELL_WIDTH,
        toLeft,
        toWidth,
      });
    });

    expect(setMultiSelectState).toHaveBeenCalledOnce();
    const updaterFn = setMultiSelectState.mock.calls[0]![0] as (
      prev: MultiSelectState,
    ) => MultiSelectState;
    const nextState = updaterFn(makeState());
    expect(nextState.selectedResourceIds).toContain("r1");
    expect(nextState.selectedAllocationIds).toHaveLength(0);
  });

  // -------------------------------------------------------------------------
  // project view — allocation hit
  // -------------------------------------------------------------------------

  it("selects allocation in project view when row and alloc intersect", () => {
    /**
     * Same geometry as resource test above:
     * selLeftCanvas=44, selRightCanvas=244
     * Alloc on day 2, 3 days (left=80, right=200) → intersects
     */
    const allocStart = new Date(baseDate);
    allocStart.setDate(allocStart.getDate() + 2);
    const allocEnd = new Date(allocStart);
    allocEnd.setDate(allocEnd.getDate() + 3);

    const alloc = makeAlloc("proj-alloc-1", allocStart.toISOString(), allocEnd.toISOString());

    const rowEl = makeRowElement(
      { projectResourceRow: "", projectId: "proj-1", resourceId: "res-1" },
      { top: 120, bottom: 160, left: 0, right: 1200 },
    );
    canvasDom.appendChild(rowEl);

    const projectGroups = [
      {
        id: "proj-1",
        resourceRows: [{ resource: { id: "res-1" }, allocs: [alloc as never] }],
      },
    ];

    const state = makeState({ startX: 300, currentX: 500, startY: 100, currentY: 200 });

    renderHook(() => {
      const canvasRef = useRef<HTMLDivElement | null>(canvasDom);
      useMultiSelectIntersection({
        multiSelectState: state,
        setMultiSelectState,
        clearMultiSelect,
        canvasRef,
        viewMode: "project",
        resources: [],
        allocsByResource: new Map(),
        projectGroups,
        openDemandsByProject: new Map(),
        dates: buildDates(30),
        today: baseDate,
        CELL_WIDTH,
        toLeft,
        toWidth,
      });
    });

    expect(setMultiSelectState).toHaveBeenCalledOnce();
    const updaterFn = setMultiSelectState.mock.calls[0]![0] as (
      prev: MultiSelectState,
    ) => MultiSelectState;
    const nextState = updaterFn(makeState());
    expect(nextState.selectedAllocationIds).toContain("proj-alloc-1");
  });

  // -------------------------------------------------------------------------
  // project view — demand row hit
  // -------------------------------------------------------------------------

  it("selects demand entry in project view when demand row intersects", () => {
    const demandStart = new Date(baseDate);
    demandStart.setDate(demandStart.getDate() + 2);
    const demandEnd = new Date(demandStart);
    demandEnd.setDate(demandEnd.getDate() + 3);

    const demand = makeAlloc("demand-1", demandStart.toISOString(), demandEnd.toISOString());

    const demandRowEl = makeRowElement(
      { projectDemandRow: "", projectId: "proj-1" },
      { top: 120, bottom: 160, left: 0, right: 1200 },
    );
    canvasDom.appendChild(demandRowEl);

    const state = makeState({ startX: 300, currentX: 500, startY: 100, currentY: 200 });

    renderHook(() => {
      const canvasRef = useRef<HTMLDivElement | null>(canvasDom);
      useMultiSelectIntersection({
        multiSelectState: state,
        setMultiSelectState,
        clearMultiSelect,
        canvasRef,
        viewMode: "project",
        resources: [],
        allocsByResource: new Map(),
        projectGroups: [],
        openDemandsByProject: new Map([["proj-1", [demand as never]]]),
        dates: buildDates(30),
        today: baseDate,
        CELL_WIDTH,
        toLeft,
        toWidth,
      });
    });

    expect(setMultiSelectState).toHaveBeenCalledOnce();
    const updaterFn = setMultiSelectState.mock.calls[0]![0] as (
      prev: MultiSelectState,
    ) => MultiSelectState;
    const nextState = updaterFn(makeState());
    expect(nextState.selectedAllocationIds).toContain("demand-1");
  });

  // -------------------------------------------------------------------------
  // dateRange in the result
  // -------------------------------------------------------------------------

  it("includes a dateRange reflecting the selected column band", () => {
    /**
     * selLeftCanvas = 300 - 256 = 44  → colIndex = floor(44/40)  = 1  → dates[1]
     * selRightCanvas = 500 - 256 = 244 → colIndex = floor(244/40) = 6  → dates[6]
     */
    const allocStart = new Date(baseDate);
    allocStart.setDate(allocStart.getDate() + 2);
    const allocEnd = new Date(allocStart);
    allocEnd.setDate(allocEnd.getDate() + 3);

    const alloc = makeAlloc("range-alloc", allocStart.toISOString(), allocEnd.toISOString());

    const rowEl = makeRowElement({ index: "0" }, { top: 120, bottom: 160, left: 0, right: 1200 });
    canvasDom.appendChild(rowEl);

    const dates = buildDates(30);
    const state = makeState({ startX: 300, currentX: 500, startY: 100, currentY: 200 });

    renderHook(() => {
      const canvasRef = useRef<HTMLDivElement | null>(canvasDom);
      useMultiSelectIntersection({
        multiSelectState: state,
        setMultiSelectState,
        clearMultiSelect,
        canvasRef,
        viewMode: "resource",
        resources: [{ id: "r1" } as never],
        allocsByResource: new Map([["r1", [alloc as never]]]),
        projectGroups: [],
        openDemandsByProject: new Map(),
        dates,
        today: baseDate,
        CELL_WIDTH,
        toLeft,
        toWidth,
      });
    });

    expect(setMultiSelectState).toHaveBeenCalledOnce();
    const updaterFn = setMultiSelectState.mock.calls[0]![0] as (
      prev: MultiSelectState,
    ) => MultiSelectState;
    const nextState = updaterFn(makeState());
    expect(nextState.dateRange).not.toBeNull();
    // col index 1 → dates[1]
    expect(nextState.dateRange?.start).toEqual(dates[1]);
    // col index 6 → dates[6]
    expect(nextState.dateRange?.end).toEqual(dates[6]);
  });

  // -------------------------------------------------------------------------
  // Selection rectangle normalisation (drag from right-to-left / bottom-to-top)
  // -------------------------------------------------------------------------

  it("correctly normalises a right-to-left / bottom-to-top drag", () => {
    /**
     * User dragged from (500, 200) to (300, 100) — swapped start/current.
     * After normalisation: selLeft=300, selRight=500, selTop=100, selBottom=200.
     * Same geometry as forward drag → same result.
     */
    const allocStart = new Date(baseDate);
    allocStart.setDate(allocStart.getDate() + 2);
    const allocEnd = new Date(allocStart);
    allocEnd.setDate(allocEnd.getDate() + 3);

    const alloc = makeAlloc("rtl-alloc", allocStart.toISOString(), allocEnd.toISOString());

    const rowEl = makeRowElement({ index: "0" }, { top: 120, bottom: 160, left: 0, right: 1200 });
    canvasDom.appendChild(rowEl);

    // Swapped: startX > currentX, startY > currentY
    const state = makeState({ startX: 500, currentX: 300, startY: 200, currentY: 100 });

    renderHook(() => {
      const canvasRef = useRef<HTMLDivElement | null>(canvasDom);
      useMultiSelectIntersection({
        multiSelectState: state,
        setMultiSelectState,
        clearMultiSelect,
        canvasRef,
        viewMode: "resource",
        resources: [{ id: "r1" } as never],
        allocsByResource: new Map([["r1", [alloc as never]]]),
        projectGroups: [],
        openDemandsByProject: new Map(),
        dates: buildDates(30),
        today: baseDate,
        CELL_WIDTH,
        toLeft,
        toWidth,
      });
    });

    expect(setMultiSelectState).toHaveBeenCalledOnce();
    const updaterFn = setMultiSelectState.mock.calls[0]![0] as (
      prev: MultiSelectState,
    ) => MultiSelectState;
    const nextState = updaterFn(makeState());
    expect(nextState.selectedAllocationIds).toContain("rtl-alloc");
    expect(nextState.selectedResourceIds).toContain("r1");
  });

  // -------------------------------------------------------------------------
  // Multiple allocations — only overlapping ones are selected
  // -------------------------------------------------------------------------

  it("selects only allocations that overlap the X range", () => {
    const dates = buildDates(30);

    // Alloc A: day 2 (left=80), 3 days (right=200) → inside [44, 244]
    const aStart = new Date(baseDate);
    aStart.setDate(aStart.getDate() + 2);
    const aEnd = new Date(aStart);
    aEnd.setDate(aEnd.getDate() + 3);

    // Alloc B: day 15 (left=600), 2 days (right=680) → outside [44, 244]
    const bStart = new Date(baseDate);
    bStart.setDate(bStart.getDate() + 15);
    const bEnd = new Date(bStart);
    bEnd.setDate(bEnd.getDate() + 2);

    const allocA = makeAlloc("inside", aStart.toISOString(), aEnd.toISOString());
    const allocB = makeAlloc("outside", bStart.toISOString(), bEnd.toISOString());

    const rowEl = makeRowElement({ index: "0" }, { top: 120, bottom: 160, left: 0, right: 1200 });
    canvasDom.appendChild(rowEl);

    const state = makeState({ startX: 300, currentX: 500, startY: 100, currentY: 200 });

    renderHook(() => {
      const canvasRef = useRef<HTMLDivElement | null>(canvasDom);
      useMultiSelectIntersection({
        multiSelectState: state,
        setMultiSelectState,
        clearMultiSelect,
        canvasRef,
        viewMode: "resource",
        resources: [{ id: "r1" } as never],
        allocsByResource: new Map([["r1", [allocA as never, allocB as never]]]),
        projectGroups: [],
        openDemandsByProject: new Map(),
        dates,
        today: baseDate,
        CELL_WIDTH,
        toLeft,
        toWidth,
      });
    });

    expect(setMultiSelectState).toHaveBeenCalledOnce();
    const updaterFn = setMultiSelectState.mock.calls[0]![0] as (
      prev: MultiSelectState,
    ) => MultiSelectState;
    const nextState = updaterFn(makeState());
    expect(nextState.selectedAllocationIds).toContain("inside");
    expect(nextState.selectedAllocationIds).not.toContain("outside");
  });
});
