import {
  h,
  Fragment,
  createVNode,
  openBlock,
  createBlock,
  render,
  nodeOps,
  TestElement,
  serialize,
  serializeInner as inner,
  VNode,
  ref,
  nextTick
} from '@vue/runtime-test'
import { PatchFlags } from '@vue/shared'

describe('renderer: optimized mode', () => {
  let root: TestElement
  let block: VNode | null = null

  beforeEach(() => {
    root = nodeOps.createElement('div')
    block = null
  })

  const renderWithBlock = (renderChildren: () => VNode[]) => {
    render(
      (openBlock(), (block = createBlock('div', null, renderChildren()))),
      root
    )
  }

  test('basic use of block', () => {
    render((openBlock(), (block = createBlock('p', null, 'foo'))), root)

    expect(block.dynamicChildren!.length).toBe(0)
    expect(inner(root)).toBe('<p>foo</p>')
  })

  test('block can appear anywhere in the vdom tree', () => {
    render(
      h('div', (openBlock(), (block = createBlock('p', null, 'foo')))),
      root
    )

    expect(block.dynamicChildren!.length).toBe(0)
    expect(inner(root)).toBe('<div><p>foo</p></div>')
  })

  test('block should collect dynamic vnodes', () => {
    renderWithBlock(() => [
      createVNode('p', null, 'foo', PatchFlags.TEXT),
      createVNode('i')
    ])

    expect(block!.dynamicChildren!.length).toBe(1)
    expect(serialize(block!.dynamicChildren![0].el as TestElement)).toBe(
      '<p>foo</p>'
    )
  })

  test('block can disable tracking', () => {
    render(
      // disable tracking
      (openBlock(true),
      (block = createBlock('div', null, [
        createVNode('p', null, 'foo', PatchFlags.TEXT)
      ]))),
      root
    )

    expect(block.dynamicChildren!.length).toBe(0)
  })

  test('block as dynamic children', () => {
    renderWithBlock(() => [
      (openBlock(), createBlock('div', { key: 0 }, [h('p')]))
    ])

    expect(block!.dynamicChildren!.length).toBe(1)
    expect(block!.dynamicChildren![0].dynamicChildren!.length).toBe(0)
    expect(serialize(block!.dynamicChildren![0].el as TestElement)).toBe(
      '<div><p></p></div>'
    )

    renderWithBlock(() => [
      (openBlock(), createBlock('div', { key: 1 }, [h('i')]))
    ])

    expect(block!.dynamicChildren!.length).toBe(1)
    expect(block!.dynamicChildren![0].dynamicChildren!.length).toBe(0)
    expect(serialize(block!.dynamicChildren![0].el as TestElement)).toBe(
      '<div><i></i></div>'
    )
  })

  test('PatchFlags: PatchFlags.TEXT', async () => {
    renderWithBlock(() => [createVNode('p', null, 'foo', PatchFlags.TEXT)])

    expect(inner(root)).toBe('<div><p>foo</p></div>')
    expect(block!.dynamicChildren!.length).toBe(1)
    expect(serialize(block!.dynamicChildren![0].el as TestElement)).toBe(
      '<p>foo</p>'
    )

    renderWithBlock(() => [createVNode('p', null, 'bar', PatchFlags.TEXT)])

    expect(inner(root)).toBe('<div><p>bar</p></div>')
    expect(block!.dynamicChildren!.length).toBe(1)
    expect(serialize(block!.dynamicChildren![0].el as TestElement)).toBe(
      '<p>bar</p>'
    )
  })

  test('PatchFlags: PatchFlags.CLASS', async () => {
    renderWithBlock(() => [
      createVNode('p', { class: 'foo' }, '', PatchFlags.CLASS)
    ])

    expect(inner(root)).toBe('<div><p class="foo"></p></div>')
    expect(block!.dynamicChildren!.length).toBe(1)
    expect(serialize(block!.dynamicChildren![0].el as TestElement)).toBe(
      '<p class="foo"></p>'
    )

    renderWithBlock(() => [
      createVNode('p', { class: 'bar' }, '', PatchFlags.CLASS)
    ])

    expect(inner(root)).toBe('<div><p class="bar"></p></div>')
    expect(block!.dynamicChildren!.length).toBe(1)
    expect(serialize(block!.dynamicChildren![0].el as TestElement)).toBe(
      '<p class="bar"></p>'
    )
  })

  test('PatchFlags: PatchFlags.STYLE', async () => {
    renderWithBlock(() => [
      createVNode('p', { style: 'color: red' }, '', PatchFlags.STYLE)
    ])

    expect(inner(root)).toBe('<div><p style="color: red"></p></div>')
    expect(block!.dynamicChildren!.length).toBe(1)
    expect(serialize(block!.dynamicChildren![0].el as TestElement)).toBe(
      '<p style="color: red"></p>'
    )

    renderWithBlock(() => [
      createVNode('p', { style: 'color: green' }, '', PatchFlags.STYLE)
    ])

    expect(inner(root)).toBe('<div><p style="color: green"></p></div>')
    expect(block!.dynamicChildren!.length).toBe(1)
    expect(serialize(block!.dynamicChildren![0].el as TestElement)).toBe(
      '<p style="color: green"></p>'
    )
  })

  test('PatchFlags: PatchFlags.PROPS', async () => {
    renderWithBlock(() => [
      createVNode('p', { id: 'foo' }, '', PatchFlags.PROPS, ['id'])
    ])

    expect(inner(root)).toBe('<div><p id="foo"></p></div>')
    expect(block!.dynamicChildren!.length).toBe(1)
    expect(serialize(block!.dynamicChildren![0].el as TestElement)).toBe(
      '<p id="foo"></p>'
    )

    renderWithBlock(() => [
      createVNode('p', { id: 'bar' }, '', PatchFlags.PROPS, ['id'])
    ])

    expect(inner(root)).toBe('<div><p id="bar"></p></div>')
    expect(block!.dynamicChildren!.length).toBe(1)
    expect(serialize(block!.dynamicChildren![0].el as TestElement)).toBe(
      '<p id="bar"></p>'
    )
  })

  test('PatchFlags: PatchFlags.FULL_PROPS', async () => {
    let propName = 'foo'

    renderWithBlock(() => [
      createVNode('p', { [propName]: 'dynamic' }, '', PatchFlags.FULL_PROPS)
    ])

    expect(inner(root)).toBe('<div><p foo="dynamic"></p></div>')
    expect(block!.dynamicChildren!.length).toBe(1)
    expect(serialize(block!.dynamicChildren![0].el as TestElement)).toBe(
      '<p foo="dynamic"></p>'
    )

    propName = 'bar'
    renderWithBlock(() => [
      createVNode('p', { [propName]: 'dynamic' }, '', PatchFlags.FULL_PROPS)
    ])

    expect(inner(root)).toBe('<div><p bar="dynamic"></p></div>')
    expect(block!.dynamicChildren!.length).toBe(1)
    expect(serialize(block!.dynamicChildren![0].el as TestElement)).toBe(
      '<p bar="dynamic"></p>'
    )
  })

  // the order and length of the list will not change
  test('PatchFlags: PatchFlags.STABLE_FRAGMENT', async () => {
    let list = ['foo', 'bar']
    render(
      (openBlock(),
      (block = createBlock(
        Fragment,
        null,
        list.map(item => {
          return createVNode('p', null, item, PatchFlags.TEXT)
        }),
        PatchFlags.STABLE_FRAGMENT
      ))),
      root
    )

    expect(inner(root)).toBe('<p>foo</p><p>bar</p>')
    expect(block.dynamicChildren!.length).toBe(2)
    expect(serialize(block.dynamicChildren![0].el as TestElement)).toBe(
      '<p>foo</p>'
    )
    expect(serialize(block.dynamicChildren![1].el as TestElement)).toBe(
      '<p>bar</p>'
    )

    list = list.map(item => item.repeat(2))
    render(
      (openBlock(),
      createBlock(
        Fragment,
        null,
        list.map(item => {
          return createVNode('p', null, item, PatchFlags.TEXT)
        }),
        PatchFlags.STABLE_FRAGMENT
      )),
      root
    )

    expect(inner(root)).toBe('<p>foofoo</p><p>barbar</p>')
    expect(block.dynamicChildren!.length).toBe(2)
    expect(serialize(block.dynamicChildren![0].el as TestElement)).toBe(
      '<p>foofoo</p>'
    )
    expect(serialize(block.dynamicChildren![1].el as TestElement)).toBe(
      '<p>barbar</p>'
    )
  })

  // A Fragment with `UNKEYED_FRAGMENT` flag will always patch its children,
  // so there's no need for tracking dynamicChildren.
  test('PatchFlags: PatchFlags.UNKEYED_FRAGMENT', async () => {
    const list = [{ tag: 'p', text: 'foo' }]
    render(
      (openBlock(true),
      (block = createBlock(
        Fragment,
        null,
        list.map(item => {
          return createVNode(item.tag, null, item.text)
        }),
        PatchFlags.UNKEYED_FRAGMENT
      ))),
      root
    )

    expect(inner(root)).toBe('<p>foo</p>')
    expect(block.dynamicChildren!.length).toBe(0)

    list.unshift({ tag: 'i', text: 'bar' })
    render(
      (openBlock(true),
      createBlock(
        Fragment,
        null,
        list.map(item => {
          return createVNode(item.tag, null, item.text)
        }),
        PatchFlags.UNKEYED_FRAGMENT
      )),
      root
    )

    expect(inner(root)).toBe('<i>bar</i><p>foo</p>')
    expect(block.dynamicChildren!.length).toBe(0)
  })

  // A Fragment with `KEYED_FRAGMENT` will always patch its children,
  // so there's no need for tracking dynamicChildren.
  test('PatchFlags: PatchFlags.KEYED_FRAGMENT', async () => {
    const list = [{ tag: 'p', text: 'foo' }]
    render(
      (openBlock(true),
      (block = createBlock(
        Fragment,
        null,
        list.map(item => {
          return createVNode(item.tag, { key: item.tag }, item.text)
        }),
        PatchFlags.KEYED_FRAGMENT
      ))),
      root
    )

    expect(inner(root)).toBe('<p>foo</p>')
    expect(block.dynamicChildren!.length).toBe(0)

    list.unshift({ tag: 'i', text: 'bar' })
    render(
      (openBlock(true),
      createBlock(
        Fragment,
        null,
        list.map(item => {
          return createVNode(item.tag, { key: item.tag }, item.text)
        }),
        PatchFlags.KEYED_FRAGMENT
      )),
      root
    )

    expect(inner(root)).toBe('<i>bar</i><p>foo</p>')
    expect(block.dynamicChildren!.length).toBe(0)
  })

  test('PatchFlags: PatchFlags.NEED_PATCH', async () => {
    const spyMounted = jest.fn()
    const spyUpdated = jest.fn()
    const count = ref(0)
    const Comp = {
      setup() {
        return () => {
          count.value
          return (
            openBlock(),
            (block = createBlock('div', null, [
              createVNode(
                'p',
                { onVnodeMounted: spyMounted, onVnodeBeforeUpdate: spyUpdated },
                '',
                PatchFlags.NEED_PATCH
              )
            ]))
          )
        }
      }
    }

    render(h(Comp), root)

    expect(inner(root)).toBe('<div><p></p></div>')
    expect(block!.dynamicChildren!.length).toBe(1)
    expect(serialize(block!.dynamicChildren![0].el as TestElement)).toBe(
      '<p></p>'
    )
    expect(spyMounted).toHaveBeenCalledTimes(1)
    expect(spyUpdated).toHaveBeenCalledTimes(0)

    count.value++
    await nextTick()

    expect(inner(root)).toBe('<div><p></p></div>')
    expect(block!.dynamicChildren!.length).toBe(1)
    expect(serialize(block!.dynamicChildren![0].el as TestElement)).toBe(
      '<p></p>'
    )
    expect(spyMounted).toHaveBeenCalledTimes(1)
    expect(spyUpdated).toHaveBeenCalledTimes(1)
  })

  test('PatchFlags: PatchFlags.BAIL', async () => {
    render(
      (openBlock(),
      (block = createBlock('div', null, [createVNode('p', null, 'foo')]))),
      root
    )

    expect(inner(root)).toBe('<div><p>foo</p></div>')
    expect(block!.dynamicChildren!.length).toBe(0)

    render(
      (openBlock(),
      (block = createBlock(
        'div',
        null,
        [createVNode('i', null, 'bar')],
        PatchFlags.BAIL
      ))),
      root
    )

    expect(inner(root)).toBe('<div><i>bar</i></div>')
    expect(block!.dynamicChildren).toBe(null)
  })
})
