import { useState } from 'react'
import { describe, it, expect, vi } from 'vitest'
import { render, screen, fireEvent, act } from '@testing-library/react'
import { WikilinkChatInput } from './WikilinkChatInput'
import type { VaultEntry } from '../types'

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,
  trashed: false,
  trashedAt: null,
  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' }),
  makeEntry({ path: '/vault/gamma.md', title: 'Gamma', filename: 'gamma.md' }),
  makeEntry({ path: '/vault/trashed.md', title: 'Trashed', filename: 'trashed.md', trashed: true }),
  makeEntry({ path: '/vault/archived.md', title: 'Archived', filename: 'archived.md', archived: true }),
]

/** Wrapper that manages controlled value state so onChange/selectSuggestion work correctly. */
function Controlled({ onSend, ...props }: {
  entries: VaultEntry[]
  onSend?: (text: string, refs: Array<{ title: string; path: string; type: string | null }>) => void
  disabled?: boolean
  placeholder?: string
}) {
  const [value, setValue] = useState('')
  return (
    <WikilinkChatInput
      entries={props.entries}
      value={value}
      onChange={setValue}
      onSend={onSend ?? vi.fn()}
      disabled={props.disabled}
      placeholder={props.placeholder}
    />
  )
}

/** Helper: type in the input and wait for debounce to flush. */
async function typeAndWait(text: string) {
  fireEvent.change(screen.getByTestId('agent-input'), {
    target: { value: text, selectionStart: text.length },
  })
  await act(() => { vi.advanceTimersByTime(150) })
}

describe('WikilinkChatInput', () => {
  it('renders input with placeholder', () => {
    render(<Controlled entries={entries} placeholder="Ask something..." />)
    const input = screen.getByTestId('agent-input') as HTMLInputElement
    expect(input.placeholder).toBe('Ask something...')
  })

  it('calls onChange when typing', () => {
    const onChange = vi.fn()
    render(
      <WikilinkChatInput entries={entries} value="" onChange={onChange} onSend={vi.fn()} />,
    )
    fireEvent.change(screen.getByTestId('agent-input'), { target: { value: 'hello' } })
    expect(onChange).toHaveBeenCalledWith('hello')
  })

  it('shows wikilink menu when [[ is typed', async () => {
    vi.useFakeTimers()
    render(<Controlled entries={entries} />)
    await typeAndWait('[[a')
    expect(screen.getByTestId('wikilink-menu')).toBeTruthy()
    vi.useRealTimers()
  })

  it('filters suggestions matching query', async () => {
    vi.useFakeTimers()
    render(<Controlled entries={entries} />)
    await typeAndWait('[[alp')
    const menu = screen.getByTestId('wikilink-menu')
    expect(menu.textContent).toContain('Alpha')
    expect(menu.textContent).not.toContain('Beta')
    vi.useRealTimers()
  })

  it('excludes trashed and archived entries from suggestions', async () => {
    vi.useFakeTimers()
    render(<Controlled entries={entries} />)
    await typeAndWait('[[t')
    const menu = screen.queryByTestId('wikilink-menu')
    if (menu) {
      expect(menu.textContent).not.toContain('Trashed')
      expect(menu.textContent).not.toContain('Archived')
    }
    vi.useRealTimers()
  })

  it('selects suggestion on click and creates pill', async () => {
    vi.useFakeTimers()
    render(<Controlled entries={entries} />)
    await typeAndWait('[[a')

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

    const pills = screen.queryAllByTestId('reference-pill')
    expect(pills.length).toBe(1)
    vi.useRealTimers()
  })

  it('does not duplicate pills for same note', async () => {
    vi.useFakeTimers()
    render(<Controlled entries={entries} />)

    // First selection
    await typeAndWait('[[alp')
    fireEvent.click(screen.getByTestId('wikilink-menu').querySelectorAll('[class*="cursor-pointer"]')[0])
    expect(screen.queryAllByTestId('reference-pill').length).toBe(1)

    // Second selection of same note
    await typeAndWait('[[alp')
    fireEvent.click(screen.getByTestId('wikilink-menu').querySelectorAll('[class*="cursor-pointer"]')[0])
    expect(screen.queryAllByTestId('reference-pill').length).toBe(1) // No duplicate
    vi.useRealTimers()
  })

  it('removes pill when x button is clicked', async () => {
    vi.useFakeTimers()
    render(<Controlled entries={entries} />)

    await typeAndWait('[[alp')
    fireEvent.click(screen.getByTestId('wikilink-menu').querySelectorAll('[class*="cursor-pointer"]')[0])
    expect(screen.queryAllByTestId('reference-pill').length).toBe(1)

    const pill = screen.getByTestId('reference-pill')
    fireEvent.click(pill.querySelector('button')!)
    expect(screen.queryAllByTestId('reference-pill').length).toBe(0)
    vi.useRealTimers()
  })

  it('navigates suggestions with keyboard', async () => {
    vi.useFakeTimers()
    render(<Controlled entries={entries} />)
    await typeAndWait('[[a')

    const input = screen.getByTestId('agent-input')
    fireEvent.keyDown(input, { key: 'ArrowDown' })
    fireEvent.keyDown(input, { key: 'ArrowUp' })
    // Escape closes menu
    fireEvent.keyDown(input, { key: 'Escape' })
    expect(screen.queryByTestId('wikilink-menu')).toBeNull()
    vi.useRealTimers()
  })

  it('selects suggestion with Enter key', async () => {
    vi.useFakeTimers()
    render(<Controlled entries={entries} />)
    await typeAndWait('[[alp')
    expect(screen.getByTestId('wikilink-menu')).toBeTruthy()

    fireEvent.keyDown(screen.getByTestId('agent-input'), { key: 'Enter' })
    expect(screen.queryAllByTestId('reference-pill').length).toBe(1)
    vi.useRealTimers()
  })

  it('calls onSend with text and references on Enter without menu', async () => {
    vi.useFakeTimers()
    const onSend = vi.fn()
    render(<Controlled entries={entries} onSend={onSend} />)

    // Type non-wikilink text
    fireEvent.change(screen.getByTestId('agent-input'), {
      target: { value: 'hello', selectionStart: 5 },
    })
    fireEvent.keyDown(screen.getByTestId('agent-input'), { key: 'Enter' })
    expect(onSend).toHaveBeenCalledWith('hello', [])
    vi.useRealTimers()
  })

  it('does not send on Enter+Shift', () => {
    const onSend = vi.fn()
    render(
      <WikilinkChatInput entries={entries} value="hello" onChange={vi.fn()} onSend={onSend} />,
    )
    fireEvent.keyDown(screen.getByTestId('agent-input'), { key: 'Enter', shiftKey: true })
    expect(onSend).not.toHaveBeenCalled()
  })

  it('does not send on Enter when input is empty', () => {
    const onSend = vi.fn()
    render(
      <WikilinkChatInput entries={entries} value="" onChange={vi.fn()} onSend={onSend} />,
    )
    fireEvent.keyDown(screen.getByTestId('agent-input'), { key: 'Enter' })
    expect(onSend).not.toHaveBeenCalled()
  })

  it('disables input when disabled prop is true', () => {
    render(<Controlled entries={entries} disabled />)
    expect((screen.getByTestId('agent-input') as HTMLInputElement).disabled).toBe(true)
  })

  it('closes menu when ]] is typed', async () => {
    vi.useFakeTimers()
    render(<Controlled entries={entries} />)
    await typeAndWait('[[alp')
    expect(screen.getByTestId('wikilink-menu')).toBeTruthy()

    // Type ]] to close
    fireEvent.change(screen.getByTestId('agent-input'), {
      target: { value: '[[alp]]', selectionStart: 7 },
    })
    expect(screen.queryByTestId('wikilink-menu')).toBeNull()
    vi.useRealTimers()
  })

  it('shows type badge for non-Note types in suggestions', async () => {
    vi.useFakeTimers()
    render(<Controlled entries={entries} />)
    await typeAndWait('[[alp')
    const menu = screen.getByTestId('wikilink-menu')
    expect(menu.textContent).toContain('Project')
    vi.useRealTimers()
  })

  it('matches by alias', async () => {
    vi.useFakeTimers()
    const entriesWithAlias = [
      ...entries,
      makeEntry({ path: '/vault/delta.md', title: 'Delta', filename: 'delta.md', aliases: ['DLT'] }),
    ]
    render(<Controlled entries={entriesWithAlias} />)
    await typeAndWait('[[DLT')
    const menu = screen.getByTestId('wikilink-menu')
    expect(menu.textContent).toContain('Delta')
    vi.useRealTimers()
  })

  it('sends references with onSend after selecting pills', async () => {
    vi.useFakeTimers()
    const onSend = vi.fn()
    render(<Controlled entries={entries} onSend={onSend} />)

    // Select a pill first
    await typeAndWait('[[alp')
    fireEvent.click(screen.getByTestId('wikilink-menu').querySelectorAll('[class*="cursor-pointer"]')[0])
    expect(screen.queryAllByTestId('reference-pill').length).toBe(1)

    // Type a message and send
    fireEvent.change(screen.getByTestId('agent-input'), {
      target: { value: 'tell me about it', selectionStart: 16 },
    })
    fireEvent.keyDown(screen.getByTestId('agent-input'), { key: 'Enter' })

    expect(onSend).toHaveBeenCalledOnce()
    const [text, refs] = onSend.mock.calls[0]
    expect(text).toBe('tell me about it')
    expect(refs).toHaveLength(1)
    expect(refs[0].title).toBe('Alpha')
    expect(refs[0].path).toBe('/vault/alpha.md')
    vi.useRealTimers()
  })
})
