import { expect } from 'chai'

import Fetcher from '../Fetcher'
import makeFakeFetch from '../../../../tests/util/fakeFetch'
import { jsoniblePromise } from '../jsonify'

import { ADD_ALERT } from '@instructure/quiz-common/lib/constants'

import {
  callbacksCompleteForCall,
  errorCallFor,
  startCallFor,
  successCallFor
} from '../callHelpers'

describe('Fetcher', () => {
  const SOME_CALL_TYPE = 'SOME_CALL_TYPE'
  const dispatchStub = sinon.stub()
  const getStateStub = sinon.stub()
  const userToken = 'aFakeToken'
  const apiEndpoint = 'blob:/'

  useFakeFetching({dispatchStub, getStateStub, userToken, apiEndpoint})

  const makeFetcher = (opts = {}) => {
    const fetchParams = Object.assign({
      callType: SOME_CALL_TYPE
    }, opts)
    return new Fetcher(fetchParams)
  }

  describe('appendQueryParams', () => {
    it('does not append params when not present', () => {
      const fetcher = new Fetcher()
      expect(fetcher.appendQueryParams('instructure.biz/path', {}))
        .to.eq('instructure.biz/path')
    })

    it('concatenates decamelized query string', () => {
      const fetcher = new Fetcher()
      expect(fetcher.appendQueryParams('instructure.ru/path', {
        oneFish: 2,
        redFish: 'blue'
      })).to.eq('instructure.ru/path?one_fish=2&red_fish=blue')
    })

    it('serializes array params to query string', () => {
      const fetcher = new Fetcher()
      expect(fetcher.appendQueryParams('instructure.ru/path', {
        ids: [1, 2, 3]
      })).to.eq('instructure.ru/path?ids[]=1&ids[]=2&ids[]=3')
    })
  })

  const handlesHeaderAppropriately = (methodName) => {
    it('adds the accessToken header by default', () => {
      const response = jsoniblePromise({some: 'response'})
      Fetcher.fakeFetch = sinon.spy(makeFakeFetch(response))

      const fetcher = makeFetcher()
      fetcher[methodName]('some_url.net', {})

      expect(Fetcher.fakeFetch.getCall(0).args[1].headers.Authorization)
        .to.include(userToken)
    })

    it('adds custom header if given', () => {
      const response = jsoniblePromise({some: 'response'})
      Fetcher.fakeFetch = sinon.spy(makeFakeFetch(response))

      const customHeaders = {foo: 'bar'}
      const fetcher = makeFetcher({headers: customHeaders})
      fetcher[methodName]('some_url.net', {})

      expect(Fetcher.fakeFetch.getCall(0).args[1].headers)
        .to.be.equal(customHeaders)
    })
  }

  const shouldJsonifyTheResponse = (action) => {
    it('should jsonify the response', async () => {
      const response = jsoniblePromise({some: 'response'})
      const jsonifySpy = sinon.spy(response, 'json')
      Fetcher.fakeFetch = makeFakeFetch(response)

      const fetcher = makeFetcher()
      const fetchPromise = fetcher[action]('some_url.net', {})
      await expect(fetchPromise).to.be.fulfilled()
      expect(jsonifySpy).to.have.been.called()
    })
  }

  const shouldTakeArbitraryResponseHandler = (action) => {
    it('should handle the response', async () => {
      const response = jsoniblePromise({some: 'response'})
      const responseSpy = sinon.spy(response, 'json')
      Fetcher.fakeFetch = makeFakeFetch(response)

      const fetcher = makeFetcher({asyncResponseHandler: responseSpy})
      const fetchPromise = fetcher[action]('some_url.net', {})
      await expect(fetchPromise).to.be.fulfilled()
      expect(responseSpy).to.have.been.called()
    })
  }

  const shouldAlertProperly = (action) => {
    it('should dispatch a success alert if successful and successMessage given', async () => {
      const response = jsoniblePromise({some: 'response'})
      Fetcher.fakeFetch = makeFakeFetch(response)
      const exampleSuccessMessage = 'Success: API call works!'
      const fetcher = makeFetcher({successMessage: exampleSuccessMessage})
      const fetchPromise = fetcher[action]('some_url.net', {})

      await expect(fetchPromise).to.be.fulfilled()
      const actionDispatched = dispatchStub.getCall(1).args[0]
      expect(actionDispatched.type).to.be.equal(ADD_ALERT)
      expect(actionDispatched.payload.message).to.be.equal(exampleSuccessMessage)
      expect(actionDispatched.payload.variant).to.be.equal('success')
    })

    it('should dispatch an error alert if error and json', async () => {
      const response = jsoniblePromise(
        {error: 'error'},
        {status: 400}
      )
      Fetcher.fakeFetch = makeFakeFetch(response)
      const fetcher = makeFetcher()
      const fetchPromise = fetcher[action]('some_url.net')

      await expect(fetchPromise).to.be.rejected()
      const actionDispatched = dispatchStub.getCall(1).args[0]
      expect(actionDispatched.type).to.be.equal(ADD_ALERT)
      expect(actionDispatched.payload.message).to.include('400 Error')
      expect(actionDispatched.payload.variant).to.be.equal('error')
    })

    it('should dispatch an error alert if error and text/html', async () => {
      const response = jsoniblePromise(
        {error: 'error'},
        {status: 500, contentType: 'text/html; charset=utf-8'}
      )
      Fetcher.fakeFetch = makeFakeFetch(response)
      const fetcher = makeFetcher()
      const fetchPromise = fetcher[action]('some_url.net', {})

      await expect(fetchPromise).to.be.rejected()
      const actionDispatched = dispatchStub.getCall(1).args[0]
      expect(actionDispatched.type).to.be.equal(ADD_ALERT)
      expect(actionDispatched.payload.message).to.include('500 Error')
      expect(actionDispatched.payload.variant).to.be.equal('error')
    })

    context('with a custom error handler', () => {
      context('when there is a non-success response code', () => {
        it('should dispatch a custom ErrorHandler', async () => {
          const onErrorStub = sinon.stub().returns({})
          const response = jsoniblePromise(
            {error: 'error'},
            {status: 500, contentType: 'text/html; charset=utf-8'}
          )
          Fetcher.fakeFetch = makeFakeFetch(response)
          const fetcher = makeFetcher({onError: onErrorStub})
          const fetchPromise = fetcher[action]('some_url.net', {})

          await expect(fetchPromise).to.be.rejected()
          expect(onErrorStub).to.have.been.called()
        })
      })

      context('when there is a request failure', () => {
        it('should dispatch a custom ErrorHandler', async () => {
          const errorAction = { type: 'EXAMPLE' }
          const onErrorStub = sinon.stub().returns(errorAction)
          Fetcher.fakeFetch = (url, params) => {
            return Promise.reject({ statusText: 'Failed to fetch' })
          }
          const fetcher = makeFetcher({ onError: onErrorStub, callType: null })
          const fetchPromise = fetcher[action]('some_url.net', {})

          await expect(fetchPromise).to.be.rejected()
          expect(onErrorStub).to.have.been.called()
          expect(dispatchStub.calledWith(errorAction)).to.be.true()
        })
      })

      context("when the custom error handler doesn't return an action", () => {
        it('should not call dispatch', async () => {
          const onErrorStub = sinon.stub()
          Fetcher.fakeFetch = (url, params) => {
            return Promise.reject({ statusText: 'Not Found' })
          }
          const fetcher = makeFetcher({ onError: onErrorStub, callType: null })
          const fetchPromise = fetcher[action]('some_url.net', {})

          await expect(fetchPromise).to.be.rejected()
          expect(onErrorStub).to.have.been.called()
          expect(dispatchStub).not.to.have.been.called()
        })
      })
    })
  }

  const shouldMarkCallsProperly = (methodName) => {
    it('should mark the call type as started', () => {
      const response = jsoniblePromise({some: 'response'})
      Fetcher.fakeFetch = makeFakeFetch(response)
      const fetcher = makeFetcher()
      fetcher[methodName]('some_url.net', {})

      expect(dispatchStub.calledWith(startCallFor(SOME_CALL_TYPE))).to.be.true()
    })

    it('should mark the call type as a success if response is success', async () => {
      const response = jsoniblePromise({some: 'response'})
      Fetcher.fakeFetch = makeFakeFetch(response)
      const fetcher = makeFetcher()
      const fetchPromise = fetcher[methodName]('some_url.net', {})

      await expect(fetchPromise).to.be.fulfilled()
      expect(dispatchStub.calledWith(successCallFor(SOME_CALL_TYPE))).to.be.true()
    })

    it('should mark the call type as an error if there was a request failure', async () => {
      Fetcher.fakeFetch = (url, params) => {
        return Promise.reject({ statusText: 'Failed to fetch' })
      }
      const fetcher = makeFetcher()
      const fetchPromise = fetcher[methodName]('some_url.net', {})

      await expect(fetchPromise).to.be.rejected()
      expect(dispatchStub.calledWith(errorCallFor(SOME_CALL_TYPE))).to.be.true()
    })

    it('should mark the call type as an error if response is an error', async () => {
      const response = jsoniblePromise({some: 'response'}, {status: 500})
      Fetcher.fakeFetch = makeFakeFetch(response)
      const fetcher = makeFetcher()
      const fetchPromise = fetcher[methodName]('some_url.net', {})

      await expect(fetchPromise).to.be.rejected()
      expect(dispatchStub.calledWith(errorCallFor(SOME_CALL_TYPE))).to.be.true()
    })

    it('should mark the callbacks complete if markCallbacksComplete is called', () => {
      const fetcher = makeFetcher()
      fetcher.markCallbacksComplete()

      expect(dispatchStub.calledWith(callbacksCompleteForCall(SOME_CALL_TYPE))).to.be.true()
    })
  }

  const makesProperCallOfType = (actionType, methodName, params = {}) => {
    it(`should make a proper ${actionType} call`, () => {
      const response = jsoniblePromise({some: 'response'})
      Fetcher.fakeFetch = sinon.spy(makeFakeFetch(response))

      const fetcher = makeFetcher()
      fetcher[methodName]('some_url.net', params)

      expect(Fetcher.fakeFetch.getCall(0).args[1].method).to.be.equal(actionType)
      if (params.body) {
        expect(Fetcher.fakeFetch.getCall(0).args[1].body).to.be.equal(params.body)
      }
    })
  }

  const handlesCustomEndpoint = (action) => {
    it('should use the correct base endpoint', () => {
      Fetcher.fakeFetch = sinon.stub().returns(Promise.resolve())

      const fetcher = makeFetcher()
      fetcher[action]('/path', {})

      expect(Fetcher.fakeFetch.firstCall.args[0]).to.equal('blob://path')
    })

    it('should not use the base endpoint if an absolute url is given', () => {
      Fetcher.fakeFetch = sinon.stub().returns(Promise.resolve())

      const fetcher = makeFetcher()
      fetcher[action]('http://minio.docker/path')

      expect(Fetcher.fakeFetch.firstCall.args[0]).to.equal('http://minio.docker/path')
    })
  }

  describe('#get', () => {
    shouldJsonifyTheResponse('get')
    shouldTakeArbitraryResponseHandler('get')
    shouldAlertProperly('get')
    makesProperCallOfType('GET', 'get', 'some_url.net')
    shouldMarkCallsProperly('get')
    handlesHeaderAppropriately('get')
    handlesCustomEndpoint('get')

    it('appends params to URL', () => {
      Fetcher.fakeFetch = sinon.stub().returns(Promise.resolve())

      const fetcher = makeFetcher({ state: {
        config: { apiEndpoint: 'blob:/' }
      } })
      fetcher.get('/path', { one: 'two' })

      expect(Fetcher.fakeFetch.firstCall.args[0]).to.equal('blob://path?one=two')
    })
  })

  describe('#post', () => {
    shouldJsonifyTheResponse('post')
    shouldTakeArbitraryResponseHandler('post')
    shouldAlertProperly('post')
    makesProperCallOfType('POST', 'post', {body: 'exampleBody'})
    shouldMarkCallsProperly('post')
    handlesHeaderAppropriately('post')
    handlesCustomEndpoint('post')
  })

  describe('#patch', () => {
    shouldJsonifyTheResponse('patch')
    shouldTakeArbitraryResponseHandler('patch')
    shouldAlertProperly('patch')
    makesProperCallOfType('PATCH', 'patch', {body: 'exampleBody'})
    shouldMarkCallsProperly('patch')
    handlesHeaderAppropriately('patch')
    handlesCustomEndpoint('patch')
  })

  describe('#put', () => {
    shouldJsonifyTheResponse('put')
    shouldTakeArbitraryResponseHandler('put')
    shouldAlertProperly('put')
    makesProperCallOfType('PUT', 'put', {body: 'exampleBody'})
    shouldMarkCallsProperly('put')
    handlesHeaderAppropriately('put')
    handlesCustomEndpoint('put')
  })

  describe('#delete', () => {
    it('should not jsonify the response', async () => {
      const response = jsoniblePromise({some: 'response'})
      const jsonifySpy = sinon.spy(response, 'json')
      Fetcher.fakeFetch = makeFakeFetch(response)

      const fetcher = makeFetcher()
      const fetchPromise = fetcher.delete('some_url.net')

      await expect(fetchPromise).to.be.fulfilled()
      expect(jsonifySpy).not.to.have.been.called()
    })
    shouldAlertProperly('delete')
    makesProperCallOfType('DELETE', 'delete', 'some_url.net')
    shouldMarkCallsProperly('delete')
    handlesHeaderAppropriately('delete')
    handlesCustomEndpoint('delete')
  })
})
