import { assert, fixture, html, nextFrame, oneEvent } from '@open-wc/testing'
import sinon from 'sinon'
import UiSelect from '../../../src/md/select/internals/Select.js'
import UiOption from '../../../src/md/select/internals/Option.js'
import type { UiSelectChangeEvent } from '../../../src/md/select/internals/Select.js'

import '../../../src/md/select/ui-select.js'
import '../../../src/md/select/ui-option.js'
import '../../../src/md/icons/ui-icon.js'

describe('md', () => {
  describe('UiSelect', () => {
    async function basicFixture(): Promise<UiSelect> {
      return fixture(html`
        <ui-select label="Select an option">
          <ui-option value="apple">Apple</ui-option>
          <ui-option value="banana">Banana</ui-option>
          <ui-option value="cherry">Cherry</ui-option>
        </ui-select>
      `)
    }

    async function withValueFixture(): Promise<UiSelect> {
      return fixture(html`
        <ui-select label="Select an option" value="banana">
          <ui-option value="apple">Apple</ui-option>
          <ui-option value="banana">Banana</ui-option>
          <ui-option value="cherry">Cherry</ui-option>
        </ui-select>
      `)
    }

    async function requiredFixture(): Promise<UiSelect> {
      return fixture(html`
        <ui-select label="Required field" required>
          <ui-option value="apple">Apple</ui-option>
          <ui-option value="banana">Banana</ui-option>
        </ui-select>
      `)
    }

    async function disabledFixture(): Promise<UiSelect> {
      return fixture(html`
        <ui-select label="Disabled select" disabled>
          <ui-option value="apple">Apple</ui-option>
          <ui-option value="banana">Banana</ui-option>
        </ui-select>
      `)
    }

    async function formFixture(): Promise<HTMLFormElement> {
      return fixture(html`
        <form>
          <input name="text" value="test" />
          <ui-select name="fruit" label="Select fruit">
            <ui-option value="apple">Apple</ui-option>
            <ui-option value="banana">Banana</ui-option>
          </ui-select>
        </form>
      `)
    }

    async function invalidFixture(): Promise<UiSelect> {
      return fixture(html`
        <ui-select label="Invalid select" invalid invalidText="This field is required">
          <ui-option value="apple">Apple</ui-option>
          <ui-option value="banana">Banana</ui-option>
        </ui-select>
      `)
    }

    async function emptyFixture(): Promise<UiSelect> {
      return fixture(html`<ui-select label="Empty select"></ui-select>`)
    }

    describe('Basic functionality', () => {
      it('should create select element', async () => {
        const element = await basicFixture()
        assert.instanceOf(element, UiSelect)
        assert.equal(element.tagName.toLowerCase(), 'ui-select')
      })

      it('has formAssociated set', async () => {
        assert.isTrue(UiSelect.formAssociated)
      })

      it('should have correct default properties', async () => {
        const element = await basicFixture()
        assert.isUndefined(element.value)
        assert.isUndefined(element.name)
        assert.equal(element.label, 'Select an option')
        assert.isFalse(element.required)
        assert.isUndefined(element.invalid)
        assert.isUndefined(element.invalidText)
        assert.isFalse(element.disabled)
        assert.isFalse(element.open)
        assert.isNull(element.selectedItem)
        assert.equal(element.renderValue, '')
      })

      it('should set correct ARIA attributes', async () => {
        const element = await basicFixture()
        assert.equal(element.getAttribute('role'), 'combobox')
        assert.equal(element.getAttribute('aria-haspopup'), 'listbox')
        assert.equal(element.getAttribute('aria-controls'), 'menu')
        assert.equal(element.getAttribute('aria-label'), 'Select an option')
        assert.equal(element.tabIndex, 0)
      })

      it('should not have tabindex when disabled', async () => {
        const element = await disabledFixture()
        assert.equal(element.tabIndex, -1)
      })
    })

    describe('Value and selection', () => {
      it('should set value and select corresponding option', async () => {
        const element = await basicFixture()
        element.value = 'banana'
        await element.updateComplete
        // We need it here because the observer also awaits updateComplete
        await nextFrame()

        assert.equal(element.value, 'banana')
        assert.isNotNull(element.selectedItem)
        assert.equal(element.selectedItem!.value, 'banana')
        assert.equal(element.renderValue, 'Banana')
      })

      it('should initialize with preselected value', async () => {
        const element = await withValueFixture()
        await element.updateComplete

        assert.equal(element.value, 'banana')
        assert.isNotNull(element.selectedItem)
        assert.equal(element.selectedItem!.value, 'banana')
        assert.equal(element.renderValue, 'Banana')
      })

      it('should handle invalid value gracefully', async () => {
        const element = await basicFixture()
        element.value = 'nonexistent'
        await element.updateComplete

        assert.equal(element.value, 'nonexistent')
        assert.isNull(element.selectedItem)
        assert.equal(element.renderValue, '')
      })

      it('should clear selection when value is undefined', async () => {
        const element = await withValueFixture()
        await element.updateComplete

        element.value = undefined
        await element.updateComplete
        // We need it here because the observer also awaits updateComplete
        await nextFrame()

        assert.isUndefined(element.value, 'the value should be undefined')
        assert.isNull(element.selectedItem, 'selected item should be null')
        assert.equal(element.renderValue, '')
      })
    })

    describe('Form integration', () => {
      it('should have no form when not in form', async () => {
        const element = await basicFixture()
        assert.isNull(element.form)
      })

      it('should have form when in a form', async () => {
        const form = await formFixture()
        const select = form.querySelector('ui-select')!
        assert.ok(select.form, 'has a form')
        assert.isTrue(select.form === form, 'has the parent form')
      })

      it('should participate in form submission', async () => {
        const form = await formFixture()
        const select = form.querySelector('ui-select')!
        select.value = 'apple'
        await select.updateComplete

        const formData = new FormData(form)
        assert.equal(formData.get('fruit'), 'apple')
      })

      it('should not submit when no value selected', async () => {
        const form = await formFixture()
        const formData = new FormData(form)
        assert.isNull(formData.get('fruit'))
      })

      it('should reset value on form reset', async () => {
        const form = await formFixture()
        const select = form.querySelector('ui-select')!
        select.value = 'apple'
        await select.updateComplete

        select.formResetCallback()
        assert.isUndefined(select.value)
      })

      it('should restore state', async () => {
        const element = await basicFixture()
        element.formStateRestoreCallback('cherry')
        assert.equal(element.value, 'cherry')

        element.formStateRestoreCallback(null)
        assert.isUndefined(element.value)
      })
    })

    describe('Validation', () => {
      it('should be valid by default', async () => {
        const element = await basicFixture()
        assert.isTrue(element.checkValidity())
        assert.isTrue(element.validity.valid)
        assert.equal(element.validationMessage, '')
      })

      it('should be invalid when required and no value', async () => {
        const element = await requiredFixture()
        element.validate()
        await element.updateComplete

        assert.isFalse(element.checkValidity())
        assert.isFalse(element.validity.valid)
        assert.isTrue(element.validity.valueMissing)
        assert.equal(element.validationMessage, 'Please select an item.')
        assert.isTrue(element.invalid)
        assert.equal(element.invalidText, 'Please select an item.')
      })

      it('should be valid when required and has value', async () => {
        const element = await requiredFixture()
        element.value = 'apple'
        await element.updateComplete

        assert.isTrue(element.checkValidity())
        assert.isTrue(element.validity.valid)
        assert.equal(element.validationMessage, '')
        assert.isFalse(element.invalid)
      })

      it('should display invalid state', async () => {
        const element = await invalidFixture()
        assert.isTrue(element.invalid)
        assert.equal(element.invalidText, 'This field is required')
      })
    })

    describe('Keyboard interaction', () => {
      it('should open dropdown on Enter key', async () => {
        const element = await basicFixture()
        assert.isFalse(element.open)

        element.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter' }))
        await nextFrame()

        assert.isTrue(element.open)
      })

      it('should open dropdown on Space key', async () => {
        const element = await basicFixture()
        assert.isFalse(element.open)

        element.dispatchEvent(new KeyboardEvent('keydown', { key: ' ' }))
        await nextFrame()

        assert.isTrue(element.open)
      })

      it('should open dropdown on ArrowDown key', async () => {
        const element = await basicFixture()
        assert.isFalse(element.open)

        element.dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowDown' }))
        await nextFrame()

        assert.isTrue(element.open)
      })

      it('should open dropdown on ArrowUp key', async () => {
        const element = await basicFixture()
        assert.isFalse(element.open)

        element.dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowUp' }))
        await nextFrame()

        assert.isTrue(element.open)
      })

      it('should close dropdown on Escape key', async () => {
        const element = await basicFixture()
        element.open = true
        await element.updateComplete

        element.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape' }))
        await nextFrame()

        assert.isFalse(element.open)
      })

      it('should not respond to keyboard when disabled', async () => {
        const element = await disabledFixture()
        element.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter' }))
        await nextFrame()

        assert.isFalse(element.open)
      })

      it('should handle Tab key when menu is open', async () => {
        const element = await basicFixture()
        element.open = true
        await element.updateComplete

        element.dispatchEvent(new KeyboardEvent('keydown', { key: 'Tab' }))
        await element.updateComplete

        assert.isFalse(element.open)
      })
    })

    describe('Mouse interaction', () => {
      it('should open dropdown on click', async () => {
        const element = await basicFixture()
        assert.isFalse(element.open)

        element.click()
        await nextFrame()

        assert.isTrue(element.open)
      })

      it('should not respond to click when disabled', async () => {
        const element = await disabledFixture()
        element.click()
        await nextFrame()

        assert.isFalse(element.open)
      })

      it('should close on blur when focus leaves component', async () => {
        const element = await basicFixture()
        element.open = true
        await element.updateComplete

        // Simulate blur event with no related target (focus leaving completely)
        element.dispatchEvent(new FocusEvent('blur', { relatedTarget: null }))
        await nextFrame()

        assert.isFalse(element.open)
      })

      it('should not close on blur when focus moves to menu', async () => {
        const element = await basicFixture()
        element.open = true
        await element.updateComplete

        const menu = element.shadowRoot!.querySelector('.menu')!
        // Simulate blur event with menu as related target
        element.dispatchEvent(new FocusEvent('blur', { relatedTarget: menu as HTMLElement }))
        await nextFrame()

        assert.isTrue(element.open)
      })
    })

    describe('Selection events', () => {
      it('should dispatch change event when selection changes', async () => {
        const element = await basicFixture()
        element.open = true
        await element.updateComplete

        const changePromise = oneEvent(element, 'change')

        // Simulate option selection
        const option = element.querySelector('ui-option[value="apple"]') as UiOption
        element.handleSelect(new CustomEvent('select', { detail: { item: option } }))

        const changeEvent = (await changePromise) as CustomEvent<UiSelectChangeEvent>
        assert.equal(changeEvent.detail.value, 'apple')
        assert.equal(changeEvent.detail.item, option)
        assert.isFalse(changeEvent.bubbles)
        assert.isTrue(changeEvent.composed)
        assert.isFalse(element.open) // Should close after selection
      })

      it('should dispatch open event when opening', async () => {
        const element = await basicFixture()
        await element.updateComplete

        const openPromise = oneEvent(element, 'open')
        element.open = true
        await element.updateComplete

        const openEvent = await openPromise
        assert.isFalse(openEvent.bubbles)
        assert.isTrue(openEvent.composed)
      })

      it('should dispatch close event when closing', async () => {
        const element = await basicFixture()
        element.open = true
        await element.updateComplete

        const closePromise = oneEvent(element, 'close')
        element.open = false
        await element.updateComplete

        const closeEvent = await closePromise
        assert.isFalse(closeEvent.bubbles)
        assert.isTrue(closeEvent.composed)
      })
    })

    describe('Menu integration', () => {
      it('should highlight selected item when menu opens', async () => {
        const element = await withValueFixture()
        await element.updateComplete

        // Check that menu shows popover when opened
        element.open = true
        await element.updateComplete

        const menu = element.shadowRoot!.querySelector('ui-menu')!
        assert.isTrue(menu.matches(':popover-open'))
      })

      it('should highlight first item when no selection and menu opens', async () => {
        const element = await basicFixture()
        await element.updateComplete

        // Check that menu opens correctly
        element.open = true
        await element.updateComplete

        const menu = element.shadowRoot!.querySelector('ui-menu')!
        assert.isTrue(menu.matches(':popover-open'))
      })

      it('should handle menu close event', async () => {
        const element = await basicFixture()
        element.open = true
        await element.updateComplete

        element.handleMenuClose()
        assert.isFalse(element.open)
      })

      it('should handle highlight change events', async () => {
        const element = await basicFixture()
        const option = element.querySelector('ui-option') as UiOption
        option.id = 'test-option'

        element.handleHighlightChange(
          new CustomEvent('highlightchange', {
            detail: { item: option },
          })
        )

        await element.updateComplete

        // Check that the highlight change was processed
        // by verifying the DOM reflects the active descendant
        const container = element.shadowRoot!.querySelector('.ui-select')!
        assert.equal(container.getAttribute('aria-activedescendant'), 'test-option')
      })

      it('should clear aria-activedescendant when no item highlighted', async () => {
        const element = await basicFixture()
        element.handleHighlightChange(
          new CustomEvent('highlightchange', {
            detail: { item: null },
          })
        )

        // Check that aria-activedescendant is cleared
        const container = element.shadowRoot!.querySelector('.ui-select')!
        assert.equal(container.getAttribute('aria-activedescendant'), '')
      })
    })

    describe('Accessibility', () => {
      it('should update aria-expanded when opening/closing', async () => {
        const element = await basicFixture()
        await element.updateComplete

        element.open = true
        await element.updateComplete
        assert.equal(element.getAttribute('aria-expanded'), 'true')

        element.open = false
        await element.updateComplete
        assert.equal(element.getAttribute('aria-expanded'), 'false')
      })

      it('should set aria-label from label property', async () => {
        const element = await basicFixture()
        element.label = 'Test label'
        await element.updateComplete

        assert.equal(element.getAttribute('aria-label'), 'Test label')
      })

      it('should remove aria-label when label is cleared', async () => {
        const element = await basicFixture()
        element.label = undefined
        await element.updateComplete

        assert.isFalse(element.hasAttribute('aria-label'))
      })

      it('should support aria-activedescendant', async () => {
        const element = await basicFixture()
        const option = element.querySelector('ui-option') as UiOption
        option.id = 'test-id'

        // Simulate highlight change to set aria-activedescendant
        element.handleHighlightChange(
          new CustomEvent('highlightchange', {
            detail: { item: option },
          })
        )
        await element.updateComplete

        const container = element.shadowRoot!.querySelector('.ui-select')!
        assert.equal(container.getAttribute('aria-activedescendant'), 'test-id')
      })
    })

    describe('Edge cases', () => {
      it('should handle empty option list', async () => {
        const element = await emptyFixture()
        element.value = 'nonexistent'
        await element.updateComplete

        assert.equal(element.value, 'nonexistent')
        assert.isNull(element.selectedItem)
        assert.equal(element.renderValue, '')
      })

      it('should handle setting open before menu is rendered', async () => {
        const element = await basicFixture()
        await element.updateComplete

        // This should not throw an error even with timing issues
        element.open = true
        await element.updateComplete

        assert.isTrue(element.open)

        element.open = false
        await element.updateComplete

        assert.isFalse(element.open)
      })

      it('should maintain focus during interaction', async () => {
        const element = await basicFixture()
        const focusSpy = sinon.spy(element, 'focus')

        // Simulate selection
        const option = element.querySelector('ui-option[value="apple"]') as UiOption
        element.handleSelect(new CustomEvent('select', { detail: { item: option } }))

        assert.isTrue(focusSpy.called)
      })

      it('should handle selection with prevented event', async () => {
        const element = await basicFixture()
        element.open = true
        await element.updateComplete

        const event = new CustomEvent('select', {
          detail: { item: element.querySelector('ui-option') as UiOption },
        })
        const stopPropagationSpy = sinon.spy(event, 'stopPropagation')

        element.handleSelect(event)
        assert.isTrue(stopPropagationSpy.called)
      })
    })

    describe('Rendering', () => {
      it('should render with correct classes', async () => {
        const element = await basicFixture()
        await element.updateComplete

        const container = element.shadowRoot!.querySelector('.ui-select')!
        assert.isTrue(container.classList.contains('ui-select'))
        assert.isFalse(container.classList.contains('open'))
        assert.isFalse(container.classList.contains('disabled'))
      })

      it('should render with open class when open', async () => {
        const element = await basicFixture()
        element.open = true
        await element.updateComplete

        const container = element.shadowRoot!.querySelector('.ui-select')!
        assert.isTrue(container.classList.contains('open'))
      })

      it('should render with disabled class when disabled', async () => {
        const element = await disabledFixture()
        await element.updateComplete

        const container = element.shadowRoot!.querySelector('.ui-select')!
        assert.isTrue(container.classList.contains('disabled'))
      })

      it('should render focus ring', async () => {
        const element = await basicFixture()
        await element.updateComplete

        const focusRing = element.shadowRoot!.querySelector('md-focus-ring')!
        assert.isNotNull(focusRing)
        assert.equal(focusRing.getAttribute('part'), 'focus-ring')
      })

      it('should render text field with correct properties', async () => {
        const element = await withValueFixture()
        await element.updateComplete

        const textField = element.shadowRoot!.querySelector('ui-outlined-text-field')!
        assert.equal(textField.label, 'Select an option')
        // The text field should show the render value of the selected option
        assert.equal(textField.value, element.renderValue)
        assert.isTrue(textField.hasAttribute('readonly'))
        assert.equal(textField.tabIndex, -1)
        assert.isTrue(textField.hasAttribute('inert'))
        assert.equal(textField.getAttribute('aria-hidden'), 'true')
      })

      it('should render dropdown icon', async () => {
        const element = await basicFixture()
        await element.updateComplete

        const icon = element.shadowRoot!.querySelector('ui-icon[slot="suffix"]')!
        assert.equal(icon.textContent!.trim(), 'arrow_drop_down')
      })

      it('should render menu with correct attributes', async () => {
        const element = await basicFixture()
        await element.updateComplete

        const menu = element.shadowRoot!.querySelector('ui-menu')!
        assert.equal(menu.id, 'menu')
        assert.equal(menu.getAttribute('popover'), 'auto')
        assert.equal(menu.getAttribute('selector'), 'ui-option')
      })

      it('should slot options correctly', async () => {
        const element = await basicFixture()
        await element.updateComplete

        const options = element.querySelectorAll('ui-option')
        assert.equal(options.length, 3)
        assert.equal(options[0].value, 'apple')
        assert.equal(options[1].value, 'banana')
        assert.equal(options[2].value, 'cherry')
      })
    })
  })
})
