import { patch } from 'web/runtime/patch'
import { createPatchFunction } from 'core/vdom/patch'
import baseModules from 'core/vdom/modules/index'
import * as nodeOps from 'web/runtime/node-ops'
import platformModules from 'web/runtime/modules/index'
import VNode from 'core/vdom/vnode'

const modules = baseModules.concat(platformModules) as any[]

describe('vdom patch: hooks', () => {
  let vnode0
  beforeEach(() => {
    vnode0 = new VNode('p', { attrs: { id: '1' } }, [
      createTextVNode('hello world')
    ])
    patch(null, vnode0)
  })

  it('should call `insert` listener after both parents, siblings and children have been inserted', () => {
    const result: any[] = []
    function insert(vnode) {
      expect(vnode.elm.children.length).toBe(2)
      expect(vnode.elm.parentNode.children.length).toBe(3)
      result.push(vnode)
    }
    const vnode1 = new VNode('div', {}, [
      new VNode('span', {}, undefined, 'first sibling'),
      new VNode('div', { hook: { insert } }, [
        new VNode('span', {}, undefined, 'child 1'),
        new VNode('span', {}, undefined, 'child 2')
      ]),
      new VNode('span', {}, undefined, 'can touch me')
    ])
    patch(vnode0, vnode1)
    expect(result.length).toBe(1)
  })

  it('should call `prepatch` listener', () => {
    const result: any[] = []
    function prepatch(oldVnode, newVnode) {
      expect(oldVnode).toEqual(vnode1.children[1])
      expect(newVnode).toEqual(vnode2.children[1])
      result.push(newVnode)
    }
    const vnode1 = new VNode('div', {}, [
      new VNode('span', {}, undefined, 'first sibling'),
      new VNode('div', { hook: { prepatch } }, [
        new VNode('span', {}, undefined, 'child 1'),
        new VNode('span', {}, undefined, 'child 2')
      ])
    ])
    const vnode2 = new VNode('div', {}, [
      new VNode('span', {}, undefined, 'first sibling'),
      new VNode('div', { hook: { prepatch } }, [
        new VNode('span', {}, undefined, 'child 1'),
        new VNode('span', {}, undefined, 'child 2')
      ])
    ])
    patch(vnode0, vnode1)
    patch(vnode1, vnode2)
    expect(result.length).toBe(1)
  })

  it('should call `postpatch` after `prepatch` listener', () => {
    const pre: any[] = []
    const post: any[] = []
    function prepatch(oldVnode, newVnode) {
      pre.push(pre)
    }
    function postpatch(oldVnode, newVnode) {
      expect(pre.length).toBe(post.length + 1)
      post.push(post)
    }
    const vnode1 = new VNode('div', {}, [
      new VNode('span', {}, undefined, 'first sibling'),
      new VNode('div', { hook: { prepatch, postpatch } }, [
        new VNode('span', {}, undefined, 'child 1'),
        new VNode('span', {}, undefined, 'child 2')
      ])
    ])
    const vnode2 = new VNode('div', {}, [
      new VNode('span', {}, undefined, 'first sibling'),
      new VNode('div', { hook: { prepatch, postpatch } }, [
        new VNode('span', {}, undefined, 'child 1'),
        new VNode('span', {}, undefined, 'child 2')
      ])
    ])
    patch(vnode0, vnode1)
    patch(vnode1, vnode2)
    expect(pre.length).toBe(1)
    expect(post.length).toBe(1)
  })

  it('should call `update` listener', () => {
    const result1: any[] = []
    const result2: any[] = []
    function cb(result, oldVnode, newVnode) {
      if (result.length > 1) {
        expect(result[result.length - 1]).toEqual(oldVnode)
      }
      result.push(newVnode)
    }
    const vnode1 = new VNode('div', {}, [
      new VNode('span', {}, undefined, 'first sibling'),
      new VNode('div', { hook: { update: cb.bind(null, result1) } }, [
        new VNode('span', {}, undefined, 'child 1'),
        new VNode(
          'span',
          { hook: { update: cb.bind(null, result2) } },
          undefined,
          'child 2'
        )
      ])
    ])
    const vnode2 = new VNode('div', {}, [
      new VNode('span', {}, undefined, 'first sibling'),
      new VNode('div', { hook: { update: cb.bind(null, result1) } }, [
        new VNode('span', {}, undefined, 'child 1'),
        new VNode(
          'span',
          { hook: { update: cb.bind(null, result2) } },
          undefined,
          'child 2'
        )
      ])
    ])
    patch(vnode0, vnode1)
    patch(vnode1, vnode2)
    expect(result1.length).toBe(1)
    expect(result2.length).toBe(1)
  })

  it('should call `remove` listener', () => {
    const result: any[] = []
    function remove(vnode, rm) {
      const parent = vnode.elm.parentNode
      expect(vnode.elm.children.length).toBe(2)
      expect(vnode.elm.children.length).toBe(2)
      result.push(vnode)
      rm()
      expect(parent.children.length).toBe(1)
    }
    const vnode1 = new VNode('div', {}, [
      new VNode('span', {}, undefined, 'first sibling'),
      new VNode('div', { hook: { remove } }, [
        new VNode('span', {}, undefined, 'child 1'),
        new VNode('span', {}, undefined, 'child 2')
      ])
    ])
    const vnode2 = new VNode('div', {}, [
      new VNode('span', {}, undefined, 'first sibling')
    ])
    patch(vnode0, vnode1)
    patch(vnode1, vnode2)
    expect(result.length).toBe(1)
  })

  it('should call `init` and `prepatch` listeners on root', () => {
    let count = 0
    function init(vnode) {
      count++
    }
    function prepatch(oldVnode, newVnode) {
      count++
    }
    const vnode1 = new VNode('div', { hook: { init, prepatch } })
    patch(vnode0, vnode1)
    expect(count).toBe(1)
    const vnode2 = new VNode('span', { hook: { init, prepatch } })
    patch(vnode1, vnode2)
    expect(count).toBe(2)
  })

  it('should remove element when all remove listeners are done', () => {
    let rm1, rm2, rm3
    const patch1 = createPatchFunction({
      nodeOps,
      // @ts-ignore - TODO dtw
      modules: modules.concat([
        {
          remove(_, rm) {
            rm1 = rm
          }
        },
        {
          remove(_, rm) {
            rm2 = rm
          }
        }
      ])
    })
    const vnode1 = new VNode('div', {}, [
      new VNode('a', {
        hook: {
          remove(_, rm) {
            rm3 = rm
          }
        }
      })
    ])
    const vnode2 = new VNode('div', {}, [])
    let elm = patch1(vnode0, vnode1)
    expect(elm.children.length).toBe(1)
    elm = patch1(vnode1, vnode2)
    expect(elm.children.length).toBe(1)
    rm1()
    expect(elm.children.length).toBe(1)
    rm3()
    expect(elm.children.length).toBe(1)
    rm2()
    expect(elm.children.length).toBe(0)
  })

  it('should invoke the remove hook on replaced root', () => {
    const result: any[] = []
    const parent = nodeOps.createElement('div')
    vnode0 = nodeOps.createElement('div')
    parent.appendChild(vnode0)
    function remove(vnode, rm) {
      result.push(vnode)
      rm()
    }
    const vnode1 = new VNode('div', { hook: { remove } }, [
      new VNode('b', {}, undefined, 'child 1'),
      new VNode('i', {}, undefined, 'child 2')
    ])
    const vnode2 = new VNode('span', {}, [
      new VNode('b', {}, undefined, 'child 1'),
      new VNode('i', {}, undefined, 'child 2')
    ])
    patch(vnode0, vnode1)
    patch(vnode1, vnode2)
    expect(result.length).toBe(1)
  })

  it('should invoke global `destroy` hook for all removed children', () => {
    const result: any[] = []
    function destroy(vnode) {
      result.push(vnode)
    }
    const vnode1 = new VNode('div', {}, [
      new VNode('span', {}, undefined, 'first sibling'),
      new VNode('div', {}, [
        new VNode('span', { hook: { destroy } }, undefined, 'child 1'),
        new VNode('span', {}, undefined, 'child 2')
      ])
    ])
    const vnode2 = new VNode('div')
    patch(vnode0, vnode1)
    patch(vnode1, vnode2)
    expect(result.length).toBe(1)
  })

  it('should handle text vnodes with `undefined` `data` property', () => {
    const vnode1 = new VNode('div', {}, [createTextVNode(' ')])
    const vnode2 = new VNode('div', {}, [])
    patch(vnode0, vnode1)
    patch(vnode1, vnode2)
  })

  it('should invoke `destroy` module hook for all removed children', () => {
    let created = 0
    let destroyed = 0
    const patch1 = createPatchFunction({
      nodeOps,
      modules: modules.concat([
        {
          create() {
            created++
          }
        },
        {
          destroy() {
            destroyed++
          }
        }
      ])
    })
    const vnode1 = new VNode('div', {}, [
      new VNode('span', {}, undefined, 'first sibling'),
      new VNode('div', {}, [
        new VNode('span', {}, undefined, 'child 1'),
        new VNode('span', {}, undefined, 'child 2')
      ])
    ])
    const vnode2 = new VNode('div', {})
    patch1(vnode0, vnode1)
    expect(destroyed).toBe(1) // should invoke for replaced root nodes too
    patch1(vnode1, vnode2)
    expect(created).toBe(5)
    expect(destroyed).toBe(5)
  })

  it('should not invoke `create` and `remove` module hook for text nodes', () => {
    let created = 0
    let removed = 0
    const patch1 = createPatchFunction({
      nodeOps,
      modules: modules.concat([
        {
          create() {
            created++
          }
        },
        {
          remove() {
            removed++
          }
        }
      ])
    })
    const vnode1 = new VNode('div', {}, [
      new VNode('span', {}, undefined, 'first child'),
      createTextVNode(''),
      new VNode('span', {}, undefined, 'third child')
    ])
    const vnode2 = new VNode('div', {})
    patch1(vnode0, vnode1)
    patch1(vnode1, vnode2)
    expect(created).toBe(3)
    expect(removed).toBe(2)
  })

  it('should not invoke `destroy` module hook for text nodes', () => {
    let created = 0
    let destroyed = 0
    const patch1 = createPatchFunction({
      nodeOps,
      modules: modules.concat([
        {
          create() {
            created++
          }
        },
        {
          destroy() {
            destroyed++
          }
        }
      ])
    })
    const vnode1 = new VNode('div', {}, [
      new VNode('span', {}, undefined, 'first sibling'),
      new VNode('div', {}, [
        new VNode('span', {}, undefined, 'child 1'),
        new VNode('span', {}, [
          createTextVNode('text1'),
          createTextVNode('text2')
        ])
      ])
    ])
    const vnode2 = new VNode('div', {})
    patch1(vnode0, vnode1)
    expect(destroyed).toBe(1) // should invoke for replaced root nodes too
    patch1(vnode1, vnode2)
    expect(created).toBe(5)
    expect(destroyed).toBe(5)
  })

  it('should call `create` listener before inserted into parent but after children', () => {
    const result: any[] = []
    function create(empty, vnode) {
      expect(vnode.elm.children.length).toBe(2)
      expect(vnode.elm.parentNode).toBe(null)
      result.push(vnode)
    }
    const vnode1 = new VNode('div', {}, [
      new VNode('span', {}, undefined, 'first sibling'),
      new VNode('div', { hook: { create } }, [
        new VNode('span', {}, undefined, 'child 1'),
        new VNode('span', {}, undefined, 'child 2')
      ]),
      new VNode('span', {}, undefined, "can't touch me")
    ])
    patch(vnode0, vnode1)
    expect(result.length).toBe(1)
  })
})