import { describe, it, expect, afterEach, beforeAll } from "vitest"
import { mount } from "@vue/test-utils"
import { h, nextTick } from "vue"
import {
  Combobox,
  ComboboxAnchor,
  ComboboxContent,
  ComboboxEmpty,
  ComboboxGroup,
  ComboboxInput,
  ComboboxItem,
  ComboboxItemIndicator,
  ComboboxSeparator,
} from "."

beforeAll(() => {
  if (typeof global.ResizeObserver === "undefined") {
    global.ResizeObserver = class {
      observe(): void {}
      unobserve(): void {}
      disconnect(): void {}
    } as unknown as typeof ResizeObserver
  }
})

function mountCombobox(
  rootProps: Record<string, unknown> = {},
  itemValues: string[] = ["apple", "banana", "cherry"],
) {
  return mount(Combobox, {
    props: { open: true, ...rootProps },
    attachTo: document.body,
    slots: {
      default: () => [
        h(ComboboxAnchor, null, {
          default: () => h(ComboboxInput, { placeholder: "Search fruits..." }),
        }),
        h(ComboboxContent, null, {
          default: () => [
            h(ComboboxEmpty, null, { default: () => "No results" }),
            ...itemValues.map((v) =>
              h(
                ComboboxItem,
                { value: v },
                { default: () => v.charAt(0).toUpperCase() + v.slice(1) },
              ),
            ),
          ],
        }),
      ],
    },
  })
}

describe("Combobox", () => {
  afterEach(() => {
    document.body.innerHTML = ""
  })

  describe("default rendering", () => {
    it("renders without errors", () => {
      const wrapper = mountCombobox()
      expect(wrapper.exists()).toBe(true)
      wrapper.unmount()
    })

    it("renders anchor with base classes", () => {
      const wrapper = mountCombobox()
      const anchor = wrapper.element.querySelector(":scope > *") as HTMLElement | null
      // The Combobox root renders the anchor as a child element.
      expect(wrapper.html()).toContain("border-input")
      expect(wrapper.html()).toContain("bg-background")
      expect(anchor).toBeTruthy()
      wrapper.unmount()
    })

    it("renders an input element with placeholder", () => {
      const wrapper = mountCombobox()
      const input = wrapper.find("input")
      expect(input.exists()).toBe(true)
      expect(input.attributes("placeholder")).toBe("Search fruits...")
      wrapper.unmount()
    })
  })

  describe("portal content when open", () => {
    it("renders items in the portal", async () => {
      const wrapper = mountCombobox()
      await nextTick()
      const items = document.body.querySelectorAll('[role="option"]')
      expect(items.length).toBe(3)
      expect(items[0]?.textContent).toContain("Apple")
      expect(items[1]?.textContent).toContain("Banana")
      expect(items[2]?.textContent).toContain("Cherry")
      wrapper.unmount()
    })

    it("content has popover styling classes", async () => {
      const wrapper = mountCombobox()
      await nextTick()
      const content = document.body.querySelector('[role="listbox"]')
      expect(content).toBeTruthy()
      expect(content?.className).toContain("rounded-md")
      expect(content?.className).toContain("border")
      expect(content?.className).toContain("bg-popover")
      expect(content?.className).toContain("shadow-md")
      wrapper.unmount()
    })

    it("items have base item classes", async () => {
      const wrapper = mountCombobox()
      await nextTick()
      const item = document.body.querySelector('[role="option"]')
      expect(item).toBeTruthy()
      for (const cls of [
        "relative",
        "flex",
        "cursor-default",
        "select-none",
        "items-center",
        "rounded-sm",
        "py-1.5",
        "pl-8",
        "pr-2",
        "text-sm",
      ]) {
        expect(item?.className).toContain(cls)
      }
      wrapper.unmount()
    })
  })

  describe("search filtering", () => {
    it("filters items by typing into the input", async () => {
      const wrapper = mountCombobox()
      await nextTick()
      const input = wrapper.find("input").element as HTMLInputElement
      input.value = "ban"
      input.dispatchEvent(new Event("input", { bubbles: true }))
      await nextTick()
      await nextTick()
      const items = document.body.querySelectorAll('[role="option"]')
      // We don't enforce exact filter semantics (reka filtering varies), but
      // we expect typing not to crash and the input value to update.
      expect(input.value).toBe("ban")
      expect(items.length).toBeGreaterThanOrEqual(0)
      wrapper.unmount()
    })
  })

  describe("empty state", () => {
    it("renders the empty slot text in the DOM", async () => {
      const wrapper = mountCombobox({}, [])
      await nextTick()
      expect(document.body.textContent).toContain("No results")
      wrapper.unmount()
    })
  })

  describe("v-model", () => {
    it("accepts an initial modelValue without erroring", () => {
      const wrapper = mountCombobox({ modelValue: "apple" })
      expect(wrapper.exists()).toBe(true)
      wrapper.unmount()
    })

    it("supports array modelValue for multi-select", () => {
      const wrapper = mountCombobox({ multiple: true, modelValue: ["apple", "banana"] })
      expect(wrapper.exists()).toBe(true)
      wrapper.unmount()
    })
  })

  describe("grouping + separator", () => {
    it("renders grouped items with a separator", async () => {
      const wrapper = mount(Combobox, {
        props: { open: true },
        attachTo: document.body,
        slots: {
          default: () => [
            h(ComboboxAnchor, null, {
              default: () => h(ComboboxInput, { placeholder: "Search..." }),
            }),
            h(ComboboxContent, null, {
              default: () => [
                h(ComboboxGroup, null, {
                  default: () => [
                    h(ComboboxItem, { value: "a" }, { default: () => "Alpha" }),
                  ],
                }),
                h(ComboboxSeparator),
                h(ComboboxGroup, null, {
                  default: () => [
                    h(ComboboxItem, { value: "b" }, { default: () => "Beta" }),
                  ],
                }),
              ],
            }),
          ],
        },
      })
      await nextTick()
      expect(document.body.textContent).toContain("Alpha")
      expect(document.body.textContent).toContain("Beta")
      wrapper.unmount()
    })
  })

  describe("item indicator", () => {
    it("renders a check svg inside indicator slot default", async () => {
      const wrapper = mount(Combobox, {
        props: { open: true, modelValue: "apple" },
        attachTo: document.body,
        slots: {
          default: () => [
            h(ComboboxAnchor, null, {
              default: () => h(ComboboxInput, { placeholder: "Search..." }),
            }),
            h(ComboboxContent, null, {
              default: () => [
                h(
                  ComboboxItem,
                  { value: "apple" },
                  {
                    default: () => [h(ComboboxItemIndicator), "Apple"],
                  },
                ),
              ],
            }),
          ],
        },
      })
      await nextTick()
      const svg = document.body.querySelector("svg")
      expect(svg).toBeTruthy()
      wrapper.unmount()
    })
  })

  describe("custom class", () => {
    it("anchor accepts custom class", () => {
      const wrapper = mount(Combobox, {
        props: { open: true },
        attachTo: document.body,
        slots: {
          default: () => h(ComboboxAnchor, { class: "my-anchor" }, {
            default: () => h(ComboboxInput, { placeholder: "..." }),
          }),
        },
      })
      expect(wrapper.html()).toContain("my-anchor")
      wrapper.unmount()
    })
  })
})
