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 { SuggestInput } from './suggest-input.js'
import { SuggestManager } from './suggest-manager.js'
import type { SuggestionResult } from './suggestion-result.js'

type TestEntry = { id: number; name: string }

const createSuggestionResult = (entry: TestEntry): SuggestionResult => ({
  element: <div>{entry.name}</div>,
  score: entry.id,
})

describe('SuggestInput', () => {
  let originalAnimate: typeof Element.prototype.animate

  beforeEach(() => {
    document.body.innerHTML = '<div id="root"></div>'
    originalAnimate = Element.prototype.animate

    Element.prototype.animate = vi.fn(() => {
      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(),
      }
      return mockAnimation as unknown as Animation
    })
  })

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

  const createManager = () => {
    const getEntries = vi.fn().mockResolvedValue([])
    const getSuggestionEntry = vi.fn().mockImplementation(createSuggestionResult)
    return new SuggestManager<TestEntry>(getEntries, getSuggestionEntry)
  }

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

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

      await flushUpdates()

      const suggestInput = document.querySelector('shades-suggest-input')
      expect(suggestInput).not.toBeNull()

      manager[Symbol.dispose]()
    })
  })

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

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

      await flushUpdates()

      const input = document.querySelector('shades-suggest-input input') as HTMLInputElement
      expect(input).not.toBeNull()
      expect(input.placeholder).toBe('Type to search...')
      expect(input.autofocus).toBe(true)

      manager[Symbol.dispose]()
    })
  })

  it('should focus input when isOpened becomes true', async () => {
    await usingAsync(createInjector(), async (injector) => {
      const rootElement = document.getElementById('root') as HTMLDivElement
      const manager = createManager()

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

      await flushUpdates()

      const input = document.querySelector('shades-suggest-input input') as HTMLInputElement
      const focusSpy = vi.spyOn(input, 'focus')

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

      expect(focusSpy).toHaveBeenCalled()

      manager[Symbol.dispose]()
    })
  })

  it('should clear input value when isOpened becomes false', async () => {
    await usingAsync(createInjector(), async (injector) => {
      const rootElement = document.getElementById('root') as HTMLDivElement
      const manager = createManager()

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

      await flushUpdates()

      const input = document.querySelector('shades-suggest-input input') as HTMLInputElement
      input.value = 'test query'

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

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

      expect(input.value).toBe('')

      manager[Symbol.dispose]()
    })
  })

  it('should not clear input when first mounted with isOpened false', async () => {
    await usingAsync(createInjector(), async (injector) => {
      const rootElement = document.getElementById('root') as HTMLDivElement
      const manager = createManager()

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

      await flushUpdates()

      const input = document.querySelector('shades-suggest-input input') as HTMLInputElement

      input.value = 'initial value'
      expect(input.value).toBe('initial value')

      manager[Symbol.dispose]()
    })
  })

  it('should have correct CSS styling applied', async () => {
    await usingAsync(createInjector(), async (injector) => {
      const rootElement = document.getElementById('root') as HTMLDivElement
      const manager = createManager()

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

      await flushUpdates()

      const suggestInput = document.querySelector('shades-suggest-input') as HTMLElement
      expect(suggestInput).not.toBeNull()

      const input = suggestInput.querySelector('input') as HTMLInputElement
      expect(input).not.toBeNull()

      manager[Symbol.dispose]()
    })
  })
})
