import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import {
  fireEvent,
  render,
  screen,
  waitFor,
  within,
} from '@testing-library/react';
import { NuqsTestingAdapter, type UrlUpdateEvent } from 'nuqs/adapters/testing';
import { OverlayProvider } from 'overlay-kit';
import { MemoryRouter } from 'react-router';
import { beforeEach, describe, expect, it, vi } from 'vitest';

import { SampleTablePage } from './SampleTablePage';

const SEED_POSTS = [
  { id: 1, userId: 1, title: '가 첫 글', body: '본문 1' },
  { id: 2, userId: 1, title: '나 둘째 글', body: '본문 2' },
  { id: 3, userId: 1, title: '다 셋째 글', body: '본문 3' },
  { id: 4, userId: 2, title: '라 넷째 글', body: '본문 4' },
  { id: 5, userId: 2, title: '마 다섯째 글', body: '본문 5' },
  { id: 6, userId: 3, title: '바 여섯째 글', body: '본문 6' },
  { id: 7, userId: 3, title: '사 일곱째 글', body: '본문 7' },
];

// _generated 직접 import는 depcruise가 봉인한다 — 목 데이터는 vi.hoisted 참조로 주입.
const mocks = vi.hoisted(() => ({
  posts: [] as { id: number; userId: number; title: string; body: string }[],
  deleteFails: false,
  listFailures: 0,
}));

vi.mock('@/shared/api/_generated/api', () => ({
  getPosts: vi.fn(async () => {
    if (mocks.listFailures > 0) {
      mocks.listFailures -= 1;
      throw new Error('mock: 목록 조회 실패');
    }
    return mocks.posts;
  }),
  getPost: vi.fn(),
  createPost: vi.fn(),
  deletePost: vi.fn(async () => {
    if (mocks.deleteFails) {
      throw new Error('mock: 삭제 실패');
    }
  }),
}));

beforeEach(() => {
  mocks.posts = SEED_POSTS;
  mocks.deleteFails = false;
  mocks.listFailures = 0;
});

const renderPage = (
  searchParams = '',
  onUrlUpdate?: (event: UrlUpdateEvent) => void
) => {
  const client = new QueryClient({
    defaultOptions: { queries: { retry: false } },
  });

  return render(
    <QueryClientProvider client={client}>
      <NuqsTestingAdapter searchParams={searchParams} onUrlUpdate={onUrlUpdate}>
        <MemoryRouter>
          <OverlayProvider>
            <SampleTablePage />
          </OverlayProvider>
        </MemoryRouter>
      </NuqsTestingAdapter>
    </QueryClientProvider>
  );
};

const findTable = async () => within(await screen.findByRole('table'));

describe('SampleTablePage', () => {
  it('첫 페이지에는 페이지 크기(5개)만큼만 보여준다', async () => {
    renderPage();

    const table = await findTable();
    expect(table.getByText('가 첫 글')).toBeInTheDocument();
    expect(table.getByText('마 다섯째 글')).toBeInTheDocument();
    expect(table.queryByText('바 여섯째 글')).not.toBeInTheDocument();
  });

  it('URL의 검색어(q)로 목록을 거른다', async () => {
    renderPage('?q=둘째');

    const table = await findTable();
    expect(table.getByText('나 둘째 글')).toBeInTheDocument();
    expect(table.queryByText('가 첫 글')).not.toBeInTheDocument();
  });

  it('URL의 작성자(author)로 목록을 거른다', async () => {
    renderPage('?author=3');

    const table = await findTable();
    expect(table.getByText('바 여섯째 글')).toBeInTheDocument();
    expect(table.getByText('사 일곱째 글')).toBeInTheDocument();
    expect(table.queryByText('가 첫 글')).not.toBeInTheDocument();
  });

  it('URL의 페이지(page)로 다음 페이지를 보여준다', async () => {
    renderPage('?page=2');

    const table = await findTable();
    expect(table.getByText('바 여섯째 글')).toBeInTheDocument();
    expect(table.queryByText('가 첫 글')).not.toBeInTheDocument();
  });

  it('검색어를 바꾸면 페이지가 1로 되돌아간다', async () => {
    const updates: string[] = [];
    renderPage('?page=2', (event) => {
      updates.push(event.queryString);
    });

    await screen.findByRole('table');
    fireEvent.change(screen.getByLabelText('글 검색'), {
      target: { value: '둘째' },
    });

    await waitFor(() => {
      const last = updates.at(-1) ?? '';
      expect(last).toContain('q=');
      expect(last).not.toContain('page=');
    });
  });

  it('목록 패널과 새 글 버튼을 함께 보여준다', async () => {
    renderPage();

    await screen.findByRole('table');
    expect(screen.getByText('최근 글')).toBeInTheDocument();
    expect(screen.getByRole('button', { name: '새 글' })).toBeInTheDocument();
    expect(
      screen.getAllByRole('button', { name: '삭제' }).length
    ).toBeGreaterThan(0);
  });

  it('삭제를 확정하면 표와 목록이 다시 조회된다', async () => {
    renderPage();
    await screen.findByRole('table');

    mocks.posts = SEED_POSTS.slice(1);

    fireEvent.click(screen.getAllByRole('button', { name: '삭제' })[0]);
    const dialog = await screen.findByRole('dialog');
    fireEvent.click(within(dialog).getByRole('button', { name: '삭제' }));

    await waitFor(() => {
      expect(screen.queryAllByText('가 첫 글')).toHaveLength(0);
    });
    expect(screen.getAllByText('나 둘째 글').length).toBeGreaterThan(0);
  });

  it('삭제가 실패하면 목록이 그대로 남고 버튼이 다시 눌린다', async () => {
    mocks.deleteFails = true;
    renderPage();
    await screen.findByRole('table');

    fireEvent.click(screen.getAllByRole('button', { name: '삭제' })[0]);
    const dialog = await screen.findByRole('dialog');
    fireEvent.click(within(dialog).getByRole('button', { name: '삭제' }));

    await waitFor(() => {
      expect(screen.queryByRole('dialog')).toBeNull();
    });

    expect(screen.getAllByText('가 첫 글').length).toBeGreaterThan(0);
    await waitFor(() => {
      expect(screen.getAllByRole('button', { name: '삭제' })[0]).toBeEnabled();
    });
  });

  it('목록을 못 불러왔을 때 다시 시도가 실제로 다시 불러온다', async () => {
    mocks.listFailures = 1;
    renderPage();

    expect(
      await screen.findByText('대시보드를 불러오지 못했어요.')
    ).toBeInTheDocument();

    fireEvent.click(screen.getAllByRole('button', { name: '다시 시도' })[0]);

    const table = await findTable();
    expect(table.getByText('가 첫 글')).toBeInTheDocument();
  });
});
