import { beforeEach, describe, expect, it } from "vitest"
import { mount } from "@vue/test-utils"
import { createPinia, setActivePinia } from "pinia"
import ReactiveCanvasBuilder from "./ReactiveCanvasBuilder.vue"
import {
  __resetReactiveCanvasBuilderIdCounter,
  REACTIVE_CANVAS_BLOCK_TYPES,
  useReactiveCanvasBuilderStore,
  type ReactiveCanvasBuilderSchema,
} from "@/stores/reactiveCanvasBuilderStore"

const BUILDER_ID = "rcb-test"

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("ReactiveCanvasBuilder", () => {
  beforeEach(() => {
    setActivePinia(createPinia())
    __resetReactiveCanvasBuilderIdCounter()
  })

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

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

  it("lists every block type in the palette", () => {
    const wrapper = mount(ReactiveCanvasBuilder, {
      props: { builderId: BUILDER_ID },
    })
    for (const t of REACTIVE_CANVAS_BLOCK_TYPES) {
      expect(
        wrapper
          .find(`[data-testid='reactive-canvas-builder-palette-${t}']`)
          .exists(),
      ).toBe(true)
    }
  })

  it("clicking a palette button appends a block and selects it", async () => {
    const wrapper = mount(ReactiveCanvasBuilder, {
      props: { builderId: BUILDER_ID },
    })
    await wrapper
      .find("[data-testid='reactive-canvas-builder-palette-text-block']")
      .trigger("click")

    const store = useReactiveCanvasBuilderStore()
    const schema = store.getSchema(BUILDER_ID)
    expect(schema.blocks).toHaveLength(1)
    expect(schema.blocks[0]!.type).toBe("text-block")
    expect(
      wrapper
        .find("[data-testid='reactive-canvas-builder-inspector-fields']")
        .exists(),
    ).toBe(true)
  })

  it("editing x patches the selected block", async () => {
    const wrapper = mount(ReactiveCanvasBuilder, {
      props: { builderId: BUILDER_ID },
    })
    await wrapper
      .find("[data-testid='reactive-canvas-builder-palette-text-block']")
      .trigger("click")
    await wrapper
      .find("[data-testid='reactive-canvas-builder-inspector-x']")
      .setValue("100")

    const store = useReactiveCanvasBuilderStore()
    const schema = store.getSchema(BUILDER_ID)
    expect(schema.blocks[0]!.x).toBe(100)
  })

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

    const store = useReactiveCanvasBuilderStore()
    expect(store.getSchema(BUILDER_ID).blocks).toHaveLength(0)
  })

  it("editing canvas width patches the schema", async () => {
    const wrapper = mount(ReactiveCanvasBuilder, {
      props: { builderId: BUILDER_ID },
    })
    await wrapper
      .find("[data-testid='reactive-canvas-builder-canvas-width']")
      .setValue("1024")

    const store = useReactiveCanvasBuilderStore()
    expect(store.getSchema(BUILDER_ID).width).toBe(1024)
  })

  it("parses props JSON and writes to the schema", async () => {
    const wrapper = mount(ReactiveCanvasBuilder, {
      props: { builderId: BUILDER_ID },
    })
    await wrapper
      .find("[data-testid='reactive-canvas-builder-palette-text-block']")
      .trigger("click")
    await wrapper
      .find("[data-testid='reactive-canvas-builder-inspector-props']")
      .setValue('{"text":"Hello"}')

    const store = useReactiveCanvasBuilderStore()
    expect(store.getSchema(BUILDER_ID).blocks[0]!.props).toEqual({
      text: "Hello",
    })
    expect(
      wrapper
        .find("[data-testid='reactive-canvas-builder-inspector-props-error']")
        .exists(),
    ).toBe(false)
  })

  it("shows an error and does not patch when props JSON is invalid", async () => {
    const wrapper = mount(ReactiveCanvasBuilder, {
      props: { builderId: BUILDER_ID },
    })
    await wrapper
      .find("[data-testid='reactive-canvas-builder-palette-text-block']")
      .trigger("click")
    await wrapper
      .find("[data-testid='reactive-canvas-builder-inspector-props']")
      .setValue("{not valid json}")

    const store = useReactiveCanvasBuilderStore()
    // Initial default props for text-block carry a `text` key.
    expect(store.getSchema(BUILDER_ID).blocks[0]!.props).toEqual({
      text: "Sample text",
    })
    expect(
      wrapper
        .find("[data-testid='reactive-canvas-builder-inspector-props-error']")
        .exists(),
    ).toBe(true)
  })

  it("loads an existing schema from modelValue on mount", () => {
    const seed: ReactiveCanvasBuilderSchema = {
      width: 1200,
      height: 700,
      blocks: [
        {
          id: "seeded",
          type: "image-block",
          x: 10,
          y: 20,
          width: 200,
          height: 100,
          props: { src: "logo.png" },
        },
      ],
    }
    mount(ReactiveCanvasBuilder, {
      props: { builderId: BUILDER_ID, modelValue: seed },
    })

    const store = useReactiveCanvasBuilderStore()
    const schema = store.getSchema(BUILDER_ID)
    expect(schema.width).toBe(1200)
    expect(schema.height).toBe(700)
    expect(schema.blocks).toHaveLength(1)
    expect(schema.blocks[0]!.type).toBe("image-block")
    expect(schema.blocks[0]!.props.src).toBe("logo.png")
  })

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

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

  describe("pointer-driven drag + resize", () => {
    it("dragging a block updates x / y by the raw pixel delta", async () => {
      const wrapper = mount(ReactiveCanvasBuilder, {
        props: { builderId: BUILDER_ID },
        attachTo: document.body,
      })
      await wrapper
        .find("[data-testid='reactive-canvas-builder-palette-text-block']")
        .trigger("click")
      const store = useReactiveCanvasBuilderStore()
      const block = store.getSchema(BUILDER_ID).blocks[0]!
      const startX = block.x
      const startY = block.y

      dispatchPointer(
        wrapper.find(`[data-testid='reactive-canvas-builder-block-${block.id}']`).element,
        "pointerdown",
        { clientX: 0, clientY: 0, button: 0 },
      )
      dispatchPointer(window, "pointermove", { clientX: 42, clientY: 30 })
      dispatchPointer(window, "pointerup", { clientX: 42, clientY: 30 })

      const after = store.getSchema(BUILDER_ID).blocks[0]!
      expect(after.x).toBe(startX + 42)
      expect(after.y).toBe(startY + 30)
    })

    it("resizing via the bottom-right handle grows width + height", async () => {
      const wrapper = mount(ReactiveCanvasBuilder, {
        props: { builderId: BUILDER_ID },
        attachTo: document.body,
      })
      await wrapper
        .find("[data-testid='reactive-canvas-builder-palette-text-block']")
        .trigger("click")
      const store = useReactiveCanvasBuilderStore()
      const block = store.getSchema(BUILDER_ID).blocks[0]!
      const startW = block.width
      const startH = block.height

      dispatchPointer(
        wrapper.find(`[data-testid='reactive-canvas-builder-block-${block.id}-resize-br']`).element,
        "pointerdown",
        { clientX: 0, clientY: 0, button: 0 },
      )
      dispatchPointer(window, "pointermove", { clientX: 50, clientY: 30 })
      dispatchPointer(window, "pointerup", { clientX: 50, clientY: 30 })

      const after = store.getSchema(BUILDER_ID).blocks[0]!
      expect(after.width).toBe(startW + 50)
      expect(after.height).toBe(startH + 30)
      // Position should not change for a br resize.
      expect(after.x).toBe(block.x)
      expect(after.y).toBe(block.y)
    })

    it("resizing via the top-left handle adjusts x, y, width, height", async () => {
      const wrapper = mount(ReactiveCanvasBuilder, {
        props: { builderId: BUILDER_ID },
        attachTo: document.body,
      })
      await wrapper
        .find("[data-testid='reactive-canvas-builder-palette-text-block']")
        .trigger("click")
      const store = useReactiveCanvasBuilderStore()
      const block = store.getSchema(BUILDER_ID).blocks[0]!
      const startX = block.x
      const startY = block.y
      const startW = block.width
      const startH = block.height

      dispatchPointer(
        wrapper.find(`[data-testid='reactive-canvas-builder-block-${block.id}-resize-tl']`).element,
        "pointerdown",
        { clientX: 0, clientY: 0, button: 0 },
      )
      dispatchPointer(window, "pointermove", { clientX: -10, clientY: -20 })
      dispatchPointer(window, "pointerup", { clientX: -10, clientY: -20 })

      const after = store.getSchema(BUILDER_ID).blocks[0]!
      expect(after.x).toBe(startX - 10)
      expect(after.y).toBe(startY - 20)
      expect(after.width).toBe(startW + 10)
      expect(after.height).toBe(startH + 20)
    })

    it("resize clamps to the minimum size", async () => {
      const wrapper = mount(ReactiveCanvasBuilder, {
        props: { builderId: BUILDER_ID },
        attachTo: document.body,
      })
      await wrapper
        .find("[data-testid='reactive-canvas-builder-palette-text-block']")
        .trigger("click")
      const store = useReactiveCanvasBuilderStore()
      const block = store.getSchema(BUILDER_ID).blocks[0]!

      dispatchPointer(
        wrapper.find(`[data-testid='reactive-canvas-builder-block-${block.id}-resize-br']`).element,
        "pointerdown",
        { clientX: 0, clientY: 0, button: 0 },
      )
      dispatchPointer(window, "pointermove", {
        clientX: -10000,
        clientY: -10000,
      })
      dispatchPointer(window, "pointerup", {
        clientX: -10000,
        clientY: -10000,
      })

      const after = store.getSchema(BUILDER_ID).blocks[0]!
      expect(after.width).toBe(20)
      expect(after.height).toBe(20)
    })

    it("drag clamps to canvas bounds (block can't escape)", async () => {
      const wrapper = mount(ReactiveCanvasBuilder, {
        props: { builderId: BUILDER_ID },
        attachTo: document.body,
      })
      await wrapper
        .find("[data-testid='reactive-canvas-builder-palette-text-block']")
        .trigger("click")
      const store = useReactiveCanvasBuilderStore()
      const schema = store.getSchema(BUILDER_ID)
      const block = schema.blocks[0]!

      dispatchPointer(
        wrapper.find(`[data-testid='reactive-canvas-builder-block-${block.id}']`).element,
        "pointerdown",
        { clientX: 0, clientY: 0, button: 0 },
      )
      // Try to drag well past the right edge.
      dispatchPointer(window, "pointermove", {
        clientX: 10000,
        clientY: 10000,
      })
      dispatchPointer(window, "pointerup", {
        clientX: 10000,
        clientY: 10000,
      })

      const after = store.getSchema(BUILDER_ID).blocks[0]!
      expect(after.x).toBeLessThanOrEqual(schema.width - after.width)
      expect(after.y).toBeLessThanOrEqual(schema.height - after.height)
      expect(after.x).toBeGreaterThanOrEqual(0)
      expect(after.y).toBeGreaterThanOrEqual(0)
    })

    it("dragging a block selects it if not already selected", async () => {
      const wrapper = mount(ReactiveCanvasBuilder, {
        props: { builderId: BUILDER_ID },
        attachTo: document.body,
      })
      // Add two blocks so we can deselect the active one.
      await wrapper
        .find("[data-testid='reactive-canvas-builder-palette-text-block']")
        .trigger("click")
      await wrapper
        .find("[data-testid='reactive-canvas-builder-palette-button-block']")
        .trigger("click")
      const store = useReactiveCanvasBuilderStore()
      const [first, second] = store.getSchema(BUILDER_ID).blocks
      // After two adds the second block is selected. Clear selection.
      store.select(BUILDER_ID, null)
      expect(store.getSelection(BUILDER_ID).id).toBeNull()

      dispatchPointer(
        wrapper.find(`[data-testid='reactive-canvas-builder-block-${first!.id}']`).element,
        "pointerdown",
        { clientX: 0, clientY: 0, button: 0 },
      )
      dispatchPointer(window, "pointermove", { clientX: 5, clientY: 5 })
      dispatchPointer(window, "pointerup", { clientX: 5, clientY: 5 })

      expect(store.getSelection(BUILDER_ID).id).toBe(first!.id)
      void second
    })
  })
})
