import { renderHook, act, waitFor } from '@testing-library/react'
import { useTableURL, UseTableURLConfig } from './useTableURL'
import { SortState, FilterState } from '../types'

// Mock Next.js navigation hooks
const mockReplace = jest.fn()
const mockSearchParams = new URLSearchParams()
const mockPathname = '/test-path'

jest.mock('next/navigation', () => ({
  useSearchParams: jest.fn(() => mockSearchParams),
  useRouter: jest.fn(() => ({
    replace: mockReplace,
  })),
  usePathname: jest.fn(() => mockPathname),
}))

describe('useTableURL', () => {
  beforeEach(() => {
    jest.clearAllMocks()
    jest.useFakeTimers()

    // Reset search params
    mockSearchParams.forEach((_, key) => {
      mockSearchParams.delete(key)
    })
  })

  afterEach(() => {
    jest.runOnlyPendingTimers()
    jest.useRealTimers()
  })

  describe('initialization', () => {
    it('should return empty state when persistToUrl is false', () => {
      const { result } = renderHook(() =>
        useTableURL({
          tableId: 'test',
          persistToUrl: false,
        })
      )

      const state = result.current.getURLState()

      expect(state).toEqual({
        sortBy: null,
        sortDirection: 'asc',
        filters: {},
        page: 1,
        search: '',
      })
    })

    it('should return empty state when URL has no params', () => {
      const { result } = renderHook(() =>
        useTableURL({
          tableId: 'test',
          persistToUrl: true,
        })
      )

      const state = result.current.getURLState()

      expect(state).toEqual({
        sortBy: null,
        sortDirection: 'asc',
        filters: {},
        page: 1,
        search: '',
      })
    })

    it('should parse existing URL params on mount', () => {
      mockSearchParams.set('test_sortBy', 'name')
      mockSearchParams.set('test_sortDir', 'desc')
      mockSearchParams.set('test_page', '3')
      mockSearchParams.set('test_search', 'acme')

      const { result } = renderHook(() =>
        useTableURL({
          tableId: 'test',
          persistToUrl: true,
        })
      )

      const state = result.current.getURLState()

      expect(state).toEqual({
        sortBy: 'name',
        sortDirection: 'desc',
        filters: {},
        page: 3,
        search: 'acme',
      })
    })

    it('should parse filter params from URL', () => {
      mockSearchParams.set('test_filter_status', 'active')
      mockSearchParams.set('test_filter_tags', JSON.stringify(['tag1', 'tag2']))

      const { result } = renderHook(() =>
        useTableURL({
          tableId: 'test',
          persistToUrl: true,
        })
      )

      const state = result.current.getURLState()

      expect(state.filters).toEqual({
        status: 'active',
        tags: ['tag1', 'tag2'],
      })
    })

    it('should handle invalid page number gracefully', () => {
      mockSearchParams.set('test_page', 'invalid')

      const { result } = renderHook(() =>
        useTableURL({
          tableId: 'test',
          persistToUrl: true,
        })
      )

      const state = result.current.getURLState()

      expect(state.page).toBe(1)
    })

    it('should handle negative page number', () => {
      mockSearchParams.set('test_page', '-5')

      const { result } = renderHook(() =>
        useTableURL({
          tableId: 'test',
          persistToUrl: true,
        })
      )

      const state = result.current.getURLState()

      expect(state.page).toBe(1)
    })

    it('should namespace params by tableId', () => {
      mockSearchParams.set('other_sortBy', 'otherColumn')
      mockSearchParams.set('test_sortBy', 'testColumn')

      const { result } = renderHook(() =>
        useTableURL({
          tableId: 'test',
          persistToUrl: true,
        })
      )

      const state = result.current.getURLState()

      expect(state.sortBy).toBe('testColumn')
    })
  })

  describe('setSortToURL', () => {
    it('should update sort params in URL', async () => {
      const { result } = renderHook(() =>
        useTableURL({
          tableId: 'test',
          persistToUrl: true,
          debounceMs: 100,
        })
      )

      act(() => {
        result.current.setSortToURL({
          sortBy: 'name',
          sortDirection: 'asc',
        })
      })

      // Fast-forward debounce
      act(() => {
        jest.advanceTimersByTime(100)
      })

      await waitFor(() => {
        expect(mockReplace).toHaveBeenCalledWith(
          expect.stringContaining('test_sortBy=name'),
          { scroll: false }
        )
        expect(mockReplace).toHaveBeenCalledWith(
          expect.stringContaining('test_sortDir=asc'),
          { scroll: false }
        )
      })
    })

    it('should reset page to 1 when sorting changes', async () => {
      mockSearchParams.set('test_page', '5')

      const { result } = renderHook(() =>
        useTableURL({
          tableId: 'test',
          persistToUrl: true,
          debounceMs: 100,
        })
      )

      act(() => {
        result.current.setSortToURL({
          sortBy: 'name',
          sortDirection: 'asc',
        })
      })

      act(() => {
        jest.advanceTimersByTime(100)
      })

      await waitFor(() => {
        expect(mockReplace).toHaveBeenCalledWith(
          expect.stringContaining('test_page=1'),
          { scroll: false }
        )
      })
    })

    it('should remove sort params when sortBy is null', async () => {
      mockSearchParams.set('test_sortBy', 'name')
      mockSearchParams.set('test_sortDir', 'asc')

      const { result } = renderHook(() =>
        useTableURL({
          tableId: 'test',
          persistToUrl: true,
          debounceMs: 100,
        })
      )

      act(() => {
        result.current.setSortToURL({
          sortBy: null,
          sortDirection: 'asc',
        })
      })

      act(() => {
        jest.advanceTimersByTime(100)
      })

      await waitFor(() => {
        const lastCall = mockReplace.mock.calls[mockReplace.mock.calls.length - 1]
        const url = lastCall?.[0] || ''
        expect(url).not.toContain('test_sortBy')
        expect(url).not.toContain('test_sortDir')
      })
    })

    it('should not update URL when persistToUrl is false', () => {
      const { result } = renderHook(() =>
        useTableURL({
          tableId: 'test',
          persistToUrl: false,
        })
      )

      act(() => {
        result.current.setSortToURL({
          sortBy: 'name',
          sortDirection: 'asc',
        })
      })

      act(() => {
        jest.advanceTimersByTime(1000)
      })

      expect(mockReplace).not.toHaveBeenCalled()
    })

    it('should debounce rapid sort changes', async () => {
      const { result } = renderHook(() =>
        useTableURL({
          tableId: 'test',
          persistToUrl: true,
          debounceMs: 300,
        })
      )

      // Make multiple rapid changes
      act(() => {
        result.current.setSortToURL({ sortBy: 'name', sortDirection: 'asc' })
      })

      act(() => {
        jest.advanceTimersByTime(100)
      })

      act(() => {
        result.current.setSortToURL({ sortBy: 'email', sortDirection: 'desc' })
      })

      act(() => {
        jest.advanceTimersByTime(100)
      })

      act(() => {
        result.current.setSortToURL({ sortBy: 'date', sortDirection: 'asc' })
      })

      // Should not have called replace yet
      expect(mockReplace).not.toHaveBeenCalled()

      // Fast-forward past debounce
      act(() => {
        jest.advanceTimersByTime(300)
      })

      // Should only update once with the final value
      await waitFor(() => {
        expect(mockReplace).toHaveBeenCalledTimes(1)
        expect(mockReplace).toHaveBeenCalledWith(
          expect.stringContaining('test_sortBy=date'),
          { scroll: false }
        )
      })
    })
  })

  describe('setFiltersToURL', () => {
    it('should update filter params in URL', async () => {
      const { result } = renderHook(() =>
        useTableURL({
          tableId: 'test',
          persistToUrl: true,
          debounceMs: 100,
        })
      )

      act(() => {
        result.current.setFiltersToURL({
          status: 'active',
          category: 'tech',
        })
      })

      act(() => {
        jest.advanceTimersByTime(100)
      })

      await waitFor(() => {
        expect(mockReplace).toHaveBeenCalledWith(
          expect.stringContaining('test_filter_status=active'),
          { scroll: false }
        )
        expect(mockReplace).toHaveBeenCalledWith(
          expect.stringContaining('test_filter_category=tech'),
          { scroll: false }
        )
      })
    })

    it('should encode complex filter values as JSON', async () => {
      const { result } = renderHook(() =>
        useTableURL({
          tableId: 'test',
          persistToUrl: true,
          debounceMs: 100,
        })
      )

      act(() => {
        result.current.setFiltersToURL({
          tags: ['tag1', 'tag2'],
          range: { min: 0, max: 100 },
        })
      })

      act(() => {
        jest.advanceTimersByTime(100)
      })

      await waitFor(() => {
        const lastCall = mockReplace.mock.calls[mockReplace.mock.calls.length - 1]
        const url = lastCall?.[0] || ''
        expect(url).toContain('test_filter_tags')
        expect(url).toContain('test_filter_range')
      })
    })

    it('should remove filter params when filter value is empty', async () => {
      mockSearchParams.set('test_filter_status', 'active')

      const { result } = renderHook(() =>
        useTableURL({
          tableId: 'test',
          persistToUrl: true,
          debounceMs: 100,
        })
      )

      act(() => {
        result.current.setFiltersToURL({
          status: '',
        })
      })

      act(() => {
        jest.advanceTimersByTime(100)
      })

      await waitFor(() => {
        const lastCall = mockReplace.mock.calls[mockReplace.mock.calls.length - 1]
        const url = lastCall?.[0] || ''
        expect(url).not.toContain('test_filter_status')
      })
    })

    it('should reset page to 1 when filters change', async () => {
      mockSearchParams.set('test_page', '5')

      const { result } = renderHook(() =>
        useTableURL({
          tableId: 'test',
          persistToUrl: true,
          debounceMs: 100,
        })
      )

      act(() => {
        result.current.setFiltersToURL({
          status: 'active',
        })
      })

      act(() => {
        jest.advanceTimersByTime(100)
      })

      await waitFor(() => {
        expect(mockReplace).toHaveBeenCalledWith(
          expect.stringContaining('test_page=1'),
          { scroll: false }
        )
      })
    })

    it('should clear old filters when setting new ones', async () => {
      mockSearchParams.set('test_filter_oldFilter', 'oldValue')

      const { result } = renderHook(() =>
        useTableURL({
          tableId: 'test',
          persistToUrl: true,
          debounceMs: 100,
        })
      )

      act(() => {
        result.current.setFiltersToURL({
          newFilter: 'newValue',
        })
      })

      act(() => {
        jest.advanceTimersByTime(100)
      })

      await waitFor(() => {
        const lastCall = mockReplace.mock.calls[mockReplace.mock.calls.length - 1]
        const url = lastCall?.[0] || ''
        expect(url).not.toContain('oldFilter')
        expect(url).toContain('newFilter')
      })
    })
  })

  describe('setPageToURL', () => {
    it('should update page param in URL', async () => {
      const { result } = renderHook(() =>
        useTableURL({
          tableId: 'test',
          persistToUrl: true,
          debounceMs: 100,
        })
      )

      act(() => {
        result.current.setPageToURL(5)
      })

      act(() => {
        jest.advanceTimersByTime(100)
      })

      await waitFor(() => {
        expect(mockReplace).toHaveBeenCalledWith(
          expect.stringContaining('test_page=5'),
          { scroll: false }
        )
      })
    })

    it('should remove page param when page is 1', async () => {
      mockSearchParams.set('test_page', '5')

      const { result } = renderHook(() =>
        useTableURL({
          tableId: 'test',
          persistToUrl: true,
          debounceMs: 100,
        })
      )

      act(() => {
        result.current.setPageToURL(1)
      })

      act(() => {
        jest.advanceTimersByTime(100)
      })

      await waitFor(() => {
        const lastCall = mockReplace.mock.calls[mockReplace.mock.calls.length - 1]
        const url = lastCall?.[0] || ''
        expect(url).not.toContain('test_page')
      })
    })
  })

  describe('setSearchToURL', () => {
    it('should update search param in URL', async () => {
      const { result } = renderHook(() =>
        useTableURL({
          tableId: 'test',
          persistToUrl: true,
          debounceMs: 100,
        })
      )

      act(() => {
        result.current.setSearchToURL('acme corp')
      })

      act(() => {
        jest.advanceTimersByTime(100)
      })

      await waitFor(() => {
        expect(mockReplace).toHaveBeenCalledWith(
          expect.stringContaining('test_search=acme+corp'),
          { scroll: false }
        )
      })
    })

    it('should remove search param when search is empty', async () => {
      mockSearchParams.set('test_search', 'acme')

      const { result } = renderHook(() =>
        useTableURL({
          tableId: 'test',
          persistToUrl: true,
          debounceMs: 100,
        })
      )

      act(() => {
        result.current.setSearchToURL('')
      })

      act(() => {
        jest.advanceTimersByTime(100)
      })

      await waitFor(() => {
        const lastCall = mockReplace.mock.calls[mockReplace.mock.calls.length - 1]
        const url = lastCall?.[0] || ''
        expect(url).not.toContain('test_search')
      })
    })

    it('should reset page to 1 when search changes', async () => {
      mockSearchParams.set('test_page', '5')

      const { result } = renderHook(() =>
        useTableURL({
          tableId: 'test',
          persistToUrl: true,
          debounceMs: 100,
        })
      )

      act(() => {
        result.current.setSearchToURL('acme')
      })

      act(() => {
        jest.advanceTimersByTime(100)
      })

      await waitFor(() => {
        expect(mockReplace).toHaveBeenCalledWith(
          expect.stringContaining('test_page=1'),
          { scroll: false }
        )
      })
    })

    it('should debounce rapid search changes', async () => {
      const { result } = renderHook(() =>
        useTableURL({
          tableId: 'test',
          persistToUrl: true,
          debounceMs: 300,
        })
      )

      // Simulate typing
      act(() => {
        result.current.setSearchToURL('a')
      })

      act(() => {
        jest.advanceTimersByTime(100)
      })

      act(() => {
        result.current.setSearchToURL('ac')
      })

      act(() => {
        jest.advanceTimersByTime(100)
      })

      act(() => {
        result.current.setSearchToURL('acme')
      })

      // Should not have called replace yet
      expect(mockReplace).not.toHaveBeenCalled()

      // Fast-forward past debounce
      act(() => {
        jest.advanceTimersByTime(300)
      })

      // Should only update once with the final value
      await waitFor(() => {
        expect(mockReplace).toHaveBeenCalledTimes(1)
        expect(mockReplace).toHaveBeenCalledWith(
          expect.stringContaining('test_search=acme'),
          { scroll: false }
        )
      })
    })
  })

  describe('clearURLState', () => {
    it('should remove all table-related params from URL', async () => {
      mockSearchParams.set('test_sortBy', 'name')
      mockSearchParams.set('test_page', '5')
      mockSearchParams.set('test_search', 'acme')
      mockSearchParams.set('test_filter_status', 'active')
      mockSearchParams.set('other_param', 'keep-this')

      const { result } = renderHook(() =>
        useTableURL({
          tableId: 'test',
          persistToUrl: true,
        })
      )

      act(() => {
        result.current.clearURLState()
      })

      expect(mockReplace).toHaveBeenCalledTimes(1)
      const url = mockReplace.mock.calls[0][0]

      expect(url).not.toContain('test_sortBy')
      expect(url).not.toContain('test_page')
      expect(url).not.toContain('test_search')
      expect(url).not.toContain('test_filter_status')
      expect(url).toContain('other_param=keep-this')
    })

    it('should navigate to pathname without params when all removed', () => {
      mockSearchParams.set('test_sortBy', 'name')

      const { result } = renderHook(() =>
        useTableURL({
          tableId: 'test',
          persistToUrl: true,
        })
      )

      act(() => {
        result.current.clearURLState()
      })

      expect(mockReplace).toHaveBeenCalledWith('/test-path', { scroll: false })
    })
  })

  describe('hasURLState', () => {
    it('should return false when persistToUrl is false', () => {
      const { result } = renderHook(() =>
        useTableURL({
          tableId: 'test',
          persistToUrl: false,
        })
      )

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

    it('should return false when URL has no table params', () => {
      const { result } = renderHook(() =>
        useTableURL({
          tableId: 'test',
          persistToUrl: true,
        })
      )

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

    it('should return true when URL has table params', () => {
      mockSearchParams.set('test_sortBy', 'name')

      const { result } = renderHook(() =>
        useTableURL({
          tableId: 'test',
          persistToUrl: true,
        })
      )

      expect(result.current.hasURLState()).toBe(true)
    })

    it('should not detect params from other tables', () => {
      mockSearchParams.set('other_sortBy', 'name')

      const { result } = renderHook(() =>
        useTableURL({
          tableId: 'test',
          persistToUrl: true,
        })
      )

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

  describe('edge cases', () => {
    it('should handle special characters in filter values', async () => {
      const { result } = renderHook(() =>
        useTableURL({
          tableId: 'test',
          persistToUrl: true,
          debounceMs: 100,
        })
      )

      act(() => {
        result.current.setFiltersToURL({
          name: 'Acme & Co.',
        })
      })

      act(() => {
        jest.advanceTimersByTime(100)
      })

      await waitFor(() => {
        expect(mockReplace).toHaveBeenCalled()
      })
    })

    it('should handle URL encoding for search terms', async () => {
      const { result } = renderHook(() =>
        useTableURL({
          tableId: 'test',
          persistToUrl: true,
          debounceMs: 100,
        })
      )

      act(() => {
        result.current.setSearchToURL('test@example.com')
      })

      act(() => {
        jest.advanceTimersByTime(100)
      })

      await waitFor(() => {
        const lastCall = mockReplace.mock.calls[mockReplace.mock.calls.length - 1]
        const url = lastCall?.[0] || ''
        expect(url).toContain('test_search')
      })
    })

    it('should not update URL when params have not changed', async () => {
      mockSearchParams.set('test_sortBy', 'name')
      mockSearchParams.set('test_sortDir', 'asc')

      const { result } = renderHook(() =>
        useTableURL({
          tableId: 'test',
          persistToUrl: true,
          debounceMs: 100,
        })
      )

      // Set the same values
      act(() => {
        result.current.setSortToURL({
          sortBy: 'name',
          sortDirection: 'asc',
        })
      })

      act(() => {
        jest.advanceTimersByTime(100)
      })

      // Should not call replace when params are identical
      // Note: This test may need adjustment based on implementation details
      await waitFor(() => {
        // The implementation may still call replace, but ideally shouldn't
        // This is testing the optimization logic
      })
    })

    it('should cleanup debounce timer on unmount', () => {
      const { result, unmount } = renderHook(() =>
        useTableURL({
          tableId: 'test',
          persistToUrl: true,
          debounceMs: 300,
        })
      )

      act(() => {
        result.current.setSearchToURL('test')
      })

      // Unmount before debounce completes
      unmount()

      // Fast-forward timers
      act(() => {
        jest.advanceTimersByTime(300)
      })

      // Should not have called replace after unmount
      expect(mockReplace).not.toHaveBeenCalled()
    })
  })

  describe('custom debounce timing', () => {
    it('should respect custom debounceMs value', async () => {
      const { result } = renderHook(() =>
        useTableURL({
          tableId: 'test',
          persistToUrl: true,
          debounceMs: 500,
        })
      )

      act(() => {
        result.current.setSearchToURL('test')
      })

      act(() => {
        jest.advanceTimersByTime(400)
      })

      // Should not have updated yet
      expect(mockReplace).not.toHaveBeenCalled()

      act(() => {
        jest.advanceTimersByTime(100)
      })

      // Now it should have updated
      await waitFor(() => {
        expect(mockReplace).toHaveBeenCalled()
      })
    })

    it('should use default debounce of 300ms', async () => {
      const { result } = renderHook(() =>
        useTableURL({
          tableId: 'test',
          persistToUrl: true,
          // debounceMs not specified, should default to 300
        })
      )

      act(() => {
        result.current.setSearchToURL('test')
      })

      act(() => {
        jest.advanceTimersByTime(299)
      })

      expect(mockReplace).not.toHaveBeenCalled()

      act(() => {
        jest.advanceTimersByTime(1)
      })

      await waitFor(() => {
        expect(mockReplace).toHaveBeenCalled()
      })
    })
  })
})
