import '@testing-library/jest-dom'
import { fireEvent } from '@testing-library/dom'
import { createElementTest, BaseTestConfig } from '../../tests/test-framework'
import { CustomElementFor } from '../../tests/component-registry'
import { type IPktListbox } from './listbox'
import './listbox'

// jsdom does not implement scrollIntoView
HTMLElement.prototype.scrollIntoView = function () {}

// focusAndScrollIntoView uses setTimeout(0) for focus, so we need to flush
const flushFocusTimers = () => new Promise((resolve) => setTimeout(resolve, 10))

export interface ListboxTestConfig extends Partial<IPktListbox>, BaseTestConfig {
  label?: string
  id?: string
}

// Properties that must be set via JS because their attribute names are kebab-case
const jsOnlyProps = [
  'options',
  'isOpen',
  'includeSearch',
  'isMultiSelect',
  'allowUserInput',
  'maxIsReached',
  'customUserInput',
  'searchPlaceholder',
  'searchValue',
  'maxLength',
  'userMessage',
] as const

export const createListboxTest = async (config: ListboxTestConfig = {}) => {
  // Separate JS-only props from HTML-safe attributes (id, label, disabled)
  const htmlConfig: Record<string, unknown> = {}
  const jsConfig: Record<string, unknown> = {}

  for (const [key, value] of Object.entries(config)) {
    if ((jsOnlyProps as readonly string[]).includes(key)) {
      jsConfig[key] = value
    } else {
      htmlConfig[key] = value
    }
  }

  const { container, element } = await createElementTest<
    CustomElementFor<'pkt-listbox'>,
    BaseTestConfig & Record<string, unknown>
  >('pkt-listbox', htmlConfig)

  // Set JS-only properties directly
  for (const [key, value] of Object.entries(jsConfig)) {
    ;(element as any)[key] = value
  }
  await element.updateComplete

  return {
    container,
    listbox: element,
  }
}

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

describe('PktListbox', () => {
  describe('Option click handling', () => {
    test('dispatches option-toggle event with correct value', async () => {
      const options = [
        { value: 'option1', label: 'Option 1' },
        { value: 'option2', label: 'Option 2' },
      ]
      const { listbox } = await createListboxTest({ options })

      let toggledValue: string | null = null
      listbox.addEventListener('option-toggle', (e: any) => {
        toggledValue = e.detail
      })

      const optionElement = listbox.querySelector('.pkt-listbox__option')
      fireEvent.click(optionElement!)
      await listbox.updateComplete

      expect(toggledValue).toBe('option1')
    })

    test('does not dispatch event for disabled options', async () => {
      const options = [
        { value: 'disabled-option', label: 'Disabled Option', disabled: true },
      ]
      const { listbox } = await createListboxTest({ options })

      let toggledValue: string | null = null
      listbox.addEventListener('option-toggle', (e: any) => {
        toggledValue = e.detail
      })

      const optionElement = listbox.querySelector('.pkt-listbox__option')
      fireEvent.click(optionElement!)
      await listbox.updateComplete

      expect(toggledValue).toBeNull()
    })

    test('does not dispatch event for globally disabled listbox', async () => {
      const options = [
        { value: 'option1', label: 'Option 1' },
      ]
      const { listbox } = await createListboxTest({ disabled: true, options })

      let toggledValue: string | null = null
      listbox.addEventListener('option-toggle', (e: any) => {
        toggledValue = e.detail
      })

      const optionElement = listbox.querySelector('.pkt-listbox__option')
      fireEvent.click(optionElement!)
      await listbox.updateComplete

      expect(toggledValue).toBeNull()
    })

    test('allows deselecting when maxIsReached and option is selected', async () => {
      const options = [
        { value: 'selected', label: 'Selected', selected: true },
        { value: 'unselected', label: 'Unselected' },
      ]
      const { listbox } = await createListboxTest({
        isMultiSelect: true,
        maxIsReached: true,
        options,
      })

      let toggledValue: string | null = null
      listbox.addEventListener('option-toggle', (e: any) => {
        toggledValue = e.detail
      })

      const optionElements = listbox.querySelectorAll('.pkt-listbox__option')
      fireEvent.click(optionElements[0]) // selected
      await listbox.updateComplete

      expect(toggledValue).toBe('selected')
    })
  })

  describe('Keyboard navigation', () => {
    test('navigates down with ArrowDown key', async () => {
      const options = [
        { value: 'option1', label: 'Option 1' },
        { value: 'option2', label: 'Option 2' },
        { value: 'option3', label: 'Option 3' },
      ]
      const { listbox } = await createListboxTest({ options })

      const optionElements = listbox.querySelectorAll('.pkt-listbox__option')
      ;(optionElements[0] as HTMLElement).focus()

      fireEvent.keyDown(optionElements[0], { key: 'ArrowDown' })
      await flushFocusTimers()

      expect(document.activeElement).toBe(optionElements[1])
    })

    test('navigates up with ArrowUp key', async () => {
      const options = [
        { value: 'option1', label: 'Option 1' },
        { value: 'option2', label: 'Option 2' },
        { value: 'option3', label: 'Option 3' },
      ]
      const { listbox } = await createListboxTest({ options })

      const optionElements = listbox.querySelectorAll('.pkt-listbox__option')
      ;(optionElements[2] as HTMLElement).focus()

      fireEvent.keyDown(optionElements[2], { key: 'ArrowUp' })
      await flushFocusTimers()

      expect(document.activeElement).toBe(optionElements[1])
    })

    test('navigates to first option with Home key', async () => {
      const options = [
        { value: 'option1', label: 'Option 1' },
        { value: 'option2', label: 'Option 2' },
        { value: 'option3', label: 'Option 3' },
      ]
      const { listbox } = await createListboxTest({ options })

      const optionElements = listbox.querySelectorAll('.pkt-listbox__option')
      ;(optionElements[2] as HTMLElement).focus()

      fireEvent.keyDown(optionElements[2], { key: 'Home' })
      await flushFocusTimers()

      expect(document.activeElement).toBe(optionElements[0])
    })

    test('navigates to last option with End key', async () => {
      const options = [
        { value: 'option1', label: 'Option 1' },
        { value: 'option2', label: 'Option 2' },
        { value: 'option3', label: 'Option 3' },
      ]
      const { listbox } = await createListboxTest({ options })

      const optionElements = listbox.querySelectorAll('.pkt-listbox__option')
      ;(optionElements[0] as HTMLElement).focus()

      fireEvent.keyDown(optionElements[0], { key: 'End' })
      await flushFocusTimers()

      expect(document.activeElement).toBe(optionElements[2])
    })

    test('selects option with Enter key', async () => {
      const options = [
        { value: 'option1', label: 'Option 1' },
        { value: 'option2', label: 'Option 2' },
      ]
      const { listbox } = await createListboxTest({ options })

      let toggledValue: string | null = null
      listbox.addEventListener('option-toggle', (e: any) => {
        toggledValue = e.detail
      })

      const optionElement = listbox.querySelector('.pkt-listbox__option') as HTMLElement
      optionElement.focus()

      fireEvent.keyDown(optionElement, { key: 'Enter' })
      await listbox.updateComplete

      expect(toggledValue).toBe('option1')
    })

    test('selects option with Space key', async () => {
      const options = [
        { value: 'option1', label: 'Option 1' },
        { value: 'option2', label: 'Option 2' },
      ]
      const { listbox } = await createListboxTest({ options })

      let toggledValue: string | null = null
      listbox.addEventListener('option-toggle', (e: any) => {
        toggledValue = e.detail
      })

      const optionElement = listbox.querySelector('.pkt-listbox__option') as HTMLElement
      optionElement.focus()

      fireEvent.keyDown(optionElement, { key: ' ' })
      await listbox.updateComplete

      expect(toggledValue).toBe('option1')
    })

    test('closes options with Escape key', async () => {
      const options = [
        { value: 'option1', label: 'Option 1' },
      ]
      const { listbox } = await createListboxTest({ isOpen: true, options })

      let closedFired = false
      listbox.addEventListener('close-options', () => {
        closedFired = true
      })

      const optionElement = listbox.querySelector('.pkt-listbox__option') as HTMLElement
      optionElement.focus()

      fireEvent.keyDown(optionElement, { key: 'Escape' })
      await listbox.updateComplete

      expect(closedFired).toBe(true)
    })

    test('dispatches select-all event with Ctrl+A', async () => {
      const options = [
        { value: 'option1', label: 'Option 1' },
        { value: 'option2', label: 'Option 2' },
      ]
      const { listbox } = await createListboxTest({
        isMultiSelect: true,
        options,
      })

      let selectAllFired = false
      listbox.addEventListener('select-all', () => {
        selectAllFired = true
      })

      const optionElement = listbox.querySelector('.pkt-listbox__option') as HTMLElement
      optionElement.focus()

      fireEvent.keyDown(optionElement, { key: 'a', ctrlKey: true })
      await listbox.updateComplete

      expect(selectAllFired).toBe(true)
    })
  })

  describe('Search functionality', () => {
    test('renders search input when includeSearch is true', async () => {
      const { listbox } = await createListboxTest({ includeSearch: true })

      const searchInput = listbox.querySelector('[role="searchbox"]')
      expect(searchInput).toBeInTheDocument()
    })

    test('does not render search input when includeSearch is false', async () => {
      const { listbox } = await createListboxTest({ includeSearch: false })

      const searchInput = listbox.querySelector('[role="searchbox"]')
      expect(searchInput).not.toBeInTheDocument()
    })

    test('filters options by search value', async () => {
      const options = [
        { value: 'apple', label: 'Apple' },
        { value: 'banana', label: 'Banana' },
        { value: 'cherry', label: 'Cherry' },
      ]
      const { listbox } = await createListboxTest({
        includeSearch: true,
        options,
      })

      listbox.searchValue = 'app'
      listbox.filterOptions()
      await listbox.updateComplete

      const visibleOptions = listbox.querySelectorAll('.pkt-listbox__option')
      expect(visibleOptions).toHaveLength(1)
      expect(visibleOptions[0].textContent?.trim()).toContain('Apple')
    })

    test('shows all options when search is cleared', async () => {
      const options = [
        { value: 'apple', label: 'Apple' },
        { value: 'banana', label: 'Banana' },
      ]
      const { listbox } = await createListboxTest({
        includeSearch: true,
        options,
      })

      // Filter
      listbox.searchValue = 'app'
      listbox.filterOptions()
      await listbox.updateComplete
      expect(listbox.querySelectorAll('.pkt-listbox__option')).toHaveLength(1)

      // Clear
      listbox.searchValue = ''
      listbox.filterOptions()
      await listbox.updateComplete
      expect(listbox.querySelectorAll('.pkt-listbox__option')).toHaveLength(2)
    })

    test('dispatches search event when typing in search input', async () => {
      const { listbox } = await createListboxTest({ includeSearch: true })

      let searchDetail: string | null = null
      listbox.addEventListener('search', (e: any) => {
        searchDetail = e.detail
      })

      const searchInput = listbox.querySelector('[role="searchbox"]') as HTMLInputElement
      fireEvent.input(searchInput, { target: { value: 'test' } })
      await listbox.updateComplete

      expect(searchDetail).toBe('test')
    })

    test('applies search placeholder', async () => {
      const { listbox } = await createListboxTest({
        includeSearch: true,
        searchPlaceholder: 'Search here...',
      })

      const searchInput = listbox.querySelector('[role="searchbox"]') as HTMLInputElement
      expect(searchInput?.placeholder).toBe('Search here...')
    })
  })

  describe('Multi-select features', () => {
    test('renders checkboxes in multi-select mode', async () => {
      const options = [
        { value: 'option1', label: 'Option 1' },
        { value: 'option2', label: 'Option 2' },
      ]
      const { listbox } = await createListboxTest({
        isMultiSelect: true,
        options,
      })

      const checkboxes = listbox.querySelectorAll('input[type="checkbox"]')
      expect(checkboxes).toHaveLength(2)
    })

    test('renders check icon for selected option in single-select mode', async () => {
      const options = [
        { value: 'option1', label: 'Option 1', selected: true },
        { value: 'option2', label: 'Option 2' },
      ]
      const { listbox } = await createListboxTest({ options })

      const checkIcon = listbox.querySelector('pkt-icon[name="check-big"]')
      expect(checkIcon).toBeInTheDocument()
    })

    test('shows maximum reached banner', async () => {
      const options = [
        { value: 'option1', label: 'Option 1', selected: true },
        { value: 'option2', label: 'Option 2', selected: true },
      ]
      const { listbox } = await createListboxTest({
        isMultiSelect: true,
        maxLength: 3,
        options,
      })

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

    test('disables unselected checkboxes when maxIsReached', async () => {
      const options = [
        { value: 'selected', label: 'Selected', selected: true },
        { value: 'unselected', label: 'Unselected' },
      ]
      const { listbox } = await createListboxTest({
        isMultiSelect: true,
        maxIsReached: true,
        options,
      })

      const checkboxes = listbox.querySelectorAll('input[type="checkbox"]')
      expect(checkboxes[0]).not.toBeDisabled() // selected can still deselect
      expect(checkboxes[1]).toBeDisabled() // unselected is disabled
    })
  })

  describe('User input banner', () => {
    test('shows new option banner when customUserInput is set', async () => {
      const { listbox } = await createListboxTest({
        allowUserInput: true,
        customUserInput: 'New Value',
      })

      const newOptionBanner = listbox.querySelector('.pkt-listbox__banner--new-option')
      expect(newOptionBanner).toBeInTheDocument()
      expect(newOptionBanner?.getAttribute('data-value')).toBe('New Value')
    })

    test('does not show new option banner when allowUserInput is false', async () => {
      const { listbox } = await createListboxTest({
        allowUserInput: false,
        customUserInput: 'New Value',
      })

      const newOptionBanner = listbox.querySelector('.pkt-listbox__banner--new-option')
      expect(newOptionBanner).not.toBeInTheDocument()
    })

    test('dispatches option-toggle when clicking new option banner', async () => {
      const { listbox } = await createListboxTest({
        allowUserInput: true,
        customUserInput: 'New Value',
      })

      let toggledValue: string | null = null
      listbox.addEventListener('option-toggle', (e: any) => {
        toggledValue = e.detail
      })

      const newOptionBanner = listbox.querySelector('.pkt-listbox__banner--new-option')
      fireEvent.click(newOptionBanner!)
      await listbox.updateComplete

      expect(toggledValue).toBe('New Value')
    })
  })

  describe('User message display', () => {
    test('shows user message when set', async () => {
      const { listbox } = await createListboxTest({
        userMessage: 'Ingen treff i søket',
      })

      const messageEl = listbox.querySelector('.pkt-listbox__banner--user-message')
      expect(messageEl).toBeInTheDocument()
      expect(messageEl?.textContent).toContain('Ingen treff i søket')
    })

    test('does not show user message when null', async () => {
      const { listbox } = await createListboxTest({
        userMessage: null,
      })

      const messageEl = listbox.querySelector('.pkt-listbox__banner--user-message')
      expect(messageEl).not.toBeInTheDocument()
    })
  })

  describe('Option rendering', () => {
    test('renders option prefix when present', async () => {
      const options = [
        { value: 'no', label: 'Norway', prefix: 'NO' },
      ]
      const { listbox } = await createListboxTest({ options })

      const prefix = listbox.querySelector('.pkt-listbox__option-prefix')
      expect(prefix).toBeInTheDocument()
      expect(prefix?.textContent).toBe('NO')
    })

    test('renders option description when present', async () => {
      const options = [
        { value: 'option1', label: 'Option 1', description: 'A description' },
      ]
      const { listbox } = await createListboxTest({ options })

      const description = listbox.querySelector('.pkt-listbox__option-description')
      expect(description).toBeInTheDocument()
      expect(description?.textContent).toBe('A description')
    })

    test('uses value as label when label is not provided', async () => {
      const options = [
        { value: 'my-value' },
      ]
      const { listbox } = await createListboxTest({ options })

      const label = listbox.querySelector('.pkt-listbox__option-label')
      expect(label?.textContent?.trim()).toBe('my-value')
    })

    test('sets correct data attributes on options', async () => {
      const options = [
        { value: 'option1', label: 'Option 1', selected: true },
      ]
      const { listbox } = await createListboxTest({ options })

      const optionEl = listbox.querySelector('.pkt-listbox__option')
      expect(optionEl?.getAttribute('data-value')).toBe('option1')
      expect(optionEl?.getAttribute('data-selected')).toBe('true')
      expect(optionEl?.getAttribute('aria-selected')).toBe('true')
      expect(optionEl?.getAttribute('role')).toBe('option')
    })

    test('renders selected class on selected option in single mode', async () => {
      const options = [
        { value: 'option1', label: 'Option 1', selected: true },
        { value: 'option2', label: 'Option 2' },
      ]
      const { listbox } = await createListboxTest({ options })

      const selectedOption = listbox.querySelector('.pkt-listbox__option--selected')
      expect(selectedOption).toBeInTheDocument()
    })

    test('renders checkbox class on options in multi-select mode', async () => {
      const options = [
        { value: 'option1', label: 'Option 1' },
      ]
      const { listbox } = await createListboxTest({
        isMultiSelect: true,
        options,
      })

      const checkboxOption = listbox.querySelector('.pkt-listbox__option--checkBox')
      expect(checkboxOption).toBeInTheDocument()
    })
  })
})
