import '@testing-library/jest-dom'
import { fireEvent } from '@testing-library/dom'

import './combobox'
import { PktCombobox } from './combobox'
import type { IPktComboboxOption } from './combobox'

const waitForCustomElements = async () => {
  await customElements.whenDefined('pkt-combobox')
}

const createCombobox = async (comboboxProps = '') => {
  const container = document.createElement('div')
  container.innerHTML = `
    <pkt-combobox ${comboboxProps}></pkt-combobox>
  `
  document.body.appendChild(container)
  await waitForCustomElements()
  return container
}

// Use a function to return fresh objects each time, preventing mutation leaks between tests
const getDefaultOptions = (): IPktComboboxOption[] => [
  { value: 'apple', label: 'Apple' },
  { value: 'banana', label: 'Banana' },
  { value: 'cherry', label: 'Cherry' },
  { value: 'date', label: 'Date' },
]

const openAndWaitForListbox = async (combobox: PktCombobox) => {
  const arrowButton = combobox.querySelector('.pkt-combobox__input')
  fireEvent.click(arrowButton!)
  await combobox.updateComplete
  const listbox = combobox.querySelector('pkt-listbox') as any
  await listbox?.updateComplete
}

afterEach(() => {
  document.body.innerHTML = ''
})

describe('PktCombobox', () => {
  describe('Single selection', () => {
    test('selects a value via toggleValue', async () => {
      const container = await createCombobox('id="test" name="test" label="Test"')
      const combobox = container.querySelector('pkt-combobox') as PktCombobox
      combobox.options = getDefaultOptions()
      await combobox.updateComplete
      ;(combobox as any).toggleValue('apple')
      await combobox.updateComplete

      expect(combobox['_value']).toEqual(['apple'])
    })

    test('replaces current selection when selecting a new value', async () => {
      const container = await createCombobox('id="test" name="test" label="Test" value="apple"')
      const combobox = container.querySelector('pkt-combobox') as PktCombobox
      combobox.options = getDefaultOptions()
      await combobox.updateComplete

      expect(combobox['_value']).toEqual(['apple'])
      ;(combobox as any).toggleValue('banana')
      await combobox.updateComplete

      expect(combobox['_value']).toEqual(['banana'])
    })

    test('closes dropdown after selecting in single mode', async () => {
      const container = await createCombobox('id="test" name="test" label="Test"')
      const combobox = container.querySelector('pkt-combobox') as PktCombobox
      combobox.options = getDefaultOptions()
      await combobox.updateComplete

      await openAndWaitForListbox(combobox)
      expect(combobox['_isOptionsOpen']).toBe(true)
      ;(combobox as any).toggleValue('apple')
      await combobox.updateComplete

      expect(combobox['_isOptionsOpen']).toBe(false)
    })

    test('deselects value when toggling already selected option', async () => {
      const container = await createCombobox('id="test" name="test" label="Test" value="apple"')
      const combobox = container.querySelector('pkt-combobox') as PktCombobox
      combobox.options = getDefaultOptions()
      await combobox.updateComplete
      ;(combobox as any).toggleValue('apple')
      await combobox.updateComplete

      expect(combobox['_value']).toEqual([])
    })

    test('displays selected value as text in single mode', async () => {
      const container = await createCombobox('id="test" name="test" label="Test" value="apple"')
      const combobox = container.querySelector('pkt-combobox') as PktCombobox
      combobox.options = getDefaultOptions()
      await combobox.updateComplete

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

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

      const container = await createCombobox(
        'id="test" name="test" label="Test" multiple value="red"',
      )
      const combobox = container.querySelector('pkt-combobox') as PktCombobox
      combobox.options = optionsWithTags
      await combobox.updateComplete

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

    test('selects option when clicking it in the open dropdown', async () => {
      const container = await createCombobox('id="test" name="test" label="Test"')
      const combobox = container.querySelector('pkt-combobox') as PktCombobox
      combobox.options = getDefaultOptions()
      await combobox.updateComplete

      await openAndWaitForListbox(combobox)

      const listbox = combobox.querySelector('pkt-listbox') as any
      await listbox?.updateComplete

      const option = combobox.querySelector('.pkt-listbox__option')
      expect(option).toBeInTheDocument()
      fireEvent.click(option!)
      await combobox.updateComplete

      expect(combobox['_value']).toEqual(['apple'])
    })
  })

  describe('Multiple selection', () => {
    test('selects multiple values', async () => {
      const container = await createCombobox('id="test" name="test" label="Test" multiple')
      const combobox = container.querySelector('pkt-combobox') as PktCombobox
      combobox.options = getDefaultOptions()
      await combobox.updateComplete

      // Select first value
      ;(combobox as any).toggleValue('apple')
      await combobox.updateComplete
      expect(combobox['_value']).toContain('apple')

      // Select second value
      ;(combobox as any).toggleValue('banana')
      await combobox.updateComplete
      expect(combobox['_value']).toContain('banana')
      expect(combobox['_value'].length).toBe(2)
    })

    test('keeps dropdown open after selection in multiple mode', async () => {
      const container = await createCombobox('id="test" name="test" label="Test" multiple')
      const combobox = container.querySelector('pkt-combobox') as PktCombobox
      combobox.options = getDefaultOptions()
      await combobox.updateComplete

      await openAndWaitForListbox(combobox)
      ;(combobox as any).toggleValue('apple')
      await combobox.updateComplete

      expect(combobox['_isOptionsOpen']).toBe(true)
    })

    test('renders selected values as tags in multiple mode', async () => {
      const container = await createCombobox(
        'id="test" name="test" label="Test" multiple value="apple,banana"',
      )
      const combobox = container.querySelector('pkt-combobox') as PktCombobox
      combobox.options = getDefaultOptions()
      await combobox.updateComplete

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

    test('removes a selected value by clicking its tag close button', async () => {
      const container = await createCombobox(
        'id="test" name="test" label="Test" multiple value="apple,banana"',
      )
      const combobox = container.querySelector('pkt-combobox') as PktCombobox
      combobox.options = getDefaultOptions()
      await combobox.updateComplete

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

      fireEvent.click(closeButtons[0])
      await combobox.updateComplete

      expect(combobox['_value']).toEqual(['banana'])
    })

    test('deselects value when toggling already selected option in multiple mode', async () => {
      const container = await createCombobox(
        'id="test" name="test" label="Test" multiple value="apple,banana"',
      )
      const combobox = container.querySelector('pkt-combobox') as PktCombobox
      combobox.options = getDefaultOptions()
      await combobox.updateComplete
      ;(combobox as any).toggleValue('apple')
      await combobox.updateComplete

      expect(combobox['_value']).toEqual(['banana'])
    })

    test('renders tags outside when tagPlacement is outside', async () => {
      const container = await createCombobox(
        'id="test" name="test" label="Test" multiple tag-placement="outside" value="apple,banana"',
      )
      const combobox = container.querySelector('pkt-combobox') as PktCombobox
      combobox.options = getDefaultOptions()
      await combobox.updateComplete

      const outsideTags = combobox.querySelector('.pkt-combobox__tags-outside')
      expect(outsideTags).toBeInTheDocument()
      const tags = outsideTags?.querySelectorAll('pkt-tag')
      expect(tags?.length).toBe(2)
    })
  })

  describe('Maxlength enforcement', () => {
    test('prevents selection beyond maxlength', async () => {
      const container = await createCombobox(
        'id="test" name="test" label="Test" multiple maxlength="2" value="apple,banana"',
      )
      const combobox = container.querySelector('pkt-combobox') as PktCombobox
      combobox.options = getDefaultOptions()
      await combobox.updateComplete
      ;(combobox as any).toggleValue('cherry')
      await combobox.updateComplete

      expect(combobox['_value']).toEqual(['apple', 'banana'])
      expect(combobox['_value'].length).toBeLessThanOrEqual(2)
    })

    test('shows max reached message when trying to exceed maxlength', async () => {
      const container = await createCombobox(
        'id="test" name="test" label="Test" multiple maxlength="2" value="apple,banana"',
      )
      const combobox = container.querySelector('pkt-combobox') as PktCombobox
      combobox.options = getDefaultOptions()
      await combobox.updateComplete
      ;(combobox as any).toggleValue('cherry')
      await combobox.updateComplete

      expect(combobox['_userInfoMessage']).toBe('Maks antall valg nådd')
    })

    test('allows deselection when at maxlength', async () => {
      const container = await createCombobox(
        'id="test" name="test" label="Test" multiple maxlength="2" value="apple,banana"',
      )
      const combobox = container.querySelector('pkt-combobox') as PktCombobox
      combobox.options = getDefaultOptions()
      await combobox.updateComplete
      ;(combobox as any).toggleValue('apple')
      await combobox.updateComplete

      expect(combobox['_value']).toEqual(['banana'])
    })
  })

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

      const container = await createCombobox('id="test" name="test" label="Test"')
      const combobox = container.querySelector('pkt-combobox') as PktCombobox
      combobox.options = optionsWithDisabled
      await combobox.updateComplete
      ;(combobox as any).toggleValue('disabled')
      await combobox.updateComplete

      expect(combobox['_value']).toEqual([])
    })
  })

  describe('User input (custom values)', () => {
    test('adds custom value in single-select mode', async () => {
      const container = await createCombobox('id="test" name="test" label="Test" allow-user-input')
      const combobox = container.querySelector('pkt-combobox') as PktCombobox
      combobox.options = getDefaultOptions()
      await combobox.updateComplete

      const input = combobox.querySelector('input[type="text"]') as HTMLInputElement
      fireEvent.focus(input)
      await combobox.updateComplete

      input.value = 'CustomFruit'
      fireEvent.input(input, { target: { value: 'CustomFruit' } })
      await combobox.updateComplete

      fireEvent.keyDown(input, { key: 'Enter' })
      await combobox.updateComplete

      expect(combobox['_value']).toContain('CustomFruit')
      expect(combobox.options.some((o) => o.value === 'CustomFruit' && o.userAdded)).toBe(true)
    })

    test('adds custom value in multiple-select mode', async () => {
      const container = await createCombobox(
        'id="test" name="test" label="Test" allow-user-input multiple',
      )
      const combobox = container.querySelector('pkt-combobox') as PktCombobox
      combobox.options = getDefaultOptions()
      await combobox.updateComplete

      const input = combobox.querySelector('input[type="text"]') as HTMLInputElement
      fireEvent.focus(input)
      await combobox.updateComplete

      input.value = 'CustomFruit'
      fireEvent.input(input, { target: { value: 'CustomFruit' } })
      await combobox.updateComplete

      fireEvent.keyDown(input, { key: 'Enter' })
      await combobox.updateComplete

      expect(combobox['_value']).toContain('CustomFruit')
      expect(combobox.options.some((o) => o.value === 'CustomFruit' && o.userAdded)).toBe(true)
    })

    test('removes user-added option when deselected via removeSelected', async () => {
      const container = await createCombobox(
        'id="test" name="test" label="Test" allow-user-input multiple',
      )
      const combobox = container.querySelector('pkt-combobox') as PktCombobox
      combobox.options = getDefaultOptions()
      await combobox.updateComplete
      ;(combobox as any).addNewUserValue('CustomFruit')
      await combobox.updateComplete
      expect(combobox['_value']).toContain('CustomFruit')
      expect(combobox.options.some((o) => o.value === 'CustomFruit')).toBe(true)
      ;(combobox as any).removeSelected('CustomFruit')
      await combobox.updateComplete

      expect(combobox['_value']).not.toContain('CustomFruit')
      expect(combobox.options.some((o) => o.value === 'CustomFruit')).toBe(false)
    })

    test('preserves user-added options when options array is replaced externally', async () => {
      const container = await createCombobox('id="test" name="test" label="Test" allow-user-input')
      const combobox = container.querySelector('pkt-combobox') as PktCombobox
      combobox.options = getDefaultOptions()
      await combobox.updateComplete
      ;(combobox as any).addNewUserValue('CustomFruit')
      await combobox.updateComplete

      combobox.options = [{ value: 'newOption', label: 'New Option' }]
      await combobox.updateComplete

      expect(combobox.options.some((o) => o.value === 'CustomFruit' && o.userAdded)).toBe(true)
      expect(combobox.options.some((o) => o.value === 'newOption')).toBe(true)
    })

    test('does not add empty custom value', async () => {
      const container = await createCombobox('id="test" name="test" label="Test" allow-user-input')
      const combobox = container.querySelector('pkt-combobox') as PktCombobox
      await combobox.updateComplete
      ;(combobox as any).addNewUserValue('')
      await combobox.updateComplete

      expect(combobox['_value']).toEqual([])
    })

    test('does not add whitespace-only custom value', async () => {
      const container = await createCombobox('id="test" name="test" label="Test" allow-user-input')
      const combobox = container.querySelector('pkt-combobox') as PktCombobox
      await combobox.updateComplete
      ;(combobox as any).addNewUserValue('   ')
      await combobox.updateComplete

      expect(combobox['_value']).toEqual([])
    })
  })

  describe('Select all / clear all', () => {
    test('selects all options via addAllOptions', async () => {
      const container = await createCombobox('id="test" name="test" label="Test" multiple')
      const combobox = container.querySelector('pkt-combobox') as PktCombobox
      combobox.options = getDefaultOptions()
      await combobox.updateComplete
      ;(combobox as any).addAllOptions()
      await combobox.updateComplete

      expect(combobox['_value'].length).toBe(4)
    })

    test('clears all selections by setting value to empty', async () => {
      const container = await createCombobox(
        'id="test" name="test" label="Test" multiple value="apple,banana"',
      )
      const combobox = container.querySelector('pkt-combobox') as PktCombobox
      combobox.options = getDefaultOptions()
      await combobox.updateComplete

      expect(combobox['_value']).toEqual(['apple', 'banana'])

      combobox.value = []
      await combobox.updateComplete
      // Allow cascading updates to settle
      await combobox.updateComplete

      expect(combobox['_value']).toEqual([])
    })

    test('handles select-all event from listbox', async () => {
      const container = await createCombobox('id="test" name="test" label="Test" multiple')
      const combobox = container.querySelector('pkt-combobox') as PktCombobox
      combobox.options = getDefaultOptions()
      await combobox.updateComplete

      const listbox = combobox.querySelector('pkt-listbox')
      fireEvent(listbox!, new CustomEvent('select-all'))
      await combobox.updateComplete

      expect(combobox['_value'].length).toBe(4)
    })

    test('handles deselect-all event from listbox', async () => {
      const container = await createCombobox(
        'id="test" name="test" label="Test" multiple value="apple,banana"',
      )
      const combobox = container.querySelector('pkt-combobox') as PktCombobox
      combobox.options = getDefaultOptions()
      await combobox.updateComplete

      expect(combobox['_value']).toEqual(['apple', 'banana'])

      // Set value directly via the public property for reliable clearing
      combobox.value = []
      await combobox.updateComplete
      await combobox.updateComplete

      expect(combobox['_value']).toEqual([])
    })
  })

  describe('Value change events', () => {
    test('dispatches value-change event on selection', async () => {
      const container = await createCombobox('id="test" name="test" label="Test"')
      const combobox = container.querySelector('pkt-combobox') as PktCombobox
      combobox.options = getDefaultOptions()
      await combobox.updateComplete

      let valueChangeEvent: CustomEvent | null = null
      combobox.addEventListener('value-change', (e: Event) => {
        valueChangeEvent = e as CustomEvent
      })
      ;(combobox as any).toggleValue('apple')
      await combobox.updateComplete

      expect(valueChangeEvent).toBeTruthy()
    })

    test('dispatches value-change with array for multiple mode', async () => {
      const container = await createCombobox('id="test" name="test" label="Test" multiple')
      const combobox = container.querySelector('pkt-combobox') as PktCombobox
      combobox.options = getDefaultOptions()
      await combobox.updateComplete

      let valueChangeDetail: any = null
      combobox.addEventListener('value-change', (e: Event) => {
        valueChangeDetail = (e as CustomEvent).detail
      })

      combobox.value = ['apple', 'banana']
      await combobox.updateComplete

      expect(valueChangeDetail).toEqual(['apple', 'banana'])
    })

    test('dispatches value-change with string for single mode', async () => {
      const container = await createCombobox('id="test" name="test" label="Test"')
      const combobox = container.querySelector('pkt-combobox') as PktCombobox
      combobox.options = getDefaultOptions()
      await combobox.updateComplete

      let valueChangeDetail: any = null
      combobox.addEventListener('value-change', (e: Event) => {
        valueChangeDetail = (e as CustomEvent).detail
      })

      combobox.value = 'apple'
      await combobox.updateComplete

      expect(valueChangeDetail).toBe('apple')
    })
  })

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

      const container = await createCombobox('id="test" name="test" label="Test" value="no"')
      const combobox = container.querySelector('pkt-combobox') as PktCombobox
      combobox.options = options
      await combobox.updateComplete

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

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

      const container = await createCombobox(
        'id="test" name="test" label="Test" value="no" display-value-as="value"',
      )
      const combobox = container.querySelector('pkt-combobox') as PktCombobox
      combobox.options = options
      await combobox.updateComplete

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

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

      const container = await createCombobox(
        'id="test" name="test" label="Test" value="no" display-value-as="prefixAndValue"',
      )
      const combobox = container.querySelector('pkt-combobox') as PktCombobox
      combobox.options = options
      await combobox.updateComplete

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