// Note: emits and listener fallthrough is tested in
// ./rendererAttrsFallthrough.spec.ts.

import { render, defineComponent, h, nodeOps } from '@vue/runtime-test'
import { isEmitListener } from '../src/componentEmits'

describe('component: emit', () => {
  test('trigger handlers', () => {
    const Foo = defineComponent({
      render() {},
      created() {
        // the `emit` function is bound on component instances
        this.$emit('foo')
        this.$emit('bar')
        this.$emit('!baz')
      }
    })

    const onfoo = jest.fn()
    const onBar = jest.fn()
    const onBaz = jest.fn()
    const Comp = () => h(Foo, { onfoo, onBar, ['on!baz']: onBaz })
    render(h(Comp), nodeOps.createElement('div'))

    expect(onfoo).not.toHaveBeenCalled()
    // only capitalized or special chars are considered event listeners
    expect(onBar).toHaveBeenCalled()
    expect(onBaz).toHaveBeenCalled()
  })

  // for v-model:foo-bar usage in DOM templates
  test('trigger hyphenated events for update:xxx events', () => {
    const Foo = defineComponent({
      render() {},
      created() {
        this.$emit('update:fooProp')
        this.$emit('update:barProp')
      }
    })

    const fooSpy = jest.fn()
    const barSpy = jest.fn()
    const Comp = () =>
      h(Foo, {
        'onUpdate:fooProp': fooSpy,
        'onUpdate:bar-prop': barSpy
      })
    render(h(Comp), nodeOps.createElement('div'))

    expect(fooSpy).toHaveBeenCalled()
    expect(barSpy).toHaveBeenCalled()
  })

  test('should trigger array of listeners', async () => {
    const Child = defineComponent({
      setup(_, { emit }) {
        emit('foo', 1)
        return () => h('div')
      }
    })

    const fn1 = jest.fn()
    const fn2 = jest.fn()

    const App = {
      setup() {
        return () =>
          h(Child, {
            onFoo: [fn1, fn2]
          })
      }
    }

    render(h(App), nodeOps.createElement('div'))
    expect(fn1).toHaveBeenCalledTimes(1)
    expect(fn1).toHaveBeenCalledWith(1)
    expect(fn2).toHaveBeenCalledTimes(1)
    expect(fn1).toHaveBeenCalledWith(1)
  })

  test('warning for undeclared event (array)', () => {
    const Foo = defineComponent({
      emits: ['foo'],
      render() {},
      created() {
        // @ts-ignore
        this.$emit('bar')
      }
    })
    render(h(Foo), nodeOps.createElement('div'))
    expect(
      `Component emitted event "bar" but it is neither declared`
    ).toHaveBeenWarned()
  })

  test('warning for undeclared event (object)', () => {
    const Foo = defineComponent({
      emits: {
        foo: null
      },
      render() {},
      created() {
        // @ts-ignore
        this.$emit('bar')
      }
    })
    render(h(Foo), nodeOps.createElement('div'))
    expect(
      `Component emitted event "bar" but it is neither declared`
    ).toHaveBeenWarned()
  })

  test('should not warn if has equivalent onXXX prop', () => {
    const Foo = defineComponent({
      props: ['onFoo'],
      emits: [],
      render() {},
      created() {
        // @ts-ignore
        this.$emit('foo')
      }
    })
    render(h(Foo), nodeOps.createElement('div'))
    expect(
      `Component emitted event "bar" but it is neither declared`
    ).not.toHaveBeenWarned()
  })

  test('validator warning', () => {
    const Foo = defineComponent({
      emits: {
        foo: (arg: number) => arg > 0
      },
      render() {},
      created() {
        this.$emit('foo', -1)
      }
    })
    render(h(Foo), nodeOps.createElement('div'))
    expect(`event validation failed for event "foo"`).toHaveBeenWarned()
  })

  test('merging from mixins', () => {
    const mixin = {
      emits: {
        foo: (arg: number) => arg > 0
      }
    }
    const Foo = defineComponent({
      mixins: [mixin],
      render() {},
      created() {
        this.$emit('foo', -1)
      }
    })
    render(h(Foo), nodeOps.createElement('div'))
    expect(`event validation failed for event "foo"`).toHaveBeenWarned()
  })

  test('.once', () => {
    const Foo = defineComponent({
      render() {},
      emits: {
        foo: null
      },
      created() {
        this.$emit('foo')
        this.$emit('foo')
      }
    })
    const fn = jest.fn()
    render(
      h(Foo, {
        onFooOnce: fn
      }),
      nodeOps.createElement('div')
    )
    expect(fn).toHaveBeenCalledTimes(1)
  })

  describe('isEmitListener', () => {
    test('array option', () => {
      const def1 = { emits: ['click'] }
      expect(isEmitListener(def1, 'onClick')).toBe(true)
      expect(isEmitListener(def1, 'onclick')).toBe(false)
      expect(isEmitListener(def1, 'onBlick')).toBe(false)
    })

    test('object option', () => {
      const def2 = { emits: { click: null } }
      expect(isEmitListener(def2, 'onClick')).toBe(true)
      expect(isEmitListener(def2, 'onclick')).toBe(false)
      expect(isEmitListener(def2, 'onBlick')).toBe(false)
    })

    test('with mixins and extends', () => {
      const mixin1 = { emits: ['foo'] }
      const mixin2 = { emits: ['bar'] }
      const extend = { emits: ['baz'] }
      const def3 = {
        mixins: [mixin1, mixin2],
        extends: extend
      }
      expect(isEmitListener(def3, 'onFoo')).toBe(true)
      expect(isEmitListener(def3, 'onBar')).toBe(true)
      expect(isEmitListener(def3, 'onBaz')).toBe(true)
      expect(isEmitListener(def3, 'onclick')).toBe(false)
      expect(isEmitListener(def3, 'onBlick')).toBe(false)
    })

    test('.once listeners', () => {
      const def2 = { emits: { click: null } }
      expect(isEmitListener(def2, 'onClickOnce')).toBe(true)
      expect(isEmitListener(def2, 'onclickOnce')).toBe(false)
    })
  })
})