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

import TagDetailPage from '@/pages/tags/TagDetailPage'
import { buildTag } from '@/test/factories'
import { renderWithProviders } from '@/test/render'

const tagsApi = vi.hoisted (() => ({
  fetchTag: vi.fn (),
}))

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

const toastApi = vi.hoisted (() => ({
  toast: vi.fn (),
}))

vi.mock ('@/lib/tags', () => tagsApi)
vi.mock ('@/lib/api', () => api)
vi.mock ('@/components/ui/use-toast', () => toastApi)

const renderPage = () =>
  renderWithProviders (
      <Routes>
        <Route path="/tags/:id" element={<TagDetailPage/>}/>
      </Routes>,
      { route: '/tags/7' },
)

describe ('TagDetailPage', () => {
  beforeEach (() => {
    vi.clearAllMocks ()
    api.isApiError.mockReturnValue (false)
  })

  it ('loads and displays an editable tag', async () => {
    tagsApi.fetchTag.mockResolvedValueOnce (
        buildTag ({ id: 7, name: '虹夏', category: 'character', aliases: ['drums'] }),
    )

    renderPage ()

    expect (await screen.findByDisplayValue ('虹夏')).toBeInTheDocument ()
    expect (screen.getByRole ('combobox')).toHaveValue ('character')
    expect (screen.getByDisplayValue ('drums')).toBeInTheDocument ()
  })

  it ('submits edited tag fields', async () => {
    tagsApi.fetchTag.mockResolvedValueOnce (buildTag ({ id: 7, name: 'old' }))
    api.apiPut.mockResolvedValueOnce (buildTag ({ id: 7, name: 'new', aliases: ['alias'] }))

    renderPage ()

    const name = await screen.findByDisplayValue ('old')
    fireEvent.change (name, { target: { value: 'new' } })
    fireEvent.submit (screen.getByRole ('button', { name: '更新' }).closest ('form')!)

    await waitFor (() => {
      expect (api.apiPut).toHaveBeenCalledWith ('/tags/7', expect.any (FormData))
    })
    const formData = api.apiPut.mock.calls[0]?.[1] as FormData
    expect (formData.get ('name')).toBe ('new')
    expect (toastApi.toast).toHaveBeenCalledWith ({ description: '更新しました．' })
  })

  it ('keeps nico tags disabled', async () => {
    tagsApi.fetchTag.mockResolvedValueOnce (buildTag ({ category: 'nico' }))

    renderPage ()

    expect (await screen.findByRole ('button', { name: '更新' })).toBeDisabled ()
  })

  it ('shows validation errors returned for tag fields', async () => {
    tagsApi.fetchTag.mockResolvedValueOnce (buildTag ({ id: 7, name: 'old' }))
    api.isApiError.mockReturnValue (true)
    api.apiPut.mockRejectedValueOnce ({
      response: {
        status: 422,
        data:   {
          type:        'validation_error',
          message:     '入力内容を確認してください．',
          errors:      { category: ['ニコタグは変更できません．'] },
          base_errors: [],
        },
      },
    })

    renderPage ()

    await screen.findByDisplayValue ('old')
    fireEvent.submit (screen.getByRole ('button', { name: '更新' }).closest ('form')!)

    expect (await screen.findByText ('ニコタグは変更できません．')).toBeInTheDocument ()
    expect (screen.getByRole ('combobox')).toHaveAttribute ('aria-invalid', 'true')
  })
})
