import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
import { render, screen, fireEvent, act } from '@testing-library/react'
import SearchOverlay from '@/components/search/SearchOverlay'

process.env.NEXT_PUBLIC_DIRECTUS_URL = 'https://cms.achmad.dev'
process.env.NEXT_PUBLIC_DIRECTUS_TOKEN = 'test-token'

const mockPush = vi.fn()

vi.mock('next/navigation', () => ({
  useRouter: vi.fn(() => ({ push: mockPush })),
}))

const mockFetch = vi.fn()
global.fetch = mockFetch

beforeEach(() => {
  vi.resetAllMocks()
  vi.useFakeTimers()
  mockFetch.mockResolvedValue({
    json: () => Promise.resolve({ data: [] }),
  })
  mockPush.mockReset()
})

afterEach(() => {
  vi.useRealTimers()
})

function openSearch() {
  window.dispatchEvent(new CustomEvent('kotobane:open-search'))
}

describe('SearchOverlay', () => {
  it('does not render when closed initially', () => {
    const { container } = render(<SearchOverlay />)
    expect(container.innerHTML).toBe('')
  })

  it('opens when custom event is dispatched', () => {
    render(<SearchOverlay />)
    act(() => {
      openSearch()
    })
    expect(screen.getByPlaceholderText('Search articles…')).toBeInTheDocument()
  })

  it('opens when Ctrl+K / Cmd+K is pressed', () => {
    render(<SearchOverlay />)
    fireEvent.keyDown(window, { key: 'k', metaKey: true })
    expect(screen.getByPlaceholderText('Search articles…')).toBeInTheDocument()
  })

  it('closes when Escape is pressed', () => {
    render(<SearchOverlay />)
    act(() => {
      openSearch()
    })
    expect(screen.getByPlaceholderText('Search articles…')).toBeInTheDocument()

    fireEvent.keyDown(window, { key: 'Escape' })
    expect(screen.queryByPlaceholderText('Search articles…')).not.toBeInTheDocument()
  })

  it('closes when clicking the backdrop', () => {
    render(<SearchOverlay />)
    act(() => {
      openSearch()
    })
    expect(screen.getByPlaceholderText('Search articles…')).toBeInTheDocument()

    const backdrop = document.querySelector('.bg-black\\/60')
    expect(backdrop).toBeInTheDocument()
    if (backdrop) fireEvent.click(backdrop)
    expect(screen.queryByPlaceholderText('Search articles…')).not.toBeInTheDocument()
  })

  it('fetches search results after debounce delay', async () => {
    mockFetch.mockResolvedValue({
      json: () =>
        Promise.resolve({
          data: [
            {
              id: '1',
              title: 'Search Result',
              slug: 'search-result',
              category: { slug: 'anime', name: 'Anime' },
            },
          ],
        }),
    })

    render(<SearchOverlay />)
    act(() => {
      openSearch()
    })

    const input = screen.getByPlaceholderText('Search articles…')
    fireEvent.change(input, { target: { value: 'Frieren' } })

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

    expect(screen.getByText('Search Result')).toBeInTheDocument()
  })

  it('shows category name in search results', async () => {
    mockFetch.mockResolvedValue({
      json: () =>
        Promise.resolve({
          data: [
            {
              id: '1',
              title: 'Result',
              slug: 'result',
              category: { slug: 'music', name: 'Music' },
            },
          ],
        }),
    })

    render(<SearchOverlay />)
    act(() => {
      openSearch()
    })

    const input = screen.getByPlaceholderText('Search articles…')
    fireEvent.change(input, { target: { value: 'song' } })

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

    expect(screen.getByText('Music')).toBeInTheDocument()
  })

  it('shows loading indicator while searching', async () => {
    mockFetch.mockImplementation(
      () =>
        new Promise((resolve) =>
          setTimeout(
            () =>
              resolve({
                json: () => Promise.resolve({ data: [] }),
              }),
            500,
          ),
        ),
    )

    render(<SearchOverlay />)
    act(() => {
      openSearch()
    })

    const input = screen.getByPlaceholderText('Search articles…')
    fireEvent.change(input, { target: { value: 'test' } })

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

    // After the debounce fires, setLoading(true) is called synchronously
    expect(screen.getByText('Searching…')).toBeInTheDocument()
  })

  it('shows no results message when search returns empty', async () => {
    render(<SearchOverlay />)
    act(() => {
      openSearch()
    })

    const input = screen.getByPlaceholderText('Search articles…')
    fireEvent.change(input, { target: { value: 'zzzz' } })

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

    expect(screen.getByText(/no results/i)).toBeInTheDocument()
  })

  it('navigates to article on click', async () => {
    mockFetch.mockResolvedValue({
      json: () =>
        Promise.resolve({
          data: [
            {
              id: '1',
              title: 'Clickable Result',
              slug: 'clickable-result',
              category: { slug: 'anime', name: 'Anime' },
            },
          ],
        }),
    })

    render(<SearchOverlay />)
    act(() => {
      openSearch()
    })

    const input = screen.getByPlaceholderText('Search articles…')
    fireEvent.change(input, { target: { value: 'item' } })

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

    expect(screen.getByText('Clickable Result')).toBeInTheDocument()

    fireEvent.click(screen.getByText('Clickable Result'))
    expect(mockPush).toHaveBeenCalledWith('/anime/clickable-result')
  })

  it('clears results when input is cleared', () => {
    render(<SearchOverlay />)
    act(() => {
      openSearch()
    })

    const input = screen.getByPlaceholderText('Search articles…')
    fireEvent.change(input, { target: { value: 'test' } })
    fireEvent.change(input, { target: { value: '' } })

    expect(screen.queryByText('No results')).not.toBeInTheDocument()
  })

  it('debounces search requests', () => {
    render(<SearchOverlay />)
    act(() => {
      openSearch()
    })

    const input = screen.getByPlaceholderText('Search articles…')
    fireEvent.change(input, { target: { value: 'a' } })
    fireEvent.change(input, { target: { value: 'ab' } })
    fireEvent.change(input, { target: { value: 'abc' } })

    act(() => {
      vi.advanceTimersByTime(100)
    })

    // Should not have fired yet (debounce is 300ms)
    expect(mockFetch).not.toHaveBeenCalled()
  })

  it('navigates on Enter key press', async () => {
    mockFetch.mockResolvedValue({
      json: () =>
        Promise.resolve({
          data: [
            {
              id: '1',
              title: 'Enter Result',
              slug: 'enter-result',
              category: { slug: 'manga', name: 'Manga' },
            },
          ],
        }),
    })

    render(<SearchOverlay />)
    act(() => {
      openSearch()
    })

    const input = screen.getByPlaceholderText('Search articles…')
    fireEvent.change(input, { target: { value: 'manga' } })

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

    expect(screen.getByText('Enter Result')).toBeInTheDocument()

    fireEvent.keyDown(input, { key: 'Enter' })
    expect(mockPush).toHaveBeenCalledWith('/manga/enter-result')
  })

  it('clears search results when overlay reopens via Escape', async () => {
    mockFetch.mockResolvedValue({
      json: () =>
        Promise.resolve({
          data: [
            {
              id: '1',
              title: 'Temp Result',
              slug: 'temp',
              category: { slug: 'anime', name: 'Anime' },
            },
          ],
        }),
    })

    render(<SearchOverlay />)
    act(() => {
      openSearch()
    })

    const input = screen.getByPlaceholderText('Search articles…')
    fireEvent.change(input, { target: { value: 'temp' } })

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

    expect(screen.getByText('Temp Result')).toBeInTheDocument()

    fireEvent.keyDown(window, { key: 'Escape' })

    act(() => {
      openSearch()
    })

    expect(screen.queryByText('Temp Result')).not.toBeInTheDocument()
    expect(screen.getByPlaceholderText('Search articles…')).toHaveValue('')
  })
})
