// src/hooks/useInfiniteQuery.test.ts
import { renderHook, waitFor, act } from '@testing-library/react';
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { useInfiniteQuery, PaginatedResponse } from './useInfiniteQuery';

// Mock the API function that the hook will call
const mockApiFunction = vi.fn();

describe('useInfiniteQuery Hook', () => {
  beforeEach(() => {
    vi.clearAllMocks();
  });

  // Helper to create a mock paginated response
  const createMockResponse = <T>(items: T[], nextCursor: number | string | null): Response => {
    const paginatedResponse: PaginatedResponse<T> = { items, nextCursor };
    return new Response(JSON.stringify(paginatedResponse));
  };

  it('should be in loading state initially and fetch the first page', async () => {
    const page1Items = [{ id: 1 }, { id: 2 }];
    mockApiFunction.mockResolvedValue(createMockResponse(page1Items, 2));

    const { result } = renderHook(() => useInfiniteQuery(mockApiFunction, { initialCursor: 1 }));

    // Initial state
    expect(result.current.isLoading).toBe(true);
    expect(result.current.data).toEqual([]);

    await waitFor(() => {
      expect(result.current.isLoading).toBe(false);
      expect(result.current.data).toEqual(page1Items);
      expect(result.current.hasNextPage).toBe(true);
    });

    expect(mockApiFunction).toHaveBeenCalledWith(1);
  });

  it('should fetch the next page and append data', async () => {
    const page1Items = [{ id: 1 }];
    const page2Items = [{ id: 2 }];
    mockApiFunction
      .mockResolvedValueOnce(createMockResponse(page1Items, 2))
      .mockResolvedValueOnce(createMockResponse(page2Items, null)); // Last page

    const { result } = renderHook(() => useInfiniteQuery(mockApiFunction, { initialCursor: 1 }));

    // Wait for the first page to load
    await waitFor(() => expect(result.current.isLoading).toBe(false));
    expect(result.current.data).toEqual(page1Items);

    // Act: fetch the next page
    act(() => {
      result.current.fetchNextPage();
    });

    // Check fetching state
    expect(result.current.isFetchingNextPage).toBe(true);

    // Wait for the second page to load
    await waitFor(() => {
      expect(result.current.isFetchingNextPage).toBe(false);
      // Data should be appended
      expect(result.current.data).toEqual([...page1Items, ...page2Items]);
      // hasNextPage should now be false
      expect(result.current.hasNextPage).toBe(false);
    });

    expect(mockApiFunction).toHaveBeenCalledTimes(2);
    expect(mockApiFunction).toHaveBeenCalledWith(2); // Called with the next cursor
  });

  it('should handle API errors', async () => {
    const apiError = new Error('Network Error');
    mockApiFunction.mockRejectedValue(apiError);

    const { result } = renderHook(() => useInfiniteQuery(mockApiFunction));

    await waitFor(() => {
      expect(result.current.isLoading).toBe(false);
      expect(result.current.error).toEqual(apiError);
      expect(result.current.data).toEqual([]);
    });
  });

  it('should set hasNextPage to false when nextCursor is null', async () => {
    const page1Items = [{ id: 1 }];
    mockApiFunction.mockResolvedValue(createMockResponse(page1Items, null));

    const { result } = renderHook(() => useInfiniteQuery(mockApiFunction));

    await waitFor(() => {
      expect(result.current.hasNextPage).toBe(false);
    });
  });

  it('should not fetch next page if hasNextPage is false or already fetching', async () => {
    const page1Items = [{ id: 1 }];
    mockApiFunction.mockResolvedValue(createMockResponse(page1Items, null)); // No next page

    const { result } = renderHook(() => useInfiniteQuery(mockApiFunction));

    // Wait for initial fetch
    await waitFor(() => expect(result.current.isLoading).toBe(false));
    expect(result.current.hasNextPage).toBe(false);
    expect(mockApiFunction).toHaveBeenCalledTimes(1);

    // Act: try to fetch next page
    act(() => {
      result.current.fetchNextPage();
    });

    // Assert: no new API call was made
    expect(mockApiFunction).toHaveBeenCalledTimes(1);
    expect(result.current.isFetchingNextPage).toBe(false);
  });

  it('should refetch the first page when refetch is called', async () => {
    const page1Items = [{ id: 1 }];
    const page2Items = [{ id: 2 }];
    const refetchedItems = [{ id: 10 }];

    mockApiFunction
      .mockResolvedValueOnce(createMockResponse(page1Items, 2))
      .mockResolvedValueOnce(createMockResponse(page2Items, 3))
      .mockResolvedValueOnce(createMockResponse(refetchedItems, 11)); // Refetch response

    const { result } = renderHook(() => useInfiniteQuery(mockApiFunction, { initialCursor: 1 }));

    // Load first two pages
    await waitFor(() => expect(result.current.isLoading).toBe(false));
    act(() => { result.current.fetchNextPage(); });
    await waitFor(() => expect(result.current.isFetchingNextPage).toBe(false));

    expect(result.current.data).toEqual([...page1Items, ...page2Items]);
    expect(mockApiFunction).toHaveBeenCalledTimes(2);

    // Act: call refetch
    act(() => { result.current.refetch(); });

    // Assert: data is cleared and then repopulated with the first page
    expect(result.current.isLoading).toBe(true);
    await waitFor(() => expect(result.current.isLoading).toBe(false));
    expect(result.current.data).toEqual(refetchedItems);
    expect(mockApiFunction).toHaveBeenCalledTimes(3);
    expect(mockApiFunction).toHaveBeenLastCalledWith(1); // Called with initial cursor
  });
});