import ReactDOM from 'react-dom'
import ReactTestUtils from 'react-dom/test-utils'

import { fromJS, List, Map } from 'immutable'
import { sync } from 'most-subject'
import { PaginationButton } from '@instructure/ui-pagination/lib/components/Pagination'
import Spinner from '../../spinner/Spinner'
import PaginatedCollection from '../presenter'

const defer = () => {
  let resolve
  const promise = new Promise((_resolve) => resolve = _resolve)
  return { promise, resolve }
}

describe('PaginatedCollection', () => {
  const getPageStub = sinon.stub().returns(Promise.resolve({
    records: [1, 2, 3],
    totalPages: 4
  }))
  const mergeStub = sinon.stub()
  const setStub = sinon.stub()
  const renderRecordsStub = sinon.stub().returns('Your Collection Here')
  const testbed = createTestbed(PaginatedCollection, {
    renderRecords: renderRecordsStub,
    getPage: getPageStub,
    totalPages: 4,
    currentPage: 1,
    contents: Map({ 1: List([1, 2, 3]) }),
    stateKey: 'MY_KEY',
    merge: mergeStub,
    set: setStub
  })

  beforeEach(() => {
    getPageStub.resetHistory()
    mergeStub.resetHistory()
    renderRecordsStub.resetHistory()
  })

  it('does not fetch current page when present', () => {
    testbed.render()
    expect(getPageStub).not.to.have.been.called()
  })

  it('fetches current page on mount', () => {
    testbed.render({ contents: Map() })
    expect(getPageStub).to.have.been.calledOnce()
    expect(getPageStub.firstCall.args[0]).to.eql(1)
  })

  it('resets to first page when stateKey changes', () => {
    testbed.render()
    testbed.render({ stateKey: 'OTHER_KEY' })
    expect(mergeStub).to.have.been.calledWith('OTHER_KEY', { current: 1 })
  })

  it('clears all pages and resets to first page when params change', () => {
    testbed.render()
    testbed.render({ params: fromJS({ abc: 123 }) })
    expect(setStub).to.have.been.calledWith('MY_KEY', fromJS({ current: 1 }))
  })

  it('updates correct page after consecutive rerenders', async () => {
    const page = (records) => ({ records, totalPages: 1 })

    const loadFirstKey = defer()
    const loadSecondKey = defer()
    const getPage = sinon.stub()
      .onCall(0).returns(loadFirstKey.promise)
      .onCall(1).returns(loadSecondKey.promise)

    // componentDidUpdate is not called for the first render, and it's what
    // loads pages.
    testbed.render({ stateKey: 'FIRST_KEY' })
    const mergeCalls = sync()
    const merge = sinon.fake((...args) => mergeCalls.next(args))
    // We need to wait for all four calls to merge(), some of which happen
    // asynchronously in response to promise resolutions. Begin collecting calls,
    // and convert that stream into a promise of all expected calls.
    const calls = mergeCalls.take(4).reduce((acc, cur) => [...acc, cur], [])

    // Render pages and then resolve loads in reverse order. We shouldn't be
    // updating the old collection page cache with data from the new state key
    testbed.render({ contents: Map(), getPage, merge })
    testbed.render({ contents: Map(), getPage, merge, stateKey: 'OTHER_KEY' })
    loadSecondKey.resolve(page([4, 5, 6]))
    loadFirstKey.resolve(page([1, 2, 3]))
    expect(await calls).to.deep.equal([
      ['MY_KEY', { current: 1 }],
      ['OTHER_KEY', { current: 1 }],
      ['OTHER_KEY', { contents: { 1: [4, 5, 6] }, total: 1 }],
      ['MY_KEY', { contents: { 1: [1, 2, 3] }, total: 1 }]
    ])
  })

  it('ignores updates from old parameters', async () => {
    const page = (records) => ({ records, totalPages: 1 })

    const loadFirstParams = defer()
    const loadSecondParams = defer()
    const getPage = sinon.stub()
      .onCall(0).returns(loadFirstParams.promise)
      .onCall(1).returns(loadSecondParams.promise)

    // componentDidUpdate is not called for the first render, and it's what
    // loads pages.
    testbed.render({ })
    const merged = defer()
    const merge = sinon.fake((...args) => merged.resolve(args))

    // Render pages and then resolve loads in reverse order. We shouldn't be
    // updating the collection with stale data from fetching with old parameters
    const params = fromJS({ x: 1 })
    testbed.render({ contents: Map(), getPage, merge })
    testbed.render({ contents: Map(), getPage, merge, params })
    loadFirstParams.resolve(page([1, 2, 3]))
    loadSecondParams.resolve(page([4, 5, 6]))
    expect(await merged.promise).to.deep.equal(
      ['MY_KEY', { contents: { 1: [4, 5, 6] }, total: 1 }],
    )
  })

  it('updates state when a page is fetched', (done) => {
    testbed.render({ contents: Map() })
    setTimeout(() => { // wait for the getPage promise to resolve
      expect(mergeStub).to.have.been.calledWith('MY_KEY', {
        contents: { 1: [1, 2, 3] },
        total: 4
      })
      done()
    }, 0)
  })

  it('updates current page on page change', () => {
    testbed.render()
    const buttons = testbed.findChildrenByType(PaginationButton)
    const element = testbed.findChildrenByTagName('button', buttons[1])[0]
    ReactTestUtils.Simulate.click(element)
    expect(mergeStub).to.have.been.calledOnce()
    expect(mergeStub.firstCall.args).to.eql(['MY_KEY', { current: 2 }])
  })

  it('fetches page on update', () => {
    testbed.render()
    testbed.render({ currentPage: 2 })
    expect(getPageStub).to.have.been.calledOnce()
    expect(getPageStub.firstCall.args[0]).to.eql(2)
  })

  it('does not fetch page when present', () => {
    const contents = Map({ 1: List([1, 2, 3]), 2: List([4, 5, 6]) })
    testbed.render({ contents })
    testbed.render({ contents, currentPage: 2 })
    expect(getPageStub.called).to.be.false()
  })

  it('renders pagination controls', () => {
    testbed.render()
    const buttons = testbed.findChildrenByType(PaginationButton)

    expect(buttons.map((b) => ReactDOM.findDOMNode(b).innerText.trim())).to.eql(['1', '2', '3', '4'])
  })

  it('renders collection even when no records are present', () => {
    testbed.render({ contents: Map({ 1: List() }) })
    expect(renderRecordsStub).to.have.been.calledOnce()
    expect(renderRecordsStub.firstCall.args[0]).to.eql([])
  })

  it('renders empty state when provided', () => {
    testbed.render({ contents: Map({ 1: List() }), empty: 'nothing here' })
    expect(testbed.getText()).to.include('nothing here')
  })

  it('renders collection with records in vanilla JS', () => {
    testbed.render({ empty: 'nothing here' })
    expect(renderRecordsStub).to.have.been.calledOnce()
    expect(renderRecordsStub.firstCall.args[0]).to.eql([1, 2, 3])
    expect(testbed.getText()).to.not.include('nothing here')
    expect(testbed.getText()).to.include('Your Collection Here')
  })

  it('renders loading state when records are not present', () => {
    testbed.render({ contents: Map(), empty: 'nothing here' })
    expect(testbed.getText()).to.not.include('nothing here')
    expect(testbed.findChildrenByType(Spinner).length).to.eq(1)
  })
})
