import Vue from 'vue'
import {
  MAX_UPDATE_COUNT,
  queueWatcher as _queueWatcher
} from 'core/observer/scheduler'

function queueWatcher(watcher) {
  watcher.vm = {} // mock vm
  _queueWatcher(watcher)
}

describe('Scheduler', () => {
  let spy
  beforeEach(() => {
    spy = vi.fn()
  })

  it('queueWatcher', done => {
    queueWatcher({
      run: spy
    })
    waitForUpdate(() => {
      expect(spy.mock.calls.length).toBe(1)
    }).then(done)
  })

  it('dedup', done => {
    queueWatcher({
      id: 1,
      run: spy
    })
    queueWatcher({
      id: 1,
      run: spy
    })
    waitForUpdate(() => {
      expect(spy.mock.calls.length).toBe(1)
    }).then(done)
  })

  it('allow duplicate when flushing', done => {
    const job = {
      id: 1,
      run: spy
    }
    queueWatcher(job)
    queueWatcher({
      id: 2,
      run() {
        queueWatcher(job)
      }
    })
    waitForUpdate(() => {
      expect(spy.mock.calls.length).toBe(2)
    }).then(done)
  })

  it('call user watchers before component re-render', done => {
    const calls: any[] = []
    const vm = new Vue({
      data: {
        a: 1
      },
      template: '<div>{{ a }}</div>',
      watch: {
        a() {
          calls.push(1)
        }
      },
      beforeUpdate() {
        calls.push(2)
      }
    }).$mount()
    vm.a = 2
    waitForUpdate(() => {
      expect(calls).toEqual([1, 2])
    }).then(done)
  })

  it('call user watcher triggered by component re-render immediately', done => {
    // this happens when a component re-render updates the props of a child
    const calls: any[] = []
    const vm = new Vue({
      data: {
        a: 1
      },
      watch: {
        a() {
          calls.push(1)
        }
      },
      beforeUpdate() {
        calls.push(2)
      },
      template: '<div><test :a="a"></test></div>',
      components: {
        test: {
          props: ['a'],
          template: '<div>{{ a }}</div>',
          watch: {
            a() {
              calls.push(3)
            }
          },
          beforeUpdate() {
            calls.push(4)
          }
        }
      }
    }).$mount()
    vm.a = 2
    waitForUpdate(() => {
      expect(calls).toEqual([1, 2, 3, 4])
    }).then(done)
  })

  it('warn against infinite update loops', function (done) {
    let count = 0
    const job = {
      id: 1,
      run() {
        count++
        queueWatcher(job)
      }
    }
    queueWatcher(job)
    waitForUpdate(() => {
      expect(count).toBe(MAX_UPDATE_COUNT + 1)
      expect('infinite update loop').toHaveBeenWarned()
    }).then(done)
  })

  it('should call newly pushed watcher after current watcher is done', done => {
    const callOrder: any[] = []
    queueWatcher({
      id: 1,
      user: true,
      run() {
        callOrder.push(1)
        queueWatcher({
          id: 2,
          run() {
            callOrder.push(3)
          }
        })
        callOrder.push(2)
      }
    })
    waitForUpdate(() => {
      expect(callOrder).toEqual([1, 2, 3])
    }).then(done)
  })

  // GitHub issue #5191
  it('emit should work when updated hook called', done => {
    const el = document.createElement('div')
    const vm = new Vue({
      template: `<div><child @change="bar" :foo="foo"></child></div>`,
      data: {
        foo: 0
      },
      methods: {
        bar: spy
      },
      components: {
        child: {
          template: `<div>{{foo}}</div>`,
          props: ['foo'],
          updated() {
            this.$emit('change')
          }
        }
      }
    }).$mount(el)
    vm.$nextTick(() => {
      vm.foo = 1
      vm.$nextTick(() => {
        expect(vm.$el.innerHTML).toBe('<div>1</div>')
        expect(spy).toHaveBeenCalled()
        done()
      })
    })
  })
})