import { fromJS, List, Map } from 'immutable'

import QuizEntry from '../QuizEntry'
import Bank from '../Bank'
import BankEntry from '../BankEntry'
import Item from '../Item'
import Stimulus from '../Stimulus'
import { setFakeStore, unsetFakeStore } from '../../../reduxStore'
import {
  TEMPORARY_ITEM_ID,
  TEMPORARY_STIMULUS_ID,
  TEMPORARY_QUIZ_ENTRY_ID
} from '@instructure/quiz-common/lib/constants'

describe('QuizEntry Record', () => {
  it('#getProperties returns parsed properties when present', () => {
    const defaultProps = new QuizEntry({stimulusQuizEntryId: 55}).getProperties()
    expect(defaultProps.points_possible).to.eq(1)
    expect(defaultProps.position).to.eq(null)
    expect(defaultProps.stimulus_quiz_entry_id).to.eq(55)
  })

  describe('#getEntry', () => {
    const fakeState = fromJS({
      items: { 1: {} },
      bankEntries: { 1: { entryType: 'Item', entry: { get: () => {} } } },
      modifications: {
        items: { TEMPORARY_ITEM_ID: {} },
        stimuli: { TEMPORARY_STIMULUS_ID: {} }
      },
      stimuli: { 44: {} },
      banks: { 1: {} }
    })

    before(() => {
      setFakeStore(fakeState)
    })

    after(() => {
      unsetFakeStore()
    })

    const sharedExamples = (type) => {
      const quizEntry = new QuizEntry({ entryType: type, entry: Map({ id: '1' }) })

      it('returns correct entry', () => {
        expect(quizEntry.getEntry().constructor.name).to.equal(type)
      })

      it('returns correct entry by entryId', () => {
        expect(quizEntry.merge({ entry: Map(), entryId: '1' }).getEntry().constructor.name).to.equal(type)
      })

      it('sets reference to self on entry', () => {
        expect(quizEntry.getEntry().quizEntry).to.eq(quizEntry)
      })
    }

    context('item', () => {
      const type = 'Item'

      it('returns correct entry', () => {
        const quizEntry = new QuizEntry({entryType: type, entry: TEMPORARY_ITEM_ID})
        expect(quizEntry.getEntry().constructor.name).to.equal('Item')
      })

      sharedExamples(type)
    })

    context('stimulus', () => {
      const type = 'Stimulus'

      it('returns correct entry', () => {
        const quizEntry = new QuizEntry({entryType: type, entry: TEMPORARY_STIMULUS_ID})
        expect(quizEntry.getEntry().constructor.name).to.equal(type)
      })

      sharedExamples(type)
    })

    context('bankEntry', () => {
      const type = 'BankEntry'

      sharedExamples(type)
    })

    context('Bank', () => {
      const type = 'Bank'

      sharedExamples(type)
    })
  })

  describe('#getItem', () => {
    const quizEntryId = '3'
    const itemOverrides = {
      properties: { varyPointsByAnswer: true },
      scoringAlgorithm: 'VaryPointsByAnswer'
    }
    const fakeState = fromJS({
      items: { 1: { id: '1', title: 'Math' } },
      bankEntries: { 2: { id: '2', entryType: 'Item', entry: { id: '1' } } },
      modifications: {
        items: { 1: { title: 'Mather' } },
        quizEntries: { [quizEntryId]: { properties: { itemOverrides } } }
      }
    })

    before(() => {
      setFakeStore(fakeState)
    })

    after(() => {
      unsetFakeStore()
    })

    const sharedExamples = (quizEntryAttributes) => {
      const quizEntry = new QuizEntry({ id: quizEntryId, ...quizEntryAttributes })

      it('returns correct item', () => {
        expect(quizEntry.getItem().title).to.eq('Math')
      })

      it('gets working item from working quiz entry', () => {
        expect(quizEntry.set('isWorkingInstance', true).getItem().title).to.eq('Mather')
      })

      it('merges item overrides', () => {
        expect(quizEntry.set('properties', fromJS({
          itemOverrides: { title: 'Mathest' }
        })).getItem().title).to.eq('Mathest')
      })

      it('merges item overrides from working quiz entry', () => {
        expect(quizEntry.getItem().toJS()).to.deep.include(itemOverrides)
      })
    }

    context('Item', () => {
      sharedExamples({ entryType: 'Item', entry: Map({ id: '1' }) })
    })

    context('BankEntry', () => {
      sharedExamples({ entryType: 'BankEntry', entry: Map({ id: '2' }) })
    })

    context('Stimulus', () => {
      it('returns undefined', () => {
        const quizEntry = new QuizEntry({ entryType: 'Stimulus', entry: TEMPORARY_STIMULUS_ID })
        expect(quizEntry.getItem()).to.be.undefined()
      })
    })

    context('Bank', () => {
      it('returns undefined', () => {
        const quizEntry = new QuizEntry({ entryType: 'Bank' })
        expect(quizEntry.getItem()).to.be.undefined()
      })
    })
  })

  describe('#getNestedEntries', () => {
    const fakeState = fromJS({
      items: { 1: {} },
      bankEntries: {
        1: { entryType: 'Item', entry: { get: () => {} } },
        2: { entryType: 'Stimulus', entry: { get: () => {} } }
      },
      modifications: {
        items: { TEMPORARY_ITEM_ID: {}, 1: {isStimulus: false} },
        stimuli: { TEMPORARY_STIMULUS_ID: {} },
        quizEntries: {
          [TEMPORARY_QUIZ_ENTRY_ID]: {
            id: TEMPORARY_QUIZ_ENTRY_ID,
            stimulusQuizEntryId: '1',
            entryType: 'Item'
          }
        }
      },
      banks: { 1: {} },
      stimuli: { 44: {} },
      quizEntries: {
        1: {
          id: '1',
          entryType: 'Stimulus'
        }
      }
    })

    before(() => {
      setFakeStore(fakeState)
    })

    after(() => {
      unsetFakeStore()
    })

    const sharedExample = (type) => {
      it('returns correct entries', () => {
        const quizEntry = new QuizEntry({
          entryType: type,
          entry: { get: () => '1' }
        })
        expect(quizEntry.getNestedEntries().get(0).constructor.name).to.equal('QuizEntry')
      })
    }

    sharedExample('Item')
    sharedExample('Bank')

    context('BankEntry', () => {
      context('attached to item', () => {
        sharedExample('BankEntry')
      })

      context('attached to stimulus', () => {
        setFakeStore(fakeState)
        const quizEntry = new QuizEntry({
          entryType: 'BankEntry',
          entry: { get: () => '2' }
        })
        expect(quizEntry.getNestedEntries()).to.equal(List())
      })
    })

    context('stimulus', () => {
      it('returns correct entries', () => {
        const quizEntry = new QuizEntry({entryType: 'Stimulus', entry: {get: () => '44'}})
        expect(quizEntry.getNestedEntries()).to.eql(List())
      })

      it('returns correct entries', () => {
        const quizEntry = new QuizEntry({entryType: 'Stimulus', entry: TEMPORARY_STIMULUS_ID})
        expect(quizEntry.getNestedEntries()).to.eql(List())
      })

      it('returns temporary quizEntry when creating a new entry', () => {
        const quizEntry = new QuizEntry(fakeState.getIn(['quizEntries', '1']))
        expect(quizEntry.getNestedEntries().size).to.eq(1)
      })
    })
  })

  describe('#getWorkingInstance', () => {
    const fakeState = fromJS({
      modifications: {
        quizEntries: { 83: {
          pointsPossible: 4,
          properties: {
            itemOverrides: {
              properties: {
                shuffleRules: {
                  choices: {
                    toLock: [3]
                  }
                }
              }
            }
          }
        } }
      }
    })

    before(() => {
      setFakeStore(fakeState)
    })

    after(() => {
      unsetFakeStore()
    })

    it('returns quiz entry from the store', () => {
      const quizEntry = new QuizEntry({ id: '83', pointsPossible: 2 })
      const workingQuizEntry = quizEntry.getWorkingInstance()
      expect(workingQuizEntry.pointsPossible).to.eq(4)
    })

    it('sets isWorkingInstance', () => {
      const quizEntry = new QuizEntry({ id: '83', pointsPossible: 2 })
      const workingQuizEntry = quizEntry.getWorkingInstance()
      expect(workingQuizEntry.isWorkingInstance).to.be.true()
    })

    it('deep merges modifications to object attributes', () => {
      const quizEntry = new QuizEntry(fromJS({
        id: '83',
        properties: {
          itemOverrides: {
            properties: {
              shuffleRules: {
                choices: {
                  shuffled: true,
                  toLock: [0, 3]
                }
              }
            }
          }
        }
      }))
      const workingQuizEntry = quizEntry.getWorkingInstance()
      const shuffleRules = workingQuizEntry.properties.toJS().itemOverrides.properties.shuffleRules
      expect(shuffleRules.choices.shuffled).to.eq(true)
      expect(shuffleRules.choices.toLock).to.eql([3])
    })
  })

  describe('#hasModifications', () => {
    it('returns false if it has no modifications', () => {
      const quizEntry = new QuizEntry()
      const unmodifiedQuizEntry = quizEntry.merge({})
      sinon.stub(quizEntry, 'getWorkingInstance').returns(unmodifiedQuizEntry)
      expect(quizEntry.hasModifications()).to.be.false()
    })

    it('returns true if modifications exist', () => {
      const quizEntry = new QuizEntry()
      const modifiedQuizEntry = quizEntry.merge({ pointsPossible: '99' })
      sinon.stub(quizEntry, 'getWorkingInstance').returns(modifiedQuizEntry)
      expect(quizEntry.hasModifications()).to.be.true()
    })

    it('ignores the isWorkingInstance attribute', () => {
      const quizEntry = new QuizEntry()
      const unmodifiedQuizEntry = quizEntry.merge({ isWorkingInstance: true })
      sinon.stub(quizEntry, 'getWorkingInstance').returns(unmodifiedQuizEntry)
      expect(quizEntry.hasModifications()).to.be.false()
    })
  })

  describe('getters', () => {
    const createEntry = (entryType, attrs) => {
      const bankEntry = new BankEntry()
      sinon.stub(bankEntry, 'getEntry').returns(new Item())
      return {
        Bank: new Bank(attrs),
        BankEntry: bankEntry,
        Item: new Item(attrs),
        Stimulus: new Stimulus(attrs)
      }[entryType]
    }

    const stubGetEntry = (qe, entryType, attrs) => {
      sinon.stub(qe, 'getEntry').returns(createEntry(entryType, attrs))
    }

    const sharedExamples = (entryType, attrs) => {
      const qe = new QuizEntry({entryType})
      stubGetEntry(qe, entryType, attrs)

      it('returns the correct type for isBank', () => {
        expect(qe.isBank).to.eq(entryType === 'Bank')
      })

      it('returns the correct type for isBankEntry', () => {
        expect(qe.isBankEntry).to.eq(entryType === 'BankEntry')
      })

      it('returns the correct type for isStimulus', () => {
        expect(qe.isStimulus).to.eq(entryType === 'Stimulus')
      })

      it('returns the correct type for isItem', () => {
        expect(qe.isItem).to.eq(entryType === 'Item')
      })

      it('returns the correct value for isInStimulus', () => {
        expect(qe.isInStimulus).to.eq(false)
      })

      it('returns the correct type for type', () => {
        expect(qe.type).to.eq(entryType)
      })

      it('returns the correct value for hasNestedRecords', () => {
        expect(qe.hasNestedRecords).to.eq(entryType === 'Stimulus')
      })

      it('returns the correct value for hasStimulus', () => {
        expect(qe.hasStimulus).to.eq(entryType === 'Stimulus')
      })

      it('returns the correct value for isArchivedBank', () => {
        expect(qe.isArchivedBank).to.eq(entryType === 'Bank' && qe.getEntry().archived)
      })
    }

    sharedExamples('Bank')
    sharedExamples('Bank', { archived: true })
    sharedExamples('Stimulus')
    sharedExamples('Item')
    sharedExamples('BankEntry')

    describe('pointsPossible', () => {
      it('returns own pointsPossible when interactionPoints is undefined', () => {
        const qe = new QuizEntry({ pointsPossible: 4 })
        const item = new Item()
        sinon.stub(qe, 'getItem').returns(item)
        expect(qe.pointsPossible).to.equal(4)
      })

      it('returns interactionPoints when given', () => {
        const qe = new QuizEntry({ pointsPossible: 4 })
        const item = new Item({
          properties: { varyPointsByAnswer: true },
          scoringData: {
            values: [{ points: 3 }, { points: 5 }]
          }
        })
        sinon.stub(qe, 'getItem').returns(item)
        expect(qe.pointsPossible).to.equal(5)
      })

      it('returns 0 when iteractionPoints is 0', () => {
        const qe = new QuizEntry({ pointsPossible: 4 })
        const item = new Item({
          properties: { varyPointsByAnswer: true },
          scoringData: {
            values: [{ points: 0 }, { points: 0 }]
          }
        })
        sinon.stub(qe, 'getItem').returns(item)
        expect(qe.pointsPossible).to.equal(0)
      })

      it('returns 0 for Stimulus entries', () => {
        const qe = new QuizEntry({ entryType: 'Stimulus', pointsPossible: 4 })
        const item = new Item()
        sinon.stub(qe, 'getItem').returns(item)
        expect(qe.pointsPossible).to.eq(0)
      })
    })

    describe('positionSlots', () => {
      context('stimulus type', () => {
        it('returns the number of items in the stimulus', () => {
          const qe = new QuizEntry({entryType: 'Stimulus'})
          sinon.stub(qe, 'getNestedEntries').returns(List([new QuizEntry(), new QuizEntry(), new QuizEntry()]))
          expect(qe.positionSlots).to.eq(3)
        })
      })

      const sharedExample = (entryType) => {
        it(`returns 1 for ${entryType}`, () => {
          const qe = new QuizEntry({entryType})
          expect(qe.positionSlots).to.eq(1)
        })
      }

      sharedExample('Bank')
      sharedExample('Item')
    })

    describe('displayPositionSlots', () => {
      context('for banks', () => {
        it('returns the itemEntryCount', () => {
          const qe = new QuizEntry({entryType: 'Bank'})
          sinon.stub(qe, 'getEntry').returns({itemEntryCount: 5})
          expect(qe.displayPositionSlots).to.eq(5)
        })
      })

      context('for stimulus', () => {
        it('returns the entryCount', () => {
          const qe = new QuizEntry({entryType: 'Stimulus'})
          sinon.stub(qe, 'getNestedEntries').returns(List([new QuizEntry(), new QuizEntry(), new QuizEntry()]))
          expect(qe.displayPositionSlots).to.eq(3)
        })

        it('honors the nested banks count', () => {
          const qe = new QuizEntry({entryType: 'Stimulus'})
          const bank = new QuizEntry({entryType: 'Bank'})
          const bankRecord = new Bank({itemEntryCount: 3})
          sinon.stub(bank, 'getEntry').returns(bankRecord)
          sinon.stub(qe, 'getNestedEntries').returns(List([bank, new QuizEntry(), new QuizEntry()]))
          expect(qe.displayPositionSlots).to.eq(5)
        })
      })

      context('for banked stimulus', () => {
        it('returns the entryCount', () => {
          const qe = new QuizEntry({
            entryType: 'BankEntry'
          })
          const be = new BankEntry()
          sinon.stub(be, 'getEntry').returns(new Stimulus())
          sinon.stub(qe, 'getEntry').returns(be)
          sinon.stub(qe, 'getNestedEntries').returns(List([new QuizEntry(), new QuizEntry(), new QuizEntry()]))
          expect(qe.displayPositionSlots).to.eq(3)
        })
      })

      const sharedExample = (entryType) => {
        it(`returns 1 for ${entryType}`, () => {
          const qe = new QuizEntry({entryType})
          expect(qe.displayPositionSlots).to.eq(1)
        })
      }

      sharedExample('Item')
    })
  })

  describe('#positionToDisplay', () => {
    context('for banks', () => {
      it('returns a #-# string', () => {
        const qe = new QuizEntry({ entryType: 'Bank' })
        sinon.stub(qe, 'getEntry').returns({ itemEntryCount: 5 })
        expect(qe.positionToDisplay(5)).to.eq('5 - 9')
      })

      it('honors sampleNum', () => {
        const qe = new QuizEntry({ entryType: 'Bank', properties: Map({ sampleNum: '3' }) })
        sinon.stub(qe, 'getEntry').returns({ itemEntryCount: 5 })
        expect(qe.positionToDisplay(5)).to.eq('5 - 7')
      })

      it('does not return a range if sample num is 1', () => {
        const qe = new QuizEntry({ entryType: 'Bank', properties: Map({ sampleNum: '1' }) })
        sinon.stub(qe, 'getEntry').returns({ itemEntryCount: 5 })
        expect(qe.positionToDisplay(5)).to.eq('5')
      })

      it('does not return a range if bankSize is 1', () => {
        const qe = new QuizEntry({ entryType: 'Bank' })
        sinon.stub(qe, 'getEntry').returns({ itemEntryCount: 1 })
        expect(qe.positionToDisplay(5)).to.eq('5')
      })
    })

    const sharedExample = (entryType) => {
      it(`returns the number given for ${entryType}`, () => {
        const qe = new QuizEntry({entryType})
        expect(qe.positionToDisplay(5)).to.eq('5')
      })
    }

    sharedExample('Stimulus')
    sharedExample('Item')
  })

  describe('#getErrors', () => {
    const createQuizEntry = (override = {}) => {
      const qe = new QuizEntry(Object.assign({
        entryType: 'Bank',
        pointsPossible: 1,
        properties: Map({sampleNum: '', selection: 'rand'})
      }, override))
      sinon.stub(qe, 'getEntry').returns({itemEntryCount: 5})
      return qe
    }
    it('renders two error messages when both inputs are blank', () => {
      const qe = createQuizEntry({pointsPossible: ''})
      expect(qe.isValid()).to.eq(false)
      const errors = qe.getErrors()
      const pointsMessages = errors.get('pointsPossible')
      const sampleMessages = errors.get('sampleNum')
      const deletedBankMessages = errors.get('deletedBank')
      expect(deletedBankMessages.size).to.eq(0)
      expect(pointsMessages.size).to.eq(1)
      expect(pointsMessages.first().get('text')).to.eq('Must be a number')
      expect(sampleMessages.size).to.eq(1)
      expect(sampleMessages.first().get('text')).to.eq('Must be a whole number')
    })

    it('renders one error message when only points possible is blank', () => {
      const qe = createQuizEntry({pointsPossible: '', properties: Map({selection: 'all'})})
      expect(qe.isValid()).to.eq(false)
      const errors = qe.getErrors()
      const pointsMessages = errors.get('pointsPossible')
      const sampleMessages = errors.get('sampleNum')
      const deletedBankMessages = errors.get('deletedBank')
      expect(deletedBankMessages.size).to.eq(0)
      expect(pointsMessages.size).to.eq(1)
      expect(sampleMessages.size).to.eq(0)
      expect(pointsMessages.first().get('text')).to.eq('Must be a number')
    })

    it('renders one error message when sampleNum is NaN', () => {
      const qe = createQuizEntry({properties: Map({selection: 'rand', sampleNum: 'NaN'})})
      expect(qe.isValid()).to.eq(false)
      const errors = qe.getErrors()
      const pointsMessages = errors.get('pointsPossible')
      const sampleMessages = errors.get('sampleNum')
      const deletedBankMessages = errors.get('deletedBank')
      expect(deletedBankMessages.size).to.eq(0)
      expect(pointsMessages.size).to.eq(0)
      expect(sampleMessages.size).to.eq(1)
      expect(sampleMessages.first().get('text')).to.eq('Must be a whole number')
    })

    it('renders one error message when sampleNum is not an integer', () => {
      const qe = createQuizEntry({properties: Map({selection: 'rand', sampleNum: '1.1'})})
      expect(qe.isValid()).to.eq(false)
      const errors = qe.getErrors()
      const pointsMessages = errors.get('pointsPossible')
      const sampleMessages = errors.get('sampleNum')
      const deletedBankMessages = errors.get('deletedBank')
      expect(deletedBankMessages.size).to.eq(0)
      expect(pointsMessages.size).to.eq(0)
      expect(sampleMessages.size).to.eq(1)
      expect(sampleMessages.first().get('text')).to.eq('Must be a whole number')
    })

    it('renders one error message when sampleNum is <= 0', () => {
      const qe = createQuizEntry({properties: Map({selection: 'rand', sampleNum: '0'})})
      expect(qe.isValid()).to.eq(false)
      const errors = qe.getErrors()
      const pointsMessages = errors.get('pointsPossible')
      const sampleMessages = errors.get('sampleNum')
      const deletedBankMessages = errors.get('deletedBank')
      expect(deletedBankMessages.size).to.eq(0)
      expect(pointsMessages.size).to.eq(0)
      expect(sampleMessages.size).to.eq(1)
      expect(sampleMessages.first().get('text')).to.eq('Must be at least 1')
    })

    it('renders one error message when sampleNum is > bank entry count', () => {
      const qe = createQuizEntry({properties: Map({selection: 'rand', sampleNum: '6'})})
      expect(qe.isValid()).to.eq(false)
      const errors = qe.getErrors()
      const pointsMessages = errors.get('pointsPossible')
      const sampleMessages = errors.get('sampleNum')
      const deletedBankMessages = errors.get('deletedBank')
      expect(deletedBankMessages.size).to.eq(0)
      expect(pointsMessages.size).to.eq(0)
      expect(sampleMessages.size).to.eq(1)
      expect(sampleMessages.first().get('text')).to.eq('Max number exceeded (this bank contains 5 questions)')
    })

    it('renders one error message when entry is archived bank', () => {
      const qe = new QuizEntry({ entryType: 'Bank' })
      sinon.stub(qe, 'getEntry').returns(new Bank({ archived: true }))
      expect(qe.isValid()).to.be.false()
      const errors = qe.getErrors()
      const deletedBankMessages = errors.get('deletedBank')
      const pointsMessages = errors.get('pointsPossible')
      const sampleMessages = errors.get('sampleNum')
      expect(deletedBankMessages.size).to.eq(1)
      expect(pointsMessages.size).to.eq(0)
      expect(sampleMessages.size).to.eq(0)
      expect(deletedBankMessages.first().get('text')).to.eq('This item bank no longer exists. Please select a valid bank.')
    })
  })
})
