content: import _ from 'lodash'
  import { spy, expect } from 'chai'
  import React from 'react'
  import { renderToString, renderToStaticMarkup } from 'react-dom/server'
  import delay from 'timeout-as-promise'
  import { mount } from 'enzyme'
  import { withAsyncState } from '../src/react-async-state'
  
  describe('ReactAsyncState', () => {
    function Foo() {
      return (
        <div />
      )
    }
  
    function imitateHotReloading(root, TargetClass, SourceClass) {
      _.forOwn(SourceClass.prototype, (value, key) => {
        if (_.isFunction(value) && key !== 'render') {
          // eslint-disable-next-line no-param-reassign
          TargetClass.prototype[key] = value
        }
      })
  
      root.instance().forceUpdate()
    }
  
    describe('Decorator', () => {
      it('should accept `mapAsyncDataToProps` only if it `function`', () => {
        expect(() => withAsyncState()).to.throw(Error)
        expect(() => withAsyncState('')).to.throw(Error)
        expect(() => withAsyncState({})).to.throw(Error)
        expect(() => withAsyncState(() => ({}))).to.not.throw(Error)
      })
  
      it('should accept `options` only if it `object`', () => {
        expect(() => withAsyncState(() => ({}), '')).to.throw(Error)
        expect(() => withAsyncState(() => ({}), [])).to.throw(Error)
        expect(() => withAsyncState(() => ({}), () => ({}))).to.throw(Error)
        expect(() => withAsyncState(() => ({}), {})).to.not.throw(Error)
      })
  
      it('should accept `options.mergeProps` only if it `function`', () => {
        expect(() => withAsyncState(() => ({}), { mergeProps: '' })).to.throw(Error)
        expect(() => withAsyncState(() => ({}), { mergeProps: [] })).to.throw(Error)
        expect(() => withAsyncState(() => ({}), { mergeProps: {} })).to.throw(Error)
        expect(() => withAsyncState(() => ({}), { mergeProps: () => ({}) })).to.not.throw(Error)
      })
  
      it('should accept `options.getInitialState` only if it `function`', () => {
        expect(() => withAsyncState(() => ({}), { getInitialState: '' })).to.throw(Error)
        expect(() => withAsyncState(() => ({}), { getInitialState: [] })).to.throw(Error)
        expect(() => withAsyncState(() => ({}), { getInitialState: {} })).to.throw(Error)
        expect(() => withAsyncState(() => ({}), { getInitialState: () => ({}) })).to.not.throw(Error)
      })
  
      it('should render decorated component', () => {
        const Root = withAsyncState(() => ({}))(props => <Foo {...props} />)
  
        const root = mount(
          <Root
            bar="baz"
          />
        )
  
        const foo = root.find(Foo)
  
        expect(Boolean(foo)).to.be.equal(true)
        expect(foo.props()).to.be.deep.equal({
          bar: 'baz',
        })
      })
  
      it('should hoist statics from child component', () => {
        const Root = props => <Foo {...props} />
  
        Root.a = 1
        Root.b = 2
        Root.c = 3
  
        const AsyncRoot = withAsyncState(() => ({}))(Root)
  
        expect(Root.a).to.be.equal(1)
        expect(Root.b).to.be.equal(2)
        expect(Root.c).to.be.equal(3)
  
        expect(AsyncRoot.a).to.be.equal(Root.a)
        expect(AsyncRoot.b).to.be.equal(Root.b)
        expect(AsyncRoot.c).to.be.equal(Root.c)
      })
    })
  
    describe('mapAsyncDataToProps', () => {
      it('should be called one time after mount', () => {
        const mapAsyncDataToProps = spy(() => ({}))
        const decorator = withAsyncState(mapAsyncDataToProps)
        const DecoratedFoo = decorator(Foo)
  
        mount(<DecoratedFoo />)
  
        mapAsyncDataToProps.should.have.been.called.exactly(1)
      })
  
      it('should NOT be called after SSR', () => {
        const mapAsyncDataToProps = spy(() => ({}))
        const decorator = withAsyncState(mapAsyncDataToProps)
        const DecoratedFoo = decorator(Foo)
  
        renderToString(<DecoratedFoo />)
  
        expect(mapAsyncDataToProps).to.not.have.been.called()
  
        renderToStaticMarkup(<DecoratedFoo />)
  
        expect(mapAsyncDataToProps).to.not.have.been.called()
      })
  
      it('should pass values after initial render', () => {
        const decorator = withAsyncState(() => ({ bar: 'baz' }))
        const DecoratedFoo = decorator(Foo)
  
        const root = mount(<DecoratedFoo foo="bar" />)
        const foo = root.find(Foo)
  
        expect(root.state()).to.be.deep.equal({})
        expect(foo.props()).to.be.deep.equal({
          foo: 'bar',
        })
  
        return delay().then(() => {
          expect(root.state()).to.be.deep.equal({
            bar: 'baz',
          })
  
          expect(foo.props()).to.be.deep.equal({
            foo: 'bar',
            bar: 'baz',
          })
        })
      })
  
      it('should pass remapped values on props change', () => {
        const decorator = withAsyncState(({ rating }) => ({ percentage: (rating * 100) / 5 }))
        const DecoratedFoo = decorator(Foo)
  
        const root = mount(<DecoratedFoo rating={3} />)
        const foo = root.find(Foo)
  
        expect(foo.props()).to.be.deep.equal({
          rating: 3,
        })
  
        return delay().then(() => {
          expect(foo.props()).to.be.deep.equal({
            rating: 3,
            percentage: 60,
          })
  
          root.setProps({
            rating: 4,
          })
  
          expect(foo.props()).to.be.deep.equal({
            rating: 4,
            percentage: 60,
          })
  
          return delay()
        }).then(() => {
          expect(foo.props()).to.be.deep.equal({
            rating: 4,
            percentage: 80,
          })
  
          root.setProps({
            rating: 2,
          })
  
          expect(foo.props()).to.be.deep.equal({
            rating: 2,
            percentage: 80,
          })
  
          return delay()
        }).then(() => {
          expect(foo.props()).to.be.deep.equal({
            rating: 2,
            percentage: 40,
          })
        })
      })
  
      it('should NOT be called more then one time if do not have arguments', () => {
        const mapAsyncDataToProps = spy(() => ({ bar: 'baz' }))
        const decorator = withAsyncState(mapAsyncDataToProps)
        const DecoratedFoo = decorator(Foo)
  
        const root = mount(<DecoratedFoo foo="bar" />)
        const foo = root.find(Foo)
  
        mapAsyncDataToProps.should.have.been.called.exactly(1)
  
        expect(root.state()).to.be.deep.equal({})
        expect(foo.props()).to.be.deep.equal({
          foo: 'bar',
        })
  
        return delay().then(() => {
          mapAsyncDataToProps.should.have.been.called.exactly(1)
  
          expect(root.state()).to.be.deep.equal({
            bar: 'baz',
          })
  
          expect(foo.props()).to.be.deep.equal({
            foo: 'bar',
            bar: 'baz',
          })
  
          root.setProps({
            foo: 'baz',
          })
  
          mapAsyncDataToProps.should.have.been.called.exactly(1)
  
          expect(root.state()).to.be.deep.equal({
            bar: 'baz',
          })
  
          expect(foo.props()).to.be.deep.equal({
            foo: 'baz',
            bar: 'baz',
          })
  
          return delay()
        }).then(() => {
          mapAsyncDataToProps.should.have.been.called.exactly(1)
  
          expect(root.state()).to.be.deep.equal({
            bar: 'baz',
          })
  
          expect(foo.props()).to.be.deep.equal({
            foo: 'baz',
            bar: 'baz',
          })
  
          root.setProps({
            foo: 'quoz',
          })
  
          mapAsyncDataToProps.should.have.been.called.exactly(1)
  
          expect(root.state()).to.be.deep.equal({
            bar: 'baz',
          })
  
          expect(foo.props()).to.be.deep.equal({
            foo: 'quoz',
            bar: 'baz',
          })
  
          return delay()
        }).then(() => {
          mapAsyncDataToProps.should.have.been.called.exactly(1)
  
          expect(root.state()).to.be.deep.equal({
            bar: 'baz',
          })
  
          expect(foo.props()).to.be.deep.equal({
            foo: 'quoz',
            bar: 'baz',
          })
        })
      })
  
      it('should NOT pass values after unmount', () => {
        const decorator = withAsyncState(({ rating }) => delay(10).then(() => ({
          percentage: (rating * 100) / 5,
        })))
  
        const DecoratedFoo = decorator(Foo)
  
        const root = mount(<DecoratedFoo rating={3} />)
        const foo = root.find(Foo)
        const instance = root.instance()
  
        expect(instance.state).to.be.deep.equal({})
        expect(foo.props()).to.be.deep.equal({
          rating: 3,
        })
  
        return delay(15).then(() => {
          expect(instance.state).to.be.deep.equal({
            percentage: 60,
          })
  
          expect(foo.props()).to.be.deep.equal({
            rating: 3,
            percentage: 60,
          })
  
          root.setProps({
            rating: 4,
          })
  
          expect(instance.state).to.be.deep.equal({
            percentage: 60,
          })
  
          expect(foo.props()).to.be.deep.equal({
            rating: 4,
            percentage: 60,
          })
  
          root.unmount()
  
          expect(instance.state).to.be.deep.equal({
            percentage: 60,
          })
  
          return delay(15)
        }).then(() => {
          expect(instance.state).to.be.deep.equal({
            percentage: 60,
          })
        })
      })
  
      it('should pass only newest values', () => {
        const mapAsyncDataToProps = spy(({ ms }) => delay(ms).then(() => ({
          delayed: ms,
        })))
  
        const decorator = withAsyncState(mapAsyncDataToProps)
  
        const DecoratedFoo = decorator(Foo)
  
        const foo = mount(<DecoratedFoo ms={10} />)
  
        expect(foo.state()).to.be.deep.equal({})
  
        return delay(15).then(() => {
          expect(foo.state()).to.be.deep.equal({
            delayed: 10,
          })
  
          foo.setProps({ ms: 90 })
          foo.setProps({ ms: 80 })
          foo.setProps({ ms: 110 })
          foo.setProps({ ms: 100 })
  
          return delay(110)
        }).then(() => {
          expect(foo.state()).to.be.deep.equal({
            delayed: 100,
          })
  
          foo.setProps({ ms: 10 })
          foo.setProps({ ms: 15 })
          foo.setProps({ ms: 100 })
          foo.setProps({ ms: 5 })
  
          return delay(110)
        }).then(() => {
          expect(foo.state()).to.be.deep.equal({
            delayed: 5,
          })
        })
      })
  
      it('should pass values on hot update', () => {
        const Root = withAsyncState(({ foo }) => ({ bar: foo * 1 }))(props => <Foo {...props} />)
        const Root2 = withAsyncState(({ foo }) => ({ bar: foo * 2 }))(props => <Foo {...props} />)
        const Root3 = withAsyncState(({ foo }) => ({ bar: foo * 3 }))(props => <Foo {...props} />)
  
        const root = mount(<Root foo={10} />)
        const foo = root.find(Foo)
  
        expect(foo.props()).to.be.deep.equal({
          foo: 10,
        })
  
        return delay().then(() => {
          expect(foo.props()).to.be.deep.equal({
            foo: 10,
            bar: 10,
          })
  
          imitateHotReloading(root, Root, Root2)
  
          expect(foo.props()).to.be.deep.equal({
            foo: 10,
            bar: 10,
          })
  
          return delay()
        }).then(() => {
          expect(foo.props()).to.be.deep.equal({
            foo: 10,
            bar: 20,
          })
  
          imitateHotReloading(root, Root, Root3)
  
          expect(foo.props()).to.be.deep.equal({
            foo: 10,
            bar: 20,
          })
  
          return delay()
        }).then(() => {
          expect(foo.props()).to.be.deep.equal({
            foo: 10,
            bar: 30,
          })
        })
      })
    })
  
    describe('options.pure', () => {
      let count = 0
  
      beforeEach(() => {
        count = 0
      })
  
      function Bar() {
        return (
          <div>{++count}</div>
        )
      }
  
      it('should NOT allow render when `pure` is `true` and props are same', () => {
        const decorator = withAsyncState(({ foo }) => ({ bar: foo * 10 }))
        const DecoratedBar = decorator(Bar)
  
        const foo = mount(<DecoratedBar foo={1} />)
  
        expect(foo.text()).to.be.equal('1')
  
        return delay()
          .then(() => {
            expect(foo.text()).to.be.equal('2')
  
            foo.setProps({ foo: 1 })
  
            expect(foo.text()).to.be.equal('2')
  
            return delay()
          })
          .then(() => {
            expect(foo.text()).to.be.equal('2')
  
            foo.setProps({ foo: 2 })
  
            expect(foo.text()).to.be.equal('3')
  
            return delay()
          })
          .then(() => {
            expect(foo.text()).to.be.equal('4')
  
            foo.setProps({ foo: 2 })
  
            expect(foo.text()).to.be.equal('4')
  
            return delay()
          })
          .then(() => {
            expect(foo.text()).to.be.equal('4')
  
            return delay()
          })
          .then(() => {
            expect(foo.text()).to.be.equal('4')
          })
      })
  
      it('should allow render when `pure` is `false` and props are same', () => {
        const decorator = withAsyncState(({ foo }) => ({ bar: foo * 10 }), {
          pure: false,
        })
  
        const DecoratedFoo = decorator(Bar)
  
        const foo = mount(<DecoratedFoo foo={1} />)
  
        expect(foo.text()).to.be.equal('1')
  
        return delay()
          .then(() => {
            expect(foo.text()).to.be.equal('2')
  
            foo.setProps({ foo: 1 })
  
            expect(foo.text()).to.be.equal('3')
  
            return delay()
          })
          .then(() => {
            expect(foo.text()).to.be.equal('4')
  
            foo.setProps({ foo: 2 })
  
            expect(foo.text()).to.be.equal('5')
  
            return delay()
          })
          .then(() => {
            expect(foo.text()).to.be.equal('6')
  
            foo.setProps({ foo: 2 })
  
            expect(foo.text()).to.be.equal('7')
  
            return delay()
          })
          .then(() => {
            expect(foo.text()).to.be.equal('8')
  
            return delay()
          })
          .then(() => {
            expect(foo.text()).to.be.equal('8')
          })
      })
    })
  
    describe('options.mergeProps', () => {
      it('should override mapped values with props by default', () => {
        const decorator = withAsyncState(({ foo, bar }) => ({
          foo: foo * 2,
          bar: bar * 3,
          baz: foo + bar,
        }))
  
        const DecoratedFoo = decorator(Foo)
        const root = mount(<DecoratedFoo foo={1} bar={2} />)
        const foo = root.find(Foo)
  
        expect(root.state()).to.be.deep.equal({})
        expect(foo.props()).to.be.deep.equal({
          foo: 1,
          bar: 2,
        })
  
        return delay().then(() => {
          expect(root.state()).to.be.deep.equal({
            foo: 2,
            bar: 6,
            baz: 3,
          })
  
          expect(foo.props()).to.be.deep.equal({
            foo: 1,
            bar: 2,
            baz: 3,
          })
  
          root.setProps({
            foo: 5,
            bar: 10,
          })
  
          expect(root.state()).to.be.deep.equal({
            foo: 2,
            bar: 6,
            baz: 3,
          })
  
          expect(foo.props()).to.be.deep.equal({
            foo: 5,
            bar: 10,
            baz: 3,
          })
  
          return delay()
        }).then(() => {
          expect(root.state()).to.be.deep.equal({
            foo: 10,
            bar: 30,
            baz: 15,
          })
  
          expect(foo.props()).to.be.deep.equal({
            foo: 5,
            bar: 10,
            baz: 15,
          })
        })
      })
  
      it('should use custom mergeProps', () => {
        const decorator = withAsyncState(({ foo, bar }) => ({
          foo: foo * 2,
          bar: bar * 3,
          quoz: foo + bar,
        }), {
          mergeProps(props, state) {
            return _.assign({}, props, state)
          },
        })
  
        const DecoratedFoo = decorator(Foo)
        const root = mount(<DecoratedFoo foo={1} bar={2} baz={3} />)
        const foo = root.find(Foo)
  
        expect(root.state()).to.be.deep.equal({})
        expect(foo.props()).to.be.deep.equal({
          foo: 1,
          bar: 2,
          baz: 3,
        })
  
        return delay().then(() => {
          expect(root.state()).to.be.deep.equal({
            foo: 2,
            bar: 6,
            quoz: 3,
          })
  
          expect(foo.props()).to.be.deep.equal({
            foo: 2,
            bar: 6,
            baz: 3,
            quoz: 3,
          })
  
          root.setProps({
            foo: 5,
            bar: 10,
            baz: 20,
          })
  
          expect(root.state()).to.be.deep.equal({
            foo: 2,
            bar: 6,
            quoz: 3,
          })
  
          expect(foo.props()).to.be.deep.equal({
            foo: 2,
            bar: 6,
            baz: 20,
            quoz: 3,
          })
  
          return delay()
        }).then(() => {
          expect(root.state()).to.be.deep.equal({
            foo: 10,
            bar: 30,
            quoz: 15,
          })
  
          expect(foo.props()).to.be.deep.equal({
            foo: 10,
            bar: 30,
            baz: 20,
            quoz: 15,
          })
        })
      })
    })
  
    describe('options.getInitialState', () => {
      it('should be called one time after mount', () => {
        const getInitialState = spy(() => ({}))
        const decorator = withAsyncState(() => ({}), { getInitialState })
  
        const DecoratedFoo = decorator(Foo)
  
        mount(<DecoratedFoo />)
  
        getInitialState.should.have.been.called.exactly(1)
      })
  
      it('should be called one time after SSR', () => {
        const getInitialState = spy(() => ({}))
        const decorator = withAsyncState(() => ({}), { getInitialState })
        const DecoratedFoo = decorator(Foo)
  
        renderToString(<DecoratedFoo />)
  
        getInitialState.should.have.been.called.exactly(1)
  
        renderToStaticMarkup(<DecoratedFoo />)
  
        getInitialState.should.have.been.called.exactly(2)
      })
  
      it('should pass initial values to during initial render and mapped values after delay', () => {
        const decorator = withAsyncState(({ rating }) => ({ percentage: (rating * 100) / 5 }), {
          getInitialState() {
            return {
              percentage: 0,
            }
          },
        })
  
        const DecoratedFoo = decorator(Foo)
  
        const root = mount(<DecoratedFoo rating={3} />)
        const foo = root.find(Foo)
  
        expect(foo.props()).to.be.deep.equal({
          rating: 3,
          percentage: 0,
        })
  
        return delay().then(() => {
          expect(foo.props()).to.be.deep.equal({
            rating: 3,
            percentage: 60,
          })
  
          root.setProps({
            rating: 4,
          })
  
          expect(foo.props()).to.be.deep.equal({
            rating: 4,
            percentage: 60,
          })
  
          return delay()
        }).then(() => {
          expect(foo.props()).to.be.deep.equal({
            rating: 4,
            percentage: 80,
          })
  
          root.setProps({
            rating: 2,
          })
  
          expect(foo.props()).to.be.deep.equal({
            rating: 2,
            percentage: 80,
          })
  
          return delay()
        }).then(() => {
          expect(foo.props()).to.be.deep.equal({
            rating: 2,
            percentage: 40,
          })
        })
      })
  
      it('should reinitialize and pass values on hot update', () => {
        const Root = withAsyncState(({ foo }) => ({ bar: foo * 1 }), {
          getInitialState() {
            return {
              bar: 1,
            }
          },
        })(props => <Foo {...props} />)
  
        const Root2 = withAsyncState(({ foo }) => ({ bar: foo * 2 }), {
          getInitialState() {
            return {
              bar: 2,
            }
          },
        })(props => <Foo {...props} />)
  
        const Root3 = withAsyncState(({ foo }) => ({ bar: foo * 3 }), {
          getInitialState() {
            return {
              bar: 3,
            }
          },
        })(props => <Foo {...props} />)
  
        const root = mount(<Root foo={10} />)
        const foo = root.find(Foo)
  
        expect(foo.props()).to.be.deep.equal({
          foo: 10,
          bar: 1,
        })
  
        return delay().then(() => {
          expect(foo.props()).to.be.deep.equal({
            foo: 10,
            bar: 10,
          })
  
          imitateHotReloading(root, Root, Root2)
  
          expect(foo.props()).to.be.deep.equal({
            foo: 10,
            bar: 2,
          })
  
          return delay()
        }).then(() => {
          expect(foo.props()).to.be.deep.equal({
            foo: 10,
            bar: 20,
          })
  
          imitateHotReloading(root, Root, Root3)
  
          expect(foo.props()).to.be.deep.equal({
            foo: 10,
            bar: 3,
          })
  
          return delay()
        }).then(() => {
          expect(foo.props()).to.be.deep.equal({
            foo: 10,
            bar: 30,
          })
        })
      })
    })
  
    describe('options.reinitializeOnPropsChange', () => {
      it('should initialize state once when `false`', () => {
        const getInitialState = spy(() => ({ percentage: 0 }))
  
        const decorator = withAsyncState(({ rating }) => ({
          percentage: (rating * 100) / 5,
        }), {
          getInitialState,
        })
  
        const DecoratedFoo = decorator(Foo)
  
        const root = mount(<DecoratedFoo rating={3} />)
        const foo = root.find(Foo)
  
        getInitialState.should.have.been.called.exactly(1)
  
        expect(foo.props()).to.be.deep.equal({
          rating: 3,
          percentage: 0,
        })
  
        return delay().then(() => {
          expect(foo.props()).to.be.deep.equal({
            rating: 3,
            percentage: 60,
          })
  
          root.setProps({
            rating: 4,
          })
  
          getInitialState.should.have.been.called.exactly(1)
  
          expect(foo.props()).to.be.deep.equal({
            rating: 4,
            percentage: 60,
          })
  
          return delay()
        }).then(() => {
          expect(foo.props()).to.be.deep.equal({
            rating: 4,
            percentage: 80,
          })
  
          root.setProps({
            rating: 2,
          })
  
          getInitialState.should.have.been.called.exactly(1)
  
          expect(foo.props()).to.be.deep.equal({
            rating: 2,
            percentage: 80,
          })
  
          return delay()
        }).then(() => {
          expect(foo.props()).to.be.deep.equal({
            rating: 2,
            percentage: 40,
          })
  
          getInitialState.should.have.been.called.exactly(1)
        })
      })
  
      it('should initialize state every time when props change when `true`', () => {
        const getInitialState = spy(() => ({ percentage: 0 }))
        const decorator = withAsyncState(({ rating }) => Promise.resolve({
          percentage: (rating * 100) / 5,
        }), {
          getInitialState, reinitializeOnPropsChange: true,
        })
  
        const DecoratedFoo = decorator(Foo)
  
        const root = mount(<DecoratedFoo rating={3} />)
        const foo = root.find(Foo)
  
        getInitialState.should.have.been.called.exactly(1)
  
        expect(foo.props()).to.be.deep.equal({
          rating: 3,
          percentage: 0,
        })
  
        return delay().then(() => {
          expect(foo.props()).to.be.deep.equal({
            rating: 3,
            percentage: 60,
          })
  
          root.setProps({
            rating: 4,
          })
  
          getInitialState.should.have.been.called.exactly(2)
  
          expect(foo.props()).to.be.deep.equal({
            rating: 4,
            percentage: 0,
          })
  
          return delay()
        }).then(() => {
          expect(foo.props()).to.be.deep.equal({
            rating: 4,
            percentage: 80,
          })
  
          root.setProps({
            rating: 2,
          })
  
          getInitialState.should.have.been.called.exactly(3)
  
          expect(foo.props()).to.be.deep.equal({
            rating: 2,
            percentage: 0,
          })
  
          return delay()
        }).then(() => {
          expect(foo.props()).to.be.deep.equal({
            rating: 2,
            percentage: 40,
          })
  
          getInitialState.should.have.been.called.exactly(3)
        })
      })
    })
  })
  