import { createInjector } from '@furystack/inject'
import { createComponent, flushUpdates, initializeShadeRoot } from '@furystack/shades'
import { usingAsync } from '@furystack/utils'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { CommandPaletteManager } from './command-palette-manager.js'
import { CommandPaletteSuggestionList } from './command-palette-suggestion-list.js'
import type { CommandPaletteSuggestionResult } from './command-provider.js'

describe('CommandPaletteSuggestionList', () => {
  let originalAnimate: typeof Element.prototype.animate
  let animateCalls: Array<{ keyframes: unknown; options: unknown }>

  beforeEach(() => {
    document.body.innerHTML = '<div id="root" style="width: 500px;"></div>'
    animateCalls = []
    originalAnimate = Element.prototype.animate

    Element.prototype.animate = vi.fn(
      (keyframes: Keyframe[] | PropertyIndexedKeyframes | null, options?: number | KeyframeAnimationOptions) => {
        animateCalls.push({ keyframes, options })
        const mockAnimation = {
          onfinish: null as ((event: AnimationPlaybackEvent) => void) | null,
          oncancel: null as ((event: AnimationPlaybackEvent) => void) | null,
          cancel: vi.fn(),
          play: vi.fn(),
          pause: vi.fn(),
          finish: vi.fn(),
          addEventListener: vi.fn(),
          removeEventListener: vi.fn(),
        }

        setTimeout(() => {
          mockAnimation.onfinish?.({} as AnimationPlaybackEvent)
        }, 10)

        return mockAnimation as unknown as Animation
      },
    )
  })

  afterEach(() => {
    document.body.innerHTML = ''
    Element.prototype.animate = originalAnimate
    vi.restoreAllMocks()
  })

  const createManager = () => {
    return new CommandPaletteManager([])
  }

  const createSuggestion = (text: string, score: number): CommandPaletteSuggestionResult => ({
    element: <span>{text}</span>,
    score,
    onSelected: vi.fn(),
  })

  it('should render as custom element', async () => {
    await usingAsync(createInjector(), async (injector) => {
      await usingAsync(createManager(), async (manager) => {
        const rootElement = document.getElementById('root') as HTMLDivElement

        initializeShadeRoot({
          injector,
          rootElement,
          jsxElement: <CommandPaletteSuggestionList manager={manager} />,
        })

        await flushUpdates()

        const component = document.querySelector('shade-command-palette-suggestion-list')
        expect(component).not.toBeNull()
      })
    })
  })

  it('should render suggestion items container', async () => {
    await usingAsync(createInjector(), async (injector) => {
      await usingAsync(createManager(), async (manager) => {
        const rootElement = document.getElementById('root') as HTMLDivElement

        initializeShadeRoot({
          injector,
          rootElement,
          jsxElement: <CommandPaletteSuggestionList manager={manager} />,
        })

        await flushUpdates()

        const component = document.querySelector('shade-command-palette-suggestion-list') as HTMLElement
        const container = component?.querySelector('.suggestion-items-container')
        expect(container).not.toBeNull()
      })
    })
  })

  it('should render suggestions from manager', async () => {
    await usingAsync(createInjector(), async (injector) => {
      await usingAsync(createManager(), async (manager) => {
        manager.currentSuggestions.setValue([createSuggestion('Command 1', 100), createSuggestion('Command 2', 90)])
        const rootElement = document.getElementById('root') as HTMLDivElement

        initializeShadeRoot({
          injector,
          rootElement,
          jsxElement: <CommandPaletteSuggestionList manager={manager} />,
        })

        await flushUpdates()

        const component = document.querySelector('shade-command-palette-suggestion-list') as HTMLElement
        const items = component?.querySelectorAll('.suggestion-item')
        expect(items?.length).toBe(2)
      })
    })
  })

  it('should render suggestion content', async () => {
    await usingAsync(createInjector(), async (injector) => {
      await usingAsync(createManager(), async (manager) => {
        manager.currentSuggestions.setValue([createSuggestion('Test Command', 100)])
        const rootElement = document.getElementById('root') as HTMLDivElement

        initializeShadeRoot({
          injector,
          rootElement,
          jsxElement: <CommandPaletteSuggestionList manager={manager} />,
        })

        await flushUpdates()

        const component = document.querySelector('shade-command-palette-suggestion-list') as HTMLElement
        const item = component?.querySelector('.suggestion-item')
        expect(item?.textContent).toContain('Test Command')
      })
    })
  })

  it('should mark first item as selected by default', async () => {
    await usingAsync(createInjector(), async (injector) => {
      await usingAsync(createManager(), async (manager) => {
        manager.currentSuggestions.setValue([createSuggestion('Command 1', 100), createSuggestion('Command 2', 90)])
        manager.selectedIndex.setValue(0)
        const rootElement = document.getElementById('root') as HTMLDivElement

        initializeShadeRoot({
          injector,
          rootElement,
          jsxElement: <CommandPaletteSuggestionList manager={manager} />,
        })

        await flushUpdates()

        const component = document.querySelector('shade-command-palette-suggestion-list') as HTMLElement
        const items = component?.querySelectorAll('.suggestion-item')
        expect(items?.[0]?.classList.contains('selected')).toBe(true)
        expect(items?.[1]?.classList.contains('selected')).toBe(false)
      })
    })
  })

  it('should update selected class when selectedIndex changes', async () => {
    await usingAsync(createInjector(), async (injector) => {
      await usingAsync(createManager(), async (manager) => {
        manager.currentSuggestions.setValue([createSuggestion('Command 1', 100), createSuggestion('Command 2', 90)])
        manager.selectedIndex.setValue(0)
        const rootElement = document.getElementById('root') as HTMLDivElement

        initializeShadeRoot({
          injector,
          rootElement,
          jsxElement: <CommandPaletteSuggestionList manager={manager} />,
        })

        await flushUpdates()

        manager.selectedIndex.setValue(1)
        await flushUpdates()

        const component = document.querySelector('shade-command-palette-suggestion-list') as HTMLElement
        const items = component?.querySelectorAll('.suggestion-item')
        expect(items?.[0]?.classList.contains('selected')).toBe(false)
        expect(items?.[1]?.classList.contains('selected')).toBe(true)
      })
    })
  })

  it('should call selectSuggestion when item is clicked while opened', async () => {
    await usingAsync(createInjector(), async (injector) => {
      await usingAsync(createManager(), async (manager) => {
        const suggestion = createSuggestion('Click Me', 100)
        manager.currentSuggestions.setValue([suggestion])
        manager.isOpened.setValue(true)
        const rootElement = document.getElementById('root') as HTMLDivElement

        initializeShadeRoot({
          injector,
          rootElement,
          jsxElement: <CommandPaletteSuggestionList manager={manager} />,
        })

        await flushUpdates()

        const selectSpy = vi.spyOn(manager, 'selectSuggestion')

        const component = document.querySelector('shade-command-palette-suggestion-list') as HTMLElement
        const item = component?.querySelector('.suggestion-item') as HTMLElement
        item?.click()

        expect(selectSpy).toHaveBeenCalledWith(injector, 0)
      })
    })
  })

  it('should not call selectSuggestion when item is clicked while closed', async () => {
    await usingAsync(createInjector(), async (injector) => {
      await usingAsync(createManager(), async (manager) => {
        const suggestion = createSuggestion('Click Me', 100)
        manager.currentSuggestions.setValue([suggestion])
        manager.isOpened.setValue(false)
        const rootElement = document.getElementById('root') as HTMLDivElement

        initializeShadeRoot({
          injector,
          rootElement,
          jsxElement: <CommandPaletteSuggestionList manager={manager} />,
        })

        await flushUpdates()

        const selectSpy = vi.spyOn(manager, 'selectSuggestion')

        const component = document.querySelector('shade-command-palette-suggestion-list') as HTMLElement
        const item = component?.querySelector('.suggestion-item') as HTMLElement
        item?.click()

        expect(selectSpy).not.toHaveBeenCalled()
      })
    })
  })

  it('should animate slide-in when opening', async () => {
    await usingAsync(createInjector(), async (injector) => {
      await usingAsync(createManager(), async (manager) => {
        manager.isOpened.setValue(false)
        const rootElement = document.getElementById('root') as HTMLDivElement

        initializeShadeRoot({
          injector,
          rootElement,
          jsxElement: <CommandPaletteSuggestionList manager={manager} />,
        })

        await flushUpdates()
        animateCalls = []

        manager.isOpened.setValue(true)
        await flushUpdates()

        const slideAnimation = animateCalls.find(
          (call) =>
            Array.isArray(call.keyframes) &&
            call.keyframes.some((kf: Keyframe) => kf.transform === 'translate(0, -50px)') &&
            call.keyframes.some((kf: Keyframe) => kf.transform === 'translate(0, 0)'),
        )

        expect(slideAnimation).toBeDefined()
        expect((slideAnimation?.options as KeyframeAnimationOptions)?.duration).toBe(500)
        expect((slideAnimation?.options as KeyframeAnimationOptions)?.fill).toBe('forwards')
      })
    })
  })

  it('should animate slide-out when closing', async () => {
    await usingAsync(createInjector(), async (injector) => {
      await usingAsync(createManager(), async (manager) => {
        manager.isOpened.setValue(true)
        const rootElement = document.getElementById('root') as HTMLDivElement

        initializeShadeRoot({
          injector,
          rootElement,
          jsxElement: <CommandPaletteSuggestionList manager={manager} />,
        })

        await flushUpdates()
        animateCalls = []

        manager.isOpened.setValue(false)
        await flushUpdates()

        const slideAnimation = animateCalls.find(
          (call) =>
            Array.isArray(call.keyframes) &&
            call.keyframes.some((kf: Keyframe) => kf.transform === 'translate(0, 0)') &&
            call.keyframes.some((kf: Keyframe) => kf.transform === 'translate(0, -50px)'),
        )

        expect(slideAnimation).toBeDefined()
        expect((slideAnimation?.options as KeyframeAnimationOptions)?.duration).toBe(200)
      })
    })
  })

  it('should set container display to initial when opening', async () => {
    await usingAsync(createInjector(), async (injector) => {
      await usingAsync(createManager(), async (manager) => {
        manager.isOpened.setValue(false)
        const rootElement = document.getElementById('root') as HTMLDivElement

        initializeShadeRoot({
          injector,
          rootElement,
          jsxElement: <CommandPaletteSuggestionList manager={manager} />,
        })

        await flushUpdates()

        manager.isOpened.setValue(true)
        await flushUpdates()

        const component = document.querySelector('shade-command-palette-suggestion-list') as HTMLElement
        const container = component?.querySelector('.suggestion-items-container') as HTMLElement
        expect(container?.style.display).toBe('initial')
      })
    })
  })

  it('should set container display to none when closing', async () => {
    await usingAsync(createInjector(), async (injector) => {
      await usingAsync(createManager(), async (manager) => {
        manager.isOpened.setValue(true)
        const rootElement = document.getElementById('root') as HTMLDivElement

        initializeShadeRoot({
          injector,
          rootElement,
          jsxElement: <CommandPaletteSuggestionList manager={manager} />,
        })

        await flushUpdates()

        manager.isOpened.setValue(false)
        await flushUpdates()

        const component = document.querySelector('shade-command-palette-suggestion-list') as HTMLElement
        const container = component?.querySelector('.suggestion-items-container') as HTMLElement
        expect(container?.style.display).toBe('none')
      })
    })
  })

  it('should render empty list when no suggestions', async () => {
    await usingAsync(createInjector(), async (injector) => {
      await usingAsync(createManager(), async (manager) => {
        manager.currentSuggestions.setValue([])
        const rootElement = document.getElementById('root') as HTMLDivElement

        initializeShadeRoot({
          injector,
          rootElement,
          jsxElement: <CommandPaletteSuggestionList manager={manager} />,
        })

        await flushUpdates()

        const component = document.querySelector('shade-command-palette-suggestion-list') as HTMLElement
        const items = component?.querySelectorAll('.suggestion-item')
        expect(items?.length).toBe(0)
      })
    })
  })

  it('should support fullScreenSuggestions prop', async () => {
    await usingAsync(createInjector(), async (injector) => {
      await usingAsync(createManager(), async (manager) => {
        manager.isOpened.setValue(true)
        const rootElement = document.getElementById('root') as HTMLDivElement

        initializeShadeRoot({
          injector,
          rootElement,
          jsxElement: <CommandPaletteSuggestionList manager={manager} fullScreenSuggestions />,
        })

        await flushUpdates()

        const component = document.querySelector('shade-command-palette-suggestion-list') as HTMLElement
        const container = component?.querySelector('.suggestion-items-container') as HTMLElement
        expect(container?.style.left).toBe('0px')
        expect(container?.style.width).toBe('calc(100% - 42px)')
      })
    })
  })

  it('should set max height based on window height', async () => {
    await usingAsync(createInjector(), async (injector) => {
      await usingAsync(createManager(), async (manager) => {
        manager.isOpened.setValue(true)
        const rootElement = document.getElementById('root') as HTMLDivElement

        initializeShadeRoot({
          injector,
          rootElement,
          jsxElement: <CommandPaletteSuggestionList manager={manager} />,
        })

        await flushUpdates()

        const component = document.querySelector('shade-command-palette-suggestion-list') as HTMLElement
        const container = component?.querySelector('.suggestion-items-container') as HTMLElement
        const expectedMaxHeight = `${window.innerHeight * 0.8}px`
        expect(container?.style.maxHeight).toBe(expectedMaxHeight)
      })
    })
  })

  it('should have correct CSS styles for suggestion items container', async () => {
    await usingAsync(createInjector(), async (injector) => {
      await usingAsync(createManager(), async (manager) => {
        const rootElement = document.getElementById('root') as HTMLDivElement

        initializeShadeRoot({
          injector,
          rootElement,
          jsxElement: <CommandPaletteSuggestionList manager={manager} />,
        })

        await flushUpdates()

        const component = document.querySelector('shade-command-palette-suggestion-list') as HTMLElement
        const container = component?.querySelector('.suggestion-items-container') as HTMLElement
        const computedStyle = window.getComputedStyle(container)

        expect(computedStyle.position).toBe('absolute')
        expect(computedStyle.overflow).toBe('hidden')
        expect(computedStyle.zIndex).toBe('1')
      })
    })
  })

  it('should call selectSuggestion with correct index for second item', async () => {
    await usingAsync(createInjector(), async (injector) => {
      await usingAsync(createManager(), async (manager) => {
        manager.currentSuggestions.setValue([
          createSuggestion('First', 100),
          createSuggestion('Second', 90),
          createSuggestion('Third', 80),
        ])
        manager.isOpened.setValue(true)
        const rootElement = document.getElementById('root') as HTMLDivElement

        initializeShadeRoot({
          injector,
          rootElement,
          jsxElement: <CommandPaletteSuggestionList manager={manager} />,
        })

        await flushUpdates()

        const selectSpy = vi.spyOn(manager, 'selectSuggestion')

        const component = document.querySelector('shade-command-palette-suggestion-list') as HTMLElement
        const items = component?.querySelectorAll('.suggestion-item')
        ;(items[1] as HTMLElement)?.click()

        expect(selectSpy).toHaveBeenCalledWith(injector, 1)
      })
    })
  })

  it('should update container z-index when opening and closing', async () => {
    await usingAsync(createInjector(), async (injector) => {
      await usingAsync(createManager(), async (manager) => {
        manager.isOpened.setValue(false)
        const rootElement = document.getElementById('root') as HTMLDivElement

        initializeShadeRoot({
          injector,
          rootElement,
          jsxElement: <CommandPaletteSuggestionList manager={manager} />,
        })

        await flushUpdates()

        manager.isOpened.setValue(true)
        await flushUpdates()

        const component = document.querySelector('shade-command-palette-suggestion-list') as HTMLElement
        const container = component?.querySelector('.suggestion-items-container') as HTMLElement
        expect(container?.style.zIndex).toBe('1')

        manager.isOpened.setValue(false)
        await flushUpdates()

        expect(container?.style.zIndex).toBe('-1')
      })
    })
  })
})
