import configureStore from 'redux-mock-store'
import multi from 'redux-multi'
import thunkMiddleware from 'redux-thunk'
import { Map } from 'immutable'
import { expect } from 'chai'

import { DO_NOTHING, UPDATE_QUIZ_ENTRY_CALL, MOVED_QUIZ_ENTRY } from '@instructure/quiz-common/lib/constants'
import Fetcher from '../../../common/util/Fetcher'
import Item from '../../../common/records/Item'
import QuizEntry from '../../../common/records/QuizEntry'
import Stimulus from '../../../common/records/Stimulus'

import { startCallFor, successCallFor } from '../../../common/util/callHelpers'
import {
  addQuizEntries,
  addQuizEntry,
  moveQuizEntry as moveQuizEntryAction
} from '../../../common/actions/quizEntries'
import { associateQuizEntries } from '../../../common/actions/quizzes'
import { set as setUI } from '../../../common/actions/ui'
import { scrollToItem } from '../../../common/actions/scrolling'

import {
  createItemAndQuizEntry,
  createQuizEntry,
  createStimulusAndQuizEntry,
  getQuizEntries,
  getQuizEntry,
  moveImmutableQuizEntry,
  moveQuizEntry,
  removeQuizEntry,
  removeStimulusQuizEntry,
  updateQuizEntry
} from '../quizEntries'

import makeFakeFetch from '../../../../tests/util/fakeFetch'
import { jsoniblePromise } from '../../../common/util/jsonify'
import { setFakeStore, unsetFakeStore } from '../../../reduxStore'

describe('quizEntries api actions creators', () => {
  const mockStoreFactory = configureStore([multi, thunkMiddleware])
  const mockStore = (state = Map()) => {
    const store = mockStoreFactory(state)
    // Fetcher dispatches CALL_STATUS actions. This makes it dispatch the
    // actions to the mock store
    Fetcher.fakeDispatch = store.dispatch
    // We also need to do this to make all the ReduxRecord relational stuff use
    // our fake state
    setFakeStore(state)
    return store
  }

  afterEach(() => {
    unsetFakeStore()
  })

  useFakeFetching()

  describe('getQuizEntries', () => {
    const quizId = '7'
    const fakeQuizEntries = [{
      id: '2',
      entryId: '3',
      quiz: '7',
      entry_type: 'Item',
      entry: {
        id: '3'
      }
    }]

    it('should hit the right endpoint', () => {
      const store = mockStore()
      store.dispatch(getQuizEntries(quizId))
      expect(Fetcher.fakeFetch).to.have.been.calledWith(`/api/quizzes/${quizId}/quiz_entries`)
    })

    const fakeGetQuizEntries = () => {
      const response = jsoniblePromise(fakeQuizEntries)
      Fetcher.fakeFetch = makeFakeFetch(response)
      return getQuizEntries(quizId)
    }

    it('should try to add to the quiz entries in the store', async () => {
      const actionPayload = [{
        id: '2',
        entryId: '3',
        quiz: '7',
        entryType: 'Item',
        entry: {
          id: '3',
          schema: 'Item'
        }
      }]
      const store = mockStore()
      const handleItemAction = addQuizEntries(actionPayload)
      await store.dispatch(fakeGetQuizEntries())
      expect(store.getActions()).to.deep.include(handleItemAction)
    })

    it('should try to associate the entry with the quiz', async () => {
      const store = mockStore()
      const associateEntryAction = associateQuizEntries(quizId, fakeQuizEntries)
      await store.dispatch(fakeGetQuizEntries())
      expect(store.getActions()).to.deep.include(associateEntryAction)
    })
  })

  describe('updateQuizEntry', () => {
    const quizId = '1'
    const quizEntryId = '22'
    const quizEntryMod = {
      id: quizEntryId,
      pointsPossible: '5',
      position: '4',
      quiz: quizId,
      entryId: null
    }

    const fakeUpdateQuizEntry = (opts) => {
      const response = jsoniblePromise(quizEntryMod)
      Fetcher.fakeFetch = makeFakeFetch(response)
      return updateQuizEntry(quizId, quizEntryId, quizEntryMod, opts)
    }

    it('should hit the right endpoint', async () => {
      const store = mockStore()
      await store.dispatch(fakeUpdateQuizEntry())
      expect(Fetcher.fakeFetch).to.have.been.calledWith(`/api/quizzes/${quizId}/quiz_entries/${quizEntryId}`)
    })

    it('should filter out null quiz_entry values', async () => {
      const store = mockStore()
      await store.dispatch(fakeUpdateQuizEntry())
      const callJson = Fetcher.fakeFetch.getCall(0).args[1]

      expect(callJson.body).to.not.include('entry_id')
    })

    it('should mark the update quiz entry call as started', async () => {
      const store = mockStore()
      await store.dispatch(fakeUpdateQuizEntry())
      const updateCallStartAction = startCallFor(UPDATE_QUIZ_ENTRY_CALL)
      expect(store.getActions()).to.deep.include(updateCallStartAction)
    })

    it('should mark the update call as a success on return', async () => {
      const store = mockStore()
      await store.dispatch(fakeUpdateQuizEntry())
      const successfulUpdate = successCallFor(UPDATE_QUIZ_ENTRY_CALL)
      expect(store.getActions()).to.deep.include(successfulUpdate)
    })

    it('should try to update the quiz entry in the store', async () => {
      const store = mockStore()
      await store.dispatch(fakeUpdateQuizEntry())
      const handleItemAction = addQuizEntry([quizEntryMod])
      expect(store.getActions()).to.deep.include(handleItemAction)
    })

    it('should not update the quiz entry in the store if handleResponse is false', async () => {
      const store = mockStore()
      await store.dispatch(fakeUpdateQuizEntry({ handleResponse: false }))
      const handleItemAction = addQuizEntry([quizEntryMod])
      expect(store.getActions()).not.to.deep.include(handleItemAction)
    })
  })

  describe('moveQuizEntry', () => {
    // testrail test_id: C3003422
    const quizEntryId = '22'
    const quizId = '1'
    const position = 1
    const stimulusQuizEntryId = 5
    const moveTempItemStub = sinon.stub()
    const actionStub = sinon.stub()

    beforeEach(() => {
      actionStub.returns({ type: DO_NOTHING })
    })

    afterEach(() => {
      actionStub.reset()
      moveTempItemStub.reset()
    })

    const stubbedMoveQuizEntryCall = () => {
      return moveQuizEntry(
        quizId, position, stimulusQuizEntryId, quizEntryId, moveTempItemStub, actionStub
      )
    }

    it('moves the entry', async () => {
      const store = mockStore()
      await store.dispatch(stubbedMoveQuizEntryCall())
      const moveAction = moveQuizEntryAction(quizEntryId, position, stimulusQuizEntryId)
      expect(store.getActions()).to.deep.include(moveAction)
    })

    it('should setUI with the moved quiz entry id if position changed', async () => {
      const store = mockStore()
      await store.dispatch(stubbedMoveQuizEntryCall())
      expect(store.getActions()).to.deep.include(setUI(MOVED_QUIZ_ENTRY, quizEntryId))
    })

    it('calls updateQuizEntry with the right argument', async () => {
      const store = mockStore()
      await store.dispatch(stubbedMoveQuizEntryCall())
      expect(actionStub).to.have.been.calledWith(quizId, quizEntryId, {position, stimulusQuizEntryId})
    })

    it('calls moveTempItem', async () => {
      const store = mockStore()
      await store.dispatch(stubbedMoveQuizEntryCall())
      expect(moveTempItemStub).to.have.been.calledOnce()
    })

    it('re-fetches all quiz entries if the request fails', async () => {
      actionStub.returns(() => Promise.reject())
      const state = Map({ quizzes: Map({ activeQuizId: quizId }) })
      const store = mockStore(state)
      await expect(store.dispatch(stubbedMoveQuizEntryCall())).to.be.rejected()
      expect(Fetcher.fakeFetch).to.have.been.calledWithMatch(
        `/api/quizzes/${quizId}/quiz_entries`,
        { method: 'GET' }
      )
    })
  })

  describe('createStimulusAndQuizEntry', () => {
    const quizId = '7'
    const quizEntry = new QuizEntry({
      id: '22',
      entry: Map({ id: 'foo' }),
      position: '1',
      entryType: 'Stimulus',
      quizEntryStimulusId: 55
    })
    const fakeStimulusData = { id: 'foo' }
    const stubbedStimulusCreation = sinon.stub().returns(() => Promise.resolve(fakeStimulusData))
    const stubbedQuizEntryCreation = sinon.stub().returns({ type: DO_NOTHING })

    afterEach(() => {
      stubbedStimulusCreation.resetHistory()
      stubbedQuizEntryCreation.resetHistory()
    })

    const fakeCreateStimulusAndQuizEntry = () => {
      const response = jsoniblePromise(quizEntry)
      Fetcher.fakeFetch = makeFakeFetch(response)

      return createStimulusAndQuizEntry(
        quizId,
        fakeStimulusData,
        quizEntry,
        stubbedStimulusCreation,
        stubbedQuizEntryCreation
      )
    }

    it('calls createStimulus with the right argument', async () => {
      const store = mockStore()
      await store.dispatch(fakeCreateStimulusAndQuizEntry())
      expect(stubbedStimulusCreation).to.have.been.calledWith(fakeStimulusData)
    })

    it('calls createQuizEntry with the right arguments', async () => {
      const store = mockStore()
      await store.dispatch(fakeCreateStimulusAndQuizEntry())
      expect(stubbedQuizEntryCreation).to.have.been.calledWith('7', 'foo', 'Stimulus')
    })
  })

  describe('createItemAndQuizEntry', () => {
    const quizId = '7'
    const quizEntry = new QuizEntry({
      id: '22',
      pointsPossible: '5',
      position: '1',
      entryType: 'Item',
      stimulusQuizEntryId: '55'
    })
    const fakeItemData = { id: 'foo' }
    const stubbedItemCreation = sinon.stub().returns(() => Promise.resolve(fakeItemData))
    const stubbedQuizEntryCreation = sinon.stub().returns({ type: DO_NOTHING })

    const fakeCreateItemAndQuizEntry = () => {
      const response = jsoniblePromise(quizEntry)
      Fetcher.fakeFetch = makeFakeFetch(response)

      return createItemAndQuizEntry(
        quizId,
        fakeItemData,
        quizEntry,
        stubbedItemCreation,
        stubbedQuizEntryCreation
      )
    }

    it('calls createItem with the right argument', async () => {
      const store = mockStore()
      await store.dispatch(fakeCreateItemAndQuizEntry())
      expect(stubbedItemCreation).to.have.been.calledWith(fakeItemData)
    })

    it('calls createQuizEntry with the right arguments', async () => {
      const store = mockStore()
      const expectedEntryProperties = {
        points_possible: 5,
        position: 1,
        stimulus_quiz_entry_id: '55'
      }
      await store.dispatch(fakeCreateItemAndQuizEntry())
      expect(stubbedQuizEntryCreation).to.have.been.calledWith('7', 'foo', 'Item', expectedEntryProperties)
    })
  })

  describe('createQuizEntry', () => {
    const quizId = '7'
    const itemId = '1'
    const quizEntryId = '2'

    const stubbedCreateQuizEntryCall = () => {
      const fakeQuizEntry = {
        id: quizEntryId,
        entryId: itemId,
        quiz: quizId
      }
      const entryResponse = jsoniblePromise(fakeQuizEntry)
      const entriesResponse = jsoniblePromise([fakeQuizEntry])
      // 2nd call stubs fetch in getQuizEntries, which is called from createQuizEntry
      Fetcher.fakeFetch = makeFakeFetch(entryResponse).onCall(1).resolves(entriesResponse)
      return createQuizEntry(quizId, itemId, 'Item', {})
    }

    it('should hit the right endpoints', async () => {
      const store = mockStore()
      await store.dispatch(stubbedCreateQuizEntryCall())
      expect(Fetcher.fakeFetch).to.have.been.calledWith(`/api/quizzes/${quizId}/quiz_entries`)
    })

    it('should try to scroll to get the quiz entries and scroll to the item', async () => {
      const store = mockStore()
      await store.dispatch(stubbedCreateQuizEntryCall())
      const scrollAction = scrollToItem(quizEntryId, true)
      expect(store.getActions()).to.deep.include(scrollAction)
    })
  })

  describe('removeQuizEntry', () => {
    const quizId = '7'
    const quizEntry = {id: '1', pointsPossible: '5'}

    const stubbedRemoveQuizEntryCall = () => {
      const response = jsoniblePromise(null)
      Fetcher.fakeFetch = makeFakeFetch(response)
      return removeQuizEntry(quizId, quizEntry)
    }

    it('should hit the right endpoint', async () => {
      const store = mockStore()
      await store.dispatch(stubbedRemoveQuizEntryCall())
      const url = `/api/quizzes/${quizId}/quiz_entries/${quizEntry.id}`
      expect(Fetcher.fakeFetch).to.have.been.calledWithMatch(url, { method: 'DELETE' })
    })
  })

  describe('getQuizEntry', () => {
    const quizId = '7'
    const quizEntryId = '1'

    const stubbedGetQuizEntryCall = () => {
      const response = jsoniblePromise({ id: quizEntryId, quiz: quizId })
      Fetcher.fakeFetch = makeFakeFetch(response)
      return getQuizEntry(quizId, quizEntryId)
    }

    it('should hit the right endpoint', async () => {
      const store = mockStore()
      await store.dispatch(stubbedGetQuizEntryCall())
      expect(Fetcher.fakeFetch).to.have.been.calledWith(`/api/quizzes/${quizId}/quiz_entries/${quizEntryId}`)
    })
  })

  describe('removeStimulusQuizEntry', () => {
    const quizId = '7'
    const quizEntry = {id: '1', pointsPossible: '5'}
    const childEntries = [
      {id: '555'},
      {id: '666'}
    ]

    const stubbedRemoveQuizEntryCall = () => {
      const response = jsoniblePromise(null)
      Fetcher.fakeFetch = makeFakeFetch(response)
      return removeStimulusQuizEntry(
        quizId,
        quizEntry,
        childEntries
      )
    }

    it('should hit the right endpoint', async () => {
      const store = mockStore()
      await store.dispatch(stubbedRemoveQuizEntryCall())
      const url = `/api/quizzes/${quizId}/quiz_entries/${quizEntry.id}/stimuli`
      const params = { body: JSON.stringify({ child_ids: childEntries.map(e => e.id) }) }
      expect(Fetcher.fakeFetch).to.have.been.calledWithMatch(url, {
        method: 'DELETE',
        ...params
      })
    })
  })

  describe('moveImmutableQuizEntry', () => {
    const quizId = '3'
    const itemId = '13'
    const stimulusId = '15'
    const quizEntryId = '42'
    const stimulusQuizEntryId = '43'

    const state = Map({
      quizzes: Map({
        activeQuizId: quizId
      }),
      items: Map({
        [itemId]: new Item({
          id: itemId,
          status: 'immutable'
        }),
        [stimulusId]: new Stimulus({
          id: stimulusId
        })
      }),
      quizEntries: Map({
        [quizEntryId]: new QuizEntry({
          id: quizEntryId,
          entryId: itemId,
          entryType: 'Item',
          position: 1
        }),
        [stimulusQuizEntryId]: new QuizEntry({
          id: stimulusQuizEntryId,
          entryId: stimulusId,
          entryType: 'Stimulus',
          position: 2
        })
      })
    })

    const position = 3
    const newItemId = '16'
    const createItemStub = sinon.stub()
    const updateStub = sinon.stub()
    const fakeMoveImmutableQuizEntry = () => {
      return moveImmutableQuizEntry(
        quizId,
        position,
        stimulusQuizEntryId,
        quizEntryId,
        createItemStub,
        updateStub
      )
    }

    beforeEach(() => {
      createItemStub.returns(() => Promise.resolve({ id: newItemId }))
      updateStub.returns(() => Promise.resolve())
    })

    afterEach(() => {
      createItemStub.reset()
      updateStub.reset()
    })

    it('moves the entry', async () => {
      const store = mockStore(state)
      await store.dispatch(fakeMoveImmutableQuizEntry())
      const moveAction = moveQuizEntryAction(quizEntryId, position, stimulusQuizEntryId)
      expect(store.getActions()).to.deep.include(moveAction)
    })

    it('creates a new item', async () => {
      const store = mockStore(state)
      await store.dispatch(fakeMoveImmutableQuizEntry())
      expect(createItemStub).to.have.been.calledWithMatch(item => item instanceof Item, quizId, 'quiz', itemId)
    })

    it('associates the quiz entry with the new item and moves it', async () => {
      const store = mockStore(state)
      await store.dispatch(fakeMoveImmutableQuizEntry())
      expect(updateStub).to.have.been.calledWithMatch(quizId, quizEntryId, {
        entryId: newItemId,
        position,
        stimulusQuizEntryId
      }, { handleResponse: false })
    })

    it('re-fetches all quiz entries if createItem fails', async () => {
      createItemStub.returns(() => Promise.reject())
      const store = mockStore(state)
      await expect(store.dispatch(fakeMoveImmutableQuizEntry())).to.be.rejected()
      expect(Fetcher.fakeFetch).to.have.been.calledWithMatch(
        `/api/quizzes/${quizId}/quiz_entries`,
        { method: 'GET' }
      )
    })

    it('re-fetches all quiz entries if updateQuizEntry fails', async () => {
      updateStub.returns(() => Promise.reject())
      const store = mockStore(state)
      await expect(store.dispatch(fakeMoveImmutableQuizEntry())).to.be.rejected()
      expect(Fetcher.fakeFetch).to.have.been.calledWithMatch(
        `/api/quizzes/${quizId}/quiz_entries`,
        { method: 'GET' }
      )
    })
  })
})
