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

import {
  loadPostImportSession,
  savePostImportSession,
} from '@/lib/postImportSession'
import PostImportResultPage from '@/pages/posts/PostImportResultPage'
import { buildUser } from '@/test/factories'
import { buildPostImportRow } from '@/test/postImportFactories'
import { originalCreatedAtString } from '@/lib/utils'

import type { PostImportRow } from '@/lib/postImportSession'
import type { DialogueFormAction, DialogueFormControls } from '@/lib/dialogues/useDialogue'

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

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

const dialogue = vi.hoisted (() => ({
  form: vi.fn (() => Promise.resolve ()),
}))

vi.mock ('@/components/ui/use-toast', () => toastApi)
vi.mock ('@/lib/api', () => api)
vi.mock ('@/lib/dialogues/useDialogue', () => ({
  default: () => dialogue,
}))

describe ('PostImportResultPage', () => {
  beforeEach (() => {
    sessionStorage.clear ()
    vi.clearAllMocks ()
    globalThis.URL.createObjectURL = vi.fn (() => 'blob:preview')
    globalThis.URL.revokeObjectURL = vi.fn ()
    api.apiGet.mockResolvedValue (new Blob (['img'], { type: 'image/png' }))
  })

  it ('shows edit and retry only for recoverable failures and shows warnings', () => {
    savePostImportSession ('result-session', {
      source: '',
      repairMode: 'failed',
      rows: [
        buildPostImportRow ({
          sourceRow: 1,
          attributes: { title: 'recoverable row' },
          importStatus: 'failed',
          recoverable: true,
          importErrors: { base: ['recoverable failed'] } }),
        buildPostImportRow ({
          sourceRow: 2,
          attributes: { title: 'hard failed row' },
          importStatus: 'failed',
          importErrors: { base: ['hard failed'] } }),
        buildPostImportRow ({
          sourceRow: 3,
          attributes: {
            title: 'created row',
            originalCreatedFrom: '2024-01-01T00:00:00Z',
            originalCreatedBefore: '2024-01-02T00:00:00Z' },
          importStatus: 'created',
          createdPostId: 3,
          fieldWarnings: {
            thumbnailBase: ['サムネール画像を取得できませんでした．'] } })] })

    render (
        <HelmetProvider>
          <MemoryRouter initialEntries={['/posts/import/result-session/result']}>
            <Routes>
              <Route
                  path="/posts/import/:sessionId/result"
                  element={<PostImportResultPage user={buildUser ()}/>}/>
            </Routes>
          </MemoryRouter>
        </HelmetProvider>)

    expect (screen.getAllByRole ('button', { name: '編輯' })).toHaveLength (1)
    expect (screen.getAllByRole ('button', { name: '再試行' })).toHaveLength (1)
    expect (screen.getByText ('サムネール画像を取得できませんでした．'))
        .toBeInTheDocument ()
    expect (screen.getByText (
        originalCreatedAtString ('2024-01-01T00:00:00Z', '2024-01-02T00:00:00Z'),
    )).toBeInTheDocument ()
  })

  it ('shows hour-precision original-created ranges with the shared utility output', () => {
    const from = '2024-01-01T12:00:00Z'
    const before = '2024-01-01T13:00:00Z'
    savePostImportSession ('result-hour-range', {
      source: '',
      repairMode: 'all',
      rows: [buildPostImportRow ({
        sourceRow: 1,
        importStatus: 'created',
        createdPostId: 1,
        attributes: {
          title: 'created row',
          originalCreatedFrom: from,
          originalCreatedBefore: before } })] })

    render (
        <HelmetProvider>
          <MemoryRouter initialEntries={['/posts/import/result-hour-range/result']}>
            <Routes>
              <Route
                  path="/posts/import/:sessionId/result"
                  element={<PostImportResultPage user={buildUser ()}/>}/>
            </Routes>
          </MemoryRouter>
        </HelmetProvider>)

    expect (screen.getByText (originalCreatedAtString (from, before))).toBeInTheDocument ()
  })

  it ('keeps recoverable pending rows actionable and hides actions for hard failures', () => {
    savePostImportSession ('result-pending', {
      source: '',
      repairMode: 'failed',
      rows: [
        buildPostImportRow ({
          sourceRow: 1,
          importStatus: 'pending',
          recoverable: true,
          validationErrors: { title: ['invalid'] } }),
        buildPostImportRow ({
          sourceRow: 2,
          importStatus: 'pending',
          recoverable: true }),
        buildPostImportRow ({
          sourceRow: 3,
          importStatus: 'failed',
          importErrors: { base: ['hard failed'] } })] })

    render (
        <HelmetProvider>
          <MemoryRouter initialEntries={['/posts/import/result-pending/result']}>
            <Routes>
              <Route
                  path="/posts/import/:sessionId/result"
                  element={<PostImportResultPage user={buildUser ()}/>}/>
            </Routes>
          </MemoryRouter>
        </HelmetProvider>)

    expect (screen.getAllByRole ('button', { name: '編輯' })).toHaveLength (1)
    expect (screen.getAllByRole ('button', { name: '再試行' })).toHaveLength (1)
    expect (screen.getByText ('invalid')).toBeInTheDocument ()
    expect (screen.getByText ('hard failed')).toBeInTheDocument ()
  })

  it ('disables editing, retry, and navigation while retry is running', async () => {
    let resolveValidation: ((value: { rows: PostImportRow[] }) => void) | null = null
    savePostImportSession ('result-retry-busy', {
      source: '',
      repairMode: 'failed',
      rows: [
        buildPostImportRow ({
          sourceRow: 1,
          importStatus: 'failed',
          recoverable: true,
          importErrors: { base: ['failed'] } }),
        buildPostImportRow ({
          sourceRow: 2,
          importStatus: 'failed',
          recoverable: true,
          importErrors: { base: ['failed'] } })] })
    api.apiPost.mockImplementationOnce (() =>
        new Promise<{ rows: PostImportRow[] }> (resolve => {
          resolveValidation = resolve
        }))
    api.apiPost.mockResolvedValueOnce ({
      created: 0,
      skipped: 0,
      failed: 0,
      rows: [] })

    render (
        <HelmetProvider>
          <MemoryRouter initialEntries={['/posts/import/result-retry-busy/result']}>
            <Routes>
              <Route
                  path="/posts/import/:sessionId/result"
                  element={<PostImportResultPage user={buildUser ()}/>}/>
            </Routes>
          </MemoryRouter>
        </HelmetProvider>)

    fireEvent.click (screen.getAllByRole ('button', { name: '再試行' })[0])

    await waitFor (() => {
      screen.getAllByRole ('button', { name: '編輯' }).forEach (button => {
        expect (button).toBeDisabled ()
      })
      screen.getAllByRole ('button', { name: '再試行' }).forEach (button => {
        expect (button).toBeDisabled ()
      })
      expect (screen.getByRole ('button', { name: '確認画面へ戻る' })).toBeDisabled ()
      expect (screen.getByRole ('button', { name: '新しい URL リストを入力' })).toBeDisabled ()
    })

    resolveValidation?.({ rows: [buildPostImportRow ({ sourceRow: 1 })] })
  })

  it ('persists retry success to sessionStorage', async () => {
    savePostImportSession ('result-retry-success', {
      source: '',
      repairMode: 'failed',
      rows: [buildPostImportRow ({
        sourceRow: 1,
        importStatus: 'failed',
        recoverable: true,
        importErrors: { base: ['failed'] } })] })
    api.apiPost
        .mockResolvedValueOnce ({
          rows: [buildPostImportRow ({
            sourceRow: 1,
            importStatus: 'pending',
            recoverable: true })] })
        .mockResolvedValueOnce ({
          created: 1,
          skipped: 0,
          failed: 0,
          rows: [{
            sourceRow: 1,
            status: 'created',
            post: { id: 10 } }] })

    render (
        <HelmetProvider>
          <MemoryRouter initialEntries={['/posts/import/result-retry-success/result']}>
            <Routes>
              <Route
                  path="/posts/import/:sessionId/result"
                  element={<PostImportResultPage user={buildUser ()}/>}/>
            </Routes>
          </MemoryRouter>
        </HelmetProvider>)

    fireEvent.click (screen.getByRole ('button', { name: '再試行' }))

    await waitFor (() => {
      expect (loadPostImportSession ('result-retry-success')?.rows[0]?.importStatus)
          .toBe ('created')
    })
  })

  it ('keeps edited recoverable rows in session and retries with the edited values', async () => {
    savePostImportSession ('result-edit-retry', {
      source: '',
      repairMode: 'failed',
      rows: [buildPostImportRow ({
        sourceRow: 1,
        attributes: { title: 'old title' },
        importStatus: 'failed',
        recoverable: true,
        importErrors: { base: ['failed'] } })] })
    api.apiPost
        .mockResolvedValueOnce ({
          rows: [buildPostImportRow ({
            sourceRow: 1,
            attributes: { title: 'edited title' },
            importStatus: 'pending',
            recoverable: true })] })
        .mockResolvedValueOnce ({
          rows: [buildPostImportRow ({
            sourceRow: 1,
            attributes: { title: 'edited title' },
            importStatus: 'pending',
            recoverable: true })] })
        .mockResolvedValueOnce ({
          created: 1,
          skipped: 0,
          failed: 0,
          rows: [{
            sourceRow: 1,
            status: 'created',
            post: { id: 11 } }] })
    dialogue.form.mockImplementationOnce (async options => {
      let actions: DialogueFormAction[] = []
      const controls: DialogueFormControls = {
        close: vi.fn (),
        confirm: vi.fn (),
        setActions: next => {
          actions = next
        } }

      render (options.body (controls))
      await waitFor (() => expect (actions.length).toBe (2))
      fireEvent.change (screen.getByDisplayValue ('old title'), {
        target: { value: 'edited title' } })
      await act (async () => {
        await actions.find (action => action.label === '編輯内容を保存')?.onSelect ()
      })
    })

    render (
        <HelmetProvider>
          <MemoryRouter initialEntries={['/posts/import/result-edit-retry/result']}>
            <Routes>
              <Route
                  path="/posts/import/:sessionId/result"
                  element={<PostImportResultPage user={buildUser ()}/>}/>
            </Routes>
          </MemoryRouter>
        </HelmetProvider>)

    fireEvent.click (screen.getByRole ('button', { name: '編輯' }))

    await waitFor (() => {
      const saved = loadPostImportSession ('result-edit-retry')
      expect (saved?.rows[0]?.attributes.title).toBe ('edited title')
      expect (saved?.rows[0]?.importStatus).toBe ('pending')
    })

    fireEvent.click (screen.getByRole ('button', { name: '再試行' }))

    await waitFor (() => {
      expect (api.apiPost.mock.calls[2]?.[1]?.rows?.[0]?.attributes?.title)
          .toBe ('edited title')
    })
  })

  it ('persists restored failed state when retry fails', async () => {
    savePostImportSession ('result-retry-failure', {
      source: '',
      repairMode: 'failed',
      rows: [buildPostImportRow ({
        sourceRow: 1,
        importStatus: 'failed',
        recoverable: true,
        importErrors: { base: ['failed'] } })] })
    api.apiPost.mockRejectedValueOnce (new Error ('network error'))

    render (
        <HelmetProvider>
          <MemoryRouter initialEntries={['/posts/import/result-retry-failure/result']}>
            <Routes>
              <Route
                  path="/posts/import/:sessionId/result"
                  element={<PostImportResultPage user={buildUser ()}/>}/>
            </Routes>
          </MemoryRouter>
        </HelmetProvider>)

    fireEvent.click (screen.getByRole ('button', { name: '再試行' }))

    await waitFor (() => {
      const saved = loadPostImportSession ('result-retry-failure')
      expect (saved?.rows[0]?.importStatus).toBe ('failed')
      expect (saved?.rows[0]?.recoverable).toBe (true)
    })
  })

  it ('restores the failed row when the retry import response is incomplete', async () => {
    savePostImportSession ('result-retry-missing-row', {
      source: '',
      repairMode: 'failed',
      rows: [buildPostImportRow ({
        sourceRow: 1,
        importStatus: 'failed',
        recoverable: true,
        importErrors: { base: ['failed'] } })] })
    api.apiPost
        .mockResolvedValueOnce ({
          rows: [buildPostImportRow ({
            sourceRow: 1,
            importStatus: 'pending',
            recoverable: true })] })
        .mockResolvedValueOnce ({
          created: 1,
          skipped: 0,
          failed: 0,
          rows: [] })

    render (
        <HelmetProvider>
          <MemoryRouter initialEntries={['/posts/import/result-retry-missing-row/result']}>
            <Routes>
              <Route
                  path="/posts/import/:sessionId/result"
                  element={<PostImportResultPage user={buildUser ()}/>}/>
            </Routes>
          </MemoryRouter>
        </HelmetProvider>)

    fireEvent.click (screen.getByRole ('button', { name: '再試行' }))

    await waitFor (() => {
      const saved = loadPostImportSession ('result-retry-missing-row')
      expect (saved?.rows[0]?.importStatus).toBe ('failed')
      expect (toastApi.toast).toHaveBeenCalledWith (
          expect.objectContaining ({ title: '登録結果が不完全でした' }))
    })
  })
})
