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

import {useShopActionsDataFetching} from './useShopActionsDataFetching'

// Mock the error formatter
vi.mock('../utils/errors', () => ({
  formatError: vi.fn((_, error) => {
    if (error instanceof Error) return error
    return new Error(String(error))
  }),
  MiniError: class MiniError extends Error {
    constructor({message}: {message: string; hook?: string}) {
      super(message)
      this.name = 'MiniError'
    }
  },
}))

describe('useShopActionsDataFetching', () => {
  let mockAction: ReturnType<typeof vi.fn>

  beforeEach(() => {
    vi.clearAllMocks()
    mockAction = vi.fn()
  })

  describe('Initial Fetch', () => {
    it('fetches data on mount', async () => {
      const mockData = {data: {id: '1', name: 'Test'}}
      mockAction.mockResolvedValue({
        ok: true,
        data: mockData,
      })

      const {result} = renderHook(() =>
        useShopActionsDataFetching(mockAction, {}, {})
      )

      // Initially loading
      expect(result.current.loading).toBe(true)
      expect(result.current.data).toBeNull()
      expect(result.current.error).toBeNull()

      // Wait for fetch to complete
      await waitFor(() => {
        expect(result.current.loading).toBe(false)
      })

      expect(result.current.data).toEqual({id: '1', name: 'Test'})
      expect(result.current.error).toBeNull()
      expect(mockAction).toHaveBeenCalledWith({})
    })

    it('handles initial fetch error', async () => {
      const errorMessage = 'Network error'
      mockAction.mockResolvedValue({
        ok: false,
        error: new Error(errorMessage),
      })

      const {result} = renderHook(() =>
        useShopActionsDataFetching(mockAction, {}, {})
      )

      await waitFor(() => {
        expect(result.current.loading).toBe(false)
      })

      expect(result.current.data).toBeNull()
      expect(result.current.error).toBeInstanceOf(Error)
      expect(result.current.error?.message).toBe(errorMessage)
    })

    it('skips initial fetch when skip is true', async () => {
      mockAction.mockResolvedValue({
        ok: true,
        data: {data: 'test'},
      })

      const {result} = renderHook(() =>
        useShopActionsDataFetching(mockAction, {}, {skip: true})
      )

      // Should not be loading when skipped
      expect(result.current.loading).toBe(true) // Initially true regardless of skip
      expect(result.current.data).toBeNull()

      // Wait a bit to ensure no fetch happens
      await new Promise(resolve => setTimeout(resolve, 50))

      expect(mockAction).not.toHaveBeenCalled()
      expect(result.current.data).toBeNull()
    })
  })

  describe('Params Changes', () => {
    it('refetches when params change', async () => {
      const mockData1 = {data: {id: '1', value: 'first'}}
      const mockData2 = {data: {id: '2', value: 'second'}}

      mockAction
        .mockResolvedValueOnce({ok: true, data: mockData1})
        .mockResolvedValueOnce({ok: true, data: mockData2})

      const {result, rerender} = renderHook(
        ({params}) => useShopActionsDataFetching(mockAction, params, {}),
        {initialProps: {params: {id: '1'}}}
      )

      await waitFor(() => {
        expect(result.current.loading).toBe(false)
      })

      expect(result.current.data).toEqual({id: '1', value: 'first'})
      expect(mockAction).toHaveBeenCalledWith({id: '1'})

      // Change params
      rerender({params: {id: '2'}})

      await waitFor(() => {
        expect(result.current.data).toEqual({id: '2', value: 'second'})
      })

      expect(mockAction).toHaveBeenCalledWith({id: '2'})
      expect(mockAction).toHaveBeenCalledTimes(2)
    })

    it('does not refetch when params are structurally equal', async () => {
      const mockData = {data: {id: '1', name: 'Test'}}
      mockAction.mockResolvedValue({ok: true, data: mockData})

      const {result, rerender} = renderHook(
        ({params}) => useShopActionsDataFetching(mockAction, params, {}),
        {initialProps: {params: {id: '1', nested: {value: 'test'}}}}
      )

      await waitFor(() => {
        expect(result.current.loading).toBe(false)
      })

      expect(mockAction).toHaveBeenCalledTimes(1)

      // Rerender with structurally equal params (new object reference)
      rerender({params: {id: '1', nested: {value: 'test'}}})

      // Wait a bit to ensure no additional fetch
      await new Promise(resolve => setTimeout(resolve, 50))

      expect(mockAction).toHaveBeenCalledTimes(1)
    })
  })

  describe('Refetch', () => {
    it('refetches data without setting loading', async () => {
      const mockData1 = {data: {id: '1', value: 'initial'}}
      const mockData2 = {data: {id: '1', value: 'refetched'}}

      mockAction
        .mockResolvedValueOnce({ok: true, data: mockData1})
        .mockResolvedValueOnce({ok: true, data: mockData2})

      const {result} = renderHook(() =>
        useShopActionsDataFetching(mockAction, {}, {})
      )

      await waitFor(() => {
        expect(result.current.loading).toBe(false)
      })

      expect(result.current.data).toEqual({id: '1', value: 'initial'})

      // Track loading state during refetch
      let loadingDuringRefetch = false

      await act(async () => {
        const refetchPromise = result.current.refetch()

        // Check that loading is not set
        loadingDuringRefetch = result.current.loading

        await refetchPromise
      })

      expect(loadingDuringRefetch).toBe(false)
      expect(result.current.data).toEqual({id: '1', value: 'refetched'})
      expect(mockAction).toHaveBeenCalledTimes(2)
      expect(mockAction).toHaveBeenLastCalledWith({fetchPolicy: 'network-only'})
    })

    it('throws error on refetch failure', async () => {
      const mockData = {data: {id: '1', value: 'initial'}}
      const refetchError = new Error('Refetch failed')

      mockAction
        .mockResolvedValueOnce({ok: true, data: mockData})
        .mockResolvedValueOnce({ok: false, error: refetchError})

      const {result} = renderHook(() =>
        useShopActionsDataFetching(mockAction, {}, {})
      )

      await waitFor(() => {
        expect(result.current.loading).toBe(false)
      })

      expect(result.current.data).toEqual({id: '1', value: 'initial'})

      await act(async () => {
        await expect(result.current.refetch()).rejects.toThrow('Refetch failed')
      })

      // Data should remain unchanged on refetch error
      expect(result.current.data).toEqual({id: '1', value: 'initial'})
      expect(result.current.error).toBeInstanceOf(Error)
    })

    it('maintains data on refetch error', async () => {
      const initialData = {data: {id: '1', value: 'initial'}}

      mockAction
        .mockResolvedValueOnce({ok: true, data: initialData})
        .mockResolvedValueOnce({ok: false, error: new Error('Refetch error')})

      const {result} = renderHook(() =>
        useShopActionsDataFetching(mockAction, {}, {})
      )

      await waitFor(() => {
        expect(result.current.loading).toBe(false)
      })

      const originalData = result.current.data

      await act(async () => {
        try {
          await result.current.refetch()
        } catch {
          // Expected error
        }
      })

      // Data should not be reset on refetch error
      expect(result.current.data).toBe(originalData)
    })
  })

  describe('Validation', () => {
    it('validates data and sets error on validation failure', async () => {
      const mockData = {data: {id: '1', value: 'test'}}
      mockAction.mockResolvedValue({ok: true, data: mockData})

      const validator = vi.fn(data => {
        if (data.value === 'test') {
          throw new Error('Invalid value')
        }
      })

      const {result} = renderHook(() =>
        useShopActionsDataFetching(
          mockAction,
          {},
          {validator, hook: 'testHook'}
        )
      )

      await waitFor(() => {
        expect(result.current.loading).toBe(false)
      })

      // When validation fails and validation error is set, data is not returned
      expect(result.current.data).toBeNull()
      expect(result.current.error).toBeInstanceOf(Error)
      expect(result.current.error?.message).toBe('Invalid value')
      expect(validator).toHaveBeenCalledWith({id: '1', value: 'test'})
    })

    it('passes validation when no error is thrown', async () => {
      const mockData = {data: {id: '1', value: 'valid'}}
      mockAction.mockResolvedValue({ok: true, data: mockData})

      const validator = vi.fn(data => {
        // No error thrown - validation passes
        expect(data).toBeDefined()
      })

      const {result} = renderHook(() =>
        useShopActionsDataFetching(mockAction, {}, {validator})
      )

      await waitFor(() => {
        expect(result.current.loading).toBe(false)
      })

      expect(result.current.data).toEqual({id: '1', value: 'valid'})
      expect(result.current.error).toBeNull()
      expect(validator).toHaveBeenCalledWith({id: '1', value: 'valid'})
    })

    it('handles non-Error validation failures', async () => {
      const mockData = {data: {id: '1', value: 'test'}}
      mockAction.mockResolvedValue({ok: true, data: mockData})

      const validator = vi.fn(() => {
        // eslint-disable-next-line no-throw-literal
        throw 'String error' // Non-Error thrown
      })

      const {result} = renderHook(() =>
        useShopActionsDataFetching(
          mockAction,
          {},
          {validator, hook: 'testHook'}
        )
      )

      await waitFor(() => {
        expect(result.current.loading).toBe(false)
      })

      expect(result.current.error).toBeInstanceOf(Error)
      expect(result.current.error?.message).toBe('Validation failed')
    })
  })

  describe('Error Handling', () => {
    it('formats errors properly', async () => {
      const originalError = {
        code: 'TEST_ERROR',
        message: 'Test error message',
      }

      mockAction.mockResolvedValue({
        ok: false,
        error: originalError,
      })

      const {result} = renderHook(() =>
        useShopActionsDataFetching(mockAction, {}, {hook: 'testHook'})
      )

      await waitFor(() => {
        expect(result.current.loading).toBe(false)
      })

      expect(result.current.error).toBeDefined()
      expect(result.current.data).toBeNull()
    })

    it('resets error on successful fetch after error', async () => {
      const errorResponse = {ok: false as const, error: new Error('Error')}
      const successResponse = {ok: true as const, data: {data: 'success'}}

      mockAction
        .mockResolvedValueOnce(errorResponse)
        .mockResolvedValueOnce(successResponse)

      const {result, rerender} = renderHook(
        ({params}) => useShopActionsDataFetching(mockAction, params, {}),
        {initialProps: {params: {id: '1'}}}
      )

      await waitFor(() => {
        expect(result.current.error).toBeDefined()
      })

      // Change params to trigger new fetch
      rerender({params: {id: '2'}})

      await waitFor(() => {
        expect(result.current.error).toBeNull()
      })

      expect(result.current.data).toBe('success')
    })
  })

  describe('Fetch Policy', () => {
    it('includes fetchPolicy in action params', async () => {
      mockAction.mockResolvedValue({
        ok: true,
        data: {data: 'test'},
      })

      renderHook(() =>
        useShopActionsDataFetching(mockAction, {fetchPolicy: 'cache-first'}, {})
      )

      await waitFor(() => {
        expect(mockAction).toHaveBeenCalledWith({fetchPolicy: 'cache-first'})
      })
    })

    it('overrides fetchPolicy in refetch', async () => {
      mockAction.mockResolvedValue({
        ok: true,
        data: {data: 'test'},
      })

      const {result} = renderHook(() =>
        useShopActionsDataFetching(mockAction, {fetchPolicy: 'cache-first'}, {})
      )

      await waitFor(() => {
        expect(result.current.loading).toBe(false)
      })

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

      expect(mockAction).toHaveBeenLastCalledWith({
        fetchPolicy: 'network-only',
      })
    })
  })

  describe('Hook Lifecycle', () => {
    it('cleans up properly on unmount', async () => {
      mockAction.mockResolvedValue({
        ok: true,
        data: {data: 'test'},
      })

      const {unmount} = renderHook(() =>
        useShopActionsDataFetching(mockAction, {}, {})
      )

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

      unmount()

      // Ensure no additional calls after unmount
      expect(mockAction).toHaveBeenCalledTimes(1)
    })

    it('handles rapid mount/unmount cycles', async () => {
      let resolveFetch: ((value: any) => void) | undefined

      mockAction.mockImplementation(
        () =>
          new Promise(resolve => {
            resolveFetch = resolve
          })
      )

      const {unmount} = renderHook(() =>
        useShopActionsDataFetching(mockAction, {}, {})
      )

      // Unmount before fetch completes
      unmount()

      // Complete the fetch after unmount
      if (resolveFetch) {
        resolveFetch({ok: true, data: {data: 'test'}})
      }

      // No errors should occur
      expect(mockAction).toHaveBeenCalledTimes(1)
    })
  })
})
