import sinon from 'sinon'
import { PropTypes } from 'prop-types'

import CustomPropTypes, { requireable } from '../CustomPropTypes'

describe('CustomPropTypes', () => {
  beforeEach(() => {
    sinon.stub(console, 'error')
  })

  afterEach(() => {
    console.error.restore()
  })

  let counter = 0

  function checkProp (propTypes, propName, value) {
    // PropTypes only logs the first warning for each component. To prevent
    // this, pass a different component name on every call to checkPropTypes.
    // https://github.com/facebook/react/issues/7047#issuecomment-228614964
    counter += 1
    PropTypes.checkPropTypes(propTypes, { [propName]: value }, propName, `Component${counter}`)
  }

  it('exports a default object', () => {
    expect(CustomPropTypes).to.be.an('object')
  })

  for (const name of ['attemptHistory', 'higherOrderFunction', 'numberInRange', 'selectors']) {
    it(`includes '${name}' validator`, () => {
      expect(CustomPropTypes[name]).to.be.a('function')
    })
  }

  describe('higherOrderFunction', () => {
    const { higherOrderFunction } = CustomPropTypes

    it('returns an error for non function props', () => {
      const result = higherOrderFunction({ lolwut: true }, 'lolwut')
      expect(result).to.be.an.instanceof(Error)
    })

    it('returns an error for non higher order function props', () => {
      const props = { lolwut () {} }
      const result = higherOrderFunction(props, 'lolwut')
      expect(result).to.be.an.instanceof(Error)
    })

    it('returns undefined for higher order function props', () => {
      const props = { lolwut () { return Function.prototype } }
      const result = higherOrderFunction(props, 'lolwut')
      expect(result).to.be.undefined()
    })
  })

  describe('requireable', () => {
    it('exports a named requireable utility function', () => {
      expect(requireable).to.be.a('function')
    })

    it('returns a bound function', () => {
      expect(requireable()).to.be.a('function')
    })

    it('attaches a bound function on the `isRequired` property', () => {
      expect(requireable().isRequired).to.be.a('function')
    })

    it('does not invoke the passed function with no props present', () => {
      const spy = sinon.spy()
      requireable(spy)({}, 'lolwut')
      expect(spy).to.not.have.been.called()
    })

    it('invokes the passed function with props present', () => {
      const spy = sinon.spy()
      requireable(spy)({ lolwut: true }, 'lolwut')
      expect(spy).to.have.been.calledOnce()
    })
  })

  describe('numberInRange', () => {
    const propTypes = {
      percentage: CustomPropTypes.numberInRange(-1, 1)
    }

    it('returns an error if prop is not a number', () => {
      checkProp(propTypes, 'percentage', 'foo')
      expect(console.error).to.have.been.calledOnce()
    })

    it('returns an error if prop is less than range start', () => {
      checkProp(propTypes, 'percentage', -2)
      expect(console.error).to.have.been.calledOnce()
    })

    it('returns an error if prop is greater than range end', () => {
      checkProp(propTypes, 'percentage', 1.1)
      expect(console.error).to.have.been.calledOnce()
    })

    it('does not return an error if prop is within range', () => {
      checkProp(propTypes, 'percentage', 0.5)
      expect(console.error).not.to.have.been.called()
    })
  })

  describe('recordId', () => {
    const propTypes = {
      quizId: CustomPropTypes.recordId
    }

    it('returns an error for numbers', () => {
      checkProp(propTypes, 'quizId', 1)
      expect(console.error).to.have.been.calledOnce()
    })

    it('returns an error for "0"', () => {
      checkProp(propTypes, 'quizId', '0')
      expect(console.error).to.have.been.calledOnce()
    })

    for (const value of ['1', '10', '101']) {
      it(`does not return an error for "${value}"`, () => {
        checkProp(propTypes, 'quizId', value)
        expect(console.error).not.to.have.been.called()
      })
    }
  })
})
