import { act } from 'react';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { usePaginatedResource } from '../../src/hooks/usePaginatedResource';
import { renderHook } from '../helpers/renderHook';

type Item = {
    id: string;
};

async function flushDebounce(delay = 250) {
    await act(async () => {
        vi.advanceTimersByTime(delay);
        await Promise.resolve();
    });
}

describe('usePaginatedResource', () => {
    beforeEach(() => {
        vi.useFakeTimers();
    });

    afterEach(() => {
        vi.useRealTimers();
    });

    it('loads first page after debounce and stores response fields', async () => {
        const loadMock = vi.fn(async () => ({
            items: [{ id: '1' }] as Item[],
            page: 1,
            pageSize: 10,
            total: 25,
            totalPages: 3,
        }));

        const { result } = renderHook(() =>
            usePaginatedResource<Item>({
                load: loadMock,
                sort: '-createdAt',
            }),
        );

        await flushDebounce();

        expect(loadMock).toHaveBeenCalledWith({
            q: '',
            page: 1,
            pageSize: 10,
            sort: '-createdAt',
        });
        expect(result.current.items).toEqual([{ id: '1' }]);
        expect(result.current.total).toBe(25);
        expect(result.current.totalPages).toBe(3);
        expect(result.current.isLoading).toBe(false);
        expect(result.current.error).toBeNull();
    });

    it('resets page to 1 when query or page size changes', async () => {
        const loadMock = vi.fn(
            async ({ q, page, pageSize }: { q: string; page: number; pageSize: number }) => ({
                items: [{ id: `${q}:${page}:${pageSize}` }] as Item[],
                page,
                pageSize,
                total: 1,
                totalPages: 1,
            }),
        );

        const { result } = renderHook(() =>
            usePaginatedResource<Item>({
                load: loadMock,
            }),
        );

        await flushDebounce();

        act(() => {
            result.current.setPage(2);
        });
        await flushDebounce();
        expect(loadMock).toHaveBeenLastCalledWith({
            q: '',
            page: 2,
            pageSize: 10,
            sort: undefined,
        });

        act(() => {
            result.current.setQuery('search');
        });
        await flushDebounce();
        expect(loadMock).toHaveBeenLastCalledWith({
            q: 'search',
            page: 1,
            pageSize: 10,
            sort: undefined,
        });

        act(() => {
            result.current.setPage(3);
        });
        await flushDebounce();

        act(() => {
            result.current.setPageSize(20);
        });
        await flushDebounce();
        expect(loadMock).toHaveBeenLastCalledWith({
            q: 'search',
            page: 1,
            pageSize: 20,
            sort: undefined,
        });
    });

    it('cancels stale debounce timers when inputs change quickly', async () => {
        const loadMock = vi.fn(async () => ({
            items: [] as Item[],
            page: 1,
            pageSize: 10,
            total: 0,
            totalPages: 0,
        }));

        const { result } = renderHook(() =>
            usePaginatedResource<Item>({
                load: loadMock,
                debounceMs: 100,
            }),
        );

        act(() => {
            result.current.setQuery('latest');
        });

        await flushDebounce(100);

        expect(loadMock).toHaveBeenCalledTimes(1);
        expect(loadMock).toHaveBeenCalledWith({
            q: 'latest',
            page: 1,
            pageSize: 10,
            sort: undefined,
        });
    });

    it('maps thrown errors into string error state', async () => {
        const loadMock = vi
            .fn()
            .mockRejectedValueOnce(new Error('Load failed'))
            .mockRejectedValueOnce('unknown');

        const { result } = renderHook(() =>
            usePaginatedResource<Item>({
                load: loadMock,
            }),
        );

        await flushDebounce();
        expect(result.current.error).toBe('Load failed');
        expect(result.current.isLoading).toBe(false);

        act(() => {
            result.current.setPage(2);
        });
        await flushDebounce();

        expect(result.current.error).toBe('Request failed. Please try again.');
        expect(result.current.isLoading).toBe(false);
    });
});
