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

import NicoTagListPage from '@/pages/tags/NicoTagListPage'
import { dateString } from '@/lib/utils'
import { buildTag, buildUser } from '@/test/factories'
import { renderWithProviders } from '@/test/render'

import type { NicoTag } from '@/types'

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

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

const dialogue = vi.hoisted (() => ({
  confirm: vi.fn (),
}))

const scrollIntoView = vi.fn ()

vi.mock ('@/lib/api', () => api)
vi.mock ('@/components/ui/use-toast', () => toastApi)
vi.mock ('@/components/dialogues/DialogueProvider', async importOriginal => ({
  ...await importOriginal<
      typeof import('@/components/dialogues/DialogueProvider')> (),
  useDialogue: () => dialogue }))

const buildNicoTag = (values: Partial<NicoTag> = {}): NicoTag => ({
    ...buildTag (),
    ...values,
    category:               'nico',
    linkedTags:             values.linkedTags ?? [],
    recentPostTagCreatedAt: values.recentPostTagCreatedAt ?? null,
})

const renderPage = (route = '/tags/nico') =>
  renderWithProviders (
      <NicoTagListPage user={buildUser ({ role: 'member' })}/>,
      { route },
  )

describe ('NicoTagListPage', () => {
  beforeEach (() => {
    vi.clearAllMocks ()
    api.isApiError.mockReturnValue (false)
    dialogue.confirm.mockResolvedValue (true)
    Element.prototype.scrollIntoView = scrollIntoView
    scrollIntoView.mockClear ()
  })

  it ('loads a filtered page from URL search parameters', async () => {
    api.apiGet.mockResolvedValue ({
      tags:  [buildNicoTag ({
        id:                     1,
        name:                   'nico:linked',
        createdAt:              '2024-01-02T03:04:05Z',
        recentPostTagCreatedAt: '2025-01-02T03:04:05Z',
        updatedAt:              '2026-01-02T03:04:05Z',
      })],
      count: 21,
    })

    renderPage (
        '/tags/nico?name=linked&linked_tag=destination&link_status=linked'
	+ '&page=2&order=name:asc',
    )

    await waitFor (() => {
      expect (api.apiGet).toHaveBeenCalledWith (
          '/tags/nico',
          { params: {
            page:        2,
            limit:       20,
            name:        'linked',
            linked_tag:  'destination',
            link_status: 'linked',
            order:       'name:asc',
          } },
      )
    })
    expect (await screen.findByText ('21 件')).toBeInTheDocument ()
    expect (screen.getByLabelText ('前のページ')).toBeInTheDocument ()
    expect (screen.queryByText ('なし')).not.toBeInTheDocument ()
    expect (screen.getByText (dateString ('2025-01-02T03:04:05Z'))).toBeInTheDocument ()
    expect (screen.queryByText (dateString ('2026-01-02T03:04:05Z'))).not.toBeInTheDocument ()
    expect (screen.getByRole ('link', { name: 'ニコニコタグ ▲' })).toHaveAttribute (
        'href',
        expect.stringContaining ('order=name%3Adesc'),
    )
    expect (screen.getByRole ('link', { name: '最初に記載された日時' })).toHaveAttribute (
        'href',
        expect.stringContaining ('order=created_at%3Adesc'),
    )
    expect (screen.getByRole ('link', { name: '最近記載された日時' })).toHaveAttribute (
        'href',
        expect.stringContaining ('order=updated_at%3Adesc'),
    )

    fireEvent.mouseEnter (screen.getByLabelText ('前のページ'))
    await waitFor (() => {
      expect (api.apiGet).toHaveBeenLastCalledWith (
          '/tags/nico',
          { params: {
            page:        1,
            limit:       20,
            name:        'linked',
            linked_tag:  'destination',
            link_status: 'linked',
            order:       'name:asc',
          } },
      )
    })
  })

  it ('scrolls to the table when moving between pages', async () => {
    api.apiGet.mockResolvedValue ({
      tags:  [buildNicoTag ({ id: 1, name: 'nico:linked' })],
      count: 21,
    })

    renderPage ()

    fireEvent.click (await screen.findByLabelText ('次のページ'))

    await waitFor (() => {
      expect (scrollIntoView).toHaveBeenCalledWith ({ behavior: 'smooth' })
    })
  })

  it ('navigates with submitted search conditions', async () => {
    api.apiGet.mockResolvedValue ({ tags: [], count: 0 })
    renderPage ()

    fireEvent.change (screen.getByLabelText ('ニコニコタグ'), {
      target: { value: 'source' },
    })
    fireEvent.change (screen.getByLabelText ('連携タグ'), {
      target: { value: 'destination' },
    })
    fireEvent.change (screen.getByLabelText ('連携状態'), {
      target: { value: 'unlinked' },
    })
    fireEvent.submit (screen.getByRole ('button', { name: '検索' }).closest ('form')!)

    await waitFor (() => {
      expect (api.apiGet).toHaveBeenLastCalledWith (
          '/tags/nico',
          { params: expect.objectContaining ({
            page:        1,
            name:        'source',
            linked_tag:  'destination',
            link_status: 'unlinked',
          }) },
      )
    })
  })

  it ('updates links from a tag card', async () => {
    api.apiGet
      .mockResolvedValueOnce ({
        tags:  [buildNicoTag ({ id: 7, name: 'nico:source' })],
        count: 1,
      })
      .mockResolvedValueOnce ({
        tags: [
          buildNicoTag ({
            id:         7,
            name:       'nico:source',
            linkedTags: [buildTag ({ id: 8, name: '連携先' })],
          }),
        ],
        count: 1,
      })
    api.apiPut.mockResolvedValueOnce ([buildTag ({ id: 8, name: '連携先' })])

    renderPage ()

    fireEvent.click (await screen.findByRole ('button', { name: '編集' }))
    fireEvent.change (screen.getByLabelText ('連携する広場タグ'), {
      target: { value: '連携先' },
    })
    fireEvent.click (screen.getByRole ('button', { name: '保存' }))

    await waitFor (() => {
      expect (api.apiPut).toHaveBeenCalledWith (
          '/tags/nico/7',
          expect.any (FormData),
          { headers: { 'Content-Type': 'multipart/form-data' } },
      )
    })
    expect (await screen.findByText ('1 件')).toBeInTheDocument ()
  })

  it ('asks before discarding changes when editing another tag', async () => {
    api.apiGet.mockResolvedValueOnce ({
      tags: [
        buildNicoTag ({ id: 1, name: 'nico:first' }),
        buildNicoTag ({ id: 2, name: 'nico:second' }),
      ],
      count: 2,
    })

    renderPage ()
    dialogue.confirm.mockResolvedValueOnce (false)

    const editButtons = await screen.findAllByRole ('button', { name: '編集' })
    fireEvent.click (editButtons[0])
    fireEvent.change (screen.getByLabelText ('連携する広場タグ'), {
      target: { value: '入力中' },
    })
    fireEvent.click (screen.getAllByRole ('button', { name: '編集' })[0])

    await waitFor (() => {
      expect (dialogue.confirm).toHaveBeenCalledWith ({
        title:       '編集中の内容を破棄しますか?',
        confirmText: '破棄',
        variant:     'danger',
      })
    })
    expect (screen.getAllByLabelText ('連携する広場タグ')).toHaveLength (1)
    expect (screen.getByLabelText ('連携する広場タグ')).toHaveValue ('入力中')
  })

  it ('switches editing rows without confirmation when unchanged', async () => {
    api.apiGet.mockResolvedValueOnce ({
      tags: [
        buildNicoTag ({ id: 1, name: 'nico:first' }),
        buildNicoTag ({ id: 2, name: 'nico:second' }),
      ],
      count: 2,
    })

    renderPage ()

    const editButtons = await screen.findAllByRole ('button', { name: '編集' })
    fireEvent.click (editButtons[0])
    fireEvent.click (screen.getAllByRole ('button', { name: '編集' })[0])

    expect (dialogue.confirm).not.toHaveBeenCalled ()
    expect (screen.getAllByLabelText ('連携する広場タグ')).toHaveLength (1)
  })

  it ('shows tags field validation errors inside the edited card', async () => {
    api.apiGet.mockResolvedValueOnce ({
      tags:  [buildNicoTag ({ id: 7, name: 'nico:source' })],
      count: 1,
    })
    api.isApiError.mockReturnValue (true)
    api.apiPut.mockRejectedValueOnce ({
      response: {
        status: 422,
        data:   {
          type:        'validation_error',
          errors:      { tags: ['タグ名を確認してください．'] },
          base_errors: [],
        },
      },
    })

    renderPage ()

    fireEvent.click (await screen.findByRole ('button', { name: '編集' }))
    fireEvent.click (screen.getByRole ('button', { name: '保存' }))

    expect (await screen.findByText ('タグ名を確認してください．')).toBeInTheDocument ()
    expect (screen.getByLabelText ('連携する広場タグ')).toHaveAttribute ('aria-invalid', 'true')
  })
})
