import { afterEach, beforeEach, describe, expect, it } from 'vitest'

import {
  _resetCache,
  applyTheme,
  getTheme,
  saveTheme,
  setThemePartial,
  teardownTheme,
} from '../../services/theme-engine/engine'
import { defaultTheme } from '../../services/theme-engine/presets'
import { setupThemeTest, teardownThemeTest } from '../../services/theme-engine/test-helpers'
import type { Theme } from '../../services/theme-engine/types'
import { GlassSlider } from './glass-slider'
import { IconGalleryBrowser } from './icon-gallery-browser'
import { IconSetPicker } from './icon-set-picker'
import { RadiusSlider } from './radius-slider'

function resetDom(): void {
  document.head.replaceChildren()
  document.body.replaceChildren()
  document.documentElement.removeAttribute('data-theme')
  document.documentElement.removeAttribute('data-accent')
  document.documentElement.removeAttribute('data-density')
  document.documentElement.removeAttribute('data-icon-set')
  document.documentElement.removeAttribute('style')
}

function requireButton(root: ParentNode, selector: string): HTMLButtonElement {
  const html = root.querySelector(selector)
  if (!(html instanceof HTMLButtonElement)) {
    throw new Error(`Expected ${selector} to match a button`)
  }
  return html
}

function requireElement(root: ParentNode, selector: string): HTMLElement {
  const html = root.querySelector(selector)
  if (!(html instanceof HTMLElement)) {
    throw new Error(`Expected ${selector} to match an element`)
  }
  return html
}

describe('GlassSlider duplicate DOM id', () => {
  beforeEach(() => setupThemeTest())
  afterEach(() => teardownThemeTest())

  it('assigns unique IDs when multiple instances exist', () => {
    const slider1 = new GlassSlider()
    const slider2 = new GlassSlider({ expanded: true })
    document.body.appendChild(slider1.el)
    document.body.appendChild(slider2.el)

    const allValueSpans = document.querySelectorAll('.preview-glass-slider__value[id]')
    expect(allValueSpans).toHaveLength(2)

    const id1 = (allValueSpans[0] as HTMLElement).id
    const id2 = (allValueSpans[1] as HTMLElement).id
    expect(id1).not.toBe(id2)

    const input1 = slider1.el.querySelector('input') as HTMLInputElement
    expect(input1.getAttribute('aria-describedby')).toBe(id1)

    const input2 = slider2.el.querySelector('input') as HTMLInputElement
    expect(input2.getAttribute('aria-describedby')).toBe(id2)

    slider1.destroy()
    slider2.destroy()
  })
})

describe('RadiusSlider destroy cancels pending rAF', () => {
  beforeEach(() => setupThemeTest())
  afterEach(() => teardownThemeTest())

  it('does not fire setThemePartial after destroy', async () => {
    const slider = new RadiusSlider()
    document.body.appendChild(slider.el)

    const input = slider.el.querySelector('.preview-radius-slider__input') as HTMLInputElement
    input.value = '18'
    input.dispatchEvent(new Event('input', { bubbles: true }))

    slider.destroy()

    const themeBefore = getTheme().radius

    await new Promise(resolve => requestAnimationFrame(resolve))
    await new Promise(resolve => setTimeout(resolve, 50))

    expect(getTheme().radius).toBe(themeBefore)
  })
})

describe('RadiusSlider input handling', () => {
  beforeEach(() => {
    localStorage.clear()
    teardownTheme()
    _resetCache()
    resetDom()
    applyTheme(defaultTheme())
  })

  afterEach(() => {
    teardownTheme()
    _resetCache()
    localStorage.clear()
    resetDom()
  })

  it('updates radius immediately on input', () => {
    const slider = new RadiusSlider()
    document.body.appendChild(slider.el)

    const input = slider.el.querySelector('.preview-radius-slider__input') as HTMLInputElement
    input.value = '18'
    input.dispatchEvent(new Event('input', { bubbles: true }))

    expect(getTheme().radius).toBe(18)
  })
})

describe('IconGalleryBrowser accessibility', () => {
  beforeEach(() => {
    localStorage.clear()
    teardownTheme()
    _resetCache()
    resetDom()
    applyTheme(defaultTheme())
  })

  afterEach(() => {
    teardownTheme()
    _resetCache()
    localStorage.clear()
    resetDom()
  })

  it('marks gallery controls and icon cells for assistive technology', () => {
    const picker = new IconGalleryBrowser()
    document.body.appendChild(picker.el)

    setThemePartial({ iconSet: 'countermeasure' })
    requireButton(picker.el, '.theme-studio-icon-gallery__toggle').click()

    expect(
      requireButton(picker.el, '.theme-studio-icon-gallery__toggle').getAttribute('aria-expanded')
    ).toBe('true')
    expect(
      requireButton(picker.el, '.theme-studio-icon-gallery__toggle')
        .querySelector('svg')
        ?.getAttribute('aria-hidden')
    ).toBe('true')
    expect(requireElement(picker.el, '.theme-studio-icon-gallery__cell').getAttribute('role')).toBe(
      'img'
    )
    expect(requireElement(picker.el, '.theme-studio-icon-gallery__cell').tabIndex).toBe(0)

    picker.destroy()
  })

  it.each([
    {
      iconSet: 'lucide',
      strokeWidth: '2',
      linecap: 'round',
      linejoin: 'round',
      heading: /All Icons — Lucide style/,
    },
    {
      iconSet: 'heroicons',
      strokeWidth: '2.4',
      linecap: 'round',
      linejoin: 'round',
      heading: /All Icons — Heroicons style/,
    },
    {
      iconSet: 'phosphor',
      strokeWidth: '1.5',
      linecap: 'round',
      linejoin: 'round',
      heading: /All Icons — Phosphor style/,
    },
    {
      iconSet: 'material',
      strokeWidth: '2',
      linecap: 'square',
      linejoin: 'miter',
      heading: /All Icons — Material style/,
    },
    {
      iconSet: 'countermeasure',
      strokeWidth: '1.75',
      linecap: 'round',
      linejoin: 'round',
      heading: /CounterMeasure Brand Icons/,
    },
  ] as const)(
    'applies the $iconSet stroke and linecap to gallery cells',
    ({ iconSet, strokeWidth, linecap, linejoin, heading }) => {
      const picker = new IconGalleryBrowser()
      document.body.appendChild(picker.el)

      setThemePartial({ iconSet })
      requireButton(picker.el, '.theme-studio-icon-gallery__toggle').click()

      const heading_el = requireElement(picker.el, '.theme-studio-icon-gallery__heading')
      expect(heading_el.textContent ?? '').toMatch(heading)

      const svg = picker.el.querySelector('.theme-studio-icon-gallery__cell svg')
      expect(svg).toBeTruthy()
      expect(svg?.getAttribute('stroke-width')).toBe(strokeWidth)
      expect(svg?.getAttribute('stroke-linecap')).toBe(linecap)
      expect(svg?.getAttribute('stroke-linejoin')).toBe(linejoin)

      picker.destroy()
    }
  )
})

describe('IconSetPicker event listener cleanup', () => {
  beforeEach(() => {
    localStorage.clear()
    teardownTheme()
    _resetCache()
    resetDom()
    applyTheme(defaultTheme())
  })

  afterEach(() => {
    teardownTheme()
    _resetCache()
    localStorage.clear()
    resetDom()
  })

  it('removes cm:theme-change listener on destroy', () => {
    const picker = new IconSetPicker()
    document.body.appendChild(picker.el)

    // The picker should have a destroy method
    expect(typeof picker.destroy).toBe('function')

    // After destroy, theme changes should not update the picker
    picker.destroy()

    // Change the theme - if listener leaked, the picker would still react
    setThemePartial({ iconSet: 'countermeasure' })

    // Verify the picker tiles are NOT updated (the 'is-active' class should
    // still reflect the old state, not the new one)
    const activeTile = picker.el.querySelector('.theme-studio-icon-tile.is-active')
    // After destroy + theme change, the active tile should still show the old
    // state (not 'countermeasure') because the listener was removed
    if (activeTile) {
      expect(activeTile.getAttribute('data-icon-set')).not.toBe('countermeasure')
    }
  })
})

describe('IconSetPicker decorative SVG accessibility', () => {
  beforeEach(() => {
    localStorage.clear()
    teardownTheme()
    _resetCache()
    resetDom()
    applyTheme(defaultTheme())
  })

  afterEach(() => {
    teardownTheme()
    _resetCache()
    localStorage.clear()
    resetDom()
  })

  it('marks preview SVGs as aria-hidden', () => {
    const picker = new IconSetPicker()
    document.body.appendChild(picker.el)

    const svgs = picker.el.querySelectorAll('.theme-studio-icon-tile__row')
    expect(svgs.length).toBeGreaterThan(0)

    for (const svg of svgs) {
      expect(svg.getAttribute('aria-hidden')).toBe('true')
    }

    picker.destroy()
  })
})

describe('applyTheme persists sanitized values', () => {
  beforeEach(() => setupThemeTest(false))
  afterEach(() => teardownThemeTest())

  it('applies sanitized defaults when invalid enum values are passed', () => {
    const corrupted: Theme = {
      ...defaultTheme(),
      mode: 'invalid-mode' as Theme['mode'],
      accent: 'bogus-accent' as Theme['accent'],
    }

    saveTheme(corrupted)

    // saveTheme sanitizes enum fields before persisting. Reload from
    // localStorage to verify the round-trip preserves safe defaults.
    _resetCache()
    const reloaded = getTheme()
    applyTheme(reloaded)

    // After applyTheme, the DOM reflects sanitized values.
    const root = document.documentElement
    expect(root.getAttribute('data-theme')).toBe('dark')
  })
})
