import React from 'react'
import { expect } from 'chai'
import { mount } from 'enzyme'
import { fromJS } from 'immutable'

import {
  ADD_SESSION_ITEMS,
  OPEN_MODAL,
  CLOSE_MODAL,
  SESSION_STARTED_OR_RESUMED_EVENT,
  SET_UI,
  STUDENT_ACCESS_CODE_STATUS,
  UPDATE_QUIZ_SESSION_STATUS_ON_PAGE_LOAD
} from '@instructure/quiz-common/lib/constants'

import {
  closeSubmitConfirmModal,
  confirmSessionStartPage,
  getResumeData,
  getTimeRemaining,
  makeSessionItemCall,
  messageForSubmitModal,
  newQuizTakingSession,
  nextQuestion,
  onQuizSessionReadyForTaking,
  postQuizSession,
  recordQuizTime,
  showSubmitConfirmModal,
  submitQuiz,
  submitStudentAccessCode
} from '../taking'

import {
  backtrackingNextQuestion,
  setCurrentSessionItemPosition,
  submitQuizSessionEvent
} from '../../../common/actions/taking'

import QuizSession from '../../../common/records/QuizSession'
import { screenreaderNotification } from '../../../common/actions/alerts'
import makeFakeFetch, { makeFakeFetchFail } from '../../../../tests/util/fakeFetch'
import Fetcher from '../../../common/util/Fetcher'
import { jsoniblePromise } from '../../../common/util/jsonify'
import noop from '../../../common/util/noop'

describe('taking api actions creators', () => {
  const quizSessionId = '42'
  const fetchStub = sinon.stub()
  const dispatchStub = sinon.stub()
  const fakeResponseState = {
    taking: {
      responses: {}
    }
  }
  const fakeSessionItemsState = {
    sessionItems: {
      '42_1': {
        id: '1',
        item: {itemBody: 'STEM'},
        position: 1,
        quizSessionId
      },
      '42_2': {
        id: '2',
        item: {itemBody: 'STEM TWO'},
        position: 10,
        quizSessionId
      },
      '99_1': {
        id: '2',
        item: {itemBody: 'STEM THREE'},
        position: 5,
        quizSessionId: '99'
      }
    }
  }
  const getStateStub = sinon.stub()
  const fakeState = Object.assign({}, fakeResponseState, fakeSessionItemsState)

  useFakeFetching({dispatchStub, getStateStub, fetchStub, fakeState})

  const response = jsoniblePromise({})
  const fakeFetch = makeFakeFetch(response)

  describe('confirmSessionStartPage', () => {
    const confirmSessionStartPageThunk = confirmSessionStartPage(quizSessionId)

    const stubbedConfirmSessionStartPageCall = () => {
      Fetcher.fakeFetch = fetchStub
      confirmSessionStartPageThunk(dispatchStub, getStateStub)
    }

    it('should hit the correct endpoint', () => {
      stubbedConfirmSessionStartPageCall()
      expect(fetchStub.calledWith('/api/quiz_sessions/42')).to.be.true()
    })

    it('should call callback to handle error', async () => {
      const errorStub = sinon.stub()

      Fetcher.fakeFetch = makeFakeFetchFail({
        response: {status: 403}
      })
      await confirmSessionStartPage(quizSessionId, noop, errorStub)(dispatchStub, getStateStub)

      expect(errorStub.called).to.be.true()
    })

    it('calls a custom callback', async () => {
      const cb = sinon.stub()
      Fetcher.fakeFetch = makeFakeFetch(jsoniblePromise({
        id: '234',
        allow_backtracking: true,
        one_at_a_time_type: false
      }))
      await confirmSessionStartPage(quizSessionId, cb)(dispatchStub, getStateStub)
      expect(cb.called).to.be.true()
    })
  })

  describe('#getResumeData', () => {
    it('makes a request to the endpoint', () => {
      Fetcher.fakeFetch = fetchStub
      const quizSessionId = '11'
      getResumeData(quizSessionId)(dispatchStub)
      expect(fetchStub.calledWith('/api/quiz_sessions/11/session_item_responses/resume_data')).to.be.true()
    })

    it('dispatches the callback', (done) => {
      Fetcher.fakeFetch = fetchStub
      const response = []
      Fetcher.fakeFetch = makeFakeFetch(jsoniblePromise(response))
      getResumeData(quizSessionId)(dispatchStub).then((val) => {
        expect(dispatchStub.called).to.be.true()
        done()
      })
    })
  })

  describe('getTimeRemaining', () => {
    it('makes a request to the endpoint', () => {
      Fetcher.fakeFetch = fetchStub
      const quizSessionId = '11'
      getTimeRemaining(quizSessionId, noop)(dispatchStub, getStateStub)
      expect(fetchStub.calledWith('/api/quiz_sessions/11/time')).to.be.true()
    })

    it('calls the callback with the time', (done) => {
      const cbStub = sinon.stub()
      const response = {timeLeftInSeconds: 33}
      Fetcher.fakeFetch = makeFakeFetch(jsoniblePromise(response))
      getTimeRemaining(quizSessionId, cbStub)(dispatchStub, getStateStub)
        .then((val) => {
          expect(cbStub.calledWith(response))
          done()
        })
    })
  })

  describe('recordQuizTime', () => {
    it('makes a request to the endpoint', () => {
      Fetcher.fakeFetch = fetchStub
      const quizSessionId = '11'
      recordQuizTime(quizSessionId, noop)(dispatchStub, getStateStub)
      expect(fetchStub.calledWith('/api/quiz_sessions/11/record')).to.be.true()
    })

    it('calls the callback with the time', (done) => {
      const cbStub = sinon.stub()
      const response = {timeLeftInSeconds: 33}
      Fetcher.fakeFetch = makeFakeFetch(jsoniblePromise(response))
      recordQuizTime(quizSessionId, cbStub)(dispatchStub, getStateStub)
        .then((val) => {
          expect(cbStub.calledWith(response))
          done()
        })
    })
  })

  describe('newQuizTakingSession', () => {
    it('should hit the correct endpoint', () => {
      Fetcher.fakeFetch = fetchStub
      newQuizTakingSession(quizSessionId)(dispatchStub, getStateStub)
      expect(fetchStub.calledWith('/api/quiz_sessions/42/take')).to.be.true()
    })

    it('should call callback to handle error', async () => {
      const errorStub = sinon.stub()

      Fetcher.fakeFetch = makeFakeFetchFail({
        response: {status: 403}
      })
      await newQuizTakingSession(quizSessionId, noop, errorStub)(dispatchStub, getStateStub)

      expect(errorStub.called).to.be.true()
    })

    it('calls a custom callback', async () => {
      const cb = sinon.stub()
      Fetcher.fakeFetch = makeFakeFetch(jsoniblePromise({
        id: '234',
        allow_backtracking: true,
        one_at_a_time_type: false
      }))
      await newQuizTakingSession(quizSessionId, cb)(dispatchStub, getStateStub)
      expect(cb.called).to.be.true()
    })
  })

  describe('onQuizSessionReadyForTaking', () => {
    const quizSession = {}
    let actions, dispatch

    beforeEach(() => {
      dispatch = sinon.stub()
      onQuizSessionReadyForTaking(quizSession)(dispatch)
      expect(dispatch).to.have.been.calledOnce()
      actions = dispatch.firstCall.args[0]
    })

    it('dispatches a SESSION_STARTED_OR_RESUMED_EVENT action', () => {
      expect(actions.some((action) => action.type === SESSION_STARTED_OR_RESUMED_EVENT)).to.be.true()
    })

    it('dispatches a UPDATE_QUIZ_SESSION_STATUS_ON_PAGE_LOAD action', () => {
      expect(actions.some((action) => action.type === UPDATE_QUIZ_SESSION_STATUS_ON_PAGE_LOAD)).to.be.true()
    })
  })

  describe('submitStudentAccessCode', () => {
    it('should hit the correct endpoint', () => {
      Fetcher.fakeFetch = fetchStub
      submitStudentAccessCode(quizSessionId, 'code')(dispatchStub, getStateStub)
      expect(fetchStub.calledWith('/api/quiz_sessions/42/submit_student_access_code')).to.be.true()
    })

    it('should dispatch actions to handle results', async () => {
      Fetcher.fakeFetch = makeFakeFetch(jsoniblePromise({
        id: '234',
        accepted_student_access_code: false
      }))
      await submitStudentAccessCode(quizSessionId, 'code')(dispatchStub, getStateStub)
      const action = dispatchStub.firstCall.args[0]
      expect(action.type).to.equal(SET_UI)
      expect(action.payload.key).to.equal(STUDENT_ACCESS_CODE_STATUS)
    })
  })

  describe('#makeSessionItemCall', () => {
    const indexGetStub = sinon.stub()
    const showGetStub = sinon.stub()
    const noopDispatch = () => {}

    afterEach(() => {
      indexGetStub.resetHistory()
      showGetStub.resetHistory()
    })

    it('calls getSessionItems if backtracking is allowed', () => {
      const quizSession = new QuizSession({
        allowBacktracking: true,
        oneAtATimeType: 'question'
      })

      makeSessionItemCall(quizSession, indexGetStub, showGetStub)(noopDispatch)
      expect(indexGetStub.called).to.be.true()
      expect(showGetStub.called).to.be.false()
    })

    it('calls getSessionItems if oqaat is off', () => {
      const quizSession = new QuizSession({
        allowBacktracking: false,
        oneAtATimeType: null
      })

      makeSessionItemCall(quizSession, indexGetStub, showGetStub)(noopDispatch)
      expect(indexGetStub.called).to.be.true()
      expect(showGetStub.called).to.be.false()
    })

    it('calls getSingleSessionItem if oqaat is on and backtracking is disabled', () => {
      const quizSession = new QuizSession({
        allowBacktracking: false,
        oneAtATimeType: 'question'
      })

      makeSessionItemCall(quizSession, indexGetStub, showGetStub)(noopDispatch)
      expect(showGetStub.called).to.be.true()
      expect(indexGetStub.called).to.be.false()
    })
  })

  describe('postQuizSession', () => {
    const postQuizSessionThunk = postQuizSession(quizSessionId)

    const stubbedPostQuizSessionCall = () => {
      Fetcher.fakeFetch = fetchStub
      postQuizSessionThunk(dispatchStub, getStateStub)
    }

    it('should hit the right endpoint', () => {
      stubbedPostQuizSessionCall()
      expect(
        fetchStub.calledWith('/api/quiz_sessions/42/submit')
      ).to.be.true()
    })

    const fakePostQuizSession = () => {
      Fetcher.fakeFetch = fakeFetch
      return postQuizSession(quizSessionId)(dispatchStub, getStateStub)
    }

    it('should trigger the quiz submission event', (done) => {
      // testrail test_id:  C2954600
      const submitQuizSessionEventAction = submitQuizSessionEvent()
      fakePostQuizSession().then(() => {
        checkActionDispatched(dispatchStub, submitQuizSessionEventAction)
        done()
      })
    })

    it('should notify the screenreader that the quiz was submitted', (done) => {
      fakePostQuizSession().then(() => {
        checkActionDispatched(dispatchStub, screenreaderNotification('Quiz submitted'), [['payload', 'uuid']])
        done()
      })
    })
  })

  describe('submitQuiz', () => {
    it('should call showSubmitConfirmModal if there are unanswered questions', () => {
      submitQuiz(quizSessionId)(dispatchStub, getStateStub)

      const showSubmitConfirmThunk = dispatchStub.firstCall.args[0]
      expect(showSubmitConfirmThunk).to.be.a('function')

      showSubmitConfirmThunk(dispatchStub, getStateStub)

      const modalAction = dispatchStub.secondCall.args[0]
      expect(modalAction.type).to.equal(OPEN_MODAL)
    })

    it('should call showSubmitConfirmModal if no unanswered questions', () => {
      const answeredState = fromJS(fakeState).set('sessionItems', fromJS({}))
      const getAnsweredStateStub = sinon.stub().returns(answeredState)

      submitQuiz(quizSessionId)(dispatchStub, getAnsweredStateStub)

      const showSubmitConfirmThunk = dispatchStub.firstCall.args[0]
      expect(showSubmitConfirmThunk).to.be.a('function')

      showSubmitConfirmThunk(dispatchStub, getStateStub)

      const modalAction = dispatchStub.secondCall.args[0]
      expect(modalAction.type).to.equal(OPEN_MODAL)
    })

    it('should not call showSubmitConfirmModal when overridden', () => {
      const getAnsweredStateStub = sinon.stub().returns(fromJS({}))

      submitQuiz(quizSessionId, noop, true)(dispatchStub, getAnsweredStateStub)
      const postQuizThunk = dispatchStub.firstCall.args[0]
      expect(postQuizThunk).to.be.a('function')

      Fetcher.fakeFetch = fetchStub
      postQuizThunk(dispatchStub, getAnsweredStateStub)
      expect(fetchStub.calledWith('/api/quiz_sessions/42/submit')).to.be.true()
    })
  })

  describe('showSubmitConfirmModal', () => {
    it('should open modal', () => {
      showSubmitConfirmModal(quizSessionId)(dispatchStub, getStateStub)
      expect(dispatchStub.getCall(0).args[0].type).to.equal(OPEN_MODAL)
    })

    it('should call submitModal with correct message', () => {
      const state = fromJS({
        ...fakeResponseState,
        ...fakeSessionItemsState
      })
      showSubmitConfirmModal(quizSessionId)(dispatchStub, sinon.stub().returns(state))
      const modalContent = mount(dispatchStub.firstCall.args[0].payload.options.text)
      const modalText = modalContent.text()
      const unansweredList = modalContent.find('ul')
      expect(modalText).to.include('The following questions have not been answered:')
      expect(unansweredList).to.contain(<li>Question 1</li>)
      expect(unansweredList).to.contain(<li>Question 10</li>)
      expect(unansweredList).not.to.contain(<li>Question 5</li>)
      expect(modalText).to.include('Are you ready to submit?')
    })
  })

  describe('closeSubmitConfigModal', () => {
    it('should close the modal', () => {
      const state = fromJS({
        ...fakeResponseState,
        ...fakeSessionItemsState
      })
      closeSubmitConfirmModal(quizSessionId)(dispatchStub, sinon.stub().returns(state))
      expect(dispatchStub).to.have.been.calledOnceWith({
        type: CLOSE_MODAL,
        payload: {
          title: 'Unanswered Questions!'
        }
      })
    })
  })

  describe('messageForSubmitModal', () => {
    const renderModalContent = (unanswered) => {
      return mount(<div>{messageForSubmitModal(unanswered)}</div>)
    }

    const assertConfirmMessage = (unanswered) => {
      it('asks if the user is ready to submit', () => {
        const modalContent = renderModalContent(unanswered)
        expect(modalContent.text())
          .to.include('Upon submission you will not be able to change your answers. Are you ready to submit?')
      })
    }

    context('with unanswered questions', () => {
      const unanswered = [1, 2, 3, 10]
      assertConfirmMessage(unanswered)

      it('displays a list of unanswered questions', () => {
        const modalContent = renderModalContent(unanswered)
        const unansweredList = modalContent.find('ul')
        unanswered.forEach(n => expect(unansweredList).to.contain(<li>Question {n}</li>))
      })
    })

    context('with many unanswered questions', () => {
      const unanswered = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
      assertConfirmMessage(unanswered)

      it('warns that multiple questions have not been answered', () => {
        const modalContent = renderModalContent(unanswered)
        expect(modalContent.text())
          .to.include('Multiple questions have not been answered')
      })
    })

    context('with all questions answered', () => {
      assertConfirmMessage([])
    })
  })

  describe('nextQuestion', () => {
    let quizSessionId, allowBacktracking, currentQuestionIsLast,
      itemId, userResponse, largeFormatResponse

    const dispatchStub = sinon.stub()
    const fetchStub = sinon.stub()
    const submitStub = sinon.stub().returns({ type: 'ACTION' })

    beforeEach(() => {
      dispatchStub.resetHistory()
      fetchStub.resetHistory()
      submitStub.resetHistory()

      quizSessionId = '33'
      allowBacktracking = null
      currentQuestionIsLast = null
      itemId = null
      userResponse = null
      largeFormatResponse = null
    })

    const makeCall = (submit = submitStub) => {
      const args = [
        quizSessionId,
        allowBacktracking,
        currentQuestionIsLast,
        itemId,
        userResponse,
        largeFormatResponse,
        submit
      ]

      return nextQuestion(...args)
    }

    context('backtracking is allowed', () => {
      beforeEach(() => {
        allowBacktracking = true
      })

      afterEach(() => {
        Fetcher.fakeFetch = null
      })

      context('when current question is last', () => {
        beforeEach(() => {
          currentQuestionIsLast = true
        })

        it('submits the quiz', () => {
          makeCall()(dispatchStub)
          expect(dispatchStub.called).to.be.true()
          expect(submitStub.called).to.be.true()
        })

        it('does not dispatch if the submit callback does not return anything', () => {
          const submit = sinon.stub().returns(void 0)
          makeCall(submit)(dispatchStub)
          expect(submit).to.have.been.called()
          expect(dispatchStub).not.to.have.been.called()
        })
      })

      context('when current question is not last', () => {
        it('dispatches a backtrackingNextQuestion action', () => {
          currentQuestionIsLast = false
          makeCall()(dispatchStub)
          expect(dispatchStub.calledWith(backtrackingNextQuestion())).to.be.true()
        })
      })
    })

    context('backtracking is not allowed', () => {
      beforeEach(() => {
        allowBacktracking = false
      })

      context('itemId is blank', () => {
        it('makes a post request to the correct endpoint with no data', () => {
          Fetcher.fakeFetch = fetchStub
          makeCall()(dispatchStub)
          const callArgs = fetchStub.firstCall.args
          expect(callArgs[0].match(/33\/session_item_responses\/single_response_submit/)).to.exist()
          expect(callArgs[1].method).to.eq('POST')
          expect(callArgs[1].body).to.be.undefined()
        })

        it('sets the current session item if an item is returned from the endpoint', async () => {
          const item = {position: 5}
          Fetcher.fakeFetch = makeFakeFetch(jsoniblePromise(
            item,
            {status: 200}
          ))

          const action = setCurrentSessionItemPosition(item.position)

          await makeCall()(dispatchStub)
          const args = dispatchStub.firstCall.args
          expect(args[0][0][0].type).to.eql(ADD_SESSION_ITEMS)
          expect(args[0][1]).to.eql(action)
        })
      })

      context('itemId is present', () => {
        beforeEach(() => {
          itemId = '66'
          userResponse = {value: 'hello'}
          largeFormatResponse = {value: 'goodbye'}
        })

        it('makes a post request to the correct endpoint with data', () => {
          Fetcher.fakeFetch = fetchStub
          makeCall()(dispatchStub)
          const callArgs = fetchStub.firstCall.args
          expect(callArgs[0].match(/33\/session_item_responses\/single_response_submit/)).to.exist()
          expect(callArgs[1].method).to.eq('POST')
          expect(callArgs[1].body).to.eq(JSON.stringify({
            sessionItemResponse: {
              itemId: '66',
              userResponse: {value: 'hello'},
              largeFormatResponse: {value: 'goodbye'}
            }
          }))
        })
      })
    })
  })
})
