import {
  ref,
  render,
  h,
  nodeOps,
  nextTick,
  getCurrentInstance
} from '@vue/runtime-test'
import { normalizeVNode } from '../src/vnode'
import { createSlots } from '../src/helpers/createSlots'

describe('component: slots', () => {
  function renderWithSlots(slots: any): any {
    let instance: any
    const Comp = {
      render() {
        instance = getCurrentInstance()
        return h('div')
      }
    }

    render(h(Comp, null, slots), nodeOps.createElement('div'))
    return instance
  }

  test('initSlots: instance.slots should be set correctly', () => {
    const { slots } = renderWithSlots({ _: 1 })
    expect(slots).toMatchObject({ _: 1 })
  })

  test('initSlots: should normalize object slots (when value is null, string, array)', () => {
    const { slots } = renderWithSlots({
      _inner: '_inner',
      foo: null,
      header: 'header',
      footer: ['f1', 'f2']
    })

    expect(
      '[Vue warn]: Non-function value encountered for slot "header". Prefer function slots for better performance.'
    ).toHaveBeenWarned()

    expect(
      '[Vue warn]: Non-function value encountered for slot "footer". Prefer function slots for better performance.'
    ).toHaveBeenWarned()

    expect(slots).not.toHaveProperty('_inner')
    expect(slots).not.toHaveProperty('foo')
    expect(slots.header()).toMatchObject([normalizeVNode('header')])
    expect(slots.footer()).toMatchObject([
      normalizeVNode('f1'),
      normalizeVNode('f2')
    ])
  })

  test('initSlots: should normalize object slots (when value is function)', () => {
    let proxy: any
    const Comp = {
      render() {
        proxy = getCurrentInstance()
        return h('div')
      }
    }

    render(
      h(Comp, null, {
        header: () => 'header'
      }),
      nodeOps.createElement('div')
    )

    expect(proxy.slots.header()).toMatchObject([normalizeVNode('header')])
  })

  test('initSlots: instance.slots should be set correctly (when vnode.shapeFlag is not SLOTS_CHILDREN)', () => {
    const { slots } = renderWithSlots([h('span')])

    expect(
      '[Vue warn]: Non-function value encountered for default slot. Prefer function slots for better performance.'
    ).toHaveBeenWarned()

    expect(slots.default()).toMatchObject([normalizeVNode(h('span'))])
  })

  test('updateSlots: instance.slots should be update correctly (when slotType is number)', async () => {
    const flag1 = ref(true)

    let instance: any
    const Child = () => {
      instance = getCurrentInstance()
      return 'child'
    }

    const Comp = {
      setup() {
        return () => [
          h(
            Child,
            null,
            createSlots({ _: 2 as any }, [
              flag1.value
                ? {
                    name: 'one',
                    fn: () => [h('span')]
                  }
                : {
                    name: 'two',
                    fn: () => [h('div')]
                  }
            ])
          )
        ]
      }
    }
    render(h(Comp), nodeOps.createElement('div'))

    expect(instance.slots).toHaveProperty('one')
    expect(instance.slots).not.toHaveProperty('two')

    flag1.value = false
    await nextTick()

    expect(instance.slots).not.toHaveProperty('one')
    expect(instance.slots).toHaveProperty('two')
  })

  test('updateSlots: instance.slots should be update correctly (when slotType is null)', async () => {
    const flag1 = ref(true)

    let instance: any
    const Child = () => {
      instance = getCurrentInstance()
      return 'child'
    }

    const oldSlots = {
      header: 'header'
    }
    const newSlots = {
      footer: 'footer'
    }

    const Comp = {
      setup() {
        return () => [
          h(Child, { n: flag1.value }, flag1.value ? oldSlots : newSlots)
        ]
      }
    }
    render(h(Comp), nodeOps.createElement('div'))

    expect(instance.slots).toHaveProperty('header')
    expect(instance.slots).not.toHaveProperty('footer')

    flag1.value = false
    await nextTick()

    expect(
      '[Vue warn]: Non-function value encountered for slot "header". Prefer function slots for better performance.'
    ).toHaveBeenWarned()

    expect(
      '[Vue warn]: Non-function value encountered for slot "footer". Prefer function slots for better performance.'
    ).toHaveBeenWarned()

    expect(instance.slots).not.toHaveProperty('header')
    expect(instance.slots.footer()).toMatchObject([normalizeVNode('footer')])
  })

  test('updateSlots: instance.slots should be update correctly (when vnode.shapeFlag is not SLOTS_CHILDREN)', async () => {
    const flag1 = ref(true)

    let instance: any
    const Child = () => {
      instance = getCurrentInstance()
      return 'child'
    }

    const Comp = {
      setup() {
        return () => [
          h(Child, { n: flag1.value }, flag1.value ? ['header'] : ['footer'])
        ]
      }
    }
    render(h(Comp), nodeOps.createElement('div'))

    expect(instance.slots.default()).toMatchObject([normalizeVNode('header')])

    flag1.value = false
    await nextTick()

    expect(
      '[Vue warn]: Non-function value encountered for default slot. Prefer function slots for better performance.'
    ).toHaveBeenWarned()

    expect(instance.slots.default()).toMatchObject([normalizeVNode('footer')])
  })

  test('should respect $stable flag', async () => {
    const flag1 = ref(1)
    const flag2 = ref(2)
    const spy = jest.fn()

    const Child = () => {
      spy()
      return 'child'
    }

    const App = {
      setup() {
        return () => [
          flag1.value,
          h(
            Child,
            { n: flag2.value },
            {
              foo: () => 'foo',
              $stable: true
            }
          )
        ]
      }
    }

    render(h(App), nodeOps.createElement('div'))
    expect(spy).toHaveBeenCalledTimes(1)

    // parent re-render, props didn't change, slots are stable
    // -> child should not update
    flag1.value++
    await nextTick()
    expect(spy).toHaveBeenCalledTimes(1)

    // parent re-render, props changed
    // -> child should update
    flag2.value++
    await nextTick()
    expect(spy).toHaveBeenCalledTimes(2)
  })
})
