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

import {useShopActions} from '../../internal/useShopActions'
import {useShopActionsPaginatedDataFetching} from '../../internal/useShopActionsPaginatedDataFetching'
import {mockProducts} from '../../test-utils'

import {useProductSearch} from './useProductSearch'

// Mock lodash debounce to make tests predictable
vi.mock('lodash/debounce', () => ({
  default: (fn: any) => {
    const debounced = (...args: any[]) => fn(...args)
    debounced.cancel = vi.fn()
    return debounced
  },
}))

// Mock dependencies
vi.mock('../../internal/useShopActions', () => ({
  useShopActions: vi.fn(() => ({
    getProductSearch: vi.fn(),
  })),
}))

vi.mock('../../internal/useShopActionsPaginatedDataFetching', () => ({
  useShopActionsPaginatedDataFetching: vi.fn(),
}))

describe('useProductSearch', () => {
  let mockGetProductSearch: ReturnType<typeof vi.fn>
  let mockPaginatedDataFetching: any

  beforeEach(() => {
    vi.clearAllMocks()

    mockGetProductSearch = vi.fn()

    // Default mock for paginated data fetching
    mockPaginatedDataFetching = {
      data: mockProducts(3),
      loading: false,
      error: null,
      hasNextPage: false,
      fetchMore: vi.fn(),
      refetch: vi.fn(),
    }
    ;(useShopActions as any).mockReturnValue({
      getProductSearch: mockGetProductSearch,
    })
    ;(useShopActionsPaginatedDataFetching as any).mockReturnValue(
      mockPaginatedDataFetching
    )
  })

  describe('Basic Search', () => {
    it('returns products when query is provided', () => {
      const {result} = renderHook(() =>
        useProductSearch({
          query: 'test query',
        })
      )

      expect(result.current.products).toEqual(mockProducts(3))
      expect(result.current.loading).toBe(false)
      expect(result.current.error).toBe(null)
    })

    it('returns null products when query is empty', () => {
      const {result} = renderHook(() =>
        useProductSearch({
          query: '',
        })
      )

      expect(result.current.products).toBe(null)
    })

    it('returns null products when query contains only whitespace', () => {
      const {result} = renderHook(() =>
        useProductSearch({
          query: '   ',
        })
      )

      expect(result.current.products).toBe(null)
    })

    it('passes search parameters correctly', () => {
      renderHook(() =>
        useProductSearch({
          query: 'test',
          filters: {} as any, // Empty filters object for testing
          sortBy: 'RELEVANCE' as any,
          includeSensitive: true,
        })
      )

      expect(useShopActionsPaginatedDataFetching).toHaveBeenCalledWith(
        mockGetProductSearch,
        expect.objectContaining({
          query: 'test',
          filters: {},
          sortBy: 'RELEVANCE',
          includeSensitive: true,
        }),
        expect.objectContaining({
          skip: false,
          hook: 'useProductSearch',
        })
      )
    })

    it('handles skip parameter', () => {
      renderHook(() =>
        useProductSearch({
          query: 'test',
          skip: true,
        })
      )

      expect(useShopActionsPaginatedDataFetching).toHaveBeenCalledWith(
        expect.any(Function),
        expect.any(Object),
        expect.objectContaining({
          skip: true,
        })
      )
    })
  })

  describe('Debouncing', () => {
    it('debounces query changes', async () => {
      // Create a custom mock for debounce that we can control
      let debouncedCallback: any = null
      const mockDebounce = vi.fn((fn: any, _delay: number) => {
        const debounced = (...args: any[]) => {
          debouncedCallback = () => fn(...args)
        }
        debounced.cancel = vi.fn()
        return debounced
      })

      // Mock lodash/debounce with our custom implementation
      vi.doMock('lodash/debounce', () => ({
        default: mockDebounce,
      }))

      // Clear module cache and reimport with mocked debounce
      vi.resetModules()
      const {useProductSearch: mockedUseProductSearch} = await import(
        './useProductSearch'
      )

      const {result, rerender} = renderHook(
        ({query}) => mockedUseProductSearch({query}),
        {
          initialProps: {query: 'initial'},
        }
      )

      // Initially not typing
      expect(result.current.isTyping).toBe(false)

      // Change query
      rerender({query: 'updated'})

      // isTyping should be true immediately after query change
      expect(result.current.isTyping).toBe(true)

      // Simulate debounce callback execution
      await act(async () => {
        if (debouncedCallback) {
          debouncedCallback()
        }
      })

      // After debounce executes, isTyping should be false
      await waitFor(() => {
        expect(result.current.isTyping).toBe(false)
      })

      // Restore original mock
      vi.doMock('lodash/debounce', () => ({
        default: (fn: any) => {
          const debounced = (...args: any[]) => fn(...args)
          debounced.cancel = vi.fn()
          return debounced
        },
      }))
    })

    it('cancels debounced function on unmount', async () => {
      const cancelSpy = vi.fn()

      // Mock debounce with cancel spy
      vi.doMock('lodash/debounce', () => ({
        default: (fn: any) => {
          const debounced = (...args: any[]) => fn(...args)
          debounced.cancel = cancelSpy
          return debounced
        },
      }))

      // Clear module cache and reimport with mocked debounce
      vi.resetModules()
      const {useProductSearch: mockedUseProductSearch} = await import(
        './useProductSearch'
      )

      const {unmount} = renderHook(() =>
        mockedUseProductSearch({
          query: 'test',
        })
      )

      unmount()

      expect(cancelSpy).toHaveBeenCalled()

      // Restore original mock
      vi.doMock('lodash/debounce', () => ({
        default: (fn: any) => {
          const debounced = (...args: any[]) => fn(...args)
          debounced.cancel = vi.fn()
          return debounced
        },
      }))
    })
  })

  describe('isTyping State', () => {
    it('returns isTyping as false when debounced query matches current query', () => {
      const {result} = renderHook(() =>
        useProductSearch({
          query: 'test',
        })
      )

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

    it('returns isTyping as true when queries differ', async () => {
      // Create a custom mock for debounce that we can control
      let debouncedCallback: any = null
      const mockDebounce = vi.fn((fn: any, _delay: number) => {
        const debounced = (...args: any[]) => {
          debouncedCallback = () => fn(...args)
        }
        debounced.cancel = vi.fn()
        return debounced
      })

      // Mock lodash/debounce with our custom implementation
      vi.doMock('lodash/debounce', () => ({
        default: mockDebounce,
      }))

      // Clear module cache and reimport with mocked debounce
      vi.resetModules()
      const {useProductSearch: mockedUseProductSearch} = await import(
        './useProductSearch'
      )

      const {result, rerender} = renderHook(
        ({query}) => mockedUseProductSearch({query}),
        {
          initialProps: {query: 'initial'},
        }
      )

      // Initially not typing
      expect(result.current.isTyping).toBe(false)

      // Update query
      rerender({query: 'new query'})

      // Should be typing now
      expect(result.current.isTyping).toBe(true)

      // Simulate debounce callback execution
      await act(async () => {
        if (debouncedCallback) {
          debouncedCallback()
        }
      })

      // Should no longer be typing after debounce executes
      await waitFor(() => {
        expect(result.current.isTyping).toBe(false)
      })

      // Restore original mock
      vi.doMock('lodash/debounce', () => ({
        default: (fn: any) => {
          const debounced = (...args: any[]) => fn(...args)
          debounced.cancel = vi.fn()
          return debounced
        },
      }))
    })
  })

  describe('Pagination', () => {
    it('exposes pagination methods from underlying hook', () => {
      const mockFetchMore = vi.fn()
      const mockRefetch = vi.fn()

      ;(useShopActionsPaginatedDataFetching as any).mockReturnValue({
        data: mockProducts(3),
        loading: false,
        error: null,
        hasNextPage: true,
        fetchMore: mockFetchMore,
        refetch: mockRefetch,
      })

      const {result} = renderHook(() =>
        useProductSearch({
          query: 'test',
        })
      )

      expect(result.current.hasNextPage).toBe(true)
      expect(result.current.fetchMore).toBe(mockFetchMore)
      expect(result.current.refetch).toBe(mockRefetch)

      // Call the methods to ensure they're properly exposed
      result.current.fetchMore()
      result.current.refetch()

      expect(mockFetchMore).toHaveBeenCalledTimes(1)
      expect(mockRefetch).toHaveBeenCalledTimes(1)
    })
  })

  describe('Loading and Error States', () => {
    it('passes through loading state', () => {
      ;(useShopActionsPaginatedDataFetching as any).mockReturnValue({
        data: null,
        loading: true,
        error: null,
        hasNextPage: false,
        fetchMore: vi.fn(),
        refetch: vi.fn(),
      })

      const {result} = renderHook(() =>
        useProductSearch({
          query: 'test',
        })
      )

      expect(result.current.loading).toBe(true)
      expect(result.current.products).toBe(null)
    })

    it('passes through error state', () => {
      const testError = new Error('Search failed')

      ;(useShopActionsPaginatedDataFetching as any).mockReturnValue({
        data: null,
        loading: false,
        error: testError,
        hasNextPage: false,
        fetchMore: vi.fn(),
        refetch: vi.fn(),
      })

      const {result} = renderHook(() =>
        useProductSearch({
          query: 'test',
        })
      )

      expect(result.current.error).toBe(testError)
      expect(result.current.products).toBe(null)
    })
  })

  describe('Filters and Sorting', () => {
    it('passes filters to underlying hook', () => {
      const filters = {} as any // Mock filters object

      renderHook(() =>
        useProductSearch({
          query: 'test',
          filters,
        })
      )

      expect(useShopActionsPaginatedDataFetching).toHaveBeenCalledWith(
        expect.any(Function),
        expect.objectContaining({
          filters,
        }),
        expect.any(Object)
      )
    })

    it('passes sortBy to underlying hook', () => {
      renderHook(() =>
        useProductSearch({
          query: 'test',
          sortBy: 'RELEVANCE',
        })
      )

      expect(useShopActionsPaginatedDataFetching).toHaveBeenCalledWith(
        expect.any(Function),
        expect.objectContaining({
          sortBy: 'RELEVANCE',
        }),
        expect.any(Object)
      )
    })

    it('uses default value for includeSensitive', () => {
      renderHook(() =>
        useProductSearch({
          query: 'test',
        })
      )

      expect(useShopActionsPaginatedDataFetching).toHaveBeenCalledWith(
        expect.any(Function),
        expect.objectContaining({
          includeSensitive: false,
        }),
        expect.any(Object)
      )
    })
  })

  describe('Hook Options', () => {
    it('passes pagination parameters', () => {
      renderHook(
        () =>
          useProductSearch({
            query: 'test',
            first: 20,
          } as any) // Type assertion to avoid type error
      )

      expect(useShopActionsPaginatedDataFetching).toHaveBeenCalledWith(
        expect.any(Function),
        expect.objectContaining({
          first: 20,
        }),
        expect.any(Object)
      )
    })

    it('provides hook name for error tracking', () => {
      renderHook(() =>
        useProductSearch({
          query: 'test',
        })
      )

      expect(useShopActionsPaginatedDataFetching).toHaveBeenCalledWith(
        expect.any(Function),
        expect.any(Object),
        expect.objectContaining({
          hook: 'useProductSearch',
        })
      )
    })
  })
})
