import { beforeEach, describe, expect, it } from "vitest"
import { mount } from "@vue/test-utils"
import { createPinia, setActivePinia } from "pinia"
import ObjectBuilder from "./ObjectBuilder.vue"
import {
  __resetObjectBuilderIdCounter,
  toRecord,
  useObjectBuilderStore,
  type ObjectBuilderSchema,
} from "@/stores/objectBuilderStore"

const BUILDER_ID = "ob-test"

describe("ObjectBuilder", () => {
  beforeEach(() => {
    setActivePinia(createPinia())
    __resetObjectBuilderIdCounter()
  })

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

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

  it("lists every entry type in the palette", () => {
    const wrapper = mount(ObjectBuilder, { props: { builderId: BUILDER_ID } })
    const palette = wrapper.find("[data-testid='object-builder-palette']")
    for (const type of [
      "string",
      "number",
      "boolean",
      "array",
      "object",
      "null",
    ]) {
      expect(
        palette
          .find(`[data-testid='object-builder-palette-${type}']`)
          .exists(),
      ).toBe(true)
    }
  })

  it("clicking a palette button appends an entry and selects it", async () => {
    const wrapper = mount(ObjectBuilder, { props: { builderId: BUILDER_ID } })
    await wrapper
      .find("[data-testid='object-builder-palette-string']")
      .trigger("click")

    const store = useObjectBuilderStore()
    const schema = store.getSchema(BUILDER_ID)
    expect(schema.entries).toHaveLength(1)
    expect(schema.entries[0]!.type).toBe("string")
    expect(schema.entries[0]!.value).toBe("")
    expect(
      wrapper
        .find("[data-testid='object-builder-inspector-fields']")
        .exists(),
    ).toBe(true)
  })

  it("editing the key patches the schema", async () => {
    const wrapper = mount(ObjectBuilder, { props: { builderId: BUILDER_ID } })
    await wrapper
      .find("[data-testid='object-builder-palette-string']")
      .trigger("click")
    await wrapper
      .find("[data-testid='object-builder-inspector-key']")
      .setValue("title")

    const store = useObjectBuilderStore()
    const schema = store.getSchema(BUILDER_ID)
    expect(schema.entries[0]!.key).toBe("title")
  })

  it("editing the string value patches the schema", async () => {
    const wrapper = mount(ObjectBuilder, { props: { builderId: BUILDER_ID } })
    await wrapper
      .find("[data-testid='object-builder-palette-string']")
      .trigger("click")
    await wrapper
      .find("[data-testid='object-builder-inspector-value-string']")
      .setValue("hello")

    const store = useObjectBuilderStore()
    expect(store.getSchema(BUILDER_ID).entries[0]!.value).toBe("hello")
  })

  it("editing the number value coerces to a number", async () => {
    const wrapper = mount(ObjectBuilder, { props: { builderId: BUILDER_ID } })
    await wrapper
      .find("[data-testid='object-builder-palette-number']")
      .trigger("click")
    await wrapper
      .find("[data-testid='object-builder-inspector-value-number']")
      .setValue("42")

    const store = useObjectBuilderStore()
    expect(store.getSchema(BUILDER_ID).entries[0]!.value).toBe(42)
  })

  it("toggling boolean updates the schema", async () => {
    const wrapper = mount(ObjectBuilder, { props: { builderId: BUILDER_ID } })
    await wrapper
      .find("[data-testid='object-builder-palette-boolean']")
      .trigger("click")

    const store = useObjectBuilderStore()
    const entry = store.getSchema(BUILDER_ID).entries[0]!
    store.updateEntry(BUILDER_ID, entry.id, { value: true })
    expect(store.getSchema(BUILDER_ID).entries[0]!.value).toBe(true)
  })

  it("parses valid JSON for array entries", async () => {
    const wrapper = mount(ObjectBuilder, { props: { builderId: BUILDER_ID } })
    await wrapper
      .find("[data-testid='object-builder-palette-array']")
      .trigger("click")
    await wrapper
      .find("[data-testid='object-builder-inspector-value-json']")
      .setValue("[1, 2, 3]")

    const store = useObjectBuilderStore()
    expect(store.getSchema(BUILDER_ID).entries[0]!.value).toEqual([1, 2, 3])
  })

  it("flags invalid JSON without mutating the entry value", async () => {
    const wrapper = mount(ObjectBuilder, { props: { builderId: BUILDER_ID } })
    await wrapper
      .find("[data-testid='object-builder-palette-array']")
      .trigger("click")
    await wrapper
      .find("[data-testid='object-builder-inspector-value-json']")
      .setValue("not json")

    expect(
      wrapper
        .find("[data-testid='object-builder-inspector-value-json-error']")
        .exists(),
    ).toBe(true)
    const store = useObjectBuilderStore()
    expect(store.getSchema(BUILDER_ID).entries[0]!.value).toEqual([])
  })

  it("rejects non-array JSON for array entries", async () => {
    const wrapper = mount(ObjectBuilder, { props: { builderId: BUILDER_ID } })
    await wrapper
      .find("[data-testid='object-builder-palette-array']")
      .trigger("click")
    await wrapper
      .find("[data-testid='object-builder-inspector-value-json']")
      .setValue('{"a":1}')

    expect(
      wrapper
        .find("[data-testid='object-builder-inspector-value-json-error']")
        .exists(),
    ).toBe(true)
  })

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

    const store = useObjectBuilderStore()
    expect(store.getSchema(BUILDER_ID).entries).toHaveLength(0)
  })

  it("reorders entries when move-up is clicked", async () => {
    const wrapper = mount(ObjectBuilder, { props: { builderId: BUILDER_ID } })
    await wrapper
      .find("[data-testid='object-builder-palette-string']")
      .trigger("click")
    await wrapper
      .find("[data-testid='object-builder-palette-number']")
      .trigger("click")

    const store = useObjectBuilderStore()
    const initial = store.getSchema(BUILDER_ID).entries.map((e) => e.id)
    expect(initial).toHaveLength(2)

    // The second click selected the number entry. Move it up.
    await wrapper
      .find("[data-testid='object-builder-move-up']")
      .trigger("click")
    const reordered = store.getSchema(BUILDER_ID).entries.map((e) => e.id)
    expect(reordered[0]).toBe(initial[1])
    expect(reordered[1]).toBe(initial[0])
  })

  it("loads an existing schema from modelValue on mount", () => {
    const existingSchema: ObjectBuilderSchema = {
      entries: [
        {
          id: "preloaded-1",
          key: "title",
          type: "string",
          value: "Hello",
        },
        {
          id: "preloaded-2",
          key: "count",
          type: "number",
          value: 7,
        },
      ],
    }
    mount(ObjectBuilder, {
      props: {
        builderId: BUILDER_ID,
        modelValue: existingSchema,
      },
    })

    const store = useObjectBuilderStore()
    const schema = store.getSchema(BUILDER_ID)
    expect(schema.entries).toHaveLength(2)
    expect(schema.entries[0]!.key).toBe("title")
    expect(schema.entries[1]!.value).toBe(7)
  })

  it("normalises a plain Record passed via modelValue", () => {
    mount(ObjectBuilder, {
      props: {
        builderId: BUILDER_ID,
        modelValue: { name: "Steve", active: true, items: [1, 2] },
      },
    })

    const store = useObjectBuilderStore()
    const schema = store.getSchema(BUILDER_ID)
    expect(schema.entries.map((e) => e.key).sort()).toEqual([
      "active",
      "items",
      "name",
    ])
    const itemsEntry = schema.entries.find((e) => e.key === "items")
    expect(itemsEntry?.type).toBe("array")
  })

  it("emits update:modelValue on schema mutation", async () => {
    const wrapper = mount(ObjectBuilder, { props: { builderId: BUILDER_ID } })
    await wrapper
      .find("[data-testid='object-builder-palette-string']")
      .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 { entries: unknown[] }
    expect(schema.entries.length).toBe(1)
  })

  it("toRecord helper flattens schema entries into a plain object", () => {
    const schema: ObjectBuilderSchema = {
      entries: [
        { id: "a", key: "title", type: "string", value: "Hi" },
        { id: "b", key: "count", type: "number", value: 3 },
        { id: "c", key: "", type: "string", value: "skipped" },
      ],
    }
    expect(toRecord(schema)).toEqual({ title: "Hi", count: 3 })
  })

  it("changing the entry type resets the value to that type's default", () => {
    setActivePinia(createPinia())
    __resetObjectBuilderIdCounter()
    const store = useObjectBuilderStore()
    const id = store.addEntry(BUILDER_ID, "string")
    store.updateEntry(BUILDER_ID, id, { value: "hello" })
    store.updateEntry(BUILDER_ID, id, { type: "number" })
    const entry = store.getSchema(BUILDER_ID).entries[0]!
    expect(entry.type).toBe("number")
    expect(entry.value).toBe(0)
  })
})
