// Components
import { VSelect } from '../VSelect'
import { VDialog } from '@/components/VDialog'
import { VForm } from '@/components/VForm'
import { VListItem } from '@/components/VList'
import { VTextField } from '@/components/VTextField'

// Utilities
import { commands, render, screen, showcase, userEvent, wait, waitForClickable } from '@test'
import { getAllByRole, waitFor } from '@testing-library/vue'
import { cloneVNode, computed, nextTick, 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': <VSelect items={ items } />,
  Disabled: <VSelect items={ items } disabled />,
  Affixes: <VSelect items={ items } prefix="prefix" suffix="suffix" />,
  'Prepend/append': <VSelect items={ items } prependIcon="$vuetify" appendIcon="$vuetify" />,
  'Prepend/append inner': <VSelect items={ items } prependInnerIcon="$vuetify" appendInnerIcon="$vuetify" />,
  Placeholder: <VSelect 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'] }) }
          <VSelect
            variant={ variant }
            density={ density }
            modelValue={['California']}
            label="selection slot"
            { ...v.props }
          >{{
            selection: ({ item }) => {
              return item
            },
          }}
          </VSelect>
        </div>
      ))
    )).flat()}
  </div>
)]))

describe('VSelect', () => {
  it('should toggle menu with dropdown icon', async () => {
    const { element } = render(() => (
      <VSelect items={['Item #1', 'Item #2']} />
    ))

    const menuIcon = screen.getByCSS('.v-icon')
    expect(screen.queryAllByCSS('.v-list-item')).toHaveLength(0)
    expect(element).not.toHaveClass('v-select--active-menu')

    await userEvent.click(menuIcon)
    await expect.poll(() => screen.queryAllByCSS('.v-list-item')).toHaveLength(2)
    expect(element).toHaveClass('v-select--active-menu')

    await userEvent.click(menuIcon)
    await expect.poll(() => screen.queryAllByCSS('.v-list-item')).toHaveLength(0)
    expect(element).not.toHaveClass('v-select--active-menu')
  })

  it('should render selection slot', () => {
    const items = [
      { title: 'a' },
      { title: 'b' },
      { title: 'c' },
    ]
    let model: { title: string }[] = [{ title: 'b' }]

    render(() => (
      <VSelect
        multiple
        returnObject
        items={ items }
        modelValue={ model }
        onUpdate:modelValue={ val => model = val }
      >
        {{
          selection: ({ item, index }) => {
            return item.title.toUpperCase()
          },
        }}
      </VSelect>
    ))

    expect(screen.getByCSS('.v-select__selection')).toHaveTextContent('B')
  })

  it('should render prepend-item slot', () => {
    render(() => (
      <VSelect menu items={['Item #1', 'Item #2']}>
        {{
          'prepend-item': () => (
            <VListItem title="Foo"></VListItem>
          ),
        }}
      </VSelect>
    ))

    expect(screen.getAllByCSS('.v-list-item')[0]).toHaveTextContent('Foo')
  })

  it('should render append-item slot', () => {
    render(() => (
      <VSelect menu items={['Item #1', 'Item #2']}>
        {{
          'append-item': () => (
            <VListItem title="Foo"></VListItem>
          ),
        }}
      </VSelect>
    ))
    expect(screen.getAllByCSS('.v-list-item').at(-1)).toHaveTextContent('Foo')
  })

  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(() => (
      <VSelect
        items={ items }
        modelValue={ selectedItems }
        chips
        closableChips
        multiple
      />
    ))

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

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

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

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

      await userEvent.click(element)
      await expect(screen.findAllByRole('option', { selected: true })).resolves.toHaveLength(2)

      const option = screen.getAllByRole('option')[2]
      await waitForClickable(option)
      await userEvent.click(option)
      expect(selectedItems.value).toStrictEqual(['California', 'Colorado', 'Florida'])

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

    it('should work with objects when using multiple', async () => {
      const items = [
        {
          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(() => (
        <VSelect
          v-model={ selectedItems.value }
          items={ items }
          multiple
          chips
          closableChips
          returnObject
        />
      ))

      await userEvent.click(element)
      await expect(screen.findAllByRole('option', { selected: true })).resolves.toHaveLength(2)
      const option = screen.getAllByRole('option')[2]
      await waitForClickable(option)
      await userEvent.click(option)
      expect(selectedItems.value).toStrictEqual([
        {
          title: 'Item 1',
          value: 'item1',
        },
        {
          title: 'Item 2',
          value: 'item2',
        },
        {
          title: 'Item 3',
          value: 'item3',
        },
      ])

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

    it('should work with objects when using multiple and item-value', async () => {
      const items = [
        {
          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(() => (
        <VSelect
          v-model={ selectedItems.value }
          items={ items }
          multiple
          returnObject
          itemTitle="text"
          itemValue="id"
        />
      ))

      await userEvent.click(element)
      await commands.waitStable('.v-list')
      const options = screen.getAllByRole('option', { selected: true })
      expect(options).toHaveLength(2)
      expect(element).toHaveTextContent('Item 1')
      expect(element).toHaveTextContent('Item 2')

      await waitForClickable(options[0])
      await userEvent.click(options[0])
      expect(selectedItems.value).toStrictEqual([{
        text: 'Item 2',
        id: 'item2',
      }])
    })
  })

  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(() => (
      <VSelect
        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>
        <VSelect
          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)
  })

  // https://github.com/vuetifyjs/vuetify/issues/16442
  describe('null value', () => {
    it('should allow null as legit itemValue', async () => {
      const items = [
        { name: 'Default Language', code: null },
        { code: 'en-US', name: 'English' },
        { code: 'de-DE', name: 'German' },
      ]

      const selectedItems = null

      const { element } = render(() => (
        <VSelect
          items={ items }
          modelValue={ selectedItems }
          itemTitle="name"
          itemValue="code"
        />
      ))
      expect(element).toHaveTextContent('Default Language')
    })

    it('should mark input as "not dirty" when the v-model is null, but null is not present in the items', async () => {
      const items = [
        { code: 'en-US', name: 'English' },
        { code: 'de-DE', name: 'German' },
      ]

      render(() => (
        <VSelect
          label="Language"
          items={ items }
          modelValue={ null }
          itemTitle="name"
          itemValue="code"
        />
      ))
      expect(screen.queryAllByCSS('.v-field--dirty')).toHaveLength(0)
    })
  })

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

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

    await rerender({ label: 'Label' })
    await expect.element(input).not.toHaveAttribute('placeholder')
    input.focus()
    await expect.element(input).toHaveAttribute('placeholder', 'Placeholder')
    input.blur()
    await expect.element(input).not.toHaveAttribute('placeholder')

    await rerender({ persistentPlaceholder: true })
    await expect.element(input).toHaveAttribute('placeholder', 'Placeholder')

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

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

  // https://github.com/vuetifyjs/vuetify/issues/16210
  it('should return item object as the argument of item-title function', async () => {
    const items = [
      { id: 1, name: 'a' },
      { id: 2, name: 'b' },
    ]

    const selectedItems = ref(null)

    const itemTitleFunc = vi.fn((item: any) => (
      'Item: ' + JSON.stringify(item)
    ))

    const { element } = render(() => (
      <VSelect
        items={ items }
        modelValue={ selectedItems }
        itemTitle={ itemTitleFunc }
        itemValue="id"
      />
    ))

    await userEvent.click(element)
    await commands.waitStable('.v-list')
    await userEvent.click(screen.getAllByRole('option')[0])
    expect(selectedItems.value).toBe(1)
    expect(itemTitleFunc).toHaveBeenCalledWith({ id: 1, name: 'a' }, expect.anything())
    expect(element).toHaveTextContent(`Item: {"id":1,"name":"a"}`)
  })

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

      const selectedItems = ref(['Item 1', 'Item 2'])

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

      await userEvent.click(element)
      await commands.waitStable('.v-list')
      const options = screen.getAllByRole('option')
      expect(options).toHaveLength(2)
      expect(options[0]).toHaveTextContent('Item 3')
      expect(options[1]).toHaveTextContent('Item 4')
    })

    // https://github.com/vuetifyjs/vuetify/issues/19806
    it('should hide selected item(s) with return-object', async () => {
      const selectedItem = ref({ text: 'Item 1', id: 'item1' })
      const items = [
        {
          text: 'Item 1',
          id: 'item1',
        },
        {
          text: 'Item 2',
          id: 'item2',
        },
        {
          text: 'Item 3',
          id: 'item3',
        },
      ]
      const { element } = render(() => (
        <VSelect
          v-model={ selectedItem.value }
          hideSelected
          items={ items }
          itemTitle="text"
          itemValue="id"
          returnObject
        />
      ))

      await userEvent.click(element)
      await commands.waitStable('.v-list')
      expect(screen.queryAllByRole('option', { selected: true })).toHaveLength(0)
      let options = screen.getAllByRole('option')
      expect(options).toHaveLength(2)
      expect(options[0]).toHaveTextContent('Item 2')

      await waitForClickable(options[0])
      await userEvent.click(options[0])
      expect(selectedItem.value).toStrictEqual({ text: 'Item 2', id: 'item2' })
      expect(screen.queryAllByRole('option', { selected: true })).toHaveLength(0)
      options = screen.getAllByRole('option')
      expect(options).toHaveLength(2)
      expect(options[0]).toHaveTextContent('Item 1')
    })
  })

  // https://github.com/vuetifyjs/vuetify/issues/16055
  it('should select item after typing its first few letters', async () => {
    const items = ['aaa', 'foo', 'faa']
    const selectedItems = ref()

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

    await userEvent.click(element)
    await commands.waitStable('.v-list')
    await userEvent.keyboard('f')
    expect(screen.getAllByRole('option')).toHaveLength(3)
    expect(selectedItems.value).toBe('foo')
  })

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

    await userEvent.click(element)
    await commands.waitStable('.v-list')
    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(() => (
      <VSelect
        chips
        closableChips
        items={['foo', 'bar']}
        label="Select"
        modelValue={['foo', 'bar']}
        multiple
      />
    ))

    await expect.poll(() => screen.queryByRole('listbox')).toBeNull()
    await userEvent.click(screen.getAllByTestId('close-chip')[1])
    await expect.poll(() => screen.queryByRole('listbox')).toBeNull()
    await userEvent.click(screen.getByTestId('close-chip'))
    await expect.poll(() => screen.queryByRole('listbox')).toBeNull()
    await userEvent.click(element)
    await expect.poll(() => screen.queryByRole('listbox')).toBeVisible()
    await userEvent.keyboard('{Escape}')
    await expect.poll(() => screen.queryByRole('listbox')).toBeNull()
  })

  // https://github.com/vuetifyjs/vuetify/issues/19235
  it('should update v-model when click closable chip', async () => {
    const selectedItem = ref('abc')

    render(() => (
      <VSelect
        v-model={ selectedItem.value }
        chips
        closableChips
        items={['abc', 'def']}
      />
    ))

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

  // https://github.com/vuetifyjs/vuetify/issues/19261
  it('should not toggle v-model to null when clicking already selected item in single selection mode', async () => {
    const selectedItem = ref('abc')

    const { element } = render(() => (
      <VSelect
        v-model={ selectedItem.value }
        items={['abc', 'def']}
      />
    ))

    await userEvent.click(element)
    await commands.waitStable('.v-list')
    const options = screen.getAllByRole('option')
    expect(options).toHaveLength(2)
    await userEvent.click(options[0])
    expect(selectedItem.value).toBe('abc')
  })

  // https://github.com/vuetifyjs/vuetify/issues/18556
  // https://github.com/vuetifyjs/vuetify/issues/21205
  it('should show menu if focused and items are added when hideNoData is true"', async () => {
    const items = ref()
    render(() => <VSelect items={ items.value } hideNoData />)

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

    items.value = ['Foo', 'Bar']
    await nextTick()
    await expect.poll(() => screen.queryByRole('listbox')).toBeVisible()
  })

  // https://github.com/vuetifyjs/vuetify/issues/21205
  it('should not show menu if focused and items are added when hideNoData is false', async () => {
    const items = ref()
    render(() => <VSelect items={ items.value } />)

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

    items.value = ['Foo', 'Bar']
    await nextTick()
    await expect.poll(() => screen.queryByRole('listbox')).toBeNull()
  })

  // 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(VSelect, {
      props: { items: ['Foo', 'Bar'] },
    })

    await userEvent.click(element)
    await expect.poll(() => screen.queryByRole('listbox')).toBeVisible()
    await userEvent.click(screen.getAllByRole('option')[0])

    await rerender({ items: ['Foo', 'Bar', 'test', 'test 2'] })
    await expect.poll(() => screen.queryByRole('listbox')).toBeNull()
  })

  describe('reactivity', () => {
    it('adds a new item', async () => {
      const items = ref(['Foo'])
      const { element } = render(() => (
        <VSelect items={ items.value } />
      ))

      await userEvent.click(element)
      await expect.poll(() => screen.queryByRole('listbox')).toBeVisible()

      items.value.push('Bar')
      await expect.poll(() => screen.getByText('Bar')).toBeVisible()
    })

    it('removes an item', async () => {
      const items = ref(['Foo', 'Bar'])
      const { element } = render(() => (
        <VSelect items={ items.value } />
      ))

      await userEvent.click(element)
      await expect.poll(() => screen.queryByRole('listbox')).toBeVisible()

      items.value.splice(1, 1)
      await expect.element(screen.getByText('Bar')).not.toBeVisible()
    })

    it('updates an item title', async () => {
      const items = ref([{ title: 'Foo' }])
      const { element } = render(() => (
        <VSelect items={ items.value } />
      ))

      await userEvent.click(element)
      await expect.poll(() => screen.queryByRole('listbox')).toBeVisible()

      items.value[0].title = 'Bar'
      await expect.poll(() => screen.getByText('Bar')).toBeVisible()
    })

    it('adds a selection externally', async () => {
      const items = ref(['Foo', 'Bar'])
      const selection = ref(['Foo'])
      const { element } = render(() => (
        <VSelect v-model={ selection.value } items={ items.value } multiple />
      ))

      await userEvent.click(element)
      const menu = await screen.findByRole('listbox')
      await expect.element(menu).toBeVisible()

      selection.value.push('Bar')
      await expect.poll(() => screen.getAllByText('Bar')).toHaveLength(2)
      expect(getAllByRole(menu, 'option', { selected: true })).toHaveLength(2)
    })

    it('removes a selection externally', async () => {
      const items = ref(['Foo', 'Bar'])
      const selection = ref(['Foo', 'Bar'])
      const { element } = render(() => (
        <VSelect v-model={ selection.value } items={ items.value } multiple />
      ))

      await userEvent.click(element)
      const menu = await screen.findByRole('listbox')
      await expect.element(menu).toBeVisible()

      selection.value.splice(1, 1)
      await expect.poll(() => screen.getAllByText('Bar')).toHaveLength(1)
      expect(getAllByRole(menu, 'option', { selected: true })).toHaveLength(1)
    })

    it('adds a selected item', async () => {
      const items = ref(['Foo'])
      const selection = ref(['Foo'])
      const { element } = render(() => (
        <VSelect v-model={ selection.value } items={ items.value } multiple />
      ))

      await userEvent.click(element)
      const menu = await screen.findByRole('listbox')
      await expect.element(menu).toBeVisible()

      items.value.push('Bar')
      selection.value.push('Bar')
      await expect.poll(() => screen.getAllByText('Bar')).toHaveLength(2)
      expect(getAllByRole(menu, 'option', { selected: true })).toHaveLength(2)
    })

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

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

      expect(onFocus).toHaveBeenCalledTimes(1)
    })
  })

  it('should have reactive accessibility attributes', async () => {
    const { getByRole } = render(() => (
      <VSelect items={['Foo']} />
    ))

    const inputField = getByRole('combobox', { expanded: false })
    expect(inputField).toHaveAttribute('aria-expanded', 'false')
    expect(inputField.getAttribute('aria-controls')).toMatch(/^menu-v-\d+/)

    await userEvent.click(inputField, { force: true })
    await commands.waitStable('.v-list')

    expect(inputField).toHaveAttribute('aria-expanded', 'true')

    await commands.waitStable('.v-list')
    await userEvent.click(screen.getAllByRole('option')[0])

    expect(inputField).toHaveAttribute('aria-expanded', 'false')
  })

  // https://github.com/vuetifyjs/vuetify/issues/22052
  it('should keep the checkboxes in sync with the model', async () => {
    const items = [
      { title: 'Both', value: 'both' },
      { title: 'Option A', value: 'a' },
      { title: 'Option B', value: 'b' },
    ]

    const model = ref<string[]>([])
    const selectModel = computed({
      get: () => model.value.length === 2 ? ['both'] : model.value,
      set: val => model.value = val.includes('both') ? ['a', 'b'] : val,
    })

    const { element } = render(() => (
      <VSelect
        v-model={ selectModel.value }
        items={ items }
        multiple
        itemProps={ (item: any) =>
          (selectModel.value?.includes('both') && item.value !== 'both') ? { disabled: true } : {}
        }
      />
    ))

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

    const options = screen.getAllByRole('option')
    expect(options).toHaveLength(3)

    await userEvent.click(screen.getAllByCSS('.v-checkbox-btn input')[1])
    await userEvent.click(screen.getAllByCSS('.v-checkbox-btn input')[2])

    await expect.poll(() => model.value).toStrictEqual(['a', 'b'])
    expect(selectModel.value).toStrictEqual(['both'])
    expect(screen.getAllByCSS('.v-checkbox-btn input')[0]).toBeChecked()
    expect(screen.getAllByCSS('.v-checkbox-btn input:checked')).toHaveLength(1)
  })

  describe('clear with backspace', () => {
    it('should clear selection in single selection mode', async () => {
      const selectedItem = ref('Item 1')

      const { element } = render(() => (
        <VSelect
          clearable
          v-model={ selectedItem.value }
          items={['Item 1', 'Item 2', 'Item 3']}
        />
      ))

      expect(selectedItem.value).toBe('Item 1')

      await userEvent.click(element)
      await userEvent.keyboard('{Backspace}')

      expect(selectedItem.value).toBeNull()
    })

    it('should clear selection in multiple selection mode', async () => {
      const selectedItems = ref(['Item 1', 'Item 2'])

      const { element } = render(() => (
        <VSelect
          clearable
          v-model={ selectedItems.value }
          items={['Item 1', 'Item 2', 'Item 3']}
          multiple
        />
      ))

      expect(selectedItems.value).toHaveLength(2)

      await userEvent.click(element)
      await userEvent.keyboard('{Backspace}')

      expect(selectedItems.value).toHaveLength(0)
    })

    it('should open menu with openOnClear prop', async () => {
      const selectedItem = ref('Item 1')

      const { element } = render(() => (
        <VSelect
          clearable
          v-model={ selectedItem.value }
          items={['Item 1', 'Item 2', 'Item 3']}
          openOnClear
        />
      ))

      expect(selectedItem.value).toBe('Item 1')
      expect(screen.queryByRole('listbox')).toBeNull()

      await userEvent.click(element)
      await userEvent.keyboard('{Backspace}')

      expect(selectedItem.value).toBeNull()
      await expect.poll(() => screen.queryByRole('listbox')).toBeVisible()
    })

    it('should not clear in readonly mode', async () => {
      const selectedItem = ref('Item 1')

      const { element } = render(() => (
        <VSelect
          clearable
          v-model={ selectedItem.value }
          items={['Item 1', 'Item 2', 'Item 3']}
          readonly
        />
      ))

      expect(selectedItem.value).toBe('Item 1')

      await userEvent.click(element)
      await userEvent.keyboard('{Backspace}')

      expect(selectedItem.value).toBe('Item 1')
    })
  })

  describe('native form submission', () => {
    const items = [
      { title: 'Item 1', value: 1 },
      { title: 'Item 2', value: 2 },
      { title: 'Item 3', value: 3 },
    ]

    it('should include selected value in form data for single selection', async () => {
      let submittedData: FormData | null = null

      render(() => (
        <form
          onSubmit={ e => {
            e.preventDefault()
            submittedData = new FormData(e.target as HTMLFormElement)
          }}
        >
          <VSelect
            name="select"
            items={ items }
            modelValue={ items[0] }
          />
          <button type="submit">Submit</button>
        </form>
      ))

      const submitButton = screen.getByRole('button', { name: 'Submit' })
      await userEvent.click(submitButton)

      expect(submittedData).not.toBeNull()
      expect(submittedData!.get('select')).toBe('1')
    })

    it('should include selected values in form data for multiple selection', async () => {
      let submittedData: FormData | null = null

      render(() => (
        <form
          onSubmit={ e => {
            e.preventDefault()
            submittedData = new FormData(e.target as HTMLFormElement)
          }}
        >
          <VSelect
            multiple
            name="select"
            items={ items }
            modelValue={[items[0], items[1]]}
          />
          <button type="submit">Submit</button>
        </form>
      ))

      const submitButton = screen.getByRole('button', { name: 'Submit' })
      await userEvent.click(submitButton)

      expect(submittedData).not.toBeNull()
      const select = submittedData!.getAll('select')
      expect(select).toHaveLength(2)
      expect(select).toContain('1')
      expect(select).toContain('2')
    })
  })

  describe('menu-header and menu-footer slots', () => {
    it('should render menu-header and menu-footer slots', async () => {
      const { element } = render(() => (
        <VSelect 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>
            ),
          }}
        </VSelect>
      ))

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

      await expect.poll(() => screen.queryByTestId('header-content')).toHaveTextContent('My Header')
      await expect.poll(() => screen.queryByTestId('footer-content')).toHaveTextContent('My Footer')
    })

    it('should navigate freely between interactive elements with Tab', async () => {
      render(() => (
        <VSelect items={ Array.from({ length: 20 }, (_, i) => `Item #${i + 1}`) }>
          {{
            'menu-header': () => (
              <div>
                <VTextField data-testid="textfield-1" placeholder="Search..." />
              </div>
            ),
            'menu-footer': () => (
              <div class="d-flex justify-between">
                <button data-testid="button-1">Button 1</button>
                <button data-testid="button-2">Button 2</button>
              </div>
            ),
          }}
        </VSelect>
      ))

      await userEvent.keyboard('{Tab}{ArrowDown}')
      await commands.waitStable('.v-list')

      const menu = await screen.findByRole('listbox')
      await expect.element(menu).toBeVisible()

      await waitFor(() => {
        expect(screen.getAllByRole('option').at(0)).toHaveFocus()
      }, { timeout: 3000 })

      await wait(400)
      await userEvent.keyboard('{Tab}')
      expect(screen.getByTestId('button-1')).toHaveFocus()

      await userEvent.keyboard('{Tab}')
      expect(screen.getByTestId('button-2')).toHaveFocus()

      // Tab past footer closes menu
      await userEvent.keyboard('{Tab}')
      await expect.poll(() => screen.queryByRole('listbox')).toBeNull()
    })
  })

  // https://github.com/vuetifyjs/vuetify/issues/22697
  it('should not steal focus from another input when menu closes', async () => {
    render(() => (
      <div>
        <VTextField label="Text" data-testid="textfield" />
        <VSelect label="Select" items={['Item 1', 'Item 2']} />
      </div>
    ))

    await userEvent.keyboard('{Tab}{Tab}{ArrowDown}')
    await commands.waitStable('.v-list')

    await waitFor(() => {
      expect(screen.getAllByRole('option').at(0)).toHaveFocus()
    }, { timeout: 3000 })

    const textfield = screen.getByTestId('textfield')
    await userEvent.click(textfield)

    await expect.poll(() => screen.queryByRole('listbox')).toBeNull()
    await wait(300)

    expect(textfield.querySelector('input')).toHaveFocus()
    expect(screen.getByCSS('.v-select .v-field')).not.toHaveClass('v-field--focused')
  })

  it('should release focus on a single click outside on a non-focusable area', async () => {
    render(() => (
      <div>
        <div data-testid="outside" style="height: 40px">Outside</div>
        <VSelect label="Select" items={['Item 1', 'Item 2']} />
      </div>
    ))

    await userEvent.keyboard('{Tab}{ArrowDown}')
    await commands.waitStable('.v-list')
    await waitFor(() => expect(screen.getAllByRole('option').at(0)).toHaveFocus(), { timeout: 3000 })

    await userEvent.click(screen.getByTestId('outside'))

    await expect.poll(() => screen.queryByRole('listbox')).toBeNull()
    await wait(300)

    // focus must not bounce back to the field
    expect(screen.getByCSS('.v-select .v-field')).not.toHaveClass('v-field--focused')
  })

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

    await userEvent.keyboard('{Tab}{ArrowDown}')
    await commands.waitStable('.v-list')
    await waitFor(() => expect(screen.getAllByRole('option').at(0)).toHaveFocus(), { timeout: 3000 })
    expect(menu.value).toBe(true)

    // The focused option is removed (virtual-scroll recycle or async items reload);
    // focus falls to <body> and the menu would close.
    screen.getAllByRole('option')[0].remove()
    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('focus on open', () => {
    beforeEach(() => commands.setReduceMotionDisabled())

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

    it('should not focus the first item when opened with a pointer', async () => {
      render(() => (
        <VSelect
          items={ items }
          transition="fade-transition" // custom transition
        />
      ))

      await userEvent.click(screen.getByCSS('.v-select'))
      await commands.waitStable('.v-list')
      await wait(400)

      expect(document.activeElement?.closest('.v-list-item')).toBeNull()
    })
  })

  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 on click', async () => {
      render(() => (
        <VSelect items={ manyItems } modelValue={ 100 } />
      ))

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

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

      const target = screen.getAllByRole('option').find(el => el.textContent === '100')!
      await userEvent.click(target) // actual visibility test; workaround for vitest limitation
    })

    it('should move arrows from the selected item after Enter open', async () => {
      render(() => (
        <VSelect items={ manyItems } modelValue={ 100 } />
      ))

      await userEvent.tab()
      await userEvent.keyboard('{Enter}')
      await commands.waitStable('.v-list')

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

      await expect.poll(() => document.activeElement?.textContent?.trim()).toBe('100')

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

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

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

    it('should open with ArrowDown onto the item after selection', async () => {
      render(() => (
        <VSelect items={ manyItems } modelValue={ 100 } />
      ))

      await userEvent.tab()
      await userEvent.keyboard('{ArrowDown}')
      await commands.waitStable('.v-list')

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

      await expect.poll(() => document.activeElement?.textContent?.trim()).toBe('101')
    })

    it('should open with ArrowUp onto the item before the first selection', async () => {
      render(() => (
        <VSelect items={ manyItems } modelValue={[300, 555, 992]} multiple />
      ))

      await userEvent.tab()
      await userEvent.keyboard('{ArrowUp}')
      await commands.waitStable('.v-list')

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

      await expect.poll(() => document.activeElement?.textContent?.trim()).toBe('299')
    })
  })

  it('should close its menu when clicking another field inside a dialog', async () => {
    const dialog = ref(true)
    render(() => (
      <VDialog v-model={ dialog.value }>
        <div>
          <VTextField data-testid="other-field" label="Other" />
          <VSelect items={ items } label="Age" />
        </div>
      </VDialog>
    ))

    const select = screen.getByCSS('.v-select')
    await userEvent.click(select)
    await expect.poll(() => select).toHaveClass('v-select--active-menu')

    await userEvent.click(screen.getByTestId('other-field'))
    await expect.poll(() => select).not.toHaveClass('v-select--active-menu')
  })

  showcase({ stories })
})
