// Components
import VAutocomplete from '../VAutocomplete'

// Utilities
import {
  mount,
  Wrapper,
} from '@vue/test-utils'
import { keyCodes } from '../../../util/helpers'

describe('VAutocomplete.ts', () => {
  type Instance = InstanceType<typeof VAutocomplete>
  let mountFunction: (options?: object) => Wrapper<Instance>

  beforeEach(() => {
    document.body.setAttribute('data-app', 'true')

    mountFunction = (options = {}) => {
      return mount(VAutocomplete, {
        ...options,
        // https://github.com/vuejs/vue-test-utils/issues/1130
        sync: false,
        mocks: {
          $vuetify: {
            lang: {
              t: (val: string) => val,
            },
            theme: {
              dark: false,
            },
          },
        },
      })
    }
  })

  it('should have explicit tabindex passed through when autocomplete', () => {
    const wrapper = mountFunction({
      attrs: {
        tabindex: 10,
      },
    })

    expect(wrapper.vm.$refs.input.tabIndex).toBe(10)
    expect(wrapper.vm.$el.tabIndex).toBe(-1)
  })

  it('should emit search input changes', async () => {
    const wrapper = mountFunction({
      propsData: {
      },
    })

    const input = wrapper.find('input')
    const element = input.element as HTMLInputElement

    const update = jest.fn()
    wrapper.vm.$on('update:search-input', update)

    element.value = 'test'
    input.trigger('input')

    await wrapper.vm.$nextTick()

    expect(update).toHaveBeenCalledWith('test')
  })

  it('should filter autocomplete search results', async () => {
    const wrapper = mountFunction({
      propsData: { items: ['foo', 'bar'] },
    })

    wrapper.setData({ internalSearch: 'foo' })

    expect(wrapper.vm.filteredItems).toHaveLength(1)
    expect(wrapper.vm.filteredItems[0]).toBe('foo')
  })

  it('should filter numeric primitives', () => {
    const wrapper = mountFunction({
      propsData: {
        items: [1, 2],
      },
    })

    wrapper.setData({ internalSearch: 1 })

    expect(wrapper.vm.filteredItems).toHaveLength(1)
    expect(wrapper.vm.filteredItems[0]).toBe(1)
  })

  it('should activate when search changes and not active', async () => {
    const wrapper = mountFunction({
      propsData: {
        items: [1, 2, 3, 4],
        multiple: true,
      },
    })

    wrapper.vm.isMenuActive = true
    await wrapper.vm.$nextTick()
    wrapper.setData({ internalSearch: 2 })
    await wrapper.vm.$nextTick()

    expect(wrapper.vm.isMenuActive).toBe(true)
  })

  // TODO: this fails without sync, nextTick doesn't help
  // https://github.com/vuejs/vue-test-utils/issues/1130
  it.skip('should set searchValue to null when deactivated', async () => {
    const wrapper = mountFunction({
      propsData: {
        items: [1, 2, 3, 4],
        multiple: true,
      },
    })

    await wrapper.vm.$nextTick()

    const input = wrapper.find('input')
    const element = input.element as HTMLInputElement

    input.trigger('focus')
    element.value = '2'
    input.trigger('input')

    expect(wrapper.vm.internalSearch).toBe('2')

    wrapper.setProps({
      multiple: false,
      value: 1,
    })

    await wrapper.vm.$nextTick()

    expect(wrapper.vm.internalSearch).toBe(1)

    input.trigger('focus')
    element.value = '3'
    input.trigger('input')
    input.trigger('blur')

    expect(wrapper.vm.internalSearch).toBe('3')
  })

  it('should render role=combobox correctly when autocomplete', async () => {
    const wrapper = mountFunction()

    expect(wrapper.vm.$el.getAttribute('role')).toBeFalsy()

    const input = wrapper.find('.v-input__slot')
    expect(input.element.getAttribute('role')).toBe('combobox')
  })

  it('should not duplicate items after items update when caching is turned on', async () => {
    const wrapper = mountFunction({
      propsData: {
        cacheItems: true,
        returnObject: true,
        itemText: 'text',
        itemValue: 'id',
        items: [],
      },
    })

    wrapper.setProps({ items: [{ id: 1, text: 'A' }] })
    expect(wrapper.vm.computedItems).toHaveLength(1)
    wrapper.setProps({ items: [{ id: 1, text: 'A' }] })
    expect(wrapper.vm.computedItems).toHaveLength(1)
  })

  it('should cache items passed via prop', async () => {
    const wrapper = mountFunction({
      propsData: {
        cacheItems: true,
        items: [1, 2, 3, 4],
      },
    })

    expect(wrapper.vm.computedItems).toHaveLength(4)

    wrapper.setProps({ items: [5] })

    expect(wrapper.vm.computedItems).toHaveLength(5)
  })

  it('should not filter text with no items', async () => {
    const wrapper = mountFunction({
      propsData: {
        eager: true,
        items: ['foo', 'bar'],
      },
    })

    await wrapper.vm.$nextTick()

    wrapper.setProps({ searchInput: 'asdf' })

    // Wait for watcher
    await wrapper.vm.$nextTick()

    const tile = wrapper.find('.v-list-item__title')

    expect(tile.text()).toBe('$vuetify.noDataText')
  })

  it('should not display menu when tab focused', async () => {
    const wrapper = mountFunction({
      propsData: {
        items: [1, 2],
        value: 1,
      },
    })

    const input = wrapper.find('input')
    input.trigger('focus')

    await wrapper.vm.$nextTick()

    expect(wrapper.vm.isMenuActive).toBe(false)
  })

  // TODO: this fails without sync, nextTick doesn't help
  // https://github.com/vuejs/vue-test-utils/issues/1130
  // eslint-disable-next-line max-statements
  it.skip('should change selected index', async () => {
    const wrapper = mountFunction({
      attachToDocument: true,
      propsData: {
        items: ['foo', 'bar', 'fizz'],
        multiple: true,
        value: ['foo', 'bar', 'fizz'],
      },
    })

    await wrapper.vm.$nextTick()

    expect(wrapper.vm.selectedIndex).toBe(-1)
    expect(wrapper.vm.selectedItems).toHaveLength(3)

    // Right arrow
    wrapper.vm.changeSelectedIndex(keyCodes.right)
    expect(wrapper.vm.selectedIndex).toBe(0)

    wrapper.vm.changeSelectedIndex(keyCodes.right)
    expect(wrapper.vm.selectedIndex).toBe(1)

    wrapper.vm.changeSelectedIndex(keyCodes.right)
    expect(wrapper.vm.selectedIndex).toBe(2)

    // Left arrow
    wrapper.vm.changeSelectedIndex(keyCodes.left)
    expect(wrapper.vm.selectedIndex).toBe(1)

    wrapper.vm.changeSelectedIndex(keyCodes.left)
    expect(wrapper.vm.selectedIndex).toBe(0)

    wrapper.vm.changeSelectedIndex(keyCodes.left)
    expect(wrapper.vm.selectedIndex).toBe(-1)

    wrapper.vm.changeSelectedIndex(keyCodes.left)
    expect(wrapper.vm.selectedIndex).toBe(2)

    wrapper.vm.changeSelectedIndex(keyCodes.left)
    expect(wrapper.vm.selectedIndex).toBe(1)

    // Delete key
    wrapper.vm.changeSelectedIndex(keyCodes.backspace)
    await wrapper.vm.$nextTick()
    expect(wrapper.vm.selectedIndex).toBe(1)

    wrapper.vm.changeSelectedIndex(keyCodes.left)
    expect(wrapper.vm.selectedIndex).toBe(0)

    wrapper.vm.changeSelectedIndex(keyCodes.backspace)
    await wrapper.vm.$nextTick()
    expect(wrapper.vm.selectedIndex).toBe(0)

    // Should not change index if search is dirty
    wrapper.setProps({ searchInput: 'foo' })
    wrapper.vm.changeSelectedIndex(keyCodes.backspace)

    expect(wrapper.vm.selectedIndex).toBe(0)
    expect(wrapper.vm.selectedItems).toHaveLength(1)

    wrapper.setProps({ searchInput: undefined })

    // Should not proceed if keyCode doesn't match
    wrapper.vm.changeSelectedIndex(99)
    await wrapper.vm.$nextTick()

    expect(wrapper.vm.selectedIndex).toBe(0)
    expect(wrapper.vm.selectedItems).toHaveLength(1)

    wrapper.vm.changeSelectedIndex(keyCodes.backspace)
    await wrapper.vm.$nextTick()

    expect(wrapper.vm.selectedItems).toHaveLength(0)
    expect(wrapper.vm.selectedIndex).toBe(-1)

    // Should not change/error if called with no selection
    wrapper.vm.changeSelectedIndex(keyCodes.backspace)
    await wrapper.vm.$nextTick()

    expect(wrapper.vm.selectedIndex).toBe(-1)

    wrapper.setProps({ value: ['foo', 'bar', 'fizz'] })

    await wrapper.vm.$nextTick()

    expect(wrapper.vm.selectedItems).toHaveLength(3)

    wrapper.vm.selectedIndex = 2

    // Simulating removing items when an index already selected
    wrapper.setProps({ value: ['foo', 'bar'] })

    await wrapper.vm.$nextTick()

    expect(wrapper.vm.selectedIndex).toBe(2)

    // Backspace
    wrapper.vm.changeSelectedIndex(keyCodes.delete)
    expect(wrapper.vm.selectedIndex).toBe(-1)
  })

  it('should conditionally show the menu', async () => {
    const wrapper = mountFunction({
      attachToDocument: true,
      propsData: {
        items: ['foo', 'bar', 'fizz'],
      },
    })

    const slot = wrapper.find('.v-input__slot')
    const input = wrapper.find('input')

    // Focus input should only focus
    input.trigger('focus')

    expect(wrapper.vm.isFocused).toBe(true)
    expect(wrapper.vm.menuCanShow).toBe(true)
    expect(wrapper.vm.isMenuActive).toBe(false)

    // Clicking input should open menu
    slot.trigger('click')

    expect(wrapper.vm.isMenuActive).toBe(true)
    expect(wrapper.vm.menuCanShow).toBe(true)

    wrapper.setProps({ searchInput: 'foo' })

    expect(wrapper.vm.isMenuActive).toBe(true)
    expect(wrapper.vm.menuCanShow).toBe(true)

    // Should close menu but keep focus
    input.trigger('keydown.esc')

    expect(wrapper.vm.isFocused).toBe(true)
    expect(wrapper.vm.isMenuActive).toBe(false)
    expect(wrapper.vm.menuCanShow).toBe(true)

    // TODO: Add expects for tags when impl
  })

  // TODO: this fails without sync, nextTick doesn't help
  // https://github.com/vuejs/vue-test-utils/issues/1130
  it.skip('should have the correct selected item', async () => {
    const wrapper = mountFunction({
      propsData: {
        items: ['foo', 'bar', 'fizz'],
        multiple: true,
        value: ['foo'],
      },
    })

    expect(wrapper.vm.selectedItem).toBeNull()

    wrapper.setProps({
      multiple: false,
      value: 'foo',
    })

    expect(wrapper.vm.selectedItem).toBe('foo')
  })

  it('should reset lazySearch', async () => {
    const wrapper = mountFunction({
      propsData: {
        chips: true,
        items: ['foo', 'bar', 'fizz'],
        searchInput: 'foo',
      },
    })

    expect(wrapper.vm.lazySearch).toBe('foo')
    expect(wrapper.vm.hasSlot).toBe(true)

    wrapper.setData({ isMenuActive: true })
    await wrapper.vm.$nextTick()
    wrapper.setData({ isMenuActive: false })
    await wrapper.vm.$nextTick()

    expect(wrapper.vm.lazySearch).toBeNull()
  })

  it('should select input text on focus', async () => {
    const wrapper = mountFunction({
      attachToDocument: true,
    })
    const select = jest.fn()
    wrapper.vm.$refs.input.select = select

    const input = wrapper.find('input')
    input.trigger('focus')

    await wrapper.vm.$nextTick()

    expect(wrapper.vm.isFocused).toBe(true)
    expect(select).toHaveBeenCalledTimes(1)

    input.trigger('keydown.tab')

    await wrapper.vm.$nextTick()

    expect(wrapper.vm.isFocused).toBe(false)
    expect(select).toHaveBeenCalledTimes(1)
  })

  it('should not respond to click', () => {
    const onFocus = jest.fn()
    const wrapper = mountFunction({
      propsData: { disabled: true },
      methods: { onFocus },
    })
    const slot = wrapper.find('.v-input__slot')

    slot.trigger('click')

    expect(onFocus).not.toHaveBeenCalled()

    wrapper.setProps({ disabled: false, readonly: true })

    slot.trigger('click')

    expect(onFocus).not.toHaveBeenCalled()

    wrapper.setProps({ readonly: false })

    slot.trigger('click')

    expect(onFocus).toHaveBeenCalled()
  })

  it('should react to keydown', () => {
    const activateMenu = jest.fn()
    const changeSelectedIndex = jest.fn()
    const onEscDown = jest.fn()
    const onTabDown = jest.fn()
    const wrapper = mountFunction({
      methods: {
        activateMenu,
        changeSelectedIndex,
        onEscDown,
        onTabDown,
      },
    })

    const input = wrapper.find('input')
    const element = input.element as HTMLInputElement

    expect(wrapper.vm.isMenuActive).toBe(false)

    input.trigger('keydown.enter')
    input.trigger('keydown.space')
    input.trigger('keydown.up')
    input.trigger('keydown.down')

    expect(activateMenu).toHaveBeenCalledTimes(4)

    input.trigger('keydown.esc')

    expect(onEscDown).toHaveBeenCalledTimes(1)

    input.trigger('keydown.tab')

    expect(onTabDown).toHaveBeenCalledTimes(1)

    // Skip menu activation
    wrapper.setData({ isMenuActive: true })

    element.value = 'foo'
    input.trigger('input')

    wrapper.setProps({ hideSelected: true })

    expect(wrapper.vm.genSelections()).toEqual([])
  })

  it('should change autocomplete attribute', () => {
    const wrapper = mountFunction({
      attrs: {
        autocomplete: 'on',
      },
    })

    expect(wrapper.vm.$attrs.autocomplete).toBe('on')
  })

  it('should not delete item if readonly', async () => {
    const wrapper = mountFunction({
      propsData: {
        items: ['a', 'b', 'c'],
        multiple: true,
        value: ['a', 'b', 'c'],
      },
    })
    wrapper.vm.changeSelectedIndex(keyCodes.right)
    wrapper.vm.changeSelectedIndex(keyCodes.right)
    wrapper.vm.changeSelectedIndex(keyCodes.backspace)
    await wrapper.vm.$nextTick()
    expect(wrapper.vm.selectedItems).toHaveLength(2)

    wrapper.setProps({
      readonly: true,
    })

    wrapper.vm.changeSelectedIndex(keyCodes.backspace)
    await wrapper.vm.$nextTick()
    expect(wrapper.vm.selectedItems).toHaveLength(2)
  })
})
