import '@testing-library/jest-dom'
import { act, cleanup, render, screen, waitFor } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import { mount } from 'enzyme'
import React from 'react'
import { OverlayTrigger } from '../OverlayTrigger'

const spy = jest.fn()

jest.mock('@react-aria/utils', () => {
  const originalModule = jest.requireActual('@react-aria/utils')

  return {
    __esModule: true,
    ...originalModule,
    useId: jest.fn().mockReturnValue('1'),
  }
})

const overlay = <div id="overlay">OVERLAY</div>
const nestedOverlay = <div id="nested-overlay">NESTED OVERLAY</div>
const trigger = <div id="trigger">TRIGGER</div>

const query = (string) => global.document.querySelector(string)
const queryOverlay = () => query('#overlay')
const queryNestedOverlay = () => query('#nested-overlay')
const queryTrigger = () => query('#trigger')

describe('OverlayTrigger', () => {
  let container
  beforeEach(() => {
    container = document.createElement('div')
    document.body.appendChild(container)
  })
  afterEach(() => {
    document.body.removeChild(container)
    container = null
    jest.clearAllMocks()
    cleanup()
  })

  describe('nested OverlayTriggers', function () {
    it('should open both overlays when trigger is clicked', async () => {
      render(
        <OverlayTrigger initialIsVisible={false} overlay={overlay}>
          <OverlayTrigger initialIsVisible={false} overlay={nestedOverlay}>
            {trigger}
          </OverlayTrigger>
        </OverlayTrigger>,
        container
      )

      await userEvent.click(queryTrigger())

      expect(queryOverlay()).toBeInTheDocument()
      expect(queryNestedOverlay()).toBeInTheDocument()
    })

    it('should open both overlays when ArrowDown is pressed on trigger', async () => {
      render(
        <OverlayTrigger initialIsVisible={false} overlay={overlay}>
          <OverlayTrigger initialIsVisible={false} overlay={nestedOverlay}>
            {trigger}
          </OverlayTrigger>
        </OverlayTrigger>,
        container
      )

      await userEvent.type(queryTrigger(), '[ArrowDown]')

      expect(queryOverlay()).toBeInTheDocument()
      expect(queryNestedOverlay()).toBeInTheDocument()
    })
  })

  describe('overlay initial presence', () => {
    it('is closed when initialIsVisible is false', () => {
      render(
        <OverlayTrigger initialIsVisible={false} overlay={overlay}>
          {trigger}
        </OverlayTrigger>,
        container
      )

      expect(queryOverlay()).toBeFalsy()
    })

    it('is open initialIsVisible is true', () => {
      render(
        <OverlayTrigger initialIsVisible={true} overlay={overlay}>
          {trigger}
        </OverlayTrigger>,
        container
      )

      expect(queryOverlay()).toBeTruthy()
    })
  })

  describe('overlay is about to open', () => {
    it('it opens, unless beforeShow returns false', async () => {
      render(
        <OverlayTrigger
          overlay={overlay}
          beforeShow={() => undefined}
          afterShow={spy}
        >
          {trigger}
        </OverlayTrigger>,
        container
      )
      queryTrigger().dispatchEvent(new MouseEvent('click'))

      await waitFor(() => expect(queryOverlay()).toBeTruthy())
      expect(spy).toBeCalledTimes(1)
    })

    it('remains closed, when beforeShow returns false', async () => {
      render(
        <OverlayTrigger
          overlay={overlay}
          beforeShow={() => false}
          afterShow={spy}
        >
          {trigger}
        </OverlayTrigger>,
        container
      )
      queryTrigger().dispatchEvent(new MouseEvent('click'))

      await waitFor(() => expect(queryOverlay()).toBeFalsy())
      expect(spy).toBeCalledTimes(0)
    })
  })

  describe('overlay is about to close', () => {
    it('remains open when beforeHide returns false', async () => {
      render(
        <OverlayTrigger
          initialIsVisible={true}
          overlay={overlay}
          beforeHide={() => false}
          afterHide={spy}
        >
          {trigger}
        </OverlayTrigger>,
        container
      )
      queryTrigger().dispatchEvent(new MouseEvent('click'))

      await waitFor(() => expect(queryOverlay()).toBeTruthy())
      expect(spy).toBeCalledTimes(0)
    })

    it('it closes, unless beforeHide returns false', async () => {
      render(
        <OverlayTrigger
          initialIsVisible={true}
          overlay={overlay}
          beforeHide={() => undefined}
          afterHide={spy}
        >
          {trigger}
        </OverlayTrigger>,
        container
      )

      queryTrigger().dispatchEvent(new MouseEvent('click'))

      await waitFor(() => expect(queryOverlay()).toBeFalsy())
      expect(spy).toBeCalledTimes(1)
    })
  })
  describe('clicking outside and lifecycles', () => {
    describe('when closed', () => {
      it('does not call beforeShow nor afterShow', async () => {
        render(
          <React.Fragment>
            <OverlayTrigger
              initialIsVisible={false}
              overlay={overlay}
              beforeShow={spy}
              afterShow={spy}
            >
              {trigger}
            </OverlayTrigger>
            <div id="outside" />
          </React.Fragment>,
          container
        )
        query('#outside').dispatchEvent(new MouseEvent('click'))

        await waitFor(() => expect(queryOverlay()).toBeFalsy())
        expect(spy).toBeCalledTimes(0)
      })
    })

    describe('when open', () => {
      it('remains open when beforeHide returns promise of false', async () => {
        render(
          <React.Fragment>
            <OverlayTrigger
              initialIsVisible={true}
              overlay={overlay}
              beforeHide={() => Promise.resolve(false)}
              afterHide={spy}
            >
              {trigger}
            </OverlayTrigger>
            <div id="outside" />
          </React.Fragment>,
          container
        )
        query('#outside').dispatchEvent(new MouseEvent('click'))

        await waitFor(() => expect(queryOverlay()).toBeTruthy())
        expect(spy).toBeCalledTimes(0)
      })

      it('it closes, unless beforeHide returns false', async () => {
        render(
          <React.Fragment>
            <OverlayTrigger
              initialIsVisible={true}
              overlay={overlay}
              beforeHide={spy}
              afterHide={spy}
            >
              {trigger}
            </OverlayTrigger>
            <div id="outside" />
          </React.Fragment>,
          container
        )
        query('#outside').dispatchEvent(new MouseEvent('click'))

        await waitFor(() => expect(queryOverlay()).toBeFalsy())
        expect(spy).toBeCalledTimes(2)
      })

      it('should close if the trigger becomes "none"', () => {
        let wrapper

        act(() => {
          wrapper = mount(
            <OverlayTrigger
              initialIsVisible={true}
              overlay={overlay}
              trigger={['click', 'hover']}
            >
              {trigger}
            </OverlayTrigger>,
            container
          )
        })

        wrapper.setProps({ trigger: 'none', overlay, initialIsVisible: true })

        expect(queryOverlay()).toBeFalsy()
      })

      it('should close if one of the triggers becomes "none"', () => {
        let wrapper

        act(() => {
          wrapper = mount(
            <OverlayTrigger initialIsVisible={true} overlay={overlay}>
              {trigger}
            </OverlayTrigger>,
            container
          )
        })

        wrapper.setProps({
          trigger: ['click', 'none'],
          overlay,
          initialIsVisible: true,
        })

        expect(queryOverlay()).toBeFalsy()
      })
    })
  })

  describe('ARIA attributes', () => {
    // use overlay without id to test ARIA attributes properly
    const overlayWithoutId = <div data-overlay>OVERLAY</div>
    const queryOverlayWithoutId = () => query('[data-overlay]')
    const queryOverlayWrapper = () =>
      screen.queryByTestId('core-overlay-trigger-overlay-wrapper')

    it('should set no role by default', () => {
      render(
        <OverlayTrigger initialIsVisible overlay={overlayWithoutId}>
          {trigger}
        </OverlayTrigger>,
        container
      )

      expect(queryOverlayWithoutId().getAttribute('role')).toEqual(null)
      expect(queryTrigger().getAttribute('aria-haspopup')).toEqual(null)
    })

    describe('when passA11yPropsToOverlay is false', function () {
      describe('dialog', () => {
        it('should set aria to trigger and overlayWrapper', () => {
          render(
            <OverlayTrigger
              initialIsVisible
              overlay={overlayWithoutId}
              role="dialog"
              passA11yPropsToOverlay={false}
            >
              {trigger}
            </OverlayTrigger>,
            container
          )

          expect(queryOverlayWrapper().getAttribute('role')).toEqual('dialog')
          expect(queryTrigger().getAttribute('aria-haspopup')).toEqual('dialog')
          expect(queryTrigger().getAttribute('aria-expanded')).toEqual('true')
          expect(queryTrigger().getAttribute('aria-controls')).toEqual('1')
          expect(queryOverlayWrapper().getAttribute('id')).toEqual('1')
        })
      })

      describe('listbox', () => {
        it('should set aria to trigger and overlayWrapper', () => {
          render(
            <OverlayTrigger
              initialIsVisible
              overlay={overlayWithoutId}
              role="listbox"
              passA11yPropsToOverlay={false}
            >
              {trigger}
            </OverlayTrigger>,
            container
          )

          expect(queryOverlayWrapper().getAttribute('role')).toEqual('listbox')
          expect(queryTrigger().getAttribute('aria-haspopup')).toEqual(
            'listbox'
          )
          expect(queryTrigger().getAttribute('aria-expanded')).toEqual('true')
          expect(queryTrigger().getAttribute('aria-controls')).toEqual('1')
          expect(queryOverlayWrapper().getAttribute('id')).toEqual('1')
        })
      })

      describe('menu', () => {
        it('should set aria to trigger and overlayWrapper', () => {
          render(
            <OverlayTrigger
              initialIsVisible
              overlay={overlayWithoutId}
              role="menu"
              passA11yPropsToOverlay={false}
            >
              {trigger}
            </OverlayTrigger>,
            container
          )

          expect(queryOverlayWrapper().getAttribute('role')).toEqual('menu')
          expect(queryTrigger().getAttribute('aria-haspopup')).toEqual('menu')
          expect(queryTrigger().getAttribute('aria-expanded')).toEqual('true')
          expect(queryTrigger().getAttribute('aria-controls')).toEqual('1')
          expect(queryOverlayWrapper().getAttribute('id')).toEqual('1')
        })
      })
    })

    describe('when passA11yPropsToOverlay is true', function () {
      describe('dialog', () => {
        it('should set aria to trigger and overlay', () => {
          render(
            <OverlayTrigger
              initialIsVisible
              overlay={overlayWithoutId}
              role="dialog"
              passA11yPropsToOverlay
            >
              {trigger}
            </OverlayTrigger>,
            container
          )

          expect(queryOverlayWithoutId().getAttribute('role')).toEqual('dialog')
          expect(queryTrigger().getAttribute('aria-haspopup')).toEqual('dialog')
          expect(queryTrigger().getAttribute('aria-expanded')).toEqual('true')
          expect(queryTrigger().getAttribute('aria-controls')).toEqual('1')
          expect(queryOverlayWithoutId().getAttribute('id')).toEqual('1')
        })
      })

      describe('listbox', () => {
        it('should set aria to trigger and overlay', () => {
          render(
            <OverlayTrigger
              initialIsVisible
              overlay={overlayWithoutId}
              role="listbox"
              passA11yPropsToOverlay
            >
              {trigger}
            </OverlayTrigger>,
            container
          )

          expect(queryOverlayWithoutId().getAttribute('role')).toEqual(
            'listbox'
          )
          expect(queryTrigger().getAttribute('aria-haspopup')).toEqual(
            'listbox'
          )
          expect(queryTrigger().getAttribute('aria-expanded')).toEqual('true')
          expect(queryTrigger().getAttribute('aria-controls')).toEqual('1')
          expect(queryOverlayWithoutId().getAttribute('id')).toEqual('1')
        })
      })

      describe('menu', () => {
        it('should set aria to trigger and overlay', () => {
          render(
            <OverlayTrigger
              initialIsVisible
              overlay={overlayWithoutId}
              role="menu"
              passA11yPropsToOverlay
            >
              {trigger}
            </OverlayTrigger>,
            container
          )

          expect(queryOverlayWithoutId().getAttribute('role')).toEqual('menu')
          expect(queryTrigger().getAttribute('aria-haspopup')).toEqual('menu')
          expect(queryTrigger().getAttribute('aria-expanded')).toEqual('true')
          expect(queryTrigger().getAttribute('aria-controls')).toEqual('1')
          expect(queryOverlayWithoutId().getAttribute('id')).toEqual('1')
        })
      })

      it('should not override "role" or "id" of the overlay element if provided', () => {
        const overlayWithAria = (
          <div data-overlay role="grid" id="qwerty">
            Overlay
          </div>
        )
        render(
          <OverlayTrigger
            initialIsVisible
            overlay={overlayWithAria}
            passA11yPropsToOverlay
          >
            {trigger}
          </OverlayTrigger>,
          container
        )

        expect(query('#qwerty').getAttribute('role')).toEqual('grid')
        expect(query('#qwerty').getAttribute('id')).toEqual('qwerty')
      })
    })

    it('should not override "aria-haspopup" of the trigger element if provided', () => {
      const triggerWithAria = (
        <div id="trigger" aria-haspopup="grid">
          Trigger
        </div>
      )
      render(
        <OverlayTrigger initialIsVisible overlay={overlayWithoutId}>
          {triggerWithAria}
        </OverlayTrigger>,
        container
      )

      expect(queryTrigger().getAttribute('aria-haspopup')).toEqual('grid')
    })
  })
})
