import React, { Component } from 'react'
import { List, Map } from 'immutable'
import {
  chooseDragConnection,
  usingIE,
  getBoundsForComponent,
  posObjFromQuizEntry,
  posObjFromNonQuizEntry,
  onQuizEntryHover,
  onQuizEntryDrop
} from '../dragAndDropUtils'

import QuizEntry from '../../../../records/QuizEntry'
import { TEMPORARY_QUIZ_ENTRY_ID, SIDEBAR_ITEM_REF } from '@instructure/quiz-common/lib/constants'
import Item from '../../../../records/Item'

class TestComponent extends Component {
  storeStartQuizEntriesRef = (node) => {
    this.startQuizEntries = node
  }

  storeQuizEntriesRef = (node) => {
    this.quizEntries = node
  }

  storeEndQuizEntriesRef = (node) => {
    this.endQuizEntries = node
  }

  render () {
    const style = { width: '100px', height: '100px' }
    return (
      <div>
        <div style={style} ref={this.storeStartQuizEntriesRef} />
        <div style={style} ref={this.storeQuizEntriesRef} />
        <div style={style} ref={this.storeEndQuizEntriesRef} />
      </div>
    )
  }
}

describe('dragAndDropUtils', () => {
  const testbed = createTestbed(TestComponent, {})

  const windowOverrideIE = {
    navigator: {
      userAgent: ['Edge/', 'Trident/', 'MSIE ']
    }
  }

  const windowOverrideChrome = {
    navigator: {
      userAgent: ['Chrome']
    }
  }

  describe('#usingIE', () => {
    it('returns true if using IE or Edge', () => {
      const isIE = usingIE(windowOverrideIE)
      expect(isIE).to.be.true()
    })

    it('returns false if not using IE or Edge', () => {
      const isIE = usingIE(windowOverrideChrome)
      expect(isIE).to.be.false()
    })
  })

  describe('#chooseDragConnection', () => {
    it('chooses dragSource when using IE or Edge', () => {
      const dragSource = 1
      const dragPreview = 2
      const chosenConnection = chooseDragConnection(dragSource, dragPreview, windowOverrideIE)
      expect(chosenConnection).to.be.equal(dragSource)
    })

    it('chooses dragPreview when using IE or Edge', () => {
      const dragSource = 1
      const dragPreview = 2
      const chosenConnection = chooseDragConnection(dragSource, dragPreview, windowOverrideChrome)
      expect(chosenConnection).to.be.equal(dragPreview)
    })
  })

  describe('#getBoundsForComponent', () => {
    it('returns the bounds of the corresponding DOM element', () => {
      testbed.render()
      const bodyMargin = parseFloat(window.getComputedStyle(document.body).margin)
      const { top, bottom, right, left } = getBoundsForComponent(testbed.subject.quizEntries)
      expect(top).to.equal(100 + bodyMargin)
      expect(bottom).to.equal(200 + bodyMargin)
      expect(right).to.equal(100 + bodyMargin)
      expect(left).to.equal(bodyMargin)
    })
  })

  describe('#posObjFromQuizEntry', () => {
    it('returns a correct newPositionObj when from above and above half', () => {
      testbed.render()
      const quizEntryDragging = QuizEntry.create({ position: 1 })
      const quizEntryHovered = QuizEntry.create({ position: 2 })
      const mousePos = { x: 50, y: 140 }
      const posObj = posObjFromQuizEntry(
        quizEntryDragging, quizEntryHovered, mousePos, testbed.subject.quizEntries, false
      )
      expect(posObj).to.deep.equal({
        position: 2,
        intoEmptyStimulus: false,
        aboveHalf: true,
        fromAbove: true
      })
    })

    it('returns a correct newPositionObj when from above and below half', () => {
      testbed.render()
      const quizEntryDragging = QuizEntry.create({ position: 1 })
      const quizEntryHovered = QuizEntry.create({ position: 2 })
      const mousePos = { x: 50, y: 190 }
      const posObj = posObjFromQuizEntry(
        quizEntryDragging, quizEntryHovered, mousePos, testbed.subject.quizEntries, false
      )
      expect(posObj).to.deep.equal({
        position: 2,
        intoEmptyStimulus: false,
        aboveHalf: false,
        fromAbove: true
      })
    })

    it('returns a correct newPositionObj when from below and above half', () => {
      testbed.render()
      const quizEntryDragging = QuizEntry.create({ position: 2 })
      const quizEntryHovered = QuizEntry.create({ position: 1 })
      const mousePos = { x: 50, y: 140 }
      const posObj = posObjFromQuizEntry(
        quizEntryDragging, quizEntryHovered, mousePos, testbed.subject.quizEntries, false
      )
      expect(posObj).to.deep.equal({
        position: 1,
        intoEmptyStimulus: false,
        aboveHalf: true,
        fromAbove: false
      })
    })

    it('returns a correct newPositionObj when from below and below half', () => {
      testbed.render()
      const quizEntryDragging = QuizEntry.create({ position: 2 })
      const quizEntryHovered = QuizEntry.create({ position: 1 })
      const mousePos = { x: 50, y: 160 }
      const posObj = posObjFromQuizEntry(
        quizEntryDragging, quizEntryHovered, mousePos, testbed.subject.quizEntries, false
      )
      expect(posObj).to.deep.equal({
        position: 1,
        intoEmptyStimulus: false,
        aboveHalf: false,
        fromAbove: false
      })
    })

    context('into stimulus', () => {
      it('correctly indicates intoEmptyStimulus when on right half', () => {
        testbed.render()
        const quizEntryDragging = QuizEntry.create({ position: 1 })
        const quizEntryHovered = QuizEntry.create({ position: 2, entryType: 'Stimulus' })
        const mousePos = { x: 70, y: 165 }
        const posObj = posObjFromQuizEntry(
          quizEntryDragging, quizEntryHovered, mousePos, testbed.subject.quizEntries, false
        )
        expect(posObj).to.deep.equal({
          position: 2,
          intoEmptyStimulus: true,
          aboveHalf: false,
          fromAbove: true
        })
      })

      it('should not be intoEmptyStimulus when on right half when not deep', () => {
        testbed.render()
        const quizEntryDragging = QuizEntry.create({ position: 1 })
        const quizEntryHovered = QuizEntry.create({ position: 2, entryType: 'Stimulus' })
        const mousePos = { x: 70, y: 180 }
        const posObj = posObjFromQuizEntry(
          quizEntryDragging, quizEntryHovered, mousePos, testbed.subject.quizEntries, false
        )
        expect(posObj).to.deep.equal({
          position: 2,
          intoEmptyStimulus: false,
          aboveHalf: false,
          fromAbove: true
        })
      })

      it('does not return intoEmptyStimulus if on left half', () => {
        testbed.render()
        const quizEntryDragging = QuizEntry.create({ position: 1 })
        const quizEntryHovered = QuizEntry.create({ position: 2, entryType: 'Stimulus' })
        const mousePos = { x: 30, y: 165 }
        const posObj = posObjFromQuizEntry(
          quizEntryDragging, quizEntryHovered, mousePos, testbed.subject.quizEntries, false
        )
        expect(posObj).to.deep.equal({
          position: 2,
          intoEmptyStimulus: false,
          aboveHalf: false,
          fromAbove: true
        })
      })

      it('returns null if not empty and on right half', () => {
        testbed.render()
        const quizEntryDragging = QuizEntry.create({ position: 1 })
        const quizEntryHovered = QuizEntry.create({ position: 2, entryType: 'Stimulus' })
        sinon.stub(quizEntryHovered, 'getNestedEntries').returns(List([1, 2, 3]))
        const mousePos = { x: 70, y: 155 }
        const posObj = posObjFromQuizEntry(
          quizEntryDragging, quizEntryHovered, mousePos, testbed.subject.quizEntries, false
        )
        expect(posObj).to.be.null()
      })

      it('does not return null if not empty and on right half, but not deep', () => {
        testbed.render()
        const quizEntryDragging = QuizEntry.create({ position: 1 })
        const quizEntryHovered = QuizEntry.create({ position: 2, entryType: 'Stimulus' })
        sinon.stub(quizEntryHovered, 'getNestedEntries').returns(List([1, 2, 3]))
        const mousePos = { x: 70, y: 180 }
        const posObj = posObjFromQuizEntry(
          quizEntryDragging, quizEntryHovered, mousePos, testbed.subject.quizEntries, false
        )
        expect(posObj).to.deep.equal({
          position: 2,
          intoEmptyStimulus: false,
          aboveHalf: false,
          fromAbove: true
        })
      })

      it('returns null if dragging stimulus into stimulus', () => {
        testbed.render()
        const quizEntryDragging = QuizEntry.create({ position: 1, entryType: 'Stimulus' })
        const quizEntryHovered = QuizEntry.create({ position: 2, entryType: 'Item', stimulusQuizEntryId: '5' })
        sinon.stub(quizEntryHovered, 'getNestedEntries').returns(List([1, 2, 3]))
        const mousePos = { x: 70, y: 155 }
        const posObj = posObjFromQuizEntry(
          quizEntryDragging, quizEntryHovered, mousePos, testbed.subject.quizEntries, false
        )
        expect(posObj).to.be.null()
      })
    })
  })

  describe('#posObjFromNonQuizEntry', () => {
    it('returns the right positionObj if above all quizEntries', () => {
      testbed.render()
      const mousePos = { x: 50, y: 40 }
      const { startQuizEntries, endQuizEntries } = testbed.subject
      const quizEntryDragging = QuizEntry.create({ position: 1 })
      const posObj = posObjFromNonQuizEntry(
        quizEntryDragging, startQuizEntries, endQuizEntries, mousePos, List()
      )
      expect(posObj).to.deep.equal({
        position: 1,
        intoEmptyStimulus: false,
        aboveHalf: true,
        fromAbove: false
      })
    })

    it('returns the right positionObj if below all quizEntries and last quizEntry is not in stimulus', () => {
      testbed.render()
      const mousePos = { x: 50, y: 330 }
      const { startQuizEntries, endQuizEntries } = testbed.subject
      const quizEntries = List([
        new QuizEntry({ position: 1 }), new QuizEntry({ position: 2 }), new QuizEntry({ position: 3 })
      ])
      const quizEntryDragging = QuizEntry.create({ position: 1 })
      const posObj = posObjFromNonQuizEntry(
        quizEntryDragging, startQuizEntries, endQuizEntries, mousePos, quizEntries
      )
      expect(posObj).to.deep.equal({
        position: 3,
        intoEmptyStimulus: false,
        aboveHalf: false,
        fromAbove: true
      })
    })

    it('returns the right positionObj if below all quizEntries and last quizEntry is in stimulus', () => {
      testbed.render()
      const mousePos = { x: 50, y: 330 }
      const { startQuizEntries, endQuizEntries } = testbed.subject
      const quizEntries = List([
        new QuizEntry({ position: 1 }),
        new QuizEntry({ id: '4', position: 2 }),
        new QuizEntry({ stimulusQuizEntryId: '4', position: 3 })
      ])
      const quizEntryDragging = QuizEntry.create({ position: 1 })
      const posObj = posObjFromNonQuizEntry(
        quizEntryDragging, startQuizEntries, endQuizEntries, mousePos, quizEntries
      )
      expect(posObj).to.deep.equal({
        position: 2,
        intoEmptyStimulus: false,
        aboveHalf: false,
        fromAbove: true
      })
    })

    it('returns null if between top and bottom', () => {
      testbed.render()
      const mousePos = { x: 50, y: 150 }
      const { startQuizEntries, endQuizEntries } = testbed.subject
      const quizEntryDragging = QuizEntry.create({ position: 1 })
      const posObj = posObjFromNonQuizEntry(
        quizEntryDragging, startQuizEntries, endQuizEntries, mousePos, List()
      )
      expect(posObj).to.be.null()
    })
  })

  const moveQuizEntryStub = sinon.stub()
  const newHoverPositionStub = sinon.stub()
  const setUiStub = sinon.stub()
  const openMoveImmutableModalStub = sinon.stub()

  afterEach(() => {
    moveQuizEntryStub.resetHistory()
    newHoverPositionStub.resetHistory()
    setUiStub.resetHistory()
    openMoveImmutableModalStub.resetHistory()
  })

  describe('#onQuizEntryHover', () => {
    const props = { newHoverPosition: newHoverPositionStub }
    const examplePosObj = {
      position: 2,
      aboveHalf: true,
      fromAbove: false,
      intoEmptyStimulus: false
    }
    const monitor = {
      getItem: () => ({ position: 3 })
    }

    it('returns null if inSidebar and sidebar is closed', () => {
      testbed.render()
      const customProps = {
        newHoverPosition: newHoverPositionStub,
        sidebarOpen: false
      }
      onQuizEntryHover(customProps, monitor, testbed.subject.quizEntries, SIDEBAR_ITEM_REF)
      expect(newHoverPositionStub).to.have.not.been.called()
    })

    it('calls the newHoverPosition action with correct arguments', () => {
      testbed.render()
      const createPosStub = sinon.stub().returns(examplePosObj)
      onQuizEntryHover(props, monitor, testbed.subject.quizEntries, 'quiz_entry', createPosStub)
      const newPosition = newHoverPositionStub.getCall(0).args[0]
      expect(newPosition).to.deep.equal(examplePosObj)
    })

    it('does not call the action if nothing has changed', () => {
      testbed.render()
      const createPosStub = sinon.stub().returns(examplePosObj)
      const monitor = {
        getItem: () => (examplePosObj)
      }
      onQuizEntryHover(props, monitor, testbed.subject.quizEntries, 'quiz_entry', createPosStub)
      expect(newHoverPositionStub).to.have.not.been.called()
    })
  })

  describe('onQuizEntryDrop', () => {
    const targetQuizEntry = QuizEntry.create({ position: 2, id: 2 })
    const props = {
      moveQuizEntry: moveQuizEntryStub,
      setUi: setUiStub,
      openMoveImmutableModal: openMoveImmutableModalStub,
      quizEntry: targetQuizEntry,
      quizEntries: List([targetQuizEntry]),
      quizId: 1,
      dndPositionObj: Map({
        position: 2,
        aboveHalf: false,
        fromAbove: true,
        intoEmptyStimulus: false
      })
    }
    const getDropResultStub = sinon.stub().returns({})
    const sharedMonitor = {
      getDropResult: getDropResultStub,
      getItem: () => ({
        quizEntry: QuizEntry.create({ position: 1, id: 1 }) // moving quizEntry
      })
    }

    it('move quiz entry action called with correct moveTo position', () => {
      onQuizEntryDrop(props, sharedMonitor, null, 'refname')
      expect(moveQuizEntryStub.getCall(0).args[1]).to.be.equal(2)
    })

    it('move quiz entry action called with correct item id', () => {
      onQuizEntryDrop(props, sharedMonitor, null, 'refname')
      expect(moveQuizEntryStub.getCall(0).args[3]).to.be.equal(1)
    })

    it('does not call any action if inSidebar and sidebar is not open', () => {
      const customProps = Object.assign({}, props, { sidebarOpen: false })
      onQuizEntryDrop(customProps, sharedMonitor, null, SIDEBAR_ITEM_REF)
      expect(moveQuizEntryStub).to.have.not.been.called()
    })

    it('does not call any action if drop was already handled', () => {
      const monitor = {
        getItem: () => ({ quizEntry: QuizEntry.create() }),
        getDropResult: () => ({ handledDrop: true })
      }
      onQuizEntryDrop(props, monitor, null, 'refname')
      expect(moveQuizEntryStub).to.have.not.been.called()
    })

    it('does not move item if moveTo position and stimulusQuizEntryId are unchanged', () => {
      const monitor = {
        getDropResult: getDropResultStub,
        getItem: () => ({
          quizEntry: QuizEntry.create({ position: 2, id: 1 })
        })
      }
      onQuizEntryDrop(props, monitor, null, 'refname')
      expect(moveQuizEntryStub).to.have.not.been.called()
    })

    it('does not move stimulus if moving to same position', () => {
      const quizEntry = QuizEntry.create({ position: 2, id: 2, entryType: 'Stimulus' })
      sinon.stub(quizEntry, 'getNestedEntries').returns(List([1, 2, 3]))
      const monitor = {
        getDropResult: getDropResultStub,
        getItem: () => ({ quizEntry })
      }
      const customProps = Object.assign({}, props, { quizEntry })
      onQuizEntryDrop(customProps, monitor, null, 'refname')
      expect(moveQuizEntryStub).to.have.not.been.called()
    })

    context('with an immutable item', () => {
      const immutableQE = QuizEntry.create({ position: 3, id: 2, stimulusQuizEntryId: '4', entryType: 'Item' })
      sinon.stub(immutableQE, 'getEntry').returns(new Item({ status: 'immutable' }))
      const monitor = {
        getDropResult: getDropResultStub,
        getItem: () => ({ quizEntry: immutableQE })
      }

      it('sets move immutable data correctly', () => {
        onQuizEntryDrop(props, monitor, null, 'refname')
        expect(setUiStub.firstCall.args[1].toJS()).to.deep.eq({
          movedInSidebar: false,
          quizId: 1,
          position: 2,
          stimulusQuizEntryId: '',
          quizEntryId: 2
        })
      })

      it('opens the moveImmutable modal', () => {
        onQuizEntryDrop(props, monitor, null, 'refname')
        expect(openMoveImmutableModalStub).to.be.called()
      })

      it('does not call moveQuizEntry', () => {
        onQuizEntryDrop(props, monitor, null, 'refname')
        expect(moveQuizEntryStub).not.to.have.been.called()
      })
    })

    context('with temporary item', () => {
      const moveQuizEntryPromise = (a, b, c, d, moveTempItemCb) => {
        moveTempItemCb()
      }
      const moveTempItemStub = sinon.stub()
      const quizEntriesWithTemporaryItem = List([
        QuizEntry.create({ id: 1, position: 1 }),
        QuizEntry.create({ id: 2, position: 2 }),
        QuizEntry.create({ id: TEMPORARY_QUIZ_ENTRY_ID, position: 3 }),
        QuizEntry.create({ id: 4, position: 4 }),
        QuizEntry.create({ id: 5, position: 5 })
      ])

      afterEach(() => {
        moveTempItemStub.resetHistory()
      })

      const moveFromTo = (fromPosition, toPosition) => {
        const monitor = {
          getDropResult: getDropResultStub,
          getItem: () => ({
            quizEntry: QuizEntry.create({
              position: fromPosition,
              id: (fromPosition === 3 ? TEMPORARY_QUIZ_ENTRY_ID : fromPosition)
            })
          })
        }

        const propsWithTempItem = {
          moveQuizEntry: moveQuizEntryPromise,
          moveTemporaryQuizEntry: moveTempItemStub,
          quizEntries: quizEntriesWithTemporaryItem,
          newHoverPosition: newHoverPositionStub,
          dndPositionObj: Map({
            position: toPosition,
            aboveHalf: true,
            fromAbove: false,
            intoEmptyStimulus: false
          }),
          setUi: () => {}
        }
        onQuizEntryDrop(propsWithTempItem, monitor, null, 'refname')
      }

      it('moves the temporary item up when an item above it is dropped on it', () => {
        moveFromTo(1, 3)
        expect(moveTempItemStub.calledWith('up')).to.be.true()
      })

      it('moves the temporary item up when an item above it is dropped passed it', () => {
        moveFromTo(1, 5)
        expect(moveTempItemStub.calledWith('up')).to.be.true()
      })

      it('moves the temporary item down when an item below it is dropped on it', () => {
        moveFromTo(4, 3)
        expect(moveTempItemStub.calledWith('down')).to.be.true()
      })

      it('moves the temporary item down when an item below it is dropped passed it', () => {
        moveFromTo(4, 2)
        expect(moveTempItemStub.calledWith('down')).to.be.true()
      })

      it('does not move the temporary item when an item above it is dropped above it', () => {
        moveFromTo(1, 2)
        expect(moveTempItemStub.called).to.be.false()
      })

      it('does not move the temporary item when an item below it is dropped below it', () => {
        moveFromTo(4, 5)
        expect(moveTempItemStub.called).to.be.false()
      })

      it('moves the temporary item to a specific position when it is being dragged', () => {
        moveFromTo(3, 1)
        expect(moveTempItemStub.calledWith(1)).to.be.true()
      })
    })
  })
})
