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

import {useNavigateWithTransition} from './useNavigateWithTransition'

// Mock react-router
const mockNavigate = vi.fn()
const mockLocation = {pathname: '/current'}

vi.mock('react-router', () => ({
  useNavigate: () => mockNavigate,
  useLocation: () => mockLocation,
  NavigateOptions: {},
}))

describe('useNavigateWithTransition', () => {
  let originalStartViewTransition: any

  beforeEach(() => {
    vi.clearAllMocks()
    originalStartViewTransition = document.startViewTransition
    mockLocation.pathname = '/current'
  })

  afterEach(() => {
    document.startViewTransition = originalStartViewTransition
    document.documentElement.removeAttribute('data-navigation-type')
  })

  describe('Without View Transitions API', () => {
    beforeEach(() => {
      // Remove startViewTransition to simulate unsupported browser
      ;(document as any).startViewTransition = undefined
    })

    it('navigates directly when view transitions not supported', () => {
      const {result} = renderHook(() => useNavigateWithTransition())

      act(() => {
        result.current('/new-route')
      })

      expect(mockNavigate).toHaveBeenCalledWith('/new-route', undefined)
    })

    it('navigates with options when view transitions not supported', () => {
      const {result} = renderHook(() => useNavigateWithTransition())

      const options = {replace: true, state: {from: 'test'}}

      act(() => {
        result.current('/new-route', options)
      })

      expect(mockNavigate).toHaveBeenCalledWith('/new-route', options)
    })

    it('handles delta navigation without view transitions', () => {
      const {result} = renderHook(() => useNavigateWithTransition())

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

      expect(mockNavigate).toHaveBeenCalledWith(-1)
    })
  })

  describe('With View Transitions API', () => {
    let mockTransition: any

    beforeEach(() => {
      mockTransition = {
        finished: Promise.resolve(),
        ready: Promise.resolve(),
        types: new Set<string>(),
        updateCallbackDone: Promise.resolve(),
        skipTransition: () => {},
      }

      // Mock startViewTransition
      document.startViewTransition = vi.fn((callback: () => void) => {
        callback()
        return mockTransition
      }) as any
    })

    it('uses view transition for path navigation', async () => {
      const {result} = renderHook(() => useNavigateWithTransition())

      await act(async () => {
        result.current('/new-route')
      })

      expect(document.startViewTransition).toHaveBeenCalled()
      expect(mockNavigate).toHaveBeenCalledWith('/new-route', {
        preventScrollReset: true,
        replace: false,
      })
    })

    it('uses replace navigation for same route', async () => {
      const {result} = renderHook(() => useNavigateWithTransition())

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

      expect(document.startViewTransition).toHaveBeenCalled()
      expect(mockNavigate).toHaveBeenCalledWith('/current', {
        preventScrollReset: true,
        replace: true,
      })
    })

    it('merges custom options with defaults', async () => {
      const {result} = renderHook(() => useNavigateWithTransition())

      const customOptions = {state: {from: 'test'}, replace: true}

      await act(async () => {
        result.current('/new-route', customOptions)
      })

      expect(mockNavigate).toHaveBeenCalledWith('/new-route', {
        preventScrollReset: true,
        replace: true,
        state: {from: 'test'},
      })
    })

    it('removes navigation type attribute after transition completes', async () => {
      const removeAttributeSpy = vi.spyOn(
        document.documentElement,
        'removeAttribute'
      )

      const {result} = renderHook(() => useNavigateWithTransition())

      await act(async () => {
        result.current('/new-route')
        await mockTransition.finished
      })

      expect(removeAttributeSpy).toHaveBeenCalledWith('data-navigation-type')
    })

    it('handles view transition errors gracefully', async () => {
      const consoleErrorSpy = vi
        .spyOn(console, 'error')
        .mockImplementation(() => {})

      const errorTransition = {
        finished: Promise.reject(new Error('Transition failed')),
        ready: Promise.resolve(),
        types: new Set<string>(),
        updateCallbackDone: Promise.resolve(),
        skipTransition: () => {},
      }

      document.startViewTransition = vi.fn((callback: () => void) => {
        callback()
        return errorTransition
      }) as any

      const {result} = renderHook(() => useNavigateWithTransition())

      await act(async () => {
        result.current('/new-route')
        try {
          await errorTransition.finished
        } catch {
          // Expected error
        }
      })

      expect(consoleErrorSpy).toHaveBeenCalledWith(
        'View transition error:',
        expect.any(Error)
      )

      consoleErrorSpy.mockRestore()
    })

    it('uses view transition for delta navigation', async () => {
      const {result} = renderHook(() => useNavigateWithTransition())

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

      expect(document.startViewTransition).toHaveBeenCalled()
      expect(mockNavigate).toHaveBeenCalledWith(-1)
    })

    it('removes attribute after delta navigation transition', async () => {
      const removeAttributeSpy = vi.spyOn(
        document.documentElement,
        'removeAttribute'
      )

      const {result} = renderHook(() => useNavigateWithTransition())

      await act(async () => {
        result.current(-2)
        await mockTransition.finished
      })

      expect(removeAttributeSpy).toHaveBeenCalledWith('data-navigation-type')
    })

    it('handles positive delta navigation', async () => {
      const {result} = renderHook(() => useNavigateWithTransition())

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

      expect(document.startViewTransition).toHaveBeenCalled()
      expect(mockNavigate).toHaveBeenCalledWith(1)
    })
  })

  describe('Edge Cases', () => {
    beforeEach(() => {
      document.startViewTransition = vi.fn((callback: () => void) => {
        callback()
        return {
          finished: Promise.resolve(),
          ready: Promise.resolve(),
          types: new Set<string>(),
          updateCallbackDone: Promise.resolve(),
          skipTransition: () => {},
        }
      }) as any
    })

    it('handles navigation to empty string', async () => {
      const {result} = renderHook(() => useNavigateWithTransition())

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

      expect(mockNavigate).toHaveBeenCalledWith('', {
        preventScrollReset: true,
        replace: false,
      })
    })

    it('handles navigation to root path', async () => {
      const {result} = renderHook(() => useNavigateWithTransition())

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

      expect(mockNavigate).toHaveBeenCalledWith('/', {
        preventScrollReset: true,
        replace: false,
      })
    })

    it('handles navigation with query parameters', async () => {
      const {result} = renderHook(() => useNavigateWithTransition())

      await act(async () => {
        result.current('/search?q=test&page=2')
      })

      expect(mockNavigate).toHaveBeenCalledWith('/search?q=test&page=2', {
        preventScrollReset: true,
        replace: false,
      })
    })

    it('handles navigation with hash', async () => {
      const {result} = renderHook(() => useNavigateWithTransition())

      await act(async () => {
        result.current('/page#section')
      })

      expect(mockNavigate).toHaveBeenCalledWith('/page#section', {
        preventScrollReset: true,
        replace: false,
      })
    })

    it('handles zero delta navigation', async () => {
      const {result} = renderHook(() => useNavigateWithTransition())

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

      expect(document.startViewTransition).toHaveBeenCalled()
      expect(mockNavigate).toHaveBeenCalledWith(0)
    })
  })

  describe('Multiple Navigations', () => {
    beforeEach(() => {
      const transitions: any[] = []

      document.startViewTransition = vi.fn((callback: () => void) => {
        callback()
        const transition = {
          finished: new Promise(resolve => setTimeout(resolve, 10)),
          ready: Promise.resolve(),
          types: new Set<string>(),
          updateCallbackDone: Promise.resolve(),
          skipTransition: () => {},
        }
        transitions.push(transition)
        return transition
      }) as any
    })

    it('handles multiple sequential navigations', async () => {
      const {result} = renderHook(() => useNavigateWithTransition())

      await act(async () => {
        result.current('/route1')
        result.current('/route2')
        result.current('/route3')
      })

      expect(document.startViewTransition).toHaveBeenCalledTimes(3)
      expect(mockNavigate).toHaveBeenCalledTimes(3)
      expect(mockNavigate).toHaveBeenNthCalledWith(
        1,
        '/route1',
        expect.any(Object)
      )
      expect(mockNavigate).toHaveBeenNthCalledWith(
        2,
        '/route2',
        expect.any(Object)
      )
      expect(mockNavigate).toHaveBeenNthCalledWith(
        3,
        '/route3',
        expect.any(Object)
      )
    })

    it('handles mixed path and delta navigations', async () => {
      const {result} = renderHook(() => useNavigateWithTransition())

      await act(async () => {
        result.current('/forward')
        result.current(-1)
        result.current('/another')
      })

      expect(document.startViewTransition).toHaveBeenCalledTimes(3)
      expect(mockNavigate).toHaveBeenNthCalledWith(
        1,
        '/forward',
        expect.any(Object)
      )
      expect(mockNavigate).toHaveBeenNthCalledWith(2, -1)
      expect(mockNavigate).toHaveBeenNthCalledWith(
        3,
        '/another',
        expect.any(Object)
      )
    })
  })
})
