// Components
import { VCombobox } from '../VCombobox'
import { VForm } from '@/components/VForm'

// Utilities
import { render, screen, showcase, userEvent, wait, waitAnimationFrame, waitIdle } from '@test'
import { commands } from 'vitest/browser'
import { cloneVNode, ref } from 'vue'

const variants = ['underlined', 'outlined', 'filled', 'solo', 'plain'] as const
const densities = ['default', 'comfortable', 'compact'] as const
const items = ['California', 'Colorado', 'Florida', 'Georgia', 'Texas', 'Wyoming'] as const

const stories = Object.fromEntries(Object.entries({
  'Default input': <VCombobox />,
  Disabled: <VCombobox items={ items } disabled />,
  Affixes: <VCombobox items={ items } prefix="prefix" suffix="suffix" />,
  'Prepend/append': <VCombobox items={ items } prependIcon="$vuetify" appendIcon="$vuetify" />,
  'Prepend/append inner': <VCombobox items={ items } prependInnerIcon="$vuetify" appendInnerIcon="$vuetify" />,
  Placeholder: <VCombobox items={ items } placeholder="placeholder" persistentPlaceholder />,
}).map(([k, v]) => [k, (
  <div class="d-flex flex-column flex-grow-1">
    { variants.map(variant => (
      densities.map(density => (
        <div class="d-flex align-start" style="gap: 0.4rem; height: 100px;">
          { cloneVNode(v, { variant, density, label: `${variant} ${density}` }) }
          { cloneVNode(v, { variant, density, label: `with value`, modelValue: ['California'] }) }
          { cloneVNode(v, { variant, density, label: `chips`, chips: true, modelValue: ['California'] }) }
          <VCombobox
            variant={ variant }
            density={ density }
            modelValue={['California']}
            label="selection slot"
            { ...v.props }
          >{{
            selection: ({ item }) => {
              return item
            },
          }}
          </VCombobox>
        </div>
      ))
    )).flat()}
  </div>
)]))

describe('VCombobox', () => {
  describe('closableChips', () => {
    it('should close only first chip', async () => {
      const items = [
        'Item 1',
        'Item 2',
        'Item 3',
        'Item 4',
      ]

      const selectedItems = [
        'Item 1',
        'Item 2',
        'Item 3',
      ]

      render(() => (
        <VCombobox items={ items } modelValue={ selectedItems } multiple closableChips chips />
      ))

      await userEvent.click(screen.getAllByTestId('close-chip')[0])
      await expect.poll(() => screen.getAllByCSS('.v-chip')).toHaveLength(2)
    })
  })

  describe('complex objects', () => {
    it('single', async () => {
      const items = [
        { title: 'Item 1', value: 'item1' },
        { title: 'Item 2', value: 'item2' },
        { title: 'Item 3', value: 'item3' },
        { title: 'Item 4', value: 'item4' },
      ]
      const model = ref()
      const search = ref()
      const updateModel = vi.fn(val => model.value = val)
      const updateSearch = vi.fn(val => search.value = val)

      const { element } = render(() => (
        <VCombobox
          modelValue={ model.value }
          search={ search.value }
          onUpdate:modelValue={ updateModel }
          onUpdate:search={ updateSearch }
          items={ items }
        />
      ))

      await userEvent.click(element)
      await commands.waitStable('.v-list')
      await userEvent.click((await screen.findAllByRole('option'))[0])
      expect(model.value).toStrictEqual(items[0])
      await expect.poll(() => search.value).toBe(items[0].title)
      expect(screen.getByCSS('input')).toHaveValue(items[0].title)
      expect(screen.getByCSS('.v-combobox__selection')).toHaveTextContent(items[0].title)

      await userEvent.click(element)
      await userEvent.keyboard('{ControlOrMeta>}a{/ControlOrMeta}{Backspace}')
      await userEvent.keyboard('Item 2')
      expect(model.value).toBe('Item 2')
      expect(search.value).toBe('Item 2')
      expect(screen.getByCSS('input')).toHaveValue('Item 2')
      expect(screen.getByCSS('.v-combobox__selection')).toHaveTextContent('Item 2')

      await userEvent.click(element)
      await userEvent.keyboard('{ControlOrMeta>}a{/ControlOrMeta}{Backspace}')
      await userEvent.keyboard('item3')
      expect(model.value).toBe('item3')
      expect(search.value).toBe('item3')
      expect(screen.getByCSS('input')).toHaveValue('item3')
      expect(screen.getByCSS('.v-combobox__selection')).toHaveTextContent('item3')
    })

    it('multiple', async () => {
      const items = [
        { title: 'Item 1', value: 'item1' },
        { title: 'Item 2', value: 'item2' },
        { title: 'Item 3', value: 'item3' },
        { title: 'Item 4', value: 'item4' },
      ]
      const model = ref<(string | typeof items[number])[]>([])
      const search = ref()
      const updateModel = vi.fn(val => model.value = val)
      const updateSearch = vi.fn(val => search.value = val)

      const { element } = render(() => (
        <VCombobox
          modelValue={ model.value }
          search={ search.value }
          onUpdate:modelValue={ updateModel }
          onUpdate:search={ updateSearch }
          multiple
          items={ items }
        />
      ))

      const input = screen.getByCSS('input')

      await userEvent.click(element)
      await commands.waitStable('.v-list')
      await userEvent.click(screen.getAllByRole('option')[0])
      expect(model.value).toStrictEqual([items[0]])
      expect(search.value).toBeUndefined()
      expect(input).toHaveValue('')
      expect(screen.getByCSS('.v-combobox__selection')).toHaveTextContent(items[0].title)

      await userEvent.click(element)
      await userEvent.keyboard('Item 2{tab}')
      expect(model.value).toStrictEqual([items[0], 'Item 2'])
      expect(search.value).toBe('')
      expect(input).toHaveValue('')
      expect(screen.getAllByCSS('.v-combobox__selection').at(-1)).toHaveTextContent('Item 2')

      await userEvent.click(element)
      await userEvent.keyboard('item3{tab}')
      expect(model.value).toStrictEqual([items[0], 'Item 2', 'item3'])
      expect(search.value).toBe('')
      expect(input).toHaveValue('')
      expect(screen.getAllByCSS('.v-combobox__selection').at(-1)).toHaveTextContent('item3')
    })
  })

  describe('search', () => {
    it('should filter items', async () => {
      const items = [
        'Item 1',
        'Item 1a',
        'Item 2',
        'Item 2a',
      ]

      const { element } = render(() => (
        <VCombobox items={ items } />
      ))

      await userEvent.click(element)
      await userEvent.keyboard('Item')
      await expect(screen.findAllByRole('option')).resolves.toHaveLength(4)
      await userEvent.keyboard('{ControlOrMeta>}a{/ControlOrMeta}{Backspace}')
      await userEvent.keyboard('Item 1')
      await expect(screen.findAllByRole('option')).resolves.toHaveLength(2)
      await userEvent.keyboard('{ControlOrMeta>}a{/ControlOrMeta}{Backspace}')
      await userEvent.keyboard('Item 3')
      expect(screen.queryAllByRole('option')).toHaveLength(0)
    })

    it('should filter items when using multiple', async () => {
      const items = [
        'Item 1',
        'Item 1a',
        'Item 2',
        'Item 2a',
      ]

      const { element } = render(() => (
        <VCombobox items={ items } multiple />
      ))

      await userEvent.click(element)
      await userEvent.keyboard('Item')
      await expect(screen.findAllByRole('option')).resolves.toHaveLength(4)
      await userEvent.keyboard('{ControlOrMeta>}a{/ControlOrMeta}{Backspace}')
      await userEvent.keyboard('Item 1')
      await expect(screen.findAllByRole('option')).resolves.toHaveLength(2)
      await userEvent.keyboard('{ControlOrMeta>}a{/ControlOrMeta}{Backspace}')
      await userEvent.keyboard('Item 3')
      expect(screen.queryAllByRole('option')).toHaveLength(0)
    })

    it('should filter with custom item shape', async () => {
      const items = [
        {
          id: 1,
          name: 'Test1',
        },
        {
          id: 2,
          name: 'Antonsen PK',
        },
      ]

      const { element } = render(() => (
        <VCombobox
          items={ items }
          itemValue="id"
          itemTitle="name"
        />
      ))

      await userEvent.click(element)
      await userEvent.keyboard('test')
      await expect(screen.findByRole('option')).resolves.toHaveTextContent('Test1')

      await userEvent.keyboard('{ControlOrMeta>}a{/ControlOrMeta}{Backspace}')
      await userEvent.keyboard('antonsen')
      await expect(screen.findByRole('option')).resolves.toHaveTextContent('Antonsen PK')
    })
  })

  describe('prefilled data', () => {
    it('should work with array of strings when using multiple', async () => {
      const items = ref(['California', 'Colorado', 'Florida'])

      const selectedItems = ref(['California', 'Colorado'])

      const { element } = render(() => (
        <VCombobox
          v-model={ selectedItems.value }
          items={ items.value }
          multiple
          chips
          closableChips
        />
      ))

      const input = screen.getByCSS('input')

      await userEvent.click(element)

      await expect(screen.findAllByRole('option', { selected: true })).resolves.toHaveLength(2)
      expect(screen.getAllByCSS('.v-chip')).toHaveLength(2)

      await userEvent.click(screen.getAllByTestId('close-chip')[0])
      await expect(input).toBeVisible()
      expect(screen.getAllByCSS('.v-chip')).toHaveLength(1)
      expect(selectedItems.value).toStrictEqual(['Colorado'])
    })

    it('should work with objects when using multiple', async () => {
      const items = ref([
        {
          title: 'Item 1',
          value: 'item1',
        },
        {
          title: 'Item 2',
          value: 'item2',
        },
        {
          title: 'Item 3',
          value: 'item3',
        },
      ])

      const selectedItems = ref(
        [
          {
            title: 'Item 1',
            value: 'item1',
          },
          {
            title: 'Item 2',
            value: 'item2',
          },
        ]
      )

      const { element } = render(() => (
        <VCombobox
          v-model={ selectedItems.value }
          items={ items.value }
          multiple
          chips
          closableChips
          returnObject
        />
      ))

      const input = screen.getByCSS('input')

      await userEvent.click(element)

      await expect(screen.findAllByRole('option', { selected: true })).resolves.toHaveLength(2)
      expect(screen.getAllByCSS('.v-chip')).toHaveLength(2)

      await userEvent.click(screen.getAllByTestId('close-chip')[0])
      await expect(input).toBeVisible()
      expect(screen.getAllByCSS('.v-chip')).toHaveLength(1)
      expect(selectedItems.value).toStrictEqual([{
        title: 'Item 2',
        value: 'item2',
      }])
    })

    it('should work with objects when using multiple and item-value', async () => {
      const items = ref([
        {
          text: 'Item 1',
          id: 'item1',
        },
        {
          text: 'Item 2',
          id: 'item2',
        },
        {
          text: 'Item 3',
          id: 'item3',
        },
      ])

      const selectedItems = ref(
        [
          {
            text: 'Item 1',
            id: 'item1',
          },
          {
            text: 'Item 2',
            id: 'item2',
          },
        ]
      )

      const { element } = render(() => (
        <VCombobox
          v-model={ selectedItems.value }
          items={ items.value }
          multiple
          itemTitle="text"
          itemValue="value"
          returnObject
        />
      ))

      await userEvent.click(element)
      await commands.waitStable('.v-list')

      const options = await screen.findAllByRole('option', { selected: true })
      expect(options).toHaveLength(2)
      const inputField = screen.getByCSS('.v-field')
      expect(inputField).toHaveTextContent('Item 1')
      expect(inputField).toHaveTextContent('Item 2')

      await userEvent.click(options[0])

      expect(selectedItems.value).toStrictEqual([{
        text: 'Item 2',
        id: 'item2',
      }])
    })
  })

  describe('readonly', () => {
    it('should not be clickable when in readonly', async () => {
      const items = ['Item 1', 'Item 2', 'Item 3', 'Item 4']

      const selectedItems = 'Item 1'

      const { element } = render(() => (
        <VCombobox
          items={ items }
          modelValue={ selectedItems }
          readonly
        />
      ))

      await userEvent.click(element)

      expect(screen.queryAllByRole('option')).toHaveLength(0)
      expect(screen.queryAllByRole('listbox')).toHaveLength(0)

      await userEvent.keyboard('{ArrowDown}')
      expect(screen.queryAllByRole('option')).toHaveLength(0)
      expect(screen.queryAllByRole('listbox')).toHaveLength(0)
    })

    it('should not be clickable when in readonly form', async () => {
      const items = ['Item 1', 'Item 2', 'Item 3', 'Item 4']

      const selectedItems = 'Item 1'

      const { element } = render(() => (
        <VForm readonly>
          <VCombobox
            items={ items }
            modelValue={ selectedItems }
            readonly
          />
        </VForm>
      ))

      await userEvent.click(element)

      expect(screen.queryAllByRole('option')).toHaveLength(0)
      expect(screen.queryAllByRole('listbox')).toHaveLength(0)

      await userEvent.keyboard('{ArrowDown}')
      expect(screen.queryAllByRole('option')).toHaveLength(0)
      expect(screen.queryAllByRole('listbox')).toHaveLength(0)
    })
  })

  describe('hide-selected', () => {
    it('should hide selected item(s)', async () => {
      const items = [
        'Item 1',
        'Item 2',
        'Item 3',
        'Item 4',
      ]

      const selectedItems = [
        'Item 1',
        'Item 2',
      ]

      const { element } = render(() => (
        <VCombobox items={ items } modelValue={ selectedItems } multiple hideSelected />
      ))

      await userEvent.click(element)

      const listItems = await screen.findAllByRole('option')
      expect(listItems).toHaveLength(2)
      expect(listItems[0]).toHaveTextContent('Item 3')
      expect(listItems[1]).toHaveTextContent('Item 4')
    })
  })

  // // https://github.com/vuetifyjs/vuetify/issues/17120
  it('should display 0 when selected', async () => {
    const items = [0, 1, 2, 3, 4]

    const selectedItems = ref(undefined)

    const { element } = render(() => (
      <VCombobox
        items={ items }
        v-model={ selectedItems.value }
      />
    ))

    await userEvent.click(element)
    await commands.waitStable('.v-list')

    await userEvent.click(screen.getAllByRole('option')[0])

    await expect.poll(() => screen.getByCSS('input')).toHaveValue('0')
  })

  it('should conditionally show placeholder', async () => {
    const { rerender } = render(VCombobox, {
      props: { placeholder: 'Placeholder' },
    })

    const input = screen.getByCSS('input')
    await expect.element(input).toHaveAttribute('placeholder', 'Placeholder')

    await rerender({ label: 'Label' })
    await expect.element(input).toBeVisible()
    expect(Number(window.getComputedStyle(input, '::placeholder').opacity)).toBe(0)

    await userEvent.click(input)
    await expect.element(input).toHaveAttribute('placeholder', 'Placeholder')
    await expect.element(input).toBeVisible()
    expect(Number(window.getComputedStyle(input, '::placeholder').opacity)).toBeGreaterThan(0.2)

    await userEvent.tab()
    await rerender({ persistentPlaceholder: true })
    await expect.element(input).toHaveAttribute('placeholder', 'Placeholder')
    await expect.element(input).toBeVisible()
    expect(Number(window.getComputedStyle(input, '::placeholder').opacity)).toBeGreaterThan(0.2)

    await rerender({ modelValue: 'Foobar' })
    await expect.element(input).not.toHaveAttribute('placeholder')

    await rerender({ multiple: true, modelValue: ['Foobar'] })
    await expect.element(input).not.toHaveAttribute('placeholder')
  })

  it('should keep TextField focused while selecting items from open menu', async () => {
    const { element } = render(() => (
      <VCombobox
        multiple
        items={['California', 'Colorado', 'Florida', 'Georgia', 'Texas', 'Wyoming']}
      />
    ))

    await userEvent.click(element)

    await userEvent.keyboard('{ArrowDown}')
    await userEvent.keyboard('{ArrowDown}')
    await userEvent.keyboard('{ArrowDown}')

    expect(screen.getByCSS('.v-field')).toHaveClass('v-field--focused')
  })

  it('should not open menu when closing a chip', async () => {
    const { element } = render(() => (
      <VCombobox
        chips
        closableChips
        items={['foo', 'bar']}
        label="Select"
        modelValue={['foo', 'bar']}
        multiple
      />
    ))

    expect(screen.queryAllByRole('listbox')).toHaveLength(0)

    await userEvent.click(screen.getAllByTestId('close-chip')[1])
    expect(screen.queryAllByRole('listbox')).toHaveLength(0)

    await userEvent.click(screen.getByTestId('close-chip'))
    expect(screen.queryAllByRole('listbox')).toHaveLength(0)

    await userEvent.click(element)
    await commands.waitStable('.v-list')

    expect(screen.queryAllByRole('listbox')).toHaveLength(1)
    await userEvent.keyboard('{Escape}')
    await expect.poll(() => screen.queryAllByRole('listbox')).toHaveLength(0)
  })

  describe('auto-select-first', () => {
    it('should auto-select-first item when pressing enter', async () => {
      const selectedItems = ref([])

      const { element } = render(() => (
        <VCombobox
          v-model={ selectedItems.value }
          items={['California', 'Colorado', 'Florida', 'Georgia', 'Texas', 'Wyoming']}
          multiple
          autoSelectFirst
        />
      ))

      await userEvent.click(element)
      await commands.waitStable('.v-list')

      expect(screen.getAllByRole('option')).toHaveLength(6)

      await userEvent.keyboard('Cal')
      await expect(screen.findByRole('option')).resolves.toHaveClass('v-list-item--active')
      await userEvent.keyboard('{Enter}')
      await expect.poll(() => screen.queryAllByRole('option')).toHaveLength(6)
      expect(selectedItems.value).toStrictEqual(['California'])
    })

    it('should auto-select-first item when pressing tab', async () => {
      const selectedItems = ref([])

      const { element } = render(() => (
        <VCombobox
          v-model={ selectedItems.value }
          items={['California', 'Colorado', 'Florida', 'Georgia', 'Texas', 'Wyoming']}
          multiple
          autoSelectFirst
        />
      ))

      await userEvent.click(element)
      await commands.waitStable('.v-list')

      expect(screen.getAllByRole('option')).toHaveLength(6)

      await userEvent.keyboard('Cal')
      await expect(screen.findByRole('option')).resolves.toHaveClass('v-list-item--active')
      await userEvent.keyboard('{Tab}')
      await expect.poll(() => screen.queryAllByRole('option')).toHaveLength(0)
      expect(selectedItems.value).toStrictEqual(['California'])
    })

    it('should not auto-select-first item when blur', async () => {
      const selectedItems = ref([])

      const { element } = render(() => (
        <VCombobox
          v-model={ selectedItems.value }
          items={['California', 'Colorado', 'Florida', 'Georgia', 'Texas', 'Wyoming']}
          multiple
          autoSelectFirst
        />
      ))

      await userEvent.click(element)
      await commands.waitStable('.v-list')

      expect(screen.getAllByRole('option')).toHaveLength(6)

      await userEvent.keyboard('Cal')
      await expect(screen.findByRole('option')).resolves.toHaveClass('v-list-item--active')
      await userEvent.click(document.body)
      await expect.poll(() => screen.queryAllByRole('option')).toHaveLength(0)
      expect(selectedItems.value).toStrictEqual(['Cal'])
    })
  })

  it(`doesn't add duplicate values`, async () => {
    const selection = ref([])
    const { element } = render(() => (
      <VCombobox v-model={ selection.value } multiple />
    ))

    await userEvent.click(element)
    await userEvent.keyboard('foo{Enter}')
    await userEvent.keyboard('bar{Enter}')
    expect(selection.value).toHaveLength(2)

    await userEvent.keyboard('foo{Enter}')
    expect(selection.value).toHaveLength(2)
  })

  // https://github.com/vuetifyjs/vuetify/issues/18796
  it('should allow deleting selection via closable-chips', async () => {
    const selectedItem = ref('California')

    render(() => (
      <VCombobox
        chips
        v-model={ selectedItem.value }
        closableChips
        items={['California', 'Colorado', 'Florida', 'Georgia', 'Texas', 'Wyoming']}
      />
    ))

    await userEvent.click(screen.getByTestId('close-chip'))
    expect(selectedItem.value).toBeNull()
  })

  // https://github.com/vuetifyjs/vuetify/issues/18556
  it('should show menu if focused and items are added', async () => {
    const { rerender } = render(VCombobox)

    await userEvent.keyboard('{Tab}')
    await waitAnimationFrame()
    expect(screen.queryByRole('listbox')).toBeNull()

    await rerender({ items: ['Foo', 'Bar'] })
    await expect(screen.findByRole('listbox')).resolves.toBeVisible()
  })

  // https://github.com/vuetifyjs/vuetify/issues/19346
  it('should not show menu when focused and existing non-empty items are changed', async () => {
    const { element, rerender } = render(VCombobox, {
      props: { items: ['Foo', 'Bar'] },
    })

    await userEvent.click(element)
    await commands.waitStable('.v-list')
    await expect(screen.findByRole('listbox')).resolves.toBeVisible()

    await userEvent.click(screen.getAllByRole('option')[0])
    await rerender({ items: ['Foo', 'Bar', 'test'] })
    await waitIdle()
    await expect.poll(() => screen.queryByRole('listbox')).toBeNull()
  })

  // https://github.com/vuetifyjs/vuetify/issues/17573
  // When using selection slot or chips, input displayed next to chip/selection slot should be always empty
  it('should always have empty input value when it is unfocused and when using selection slot or chips', async () => {
    const items = ['Item 1', 'Item 2', 'Item 3', 'Item 4']
    const selectedItem = ref('Item 1')

    const { element, getByCSS } = render(() => (
      <VCombobox
        items={ items }
        chips
        v-model={ selectedItem.value }
      />
    ))

    await userEvent.click(element)
    const input = getByCSS('input')
    expect(input).toHaveValue('')

    // Blur input with a custom search input value
    await userEvent.keyboard('test')
    input.blur()
    await expect.poll(() => selectedItem.value).toBe('test')
    expect(input).toHaveValue('')

    await expect.poll(() => screen.queryByCSS('.v-overlay__content')).toBeNull()

    // Press enter key with a custom search input value
    await userEvent.click(element)
    await userEvent.keyboard('test 2{Enter}')
    await expect.poll(() => selectedItem.value).toBe('test 2')
    expect(input).toHaveValue('')

    // Close the menu kept open by the previous selection before reopening,
    // otherwise clicking an already-open field races the close/open transition
    await userEvent.keyboard('{Escape}')
    await expect.poll(() => screen.queryByCSS('.v-overlay__content')).toBeNull()

    // Search existing item and click to select
    await userEvent.click(element)
    expect(input).toHaveValue('')
    await userEvent.keyboard('Item 1')
    await commands.waitStable('.v-list')
    await userEvent.click(await screen.findByRole('option'))
    await expect.poll(() => selectedItem.value).toBe('Item 1')
  })

  // https://github.com/vuetifyjs/vuetify/issues/19319
  it('should respect return-object when blurring', async () => {
    const items = [
      { title: 'Item 1', value: 'item1' },
      { title: 'Item 2', value: 'item2' },
      { title: 'Item 3', value: 'item3' },
      { title: 'Item 4', value: 'item4' },
    ]
    const model = ref()
    const search = ref()

    const { element } = render(() => (
      <VCombobox
        search={ search.value }
        v-model={ model.value }
        items={ items }
      />
    ))

    await userEvent.click(element)
    await commands.waitStable('.v-list')
    await userEvent.click(screen.getAllByRole('option')[0])
    await expect.poll(() => model.value).toStrictEqual({ title: 'Item 1', value: 'item1' })

    await userEvent.click(document.body)
    expect(model.value).toStrictEqual({ title: 'Item 1', value: 'item1' })
  })

  it('should not fire @update:focus twice when clicking bottom of input', async () => {
    const onFocus = vi.fn()
    const { element } = render(() => (
      <VCombobox onUpdate:focused={ onFocus } />
    ))

    await userEvent.click(element, { position: { x: 10, y: 55 } })

    expect(onFocus).toHaveBeenCalledTimes(1)
  })

  it.each([
    { delimiters: [','], text: 'abc,foo, baz', expected: ['abc', 'foo', 'baz'] },
    { delimiters: [':'], text: '012:2,32:0:1', expected: ['012', '2,32', '0', '1'] },
    { delimiters: [':', '.', '-'], text: '(1) 231:13 - 123.', expected: ['(1) 231', '13', '123'] },
  ])('should ingest new items when a delimited text is pasted', async ({ delimiters, text, expected }) => {
    const model = ref(null)
    render(() => (
      <VCombobox
        v-model={ model.value }
        delimiters={ delimiters }
        multiple
      />
    ))
    await userEvent.tab()
    const lock = await commands.getLock()
    await navigator.clipboard.writeText(text)
    await userEvent.paste()
    await commands.releaseLock(lock)
    expect(model.value).toEqual(expected)
  })

  it('should show only matching items when reopening the menu if alwaysFilter is true', async () => {
    const { element } = render(() => (
      <VCombobox alwaysFilter items={['California', 'Colorado', 'Florida', 'Georgia', 'Texas', 'Wyoming']} />
    ))

    await userEvent.click(element)
    await userEvent.keyboard('c')
    await expect(screen.findAllByRole('option')).resolves.toHaveLength(2)
    await userEvent.keyboard('al')
    await expect(screen.findAllByRole('option')).resolves.toHaveLength(1)
    await userEvent.click(document.body)
    await expect.poll(() => screen.queryAllByRole('option')).toHaveLength(0)
    await userEvent.click(element)
    await expect.poll(() => screen.queryAllByRole('option')).toHaveLength(1)
  })

  it('should create new items when pasting with line break characters', async () => {
    const model = ref(null)
    render(() => (
      <VCombobox
        v-model={ model.value }
        multiple
        delimiters={[',']}
      />
    ))

    await userEvent.tab()
    const lock = await commands.getLock()
    await navigator.clipboard.writeText('foo,\nbar')
    await userEvent.paste()
    await commands.releaseLock(lock)
    expect(model.value).toEqual(['foo', 'bar'])
  })

  describe('menu-header and menu-footer slots', () => {
    it('should render menu-header and menu-footer slots', async () => {
      const { element } = render(() => (
        <VCombobox menu items={['Item #1', 'Item #2']}>
          {{
            'menu-header': () => (
              <div data-testid="header-content">My Header</div>
            ),
            'menu-footer': () => (
              <div data-testid="footer-content">My Footer</div>
            ),
          }}
        </VCombobox>
      ))

      await userEvent.click(element)
      await commands.waitStable('.v-list')

      expect(screen.getByTestId('header-content')).toHaveTextContent('My Header')
      expect(screen.getByTestId('footer-content')).toHaveTextContent('My Footer')
    })

    it('should navigate between header, list, and footer with Tab', async () => {
      const { element } = render(() => (
        <VCombobox menu items={['Item #1', 'Item #2', 'Item #3']}>
          {{
            'menu-header': () => (
              <div>
                <button data-testid="header-btn">Header Button</button>
              </div>
            ),
            'menu-footer': () => (
              <div>
                <button data-testid="footer-btn">Footer Button</button>
              </div>
            ),
          }}
        </VCombobox>
      ))

      await userEvent.click(element)
      await commands.waitStable('.v-list')

      // Navigate to list first
      await userEvent.keyboard('{ArrowDown}')
      await expect.poll(() => screen.getByTestId('header-btn')).toHaveFocus()

      // Tab to list
      await userEvent.keyboard('{Tab}')
      expect(screen.getAllByRole('option').at(0)).toHaveFocus()

      // Tab to footer
      await userEvent.keyboard('{Tab}')
      expect(screen.getByTestId('footer-btn')).toHaveFocus()

      // Shift+Tab back to list
      await userEvent.keyboard('{Shift>}{Tab}{/Shift}')
      await expect.poll(() => screen.getAllByRole('option').at(0)).toHaveFocus()

      // Shift+Tab back to header
      await userEvent.keyboard('{Shift>}{Tab}{/Shift}')
      expect(screen.getByTestId('header-btn')).toHaveFocus()
    })
  })

  it('should keep menu open and repair focus when the focused item is removed', async () => {
    const menu = ref(false)
    render(() => (
      <VCombobox
        v-model:menu={ menu.value }
        items={ Array.from({ length: 50 }, (_, i) => `Item ${i + 1}`) }
      />
    ))

    const input = screen.getByCSS('input')
    await userEvent.click(input)
    await waitIdle()
    expect(menu.value).toBe(true)

    const option = screen.getAllByRole('option')[0]
    option.focus()
    await waitIdle()
    expect(document.activeElement).toBe(option)

    // The focused option is removed (virtual-scroll recycle or async items reload);
    // focus falls to <body> and the menu would close.
    option.remove()
    await waitIdle()
    await wait(300)

    // Repaired: focus returns into the menu content and the menu stays open.
    expect(menu.value).toBe(true)
    expect(screen.getByRole('listbox').contains(document.activeElement)).toBe(true)
  })

  describe('virtual list with selection', () => {
    const manyItems = Array.from({ length: 1000 }, (_, i) => i)

    beforeEach(() => commands.setReduceMotionDisabled())

    afterEach(() => commands.setReduceMotionEnabled())

    it('should open near the selected item and arrow from selection', async () => {
      render(() => (
        <VCombobox items={ manyItems } modelValue={ 100 } />
      ))

      await userEvent.click(screen.getByCSS('input'))
      await commands.waitStable('.v-list')

      await expect.poll(() => screen.getAllByRole('option')
        .map(el => el.textContent))
        .toContain('100')

      await userEvent.keyboard('{ArrowDown}')
      expect(document.activeElement?.textContent?.trim()).toBe('101')

      await userEvent.keyboard('{ArrowUp}')
      expect(document.activeElement?.textContent?.trim()).toBe('100')
    })
  })

  showcase({ stories })
})
