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

import {useHandleAction} from '../../internal/useHandleAction'
import {useShopActions} from '../../internal/useShopActions'

import {useAsyncStorage} from './useAsyncStorage'

// Mock the internal hooks
vi.mock('../../internal/useShopActions', () => ({
  useShopActions: vi.fn(() => ({
    getPersistedItem: vi.fn(),
    setPersistedItem: vi.fn(),
    removePersistedItem: vi.fn(),
    getAllPersistedKeys: vi.fn(),
    clearPersistedItems: vi.fn(),
  })),
}))

vi.mock('../../internal/useHandleAction', () => ({
  useHandleAction: vi.fn((action: any) => action),
}))

describe('useAsyncStorage', () => {
  let mockActions: {[key: string]: ReturnType<typeof vi.fn>}

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

    // Set up mock actions with proper implementations
    mockActions = {
      getPersistedItem: vi.fn().mockResolvedValue(null),
      setPersistedItem: vi.fn().mockResolvedValue(undefined),
      removePersistedItem: vi.fn().mockResolvedValue(undefined),
      getAllPersistedKeys: vi.fn().mockResolvedValue(['key1', 'key2', 'key3']),
      clearPersistedItems: vi.fn().mockResolvedValue(undefined),
    }

    // Update the mock to return our mock actions
    ;(useShopActions as ReturnType<typeof vi.fn>).mockReturnValue(mockActions)

    // Make useHandleAction return the action directly
    ;(useHandleAction as ReturnType<typeof vi.fn>).mockImplementation(
      (action: any) => action
    )
  })

  describe('Hook Structure', () => {
    it('returns all expected methods', () => {
      const {result} = renderHook(() => useAsyncStorage())

      expect(result.current).toHaveProperty('getItem')
      expect(result.current).toHaveProperty('setItem')
      expect(result.current).toHaveProperty('removeItem')
      expect(result.current).toHaveProperty('getAllKeys')
      expect(result.current).toHaveProperty('clear')

      expect(typeof result.current.getItem).toBe('function')
      expect(typeof result.current.setItem).toBe('function')
      expect(typeof result.current.removeItem).toBe('function')
      expect(typeof result.current.getAllKeys).toBe('function')
      expect(typeof result.current.clear).toBe('function')
    })
  })

  describe('getItem', () => {
    it('calls getPersistedItem with correct parameters', async () => {
      mockActions.getPersistedItem.mockResolvedValue('test-value')
      const {result} = renderHook(() => useAsyncStorage())

      await act(async () => {
        const value = await result.current.getItem({key: 'test-key'})
        expect(value).toBe('test-value')
      })

      expect(mockActions.getPersistedItem).toHaveBeenCalledWith({
        key: 'test-key',
      })
      expect(mockActions.getPersistedItem).toHaveBeenCalledTimes(1)
    })

    it('returns null when item does not exist', async () => {
      mockActions.getPersistedItem.mockResolvedValue(null)
      const {result} = renderHook(() => useAsyncStorage())

      await act(async () => {
        const value = await result.current.getItem({key: 'non-existent'})
        expect(value).toBeNull()
      })
    })
  })

  describe('setItem', () => {
    it('calls setPersistedItem with correct parameters', async () => {
      const {result} = renderHook(() => useAsyncStorage())

      await act(async () => {
        await result.current.setItem({key: 'test-key', value: 'test-value'})
      })

      expect(mockActions.setPersistedItem).toHaveBeenCalledWith({
        key: 'test-key',
        value: 'test-value',
      })
      expect(mockActions.setPersistedItem).toHaveBeenCalledTimes(1)
    })

    it('handles empty string values', async () => {
      const {result} = renderHook(() => useAsyncStorage())

      await act(async () => {
        await result.current.setItem({key: 'test-key', value: ''})
      })

      expect(mockActions.setPersistedItem).toHaveBeenCalledWith({
        key: 'test-key',
        value: '',
      })
    })
  })

  describe('removeItem', () => {
    it('calls removePersistedItem with correct parameters', async () => {
      const {result} = renderHook(() => useAsyncStorage())

      await act(async () => {
        await result.current.removeItem({key: 'test-key'})
      })

      expect(mockActions.removePersistedItem).toHaveBeenCalledWith({
        key: 'test-key',
      })
      expect(mockActions.removePersistedItem).toHaveBeenCalledTimes(1)
    })
  })

  describe('getAllKeys', () => {
    it('returns all storage keys', async () => {
      const {result} = renderHook(() => useAsyncStorage())

      await act(async () => {
        const keys = await result.current.getAllKeys()
        expect(keys).toEqual(['key1', 'key2', 'key3'])
      })

      expect(mockActions.getAllPersistedKeys).toHaveBeenCalledWith()
      expect(mockActions.getAllPersistedKeys).toHaveBeenCalledTimes(1)
    })

    it('returns empty array when no keys exist', async () => {
      mockActions.getAllPersistedKeys.mockResolvedValue([])
      const {result} = renderHook(() => useAsyncStorage())

      await act(async () => {
        const keys = await result.current.getAllKeys()
        expect(keys).toEqual([])
      })
    })
  })

  describe('clear', () => {
    it('calls clearPersistedItems', async () => {
      const {result} = renderHook(() => useAsyncStorage())

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

      expect(mockActions.clearPersistedItems).toHaveBeenCalledWith()
      expect(mockActions.clearPersistedItems).toHaveBeenCalledTimes(1)
    })
  })

  describe('Error Handling', () => {
    it('propagates errors from getItem', async () => {
      const error = new Error('Storage error')
      mockActions.getPersistedItem.mockRejectedValue(error)
      const {result} = renderHook(() => useAsyncStorage())

      await act(async () => {
        await expect(result.current.getItem({key: 'test-key'})).rejects.toThrow(
          'Storage error'
        )
      })
    })

    it('propagates errors from setItem', async () => {
      const error = new Error('Write error')
      mockActions.setPersistedItem.mockRejectedValue(error)
      const {result} = renderHook(() => useAsyncStorage())

      await act(async () => {
        await expect(
          result.current.setItem({key: 'test-key', value: 'test-value'})
        ).rejects.toThrow('Write error')
      })
    })

    it('propagates errors from clear', async () => {
      const error = new Error('Clear error')
      mockActions.clearPersistedItems.mockRejectedValue(error)
      const {result} = renderHook(() => useAsyncStorage())

      await act(async () => {
        await expect(result.current.clear()).rejects.toThrow('Clear error')
      })
    })
  })

  describe('Stability', () => {
    it('maintains function reference stability across renders', () => {
      const {result, rerender} = renderHook(() => useAsyncStorage())

      const firstRender = {...result.current}
      rerender()
      const secondRender = {...result.current}

      // Functions should maintain reference equality
      expect(firstRender.getItem).toBe(secondRender.getItem)
      expect(firstRender.setItem).toBe(secondRender.setItem)
      expect(firstRender.removeItem).toBe(secondRender.removeItem)
      expect(firstRender.getAllKeys).toBe(secondRender.getAllKeys)
      expect(firstRender.clear).toBe(secondRender.clear)
    })
  })
})
