import React from 'react'

import {renderHook, waitFor, act} from '@testing-library/react'
import {describe, expect, it, vi} from 'vitest'

import {MinisQueryProvider} from './MinisQueryProvider'
import {useShopActionInfiniteQuery} from './useShopActionInfiniteQuery'

describe('useShopActionInfiniteQuery', () => {
  const wrapper = ({children}: {children: React.ReactNode}) => (
    <MinisQueryProvider>{children}</MinisQueryProvider>
  )

  describe('Data Fetching', () => {
    it('fetches and returns flattened array data', async () => {
      const mockAction = vi.fn().mockResolvedValue({
        ok: true,
        data: {
          data: [{id: '1'}, {id: '2'}],
          pageInfo: {hasNextPage: false, endCursor: null},
        },
      })

      const {result} = renderHook(
        () => useShopActionInfiniteQuery(['test-array'], mockAction, {}),
        {wrapper}
      )

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

      expect(result.current.data).toEqual([{id: '1'}, {id: '2'}])
      expect(result.current.hasNextPage).toBe(false)
      expect(mockAction).toHaveBeenCalledWith({})
    })

    it('handles non-array data by returning it directly', async () => {
      const mockAction = vi.fn().mockResolvedValue({
        ok: true,
        data: {
          data: {items: [{id: '1'}]},
          pageInfo: {hasNextPage: false, endCursor: null},
        },
      })

      const {result} = renderHook(
        () => useShopActionInfiniteQuery(['test-nonarray'], mockAction, {}),
        {wrapper}
      )

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

      // Non-array data is returned as-is from the first page
      expect(result.current.data).toEqual({items: [{id: '1'}]})
    })

    it('returns null when data is null', async () => {
      const mockAction = vi.fn().mockResolvedValue({
        ok: true,
        data: {
          data: null,
          pageInfo: {hasNextPage: false, endCursor: null},
        },
      })

      const {result} = renderHook(
        () => useShopActionInfiniteQuery(['test-null-data'], mockAction, {}),
        {wrapper}
      )

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

      expect(result.current.data).toBeNull()
    })
  })

  describe('Pagination Logic', () => {
    it('appends data when fetching more pages', async () => {
      const page1 = [{id: '1'}]
      const page2 = [{id: '2'}]

      const mockAction = vi
        .fn()
        .mockResolvedValueOnce({
          ok: true,
          data: {
            data: page1,
            pageInfo: {hasNextPage: true, endCursor: 'cursor1'},
          },
        })
        .mockResolvedValueOnce({
          ok: true,
          data: {
            data: page2,
            pageInfo: {hasNextPage: false, endCursor: null},
          },
        })

      const {result} = renderHook(
        () => useShopActionInfiniteQuery(['test-pagination'], mockAction, {}),
        {wrapper}
      )

      await waitFor(() => {
        expect(result.current.data).toEqual(page1)
        expect(result.current.hasNextPage).toBe(true)
      })

      // Fetch more
      await act(async () => {
        await result.current.fetchMore()
      })

      // Data should be flattened and appended
      await waitFor(() => {
        expect(result.current.data).toEqual([...page1, ...page2])
      })

      expect(result.current.hasNextPage).toBe(false)
      expect(mockAction).toHaveBeenCalledTimes(2)
    })

    it('passes cursor to subsequent pages', async () => {
      const mockAction = vi
        .fn()
        .mockResolvedValueOnce({
          ok: true,
          data: {
            data: [{id: '1'}],
            pageInfo: {hasNextPage: true, endCursor: 'cursor1'},
          },
        })
        .mockResolvedValueOnce({
          ok: true,
          data: {
            data: [{id: '2'}],
            pageInfo: {hasNextPage: false, endCursor: null},
          },
        })

      const {result} = renderHook(
        () => useShopActionInfiniteQuery(['test-cursor'], mockAction, {}),
        {wrapper}
      )

      await waitFor(() => {
        expect(result.current.data).toEqual([{id: '1'}])
        expect(result.current.hasNextPage).toBe(true)
      })

      await act(async () => {
        await result.current.fetchMore()
      })

      // Second call should include the cursor
      expect(mockAction).toHaveBeenNthCalledWith(2, {after: 'cursor1'})
    })

    it('preserves other params when fetching more', async () => {
      const mockAction = vi
        .fn()
        .mockResolvedValueOnce({
          ok: true,
          data: {
            data: [{id: '1'}],
            pageInfo: {hasNextPage: true, endCursor: 'cursor1'},
          },
        })
        .mockResolvedValueOnce({
          ok: true,
          data: {
            data: [{id: '2'}],
            pageInfo: {hasNextPage: false, endCursor: null},
          },
        })

      const {result} = renderHook(
        () =>
          useShopActionInfiniteQuery(['test-params'], mockAction, {
            query: 'shoes',
            fetchPolicy: 'cache-first',
          }),
        {wrapper}
      )

      await waitFor(() => {
        expect(result.current.data).toEqual([{id: '1'}])
        expect(result.current.hasNextPage).toBe(true)
      })

      await act(async () => {
        await result.current.fetchMore()
      })

      // Should preserve original params and add cursor
      expect(mockAction).toHaveBeenNthCalledWith(2, {
        query: 'shoes',
        fetchPolicy: 'cache-first',
        after: 'cursor1',
      })
    })
  })

  describe('Error Handling', () => {
    it('handles action errors', async () => {
      const mockAction = vi.fn().mockRejectedValue(new Error('API Error'))

      const {result} = renderHook(
        () => useShopActionInfiniteQuery(['test-api-error'], mockAction, {}),
        {wrapper}
      )

      // Wait for loading to complete (after retries)
      await waitFor(
        () => {
          expect(result.current.loading).toBe(false)
        },
        {timeout: 3000}
      )

      expect(result.current.data).toBeNull()
      expect(result.current.error).toBeInstanceOf(Error)
      expect(result.current.error?.message).toBe('API Error')
    })
  })

  describe('Skip Parameter', () => {
    it('does not fetch when skip is true', async () => {
      const mockAction = vi.fn()

      renderHook(
        () =>
          useShopActionInfiniteQuery(
            ['test-skip-inf'],
            mockAction,
            {},
            {skip: true}
          ),
        {wrapper}
      )

      await new Promise(resolve => setTimeout(resolve, 100))

      expect(mockAction).not.toHaveBeenCalled()
    })

    it('fetches when skip is false', async () => {
      const mockAction = vi.fn().mockResolvedValue({
        ok: true,
        data: {
          data: [{id: 'noskip'}],
          pageInfo: {hasNextPage: false, endCursor: null},
        },
      })

      const {result} = renderHook(
        () =>
          useShopActionInfiniteQuery(
            ['test-noskip-fetch'],
            mockAction,
            {},
            {skip: false}
          ),
        {wrapper}
      )

      await waitFor(() => {
        expect(result.current.data).toEqual([{id: 'noskip'}])
      })

      expect(mockAction).toHaveBeenCalled()
    })
  })

  describe('API Contract', () => {
    it('returns expected shape', async () => {
      const mockAction = vi.fn().mockResolvedValue({
        ok: true,
        data: {
          data: [{id: 'contract'}],
          pageInfo: {hasNextPage: false, endCursor: null},
        },
      })

      const {result} = renderHook(
        () => useShopActionInfiniteQuery(['test-contract-inf'], mockAction, {}),
        {wrapper}
      )

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

      // Verify all expected properties exist
      expect(result.current).toHaveProperty('data')
      expect(result.current).toHaveProperty('loading')
      expect(result.current).toHaveProperty('error')
      expect(result.current).toHaveProperty('hasNextPage')
      expect(result.current).toHaveProperty('fetchMore')
      expect(result.current).toHaveProperty('refetch')

      // Verify types
      expect(typeof result.current.loading).toBe('boolean')
      expect(typeof result.current.hasNextPage).toBe('boolean')
      expect(typeof result.current.fetchMore).toBe('function')
      expect(typeof result.current.refetch).toBe('function')
    })
  })

  describe('hasNextPage Logic', () => {
    it('sets hasNextPage based on pageInfo', async () => {
      const mockAction = vi.fn().mockResolvedValue({
        ok: true,
        data: {
          data: [{id: '1'}],
          pageInfo: {hasNextPage: true, endCursor: 'cursor1'},
        },
      })

      const {result} = renderHook(
        () => useShopActionInfiniteQuery(['test-hasnext'], mockAction, {}),
        {wrapper}
      )

      await waitFor(() => {
        expect(result.current.data).toEqual([{id: '1'}])
        expect(result.current.hasNextPage).toBe(true)
      })
    })

    it('returns false when no more pages', async () => {
      const mockAction = vi.fn().mockResolvedValue({
        ok: true,
        data: {
          data: [{id: '1'}],
          pageInfo: {hasNextPage: false, endCursor: null},
        },
      })

      const {result} = renderHook(
        () => useShopActionInfiniteQuery(['test-nonext'], mockAction, {}),
        {wrapper}
      )

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

      expect(result.current.hasNextPage).toBe(false)
    })
  })
})
