import { fixture, assert, nextFrame, html, oneEvent } from '@open-wc/testing'
import sinon from 'sinon'
import CurrencyPicker from '../../../src/elements/currency/internals/Picker.js'
import '../../../src/elements/currency/currency-picker.js'

describe('CurrencyPicker', () => {
  async function basicFixture(): Promise<CurrencyPicker> {
    return fixture(html`<currency-picker></currency-picker>`)
  }

  async function multiSelectFixture(): Promise<CurrencyPicker> {
    return fixture(html`<currency-picker multi></currency-picker>`)
  }

  async function allowedCurrenciesFixture(): Promise<CurrencyPicker> {
    return fixture(html`<currency-picker .allowedCurrencies="${['USD', 'EUR', 'GBP']}"></currency-picker>`)
  }

  async function requiredFixture(): Promise<CurrencyPicker> {
    return fixture(html`<currency-picker required></currency-picker>`)
  }

  async function formFixture(): Promise<{ form: HTMLFormElement; picker: CurrencyPicker }> {
    const form = (await fixture(html`
      <form>
        <currency-picker name="currencies" .selected="${['USD']}"></currency-picker>
      </form>
    `)) as HTMLFormElement
    const picker = form.querySelector('currency-picker') as CurrencyPicker
    return { form, picker }
  }

  describe('Basic Functionality', () => {
    let picker: CurrencyPicker

    beforeEach(async () => {
      picker = await basicFixture()
    })

    it('should create element', () => {
      assert.instanceOf(picker, CurrencyPicker)
    })

    it('should have default properties', () => {
      assert.deepEqual(picker.selected, [])
      assert.deepEqual(picker.allowedCurrencies, [])
      assert.equal(picker.label, 'Add Currency')
      assert.isFalse(picker.multi)
      assert.isFalse(picker.required)
      assert.isFalse(picker.disabled)
      assert.isTrue(picker.showErrors)
    })

    it('should render currency selector', () => {
      const selector = picker.shadowRoot?.querySelector('md-outlined-select')
      assert.exists(selector)
      assert.equal(selector?.getAttribute('label'), 'Add Currency')
    })

    it('should populate currency options', async () => {
      await nextFrame()
      const options = picker.shadowRoot?.querySelectorAll('md-select-option')
      assert.exists(options)
      assert.isAtLeast(options?.length || 0, 25) // Should have popular currencies
    })
  })

  describe('Currency Selection', () => {
    let picker: CurrencyPicker

    beforeEach(async () => {
      picker = await basicFixture()
    })

    it('should select a currency', async () => {
      const select = picker.shadowRoot?.querySelector('md-outlined-select') as HTMLElement
      assert.exists(select)

      // Simulate selecting USD
      const changeEvent = new CustomEvent('change', {
        detail: { value: 'USD' },
      })
      Object.defineProperty(changeEvent, 'target', {
        value: { value: 'USD' },
        writable: false,
      })

      select.dispatchEvent(changeEvent)
      await nextFrame()

      assert.deepEqual(picker.selected, ['USD'])
    })

    it('should dispatch change event on user selection', async () => {
      const changePromise = oneEvent(picker, 'change')

      // Simulate user selecting a currency through the select element
      const select = picker.shadowRoot?.querySelector('md-outlined-select') as HTMLElement
      assert.exists(select)

      const changeEvent = new CustomEvent('change', {
        detail: { value: 'USD' },
      })
      Object.defineProperty(changeEvent, 'target', {
        value: { value: 'USD' },
        writable: false,
      })

      select.dispatchEvent(changeEvent)
      await nextFrame()

      const event = await changePromise
      assert.equal(event.type, 'change')
      assert.deepEqual(event.detail.codes, ['USD'])
      assert.equal(event.detail.currencies.length, 1)
      assert.equal(event.detail.currencies[0].code, 'USD')
    })

    it('should get selected currencies', async () => {
      picker.multi = true
      picker.selected = ['USD', 'EUR']
      await picker.updateComplete
      await nextFrame()
      const currencies = picker.getSelectedCurrencies()
      assert.equal(currencies.length, 2)
      assert.equal(currencies[0].code, 'USD')
      assert.equal(currencies[1].code, 'EUR')
    })

    it('should clear selection', async () => {
      picker.selected = ['USD', 'EUR']
      await nextFrame()

      picker.clearSelection()
      await nextFrame()

      assert.deepEqual(picker.selected, [])
    })
  })

  describe('Multi-select Mode', () => {
    let picker: CurrencyPicker

    beforeEach(async () => {
      picker = await multiSelectFixture()
    })

    it('should allow multiple selections', async () => {
      picker.selected = ['USD', 'EUR', 'GBP']
      await nextFrame()

      assert.equal(picker.selected.length, 3)
      assert.include(picker.selected, 'USD')
      assert.include(picker.selected, 'EUR')
      assert.include(picker.selected, 'GBP')
    })

    it('should render chips for selected currencies', async () => {
      picker.selected = ['USD', 'EUR']
      await nextFrame()

      const chipSet = picker.shadowRoot?.querySelector('ui-chip-set')
      const chips = picker.shadowRoot?.querySelectorAll('ui-chip')

      assert.exists(chipSet)
      assert.equal(chips?.length, 2)
    })

    it('should remove currency when chip is removed', async () => {
      picker.selected = ['USD', 'EUR']
      await nextFrame()

      const changePromise = oneEvent(picker, 'change')
      const chip = picker.shadowRoot?.querySelector('ui-chip[data-code="USD"]') as HTMLElement
      assert.exists(chip)

      // Simulate remove event
      const removeEvent = new CustomEvent('remove')
      Object.defineProperty(removeEvent, 'target', {
        value: { dataset: { code: 'USD' } },
        writable: false,
      })

      chip.dispatchEvent(removeEvent)
      await nextFrame()

      // Should update selection
      assert.deepEqual(picker.selected, ['EUR'])

      // Should dispatch change event
      const event = await changePromise
      assert.equal(event.type, 'change')
      assert.deepEqual(event.detail.codes, ['EUR'])
    })

    it('should filter out selected currencies from options', async () => {
      picker.selected = ['USD']
      await nextFrame()

      const selectableCurrencies = picker.shadowRoot?.querySelectorAll('md-select-option[value="USD"]')
      assert.equal(selectableCurrencies?.length, 0) // USD should not be in options
    })
  })

  describe('Allowed Currencies', () => {
    let picker: CurrencyPicker

    beforeEach(async () => {
      picker = await allowedCurrenciesFixture()
    })

    it('should filter options based on allowed currencies', async () => {
      await nextFrame()

      const options = picker.shadowRoot?.querySelectorAll('md-select-option[value]')
      const optionValues = Array.from(options || [])
        .map((option) => option.getAttribute('value'))
        .filter(Boolean)

      // Should only have USD, EUR, GBP (plus empty option)
      assert.isAtMost(optionValues.length, 3)
      assert.include(optionValues, 'USD')
      assert.include(optionValues, 'EUR')
      assert.include(optionValues, 'GBP')
    })

    it('should validate selected currencies against allowed currencies', async () => {
      const errorPromise = oneEvent(picker, 'error')

      // Try to set a currency not in allowed list
      picker.selected = ['JPY'] // Not in allowed list
      await nextFrame()

      const errorEvent = await errorPromise
      assert.equal(errorEvent.type, 'error')
      assert.equal(errorEvent.detail.error.type, 'validation')
    })
  })

  describe('Validation and Error Handling', () => {
    let picker: CurrencyPicker

    beforeEach(async () => {
      picker = await requiredFixture()
    })

    it('should show validation error when required and empty', async () => {
      const errorPromise = oneEvent(picker, 'error')

      picker.clearSelection()
      await nextFrame()

      const errorEvent = await errorPromise
      assert.equal(errorEvent.detail.error.type, 'selection')
      assert.include(errorEvent.detail.error.message, 'required')
    })

    it('should show error state in UI when showErrors is true', async () => {
      picker.showErrors = true
      picker.clearSelection() // This should trigger required validation error
      await picker.updateComplete
      const errorElement = picker.shadowRoot?.querySelector('.error')
      const selectElement = picker.shadowRoot?.querySelector('md-outlined-select')
      const isInvalid = !picker.validity.valid
      const hasAriaInvalid = selectElement?.getAttribute('aria-invalid') === 'true'

      assert.exists(errorElement, 'Error element should exist')
      assert.isTrue(isInvalid, 'Component should be in invalid state')
      assert.isTrue(hasAriaInvalid, 'Select element should have aria-invalid=true')
    })

    it('should not show error UI when showErrors is false', async () => {
      picker.showErrors = false
      picker.clearSelection() // This should trigger required validation error
      await nextFrame()

      const errorElement = picker.shadowRoot?.querySelector('.error')
      assert.isNull(errorElement)
    })

    it('should validate invalid currency codes', async () => {
      const errorPromise = oneEvent(picker, 'error')

      picker.selected = ['INVALID_CODE']
      await nextFrame()

      const errorEvent = await errorPromise
      assert.equal(errorEvent.detail.error.type, 'validation')
      assert.include(errorEvent.detail.error.message, 'Invalid currency codes')
    })

    it('should prevent multiple selections in single-select mode', async () => {
      picker.multi = false
      const errorPromise = oneEvent(picker, 'error')

      picker.selected = ['USD', 'EUR'] // Multiple selections in single-select mode
      await nextFrame()

      const errorEvent = await errorPromise
      assert.equal(errorEvent.detail.error.type, 'selection')
      assert.include(errorEvent.detail.error.message, 'Multiple currency selection is not allowed')
    })
  })

  describe('Form Integration', () => {
    let form: HTMLFormElement
    let picker: CurrencyPicker

    beforeEach(async () => {
      const fixture = await formFixture()
      form = fixture.form
      picker = fixture.picker
    })

    it('should be form-associated', () => {
      assert.isTrue(CurrencyPicker.formAssociated)
    })

    it('should have form value', async () => {
      picker.multi = true
      picker.selected = ['USD', 'EUR']
      await nextFrame()

      // Check ElementInternals form value
      const formData = new FormData(form)
      assert.equal(formData.get('currencies'), 'USD,EUR')
    })

    it('should reset with form', async () => {
      picker.selected = ['USD', 'EUR']
      await nextFrame()

      form.reset()
      await nextFrame()

      assert.deepEqual(picker.selected, [])
    })

    it('should restore form state', async () => {
      // Simulate form state restoration
      picker.multi = true
      picker.formStateRestoreCallback('USD,EUR')
      await nextFrame()

      assert.deepEqual(picker.selected, ['USD', 'EUR'])
    })
  })

  describe('Accessibility', () => {
    let picker: CurrencyPicker

    beforeEach(async () => {
      picker = await basicFixture()
    })

    it('should have proper ARIA attributes', async () => {
      await nextFrame()

      const container = picker.shadowRoot?.querySelector('.currency-picker')
      const select = picker.shadowRoot?.querySelector('md-outlined-select')

      assert.equal(container?.getAttribute('role'), 'group')
      assert.exists(container?.getAttribute('aria-label'))
      assert.exists(select?.getAttribute('aria-invalid'))
    })

    it('should update ARIA label based on selection', async () => {
      picker.selected = ['USD']
      await nextFrame()

      const container = picker.shadowRoot?.querySelector('.currency-picker')
      const ariaLabel = container?.getAttribute('aria-label')

      assert.include(ariaLabel || '', 'USD selected')
    })

    it('should have error announcements', async () => {
      picker.showErrors = true
      picker.required = true
      picker.clearSelection()
      await nextFrame()

      const errorElement = picker.shadowRoot?.querySelector('.error')
      assert.equal(errorElement?.getAttribute('role'), 'alert')
      assert.equal(errorElement?.getAttribute('aria-live'), 'polite')
    })

    it('should be keyboard navigable', async () => {
      const select = picker.shadowRoot?.querySelector('md-outlined-select') as HTMLElement
      assert.exists(select)

      // Focus should work
      select.focus()
      assert.equal(document.activeElement, picker)
    })
  })

  describe('Performance', () => {
    let picker: CurrencyPicker

    beforeEach(async () => {
      picker = await basicFixture()
    })

    it('should handle rapid property changes efficiently', async () => {
      const start = performance.now()

      // Rapidly change properties
      for (let i = 0; i < 100; i++) {
        picker.selected = i % 2 === 0 ? ['USD'] : ['EUR']
        picker.allowedCurrencies = i % 3 === 0 ? ['USD', 'EUR'] : ['USD', 'EUR', 'GBP']
      }

      await nextFrame()
      const end = performance.now()

      // Should complete in reasonable time (less than 100ms)
      assert.isBelow(end - start, 100)
    })

    it('should not re-render unnecessarily', async () => {
      const renderSpy = sinon.spy(picker, 'render')

      // Set same values multiple times
      picker.selected = ['USD']
      picker.selected = ['USD']
      picker.selected = ['USD']

      await nextFrame()

      // Should only render once after property stabilizes
      assert.isAtMost(renderSpy.callCount, 2) // Initial + one update

      renderSpy.restore()
    })
  })

  describe('Edge Cases', () => {
    let picker: CurrencyPicker

    beforeEach(async () => {
      picker = await basicFixture()
    })

    it('should handle empty arrays gracefully', async () => {
      picker.selected = []
      picker.allowedCurrencies = []
      await nextFrame()

      assert.deepEqual(picker.selected, [])
      assert.deepEqual(picker.getSelectedCurrencies(), [])
    })

    it('should handle invalid input types', async () => {
      const errorPromise = oneEvent(picker, 'error')

      // Try to set invalid types
      // eslint-disable-next-line @typescript-eslint/no-explicit-any
      picker.selected = 'invalid' as any
      await nextFrame()

      const errorEvent = await errorPromise
      assert.equal(errorEvent.detail.error.type, 'validation')
    })

    it('should handle disconnection/reconnection', async () => {
      picker.selected = ['USD']
      await nextFrame()

      // Disconnect and reconnect
      picker.remove()
      document.body.appendChild(picker)
      await nextFrame()

      assert.deepEqual(picker.selected, ['USD'])
    })

    it('should handle disabled state', async () => {
      picker.disabled = true
      await nextFrame()

      const select = picker.shadowRoot?.querySelector('md-outlined-select')
      assert.isTrue(select?.hasAttribute('disabled'))
    })
  })
})
