import { useState } from 'react'
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
import {
  act,
  createEvent,
  fireEvent,
  render,
  screen,
  waitFor,
} from '@testing-library/react'
import { WikilinkChatInput } from './WikilinkChatInput'
import { extractDroppedPathText } from './inlineWikilinkDropText'
import {
  UNSUPPORTED_INLINE_PASTE_MESSAGE,
} from './InlineWikilinkInput'
import { isInsertBeforeInput } from './inlineWikilinkBeforeInput'
import type { VaultEntry } from '../types'

type NativeDropPayload = {
  type: string
  paths: string[]
  position: { x: number; y: number }
}
type NativeDropHandler = (event: { payload: NativeDropPayload }) => void
const nativeDropState = vi.hoisted(() => ({
  tauriMode: false,
  handlers: {} as Record<string, NativeDropHandler | undefined>,
}))

vi.mock('../mock-tauri', () => ({
  isTauri: () => nativeDropState.tauriMode,
}))

vi.mock('@tauri-apps/api/window', () => ({
  getCurrentWindow: () => ({
    onDragDropEvent: vi.fn((handler: NativeDropHandler) => {
      nativeDropState.handlers['native-drag-drop'] = handler
      return Promise.resolve(() => {
        delete nativeDropState.handlers['native-drag-drop']
      })
    }),
  }),
}))

const makeEntry = (overrides: Partial<VaultEntry> = {}): VaultEntry => ({
  path: '/vault/note/test.md',
  filename: 'test.md',
  title: 'Test Note',
  isA: 'Note',
  aliases: [],
  belongsTo: [],
  relatedTo: [],
  status: null,
  owner: null,
  cadence: null,
  archived: false,
  modifiedAt: 1700000000,
  createdAt: 1700000000,
  fileSize: 100,
  snippet: '',
  wordCount: 0,
  relationships: {},
  icon: null,
  color: null,
  order: null,
  outgoingLinks: [],
  ...overrides,
})

const entries: VaultEntry[] = [
  makeEntry({ path: '/vault/alpha.md', title: 'Alpha', filename: 'alpha.md', isA: 'Project' }),
  makeEntry({ path: '/vault/beta.md', title: 'Beta', filename: 'beta.md', isA: 'Person', aliases: ['BLT'] }),
  makeEntry({ path: '/vault/gamma.md', title: 'Gamma', filename: 'gamma.md' }),
]

function Controlled({
  onSend,
  onUnsupportedPaste,
  disabled = false,
  placeholder,
  onDraftChange,
}: {
  onSend?: (text: string, refs: Array<{ title: string; path: string; type: string | null }>) => void
  onUnsupportedPaste?: (message: string) => void
  disabled?: boolean
  placeholder?: string
  onDraftChange?: (value: string) => void
}) {
  const [value, setValue] = useState('')
  const handleChange = (nextValue: string) => {
    onDraftChange?.(nextValue)
    setValue(nextValue)
  }

  return (
    <WikilinkChatInput
      entries={entries}
      value={value}
      onChange={handleChange}
      onSend={onSend ?? vi.fn()}
      onUnsupportedPaste={onUnsupportedPaste}
      disabled={disabled}
      placeholder={placeholder}
    />
  )
}

function setSelection(editor: HTMLElement, offset: number) {
  const selection = window.getSelection()
  if (!selection) return

  const targetNode = editor.firstChild ?? editor
  const safeOffset = targetNode.nodeType === Node.TEXT_NODE
    ? Math.min(offset, targetNode.textContent?.length ?? 0)
    : Math.min(offset, targetNode.childNodes.length)

  const range = document.createRange()
  range.setStart(targetNode, safeOffset)
  range.collapse(true)
  selection.removeAllRanges()
  selection.addRange(range)
}

function updateEditorText(text: string) {
  const editor = screen.getByTestId('agent-input')
  editor.focus()
  fireEvent.focus(editor)
  editor.textContent = text
  setSelection(editor, text.length)
  fireEvent.input(editor)
}

async function waitForCompositionFlush() {
  await act(async () => {
    await Promise.resolve()
  })
}

function clickFirstSuggestion() {
  const rows = screen.getByTestId('wikilink-menu').querySelectorAll('[class*="cursor-pointer"]')
  expect(rows.length).toBeGreaterThan(0)
  fireEvent.click(rows[0])
}

function fireComposingKeyDown(editor: HTMLElement, key: string) {
  const event = createEvent.keyDown(editor, {
    key,
    keyCode: 229,
    which: 229,
  })

  Object.defineProperty(event, 'isComposing', {
    configurable: true,
    value: true,
  })

  fireEvent(editor, event)
}

function createFileLikeDataTransfer({
  plainText = '',
  uriList = '',
}: {
  plainText?: string
  uriList?: string
}) {
  return {
    getData: vi.fn((type: string) => {
      if (type === 'text/plain') return plainText
      if (type === 'text/uri-list') return uriList
      return ''
    }),
    files: [new File(['folder'], 'Projects')],
    items: [{ kind: 'file', type: '' }],
  }
}

function resetNativeDropState() {
  nativeDropState.tauriMode = false
  for (const eventName of Object.keys(nativeDropState.handlers)) {
    delete nativeDropState.handlers[eventName]
  }
}

function mockElementRect(element: HTMLElement) {
  Object.defineProperty(element, 'getBoundingClientRect', {
    configurable: true,
    value: () => ({
      x: 0,
      y: 0,
      left: 0,
      top: 0,
      right: 400,
      bottom: 48,
      width: 400,
      height: 48,
      toJSON: () => ({}),
    }),
  })
}

function emitNativePathDrop(paths: string[], position = { x: 20, y: 20 }) {
  const handler = nativeDropState.handlers['native-drag-drop']
  if (!handler) throw new Error('No native drop handler registered')
  handler({
    payload: {
      type: 'drop',
      paths,
      position,
    },
  })
}

async function waitForNativePathDropListener() {
  await waitFor(() => {
    expect(nativeDropState.handlers['native-drag-drop']).toBeDefined()
  })
}

describe('WikilinkChatInput', () => {
  beforeEach(resetNativeDropState)
  afterEach(resetNativeDropState)

  it('renders the placeholder overlay for an empty draft', () => {
    render(<Controlled placeholder="Ask something..." />)
    expect(screen.getByText('Ask something...')).toBeInTheDocument()
    expect(screen.getByTestId('agent-input')).toHaveAttribute('aria-placeholder', 'Ask something...')
  })

  it('calls onChange when the draft changes', () => {
    const onChange = vi.fn()
    render(
      <WikilinkChatInput entries={entries} value="" onChange={onChange} onSend={vi.fn()} />,
    )

    updateEditorText('hello')
    expect(onChange).toHaveBeenCalledWith('hello')
  })

  it('shows wikilink suggestions after typing [[', () => {
    render(<Controlled />)
    updateEditorText('[[a')

    const menu = screen.getByTestId('wikilink-menu')
    expect(menu.textContent).toContain('Alpha')
  })

  it('matches suggestions by alias', () => {
    render(<Controlled />)
    updateEditorText('[[BLT')

    expect(screen.getByTestId('wikilink-menu').textContent).toContain('Beta')
  })

  it('renders selected wikilinks inline instead of in a separate pill strip', () => {
    render(<Controlled />)
    updateEditorText('edit my [[alp')
    clickFirstSuggestion()

    expect(screen.queryByTestId('reference-pill')).toBeNull()
    expect(screen.getByTestId('inline-wikilink-chip')).toBeInTheDocument()
    expect(screen.getByTestId('agent-input').textContent).toContain('Alpha')
  })

  it('selects a suggestion with Enter before sending the draft', () => {
    const onSend = vi.fn()
    render(<Controlled onSend={onSend} />)
    updateEditorText('edit my [[alp')

    fireEvent.keyDown(screen.getByTestId('agent-input'), { key: 'Enter' })

    expect(screen.getByTestId('inline-wikilink-chip')).toBeInTheDocument()
    expect(onSend).not.toHaveBeenCalled()
  })

  it('does not hijack Enter while IME composition is active', async () => {
    const onDraftChange = vi.fn()
    const onSend = vi.fn()
    render(<Controlled onDraftChange={onDraftChange} onSend={onSend} />)

    const editor = screen.getByTestId('agent-input')
    fireEvent.focus(editor)
    fireEvent.compositionStart(editor)
    editor.textContent = 'ni'
    setSelection(editor, 2)
    fireEvent.input(editor)

    expect(onDraftChange).not.toHaveBeenCalled()

    fireComposingKeyDown(editor, 'Enter')
    expect(onSend).not.toHaveBeenCalled()

    editor.textContent = '你'
    setSelection(editor, 1)
    fireEvent.compositionEnd(editor)

    await waitFor(() => {
      expect(onDraftChange).toHaveBeenCalledWith('你')
    })
    expect(editor.textContent).toContain('你')
  })

  it('does not select wikilink suggestions while IME composition is active', () => {
    render(<Controlled />)
    updateEditorText('[[a')

    const editor = screen.getByTestId('agent-input')
    fireEvent.compositionStart(editor)
    fireComposingKeyDown(editor, 'Enter')

    expect(screen.queryByTestId('inline-wikilink-chip')).toBeNull()
    expect(screen.getByTestId('wikilink-menu').textContent).toContain('Alpha')
  })

  it('clears IME-injected stray text nodes after composition ends', async () => {
    const onDraftChange = vi.fn()
    render(<Controlled onDraftChange={onDraftChange} />)
    const initialEditor = screen.getByTestId('agent-input') as HTMLDivElement
    initialEditor.focus()

    fireEvent.compositionStart(initialEditor)
    initialEditor.appendChild(document.createTextNode('你'))
    fireEvent.input(initialEditor)
    fireEvent.compositionEnd(initialEditor)

    await waitFor(() => {
      expect(onDraftChange).toHaveBeenCalledWith('你')
    })
    await waitFor(() => {
      const editor = screen.getByTestId('agent-input') as HTMLDivElement
      expect(editor.textContent).toBe('你')
    })
  })

  it('does not duplicate syllables across consecutive IME compositions', async () => {
    const onDraftChange = vi.fn()
    render(<Controlled onDraftChange={onDraftChange} />)

    const firstEditor = screen.getByTestId('agent-input') as HTMLDivElement
    firstEditor.focus()
    fireEvent.compositionStart(firstEditor)
    firstEditor.appendChild(document.createTextNode('안'))
    fireEvent.input(firstEditor)
    fireEvent.compositionEnd(firstEditor)
    await waitForCompositionFlush()

    await waitFor(() => {
      expect(onDraftChange).toHaveBeenLastCalledWith('안')
    })
    await waitFor(() => {
      expect(screen.getByTestId('agent-input').textContent).toBe('안')
    })

    const secondEditor = screen.getByTestId('agent-input') as HTMLDivElement
    secondEditor.focus()
    fireEvent.compositionStart(secondEditor)
    secondEditor.appendChild(document.createTextNode('녕'))
    fireEvent.input(secondEditor)
    fireEvent.compositionEnd(secondEditor)
    await waitForCompositionFlush()

    await waitFor(() => {
      expect(onDraftChange).toHaveBeenLastCalledWith('안녕')
    })
    expect(screen.getByTestId('agent-input').textContent).toBe('안녕')
  })

  it('lets Korean composition commit text reach the native input pipeline', async () => {
    const onDraftChange = vi.fn()
    render(<Controlled onDraftChange={onDraftChange} />)

    const editor = screen.getByTestId('agent-input') as HTMLDivElement
    editor.focus()

    fireEvent.compositionStart(editor)
    editor.textContent = '안녕하세'
    setSelection(editor, '안녕하세'.length)
    fireEvent.input(editor)
    fireEvent.compositionEnd(editor)

    const commitEvent = new Event('beforeinput', {
      bubbles: true,
      cancelable: true,
    })
    Object.defineProperties(commitEvent, {
      data: { value: '요' },
      inputType: { value: 'insertText' },
      isComposing: { value: false },
    })

    fireEvent(editor, commitEvent)

    expect(commitEvent.defaultPrevented).toBe(false)
    expect(onDraftChange).not.toHaveBeenCalled()

    editor.textContent = '안녕하세요'
    setSelection(editor, '안녕하세요'.length)
    fireEvent.input(editor)
    await waitForCompositionFlush()

    await waitFor(() => {
      expect(onDraftChange).toHaveBeenLastCalledWith('안녕하세요')
    })
    expect(screen.getByTestId('agent-input').textContent).toBe('안녕하세요')
  })

  it('does not steal focus back if it was moved elsewhere during composition end', async () => {
    const onDraftChange = vi.fn()
    render(
      <>
        <Controlled onDraftChange={onDraftChange} />
        <button type="button" data-testid="other-target">Other</button>
      </>,
    )
    const initialEditor = screen.getByTestId('agent-input') as HTMLDivElement
    const otherTarget = screen.getByTestId('other-target') as HTMLButtonElement
    initialEditor.focus()

    fireEvent.compositionStart(initialEditor)
    initialEditor.appendChild(document.createTextNode('你'))
    fireEvent.input(initialEditor)

    otherTarget.focus()
    fireEvent.compositionEnd(initialEditor)

    await waitFor(() => {
      expect(onDraftChange).toHaveBeenCalledWith('你')
    })
    expect(document.activeElement).toBe(otherTarget)
  })

  it('does not reset the DOM selection while IME composition is active', () => {
    const removeAllRanges = vi.spyOn(Selection.prototype, 'removeAllRanges')
    render(<Controlled />)
    const editor = screen.getByTestId('agent-input') as HTMLDivElement
    editor.focus()

    fireEvent.compositionStart(editor)
    editor.appendChild(document.createTextNode('ni'))
    setSelection(editor, 2)

    removeAllRanges.mockClear()
    fireEvent.keyUp(editor)
    fireEvent.click(editor)
    fireEvent.mouseUp(editor)

    expect(removeAllRanges).not.toHaveBeenCalled()
    expect(editor.textContent).toContain('ni')

    removeAllRanges.mockRestore()
  })

  it('lets committed composed characters reach the native input pipeline once', () => {
    const onDraftChange = vi.fn()
    render(<Controlled onDraftChange={onDraftChange} />)
    const editor = screen.getByTestId('agent-input')
    editor.focus()

    const portugueseText = 'á é í ç ã õ'
    const accentedKey = createEvent.keyDown(editor, { key: 'á' })
    fireEvent(editor, accentedKey)
    expect(accentedKey.defaultPrevented).toBe(false)

    editor.textContent = portugueseText
    setSelection(editor, portugueseText.length)
    fireEvent.input(editor)
    expect(onDraftChange).toHaveBeenLastCalledWith(portugueseText)

    const updatedEditor = screen.getByTestId('agent-input')
    const cjkKey = createEvent.keyDown(updatedEditor, { key: '你' })
    fireEvent(updatedEditor, cjkKey)
    expect(cjkKey.defaultPrevented).toBe(false)

    updatedEditor.textContent = `${portugueseText}你`
    setSelection(updatedEditor, portugueseText.length + 1)
    fireEvent.input(updatedEditor)
    expect(onDraftChange).toHaveBeenLastCalledWith(`${portugueseText}你`)
  })

  it('remounts after native DOM input so React does not diff a mutated contentEditable tree', async () => {
    const onDraftChange = vi.fn()
    render(<Controlled onDraftChange={onDraftChange} />)
    const initialEditor = screen.getByTestId('agent-input') as HTMLDivElement
    initialEditor.focus()

    initialEditor.textContent = 'follow up after edit'
    setSelection(initialEditor, 'follow up after edit'.length)
    fireEvent.input(initialEditor)

    await waitFor(() => {
      expect(onDraftChange).toHaveBeenLastCalledWith('follow up after edit')
    })

    const remountedEditor = screen.getByTestId('agent-input') as HTMLDivElement
    expect(remountedEditor).not.toBe(initialEditor)
    expect(remountedEditor.textContent).toBe('follow up after edit')
    expect(document.activeElement).toBe(remountedEditor)
  })

  it('deletes an inline chip with a single Backspace', () => {
    render(<Controlled />)
    updateEditorText('edit my [[alp')
    clickFirstSuggestion()

    const editor = screen.getByTestId('agent-input')
    fireEvent.keyDown(editor, { key: 'Backspace' })

    expect(screen.queryByTestId('inline-wikilink-chip')).toBeNull()
  })

  it('handles Command+Backspace as a stable delete-to-line-start edit', () => {
    const onDraftChange = vi.fn()
    render(<Controlled onDraftChange={onDraftChange} />)
    updateEditorText('first line\nsecond line')
    onDraftChange.mockClear()

    const editor = screen.getByTestId('agent-input')
    setSelection(editor, 'first line\nsecond'.length)
    const commandBackspace = createEvent.keyDown(editor, { key: 'Backspace', metaKey: true })
    fireEvent(editor, commandBackspace)

    expect(commandBackspace.defaultPrevented).toBe(true)
    expect(onDraftChange).toHaveBeenCalledWith('first line\n line')
    expect(screen.getByTestId('agent-input')).toHaveFocus()
  })

  it('lets non-Command modified delete shortcuts reach the browser editor pipeline', () => {
    const onDraftChange = vi.fn()
    render(<Controlled onDraftChange={onDraftChange} />)
    updateEditorText('delete the previous words')
    onDraftChange.mockClear()

    const editor = screen.getByTestId('agent-input')
    const optionBackspace = createEvent.keyDown(editor, { key: 'Backspace', altKey: true })
    fireEvent(editor, optionBackspace)
    const controlDelete = createEvent.keyDown(editor, { key: 'Delete', ctrlKey: true })
    fireEvent(editor, controlDelete)

    expect(optionBackspace.defaultPrevented).toBe(false)
    expect(controlDelete.defaultPrevented).toBe(false)
    expect(onDraftChange).not.toHaveBeenCalled()
  })

  it('submits serialized wikilink text and resolved references', () => {
    const onSend = vi.fn()
    render(<Controlled onSend={onSend} />)

    updateEditorText('edit my [[alpha]] essay')
    fireEvent.keyDown(screen.getByTestId('agent-input'), { key: 'Enter' })

    expect(onSend).toHaveBeenCalledWith('edit my [[alpha]] essay', [
      { title: 'Alpha', path: '/vault/alpha.md', type: 'Project' },
    ])
  })

  it('does not send on Shift+Enter', () => {
    const onSend = vi.fn()
    render(<Controlled onSend={onSend} />)

    updateEditorText('hello')
    fireEvent.keyDown(screen.getByTestId('agent-input'), { key: 'Enter', shiftKey: true })

    expect(onSend).not.toHaveBeenCalled()
  })

  it('inserts one controlled newline on Shift+Enter without duplicating the draft', () => {
    const onDraftChange = vi.fn()
    render(<Controlled onDraftChange={onDraftChange} />)

    updateEditorText('first line')
    fireEvent.keyDown(screen.getByTestId('agent-input'), { key: 'Enter', shiftKey: true })

    expect(onDraftChange).toHaveBeenLastCalledWith('first line\n')
    expect(screen.getByTestId('agent-input').textContent).toBe('first line\n')

    fireEvent.keyDown(screen.getByTestId('agent-input'), { key: 'Enter', shiftKey: true })

    expect(onDraftChange).toHaveBeenLastCalledWith('first line\n\n')
    expect(screen.getByTestId('agent-input').textContent).toBe('first line\n\n')
  })

  it('keeps a long draft anchored when Ctrl+Enter inserts a line break', () => {
    const onDraftChange = vi.fn()
    const onSend = vi.fn()
    render(<Controlled onDraftChange={onDraftChange} onSend={onSend} />)

    const longDraft = Array.from({ length: 24 }, (_, index) => `line ${index + 1}`).join('\n')
    updateEditorText(longDraft)
    const editor = screen.getByTestId('agent-input') as HTMLDivElement
    editor.scrollTop = 72
    onDraftChange.mockClear()

    fireEvent.keyDown(editor, { key: 'Enter', ctrlKey: true })

    const remountedEditor = screen.getByTestId('agent-input') as HTMLDivElement
    expect(onSend).not.toHaveBeenCalled()
    expect(onDraftChange).toHaveBeenLastCalledWith(`${longDraft}\n`)
    expect(remountedEditor).not.toBe(editor)
    expect(remountedEditor.scrollTop).toBe(72)
    expect(remountedEditor).toHaveFocus()
  })

  it('inserts one controlled newline from native insertLineBreak beforeinput', () => {
    const onDraftChange = vi.fn()
    render(<Controlled onDraftChange={onDraftChange} />)

    updateEditorText('first line')
    const editor = screen.getByTestId('agent-input')
    const beforeInputEvent = new Event('beforeinput', {
      bubbles: true,
      cancelable: true,
    })
    Object.defineProperty(beforeInputEvent, 'inputType', {
      value: 'insertLineBreak',
    })

    fireEvent(editor, beforeInputEvent)

    expect(beforeInputEvent.defaultPrevented).toBe(true)
    expect(onDraftChange).toHaveBeenLastCalledWith('first line\n')
    expect(screen.getByTestId('agent-input').textContent).toBe('first line\n')
  })

  it('submits a multi-line draft with normal Enter after Shift+Enter', () => {
    const onSend = vi.fn()
    render(<Controlled onSend={onSend} />)

    updateEditorText('first line')
    fireEvent.keyDown(screen.getByTestId('agent-input'), { key: 'Enter', shiftKey: true })

    const editor = screen.getByTestId('agent-input')
    editor.textContent = 'first line\nsecond line'
    setSelection(editor, 'first line\nsecond line'.length)
    fireEvent.input(editor)
    fireEvent.keyDown(screen.getByTestId('agent-input'), { key: 'Enter' })

    expect(onSend).toHaveBeenCalledWith('first line\nsecond line', [])
  })

  it('marks the editor disabled when disabled is true', () => {
    render(<Controlled disabled />)

    const editor = screen.getByTestId('agent-input')
    expect(editor).toHaveAttribute('contenteditable', 'false')
    expect(editor).toHaveAttribute('aria-disabled', 'true')
  })

  it('rejects pasted images without freezing the editor', () => {
    const onUnsupportedPaste = vi.fn()
    render(<Controlled onUnsupportedPaste={onUnsupportedPaste} />)

    const editor = screen.getByTestId('agent-input')
    const clipboardData = {
      getData: vi.fn(() => ''),
      files: [new File(['image'], 'paste.png', { type: 'image/png' })],
      items: [{ kind: 'file', type: 'image/png' }],
    }

    fireEvent.paste(editor, { clipboardData })

    expect(onUnsupportedPaste).toHaveBeenCalledWith(UNSUPPORTED_INLINE_PASTE_MESSAGE)

    updateEditorText('still works')
    expect(editor.textContent).toContain('still works')
  })

  it('extracts dropped folder paths from text/plain payloads', () => {
    expect(extractDroppedPathText(
      createFileLikeDataTransfer({
        plainText: '/Users/test/Projects',
      }) as DataTransfer,
    )).toBe('/Users/test/Projects')
  })

  it('falls back to file URLs exposed through uri lists', () => {
    expect(extractDroppedPathText(
      createFileLikeDataTransfer({
        uriList: 'file:///Users/test/My%20Folder',
      }) as DataTransfer,
    )).toBe('"/Users/test/My Folder"')
  })

  it('inserts Tauri native folder drops into the AI composer', async () => {
    nativeDropState.tauriMode = true
    const onDraftChange = vi.fn()
    render(<Controlled onDraftChange={onDraftChange} />)

    const editor = screen.getByTestId('agent-input') as HTMLDivElement
    mockElementRect(editor)
    await waitForNativePathDropListener()
    updateEditorText('Open ')
    editor.focus()
    onDraftChange.mockClear()

    act(() => {
      emitNativePathDrop(['/Users/test/My Folder'])
    })

    await waitFor(() => {
      expect(onDraftChange).toHaveBeenCalledWith('Open "/Users/test/My Folder"')
    })
    await waitFor(() => {
      expect(screen.getByTestId('agent-input').textContent).toContain('/Users/test/My Folder')
    })
  })

  it('accepts Tauri native folder drops for the focused AI composer even when native coordinates miss', async () => {
    nativeDropState.tauriMode = true
    const onDraftChange = vi.fn()
    render(<Controlled onDraftChange={onDraftChange} />)

    const editor = screen.getByTestId('agent-input') as HTMLDivElement
    mockElementRect(editor)
    await waitForNativePathDropListener()
    updateEditorText('Open ')
    onDraftChange.mockClear()

    act(() => {
      emitNativePathDrop(['/Users/test/Projects'], { x: 900, y: 900 })
    })

    await waitFor(() => {
      expect(onDraftChange).toHaveBeenCalledWith('Open /Users/test/Projects')
    })
  })

  it('treats missing inputType as a non-insert beforeinput event', () => {
    expect(() => isInsertBeforeInput({} as InputEvent)).not.toThrow()
    expect(isInsertBeforeInput({} as InputEvent)).toBe(false)
    expect(isInsertBeforeInput({ inputType: 42 })).toBe(false)
    expect(isInsertBeforeInput({ inputType: 'insertFromPaste' } as InputEvent)).toBe(true)
  })

  it('ignores beforeinput events without inputType instead of crashing', () => {
    render(<Controlled />)

    const editor = screen.getByTestId('agent-input')
    const beforeInputEvent = new Event('beforeinput', {
      bubbles: true,
      cancelable: true,
    })

    expect(() => fireEvent(editor, beforeInputEvent)).not.toThrow()

    updateEditorText('still works')
    expect(editor.textContent).toContain('still works')
  })

  it('ignores beforeinput events with non-string inputType instead of calling startsWith', () => {
    render(<Controlled />)

    const editor = screen.getByTestId('agent-input')
    const startsWith = vi.fn(() => true)
    const beforeInputEvent = new Event('beforeinput', {
      bubbles: true,
      cancelable: true,
    })
    Object.defineProperty(beforeInputEvent, 'inputType', {
      value: { startsWith },
    })

    expect(() => fireEvent(editor, beforeInputEvent)).not.toThrow()
    expect(startsWith).not.toHaveBeenCalled()

    updateEditorText('still works')
    expect(editor.textContent).toContain('still works')
  })

  it('recovers if unsupported media lands in the editor DOM', async () => {
    const onUnsupportedPaste = vi.fn()
    render(<Controlled onUnsupportedPaste={onUnsupportedPaste} />)

    const editor = screen.getByTestId('agent-input')
    editor.innerHTML = '<img alt="paste" src="data:image/png;base64,abc" />'

    fireEvent.input(editor)

    expect(onUnsupportedPaste).toHaveBeenCalledWith(UNSUPPORTED_INLINE_PASTE_MESSAGE)
    await waitFor(() => {
      expect(screen.getByTestId('agent-input').querySelector('img')).toBeNull()
    })

    updateEditorText('still works')
    expect(screen.getByTestId('agent-input').textContent).toContain('still works')
  })
})
