import { act, fireEvent, render, screen, waitFor } from '@testing-library/react'
import type { ReactNode } from 'react'
import { beforeEach, describe, expect, it, vi } from 'vitest'

import RecallTestPanel from '../RecallTestPanel'

const mockIpcRequest = vi.fn()
const mockPerformanceNow = vi.spyOn(performance, 'now')

vi.mock('@renderer/ipc', () => ({
  ipcApi: {
    request: (...args: unknown[]) => mockIpcRequest(...args)
  }
}))
const mockToastError = vi.fn()
const mockClipboardWriteText = vi.fn()
const mockLogger = vi.hoisted(() => ({
  info: vi.fn(),
  error: vi.fn()
}))
const mockCache = vi.hoisted(() => ({
  initial: {
    'base-1': ['RAG 检索增强生成原理', '向量数据库选型对比'],
    'base-2': ['其他知识库查询']
  } as Record<string, string[]>,
  set: vi.fn()
}))

const realSearchResults = [
  {
    pageContent: 'real result from file name',
    score: 0.98,
    scoreKind: 'relevance',
    rank: 1,
    metadata: {
      itemId: 'item-1',
      itemType: 'file',
      source: '/Users/test/Downloads/真实文档.pdf',
      chunkIndex: 3,
      tokenCount: 120
    },
    itemId: 'item-1',
    chunkId: 'chunk-1'
  },
  {
    pageContent: 'real result from file path',
    score: 0.76,
    scoreKind: 'relevance',
    rank: 2,
    metadata: {
      itemId: 'item-2',
      itemType: 'file',
      source: '/Users/test/Downloads/路径文档.md',
      chunkIndex: 2,
      tokenCount: 80
    },
    itemId: 'item-2',
    chunkId: 'chunk-2'
  }
]

vi.mock('@logger', () => ({
  loggerService: {
    withContext: () => ({
      info: mockLogger.info,
      error: mockLogger.error
    })
  }
}))

vi.mock('@cherrystudio/ui', async () => {
  return {
    Button: ({ children, ...props }: { children: ReactNode; [key: string]: unknown }) => (
      <button type="button" {...props}>
        {children}
      </button>
    ),
    EmptyState: ({
      title,
      description,
      ...props
    }: {
      title?: ReactNode
      description?: ReactNode
      [key: string]: unknown
    }) => (
      <div {...props}>
        {title ? <div>{title}</div> : null}
        {description ? <div>{description}</div> : null}
      </div>
    ),
    Input: (props: { [key: string]: unknown }) => <input {...props} />
  }
})

vi.mock('@data/hooks/useCache', async () => {
  const React = await import('react')

  return {
    useCache: () => {
      const [value, setValue] = React.useState(mockCache.initial)

      return [
        value,
        (nextValue: Record<string, string[]>) => {
          mockCache.set(nextValue)
          setValue(nextValue)
        }
      ]
    }
  }
})

vi.mock('react-i18next', () => ({
  useTranslation: () => ({
    t: (key: string, options?: { count?: number; duration?: number; score?: number | string; rank?: number }) =>
      (
        ({
          'knowledge.recall.collapse': '收起片段',
          'knowledge.recall.copy': '复制片段',
          'knowledge.recall.duration': `${options?.duration ?? 0}ms`,
          'knowledge.recall.empty_description': '结果将展示匹配的文档片段和分数',
          'knowledge.recall.empty_title': '输入查询语句开始检索测试',
          'knowledge.recall.expand': '展开片段',
          'knowledge.recall.history_clear': '清空',
          'knowledge.recall.history_remove': '删除历史',
          'knowledge.recall.history_title': '搜索历史',
          'knowledge.recall.placeholder': '输入测试 Query...',
          'knowledge.recall.result_count': `${options?.count ?? 0} 个结果`,
          'knowledge.recall.result_rank': `排序 #${options?.rank ?? 0}`,
          'knowledge.recall.result_relevance': `相关度 ${options?.score ?? 0}`,
          'knowledge.recall.ranking_only': '按排序返回',
          'knowledge.recall.search_failed': '召回测试检索失败',
          'knowledge.recall.searching': '正在检索...',
          'knowledge.recall.submit': '检索',
          'knowledge.recall.top_score': `最高: ${options?.score ?? 0}`
        }) as Record<string, string>
      )[key] ?? key
  })
}))

describe('RecallTestPanel', () => {
  beforeEach(() => {
    vi.useRealTimers()
    vi.clearAllMocks()
    mockCache.initial = {
      'base-1': ['RAG 检索增强生成原理', '向量数据库选型对比'],
      'base-2': ['其他知识库查询']
    }
    mockPerformanceNow.mockReturnValue(100)
    mockIpcRequest.mockResolvedValue(realSearchResults)
    Object.assign(window, {
      toast: {
        error: mockToastError
      }
    })
    Object.defineProperty(navigator, 'clipboard', {
      configurable: true,
      value: {
        writeText: mockClipboardWriteText
      }
    })
    mockClipboardWriteText.mockResolvedValue(undefined)
  })

  it('renders the empty state with a disabled search button initially', () => {
    render(<RecallTestPanel baseId="base-1" />)

    expect(screen.getByText('输入查询语句开始检索测试')).toBeInTheDocument()
    expect(screen.getByText('结果将展示匹配的文档片段和分数')).toBeInTheDocument()
    expect(screen.getByRole('button', { name: '检索' })).toBeDisabled()
  })

  it('draws the search input focus ring inset so the panel overflow cannot clip it', () => {
    render(<RecallTestPanel baseId="base-1" />)

    // The wrapper sits inside an `overflow-x-hidden` ancestor; an outset ring would be
    // clipped on the lower/right edge, so the focus border must be drawn inset.
    const inputWrapper = screen.getByPlaceholderText('输入测试 Query...').parentElement
    expect(inputWrapper).toHaveClass('focus-within:ring-1', 'focus-within:ring-inset')
  })

  it('hides the history button and dropdown when the selected base has no search history', () => {
    mockCache.initial = {
      'base-2': ['其他知识库查询']
    }

    render(<RecallTestPanel baseId="base-1" />)

    fireEvent.focus(screen.getByPlaceholderText('输入测试 Query...'))

    expect(screen.queryByRole('button', { name: '搜索历史' })).not.toBeInTheDocument()
    expect(screen.queryByText('搜索历史')).not.toBeInTheDocument()
    expect(screen.queryByText('其他知识库查询')).not.toBeInTheDocument()
  })

  it('shows cached search query history for the selected base when the search input receives focus', () => {
    render(<RecallTestPanel baseId="base-1" />)

    fireEvent.focus(screen.getByPlaceholderText('输入测试 Query...'))

    expect(screen.getByText('搜索历史')).toBeInTheDocument()
    expect(screen.getByText('RAG 检索增强生成原理')).toBeInTheDocument()
    expect(screen.getByText('向量数据库选型对比')).toBeInTheDocument()
    expect(screen.queryByText('其他知识库查询')).not.toBeInTheDocument()
  })

  it('keeps search history open when clicking the focused input again', () => {
    render(<RecallTestPanel baseId="base-1" />)

    const input = screen.getByPlaceholderText('输入测试 Query...')
    fireEvent.focus(input)
    fireEvent.click(input)

    expect(screen.getByText('搜索历史')).toBeInTheDocument()
    expect(screen.getByText('RAG 检索增强生成原理')).toBeInTheDocument()
    expect(mockIpcRequest).not.toHaveBeenCalled()
  })

  it('closes search history when input loses focus outside the history popover', async () => {
    render(<RecallTestPanel baseId="base-1" />)

    const input = screen.getByPlaceholderText('输入测试 Query...')
    fireEvent.focus(input)
    expect(screen.getByText('搜索历史')).toBeInTheDocument()

    fireEvent.blur(input, { relatedTarget: document.body })

    await waitFor(() => {
      expect(screen.queryByText('搜索历史')).not.toBeInTheDocument()
    })
  })

  it('fills the query from history and closes the history popover without searching', async () => {
    render(<RecallTestPanel baseId="base-1" />)

    const input = screen.getByPlaceholderText('输入测试 Query...')
    fireEvent.focus(input)
    fireEvent.click(screen.getByText('RAG 检索增强生成原理'))

    expect(input).toHaveValue('RAG 检索增强生成原理')

    await waitFor(() => {
      expect(screen.queryByText('搜索历史')).not.toBeInTheDocument()
    })
    expect(screen.queryByText('5 个结果')).not.toBeInTheDocument()
  })

  it('calls runtime IPC, logs the returned data, and renders real result cards after searching', async () => {
    mockIpcRequest.mockImplementation(async () => {
      mockPerformanceNow.mockReturnValue(223)
      return realSearchResults
    })

    render(<RecallTestPanel baseId="base-1" />)

    fireEvent.change(screen.getByPlaceholderText('输入测试 Query...'), {
      target: { value: 'RAG 检索增强生成原理' }
    })
    fireEvent.click(screen.getByRole('button', { name: '检索' }))

    await waitFor(() => {
      expect(mockIpcRequest).toHaveBeenCalledWith('knowledge.search', {
        baseId: 'base-1',
        query: 'RAG 检索增强生成原理'
      })
    })
    expect(mockLogger.info).toHaveBeenCalledWith('Knowledge recall search IPC result', {
      baseId: 'base-1',
      query: 'RAG 检索增强生成原理',
      results: realSearchResults
    })
    expect(screen.getByText('2 个结果')).toBeInTheDocument()
    expect(screen.getByText('123ms')).toBeInTheDocument()
    expect(screen.getByText('最高: 98%')).toBeInTheDocument()
    expect(screen.getAllByRole('button', { name: '复制片段' })).toHaveLength(2)
    expect(screen.getByText('/Users/test/Downloads/真实文档.pdf')).toBeInTheDocument()
    expect(screen.getByText('/Users/test/Downloads/路径文档.md')).toBeInTheDocument()
    expect(screen.getByText('#3')).toBeInTheDocument()
    expect(screen.getByText('#2')).toBeInTheDocument()
    expect(screen.getByText('相关度 98%')).toBeInTheDocument()
    expect(screen.getByText('相关度 76%')).toBeInTheDocument()
    expect(screen.getByText('real result from file name')).toBeInTheDocument()
    expect(screen.getByText('real result from file path')).toBeInTheDocument()
    expect(screen.queryByText('RAG 技术指南.pdf')).not.toBeInTheDocument()
    expect(screen.queryByText('知识库最佳实践.md')).not.toBeInTheDocument()
  })

  it('keeps recall results from causing outer horizontal overflow', async () => {
    const longContent = 'x'.repeat(400)
    mockIpcRequest.mockResolvedValueOnce([
      {
        ...realSearchResults[0],
        pageContent: longContent
      }
    ])

    const { container } = render(<RecallTestPanel baseId="base-1" />)

    fireEvent.change(screen.getByPlaceholderText('输入测试 Query...'), {
      target: { value: 'long content' }
    })
    fireEvent.click(screen.getByRole('button', { name: '检索' }))

    await waitFor(() => {
      expect(screen.getByText(longContent)).toBeInTheDocument()
    })

    expect(container.firstElementChild).toHaveClass('min-w-0', 'overflow-x-hidden')
    expect(screen.getByRole('button', { name: '检索' }).parentElement).toHaveClass('w-full', 'max-w-3xl')
    expect(screen.getByText('1 个结果').closest('.overflow-y-auto')).toHaveClass('[scrollbar-width:none]')
    expect(screen.getByText('1 个结果').closest('.max-w-3xl')).toHaveClass('w-full', 'max-w-3xl')
    expect(screen.getByText(longContent)).toHaveClass('wrap-anywhere', 'whitespace-normal')
  })

  it('shows temporary icon-only copy feedback after copying a recall result', async () => {
    render(<RecallTestPanel baseId="base-1" />)

    fireEvent.change(screen.getByPlaceholderText('输入测试 Query...'), {
      target: { value: 'RAG 检索增强生成原理' }
    })
    fireEvent.click(screen.getByRole('button', { name: '检索' }))

    await waitFor(() => {
      expect(screen.getAllByRole('button', { name: '复制片段' })).toHaveLength(2)
    })

    const copyButton = screen.getAllByRole('button', { name: '复制片段' })[0]
    expect(copyButton.querySelector('.lucide-copy')).toBeInTheDocument()

    vi.useFakeTimers()

    await act(async () => {
      fireEvent.click(copyButton)
      await Promise.resolve()
      await Promise.resolve()
    })

    expect(mockClipboardWriteText).toHaveBeenCalledWith('real result from file name')
    expect(copyButton.querySelector('.lucide-check')).toBeInTheDocument()
    expect(copyButton).toHaveClass('text-success', 'opacity-100')
    expect(mockToastError).not.toHaveBeenCalledWith('message.copied')

    await act(async () => {
      await vi.advanceTimersByTimeAsync(2000)
    })

    expect(copyButton.querySelector('.lucide-copy')).toBeInTheDocument()
  })

  it('shows a searching state while runtime IPC is pending', async () => {
    let resolveSearch: (value: typeof realSearchResults) => void = () => undefined
    mockIpcRequest.mockReturnValue(
      new Promise((resolve) => {
        resolveSearch = resolve
      })
    )

    render(<RecallTestPanel baseId="base-1" />)

    fireEvent.change(screen.getByPlaceholderText('输入测试 Query...'), {
      target: { value: 'RAG 检索增强生成原理' }
    })
    fireEvent.click(screen.getByRole('button', { name: '检索' }))

    expect(screen.getByText('正在检索...')).toBeInTheDocument()
    expect(screen.getByRole('button', { name: '检索' })).toBeDisabled()

    mockPerformanceNow.mockReturnValue(223)
    resolveSearch(realSearchResults)

    await waitFor(() => {
      expect(screen.queryByText('正在检索...')).not.toBeInTheDocument()
    })
    expect(screen.getByText('2 个结果')).toBeInTheDocument()
  })

  it('does not apply pending search results after switching selected bases', async () => {
    let resolveSearch: (value: typeof realSearchResults) => void = () => undefined
    mockIpcRequest.mockReturnValue(
      new Promise((resolve) => {
        resolveSearch = resolve
      })
    )

    const { rerender } = render(<RecallTestPanel baseId="base-1" />)

    fireEvent.change(screen.getByPlaceholderText('输入测试 Query...'), {
      target: { value: 'RAG 检索增强生成原理' }
    })
    fireEvent.click(screen.getByRole('button', { name: '检索' }))

    expect(screen.getByText('正在检索...')).toBeInTheDocument()

    rerender(<RecallTestPanel baseId="base-2" />)

    await waitFor(() => {
      expect(screen.getByText('输入查询语句开始检索测试')).toBeInTheDocument()
    })
    expect(screen.getByPlaceholderText('输入测试 Query...')).toHaveValue('')

    fireEvent.focus(screen.getByPlaceholderText('输入测试 Query...'))
    expect(screen.getByText('其他知识库查询')).toBeInTheDocument()
    expect(screen.queryByText('RAG 检索增强生成原理')).not.toBeInTheDocument()

    mockPerformanceNow.mockReturnValue(223)
    resolveSearch(realSearchResults)

    await waitFor(() => {
      expect(mockLogger.info).toHaveBeenCalledWith('Knowledge recall search IPC result', {
        baseId: 'base-1',
        query: 'RAG 检索增强生成原理',
        results: realSearchResults
      })
    })
    expect(screen.queryByText('2 个结果')).not.toBeInTheDocument()
    expect(screen.queryByText('real result from file name')).not.toBeInTheDocument()
    expect(screen.queryByText('real result from file path')).not.toBeInTheDocument()
  })

  it('removes one cached query from the selected base history', () => {
    render(<RecallTestPanel baseId="base-1" />)

    fireEvent.focus(screen.getByPlaceholderText('输入测试 Query...'))
    fireEvent.click(screen.getAllByRole('button', { name: '删除历史' })[0])

    expect(mockCache.set).toHaveBeenCalledWith({
      'base-1': ['向量数据库选型对比'],
      'base-2': ['其他知识库查询']
    })
    expect(screen.queryByText('RAG 检索增强生成原理')).not.toBeInTheDocument()
    expect(screen.getByText('向量数据库选型对比')).toBeInTheDocument()
  })

  it('clears cached query history for the selected base only', () => {
    render(<RecallTestPanel baseId="base-1" />)

    fireEvent.focus(screen.getByPlaceholderText('输入测试 Query...'))
    fireEvent.click(screen.getByRole('button', { name: '清空' }))

    expect(mockCache.set).toHaveBeenCalledWith({
      'base-1': [],
      'base-2': ['其他知识库查询']
    })
    expect(screen.queryByText('搜索历史')).not.toBeInTheDocument()
    expect(screen.queryByText('RAG 检索增强生成原理')).not.toBeInTheDocument()
    expect(screen.queryByText('向量数据库选型对比')).not.toBeInTheDocument()
  })

  it('records submitted queries with dedupe and a five item limit', async () => {
    mockCache.initial = {
      'base-1': ['旧查询 1', 'RAG 检索增强生成原理', '旧查询 2', '旧查询 3', '旧查询 4'],
      'base-2': ['其他知识库查询']
    }

    render(<RecallTestPanel baseId="base-1" />)

    fireEvent.change(screen.getByPlaceholderText('输入测试 Query...'), {
      target: { value: '  RAG 检索增强生成原理  ' }
    })
    fireEvent.click(screen.getByRole('button', { name: '检索' }))

    expect(mockCache.set).toHaveBeenCalledWith({
      'base-1': ['RAG 检索增强生成原理', '旧查询 1', '旧查询 2', '旧查询 3', '旧查询 4'],
      'base-2': ['其他知识库查询']
    })

    await waitFor(() => {
      expect(screen.queryByText('正在检索...')).not.toBeInTheDocument()
    })
  })

  it('logs runtime IPC failures without throwing', async () => {
    const error = new Error('search failed')
    mockIpcRequest.mockRejectedValue(error)

    render(<RecallTestPanel baseId="base-1" />)

    fireEvent.change(screen.getByPlaceholderText('输入测试 Query...'), {
      target: { value: '  RAG 检索增强生成原理  ' }
    })
    fireEvent.click(screen.getByRole('button', { name: '检索' }))

    await waitFor(() => {
      expect(mockLogger.error).toHaveBeenCalledWith('Knowledge recall search IPC failed', error, {
        baseId: 'base-1',
        query: 'RAG 检索增强生成原理'
      })
    })
    expect(screen.getByText('0 个结果')).toBeInTheDocument()
    expect(screen.getByText('最高: 0.00')).toBeInTheDocument()
    expect(mockToastError).toHaveBeenCalledWith('召回测试检索失败: search failed')
    expect(screen.queryByText('RAG 技术指南.pdf')).not.toBeInTheDocument()
    expect(screen.queryByRole('button', { name: '复制片段' })).not.toBeInTheDocument()
  })

  it('renders ranking-only recall results without percentage scores', async () => {
    mockIpcRequest.mockResolvedValueOnce([
      {
        ...realSearchResults[0],
        score: 12.345,
        scoreKind: 'ranking',
        rank: 1
      },
      {
        ...realSearchResults[1],
        score: 3.21,
        scoreKind: 'ranking',
        rank: 2
      }
    ])

    render(<RecallTestPanel baseId="base-1" />)

    fireEvent.change(screen.getByPlaceholderText('输入测试 Query...'), {
      target: { value: '关键词检索' }
    })
    fireEvent.click(screen.getByRole('button', { name: '检索' }))

    await waitFor(() => {
      expect(screen.getByText('按排序返回')).toBeInTheDocument()
    })
    expect(screen.getByText('排序 #1')).toBeInTheDocument()
    expect(screen.getByText('排序 #2')).toBeInTheDocument()
    expect(screen.queryByText('相关度 1235%')).not.toBeInTheDocument()
  })
})
