import { render, screen } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import { mount } from 'enzyme'
import React from 'react'
import { isPromise, useDelayedCallback, wrapPromise } from '../DelayedCallback'

const DELAY = 10

const Component = 'button' //

const Enhanced = ({ after, before, delay }) => {
  const delayed = useDelayedCallback({ after, before, delay })

  return <Component onClick={delayed} />
}

describe('isPromise', () => {
  it('returns true if input is a promise object', () => {
    expect(isPromise(new Promise(() => {}))).toBe(true)
    expect(isPromise(0)).toBe(false)
    expect(isPromise('')).toBe(false)
    expect(isPromise(false)).toBe(false)
    expect(isPromise([])).toBe(false)
    expect(isPromise({})).toBe(false)
  })
})

describe('wrapPromise', () => {
  it('passes through if input is a promise', () => {
    wrapPromise(new Promise((resolve) => resolve('foo'))).then((value) =>
      expect(value).toBe('foo')
    )
  })

  it('resolves to provided input if input is a boolean', () => {
    wrapPromise(true).then((value) => expect(value).toBe(true))
    wrapPromise(false).then((value) => expect(value).toBe(false))
  })

  it('thens to true if input is not a promise or boolean', () => {
    wrapPromise('').then((value) => expect(value).toBe(true))
    wrapPromise({}).then((value) => expect(value).toBe(true))
    wrapPromise([]).then((value) => expect(value).toBe(true))
  })
})

describe('useDelayedCallback', () => {
  describe('api', () => {
    it('provides a delayed function', () => {
      const after = jest.fn()
      const before = jest.fn()
      const delay = DELAY
      const dom = mount(<Enhanced {...{ after, before, delay }} />)
      const onClick = dom.find(Component).prop('onClick')
      expect(typeof onClick).toBe('function')
    })
  })

  describe('after callback', () => {
    let after, before
    beforeEach(() => {
      after = jest.fn()
      before = jest.fn()
      jest.useFakeTimers()
    })

    afterEach(() => {
      jest.runOnlyPendingTimers()
      jest.useRealTimers()
    })

    // remove delay to make it work with FakeTimers
    const user = userEvent.setup({ delay: null })

    it('gets invoked after the delay has elapsed', async () => {
      render(<Enhanced {...{ after, before, delay: DELAY }} />)

      await user.click(screen.getByRole('button'))
      expect(after).not.toHaveBeenCalled()

      jest.advanceTimersByTime(DELAY)
      expect(after).toHaveBeenCalled()
    })

    it('should not call after if component is unmounted', async () => {
      const { unmount } = render(
        <Enhanced {...{ after, before, delay: DELAY }} />
      )

      await user.click(screen.getByRole('button'))
      unmount()

      jest.advanceTimersByTime(DELAY)
      expect(after).not.toHaveBeenCalled()
    })
  })

  describe('before callback', () => {
    it('gets invoked immediately', () => {
      const after = jest.fn()
      const before = jest.fn()
      const delay = DELAY
      const dom = mount(<Enhanced {...{ after, before, delay }} />)
      dom.simulate('click')
      expect(before).toHaveBeenCalled()
    })

    it('will not call the after callback when before returns false', (done) => {
      const after = jest.fn()
      const before = jest.fn(() => false)
      const delay = DELAY
      mount(<Enhanced {...{ after, before, delay }} />)
      setTimeout(() => {
        expect(after).toHaveBeenCalledTimes(0)
        done()
      }, DELAY * 2)
    })
  })
})
