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

import PostSearchPage from '@/pages/posts/PostSearchPage'
import { buildPost, buildTag } from '@/test/factories'
import { renderWithProviders } from '@/test/render'
import { CLIENT_SETTINGS_STORAGE_KEY } from '@/lib/settings'

const postsApi = vi.hoisted (() => ({
  fetchPosts: vi.fn (),
}))

const api = vi.hoisted (() => ({
  apiGet: vi.fn (),
}))

vi.mock ('@/lib/posts', () => postsApi)
vi.mock ('@/lib/api', () => api)

describe ('PostSearchPage', () => {
  beforeEach (() => {
    localStorage.clear ()
    vi.clearAllMocks ()
    api.apiGet.mockResolvedValue ([])
  })

  it ('loads posts from URL search filters', async () => {
    postsApi.fetchPosts.mockResolvedValueOnce ({
      posts: [buildPost ({ id: 4, title: '検索対象', tags: [buildTag ({ name: '虹夏' })] })],
      count: 1,
    })

    renderWithProviders (
        <PostSearchPage/>,
        { route: '/posts/search?title=%E6%A4%9C%E7%B4%A2&tags=x&match=any&page=2' },
    )

    await waitFor (() => {
      expect (postsApi.fetchPosts).toHaveBeenCalledWith (
          expect.objectContaining ({
            title: '検索',
            tags:  'x',
            match: 'any',
            page:  2,
            limit: 20,
          }),
      )
    })
    expect ((await screen.findAllByRole ('link', { name: '検索対象' }))[0]).toHaveAttribute (
        'href',
        '/posts/4',
    )
  })

  it ('submits form state into a new search', async () => {
    postsApi.fetchPosts.mockResolvedValue ({ posts: [], count: 0 })

    renderWithProviders (<PostSearchPage/>, { route: '/posts/search' })

    const textboxes = screen.getAllByRole ('textbox')
    fireEvent.change (textboxes[0], { target: { value: 'title' } })
    fireEvent.change (textboxes[1], { target: { value: 'https://example.com' } })
    fireEvent.change (textboxes[2], { target: { value: 'tag' } })
    fireEvent.click (screen.getByRole ('radio', { name: 'OR' }))
    fireEvent.click (screen.getByRole ('button', { name: '検索' }))

    await waitFor (() => {
      expect (postsApi.fetchPosts).toHaveBeenLastCalledWith (
          expect.objectContaining ({
            title: 'title',
            url:   'https://example.com',
            tags:  'tag',
            match: 'any',
            page:  1,
          }),
      )
    })
  })

  it ('shows the no-result message', async () => {
    postsApi.fetchPosts.mockResolvedValueOnce ({ posts: [], count: 0 })

    renderWithProviders (<PostSearchPage/>, { route: '/posts/search' })

    expect (await screen.findByText ('結果ないよ（笑）')).toBeInTheDocument ()
  })

  it ('uses URL limit before localStorage and stores the active list settings', async () => {
    localStorage.setItem (
        CLIENT_SETTINGS_STORAGE_KEY,
        JSON.stringify ({ lists: { postSearch: { limit: 100, order: 'title:asc' } } }))
    postsApi.fetchPosts.mockResolvedValueOnce ({ posts: [], count: 0 })

    renderWithProviders (
        <PostSearchPage/>,
        { route: '/posts/search?limit=50&order=created_at%3Adesc' },
    )

    await waitFor (() => {
      expect (postsApi.fetchPosts).toHaveBeenCalledWith (
          expect.objectContaining ({ limit: 50, order: 'created_at:desc' }),
      )
    })
    expect (JSON.parse (localStorage.getItem (CLIENT_SETTINGS_STORAGE_KEY) ?? '{}'))
        .toMatchObject ({ lists: { postSearch: { limit: 50, order: 'created_at:desc' } } })
  })

  it ('moves the compact limit control outside the search form and resets page on change', async () => {
    postsApi.fetchPosts.mockResolvedValue ({ posts: [], count: 0 })

    renderWithProviders (<PostSearchPage/>, { route: '/posts/search?page=4' })

    const limitSelect = await screen.findByLabelText ('件数')
    expect (limitSelect.closest ('form')).toBeNull ()

    fireEvent.change (limitSelect, { target: { value: '100' } })

    await waitFor (() => {
      expect (postsApi.fetchPosts).toHaveBeenLastCalledWith (
          expect.objectContaining ({ page: 1, limit: 100 }),
      )
    })
  })
})
