import { createElement } from 'react'
import ReactTestUtils from 'react-dom/test-utils'
import { List, fromJS, Map } from 'immutable'
import Button from '@instructure/ui-buttons/lib/components/Button'

import { wrapInDragAndDropTestContext } from '../../../../../../../tests/util/dndTestUtils'

import mouseEventOn from '../../../../../../../tests/util/mouseEventOn'

import AddToBankModal from '../../../../../../banks/components/AddToBankModal'
import ConnectedQuizEntryEdit from '../index'
import QuizEntryEdit from '../presenter'
import QuizEntryEditFooter from '../Footer'
import QuizEntryEditHeader from '../Header'

import Item from '../../../../../../common/records/Item'
import QuizEntry from '../../../../../../common/records/QuizEntry'
import Bank from '../../../../../../common/records/Bank'
import { QuizEntryBankEntry } from '../../../../../../../tests/factories/QuizEntry'

import {
  SHOULD_CLONE_ITEM,
  TEMPORARY_ITEM_ID,
  TEMPORARY_QUIZ_ENTRY_ID,
  DELETED_QUIZ_ENTRY
} from '@instructure/quiz-common/lib/constants'

describe('QuizEntryEdit', () => {
  const sandbox = sinon.createSandbox()
  const QuizEntryEditWithContext = withContext(wrapInDragAndDropTestContext(QuizEntryEdit))

  const changeItemTitleStub = sandbox.stub()
  const changeQuizEntryEntryStub = sandbox.stub()
  const changeQuizEntryPointsStub = sandbox.stub()
  const clearModificationsStub = sandbox.stub()
  const clearNextQuizEntryStub = sandbox.stub()
  const clearTemporaryItemStub = sandbox.stub()
  const cloneQuizEntryStub = sandbox.stub()
  const createItemStub = sandbox.stub()
  const createTempItemStub = sandbox.stub()
  const createTempStimulusStub = sandbox.stub()
  const createQuizEntryStub = sandbox.stub()
  const hideErrorStub = sandbox.stub()
  const moveQuizEntryStub = sandbox.stub()
  const moveTemporaryQuizEntryStub = sandbox.stub()
  const openModalStub = sandbox.stub()
  const openRegradeModalStub = sandbox.stub()
  const removeQuizEntryStub = sandbox.stub()
  const screenreaderNotificationStub = sandbox.stub()
  const setUiStub = sandbox.stub()
  const showErrorStub = sandbox.stub()
  const switchOffEditingStub = sandbox.stub()
  const updateItemStub = sandbox.stub()
  const updateQuizEntryStub = sandbox.stub()

  const workingQuizEntry = new QuizEntry({
    id: '55',
    pointsPossible: 3999
  })
  workingQuizEntry.isValid = () => true

  const quizEntry = new QuizEntry({
    id: '55',
    entryType: 'Item',
    entryId: 66,
    pointsPossible: 12,
    position: 3
  })

  const quizEntry1 = new QuizEntry({ position: 1 })
  const quizEntry2 = new QuizEntry({ position: 2 })
  const quizEntry4 = new QuizEntry({ position: 4 })
  const quizEntry5 = new QuizEntry({ position: 5 })
  const quizEntries = List([quizEntry1, quizEntry2, quizEntry, quizEntry4, quizEntry5])

  const workingItem = new Item({
    id: '1',
    properties: {
      varyPointsByAnswer: false
    }
  })
  workingItem.isValid = () => true
  workingItem.getErrors = () => fromJS({})
  workingItem.errorsFor = () => fromJS({})

  const item = new Item({ id: '66' })

  const defaultProps = {
    addContentPopoverOpen: false,
    changeItemTitle: changeItemTitleStub,
    changeQuizEntryEntry: changeQuizEntryEntryStub,
    changeQuizEntryPoints: changeQuizEntryPointsStub,
    clearModifications: clearModificationsStub,
    clearNextQuizEntry: clearNextQuizEntryStub,
    clearTemporaryItem: clearTemporaryItemStub,
    cloneQuizEntry: cloneQuizEntryStub,
    createItem: createItemStub,
    createTempItem: createTempItemStub,
    createQuizEntry: createQuizEntryStub,
    createTempStimulus: createTempStimulusStub,
    draggingAboveHalf: false,
    draggingOver: false,
    hideError: hideErrorStub,
    moveQuizEntry: moveQuizEntryStub,
    moveTemporaryQuizEntry: moveTemporaryQuizEntryStub,
    openModal: openModalStub,
    openRegradeModal: openRegradeModalStub,
    removeQuizEntry: removeQuizEntryStub,
    screenreaderNotification: screenreaderNotificationStub,
    setUi: setUiStub,
    showError: showErrorStub,
    switchOffEditing: switchOffEditingStub,
    updateItem: updateItemStub,
    updateQuizEntry: updateQuizEntryStub,
    workingEntry: workingItem,
    workingQuizEntry,
    disabledForRegrade: false,
    displayPosition: 3,
    entry: item,
    guid: 'guid123',
    isOccluded: false,
    moveToPosition: () => {},
    name: 'This Will Not Always Be An Interaction Type Name So Please Do Not Assume It Is',
    quizEntries: quizEntries,
    quizEntry,
    quizId: '99',
    withConfirm: () => {},
    appContainer: '.root',
    onEditQuizEntryToggle: () => { }
  }

  const testbed = createTestbed(QuizEntryEditWithContext, defaultProps)

  sandbox.stub(item, 'getWorkingInstance')
  sandbox.stub(item, 'hasModifications')
  sandbox.stub(quizEntry, 'getWorkingInstance')
  sandbox.stub(workingQuizEntry, 'getEntry')
  sandbox.stub(workingQuizEntry, 'getItem')

  beforeEach(() => {
    createItemStub.resolves({ id: '2' })
    createQuizEntryStub.resolves({ id: '56' })
    updateItemStub.resolvesArg(0)
    updateQuizEntryStub.resolves()

    item.getWorkingInstance.returns(workingItem)
    item.hasModifications.returns(true)
    quizEntry.getWorkingInstance.returns(workingQuizEntry)
    workingQuizEntry.getEntry.returns(workingItem)
    workingQuizEntry.getItem.returns(workingItem)
  })

  afterEach(() => {
    sandbox.reset()
  })

  shouldRenderComponent(
    testbed, {}, QuizEntryEditWithContext
  )

  it('renders its child', () => {
    const childDiv = createElement('div', { className: 'childClassname' }, 'child element')
    testbed.render({ children: childDiv })
    const childComponent = testbed.findChildrenByClass('childClassname')[0]
    expect(childComponent).to.not.be.undefined()
  })

  it('renders the Footer', () => {
    testbed.render()
    expect(testbed.findChildByType(QuizEntryEditFooter)).to.exist()
  })

  context('bank entry', () => {
    it('renders the Footer', () => {
      const { quizEntry, entry } = QuizEntryBankEntry()
      testbed.render({ quizEntry, entry })
      expect(testbed.findChildByType(QuizEntryEditFooter)).to.exist()
    })
  })

  context('bank item footers', () => {
    it('does not render a footer', () => {
      const bankQuizEntry = new QuizEntry({
        id: '55',
        entryType: 'Bank',
        entryId: 66,
        position: 3
      })
      const bankEntry = new Bank()
      sandbox.stub(bankQuizEntry, 'getEntry').returns(bankEntry)
      bankQuizEntry.getWorkingInstance = () => bankQuizEntry
      bankEntry.getWorkingInstance = () => bankEntry

      testbed.render({
        quizEntry: bankQuizEntry
      })
      expect(testbed.findChildrenByType(QuizEntryEditFooter)).to.be.empty()
    })

    it('does render a footer if "regrading"', () => {
      const bankQuizEntry = new QuizEntry({
        id: '55',
        entryType: 'Bank',
        entryId: 66,
        position: 3
      })
      const bankEntry = new Bank()
      sandbox.stub(bankQuizEntry, 'getEntry').returns(bankEntry)
      bankQuizEntry.getWorkingInstance = () => bankQuizEntry
      bankEntry.getWorkingInstance = () => bankEntry

      testbed.render({
        disabledForRegrade: true,
        quizEntry: bankQuizEntry
      })
      expect(testbed.findChildByType(QuizEntryEditFooter)).to.exist()
    })
  })

  it('renders the Header', () => {
    testbed.render()
    expect(testbed.findChildByType(QuizEntryEditHeader)).to.exist()
  })

  describe('adding to item banks', () => {
    it('renders AddToBankModal', () => {
      testbed.render()
      expect(testbed.findChildByType(AddToBankModal)).to.exist()
    })

    it('disables submit on continue if saving a new item', () => {
      testbed.render({
        workingQuizEntry: new QuizEntry({ entryType: 'Item', id: TEMPORARY_QUIZ_ENTRY_ID })
      })
      testbed.findChildByType(AddToBankModal).props.onContinue()
      expect(testbed.findChildByType(QuizEntryEditFooter).props.disableSubmit).to.be.true()
    })

    it('does not disable submit on continue if saving an existing item', () => {
      testbed.render()
      testbed.findChildByType(AddToBankModal).props.onContinue()
      expect(testbed.findChildByType(QuizEntryEditFooter).props.disableSubmit).to.be.false()
    })

    it('creates the entry for the bank if it does not exist', () => {
      testbed.render({
        entry: new Item({ id: TEMPORARY_ITEM_ID })
      })
      testbed.findChildByType(AddToBankModal).props.getEntry()
      expect(updateItemStub).not.to.have.been.called()
      expect(createItemStub).to.have.been.called()
    })

    it('creates a copy of the entry if it has modifications and shouldClone is true', () => {
      testbed.render({ shouldClone: true })
      testbed.findChildByType(AddToBankModal).props.getEntry()
      expect(updateItemStub).not.to.have.been.called()
      expect(createItemStub).to.have.been.called()
    })

    it('updates the entry and returns it for the bank if it exists', () => {
      testbed.render()
      testbed.findChildByType(AddToBankModal).props.getEntry()
      expect(createItemStub).not.to.have.been.called()
      expect(updateItemStub).to.have.been.called()
    })

    it('updates the quiz entry with freshly banked content', () => {
      const bankEntry = { id: '11' }
      testbed.render()
      testbed.findChildByType(AddToBankModal).props.onAdd(bankEntry)
      expect(updateQuizEntryStub).to.have.been
        .calledWith('99', '55', { entryId: '11', entryType: 'BankEntry' })
    })

    it('clones the quiz entry if it was immutable', () => {
      testbed.render({ shouldClone: true })
      testbed.findChildByType(AddToBankModal).props.onAdd({ id: '11' })
      expect(cloneQuizEntryStub).to.have.been
        .calledWith('99', '55', sinon.match({
          entry: {},
          entryEditable: true,
          entryId: '11',
          entryType: 'BankEntry',
          id: '55',
          pointsPossible: 3999,
          position: null,
          properties: {},
          regradeAdjustedPointsPossible: null,
          status: 'mutable',
          stimulusQuizEntryId: null
        }))
    })

    it('creates the temporary quiz entry with banked content', () => {
      const bankEntry = { id: '11' }
      testbed.render({
        workingQuizEntry: new QuizEntry({ entryType: 'Item', id: TEMPORARY_QUIZ_ENTRY_ID })
      })
      testbed.findChildByType(AddToBankModal).props.onAdd(bankEntry)
      expect(createQuizEntryStub).to.have.been
        .calledWith('99', '11', 'BankEntry', sinon.match({ points_possible: 1 }))
    })

    it('clears modifications, temporary item for temporary quiz entry with banked content', () => {
      const bankEntry = { id: '11' }
      testbed.render({
        workingQuizEntry: new QuizEntry({ entryType: 'Item', id: TEMPORARY_QUIZ_ENTRY_ID })
      })
      testbed.findChildByType(AddToBankModal).props.onAdd(bankEntry)
      expect(clearModificationsStub).to.have.been.calledWith('items', item.id)
      expect(clearTemporaryItemStub).to.have.been.calledWith(defaultProps.quizId)
      expect(switchOffEditingStub).to.have.been.called()
    })
  })

  context('unmounting', () => {
    it('sets the UI', () => {
      testbed.render()
      testbed.unmount()
      expect(setUiStub).to.have.been.calledWith(SHOULD_CLONE_ITEM, false)
    })
  })

  context('cancelling', () => {
    const cancel = (props = {}) => {
      testbed.render(props)
      const button = testbed.findChildrenByTagName('button').find((b) => b.innerText.includes('Cancel'))
      ReactTestUtils.Simulate.click(button)
    }

    it('clears modifications', () => {
      cancel()
      expect(clearModificationsStub).to.have.been.calledWith('items', item.id)
      expect(clearModificationsStub).to.have.been.calledWith('quizEntries', quizEntry.id)
    })

    it('sets the item as deleted if the quiz entry is temporary', () => {
      cancel({ quizEntry: new QuizEntry({ id: TEMPORARY_QUIZ_ENTRY_ID, entry: TEMPORARY_ITEM_ID, entryType: 'Item' }) })
      expect(setUiStub).to.have.been.calledWith(DELETED_QUIZ_ENTRY, { position: 0, inStimulus: false })
    })

    it('clears the temporary item if the quiz entry is temporary', () => {
      cancel({ quizEntry: new QuizEntry({ id: TEMPORARY_QUIZ_ENTRY_ID, entry: TEMPORARY_ITEM_ID, entryType: 'Item' }) })
      expect(clearTemporaryItemStub).to.have.been.calledWith(defaultProps.quizId)
    })

    it('does not clear the temporary item if the item is not temporary', () => {
      cancel()
      expect(clearTemporaryItemStub).to.have.not.been.called()
    })
  })

  context('submitting', () => {
    const shared = (submit) => {
      it('updates existing quiz entry and item', (done) => {
        submit()
        setTimeout(() => {
          expect(updateItemStub).to.have.been.calledWith(workingItem, defaultProps.quizId)
          expect(updateQuizEntryStub).to.have.been.calledWith(
            defaultProps.quizId,
            workingQuizEntry.id,
            sinon.match.any
          )
          expect(clearModificationsStub).to.have.been.called()
          done()
        })
      })

      it('creates temporary quiz entry and item', (done) => {
        const tempItem = item.set('id', TEMPORARY_ITEM_ID)
        const tempEntry = workingQuizEntry.set('id', TEMPORARY_QUIZ_ENTRY_ID)
        sandbox.stub(tempEntry, 'getEntry').returns(tempItem)
        sandbox.stub(tempEntry, 'getItem').returns(tempItem)
        tempEntry.isValid = () => true
        quizEntry.getWorkingInstance.returns(tempEntry)
        submit({ workingQuizEntry: tempEntry, entry: tempItem })
        setTimeout(() => {
          expect(createItemStub).to.have.been.called()
          expect(createQuizEntryStub).to.have.been.called()
          expect(clearTemporaryItemStub).to.have.been.called()
          done()
        })
      })

      it('creates the next quiz entry when needed', (done) => {
        const tempItem = item.set('id', TEMPORARY_ITEM_ID)
        const tempEntry = workingQuizEntry.set('id', TEMPORARY_QUIZ_ENTRY_ID)
        sandbox.stub(tempEntry, 'getEntry').returns(tempItem)
        sandbox.stub(tempEntry, 'getItem').returns(tempItem)
        tempEntry.isValid = () => true
        quizEntry.getWorkingInstance.returns(tempEntry)
        submit({ entry: tempItem, nextQuizEntry: Map() })
        setTimeout(() => {
          expect(createTempStimulusStub).to.have.been.called()
          expect(clearNextQuizEntryStub).to.have.been.called()
          done()
        })
      })

      it('creates temporary quiz entry for existing item', (done) => {
        const tempEntry = workingQuizEntry.set('id', TEMPORARY_QUIZ_ENTRY_ID)
        tempEntry.isValid = () => true
        sandbox.stub(tempEntry, 'getEntry').returns(item)
        sandbox.stub(tempEntry, 'getItem').returns(item)
        quizEntry.getWorkingInstance.returns(tempEntry)
        submit({workingQuizEntry: tempEntry})
        setTimeout(() => {
          expect(createQuizEntryStub).to.have.been.called()
          expect(clearModificationsStub).to.have.been.called()
          done()
        })
      })

      context('when updating item fails', () => {
        it('re-enables button', (done) => {
          submit({ updateItem: sandbox.stub().rejects() })
          setTimeout(() => {
            const button = testbed.findChildrenByTagName('button').find((b) => b.innerText.includes('Done'))
            expect(button.disabled).to.be.false()
            done()
          })
        })
      })

      context('when creating item fails', () => {
        it('re-enables button', (done) => {
          const tempItem = item.set('id', TEMPORARY_ITEM_ID)
          const tempEntry = workingQuizEntry.set('id', TEMPORARY_QUIZ_ENTRY_ID)
          tempEntry.getEntry = () => tempItem
          tempEntry.getItem = () => tempItem
          tempEntry.isValid = () => true
          quizEntry.getWorkingInstance.returns(tempEntry)
          submit({
            createItem: sandbox.stub().rejects(),
            workingQuizEntry: tempEntry,
            entry: tempItem
          })
          setTimeout(() => {
            const button = testbed.findChildrenByTagName('button').find((b) => b.innerText.includes('Done'))
            expect(button.disabled).to.be.false()
            done()
          })
        })
      })

      context('when updating quiz entry fails', () => {
        it('re-enables button', (done) => {
          submit({ updateQuizEntry: sandbox.stub().rejects() })
          setTimeout(() => {
            const button = testbed.findChildrenByTagName('button').find((b) => b.innerText.includes('Done'))
            expect(button.disabled).to.be.false()
            done()
          })
        })
      })

      context('when creating quiz entry fails', () => {
        it('re-enables button', (done) => {
          const tempItem = item.set('id', TEMPORARY_ITEM_ID)
          const tempEntry = workingQuizEntry.set('id', TEMPORARY_QUIZ_ENTRY_ID)
          tempEntry.getEntry = () => tempItem
          tempEntry.getItem = () => tempItem
          tempEntry.isValid = () => true
          quizEntry.getWorkingInstance.returns(tempEntry)
          submit({
            createQuizEntry: sandbox.stub().rejects(),
            workingQuizEntry: tempEntry,
            entry: tempItem
          })
          setTimeout(() => {
            const button = testbed.findChildrenByTagName('button').find((b) => b.innerText.includes('Done'))
            expect(button.disabled).to.be.false()
            done()
          })
        })
      })

      context('should clone', () => {
        it('submits the item if it is temporary', (done) => {
          submit({ shouldClone: true })
          setTimeout(() => {
            expect(createItemStub).to.have.been.calledWith(workingItem, '99', 'quiz', workingItem.id)
            expect(cloneQuizEntryStub).to.have.been.calledWith(
              defaultProps.quizId,
              workingQuizEntry.id,
              sinon.match({ entryId: '2', pointsPossible: 3999, id: '55' })
            )
            done()
          })
        })

        context('when cloning fails', () => {
          it('re-enables button', (done) => {
            submit({
              shouldClone: true,
              cloneQuizEntry: sandbox.stub().rejects()
            })
            setTimeout(() => {
              const button = testbed.findChildrenByTagName('button').find((b) => b.innerText.includes('Done'))
              expect(button.disabled).to.be.false()
              done()
            })
          })
        })
      })
    }

    const submitViaDoneButton = (props = {}) => {
      testbed.render(props)
      const button = testbed.findChildrenByTagName('button').find((b) => b.innerText.includes('Done'))
      ReactTestUtils.Simulate.click(button)
    }

    const submitViaOutsideClick = (props = {}) => {
      testbed.render(props)
      const outerElement = document.createElement('div')
      outerElement.className = 'root'
      document.body.appendChild(outerElement)
      mouseEventOn(outerElement, 'click')
    }

    context('via done button', () => {
      shared(submitViaDoneButton)
    })

    context('via outside click', () => {
      shared(submitViaOutsideClick)
    })

    it('calls the right things on invalid submit', (done) => {
      const tempItem = item.set('id', TEMPORARY_ITEM_ID)
      const tempEntry = workingQuizEntry.set('id', TEMPORARY_QUIZ_ENTRY_ID)
      sandbox.stub(tempEntry, 'getEntry').returns(tempItem)
      sandbox.stub(tempEntry, 'getItem').returns(tempItem)
      tempEntry.isValid = () => false
      quizEntry.getWorkingInstance.returns(tempEntry)
      submitViaOutsideClick({ workingQuizEntry: tempEntry, entry: tempItem })
      setTimeout(() => {
        expect(clearNextQuizEntryStub).to.have.been.called()
        expect(showErrorStub).to.have.been.called()
        expect(screenreaderNotificationStub).to.have.been.called()
        done()
      })
    })

    it('disables the Done button when submitting via outside click', (done) => {
      submitViaOutsideClick()
      const doneButton = testbed.findChildrenByType(Button).find((b) => testbed.getText(b).includes('Done'))
      setTimeout(() => {
        expect(doneButton.props.disabled).to.be.true()
        done()
      })
    })
  })

  describe('click off', () => {
    const clickOffExamples = () => {
      const makeFalseApp = () => {
        return document.createElement('div')
      }

      const elementInApp = () => {
        const outerElement = document.createElement('div')
        outerElement.className = 'root'
        const falseApp = makeFalseApp()
        falseApp.appendChild(outerElement)
        document.body.appendChild(falseApp)
        return outerElement
      }

      it('calls submitHandler when clicked outside', (done) => {
        testbed.render()
        const outerElement = elementInApp()
        mouseEventOn(outerElement, 'click')

        setTimeout(() => {
          expect(updateItemStub).to.have.been.called()
          done()
        })
      })

      it('does not call submitHandler when clicked outside and valid, but occluded', (done) => {
        testbed.render({ isOccluded: true })

        const outerElement = elementInApp()
        mouseEventOn(outerElement, 'click')

        setTimeout(() => {
          expect(updateItemStub).to.have.not.been.called()
          done()
        })
      })

      it('does not call clickedOutside if the element is the addContentPopover button', (done) => {
        testbed.render()

        const addContentButton = document.createElement('button')
        const outerElement = document.createElement('div')
        outerElement.className = 'AddContentPopover'
        const falseApp = makeFalseApp()
        outerElement.appendChild(addContentButton)
        falseApp.appendChild(outerElement)
        document.body.appendChild(falseApp)

        addContentButton.focus()
        mouseEventOn(addContentButton, 'click')

        setTimeout(() => {
          expect(updateItemStub).to.have.not.been.called()
          outerElement.remove()
          done()
        })
      })

      it('ignores clicks outside the app', (done) => {
        testbed.render()

        const outsideElement = document.createElement('button')
        const falseApp = makeFalseApp()
        document.body.appendChild(falseApp)
        document.body.appendChild(outsideElement)
        mouseEventOn(outsideElement, 'click')

        setTimeout(() => {
          expect(updateItemStub).to.have.not.been.called()
          outsideElement.remove()
          done()
        })
      })

      it('does not ignore clicks outside the app when an AddContentPopover is open', (done) => {
        testbed.render({ addContentPopoverOpen: true })

        const outsideElement = document.createElement('button')
        const falseApp = makeFalseApp()
        document.body.appendChild(falseApp)
        document.body.appendChild(outsideElement)
        mouseEventOn(outsideElement, 'click')

        setTimeout(() => {
          expect(updateItemStub).to.have.been.called()
          outsideElement.remove()
          done()
        })
      })
    }

    context('as an sdk and standalone app', () => {
      clickOffExamples()
    })
  })

  describe('a11y tests', () => {
    it('should meet a11y standards', (done) => {
      testbed.render()
      testbed.checkA11yStandards(done)
    })
  })

  describe('test with connect', () => {
    const connectedTestbed = createTestbed(
      wrapInDragAndDropTestContext(withContext(ConnectedQuizEntryEdit)),
      defaultProps
    )

    shouldRenderComponent(
      connectedTestbed, {}, QuizEntryEdit
    )
  })
})
