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

import {useViewTransitions} from './useViewTransitions'

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

vi.mock('react-router', () => ({
  useNavigationType: () => mockNavigationType(),
  useLocation: () => mockLocation,
}))

describe('useViewTransitions', () => {
  let originalStartViewTransition: any
  let addEventListenerSpy: ReturnType<typeof vi.spyOn>
  let removeEventListenerSpy: ReturnType<typeof vi.spyOn>
  let setAttributeSpy: ReturnType<typeof vi.spyOn>
  let removeAttributeSpy: ReturnType<typeof vi.spyOn>

  beforeEach(() => {
    vi.clearAllMocks()
    originalStartViewTransition = document.startViewTransition

    // Reset mock navigation type
    mockNavigationType.mockReturnValue('PUSH')

    // Setup spies
    addEventListenerSpy = vi.spyOn(window, 'addEventListener')
    removeEventListenerSpy = vi.spyOn(window, 'removeEventListener')
    setAttributeSpy = vi.spyOn(document.documentElement, 'setAttribute')
    removeAttributeSpy = vi.spyOn(document.documentElement, 'removeAttribute')
  })

  afterEach(() => {
    document.startViewTransition = originalStartViewTransition
    document.documentElement.removeAttribute('data-navigation-type')
    addEventListenerSpy.mockRestore()
    removeEventListenerSpy.mockRestore()
    setAttributeSpy.mockRestore()
    removeAttributeSpy.mockRestore()
  })

  describe('Event Listeners', () => {
    it('registers event listeners on mount', () => {
      renderHook(() => useViewTransitions())

      expect(addEventListenerSpy).toHaveBeenCalledWith(
        'androidbackpressed',
        expect.any(Function)
      )
      expect(addEventListenerSpy).toHaveBeenCalledWith(
        'popstate',
        expect.any(Function)
      )
    })

    it('removes event listeners on unmount', () => {
      const {unmount} = renderHook(() => useViewTransitions())

      unmount()

      expect(removeEventListenerSpy).toHaveBeenCalledWith(
        'popstate',
        expect.any(Function)
      )
      expect(removeEventListenerSpy).toHaveBeenCalledWith(
        'androidbackpressed',
        expect.any(Function)
      )
    })
  })

  describe('Navigation Type Attribute', () => {
    it('sets forward navigation type for PUSH', () => {
      mockNavigationType.mockReturnValue('PUSH')

      renderHook(() => useViewTransitions())

      expect(setAttributeSpy).toHaveBeenCalledWith(
        'data-navigation-type',
        'forward'
      )
    })

    it('sets backward navigation type for POP', () => {
      mockNavigationType.mockReturnValue('POP')

      renderHook(() => useViewTransitions())

      expect(setAttributeSpy).toHaveBeenCalledWith(
        'data-navigation-type',
        'backward'
      )
    })

    it('does not set attribute for REPLACE navigation', () => {
      mockNavigationType.mockReturnValue('REPLACE')

      renderHook(() => useViewTransitions())

      expect(setAttributeSpy).not.toHaveBeenCalledWith(
        'data-navigation-type',
        expect.any(String)
      )
    })

    it('does not override existing navigation type attribute', () => {
      // Set existing attribute
      document.documentElement.setAttribute('data-navigation-type', 'existing')
      setAttributeSpy.mockClear()

      mockNavigationType.mockReturnValue('PUSH')

      renderHook(() => useViewTransitions())

      // Should not set attribute again
      expect(setAttributeSpy).not.toHaveBeenCalledWith(
        'data-navigation-type',
        'forward'
      )
    })

    it('removes navigation type attribute on unmount', () => {
      const {unmount} = renderHook(() => useViewTransitions())

      unmount()

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

  describe('Android Back Press', () => {
    beforeEach(() => {
      // Mock startViewTransition
      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 android back press with view transition', async () => {
      renderHook(() => useViewTransitions())

      // Get the registered handler
      const androidBackHandler = addEventListenerSpy.mock.calls.find(
        call => call[0] === 'androidbackpressed'
      )?.[1] as EventListener

      expect(androidBackHandler).toBeDefined()

      // Trigger android back press
      await act(async () => {
        androidBackHandler(new Event('androidbackpressed'))
      })

      expect(document.startViewTransition).toHaveBeenCalled()
      expect(setAttributeSpy).toHaveBeenCalledWith(
        'data-navigation-type',
        'backward'
      )
    })

    it('removes attribute after android back transition completes', async () => {
      const transitionPromise = Promise.resolve()
      const mockTransition = {
        finished: transitionPromise,
        ready: Promise.resolve(),
        types: new Set<string>(),
        updateCallbackDone: Promise.resolve(),
        skipTransition: () => {},
      }

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

      renderHook(() => useViewTransitions())

      const androidBackHandler = addEventListenerSpy.mock.calls.find(
        call => call[0] === 'androidbackpressed'
      )?.[1] as EventListener

      await act(async () => {
        androidBackHandler(new Event('androidbackpressed'))
        await transitionPromise
      })

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

    it('handles view transition error on android back press', 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

      renderHook(() => useViewTransitions())

      const androidBackHandler = addEventListenerSpy.mock.calls.find(
        call => call[0] === 'androidbackpressed'
      )?.[1] as EventListener

      await act(async () => {
        androidBackHandler(new Event('androidbackpressed'))
        try {
          await errorTransition.finished
        } catch {
          // Expected error
        }
      })

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

      consoleErrorSpy.mockRestore()
    })

    it('handles android back press without view transition support', () => {
      // Remove startViewTransition
      ;(document as any).startViewTransition = undefined

      renderHook(() => useViewTransitions())

      const androidBackHandler = addEventListenerSpy.mock.calls.find(
        call => call[0] === 'androidbackpressed'
      )?.[1] as EventListener

      // Should not throw
      expect(() => {
        androidBackHandler(new Event('androidbackpressed'))
      }).not.toThrow()
    })
  })

  describe('Popstate Event', () => {
    it('sets none navigation type for iOS back gesture', () => {
      renderHook(() => useViewTransitions())

      const popstateHandler = addEventListenerSpy.mock.calls.find(
        call => call[0] === 'popstate'
      )?.[1] as EventListener

      expect(popstateHandler).toBeDefined()

      // Create popstate event with hasUAVisualTransition
      const popstateEvent = new PopStateEvent('popstate', {state: null})
      ;(popstateEvent as any).hasUAVisualTransition = true

      act(() => {
        popstateHandler(popstateEvent)
      })

      expect(setAttributeSpy).toHaveBeenCalledWith(
        'data-navigation-type',
        'none'
      )
    })

    it('does not set attribute for popstate without iOS gesture', () => {
      renderHook(() => useViewTransitions())

      const popstateHandler = addEventListenerSpy.mock.calls.find(
        call => call[0] === 'popstate'
      )?.[1] as EventListener

      setAttributeSpy.mockClear()

      // Regular popstate without hasUAVisualTransition
      const popstateEvent = new PopStateEvent('popstate', {state: null})

      act(() => {
        popstateHandler(popstateEvent)
      })

      expect(setAttributeSpy).not.toHaveBeenCalledWith(
        'data-navigation-type',
        'none'
      )
    })

    it('does not set none type after android back press', () => {
      document.startViewTransition = vi.fn((callback: () => void) => {
        callback()
        return {
          finished: Promise.resolve(),
          ready: Promise.resolve(),
          types: new Set<string>(),
          updateCallbackDone: Promise.resolve(),
          skipTransition: () => {},
        }
      }) as any

      renderHook(() => useViewTransitions())

      const androidBackHandler = addEventListenerSpy.mock.calls.find(
        call => call[0] === 'androidbackpressed'
      )?.[1] as EventListener

      const popstateHandler = addEventListenerSpy.mock.calls.find(
        call => call[0] === 'popstate'
      )?.[1] as EventListener

      // First trigger android back
      act(() => {
        androidBackHandler(new Event('androidbackpressed'))
      })

      setAttributeSpy.mockClear()

      // Then trigger popstate with iOS gesture
      const popstateEvent = new PopStateEvent('popstate', {state: null})
      ;(popstateEvent as any).hasUAVisualTransition = true

      act(() => {
        popstateHandler(popstateEvent)
      })

      // Should not set none type because android back was pressed
      expect(setAttributeSpy).not.toHaveBeenCalledWith(
        'data-navigation-type',
        'none'
      )
    })
  })

  describe('Navigation Type Changes', () => {
    it('updates attribute when navigation type changes', () => {
      mockNavigationType.mockReturnValue('PUSH')

      const {rerender} = renderHook(() => useViewTransitions())

      expect(setAttributeSpy).toHaveBeenCalledWith(
        'data-navigation-type',
        'forward'
      )

      setAttributeSpy.mockClear()

      // Change navigation type
      mockNavigationType.mockReturnValue('POP')
      rerender()

      expect(setAttributeSpy).toHaveBeenCalledWith(
        'data-navigation-type',
        'backward'
      )
    })

    it('removes and re-adds attribute on navigation type change', () => {
      mockNavigationType.mockReturnValue('PUSH')

      const {rerender} = renderHook(() => useViewTransitions())

      // Clear initial calls
      removeAttributeSpy.mockClear()
      setAttributeSpy.mockClear()

      // Change navigation type
      mockNavigationType.mockReturnValue('POP')
      rerender()

      // Should remove old attribute
      expect(removeAttributeSpy).toHaveBeenCalledWith('data-navigation-type')
      // And set new one
      expect(setAttributeSpy).toHaveBeenCalledWith(
        'data-navigation-type',
        'backward'
      )
    })
  })

  describe('Edge Cases', () => {
    it('handles multiple rapid location changes', () => {
      const {rerender} = renderHook(() => useViewTransitions())

      // Simulate rapid location changes
      for (let i = 0; i < 5; i++) {
        mockLocation.pathname = `/path-${i}`
        rerender()
      }

      // Should still have proper listeners registered
      const androidListeners = addEventListenerSpy.mock.calls.filter(
        call => call[0] === 'androidbackpressed'
      )
      const popstateListeners = addEventListenerSpy.mock.calls.filter(
        call => call[0] === 'popstate'
      )

      expect(androidListeners.length).toBeGreaterThan(0)
      expect(popstateListeners.length).toBeGreaterThan(0)
    })

    it('handles missing startViewTransition gracefully', () => {
      ;(document as any).startViewTransition = undefined

      // Should not throw
      expect(() => {
        renderHook(() => useViewTransitions())
      }).not.toThrow()
    })

    it('cleans up properly on unmount during transition', async () => {
      let resolveTransition: (() => void) | undefined
      const transitionPromise = new Promise<void>(resolve => {
        resolveTransition = resolve
      })

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

      const {unmount} = renderHook(() => useViewTransitions())

      const androidBackHandler = addEventListenerSpy.mock.calls.find(
        call => call[0] === 'androidbackpressed'
      )?.[1] as EventListener

      // Start transition
      act(() => {
        androidBackHandler(new Event('androidbackpressed'))
      })

      // Unmount before transition completes
      unmount()

      // Complete transition after unmount
      if (resolveTransition) {
        resolveTransition()
      }

      // Should have removed listeners
      expect(removeEventListenerSpy).toHaveBeenCalledWith(
        'androidbackpressed',
        expect.any(Function)
      )
    })
  })
})
