import React from 'react'
import { act, fireEvent, render, screen, waitFor } from '@testing-library/react'
import { beforeAll, describe, expect, it, vi } from 'vitest'

import Popover from './Popover'
import styles from './_popover.module.scss'
import Button from '../Button/Button'
import { type PopoverProps } from './Popover'

describe('Popover', () => {
  beforeAll(() => {
    // Reason: JSDOM doesn't provide `matchMedia` by default.
    // `useMediaQuery` relies on it, so we polyfill for Popover tests.
    if (
      typeof window !== 'undefined' &&
      typeof window.matchMedia !== 'function'
    ) {
      Object.defineProperty(window, 'matchMedia', {
        writable: true,
        value: (query: string) => ({
          matches: true,
          media: query,
          onchange: null,
          addEventListener: () => undefined,
          removeEventListener: () => undefined,
          // Legacy API (some libs still call these)
          addListener: () => undefined,
          removeListener: () => undefined,
          dispatchEvent: () => false,
        }),
      })
    }
  })

  const mockOnClickOutside = vi.fn<() => void>()
  const defaultProps = {
    children: ({
      setVisible,
      visible,
    }: {
      setVisible: (visible: boolean) => void
      visible: boolean
    }) => <Button onClick={() => setVisible(!visible)}>Click Me</Button>,
    popoverContent: <div>Popover Content</div>,
    position: 'bottom' as const,
    onClickOutside: mockOnClickOutside,
  }

  it('renders the trigger button', () => {
    render(<Popover {...defaultProps} />)
    expect(screen.getByTestId('popover-trigger')).toBeInTheDocument()
    expect(screen.getByRole('button', { name: 'Click Me' })).toBeInTheDocument()
  })

  it('shows popover content when trigger is clicked', async () => {
    render(<Popover {...defaultProps} />)
    const button = screen.getByRole('button', { name: 'Click Me' })
    fireEvent.click(button)

    expect(screen.getByTestId('popover-container')).toBeInTheDocument()
  })

  it('hides popover content when clicking outside', async () => {
    render(<Popover {...defaultProps} />)
    const button = screen.getByRole('button', { name: 'Click Me' })
    fireEvent.click(button)
    expect(screen.getByTestId('popover-container')).toBeInTheDocument()

    fireEvent.click(document.body)

    await waitFor(() => {
      expect(screen.queryByTestId('popover-container')).not.toBeInTheDocument()
      expect(mockOnClickOutside).toHaveBeenCalled()
    })
  })

  it('renders with header when header prop is provided', async () => {
    const props = {
      ...defaultProps,
      header: { headerText: 'Popover Header' },
    }
    render(<Popover {...props} />)

    const button = screen.getByRole('button', { name: 'Click Me' })
    fireEvent.click(button)

    expect(screen.getByTestId('popover-container')).toBeInTheDocument()

    expect(screen.getByText('Popover Header')).toBeInTheDocument()
    expect(screen.getByText('Popover Content')).toBeInTheDocument()
  })

  it('applies noPadding class when noPadding prop is true', async () => {
    const props = {
      ...defaultProps,
      noPadding: true,
    }
    render(<Popover {...props} />)

    const button = screen.getByRole('button', { name: 'Click Me' })
    fireEvent.click(button)

    expect(screen.getByTestId('popover-container')).toBeInTheDocument()

    const container = screen.getByTestId('popover-container')
    expect(container).toHaveClass(styles.noPadding)
  })

  it('does not open when disabled prop is true', async () => {
    const props = {
      ...defaultProps,
      disabled: true,
    }
    render(<Popover {...props} />)

    const button = screen.getByRole('button', { name: 'Click Me' })
    fireEvent.click(button)

    expect(screen.queryByTestId('popover-container')).not.toBeInTheDocument()
    expect(screen.queryByText('Popover Content')).not.toBeInTheDocument()
  })

  it('closes popover when scrolling if disableCloseOnScroll is false', async () => {
    const props = {
      ...defaultProps,
      disableCloseOnScroll: false,
    }
    render(<Popover {...props} />)

    const button = screen.getByRole('button', { name: 'Click Me' })
    fireEvent.click(button)

    expect(screen.getByTestId('popover-container')).toBeInTheDocument()

    act(() => {
      window.dispatchEvent(new Event('scroll'))
    })

    await waitFor(() => {
      expect(screen.queryByTestId('popover-container')).not.toBeInTheDocument()
    })
  })

  // TODO: This test only validates the onOpenChange scroll handler, not the useDismiss ancestorScroll config. Consider adding a test that verifies ancestorScroll is properly set based on disableCloseOnScroll.
  it('keeps popover open when scrolling if disableCloseOnScroll is true', async () => {
    const props = {
      ...defaultProps,
      disableCloseOnScroll: true,
    }
    render(<Popover {...props} />)

    const button = screen.getByRole('button', { name: 'Click Me' })
    fireEvent.click(button)

    expect(screen.getByTestId('popover-container')).toBeInTheDocument()

    act(() => {
      window.dispatchEvent(new Event('scroll'))
    })

    await waitFor(() => {
      expect(screen.getByTestId('popover-container')).toBeInTheDocument()
    })
  })

  it('renders with custom className', async () => {
    const props = {
      ...defaultProps,
      className: 'custom-class',
    }
    render(<Popover {...props} />)

    const button = screen.getByRole('button', { name: 'Click Me' })
    fireEvent.click(button)

    expect(screen.getByTestId('popover-container')).toBeInTheDocument()

    const container = screen.getByTestId('popover-container')
    expect(container).toHaveClass('custom-class')
  })

  it('renders with function as popoverContent', async () => {
    const props = {
      ...defaultProps,
      popoverContent: ({
        setVisible,
      }: {
        setVisible: (visible: boolean) => void
      }) => (
        <div>
          <p>Dynamic Content</p>
          <button onClick={() => setVisible(false)}>Close</button>
        </div>
      ),
    }

    render(<Popover {...props} />)

    const button = screen.getByRole('button', { name: 'Click Me' })
    fireEvent.click(button)

    expect(screen.getByTestId('popover-container')).toBeInTheDocument()

    expect(screen.getByText('Dynamic Content')).toBeInTheDocument()

    const closeButton = screen.getByRole('button', { name: 'Close' })
    fireEvent.click(closeButton)

    await waitFor(() => {
      expect(screen.queryByTestId('popover-container')).not.toBeInTheDocument()
      expect(screen.queryByText('Dynamic Content')).not.toBeInTheDocument()
    })
  })
  const positions: PopoverProps['position'][] = [
    'top',
    'right',
    'bottom',
    'left',
    'top-start',
    'top-end',
    'right-start',
    'right-end',
    'bottom-start',
    'bottom-end',
    'left-start',
    'left-end',
  ]

  positions.forEach((position) => {
    it(`renders with position ${position}`, async () => {
      const props = {
        ...defaultProps,
        position,
      }
      render(<Popover {...props} />)

      const button = screen.getByRole('button', { name: 'Click Me' })
      fireEvent.click(button)

      expect(screen.getByTestId('popover-floating')).toBeInTheDocument()
      expect(screen.getByTestId('popover-floating')).toHaveAttribute(
        'data-placement',
        position,
      )
    })
  })

  it('renders with maxWidth prop', async () => {
    const props = {
      ...defaultProps,
      maxWidth: '500px',
    }
    render(<Popover {...props} />)

    const button = screen.getByRole('button', { name: 'Click Me' })
    fireEvent.click(button)

    const floatingElement = screen.getByTestId('popover-floating')

    expect(floatingElement).toHaveStyle({ maxWidth: '500px' })
  })
})

/**
 * Portal-root resolution tests for the `resolveAppendTo` refactor.
 * We isolate these via dynamic imports and module mocks to avoid depending on
 * Floating UI internals / DOM layout APIs.
 */
describe('Popover portal root resolution', () => {
  function mockUseMediaQuery() {
    vi.doMock('../../hooks/responsiveHooks', () => ({
      useMediaQuery: () => true,
    }))
  }

  function mockFloatingUi() {
    vi.doMock('@floating-ui/react', () => {
      const reference = { current: null as HTMLElement | null }
      const floating = { current: null as HTMLElement | null }

      return {
        FloatingPortal: ({
          children,
          root,
        }: {
          children: React.ReactNode
          root: unknown
        }) => {
          const rootId =
            root instanceof HTMLElement
              ? (root.getAttribute('data-testid') ?? root.tagName)
              : root === document.documentElement
                ? 'HTML'
                : root === document.body
                  ? 'BODY'
                  : root
                    ? 'other'
                    : 'null'

          return (
            <div data-testid='floating-portal' data-root-id={rootId}>
              {children}
            </div>
          )
        },
        useFloating: () => ({
          refs: {
            setReference: (node: HTMLElement | null) => {
              reference.current = node
            },
            setFloating: (node: HTMLElement | null) => {
              floating.current = node
            },
            reference,
            floating,
          },
          floatingStyles: {},
          context: { placement: 'bottom' },
        }),
        useInteractions: () => ({
          getReferenceProps: () => ({}),
          getFloatingProps: () => ({}),
        }),
        useTransitionStyles: () => ({
          // Always mounted so we can observe the resolved portal root.
          isMounted: true,
          styles: {},
        }),
        // Interaction hooks are irrelevant for these tests.
        useClick: () => ({}),
        useDismiss: () => ({}),
        useRole: () => ({}),
        autoUpdate: () => undefined,
        offset: () => ({}),
        shift: () => ({}),
        flip: () => ({}),
        autoPlacement: () => ({}),
      }
    })
  }

  const baseProps = {
    children: ({
      setVisible,
      visible,
    }: {
      setVisible: (visible: boolean) => void
      visible: boolean
    }) => (
      <button type='button' onClick={() => setVisible(!visible)}>
        Toggle
      </button>
    ),
    popoverContent: <div>Content</div>,
    position: 'bottom' as const,
  }

  it("uses the trigger's parent when appendTo is 'parent' (after ref is set)", async () => {
    vi.resetModules()
    mockUseMediaQuery()
    mockFloatingUi()

    const DynamicPopover = (await import('./Popover')).default

    const { getByRole, getByTestId } = render(
      <div data-testid='wrapper'>
        <DynamicPopover {...baseProps} appendTo='parent' />
      </div>,
    )

    // First click triggers a re-render after refs are attached, so resolveAppendTo can see parentElement.
    fireEvent.click(getByRole('button', { name: 'Toggle' }))

    expect(getByTestId('floating-portal').getAttribute('data-root-id')).toBe(
      'wrapper',
    )
  })

  it('falls back to document.documentElement when document.body is null', async () => {
    vi.resetModules()
    mockUseMediaQuery()
    mockFloatingUi()

    const DynamicPopover = (await import('./Popover')).default

    const container = document.createElement('div')
    document.documentElement.appendChild(container)

    const originalBodyDescriptor = Object.getOwnPropertyDescriptor(
      Document.prototype,
      'body',
    )

    // Simulate a transient teardown state.
    Object.defineProperty(Document.prototype, 'body', {
      get: () => null,
      configurable: true,
    })

    try {
      const { getByRole, getByTestId } = render(
        <DynamicPopover {...baseProps} />,
        {
          container,
          baseElement: document.documentElement,
        },
      )

      fireEvent.click(getByRole('button', { name: 'Toggle' }))

      expect(getByTestId('floating-portal').getAttribute('data-root-id')).toBe(
        'HTML',
      )
    } finally {
      if (originalBodyDescriptor) {
        Object.defineProperty(
          Document.prototype,
          'body',
          originalBodyDescriptor,
        )
      }
      container.remove()
    }
  })

  it('does not render the portal when resolveAppendTo returns null (not client)', async () => {
    vi.resetModules()
    mockUseMediaQuery()
    mockFloatingUi()
    vi.doMock('../../services/isClient', () => ({ default: false }))

    const DynamicPopover = (await import('./Popover')).default

    const { queryByTestId } = render(<DynamicPopover {...baseProps} />)
    expect(queryByTestId('floating-portal')).toBeNull()
  })
})
