import { beforeEach, describe, expect, it } from "vitest"
import { mount } from "@vue/test-utils"
import { createPinia, setActivePinia } from "pinia"
import GridBuilder from "./GridBuilder.vue"
import {
  __resetGridBuilderIdCounter,
  useGridBuilderStore,
  type GridBuilderSchema,
} from "@/stores/gridBuilderStore"

const BUILDER_ID = "gb-test"

/**
 * jsdom doesn't lay elements out, so any test that needs to convert
 * pixel deltas into column / row indices must monkey-patch
 * `getBoundingClientRect` on the canvas grid wrapper. Helper used by
 * the drag / resize tests below.
 */
function stubGridRect(
  wrapper: ReturnType<typeof mount>,
  widthPx: number,
  cols: number,
): void {
  const grid = wrapper.find("[data-testid='grid-builder-canvas-grid']")
    .element as HTMLElement
  // We control the width; pretend the cell-column track equals widthPx
  // so each column is widthPx / cols pixels wide.
  Object.defineProperty(grid, "getBoundingClientRect", {
    configurable: true,
    value: () => ({
      x: 0,
      y: 0,
      top: 0,
      left: 0,
      right: widthPx,
      bottom: 0,
      width: widthPx,
      height: 600,
      toJSON: () => ({}),
    }),
  })
  void cols
}

function dispatchPointer(
  target: EventTarget,
  type: "pointerdown" | "pointermove" | "pointerup",
  init: { clientX?: number; clientY?: number; button?: number } = {},
): void {
  // Construct the event via its constructor with all properties set in
  // init — jsdom 27+ enforces that clientX/Y are read-only after
  // construction, so assigning them later (as @vue/test-utils' .trigger()
  // does internally) throws.
  const Ctor =
    (window as unknown as { PointerEvent?: typeof MouseEvent }).PointerEvent ??
    MouseEvent
  const event = new Ctor(type, {
    bubbles: true,
    cancelable: true,
    clientX: init.clientX ?? 0,
    clientY: init.clientY ?? 0,
    button: init.button ?? 0,
  })
  target.dispatchEvent(event)
}

describe("GridBuilder", () => {
  beforeEach(() => {
    setActivePinia(createPinia())
    __resetGridBuilderIdCounter()
  })

  it("renders the three-pane layout", () => {
    const wrapper = mount(GridBuilder, { props: { builderId: BUILDER_ID } })
    expect(wrapper.find("[data-testid='grid-builder-palette']").exists()).toBe(
      true,
    )
    expect(wrapper.find("[data-testid='grid-builder-canvas']").exists()).toBe(
      true,
    )
    expect(
      wrapper.find("[data-testid='grid-builder-inspector']").exists(),
    ).toBe(true)
  })

  it("shows an empty-state hint on the canvas when there are no cells", () => {
    const wrapper = mount(GridBuilder, { props: { builderId: BUILDER_ID } })
    expect(
      wrapper.find("[data-testid='grid-builder-canvas-empty']").exists(),
    ).toBe(true)
  })

  it("lists every cell-template preset in the palette", () => {
    const wrapper = mount(GridBuilder, { props: { builderId: BUILDER_ID } })
    const palette = wrapper.find("[data-testid='grid-builder-palette']")
    expect(palette.find("[data-testid='grid-builder-palette-add']").exists()).toBe(
      true,
    )
    for (const dim of ["1x1", "2x2", "4x2", "6x4", "12x1"]) {
      expect(
        palette.find(`[data-testid='grid-builder-palette-${dim}']`).exists(),
      ).toBe(true)
    }
  })

  it("clicking 'Add cell' appends a default-sized cell and selects it", async () => {
    const wrapper = mount(GridBuilder, { props: { builderId: BUILDER_ID } })
    await wrapper
      .find("[data-testid='grid-builder-palette-add']")
      .trigger("click")

    const store = useGridBuilderStore()
    const schema = store.getSchema(BUILDER_ID)
    expect(schema.items).toHaveLength(1)
    expect(schema.items[0]!.w).toBe(1)
    expect(schema.items[0]!.h).toBe(1)
    expect(
      wrapper.find("[data-testid='grid-builder-inspector-fields']").exists(),
    ).toBe(true)
  })

  it("clicking a cell preset appends a cell with the preset size", async () => {
    const wrapper = mount(GridBuilder, { props: { builderId: BUILDER_ID } })
    await wrapper
      .find("[data-testid='grid-builder-palette-6x4']")
      .trigger("click")

    const store = useGridBuilderStore()
    const schema = store.getSchema(BUILDER_ID)
    expect(schema.items).toHaveLength(1)
    expect(schema.items[0]!.w).toBe(6)
    expect(schema.items[0]!.h).toBe(4)
  })

  it("editing the label patches the schema", async () => {
    const wrapper = mount(GridBuilder, { props: { builderId: BUILDER_ID } })
    await wrapper
      .find("[data-testid='grid-builder-palette-add']")
      .trigger("click")
    await wrapper
      .find("[data-testid='grid-builder-inspector-label']")
      .setValue("Hero")

    const store = useGridBuilderStore()
    const schema = store.getSchema(BUILDER_ID)
    expect(schema.items[0]!.label).toBe("Hero")
  })

  it("editing the width clamps to the column count", async () => {
    const wrapper = mount(GridBuilder, { props: { builderId: BUILDER_ID } })
    await wrapper
      .find("[data-testid='grid-builder-palette-add']")
      .trigger("click")
    await wrapper
      .find("[data-testid='grid-builder-inspector-w']")
      .setValue("99")

    const store = useGridBuilderStore()
    const schema = store.getSchema(BUILDER_ID)
    expect(schema.items[0]!.w).toBe(schema.cols) // clamped to 12
  })

  it("removes the selected cell when Remove is clicked", async () => {
    const wrapper = mount(GridBuilder, { props: { builderId: BUILDER_ID } })
    await wrapper
      .find("[data-testid='grid-builder-palette-add']")
      .trigger("click")
    await wrapper.find("[data-testid='grid-builder-remove']").trigger("click")

    const store = useGridBuilderStore()
    const schema = store.getSchema(BUILDER_ID)
    expect(schema.items).toHaveLength(0)
  })

  it("loads an existing schema from modelValue on mount", () => {
    const existing: GridBuilderSchema = {
      cols: 12,
      rowHeight: 80,
      items: [
        { id: "a", label: "Hero", x: 0, y: 0, w: 12, h: 2 },
        { id: "b", label: "Side", x: 0, y: 2, w: 4, h: 3 },
      ],
    }
    mount(GridBuilder, {
      props: { builderId: BUILDER_ID, modelValue: existing },
    })

    const store = useGridBuilderStore()
    const schema = store.getSchema(BUILDER_ID)
    expect(schema.rowHeight).toBe(80)
    expect(schema.items).toHaveLength(2)
    expect(schema.items[0]!.label).toBe("Hero")
  })

  it("emits update:modelValue on schema mutation", async () => {
    const wrapper = mount(GridBuilder, { props: { builderId: BUILDER_ID } })
    await wrapper
      .find("[data-testid='grid-builder-palette-add']")
      .trigger("click")
    await wrapper.vm.$nextTick()

    const emitted = wrapper.emitted("update:modelValue")
    expect(emitted).toBeTruthy()
    const last = emitted![emitted!.length - 1] as unknown[]
    const schema = last[0] as GridBuilderSchema
    expect(schema.items).toHaveLength(1)
  })

  it("renders one cell element per item on the canvas", async () => {
    const wrapper = mount(GridBuilder, { props: { builderId: BUILDER_ID } })
    await wrapper
      .find("[data-testid='grid-builder-palette-2x2']")
      .trigger("click")
    await wrapper
      .find("[data-testid='grid-builder-palette-4x2']")
      .trigger("click")

    const grid = wrapper.find("[data-testid='grid-builder-canvas-grid']")
    expect(grid.exists()).toBe(true)
    const cells = grid.findAll("[data-testid^='grid-builder-cell-']")
    expect(cells).toHaveLength(2)
  })

  describe("pointer-driven drag + resize", () => {
    it("dragging a cell updates its x / y in the store", async () => {
      const wrapper = mount(GridBuilder, {
        props: { builderId: BUILDER_ID },
        attachTo: document.body,
      })
      await wrapper
        .find("[data-testid='grid-builder-palette-1x1']")
        .trigger("click")
      // 1200px wide / 12 cols => 100px per column.
      stubGridRect(wrapper, 1200, 12)
      const store = useGridBuilderStore()
      const cellId = store.getSchema(BUILDER_ID).items[0]!.id

      dispatchPointer(
        wrapper.find(`[data-testid='grid-builder-cell-${cellId}']`).element,
        "pointerdown",
        { clientX: 0, clientY: 0, button: 0 },
      )
      dispatchPointer(window, "pointermove", { clientX: 100, clientY: 60 })
      dispatchPointer(window, "pointerup", { clientX: 100, clientY: 60 })

      const item = store.getSchema(BUILDER_ID).items[0]!
      expect(item.x).toBe(1)
      expect(item.y).toBe(1)
    })

    it("dragging the resize handle increases width / height in the store", async () => {
      const wrapper = mount(GridBuilder, {
        props: { builderId: BUILDER_ID },
        attachTo: document.body,
      })
      await wrapper
        .find("[data-testid='grid-builder-palette-2x2']")
        .trigger("click")
      stubGridRect(wrapper, 1200, 12)
      const store = useGridBuilderStore()
      const cellId = store.getSchema(BUILDER_ID).items[0]!.id

      dispatchPointer(
        wrapper.find(`[data-testid='grid-builder-resize-${cellId}']`).element,
        "pointerdown",
        { clientX: 0, clientY: 0, button: 0 },
      )
      // +200px x -> +2 cols. +120px y / 60px rowHeight -> +2 rows.
      dispatchPointer(window, "pointermove", { clientX: 200, clientY: 120 })
      dispatchPointer(window, "pointerup", { clientX: 200, clientY: 120 })

      const item = store.getSchema(BUILDER_ID).items[0]!
      expect(item.w).toBe(4)
      expect(item.h).toBe(4)
    })

    it("resize clamps width to >= 1", async () => {
      const wrapper = mount(GridBuilder, {
        props: { builderId: BUILDER_ID },
        attachTo: document.body,
      })
      await wrapper
        .find("[data-testid='grid-builder-palette-1x1']")
        .trigger("click")
      stubGridRect(wrapper, 1200, 12)
      const store = useGridBuilderStore()
      const cellId = store.getSchema(BUILDER_ID).items[0]!.id

      dispatchPointer(
        wrapper.find(`[data-testid='grid-builder-resize-${cellId}']`).element,
        "pointerdown",
        { clientX: 0, clientY: 0, button: 0 },
      )
      // Drag massively to the left/up to try to shrink past zero.
      dispatchPointer(window, "pointermove", {
        clientX: -2000,
        clientY: -2000,
      })
      dispatchPointer(window, "pointerup", {
        clientX: -2000,
        clientY: -2000,
      })

      const item = store.getSchema(BUILDER_ID).items[0]!
      expect(item.w).toBe(1)
      expect(item.h).toBe(1)
    })

    it("drag math uses the schema's cols for column snapping", async () => {
      const seed: GridBuilderSchema = {
        cols: 6,
        rowHeight: 50,
        items: [{ id: "only", label: "Only", x: 0, y: 0, w: 1, h: 1 }],
      }
      const wrapper = mount(GridBuilder, {
        props: { builderId: BUILDER_ID, modelValue: seed },
        attachTo: document.body,
      })
      // Wait for onMounted -> loadSchema -> reactive grid render.
      await wrapper.vm.$nextTick()
      await wrapper.vm.$nextTick()
      // 600px / 6 cols => 100px per col, so a 100px move = 1 col.
      stubGridRect(wrapper, 600, 6)
      const store = useGridBuilderStore()

      dispatchPointer(
        wrapper.find("[data-testid='grid-builder-cell-only']").element,
        "pointerdown",
        { clientX: 0, clientY: 0, button: 0 },
      )
      dispatchPointer(window, "pointermove", { clientX: 200, clientY: 100 })
      dispatchPointer(window, "pointerup", { clientX: 200, clientY: 100 })

      const item = store
        .getSchema(BUILDER_ID)
        .items.find((it) => it.id === "only")!
      expect(item.x).toBe(2)
      expect(item.y).toBe(2)
    })

    it("pointerup cleans up listeners between drags", async () => {
      const wrapper = mount(GridBuilder, {
        props: { builderId: BUILDER_ID },
        attachTo: document.body,
      })
      await wrapper
        .find("[data-testid='grid-builder-palette-1x1']")
        .trigger("click")
      await wrapper
        .find("[data-testid='grid-builder-palette-1x1']")
        .trigger("click")
      stubGridRect(wrapper, 1200, 12)
      const store = useGridBuilderStore()
      const [first, second] = store.getSchema(BUILDER_ID).items
      expect(first && second).toBeTruthy()

      // Drag the first cell, then release.
      dispatchPointer(
        wrapper.find(`[data-testid='grid-builder-cell-${first!.id}']`).element,
        "pointerdown",
        { clientX: 0, clientY: 0, button: 0 },
      )
      dispatchPointer(window, "pointermove", { clientX: 100, clientY: 0 })
      dispatchPointer(window, "pointerup", { clientX: 100, clientY: 0 })
      const firstAfter = store
        .getSchema(BUILDER_ID)
        .items.find((it) => it.id === first!.id)!
      const firstX = firstAfter.x

      // Now move the pointer further. If listeners weren't removed,
      // `first` would keep updating.
      dispatchPointer(window, "pointermove", { clientX: 500, clientY: 200 })
      const firstStill = store
        .getSchema(BUILDER_ID)
        .items.find((it) => it.id === first!.id)!
      expect(firstStill.x).toBe(firstX)

      // Drag the second cell — its update should land independently.
      dispatchPointer(
        wrapper.find(`[data-testid='grid-builder-cell-${second!.id}']`).element,
        "pointerdown",
        { clientX: 0, clientY: 0, button: 0 },
      )
      dispatchPointer(window, "pointermove", { clientX: 300, clientY: 60 })
      dispatchPointer(window, "pointerup", { clientX: 300, clientY: 60 })
      const secondAfter = store
        .getSchema(BUILDER_ID)
        .items.find((it) => it.id === second!.id)!
      expect(secondAfter.x).toBe(3)
      expect(secondAfter.y).toBe(2) // appended row y=1 + 1 row delta = 2
    })

    it("drag and inspector edits don't race", async () => {
      const wrapper = mount(GridBuilder, {
        props: { builderId: BUILDER_ID },
        attachTo: document.body,
      })
      await wrapper
        .find("[data-testid='grid-builder-palette-1x1']")
        .trigger("click")
      stubGridRect(wrapper, 1200, 12)
      const store = useGridBuilderStore()
      const cellId = store.getSchema(BUILDER_ID).items[0]!.id

      // Drag the cell two cols right.
      dispatchPointer(
        wrapper.find(`[data-testid='grid-builder-cell-${cellId}']`).element,
        "pointerdown",
        { clientX: 0, clientY: 0, button: 0 },
      )
      dispatchPointer(window, "pointermove", { clientX: 200, clientY: 0 })
      dispatchPointer(window, "pointerup", { clientX: 200, clientY: 0 })

      // Inspector edits land afterwards without being clobbered.
      await wrapper
        .find("[data-testid='grid-builder-inspector-label']")
        .setValue("After drag")
      const item = store
        .getSchema(BUILDER_ID)
        .items.find((it) => it.id === cellId)!
      expect(item.label).toBe("After drag")
      expect(item.x).toBe(2)
    })

  })
})
