import '@testing-library/jest-dom'

import { fireEvent, render, screen, waitFor } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import { useForm } from 'react-hook-form'
import { axe, toHaveNoViolations } from 'jest-axe'
import { vi } from 'vitest'

import { PktButton } from '../button/Button'
import { PktTimepicker } from './Timepicker'

expect.extend(toHaveNoViolations)

const id = 'test-timepicker'
const label = 'Test Timepicker'

describe('PktTimepicker', () => {
  describe('Rendering', () => {
    test('renders without errors', () => {
      const { container } = render(<PktTimepicker id={id} name="t" label={label} />)
      expect(container.querySelector('.pkt-timepicker')).toBeInTheDocument()
    })

    test('renders spinbutton inputs and separator', () => {
      render(<PktTimepicker id={id} name="t" label={label} />)
      const spinbuttons = screen.getAllByRole('spinbutton')
      expect(spinbuttons).toHaveLength(2)
      expect(spinbuttons[0]).toHaveAttribute('id', `${id}-hours`)
      expect(spinbuttons[1]).toHaveAttribute('id', `${id}-minutes`)
    })

    test('adds stepper modifier when stepArrows is set', () => {
      const { container } = render(
        <PktTimepicker id={id} name="t" label={label} stepArrows />,
      )
      expect(container.querySelector('.pkt-timepicker--stepper')).toBeInTheDocument()
    })

    test('adds fullwidth modifier when fullwidth is set', () => {
      const { container } = render(
        <PktTimepicker id={id} name="t" label={label} fullwidth />,
      )
      expect(container.querySelector('.pkt-timepicker--fullwidth')).toBeInTheDocument()
    })

    test('applies inputSize classes on root, container, inputs and clock button', () => {
      const { container } = render(
        <PktTimepicker id={id} name="t" label={label} inputSize="small" />,
      )
      expect(container.querySelector('.pkt-timepicker--small')).toBeTruthy()
      expect(container.querySelector('.pkt-input__container--small')).toBeTruthy()
      expect(container.querySelectorAll('.pkt-input--small')).toHaveLength(2)
      expect(container.querySelector('.pkt-timepicker__button.pkt-btn--small')).toBeTruthy()
    })

    test('renders clock button when picker is visible', () => {
      render(<PktTimepicker id={id} name="t" label={label} />)
      expect(
        screen.getByRole('button', { name: /åpne tidspunkt-velger/i }),
      ).toBeInTheDocument()
    })

    test('hides clock button when hidePicker is set', () => {
      render(<PktTimepicker id={id} name="t" label={label} hidePicker />)
      expect(
        screen.queryByRole('button', { name: /åpne tidspunkt-velger/i }),
      ).not.toBeInTheDocument()
    })

    test('renders decorative clock icon when hidePicker is set', () => {
      const { container } = render(<PktTimepicker id={id} name="t" label={label} hidePicker />)
      expect(container.querySelector('.pkt-timepicker__icon')).toBeInTheDocument()
    })

    test('does not render popup when hidePicker is set', () => {
      const { container } = render(<PktTimepicker id={id} name="t" label={label} hidePicker />)
      expect(container.querySelector('.pkt-timepicker-popup')).not.toBeInTheDocument()
    })

    test('does not render popup when stepArrows is set', () => {
      const { container } = render(<PktTimepicker id={id} name="t" label={label} stepArrows />)
      expect(container.querySelector('.pkt-timepicker-popup')).not.toBeInTheDocument()
    })

    test('renders prev/next buttons when stepArrows is set', () => {
      render(<PktTimepicker id={id} name="t" label={label} stepArrows />)
      expect(screen.getByRole('button', { name: /forrige tidspunkt/i })).toBeInTheDocument()
      expect(screen.getByRole('button', { name: /neste tidspunkt/i })).toBeInTheDocument()
    })

    test('popup is hidden by default', () => {
      const { container } = render(<PktTimepicker id={id} name="t" label={label} />)
      const popup = container.querySelector('.pkt-timepicker-popup')
      expect(popup).toBeInTheDocument()
      expect(popup).toHaveAttribute('hidden')
    })
  })

  describe('Properties', () => {
    test('value sets display inputs', () => {
      render(<PktTimepicker id={id} name="t" label={label} value="09:30" />)
      const spinbuttons = screen.getAllByRole('spinbutton')
      expect(spinbuttons[0]).toHaveValue('09')
      expect(spinbuttons[1]).toHaveValue('30')
    })

    test('empty value renders empty display inputs', () => {
      render(<PktTimepicker id={id} name="t" label={label} />)
      const spinbuttons = screen.getAllByRole('spinbutton')
      expect(spinbuttons[0]).toHaveValue('')
      expect(spinbuttons[1]).toHaveValue('')
    })

    test('disabled disables inputs and clock button', () => {
      render(<PktTimepicker id={id} name="t" label={label} disabled />)
      const spinbuttons = screen.getAllByRole('spinbutton')
      expect(spinbuttons[0]).toBeDisabled()
      expect(spinbuttons[1]).toBeDisabled()
      expect(screen.getByRole('button', { name: /åpne tidspunkt-velger/i })).toBeDisabled()
    })

    test('min/max/step are not on hidden input (constraints use setCustomValidity on hours for native form validation)', () => {
      const { container } = render(
        <PktTimepicker id={id} name="t" label={label} min="08:00" max="17:00" step={300} />,
      )
      const hidden = container.querySelector<HTMLInputElement>(`#${id}-input`)
      expect(hidden?.hasAttribute('min')).toBe(false)
      expect(hidden?.hasAttribute('max')).toBe(false)
      expect(hidden?.hasAttribute('step')).toBe(false)
      expect(hidden?.hasAttribute('required')).toBe(false)
    })

    test('hidden input reflects value', () => {
      const { container } = render(
        <PktTimepicker id={id} name="t" label={label} value="09:30" />,
      )
      const hidden = container.querySelector<HTMLInputElement>(`#${id}-input`)
      expect(hidden?.value).toBe('09:30')
    })

    test('native form submit flushes spinbutton digits into the named hidden input before validation', async () => {
      const user = userEvent.setup()
      const onSubmit = vi.fn()
      render(
        <form
          onSubmit={(e) => {
            e.preventDefault()
            onSubmit(Object.fromEntries(new FormData(e.currentTarget).entries()))
          }}
        >
          <PktTimepicker id={id} name="tid" label={label} required requiredTag />
          <button type="submit">Send</button>
        </form>,
      )
      const [hoursEl, minutesEl] = screen.getAllByRole('spinbutton')
      await user.click(hoursEl)
      await user.keyboard('01')
      await user.click(minutesEl)
      await user.keyboard('02')
      await user.click(screen.getByRole('button', { name: /^send$/i }))
      expect(onSubmit).toHaveBeenCalledWith({ tid: '01:02' })
    })
  })

  describe('Popup', () => {
    test('clock button opens popup', async () => {
      const user = userEvent.setup()
      const { container } = render(<PktTimepicker id={id} name="t" label={label} />)
      await user.click(screen.getByRole('button', { name: /åpne tidspunkt-velger/i }))
      const popup = container.querySelector('.pkt-timepicker-popup')
      expect(popup).not.toHaveAttribute('hidden')
    })

    test('Escape closes popup', async () => {
      const user = userEvent.setup()
      render(<PktTimepicker id={id} name="t" label={label} />)
      const btn = screen.getByRole('button', { name: /åpne tidspunkt-velger/i })
      await user.click(btn)
      await waitFor(() => expect(btn).toHaveAttribute('aria-expanded', 'true'))
      await user.keyboard('{Escape}')
      await waitFor(() => expect(btn).toHaveAttribute('aria-expanded', 'false'))
    })

    test('popup renders hour and minute columns', async () => {
      const user = userEvent.setup()
      const { container } = render(<PktTimepicker id={id} name="t" label={label} />)
      await user.click(screen.getByRole('button', { name: /åpne tidspunkt-velger/i }))
      const cols = container.querySelectorAll('.pkt-timepicker-popup__col')
      expect(cols).toHaveLength(2)
    })
  })

  describe('Stepper', () => {
    test('next button increments by one minute step', async () => {
      const user = userEvent.setup()
      const { container } = render(
        <PktTimepicker id={id} name="t" label={label} stepArrows defaultValue="09:00" />,
      )
      await user.click(screen.getByRole('button', { name: /neste tidspunkt/i }))
      const hidden = container.querySelector<HTMLInputElement>(`#${id}-input`)
      expect(hidden?.value).toBe('09:01')
    })

    test('prev button decrements by step (5 min)', async () => {
      const user = userEvent.setup()
      const { container } = render(
        <PktTimepicker
          id={id}
          name="t"
          label={label}
          stepArrows
          step={300}
          defaultValue="09:05"
        />,
      )
      await user.click(screen.getByRole('button', { name: /forrige tidspunkt/i }))
      const hidden = container.querySelector<HTMLInputElement>(`#${id}-input`)
      expect(hidden?.value).toBe('09:00')
    })

    test('next button at 59 minutes rolls over to next hour', async () => {
      const user = userEvent.setup()
      const { container } = render(
        <PktTimepicker id={id} name="t" label={label} stepArrows defaultValue="09:59" />,
      )
      await user.click(screen.getByRole('button', { name: /neste tidspunkt/i }))
      const hidden = container.querySelector<HTMLInputElement>(`#${id}-input`)
      expect(hidden?.value).toBe('10:00')
    })
  })

  describe('Events', () => {
    test('onFocus fires when a spinbutton is focused (focus must bubble to root handler)', async () => {
      const user = userEvent.setup()
      const onFocus = vi.fn()
      const { container } = render(
        <PktTimepicker id={id} name="t" label={label} onFocus={onFocus} />,
      )
      const hoursInput = container.querySelector<HTMLInputElement>(`#${id}-hours`)!
      await user.click(hoursInput)
      expect(onFocus).toHaveBeenCalledTimes(1)
    })

    test('Backspace removes digits in hours and updates hidden value (controlled inputs)', async () => {
      const user = userEvent.setup()
      const { container } = render(
        <PktTimepicker id={id} name="t" label={label} defaultValue="09:30" />,
      )
      const hoursInput = container.querySelector<HTMLInputElement>(`#${id}-hours`)!
      const hidden = container.querySelector<HTMLInputElement>(`#${id}-input`)
      hoursInput.focus()
      await user.keyboard('{Backspace}')
      expect(hoursInput).toHaveValue('0')
      expect(hidden?.value).toBe('00:30')
      await user.keyboard('{Backspace}')
      expect(hoursInput).toHaveValue('')
      expect(hidden?.value).toBe('')
    })

    test('onValueChange fires when hours change via ArrowUp', async () => {
      const handleValueChange = vi.fn()
      const { container } = render(
        <PktTimepicker
          id={id}
          name="t"
          label={label}
          defaultValue="09:30"
          onValueChange={handleValueChange}
        />,
      )
      const hoursInput = container.querySelector<HTMLInputElement>(`#${id}-hours`)!
      fireEvent.keyDown(hoursInput, { key: 'ArrowUp' })
      await waitFor(() => {
        expect(handleValueChange).toHaveBeenCalledWith('10:30')
      })
    })
  })

  describe('Accessibility', () => {
    test('passes axe in default state', async () => {
      const { container } = render(<PktTimepicker id={id} name="t" label={label} />)
      const results = await axe(container)
      expect(results).toHaveNoViolations()
    })

    test('hours input has spinbutton role and bounds', () => {
      render(<PktTimepicker id={id} name="t" label={label} value="09:30" />)
      const hours = screen.getAllByRole('spinbutton')[0]
      expect(hours).toHaveAttribute('role', 'spinbutton')
      expect(hours).toHaveAttribute('aria-valuemin', '0')
      expect(hours).toHaveAttribute('aria-valuemax', '23')
      expect(hours).toHaveAttribute('aria-valuenow', '9')
    })
  })
})

describe('PktTimepicker + React Hook Form', () => {
  function WatchForm() {
    const { register, watch } = useForm<{ tid: string }>({
      defaultValues: { tid: '09:30' },
    })
    const values = watch()
    return (
      <form>
        <PktTimepicker id="tp-rhf-watch" label="Tid" {...register('tid')} />
        <span data-testid="watched">{values.tid ?? ''}</span>
      </form>
    )
  }

  test('register() defaultValues are reflected in watch()', async () => {
    render(<WatchForm />)
    await waitFor(() => {
      expect(screen.getByTestId('watched')).toHaveTextContent('09:30')
    })
  })

  function SubmitForm({
    onSubmit,
  }: {
    onSubmit: (data: { tid: string }) => void
  }) {
    const { register, handleSubmit } = useForm<{ tid: string }>({
      defaultValues: { tid: '14:45' },
    })
    return (
      <form onSubmit={handleSubmit(onSubmit)}>
        <PktTimepicker id="tp-rhf-submit" label="Tid" {...register('tid')} />
        <PktButton type="submit">Send</PktButton>
      </form>
    )
  }

  test('submit sends current time value from register()', async () => {
    const user = userEvent.setup()
    const onSubmit = vi.fn()
    render(<SubmitForm onSubmit={onSubmit} />)

    await user.click(screen.getByRole('button', { name: /send/i }))

    await waitFor(() => {
      expect(onSubmit).toHaveBeenCalledWith({ tid: '14:45' }, expect.anything())
    })
  })
})
