import type { FindOptions } from '@furystack/core'
import type { Injector } from '@furystack/inject'
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 { CollectionService } from '../../services/collection-service.js'
import { DataGrid } from './data-grid.js'

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

describe('DataGrid', () => {
  beforeEach(() => {
    document.body.innerHTML = '<div id="root"></div>'
  })

  afterEach(() => {
    document.body.innerHTML = ''
  })

  const createTestService = () => {
    const service = new CollectionService<TestEntry>()
    service.data.setValue({
      count: 3,
      entries: [
        { id: 1, name: 'First' },
        { id: 2, name: 'Second' },
        { id: 3, name: 'Third' },
      ],
    })
    return service
  }

  const withTestGrid = async (
    fn: (ctx: {
      injector: Injector
      service: CollectionService<TestEntry>
      findOptions: FindOptions<TestEntry, Array<keyof TestEntry>>
      onFindOptionsChange: (options: FindOptions<TestEntry, Array<keyof TestEntry>>) => void
    }) => Promise<void>,
    opts?: { createService?: () => CollectionService<TestEntry> },
  ) => {
    await usingAsync(createInjector(), async (injector) => {
      await usingAsync(opts?.createService?.() ?? createTestService(), async (service) => {
        const findOptions: FindOptions<TestEntry, Array<keyof TestEntry>> = {}
        const onFindOptionsChange = vi.fn<(options: FindOptions<TestEntry, Array<keyof TestEntry>>) => void>()
        await fn({ injector, service, findOptions, onFindOptionsChange })
      })
    })
  }

  describe('rendering', () => {
    it('should render with columns', async () => {
      await withTestGrid(async ({ injector, service, findOptions, onFindOptionsChange }) => {
        const rootElement = document.getElementById('root') as HTMLDivElement

        initializeShadeRoot({
          injector,
          rootElement,
          jsxElement: (
            <DataGrid<TestEntry, 'id' | 'name'>
              columns={['id', 'name']}
              collectionService={service}
              findOptions={findOptions}
              onFindOptionsChange={onFindOptionsChange}
              styles={{}}
              headerComponents={{}}
              rowComponents={{}}
            />
          ),
        })

        await flushUpdates()

        const grid = document.querySelector('shade-data-grid')
        expect(grid).not.toBeNull()

        const headers = grid?.querySelectorAll('th')
        expect(headers?.length).toBe(2)
      })
    })

    it('should render table structure', async () => {
      await withTestGrid(async ({ injector, service, findOptions, onFindOptionsChange }) => {
        const rootElement = document.getElementById('root') as HTMLDivElement

        initializeShadeRoot({
          injector,
          rootElement,
          jsxElement: (
            <DataGrid<TestEntry, 'id' | 'name'>
              columns={['id', 'name']}
              collectionService={service}
              findOptions={findOptions}
              onFindOptionsChange={onFindOptionsChange}
              styles={{}}
              headerComponents={{}}
              rowComponents={{}}
            />
          ),
        })

        await flushUpdates()

        const grid = document.querySelector('shade-data-grid')
        const table = grid?.querySelector('table')
        const thead = grid?.querySelector('thead')
        const tbody = grid?.querySelector('tbody')

        expect(table).not.toBeNull()
        expect(thead).not.toBeNull()
        expect(tbody).not.toBeNull()
      })
    })

    it('should render custom header components when provided', async () => {
      await withTestGrid(async ({ injector, service, findOptions, onFindOptionsChange }) => {
        const rootElement = document.getElementById('root') as HTMLDivElement

        initializeShadeRoot({
          injector,
          rootElement,
          jsxElement: (
            <DataGrid<TestEntry, 'id' | 'name'>
              columns={['id', 'name']}
              collectionService={service}
              findOptions={findOptions}
              onFindOptionsChange={onFindOptionsChange}
              styles={{}}
              headerComponents={{
                id: () => <span data-testid="custom-header-id">Custom ID Header</span>,
              }}
              rowComponents={{}}
            />
          ),
        })

        await flushUpdates()

        const grid = document.querySelector('shade-data-grid')
        const customHeader = grid?.querySelector('[data-testid="custom-header-id"]')
        expect(customHeader).not.toBeNull()
        expect(customHeader?.textContent).toBe('Custom ID Header')
      })
    })

    it('should render default header components from headerComponents.default', async () => {
      await withTestGrid(async ({ injector, service, findOptions, onFindOptionsChange }) => {
        const rootElement = document.getElementById('root') as HTMLDivElement

        initializeShadeRoot({
          injector,
          rootElement,
          jsxElement: (
            <DataGrid<TestEntry, 'id' | 'name'>
              columns={['id', 'name']}
              collectionService={service}
              findOptions={findOptions}
              onFindOptionsChange={onFindOptionsChange}
              styles={{}}
              headerComponents={{
                default: (name) => <span data-testid={`default-header-${name}`}>Default: {name}</span>,
              }}
              rowComponents={{}}
            />
          ),
        })

        await flushUpdates()

        const grid = document.querySelector('shade-data-grid')
        const defaultHeaderId = grid?.querySelector('[data-testid="default-header-id"]')
        const defaultHeaderName = grid?.querySelector('[data-testid="default-header-name"]')

        expect(defaultHeaderId?.textContent).toBe('Default: id')
        expect(defaultHeaderName?.textContent).toBe('Default: name')
      })
    })

    it('should render DataGridHeader when no custom header is provided', async () => {
      await withTestGrid(async ({ injector, service, findOptions, onFindOptionsChange }) => {
        const rootElement = document.getElementById('root') as HTMLDivElement

        initializeShadeRoot({
          injector,
          rootElement,
          jsxElement: (
            <DataGrid<TestEntry, 'id' | 'name'>
              columns={['id', 'name']}
              collectionService={service}
              findOptions={findOptions}
              onFindOptionsChange={onFindOptionsChange}
              styles={{}}
              headerComponents={{}}
              rowComponents={{}}
            />
          ),
        })

        await flushUpdates()

        const grid = document.querySelector('shade-data-grid')
        const defaultHeaders = grid?.querySelectorAll('data-grid-header')
        expect(defaultHeaders?.length).toBe(2)
      })
    })

    it('should render filter buttons when columnFilters are provided', async () => {
      await withTestGrid(async ({ injector, service, findOptions, onFindOptionsChange }) => {
        const rootElement = document.getElementById('root') as HTMLDivElement

        initializeShadeRoot({
          injector,
          rootElement,
          jsxElement: (
            <DataGrid<TestEntry, 'id' | 'name'>
              columns={['id', 'name']}
              collectionService={service}
              findOptions={findOptions}
              onFindOptionsChange={onFindOptionsChange}
              styles={{}}
              columnFilters={{ name: { type: 'string' } }}
            />
          ),
        })

        await flushUpdates()

        const grid = document.querySelector('shade-data-grid')
        const filterButtons = grid?.querySelectorAll('data-grid-filter-button')
        expect(filterButtons?.length).toBe(1)
      })
    })

    it('should not render filter buttons when columnFilters is not provided', async () => {
      await withTestGrid(async ({ injector, service, findOptions, onFindOptionsChange }) => {
        const rootElement = document.getElementById('root') as HTMLDivElement

        initializeShadeRoot({
          injector,
          rootElement,
          jsxElement: (
            <DataGrid<TestEntry, 'id' | 'name'>
              columns={['id', 'name']}
              collectionService={service}
              findOptions={findOptions}
              onFindOptionsChange={onFindOptionsChange}
              styles={{}}
            />
          ),
        })

        await flushUpdates()

        const grid = document.querySelector('shade-data-grid')
        const filterButtons = grid?.querySelectorAll('data-grid-filter-button')
        expect(filterButtons?.length).toBe(0)
      })
    })

    it('should render without headerComponents and rowComponents', async () => {
      await withTestGrid(async ({ injector, service, findOptions, onFindOptionsChange }) => {
        const rootElement = document.getElementById('root') as HTMLDivElement

        initializeShadeRoot({
          injector,
          rootElement,
          jsxElement: (
            <DataGrid<TestEntry, 'id' | 'name'>
              columns={['id', 'name']}
              collectionService={service}
              findOptions={findOptions}
              onFindOptionsChange={onFindOptionsChange}
              styles={{}}
            />
          ),
        })

        await flushUpdates()

        const grid = document.querySelector('shade-data-grid')
        expect(grid).not.toBeNull()

        const headers = grid?.querySelectorAll('data-grid-header')
        expect(headers?.length).toBe(2)
      })
    })

    it('should render with auto-generated data-nav-section attribute', async () => {
      await withTestGrid(async ({ injector, service, findOptions, onFindOptionsChange }) => {
        const rootElement = document.getElementById('root') as HTMLDivElement

        initializeShadeRoot({
          injector,
          rootElement,
          jsxElement: (
            <DataGrid<TestEntry, 'id' | 'name'>
              columns={['id', 'name']}
              collectionService={service}
              findOptions={findOptions}
              onFindOptionsChange={onFindOptionsChange}
            />
          ),
        })

        await flushUpdates()

        const wrapper = document.querySelector('.shade-grid-wrapper')
        const navSection = wrapper?.getAttribute('data-nav-section')
        expect(navSection).toBeTruthy()
        expect(navSection).toMatch(/^data-grid-/)
      })
    })

    it('should render with custom navSection', async () => {
      await withTestGrid(async ({ injector, service, findOptions, onFindOptionsChange }) => {
        const rootElement = document.getElementById('root') as HTMLDivElement

        initializeShadeRoot({
          injector,
          rootElement,
          jsxElement: (
            <DataGrid<TestEntry, 'id' | 'name'>
              columns={['id', 'name']}
              collectionService={service}
              findOptions={findOptions}
              onFindOptionsChange={onFindOptionsChange}
              navSection="my-grid"
            />
          ),
        })

        await flushUpdates()

        const wrapper = document.querySelector('.shade-grid-wrapper')
        expect(wrapper?.getAttribute('data-nav-section')).toBe('my-grid')
      })
    })
  })

  describe('focus management', () => {
    it('should clear hasFocus on focusout when focus leaves the grid', async () => {
      await withTestGrid(async ({ injector, service, findOptions, onFindOptionsChange }) => {
        const rootElement = document.getElementById('root') as HTMLDivElement
        const outsideBtn = document.createElement('button')
        document.body.appendChild(outsideBtn)

        initializeShadeRoot({
          injector,
          rootElement,
          jsxElement: (
            <DataGrid<TestEntry, 'id' | 'name'>
              columns={['id', 'name']}
              collectionService={service}
              findOptions={findOptions}
              onFindOptionsChange={onFindOptionsChange}
            />
          ),
        })

        await flushUpdates()
        await new Promise((r) => setTimeout(r, 0))

        service.hasFocus.setValue(true)

        const wrapper = document.querySelector('.shade-grid-wrapper') as HTMLElement
        wrapper?.dispatchEvent(new FocusEvent('focusout', { bubbles: true, relatedTarget: outsideBtn }))

        expect(service.hasFocus.getValue()).toBe(false)
        outsideBtn.remove()
      })
    })

    it('should clear hasFocus on focusout when focus moves outside', async () => {
      await withTestGrid(async ({ injector, service, findOptions, onFindOptionsChange }) => {
        const rootElement = document.getElementById('root') as HTMLDivElement
        const outsideEl = document.createElement('button')
        outsideEl.textContent = 'Outside'
        document.body.appendChild(outsideEl)

        initializeShadeRoot({
          injector,
          rootElement,
          jsxElement: (
            <DataGrid<TestEntry, 'id' | 'name'>
              columns={['id', 'name']}
              collectionService={service}
              findOptions={findOptions}
              onFindOptionsChange={onFindOptionsChange}
            />
          ),
        })

        await flushUpdates()
        await new Promise((r) => setTimeout(r, 0))

        service.hasFocus.setValue(true)

        const wrapper = document.querySelector('.shade-grid-wrapper') as HTMLElement
        wrapper?.dispatchEvent(new FocusEvent('focusout', { bubbles: true, relatedTarget: outsideEl }))

        expect(service.hasFocus.getValue()).toBe(false)

        outsideEl.remove()
      })
    })

    it('should clear hasFocus on focusout when relatedTarget is null', async () => {
      await withTestGrid(async ({ injector, service, findOptions, onFindOptionsChange }) => {
        const rootElement = document.getElementById('root') as HTMLDivElement

        initializeShadeRoot({
          injector,
          rootElement,
          jsxElement: (
            <DataGrid<TestEntry, 'id' | 'name'>
              columns={['id', 'name']}
              collectionService={service}
              findOptions={findOptions}
              onFindOptionsChange={onFindOptionsChange}
            />
          ),
        })

        await flushUpdates()
        await new Promise((r) => setTimeout(r, 0))

        service.hasFocus.setValue(true)

        const wrapper = document.querySelector('.shade-grid-wrapper') as HTMLElement
        wrapper?.dispatchEvent(new FocusEvent('focusout', { bubbles: true, relatedTarget: null }))

        expect(service.hasFocus.getValue()).toBe(false)
      })
    })
  })

  describe('keyboard navigation', () => {
    it('should move focus to next entry on ArrowDown', async () => {
      await withTestGrid(async ({ injector, service, findOptions, onFindOptionsChange }) => {
        const rootElement = document.getElementById('root') as HTMLDivElement

        service.hasFocus.setValue(true)
        service.focusedEntry.setValue(service.data.getValue().entries[0])

        initializeShadeRoot({
          injector,
          rootElement,
          jsxElement: (
            <DataGrid<TestEntry, 'id' | 'name'>
              columns={['id', 'name']}
              collectionService={service}
              findOptions={findOptions}
              onFindOptionsChange={onFindOptionsChange}
              styles={{}}
              headerComponents={{}}
              rowComponents={{}}
            />
          ),
        })

        await flushUpdates()

        const keydownEvent = new KeyboardEvent('keydown', { key: 'ArrowDown', bubbles: true })
        window.dispatchEvent(keydownEvent)

        expect(service.focusedEntry.getValue()).toEqual({ id: 2, name: 'Second' })
      })
    })

    it('should move focus to previous entry on ArrowUp', async () => {
      await withTestGrid(async ({ injector, service, findOptions, onFindOptionsChange }) => {
        const rootElement = document.getElementById('root') as HTMLDivElement

        service.hasFocus.setValue(true)
        service.focusedEntry.setValue(service.data.getValue().entries[1])

        initializeShadeRoot({
          injector,
          rootElement,
          jsxElement: (
            <DataGrid<TestEntry, 'id' | 'name'>
              columns={['id', 'name']}
              collectionService={service}
              findOptions={findOptions}
              onFindOptionsChange={onFindOptionsChange}
              styles={{}}
              headerComponents={{}}
              rowComponents={{}}
            />
          ),
        })

        await flushUpdates()

        const keydownEvent = new KeyboardEvent('keydown', { key: 'ArrowUp', bubbles: true })
        window.dispatchEvent(keydownEvent)

        expect(service.focusedEntry.getValue()).toEqual({ id: 1, name: 'First' })
      })
    })

    it('should handle Home to move focus to first entry', async () => {
      await withTestGrid(async ({ injector, service, findOptions, onFindOptionsChange }) => {
        const rootElement = document.getElementById('root') as HTMLDivElement

        service.hasFocus.setValue(true)
        service.focusedEntry.setValue(service.data.getValue().entries[2])

        initializeShadeRoot({
          injector,
          rootElement,
          jsxElement: (
            <DataGrid<TestEntry, 'id' | 'name'>
              columns={['id', 'name']}
              collectionService={service}
              findOptions={findOptions}
              onFindOptionsChange={onFindOptionsChange}
              styles={{}}
              headerComponents={{}}
              rowComponents={{}}
            />
          ),
        })

        await flushUpdates()

        const keydownEvent = new KeyboardEvent('keydown', { key: 'Home', bubbles: true })
        window.dispatchEvent(keydownEvent)

        expect(service.focusedEntry.getValue()).toEqual({ id: 1, name: 'First' })
      })
    })

    it('should handle End to move focus to last entry', async () => {
      await withTestGrid(async ({ injector, service, findOptions, onFindOptionsChange }) => {
        const rootElement = document.getElementById('root') as HTMLDivElement

        service.hasFocus.setValue(true)
        service.focusedEntry.setValue(service.data.getValue().entries[0])

        initializeShadeRoot({
          injector,
          rootElement,
          jsxElement: (
            <DataGrid<TestEntry, 'id' | 'name'>
              columns={['id', 'name']}
              collectionService={service}
              findOptions={findOptions}
              onFindOptionsChange={onFindOptionsChange}
              styles={{}}
              headerComponents={{}}
              rowComponents={{}}
            />
          ),
        })

        await flushUpdates()

        const keydownEvent = new KeyboardEvent('keydown', { key: 'End', bubbles: true })
        window.dispatchEvent(keydownEvent)

        expect(service.focusedEntry.getValue()).toEqual({ id: 3, name: 'Third' })
      })
    })

    it('should handle Escape to clear selection and search', async () => {
      await withTestGrid(async ({ injector, service, findOptions, onFindOptionsChange }) => {
        const rootElement = document.getElementById('root') as HTMLDivElement

        const { entries } = service.data.getValue()
        service.hasFocus.setValue(true)
        service.selection.setValue([entries[0], entries[1]])
        service.searchTerm.setValue('test')

        initializeShadeRoot({
          injector,
          rootElement,
          jsxElement: (
            <DataGrid<TestEntry, 'id' | 'name'>
              columns={['id', 'name']}
              collectionService={service}
              findOptions={findOptions}
              onFindOptionsChange={onFindOptionsChange}
              styles={{}}
              headerComponents={{}}
              rowComponents={{}}
            />
          ),
        })

        await flushUpdates()

        const keydownEvent = new KeyboardEvent('keydown', { key: 'Escape', bubbles: true })
        window.dispatchEvent(keydownEvent)

        expect(service.selection.getValue()).toEqual([])
        expect(service.searchTerm.getValue()).toBe('')
      })
    })

    it('should handle Space to toggle selection of focused entry', async () => {
      await withTestGrid(async ({ injector, service, findOptions, onFindOptionsChange }) => {
        const rootElement = document.getElementById('root') as HTMLDivElement

        const { entries } = service.data.getValue()
        service.hasFocus.setValue(true)
        service.focusedEntry.setValue(entries[0])

        initializeShadeRoot({
          injector,
          rootElement,
          jsxElement: (
            <DataGrid<TestEntry, 'id' | 'name'>
              columns={['id', 'name']}
              collectionService={service}
              findOptions={findOptions}
              onFindOptionsChange={onFindOptionsChange}
              styles={{}}
              headerComponents={{}}
              rowComponents={{}}
            />
          ),
        })

        await flushUpdates()

        const keydownEvent = new KeyboardEvent('keydown', { key: ' ', bubbles: true })
        window.dispatchEvent(keydownEvent)

        expect(service.selection.getValue()).toContain(entries[0])

        window.dispatchEvent(new KeyboardEvent('keydown', { key: ' ', bubbles: true }))
        expect(service.selection.getValue()).not.toContain(entries[0])
      })
    })

    it('should handle + to select all entries', async () => {
      await withTestGrid(async ({ injector, service, findOptions, onFindOptionsChange }) => {
        const rootElement = document.getElementById('root') as HTMLDivElement

        service.hasFocus.setValue(true)

        initializeShadeRoot({
          injector,
          rootElement,
          jsxElement: (
            <DataGrid<TestEntry, 'id' | 'name'>
              columns={['id', 'name']}
              collectionService={service}
              findOptions={findOptions}
              onFindOptionsChange={onFindOptionsChange}
              styles={{}}
              headerComponents={{}}
              rowComponents={{}}
            />
          ),
        })

        await flushUpdates()

        const keydownEvent = new KeyboardEvent('keydown', { key: '+', bubbles: true })
        window.dispatchEvent(keydownEvent)

        expect(service.selection.getValue().length).toBe(3)
      })
    })

    it('should handle - to deselect all entries', async () => {
      await withTestGrid(async ({ injector, service, findOptions, onFindOptionsChange }) => {
        const rootElement = document.getElementById('root') as HTMLDivElement

        const { entries } = service.data.getValue()
        service.hasFocus.setValue(true)
        service.selection.setValue([...entries])

        initializeShadeRoot({
          injector,
          rootElement,
          jsxElement: (
            <DataGrid<TestEntry, 'id' | 'name'>
              columns={['id', 'name']}
              collectionService={service}
              findOptions={findOptions}
              onFindOptionsChange={onFindOptionsChange}
              styles={{}}
              headerComponents={{}}
              rowComponents={{}}
            />
          ),
        })

        await flushUpdates()

        const keydownEvent = new KeyboardEvent('keydown', { key: '-', bubbles: true })
        window.dispatchEvent(keydownEvent)

        expect(service.selection.getValue().length).toBe(0)
      })
    })

    it('should handle * to invert selection', async () => {
      await withTestGrid(async ({ injector, service, findOptions, onFindOptionsChange }) => {
        const rootElement = document.getElementById('root') as HTMLDivElement

        const { entries } = service.data.getValue()
        service.hasFocus.setValue(true)
        service.selection.setValue([entries[0]])

        initializeShadeRoot({
          injector,
          rootElement,
          jsxElement: (
            <DataGrid<TestEntry, 'id' | 'name'>
              columns={['id', 'name']}
              collectionService={service}
              findOptions={findOptions}
              onFindOptionsChange={onFindOptionsChange}
              styles={{}}
              headerComponents={{}}
              rowComponents={{}}
            />
          ),
        })

        await flushUpdates()

        const keydownEvent = new KeyboardEvent('keydown', { key: '*', bubbles: true })
        window.dispatchEvent(keydownEvent)

        const selection = service.selection.getValue()
        expect(selection).not.toContain(entries[0])
        expect(selection).toContain(entries[1])
        expect(selection).toContain(entries[2])
      })
    })

    it('should not handle keyboard when not focused', async () => {
      await withTestGrid(async ({ injector, service, findOptions, onFindOptionsChange }) => {
        const rootElement = document.getElementById('root') as HTMLDivElement

        service.focusedEntry.setValue(service.data.getValue().entries[0])

        initializeShadeRoot({
          injector,
          rootElement,
          jsxElement: (
            <DataGrid<TestEntry, 'id' | 'name'>
              columns={['id', 'name']}
              collectionService={service}
              findOptions={findOptions}
              onFindOptionsChange={onFindOptionsChange}
              styles={{}}
              headerComponents={{}}
              rowComponents={{}}
            />
          ),
        })

        await flushUpdates()

        service.hasFocus.setValue(false)

        const keydownEvent = new KeyboardEvent('keydown', { key: 'ArrowDown', bubbles: true })
        window.dispatchEvent(keydownEvent)

        expect(service.focusedEntry.getValue()).toEqual({ id: 1, name: 'First' })
      })
    })

    it('should handle Insert to toggle selection and move to next', async () => {
      await withTestGrid(async ({ injector, service, findOptions, onFindOptionsChange }) => {
        const rootElement = document.getElementById('root') as HTMLDivElement

        const { entries } = service.data.getValue()
        service.hasFocus.setValue(true)
        service.focusedEntry.setValue(entries[0])

        initializeShadeRoot({
          injector,
          rootElement,
          jsxElement: (
            <DataGrid<TestEntry, 'id' | 'name'>
              columns={['id', 'name']}
              collectionService={service}
              findOptions={findOptions}
              onFindOptionsChange={onFindOptionsChange}
              styles={{}}
              headerComponents={{}}
              rowComponents={{}}
            />
          ),
        })

        await flushUpdates()

        const keydownEvent = new KeyboardEvent('keydown', { key: 'Insert', bubbles: true })
        window.dispatchEvent(keydownEvent)

        expect(service.selection.getValue()).toContain(entries[0])
        expect(service.focusedEntry.getValue()).toEqual(entries[1])
      })
    })
  })

  describe('styles', () => {
    it('should apply wrapper styles when provided', async () => {
      await withTestGrid(async ({ injector, service, findOptions, onFindOptionsChange }) => {
        const rootElement = document.getElementById('root') as HTMLDivElement

        initializeShadeRoot({
          injector,
          rootElement,
          jsxElement: (
            <DataGrid<TestEntry, 'id' | 'name'>
              columns={['id', 'name']}
              collectionService={service}
              findOptions={findOptions}
              onFindOptionsChange={onFindOptionsChange}
              styles={{
                wrapper: { backgroundColor: 'red' },
              }}
              headerComponents={{}}
              rowComponents={{}}
            />
          ),
        })

        await flushUpdates()

        const grid = document.querySelector('shade-data-grid') as HTMLElement
        expect(grid?.style.backgroundColor).toBe('red')
      })
    })

    it('should apply header styles when provided', async () => {
      await withTestGrid(async ({ injector, service, findOptions, onFindOptionsChange }) => {
        const rootElement = document.getElementById('root') as HTMLDivElement

        initializeShadeRoot({
          injector,
          rootElement,
          jsxElement: (
            <DataGrid<TestEntry, 'id' | 'name'>
              columns={['id', 'name']}
              collectionService={service}
              findOptions={findOptions}
              onFindOptionsChange={onFindOptionsChange}
              styles={{
                header: { color: 'blue' },
              }}
              headerComponents={{}}
              rowComponents={{}}
            />
          ),
        })

        await flushUpdates()

        const grid = document.querySelector('shade-data-grid')
        const headers = grid?.querySelectorAll('th') as NodeListOf<HTMLElement>
        expect(headers?.[0]?.style.color).toBe('blue')
      })
    })
  })

  describe('empty and loading states', () => {
    it('should show empty component when no data', async () => {
      await withTestGrid(
        async ({ injector, service, findOptions, onFindOptionsChange }) => {
          const rootElement = document.getElementById('root') as HTMLDivElement

          initializeShadeRoot({
            injector,
            rootElement,
            jsxElement: (
              <DataGrid<TestEntry, 'id' | 'name'>
                columns={['id', 'name']}
                collectionService={service}
                findOptions={findOptions}
                onFindOptionsChange={onFindOptionsChange}
                styles={{}}
                headerComponents={{}}
                rowComponents={{}}
                emptyComponent={<div data-testid="empty-state">No data available</div>}
              />
            ),
          })

          await flushUpdates()

          const grid = document.querySelector('shade-data-grid')
          const emptyState = grid?.querySelector('[data-testid="empty-state"]')
          expect(emptyState).not.toBeNull()
          expect(emptyState?.textContent).toBe('No data available')
        },
        { createService: () => new CollectionService<TestEntry>() },
      )
    })
  })

  describe('row interactions', () => {
    it('should pass row click to collectionService', async () => {
      const onRowClick = vi.fn()
      await withTestGrid(async ({ injector, service, findOptions, onFindOptionsChange }) => {
        service.addListener('onRowClick', onRowClick)
        const rootElement = document.getElementById('root') as HTMLDivElement

        service.data.setValue({
          count: 1,
          entries: [{ id: 1, name: 'Test' }],
        })

        initializeShadeRoot({
          injector,
          rootElement,
          jsxElement: (
            <DataGrid<TestEntry, 'id' | 'name'>
              columns={['id', 'name']}
              collectionService={service}
              findOptions={findOptions}
              onFindOptionsChange={onFindOptionsChange}
              styles={{}}
              headerComponents={{}}
              rowComponents={{}}
            />
          ),
        })

        await flushUpdates()

        const grid = document.querySelector('shade-data-grid')
        const cell = grid?.querySelector('td') as HTMLTableCellElement
        cell?.click()

        expect(onRowClick).toHaveBeenCalledWith({ id: 1, name: 'Test' })
      })
    })

    it('should pass row double click to collectionService', async () => {
      const onRowDoubleClick = vi.fn()
      await withTestGrid(async ({ injector, service, findOptions, onFindOptionsChange }) => {
        service.addListener('onRowDoubleClick', onRowDoubleClick)
        const rootElement = document.getElementById('root') as HTMLDivElement

        service.data.setValue({
          count: 1,
          entries: [{ id: 1, name: 'Test' }],
        })

        initializeShadeRoot({
          injector,
          rootElement,
          jsxElement: (
            <DataGrid<TestEntry, 'id' | 'name'>
              columns={['id', 'name']}
              collectionService={service}
              findOptions={findOptions}
              onFindOptionsChange={onFindOptionsChange}
              styles={{}}
              headerComponents={{}}
              rowComponents={{}}
            />
          ),
        })

        await flushUpdates()

        const grid = document.querySelector('shade-data-grid')
        const cell = grid?.querySelector('td') as HTMLTableCellElement
        const dblClickEvent = new MouseEvent('dblclick', { bubbles: true })
        cell?.dispatchEvent(dblClickEvent)

        expect(onRowDoubleClick).toHaveBeenCalledWith({ id: 1, name: 'Test' })
      })
    })
  })

  describe('row spatial navigation attributes', () => {
    it('should set data-spatial-nav-target on rows', async () => {
      await withTestGrid(async ({ injector, service, findOptions, onFindOptionsChange }) => {
        const rootElement = document.getElementById('root') as HTMLDivElement

        initializeShadeRoot({
          injector,
          rootElement,
          jsxElement: (
            <DataGrid<TestEntry, 'id' | 'name'>
              columns={['id', 'name']}
              collectionService={service}
              findOptions={findOptions}
              onFindOptionsChange={onFindOptionsChange}
            />
          ),
        })

        await flushUpdates()

        const rows = document.querySelectorAll('shades-data-grid-row')
        for (const row of rows) {
          expect(row.hasAttribute('data-spatial-nav-target')).toBe(true)
        }
      })
    })

    it('should set tabIndex 0 on focused row and -1 on others', async () => {
      await withTestGrid(async ({ injector, service, findOptions, onFindOptionsChange }) => {
        const rootElement = document.getElementById('root') as HTMLDivElement

        service.focusedEntry.setValue(service.data.getValue().entries[1])

        initializeShadeRoot({
          injector,
          rootElement,
          jsxElement: (
            <DataGrid<TestEntry, 'id' | 'name'>
              columns={['id', 'name']}
              collectionService={service}
              findOptions={findOptions}
              onFindOptionsChange={onFindOptionsChange}
            />
          ),
        })

        await flushUpdates()

        const rows = document.querySelectorAll<HTMLTableRowElement>('shades-data-grid-row')
        expect(rows[0]?.tabIndex).toBe(-1)
        expect(rows[1]?.tabIndex).toBe(0)
        expect(rows[2]?.tabIndex).toBe(-1)
      })
    })

    it('should sync focusedEntry on row onfocus', async () => {
      await withTestGrid(async ({ injector, service, findOptions, onFindOptionsChange }) => {
        const rootElement = document.getElementById('root') as HTMLDivElement

        initializeShadeRoot({
          injector,
          rootElement,
          jsxElement: (
            <DataGrid<TestEntry, 'id' | 'name'>
              columns={['id', 'name']}
              collectionService={service}
              findOptions={findOptions}
              onFindOptionsChange={onFindOptionsChange}
            />
          ),
        })

        await flushUpdates()

        const rows = document.querySelectorAll('shades-data-grid-row')
        rows[2]?.dispatchEvent(new FocusEvent('focus'))

        expect(service.focusedEntry.getValue()).toEqual({ id: 3, name: 'Third' })
        expect(service.hasFocus.getValue()).toBe(true)
      })
    })
  })

  describe('keyboard listener cleanup', () => {
    it('should remove keyboard listener when component is disconnected', async () => {
      await withTestGrid(async ({ injector, service, findOptions, onFindOptionsChange }) => {
        const rootElement = document.getElementById('root') as HTMLDivElement

        service.hasFocus.setValue(true)
        service.focusedEntry.setValue(service.data.getValue().entries[0])

        initializeShadeRoot({
          injector,
          rootElement,
          jsxElement: (
            <DataGrid<TestEntry, 'id' | 'name'>
              columns={['id', 'name']}
              collectionService={service}
              findOptions={findOptions}
              onFindOptionsChange={onFindOptionsChange}
              styles={{}}
              headerComponents={{}}
              rowComponents={{}}
            />
          ),
        })

        await flushUpdates()

        const grid = document.querySelector('shade-data-grid') as HTMLElement
        grid.remove()

        await flushUpdates()

        window.dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowDown', bubbles: true }))
        expect(service.focusedEntry.getValue()).toEqual({ id: 1, name: 'First' })
      })
    })
  })
})
