import ThunkQueue from '../ThunkQueue'

describe('ThunkQueue', () => {
  const dispatch = () => {}
  const getState = () => {}

  describe('#wrapActionCreator', () => {
    it('returns a function', () => {
      const queue = new ThunkQueue()
      expect(queue.wrapActionCreator(() => {})).to.be.a('function')
    })

    it('returns an action creator that wraps returned thunks', () => {
      const thunk = () => {}
      const actionCreator = sinon.stub().returns(thunk)

      const queue = new ThunkQueue()
      const wrapThunk = sinon.stub(queue, 'wrapThunk')
      const wrappedActionCreator = queue.wrapActionCreator(actionCreator)
      wrappedActionCreator('foo')

      expect(actionCreator).to.have.been.calledOnce()
      expect(actionCreator).to.have.been.calledWith('foo')

      expect(wrapThunk).to.have.been.calledOnce()
      expect(wrapThunk).to.have.been.calledWith(thunk)
    })
  })

  describe('#wrapThunk', () => {
    it('returns regular redux actions unmodified', () => {
      const queue = new ThunkQueue()
      const action = { type: 'SOME_ACTION' }
      expect(queue.wrapThunk(action)).to.deep.equal(action)
    })

    it('returns a function', async () => {
      const queue = new ThunkQueue()
      expect(queue.wrapThunk(() => {})).to.be.a('function')
    })

    it('calls the given thunk immediately when empty', async () => {
      const thunk = sinon.spy()
      const queue = new ThunkQueue()
      const wrappedThunk = queue.wrapThunk(thunk)

      await wrappedThunk(dispatch, getState)
      expect(thunk).to.have.been.calledOnce()
      expect(thunk).to.have.been.calledWith(dispatch, getState)
    })

    it('waits when there are thunks in the queue', async () => {
      const thunks = [sinon.stub().resolves(), sinon.stub().resolves()]
      const queue = new ThunkQueue()
      const wrappedThunks = thunks.map(thunk => queue.wrapThunk(thunk))
      const promises = wrappedThunks.map(thunk => thunk(dispatch, getState))

      await promises[0]
      expect(thunks[0]).to.have.been.calledOnce()
      expect(thunks[0]).to.have.been.calledWith(dispatch, getState)
      expect(thunks[1]).not.to.have.been.called()

      await promises[1]
      expect(thunks[0]).to.have.been.calledOnce()
      expect(thunks[1]).to.have.been.calledOnce()
      expect(thunks[1]).to.have.been.calledWith(dispatch, getState)
    })

    context('on error', () => {
      it('calls the given error handler', async () => {
        const error = new Error('FAIL')
        const thunk = sinon.stub().rejects(error)

        const errorHandler = sinon.spy()
        const queue = new ThunkQueue(errorHandler)
        const wrappedThunk = queue.wrapThunk(thunk)

        await wrappedThunk(dispatch, getState)
        expect(errorHandler).to.have.been.calledOnce()
        expect(errorHandler).to.have.been.calledWith(error, dispatch, getState)
      })

      it('rejects if no error handler is given', async () => {
        const error = new Error('FAIL')
        const thunk = sinon.stub().rejects(error)

        const queue = new ThunkQueue()
        const wrappedThunk = queue.wrapThunk(thunk)

        await expect(wrappedThunk(dispatch, getState)).to.be.rejectedWith(error)
      })

      it('rejects with the error thrown by the error handler', async () => {
        const thunk = sinon.stub().rejects()

        const error = new Error('FAIL')
        const errorHandler = sinon.stub().rejects(error)
        const queue = new ThunkQueue(errorHandler)
        const wrappedThunk = queue.wrapThunk(thunk)

        await expect(wrappedThunk(dispatch, getState)).to.be.rejectedWith(error)
      })

      it('cancels pending thunks', async () => {
        const queue = new ThunkQueue(() => {})
        const thunks = [sinon.stub().rejects(), sinon.stub().resolves()]
        const wrappedThunks = thunks.map(thunk => queue.wrapThunk(thunk))
        const promises = wrappedThunks.map(thunk => thunk(dispatch, getState))

        await Promise.all(promises)
        expect(thunks[0]).to.have.been.calledOnce()
        expect(thunks[1]).not.to.have.been.called()
      })

      it('resolves new thunks normally after handling an error', async () => {
        const queue = new ThunkQueue(() => {})
        const thunks = [sinon.stub().rejects(), sinon.stub().resolves()]
        const wrappedThunks = thunks.map(thunk => queue.wrapThunk(thunk))

        await wrappedThunks[0](dispatch, getState)
        await wrappedThunks[1](dispatch, getState)
        expect(thunks[1]).to.have.been.calledOnce()
        expect(thunks[1]).to.have.been.calledWith(dispatch, getState)
      })
    })
  })
})
