import '@testing-library/jest-dom'

import { fireEvent, render } from '@testing-library/react'
import { vi } from 'vitest'

import type { IPktComboboxOption } from 'shared-types/combobox'
import { PktCombobox } from './Combobox'
import type { IPktCombobox } from './types'

const comboboxId = 'test-combobox'
const label = 'Test Combobox'

const getDefaultOptions = (): IPktComboboxOption[] => [
  { value: 'apple', label: 'Apple' },
  { value: 'banana', label: 'Banana' },
  { value: 'cherry', label: 'Cherry' },
  { value: 'date', label: 'Date' },
]

const createComboboxTest = (props: Partial<IPktCombobox> = {}) => {
  const defaultProps: IPktCombobox = {
    label,
    id: comboboxId,
    ...props,
  }

  return render(<PktCombobox {...defaultProps} />)
}

const getFormInputValue = (container: HTMLElement) => {
  return (container.querySelector('input.pkt-visually-hidden') as HTMLInputElement)?.value ?? ''
}

const openDropdown = (container: HTMLElement) => {
  const arrowButton = container.querySelector('.pkt-combobox__input')
  fireEvent.click(arrowButton!)
}

const clickOption = (container: HTMLElement, value: string) => {
  const option = container.querySelector(`[data-value="${value}"][role="option"]`)
  fireEvent.click(option!)
}

describe('PktCombobox', () => {
  describe('Single selection', () => {
    test('selects a value by clicking option', () => {
      const handleValueChange = vi.fn()
      const { container } = createComboboxTest({
        options: getDefaultOptions(),
        onValueChange: handleValueChange,
      })

      openDropdown(container)
      clickOption(container, 'apple')

      expect(handleValueChange).toHaveBeenCalledWith(['apple'])
      expect(getFormInputValue(container)).toBe('apple')
    })

    test('replaces current selection when selecting a new value', () => {
      const handleValueChange = vi.fn()
      const { container } = createComboboxTest({
        defaultValue: 'apple',
        options: getDefaultOptions(),
        onValueChange: handleValueChange,
      })

      expect(getFormInputValue(container)).toBe('apple')

      openDropdown(container)
      clickOption(container, 'banana')

      expect(handleValueChange).toHaveBeenCalledWith(['banana'])
      expect(getFormInputValue(container)).toBe('banana')
    })

    test('closes dropdown after selecting in single mode', () => {
      const { container } = createComboboxTest({
        options: getDefaultOptions(),
      })

      openDropdown(container)

      const arrowButton = container.querySelector('.pkt-combobox__input')
      expect(arrowButton?.getAttribute('aria-expanded')).toBe('true')

      clickOption(container, 'apple')

      expect(arrowButton?.getAttribute('aria-expanded')).toBe('false')
    })

    test('deselects value when toggling already selected option', () => {
      const handleValueChange = vi.fn()
      const { container } = createComboboxTest({
        defaultValue: 'apple',
        options: getDefaultOptions(),
        onValueChange: handleValueChange,
      })

      openDropdown(container)
      clickOption(container, 'apple')

      expect(handleValueChange).toHaveBeenCalledWith([])
    })

    test('displays selected value as text in single mode', () => {
      const { container } = createComboboxTest({
        defaultValue: 'apple',
        options: getDefaultOptions(),
      })

      const valueSpan = container.querySelector('.pkt-combobox__value')
      expect(valueSpan?.textContent?.trim()).toBe('Apple')
    })

    test('selects option when clicking it in the open dropdown', () => {
      const handleValueChange = vi.fn()
      const { container } = createComboboxTest({
        options: getDefaultOptions(),
        onValueChange: handleValueChange,
      })

      openDropdown(container)

      const option = container.querySelector('.pkt-listbox__option')
      expect(option).toBeInTheDocument()

      fireEvent.click(option!)

      expect(handleValueChange).toHaveBeenCalledWith(['apple'])
    })
  })

  describe('Multiple selection', () => {
    test('selects multiple values', () => {
      const handleValueChange = vi.fn()
      const { container } = createComboboxTest({
        multiple: true,
        options: getDefaultOptions(),
        onValueChange: handleValueChange,
      })

      openDropdown(container)
      clickOption(container, 'apple')

      expect(handleValueChange).toHaveBeenCalledWith(['apple'])

      clickOption(container, 'banana')

      expect(handleValueChange).toHaveBeenCalledWith(['apple', 'banana'])
    })

    test('keeps dropdown open after selection in multiple mode', () => {
      const { container } = createComboboxTest({
        multiple: true,
        options: getDefaultOptions(),
      })

      openDropdown(container)
      clickOption(container, 'apple')

      const arrowButton = container.querySelector('.pkt-combobox__input')
      expect(arrowButton?.getAttribute('aria-expanded')).toBe('true')
    })

    test('renders selected values as tags in multiple mode', () => {
      const { container } = createComboboxTest({
        multiple: true,
        defaultValue: ['apple', 'banana'],
        options: getDefaultOptions(),
      })

      const tags = container.querySelectorAll('.pkt-combobox__input .pkt-tag')
      expect(tags.length).toBe(2)
    })

    test('removes a selected value by clicking its tag close button', () => {
      const handleValueChange = vi.fn()
      const { container } = createComboboxTest({
        multiple: true,
        defaultValue: ['apple', 'banana'],
        options: getDefaultOptions(),
        onValueChange: handleValueChange,
      })

      const closeButtons = container.querySelectorAll('.pkt-tag__close-btn')
      expect(closeButtons.length).toBe(2)

      fireEvent.click(closeButtons[0])

      expect(handleValueChange).toHaveBeenCalledWith(['banana'])
    })

    test('deselects value when toggling already selected option in multiple mode', () => {
      const handleValueChange = vi.fn()
      const { container } = createComboboxTest({
        multiple: true,
        defaultValue: ['apple', 'banana'],
        options: getDefaultOptions(),
        onValueChange: handleValueChange,
      })

      openDropdown(container)
      clickOption(container, 'apple')

      expect(handleValueChange).toHaveBeenCalledWith(['banana'])
    })

    test('renders tags outside when tagPlacement is outside', () => {
      const { container } = createComboboxTest({
        multiple: true,
        tagPlacement: 'outside',
        defaultValue: ['apple', 'banana'],
        options: getDefaultOptions(),
      })

      const outsideTags = container.querySelector('.pkt-combobox__tags-outside')
      expect(outsideTags).toBeInTheDocument()

      const tags = outsideTags?.querySelectorAll('.pkt-tag')
      expect(tags?.length).toBe(2)
    })

    test('renders value as tag with tagSkinColor', () => {
      const optionsWithTags: IPktComboboxOption[] = [
        { value: 'red', label: 'Red', tagSkinColor: 'red' },
        { value: 'blue', label: 'Blue', tagSkinColor: 'blue' },
      ]

      const { container } = createComboboxTest({
        multiple: true,
        defaultValue: ['red'],
        options: optionsWithTags,
      })

      const tag = container.querySelector('.pkt-tag')
      expect(tag).toBeInTheDocument()
    })

    test('renders checkboxes for multi-select options', () => {
      const { container } = createComboboxTest({
        multiple: true,
        defaultValue: ['apple'],
        options: getDefaultOptions(),
      })

      const checkboxes = container.querySelectorAll('.pkt-listbox__option input[type="checkbox"]')
      expect(checkboxes.length).toBe(4)

      const checkedBoxes = container.querySelectorAll('.pkt-listbox__option input[type="checkbox"]:checked')
      expect(checkedBoxes.length).toBe(1)
    })
  })

  describe('Maxlength enforcement', () => {
    test('prevents selection beyond maxlength', () => {
      const handleValueChange = vi.fn()
      const { container } = createComboboxTest({
        multiple: true,
        maxlength: 2,
        defaultValue: ['apple', 'banana'],
        options: getDefaultOptions(),
        onValueChange: handleValueChange,
      })

      openDropdown(container)
      clickOption(container, 'date')

      // Value should not change - cherry was clicked but max is reached
      expect(getFormInputValue(container)).toBe('apple,banana')
    })

    test('allows deselection when at maxlength', () => {
      const handleValueChange = vi.fn()
      const { container } = createComboboxTest({
        multiple: true,
        maxlength: 2,
        defaultValue: ['apple', 'banana'],
        options: getDefaultOptions(),
        onValueChange: handleValueChange,
      })

      openDropdown(container)
      clickOption(container, 'apple')

      expect(handleValueChange).toHaveBeenCalledWith(['banana'])
    })

    test('shows max reached banner', () => {
      const { container } = createComboboxTest({
        multiple: true,
        maxlength: 2,
        defaultValue: ['apple', 'banana'],
        options: getDefaultOptions(),
      })

      const banner = container.querySelector('.pkt-listbox__banner--maximum-reached')
      expect(banner).toBeInTheDocument()
      expect(banner?.textContent).toContain('2 av maks 2')
    })

    test('disables unselected options when max is reached', () => {
      const { container } = createComboboxTest({
        multiple: true,
        maxlength: 2,
        defaultValue: ['apple', 'banana'],
        options: getDefaultOptions(),
      })

      const unselectedOption = container.querySelector('[data-value="date"]')
      expect(unselectedOption?.getAttribute('data-disabled')).toBe('true')
      expect(unselectedOption?.getAttribute('tabindex')).toBe('-1')
    })
  })

  describe('Disabled options', () => {
    test('does not select disabled options', () => {
      const handleValueChange = vi.fn()
      const optionsWithDisabled: IPktComboboxOption[] = [
        { value: 'enabled', label: 'Enabled' },
        { value: 'disabled', label: 'Disabled', disabled: true },
      ]

      const { container } = createComboboxTest({
        options: optionsWithDisabled,
        onValueChange: handleValueChange,
      })

      openDropdown(container)
      clickOption(container, 'disabled')

      expect(handleValueChange).not.toHaveBeenCalled()
      expect(getFormInputValue(container)).toBe('')
    })
  })

  describe('User input (custom values)', () => {
    test('adds custom value in single-select mode', () => {
      const handleValueChange = vi.fn()
      const { container } = createComboboxTest({
        allowUserInput: true,
        options: getDefaultOptions(),
        onValueChange: handleValueChange,
      })

      const textInput = container.querySelector('input[type="text"]') as HTMLInputElement
      fireEvent.focus(textInput)

      textInput.value = 'CustomFruit'
      fireEvent.change(textInput, { target: { value: 'CustomFruit' } })
      fireEvent.keyDown(textInput, { key: 'Enter' })

      expect(handleValueChange).toHaveBeenCalledWith(['CustomFruit'])
    })

    test('adds custom value in multiple-select mode', () => {
      const handleValueChange = vi.fn()
      const { container } = createComboboxTest({
        allowUserInput: true,
        multiple: true,
        options: getDefaultOptions(),
        onValueChange: handleValueChange,
      })

      const textInput = container.querySelector('input[type="text"]') as HTMLInputElement
      fireEvent.focus(textInput)

      textInput.value = 'CustomFruit'
      fireEvent.change(textInput, { target: { value: 'CustomFruit' } })
      fireEvent.keyDown(textInput, { key: 'Enter' })

      expect(handleValueChange).toHaveBeenCalledWith(['CustomFruit'])
    })

    test('does not add empty custom value', () => {
      const { container } = createComboboxTest({
        allowUserInput: true,
      })

      const textInput = container.querySelector('input[type="text"]') as HTMLInputElement
      fireEvent.focus(textInput)

      textInput.value = ''
      fireEvent.keyDown(textInput, { key: 'Enter' })

      // No user-added option should appear in the listbox
      const options = container.querySelectorAll('.pkt-listbox__option')
      const hasUserAdded = Array.from(options).some((opt) => opt.getAttribute('data-value') === '')
      expect(hasUserAdded).toBe(false)
      expect(getFormInputValue(container)).toBe('')
    })

    test('does not add whitespace-only custom value', () => {
      const { container } = createComboboxTest({
        allowUserInput: true,
      })

      const textInput = container.querySelector('input[type="text"]') as HTMLInputElement
      fireEvent.focus(textInput)

      textInput.value = '   '
      fireEvent.change(textInput, { target: { value: '   ' } })
      fireEvent.keyDown(textInput, { key: 'Enter' })

      // No user-added option should appear in the listbox
      const options = container.querySelectorAll('.pkt-listbox__option')
      const hasUserAdded = Array.from(options).some((opt) => opt.getAttribute('data-value')?.trim() === '')
      expect(hasUserAdded).toBe(false)
      expect(getFormInputValue(container)).toBe('')
    })

    test('preserves user-added options when options prop changes', () => {
      const { container, rerender } = render(
        <PktCombobox id={comboboxId} label={label} allowUserInput options={getDefaultOptions()} />,
      )

      // Add a custom value
      const textInput = container.querySelector('input[type="text"]') as HTMLInputElement
      fireEvent.focus(textInput)
      textInput.value = 'CustomFruit'
      fireEvent.change(textInput, { target: { value: 'CustomFruit' } })
      fireEvent.keyDown(textInput, { key: 'Enter' })

      expect(getFormInputValue(container)).toBe('CustomFruit')

      // Change options prop
      rerender(<PktCombobox id={comboboxId} label={label} allowUserInput options={[{ value: 'new', label: 'New' }]} />)

      // User-added value should persist
      expect(getFormInputValue(container)).toBe('CustomFruit')

      // User-added option should still appear in the listbox
      const options = container.querySelectorAll('.pkt-listbox__option')
      const hasCustom = Array.from(options).some((opt) => opt.getAttribute('data-value') === 'CustomFruit')
      expect(hasCustom).toBe(true)
    })

    test('removes user-added option when deselected', () => {
      const handleValueChange = vi.fn()
      const { container } = createComboboxTest({
        allowUserInput: true,
        multiple: true,
        options: getDefaultOptions(),
        onValueChange: handleValueChange,
      })

      // Add a custom value
      const textInput = container.querySelector('input[type="text"]') as HTMLInputElement
      fireEvent.focus(textInput)
      textInput.value = 'CustomFruit'
      fireEvent.change(textInput, { target: { value: 'CustomFruit' } })
      fireEvent.keyDown(textInput, { key: 'Enter' })

      expect(getFormInputValue(container)).toContain('CustomFruit')

      // Remove by clicking the tag close button
      const closeButtons = container.querySelectorAll('.pkt-tag__close-btn')
      const customCloseBtn = closeButtons[closeButtons.length - 1]
      fireEvent.click(customCloseBtn!)

      expect(getFormInputValue(container)).not.toContain('CustomFruit')
    })
  })

  describe('displayValueAs modes', () => {
    test('displays value using label by default', () => {
      const options: IPktComboboxOption[] = [{ value: 'no', label: 'Norway', prefix: 'NO' }]

      const { container } = createComboboxTest({
        defaultValue: 'no',
        options,
      })

      const valueEl = container.querySelector('.pkt-combobox__value')
      expect(valueEl?.textContent?.trim()).toBe('Norway')
    })

    test('displays value using value when displayValueAs is value', () => {
      const options: IPktComboboxOption[] = [{ value: 'no', label: 'Norway', prefix: 'NO' }]

      const { container } = createComboboxTest({
        defaultValue: 'no',
        displayValueAs: 'value',
        options,
      })

      const valueEl = container.querySelector('.pkt-combobox__value')
      expect(valueEl?.textContent?.trim()).toBe('no')
    })

    test('displays prefix and value when displayValueAs is prefixAndValue', () => {
      const options: IPktComboboxOption[] = [{ value: 'no', label: 'Norway', prefix: 'NO' }]

      const { container } = createComboboxTest({
        defaultValue: 'no',
        displayValueAs: 'prefixAndValue',
        options,
      })

      const valueEl = container.querySelector('.pkt-combobox__value')
      expect(valueEl?.textContent?.trim()).toBe('NO no')
    })
  })

  describe('Value change callbacks', () => {
    test('calls onValueChange on selection', () => {
      const handleValueChange = vi.fn()
      const { container } = createComboboxTest({
        options: getDefaultOptions(),
        onValueChange: handleValueChange,
      })

      openDropdown(container)
      clickOption(container, 'apple')

      expect(handleValueChange).toHaveBeenCalledWith(['apple'])
    })

    test('calls onValueChange with array for multiple mode', () => {
      const handleValueChange = vi.fn()
      const { container } = createComboboxTest({
        multiple: true,
        defaultValue: ['apple'],
        options: getDefaultOptions(),
        onValueChange: handleValueChange,
      })

      openDropdown(container)
      clickOption(container, 'banana')

      expect(handleValueChange).toHaveBeenCalledWith(['apple', 'banana'])
    })

    test('calls onValueChange with empty array when clearing', () => {
      const handleValueChange = vi.fn()
      const { container } = createComboboxTest({
        defaultValue: 'apple',
        options: getDefaultOptions(),
        onValueChange: handleValueChange,
      })

      openDropdown(container)
      clickOption(container, 'apple') // deselect

      expect(handleValueChange).toHaveBeenCalledWith([])
    })
  })
})
