// 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 | undefined,
  ): 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 handle a non-ok response with a simple JSON error message', async () => {
    const errorPayload = { message: 'Server is on fire' };
    mockApiFunction.mockResolvedValue(new Response(JSON.stringify(errorPayload), { status: 500 }));

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

    await waitFor(() => {
      expect(result.current.isLoading).toBe(false);
      expect(result.current.error).toBeInstanceOf(Error);
      expect(result.current.error?.message).toBe('Server is on fire');
    });
  });

  it('should handle a non-ok response with a Zod-style error message array', async () => {
    const errorPayload = {
      issues: [
        { path: ['query', 'limit'], message: 'Limit must be a positive number' },
        { path: ['query', 'offset'], message: 'Offset must be non-negative' },
      ],
    };
    mockApiFunction.mockResolvedValue(new Response(JSON.stringify(errorPayload), { status: 400 }));

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

    await waitFor(() => {
      expect(result.current.isLoading).toBe(false);
      expect(result.current.error).toBeInstanceOf(Error);
      expect(result.current.error?.message).toBe(
        'query.limit: Limit must be a positive number; query.offset: Offset must be non-negative',
      );
    });
  });

  it('should handle a Zod-style error message where path is missing', async () => {
    const errorPayload = {
      issues: [{ message: 'Global error' }],
    };
    mockApiFunction.mockResolvedValue(new Response(JSON.stringify(errorPayload), { status: 400 }));

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

    await waitFor(() => {
      expect(result.current.isLoading).toBe(false);
      expect(result.current.error).toBeInstanceOf(Error);
      expect(result.current.error?.message).toBe('Error: Global error');
    });
  });

  it('should handle a non-ok response with a non-JSON body', async () => {
    mockApiFunction.mockResolvedValue(
      new Response('Internal Server Error', {
        status: 500,
        statusText: 'Server Error',
      }),
    );

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

    await waitFor(() => {
      expect(result.current.isLoading).toBe(false);
      expect(result.current.error).toBeInstanceOf(Error);
      expect(result.current.error?.message).toBe('Request failed with status 500: Server Error');
    });
  });

  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
  });

  it('should use 0 as default initialCursor if not provided', async () => {
    mockApiFunction.mockResolvedValue(createMockResponse([], null));
    renderHook(() => useInfiniteQuery(mockApiFunction));
    expect(mockApiFunction).toHaveBeenCalledWith(0);
  });

  it('should clear error when fetching next page', async () => {
    const page1Items = [{ id: 1 }];
    const error = new Error('Fetch failed');

    // First page succeeds
    mockApiFunction.mockResolvedValueOnce(createMockResponse(page1Items, 2));
    // Second page fails
    mockApiFunction.mockRejectedValueOnce(error);
    // Third attempt (retry second page) succeeds
    mockApiFunction.mockResolvedValueOnce(createMockResponse([], null));

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

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

    // Try fetch next page -> fails
    act(() => {
      result.current.fetchNextPage();
    });
    await waitFor(() => expect(result.current.error).toEqual(error));
    expect(result.current.isFetchingNextPage).toBe(false);

    // Try fetch next page again -> succeeds, error should be cleared
    act(() => {
      result.current.fetchNextPage();
    });
    expect(result.current.error).toBeNull();
    expect(result.current.isFetchingNextPage).toBe(true);

    await waitFor(() => expect(result.current.isFetchingNextPage).toBe(false));
    expect(result.current.error).toBeNull();
  });

  it('should clear error when refetching', async () => {
    const error = new Error('Initial fail');
    mockApiFunction.mockRejectedValueOnce(error);
    mockApiFunction.mockResolvedValueOnce(createMockResponse([], null));

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

    await waitFor(() => expect(result.current.error).toEqual(error));

    act(() => {
      result.current.refetch();
    });
    expect(result.current.error).toBeNull();
    expect(result.current.isLoading).toBe(true);

    await waitFor(() => expect(result.current.isLoading).toBe(false));
    expect(result.current.error).toBeNull();
  });

  it('should set hasNextPage to false if nextCursor is undefined', async () => {
    mockApiFunction.mockResolvedValue(createMockResponse([], undefined));
    const { result } = renderHook(() => useInfiniteQuery(mockApiFunction));
    await waitFor(() => expect(result.current.hasNextPage).toBe(false));
  });

  it('should handle non-Error objects thrown by apiFunction', async () => {
    mockApiFunction.mockRejectedValue('String Error');

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

    await waitFor(() => {
      expect(result.current.isLoading).toBe(false);
      expect(result.current.error).toBeInstanceOf(Error);
      expect(result.current.error?.message).toBe('An unknown error occurred.');
    });
  });
});
