import { mount } from '@vue/test-utils'
import * as Vue from 'vue'
import * as popperExports from '@popperjs/core'
import { rAF } from '@element-plus/test-utils/tick'
import ElPopper from '../src/index.vue'

import type { VueWrapper } from '@vue/test-utils'
import PopupManager from '@element-plus/utils/popup-manager'

type UnknownProps = Record<string, unknown>

jest.useFakeTimers()

const { h, nextTick } = Vue
const AXIOM = 'Rem is the best girl'
const selector = '[role="tooltip"]'
const TEST_TRIGGER = 'test-trigger'
const MOUSE_ENTER_EVENT = 'mouseenter'
const MOUSE_LEAVE_EVENT = 'mouseleave'
const CLICK_EVENT = 'click'
const FOCUS_EVENT = 'focus'
const BLUR_EVENT = 'blur'
const DISPLAY_NONE = 'display: none'

const Wrapped = (props: UnknownProps, { slots }) => {
  return h('div', h(ElPopper, props, slots))
}

// eslint-disable-next-line
const _mount = (props: UnknownProps = {}, slots = {}): VueWrapper<any> =>
  mount(Wrapped, {
    props,
    slots: {
      trigger: () =>
        h('div', {
          class: TEST_TRIGGER,
        }),
      ...slots,
    },
    attachTo: 'body',
  })

const popperMock = jest
  .spyOn(popperExports, 'createPopper')
  .mockImplementation(() => ({
    update: jest.fn(),
    forceUpdate: jest.fn(),
    setOptions: jest.fn(),
    destroy: jest.fn(),
    state: null,
  }))

describe('Popper.vue', () => {

  afterAll(() => {
    popperMock.mockReset()
  })

  beforeEach(() => {
    popperMock.mockClear()
  })

  test('render test', () => {
    let wrapper = _mount(
      {
        appendToBody: false,
      },
      {
        default: () => AXIOM,
      },
    )

    expect(wrapper.text()).toEqual(AXIOM)

    wrapper = _mount({
      content: AXIOM,
      appendToBody: false,
    })

    expect(wrapper.text()).toEqual(AXIOM)
  })

  test('append to body', () => {
    let wrapper = _mount()
    expect(wrapper.find(selector).exists()).toBe(false)

    /**
     * Current layout of `ElPopper`
     *  --> Teleport
     *   --> mask
     *    --> transition
     *     --> popper
     */

    wrapper = _mount({
      appendToBody: false,
    })

    expect(wrapper.find(selector).exists()).toBe(true)
  })

  test('popper z-index should be dynamical', () => {
    const wrapper = _mount({
      appendToBody: false,
    })

    expect(
      Number.parseInt(
        window.getComputedStyle(wrapper.find('.el-popper').element).zIndex,
      ),
    ).toBeLessThanOrEqual(PopupManager.zIndex)
  })

  test('should show popper when mouse entered and hide when popper left', async () => {
    const wrapper = _mount({
      appendToBody: false,
    })

    const popper = wrapper.find(selector)
    expect(popper.attributes('style')).toContain(DISPLAY_NONE)

    const $trigger = wrapper.find(`.${TEST_TRIGGER}`)
    await $trigger.trigger(MOUSE_ENTER_EVENT)

    expect(popper.attributes('style')).not.toContain(DISPLAY_NONE)

    await $trigger.trigger(MOUSE_LEAVE_EVENT)
  })

  test('should be able to manual open', async () => {
    const wrapper = _mount({
      manualMode: true,
      appendToBody: false,
      visible: false,
    })
    expect(wrapper.find(selector).attributes('style')).toContain(DISPLAY_NONE)
    await wrapper.find(selector).trigger(MOUSE_ENTER_EVENT)

    expect(wrapper.find(selector).attributes('style')).toContain(DISPLAY_NONE)
    await wrapper.setProps({
      visible: true,
    })

    expect(wrapper.find(selector).attributes('style')).not.toContain(
      DISPLAY_NONE,
    )
  })

  test('should not stop propagation when stop mode is disabled', async () => {
    const onMouseUp = jest.fn()
    const onMouseDown = jest.fn()
    document.addEventListener('mouseup', onMouseUp)
    document.addEventListener('mousedown', onMouseDown)

    const wrapper = _mount({
      appendToBody: false,
      stopPopperMouseEvent: false,
      visible: true,
    })
    await nextTick()

    await wrapper.find('.el-popper').trigger('mousedown')
    expect(onMouseDown).toHaveBeenCalled()
    await wrapper.find('.el-popper').trigger('mouseup')
    expect(onMouseUp).toHaveBeenCalled()

    await wrapper.setProps({
      stopPopperMouseEvent: true,
    })
    await nextTick()

    await wrapper.find('.el-popper').trigger('mousedown')
    expect(onMouseDown).toHaveBeenCalledTimes(1)
    await wrapper.find('.el-popper').trigger('mouseup')
    expect(onMouseUp).toHaveBeenCalledTimes(1)
    document.removeEventListener('mouseup', onMouseUp)
    document.removeEventListener('mousedown', onMouseDown)
  })

  test('should disable popper to popup', async () => {
    const wrapper = _mount({
      disabled: true,
      appendToBody: false,
    })

    const $trigger = wrapper.find(`.${TEST_TRIGGER}`)
    expect(wrapper.find(selector).attributes('style')).toContain(DISPLAY_NONE)
    await $trigger.trigger(MOUSE_ENTER_EVENT)

    expect(wrapper.find(selector).attributes('style')).toContain(DISPLAY_NONE)

    await wrapper.setProps({
      disabled: false,
    })

    await $trigger.trigger(MOUSE_ENTER_EVENT)

    expect(wrapper.find(selector).attributes('style')).not.toContain(
      DISPLAY_NONE,
    )
  })

  test('should initialize a new popper when component mounted', async () => {
    _mount({
      appendToBody: false,
      visible: true,
    })

    // expect(popperExports.createPopper).toHaveBeenCalledTimes(1)
  })

  test('should hide after hide after is given', async () => {
    const wrapper = _mount({
      hideAfter: 200,
      appendToBody: false,
    })
    const $trigger = wrapper.find(`.${TEST_TRIGGER}`)
    await $trigger.trigger(MOUSE_ENTER_EVENT)
    await rAF()
    await nextTick()
    expect(wrapper.find(selector).attributes('style')).not.toContain(
      DISPLAY_NONE,
    )

    await $trigger.trigger(MOUSE_LEAVE_EVENT)
    jest.runOnlyPendingTimers()
    await rAF()
    await nextTick()
    expect(wrapper.find(selector).attributes('style')).toContain(DISPLAY_NONE)
  })

  test('should throw error when there is no trigger', async () => {
    const errorHandler = jest.fn()
    mount(Wrapped, {
      slots: {
        trigger: undefined,
      },
      global: {
        config: {
          errorHandler(err: Error) {
            errorHandler(err)
          },
          warnHandler() {
            // suppress warning
          },
        },
      },
    })
    // due to vue catches the error during rendering, and throws it asynchronously
    // the only way to test this is by providing an error handler to catch it
    // first time get caught when calling setup function
    // second time get caught when calling render function
    expect(errorHandler).toHaveBeenCalledTimes(2)
  })
  describe('trigger', () => {
    test('should work with click trigger', async () => {
      const wrapper = _mount({
        trigger: [CLICK_EVENT],
        appendToBody: false,
        hideAfter: 0,
      })
      await nextTick()

      const trigger = wrapper.find(`.${TEST_TRIGGER}`)
      const popper = wrapper.findComponent(ElPopper)

      expect(popper.vm.visibility).toBe(false)
      // for now triggering event on element via DOMWrapper is not available so we need to apply
      // old way
      await trigger.trigger(CLICK_EVENT)

      expect(popper.vm.visibility).toBe(true)

      await trigger.trigger(MOUSE_LEAVE_EVENT)
      expect(popper.vm.visibility).toBe(true)

      await wrapper.find('.el-popper').trigger(MOUSE_LEAVE_EVENT)
      expect(popper.vm.visibility).toBe(true)
      await trigger.trigger(BLUR_EVENT)
      expect(popper.vm.visibility).toBe(true)
      await trigger.trigger(CLICK_EVENT)
      expect(popper.vm.visibility).toBe(false)
    })

    test('should work with string trigger', async () => {
      const wrapper = _mount({
        trigger: CLICK_EVENT,
        appendToBody: false,
        hideAfter: 0,
      })
      await nextTick()

      const trigger = wrapper.find(`.${TEST_TRIGGER}`)
      const popper = wrapper.findComponent(ElPopper)

      await trigger.trigger(CLICK_EVENT)
      expect(popper.vm.visibility).toBe(true)

      await wrapper.find('.el-popper').trigger(MOUSE_LEAVE_EVENT)
      expect(popper.vm.visibility).toBe(true)

      await trigger.trigger(CLICK_EVENT)
      expect(popper.vm.visibility).toBe(false)
    })

    test('should work with hover trigger', async () => {
      const wrapper = _mount({
        trigger: ['hover'],
        appendToBody: false,
        hideAfter: 0,
      })
      await nextTick()

      const trigger = wrapper.find(`.${TEST_TRIGGER}`)
      const popper = wrapper.findComponent(ElPopper)
      expect(popper.vm.visibility).toBe(false)
      // for now triggering event on element via DOMWrapper is not available so we need to apply
      // old way
      await trigger.trigger(MOUSE_ENTER_EVENT)

      expect(popper.vm.visibility).toBe(true)

      await trigger.trigger(BLUR_EVENT)
      expect(popper.vm.visibility).toBe(true)

      await trigger.trigger(MOUSE_LEAVE_EVENT)
      expect(popper.vm.visibility).toBe(false)

      await trigger.trigger(FOCUS_EVENT)
      expect(popper.vm.visibility).toBe(false)

      await trigger.trigger(CLICK_EVENT)
      expect(popper.vm.visibility).toBe(false)
    })

    test('should work with focus trigger', async () => {
      const wrapper = _mount({
        trigger: [FOCUS_EVENT],
        appendToBody: false,
        hideAfter: 0,
      })
      await nextTick()

      const trigger = wrapper.find(`.${TEST_TRIGGER}`)
      const popper = wrapper.findComponent(ElPopper)
      expect(popper.vm.visibility).toBe(false)
      // for now triggering event on element via DOMWrapper is not available so we need to apply
      // old way
      await trigger.trigger(FOCUS_EVENT)
      expect(popper.vm.visibility).toBe(true)

      await trigger.trigger(MOUSE_LEAVE_EVENT)
      expect(popper.vm.visibility).toBe(true)

      await trigger.trigger(CLICK_EVENT)
      expect(popper.vm.visibility).toBe(true)

      await trigger.trigger(BLUR_EVENT)
      expect(popper.vm.visibility).toBe(false)

      await trigger.trigger(MOUSE_ENTER_EVENT)
      expect(popper.vm.visibility).toBe(false)

      await trigger.trigger(CLICK_EVENT)
      expect(popper.vm.visibility).toBe(false)

      // testing
      await trigger.trigger(FOCUS_EVENT)
      expect(popper.vm.visibility).toBe(true)

      await popper.trigger(MOUSE_LEAVE_EVENT)
      expect(popper.vm.visibility).toBe(true)
    })

    test('combined trigger', async () => {
      const wrapper = _mount({
        trigger: [FOCUS_EVENT, CLICK_EVENT, 'hover'],
        appendToBody: false,
        hideAfter: 0,
      })
      await nextTick()

      const trigger = wrapper.find(`.${TEST_TRIGGER}`)
      const popper = wrapper.findComponent(ElPopper)
      expect(popper.vm.visibility).toBe(false)
      // for now triggering event on element via DOMWrapper is not available so we need to apply
      // old way
      await trigger.trigger(CLICK_EVENT)

      expect(popper.vm.visibility).toBe(true)

      await trigger.trigger(BLUR_EVENT)
      expect(popper.vm.visibility).toBe(false)

      await trigger.trigger(MOUSE_ENTER_EVENT)
      expect(popper.vm.visibility).toBe(true)

      await trigger.trigger(CLICK_EVENT)
      expect(popper.vm.visibility).toBe(false)

      await trigger.trigger(FOCUS_EVENT)
      expect(popper.vm.visibility).toBe(true)

      await trigger.trigger(CLICK_EVENT)
      expect(popper.vm.visibility).toBe(true)

      await trigger.trigger(CLICK_EVENT)
      expect(popper.vm.visibility).toBe(false)
    })

    test('should pass style and class to trigger', async () => {
      const CLASS = 'fake'
      const STYLE = 'width: 100px'
      const wrapper = _mount({
        appendToBody: false,
        class: CLASS,
        style: STYLE,
      })

      const trigger = wrapper.find(`.${TEST_TRIGGER}`)
      expect(trigger.classes(CLASS)).toBe(true)
      expect((trigger.element as HTMLDivElement).style.width).toBe('100px')
    })
  })
})
