/**
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */

import {
  clearPointers,
  createEventTarget,
  describeWithPointerEvent,
  setPointerEvent,
} from 'dom-event-testing-library'
import * as React from 'react'
import * as ReactDOM from 'react-dom'
import { act } from 'react-dom/test-utils'

import { testOnly_resetActiveModality } from '../../modality/index'
import useHover from '..'

function createRoot(rootNode) {
  return {
    render(element) {
      ReactDOM.render(element, rootNode)
    },
  }
}

describeWithPointerEvent('useHover', (hasPointerEvents) => {
  let root
  let rootNode

  beforeEach(() => {
    setPointerEvent(hasPointerEvents)
    rootNode = document.createElement('div')
    document.body.appendChild(rootNode)
    root = createRoot(rootNode)
  })

  afterEach(() => {
    root.render(null)
    document.body.removeChild(rootNode)
    rootNode = null
    root = null
    testOnly_resetActiveModality()
    // make sure all tests reset state machine tracking pointers on the mock surface
    clearPointers()
  })

  describe('contain', () => {
    let onHoverChange, onHoverStart, onHoverUpdate, onHoverEnd, ref, childRef

    const componentInit = () => {
      onHoverChange = jest.fn()
      onHoverStart = jest.fn()
      onHoverUpdate = jest.fn()
      onHoverEnd = jest.fn()
      ref = React.createRef()
      childRef = React.createRef()
      const Component = () => {
        useHover(ref, {
          onHoverChange,
          onHoverStart,
          onHoverUpdate,
          onHoverEnd,
        })
        useHover(childRef, { contain: true })
        return (
          <div ref={ref}>
            <div ref={childRef} />
          </div>
        )
      }
      act(() => {
        root.render(<Component />)
      })
    }

    test('contains the hover gesture', () => {
      componentInit()
      const target = createEventTarget(ref.current)
      const child = createEventTarget(childRef.current)
      act(() => {
        target.pointerover()
        target.pointerout()
        child.pointerover()
      })
      expect(onHoverEnd).toBeCalled()
      act(() => {
        child.pointerout()
      })
      expect(onHoverStart).toBeCalled()
    })
  })

  describe('disabled', () => {
    let onHoverChange, onHoverStart, onHoverUpdate, onHoverEnd, ref

    const componentInit = () => {
      onHoverChange = jest.fn()
      onHoverStart = jest.fn()
      onHoverUpdate = jest.fn()
      onHoverEnd = jest.fn()
      ref = React.createRef()
      const Component = () => {
        useHover(ref, {
          disabled: true,
          onHoverChange,
          onHoverStart,
          onHoverUpdate,
          onHoverEnd,
        })
        return <div ref={ref} />
      }
      act(() => {
        root.render(<Component />)
      })
    }

    test('does not call callbacks', () => {
      componentInit()
      const target = createEventTarget(ref.current)
      act(() => {
        target.pointerover()
        target.pointerout()
      })
      expect(onHoverChange).not.toBeCalled()
      expect(onHoverStart).not.toBeCalled()
      expect(onHoverUpdate).not.toBeCalled()
      expect(onHoverEnd).not.toBeCalled()
    })
  })

  describe('onHoverStart', () => {
    let onHoverStart, ref

    const componentInit = () => {
      onHoverStart = jest.fn()
      ref = React.createRef()
      const Component = () => {
        useHover(ref, { onHoverStart })
        return <div ref={ref} />
      }
      act(() => {
        root.render(<Component />)
      })
    }

    test('is called for mouse pointers', () => {
      componentInit()
      const target = createEventTarget(ref.current)
      act(() => {
        target.pointerover({ pointerType: 'mouse' })
      })
      expect(onHoverStart).toBeCalledTimes(1)
    })

    test('is not called for touch pointers', () => {
      componentInit()
      const target = createEventTarget(ref.current)
      act(() => {
        target.pointerdown({ pointerType: 'touch' })
        target.pointerup({ pointerType: 'touch' })
      })
      expect(onHoverStart).not.toBeCalled()
    })

    test('is called if a mouse pointer is used after a touch pointer', () => {
      componentInit()
      const target = createEventTarget(ref.current)
      act(() => {
        target.pointerdown({ pointerType: 'touch' })
        target.pointerup({ pointerType: 'touch' })
        target.pointerover({ pointerType: 'mouse' })
      })
      expect(onHoverStart).toBeCalledTimes(1)
    })
  })

  describe('onHoverChange', () => {
    let onHoverChange, ref

    const componentInit = () => {
      onHoverChange = jest.fn()
      ref = React.createRef()
      const Component = () => {
        useHover(ref, { onHoverChange })
        return <div ref={ref} />
      }
      act(() => {
        root.render(<Component />)
      })
    }

    test('is called for mouse pointers', () => {
      componentInit()
      const target = createEventTarget(ref.current)
      act(() => {
        target.pointerover()
      })
      expect(onHoverChange).toBeCalledTimes(1)
      expect(onHoverChange).toBeCalledWith(true)
      act(() => {
        target.pointerout()
      })
      expect(onHoverChange).toBeCalledTimes(2)
      expect(onHoverChange).toBeCalledWith(false)
    })

    test('is not called for touch pointers', () => {
      componentInit()
      const target = createEventTarget(ref.current)
      act(() => {
        target.pointerdown({ pointerType: 'touch' })
        target.pointerup({ pointerType: 'touch' })
      })
      expect(onHoverChange).not.toBeCalled()
    })
  })

  describe('onHoverEnd', () => {
    let onHoverEnd, ref, childRef

    const componentInit = () => {
      onHoverEnd = jest.fn()
      ref = React.createRef()
      childRef = React.createRef()
      const Component = () => {
        useHover(ref, { onHoverEnd })
        return (
          <div ref={ref}>
            <div ref={childRef} />
          </div>
        )
      }
      act(() => {
        root.render(<Component />)
      })
    }

    test('is called for mouse pointers', () => {
      componentInit()
      const target = createEventTarget(ref.current)
      act(() => {
        target.pointerover()
        target.pointerout()
      })
      expect(onHoverEnd).toBeCalledTimes(1)
    })

    test('is not called for touch pointers', () => {
      componentInit()
      const target = createEventTarget(ref.current)
      act(() => {
        target.pointerdown({ pointerType: 'touch' })
        target.pointerup({ pointerType: 'touch' })
      })
      expect(onHoverEnd).not.toBeCalled()
    })

    test('is not called when entering children of the target', () => {
      componentInit()
      const target = createEventTarget(ref.current)
      const child = createEventTarget(childRef.current)
      act(() => {
        target.pointerover()
        target.pointerout({ relatedTarget: childRef.current })
        child.pointerover({ relatedTarget: target.node })
      })
      expect(onHoverEnd).not.toBeCalled()
    })
  })

  describe('onHoverUpdate', () => {
    test('is called after the active pointer moves"', () => {
      const onHoverUpdate = jest.fn()
      const ref = React.createRef()
      const Component = () => {
        useHover(ref, { onHoverUpdate })
        return <div ref={ref} />
      }
      act(() => {
        root.render(<Component />)
      })

      const target = createEventTarget(ref.current)
      act(() => {
        target.pointerover()
        target.pointerhover({ x: 0, y: 0 })
        target.pointerhover({ x: 1, y: 1 })
      })
      expect(onHoverUpdate).toBeCalledTimes(2)
    })
  })

  describe('repeat use', () => {
    let onHoverChange, onHoverStart, onHoverUpdate, onHoverEnd, ref

    const componentInit = () => {
      onHoverChange = jest.fn()
      onHoverStart = jest.fn()
      onHoverUpdate = jest.fn()
      onHoverEnd = jest.fn()
      ref = React.createRef()
      const Component = () => {
        useHover(ref, {
          onHoverChange,
          onHoverStart,
          onHoverUpdate,
          onHoverEnd,
        })
        return <div ref={ref} />
      }
      act(() => {
        root.render(<Component />)
      })
    }

    test('callbacks are called each time', () => {
      componentInit()
      const target = createEventTarget(ref.current)
      act(() => {
        target.pointerover()
        target.pointerhover({ x: 1, y: 1 })
        target.pointerout()
      })
      expect(onHoverStart).toBeCalledTimes(1)
      expect(onHoverUpdate).toBeCalledTimes(1)
      expect(onHoverEnd).toBeCalledTimes(1)
      expect(onHoverChange).toBeCalledTimes(2)
      act(() => {
        target.pointerover()
        target.pointerhover({ x: 1, y: 1 })
        target.pointerout()
      })
      expect(onHoverStart).toBeCalledTimes(2)
      expect(onHoverUpdate).toBeCalledTimes(2)
      expect(onHoverEnd).toBeCalledTimes(2)
      expect(onHoverChange).toBeCalledTimes(4)
    })
  })
})
