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

import PostImportReviewPage from '@/pages/posts/PostImportReviewPage'
import { buildUser } from '@/test/factories'
import { renderWithProviders } from '@/test/render'

import type { ReactNode } from 'react'

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

vi.mock ('@/lib/api', () => api)
vi.mock ('framer-motion', () => ({
  AnimatePresence: ({ children }: { children?: ReactNode }) => <>{children}</>,
  motion: {
    div: ({ children }: { children?: ReactNode }) => <div>{children}</div>,
    main: ({ children }: { children?: ReactNode }) => <main>{children}</main> } }))

const metadata = (url: string, title: string) => ({
  url,
  title,
  thumbnailBase: 'https://example.com/thumbnail.jpg',
  originalCreatedFrom: '',
  originalCreatedBefore: '',
  duration: '',
  tags: '',
  parentPostIds: [],
  fieldWarnings: { },
  baseWarnings: [],
  displayTags: [] })


const LocationProbe = () => {
  const location = useLocation ()
  return <output aria-label="current-location">{location.pathname}{location.search}</output>
}


const renderReviewPage = (urls: string[]) => {
  const search = urls.map (url => encodeURIComponent (url)).join ('+')
  return renderWithProviders (
      <>
	<PostImportReviewPage user={buildUser ()}/>
	<LocationProbe/>
      </>,
      { route: `/posts/new?urls=${ search }` })
}

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

  it ('fetches metadata with at most four concurrent requests', async () => {
    const resolvers: Array<(value: ReturnType<typeof metadata>) => void> = []
    api.apiGet.mockImplementation ((_path, options) =>
	new Promise (resolve => {
	  resolvers.push (resolve)
	  const url = String (options.params.url)
	  void url
	}))
    const urls = Array.from (
	{ length: 6 },
	(_, index) => `https://example.com/${ index + 1 }`)

    renderReviewPage (urls)

    await waitFor (() => expect (api.apiGet).toHaveBeenCalledTimes (4))

    resolvers[0]?.(metadata (urls[0]!, 'first'))

    expect (await screen.findAllByText ('first')).toHaveLength (2)
    await waitFor (() => expect (api.apiGet).toHaveBeenCalledTimes (5))
    expect (screen.getByRole ('button', { name: '追加' })).toBeDisabled ()

    for (let index = 1; index < resolvers.length; ++index)
      resolvers[index]?.(metadata (urls[index]!, `post ${ index + 1 }`))
  })

  it ('keeps successful rows when another metadata request fails', async () => {
    api.apiGet
	.mockResolvedValueOnce (metadata ('https://example.com/one', 'first'))
	.mockRejectedValueOnce (new TypeError ('network'))

    renderReviewPage (['https://example.com/one', 'https://example.com/two'])

    expect (await screen.findAllByText ('first')).toHaveLength (2)
    await waitFor (() => {
      expect (screen.getAllByText ('登録不可')).toHaveLength (2)
    })
    expect (screen.getAllByRole ('button', { name: '編輯' })[0]).toBeEnabled ()
  })

  it ('shows existing posts with Active Storage thumbnail precedence', async () => {
    api.apiGet.mockResolvedValue ({
      ...metadata ('https://example.com/post', ''),
      existingPostId: 24,
      existingPost: {
	id: 24,
	title: 'existing post',
	url: 'https://example.com/post',
	thumbnail: 'https://example.com/storage.jpg',
	thumbnailBase: 'https://example.com/base.jpg' } })

    renderReviewPage (['https://example.com/post'])

    const disclosure = await screen.findByRole ('button', {
      name: '既存投稿による自動スキップ 1件' })
    expect (screen.getByRole ('button', { name: '追加' })).toBeDisabled ()

    fireEvent.click (disclosure)

    expect ((await screen.findAllByRole ('img', { name: 'サムネール' }))[0])
	.toHaveAttribute ('src', 'https://example.com/storage.jpg')
  })

  it ('keeps a recoverable bulk failure on the review screen', async () => {
    api.apiGet.mockResolvedValue (
	metadata ('https://example.com/post', 'new post'))
    api.apiPost.mockResolvedValue ({
      results: [{
	status: 'failed',
	recoverable: true,
	errors: { title: ['invalid title'] } }] })

    renderReviewPage (['https://example.com/post'])

    fireEvent.click (await screen.findByRole ('button', { name: '追加' }))

    expect (await screen.findAllByText ('登録失敗')).toHaveLength (2)
    expect (screen.getAllByText ('invalid title')).toHaveLength (2)
    expect (screen.getAllByRole ('button', { name: '編輯' })[0]).toBeEnabled ()
    expect (screen.getAllByRole ('button', { name: '再試行' })[0]).toBeEnabled ()
    expect (screen.getByLabelText ('current-location')).toHaveTextContent ('/posts/new?')
  })

  it ('navigates to /posts after every submitted row is created', async () => {
    api.apiGet.mockResolvedValue (
	metadata ('https://example.com/post', 'new post'))
    api.apiPost.mockResolvedValue ({
      results: [{ status: 'created', post: { id: 42 } }] })

    renderReviewPage (['https://example.com/post'])

    fireEvent.click (await screen.findByRole ('button', { name: '追加' }))

    await waitFor (() => {
      expect (screen.getByLabelText ('current-location')).toHaveTextContent ('/posts')
    })
    expect (api.apiPost).toHaveBeenCalledWith ('/posts/bulk', expect.any (FormData))
  })
})
