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 {useGenerateUserToken} from './useGenerateUserToken'

// Mock the internal hooks
vi.mock('../../internal/useShopActions', () => ({
  useShopActions: vi.fn(),
}))

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

describe('useGenerateUserToken', () => {
  const mockToken = 'test-token-123'
  const mockUserState = 'VERIFIED'

  // Helper to create a future timestamp
  const getFutureTimestamp = (hoursFromNow: number) => {
    const date = new Date()
    date.setHours(date.getHours() + hoursFromNow)
    return date.toISOString()
  }

  // Helper to create an expired timestamp
  const getExpiredTimestamp = () => {
    const date = new Date()
    date.setHours(date.getHours() - 1)
    return date.toISOString()
  }

  const createMockResponse = (overrides = {}) => ({
    data: {
      token: mockToken,
      expiresAt: getFutureTimestamp(24), // 24 hours from now
      userState: mockUserState,
      ...overrides,
    },
    userErrors: [],
  })

  let mockGenerateUserToken: ReturnType<typeof vi.fn>

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

    mockGenerateUserToken = vi.fn()
    ;(useShopActions as ReturnType<typeof vi.fn>).mockReturnValue({
      generateUserToken: mockGenerateUserToken,
    })
    ;(useHandleAction as ReturnType<typeof vi.fn>).mockImplementation(
      (action: any) => action
    )
  })

  describe('Token Generation', () => {
    it('should generate a new token on first call', async () => {
      mockGenerateUserToken.mockResolvedValue(createMockResponse())

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

      let tokenResponse: any
      await act(async () => {
        tokenResponse = await result.current.generateUserToken()
      })

      expect(tokenResponse.data).toEqual({
        token: mockToken,
        expiresAt: expect.any(String),
        userState: mockUserState,
      })
      expect(mockGenerateUserToken).toHaveBeenCalledTimes(1)
    })

    it('should throw error when token generation fails', async () => {
      const error = new Error('Network error')
      mockGenerateUserToken.mockRejectedValue(error)

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

      await expect(result.current.generateUserToken()).rejects.toThrow(
        'Network error'
      )
      expect(mockGenerateUserToken).toHaveBeenCalledTimes(1)
    })

    it('should return response even when token is incomplete', async () => {
      const incompleteResponse = {
        data: {
          token: null,
          expiresAt: null,
          userState: null,
        },
        userErrors: [
          {code: 'INVALID_TOKEN', message: 'Failed to generate token'},
        ],
      }

      mockGenerateUserToken.mockResolvedValue(incompleteResponse)

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

      const response = await result.current.generateUserToken()

      expect(response).toEqual(incompleteResponse)
      expect(mockGenerateUserToken).toHaveBeenCalledTimes(1)
    })
  })

  describe('Token Caching', () => {
    it('should return cached token on subsequent calls', async () => {
      mockGenerateUserToken.mockResolvedValue(createMockResponse())

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

      // First call - should hit the API
      let firstResponse: any
      await act(async () => {
        firstResponse = await result.current.generateUserToken()
      })

      // Second call - should return cached token
      let secondResponse: any
      await act(async () => {
        secondResponse = await result.current.generateUserToken()
      })

      // Both should be the same reference (cached)
      expect(firstResponse).toBe(secondResponse)
      expect(secondResponse.data.token).toBe(mockToken)
      expect(mockGenerateUserToken).toHaveBeenCalledTimes(1) // Only called once
    })

    it('should request new token when cached token is expired', async () => {
      mockGenerateUserToken
        .mockResolvedValueOnce(
          createMockResponse({
            token: 'expired-token',
            expiresAt: getExpiredTimestamp(),
          })
        )
        .mockResolvedValueOnce(
          createMockResponse({
            token: 'new-token',
            expiresAt: getFutureTimestamp(24),
          })
        )

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

      // First call - gets expired token
      await act(async () => {
        await result.current.generateUserToken()
      })

      // Second call - should request new token since first is expired
      let secondResponse: any
      await act(async () => {
        secondResponse = await result.current.generateUserToken()
      })

      expect(secondResponse.data.token).toBe('new-token')
      expect(mockGenerateUserToken).toHaveBeenCalledTimes(2)
    })

    it('should request new token when cached token is within 5-minute buffer', async () => {
      // Token expires in 4 minutes (within buffer)
      mockGenerateUserToken
        .mockResolvedValueOnce(
          createMockResponse({
            token: 'almost-expired-token',
            expiresAt: getFutureTimestamp(0.066), // ~4 minutes
          })
        )
        .mockResolvedValueOnce(
          createMockResponse({
            token: 'fresh-token',
            expiresAt: getFutureTimestamp(24),
          })
        )

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

      // First call
      await act(async () => {
        await result.current.generateUserToken()
      })

      // Second call - should get new token due to buffer
      let secondResponse: any
      await act(async () => {
        secondResponse = await result.current.generateUserToken()
      })

      expect(secondResponse.data.token).toBe('fresh-token')
      expect(mockGenerateUserToken).toHaveBeenCalledTimes(2)
    })

    it('should use cached token when outside 5-minute buffer', async () => {
      // Token expires in 1 hour (well outside the 5-minute buffer)
      const mockResponse = createMockResponse({
        token: 'valid-token',
        expiresAt: getFutureTimestamp(1), // 1 hour from now
      })

      mockGenerateUserToken.mockResolvedValue(mockResponse)

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

      // First call
      const firstResponse = await result.current.generateUserToken()

      // Verify the response has the expected structure
      expect(firstResponse.data.token).toBe('valid-token')
      expect(firstResponse.data.expiresAt).toBeDefined()

      // Second call - should use cached token
      const secondResponse = await result.current.generateUserToken()

      // Both responses should be the same cached object
      expect(firstResponse).toBe(secondResponse)
      expect(mockGenerateUserToken).toHaveBeenCalledTimes(1)
    })
  })

  describe('Race Condition Prevention', () => {
    it('should handle concurrent requests by returning same promise', async () => {
      // Create a delayed promise to simulate an API call
      const mockResponse = createMockResponse()
      mockGenerateUserToken.mockImplementation(
        () =>
          new Promise(resolve => {
            setTimeout(() => resolve(mockResponse), 10)
          })
      )

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

      // Make two concurrent calls
      const promise1 = result.current.generateUserToken()
      const promise2 = result.current.generateUserToken()

      // Only one API call should be made
      expect(mockGenerateUserToken).toHaveBeenCalledTimes(1)

      // Wait for both promises
      const [response1, response2] = await Promise.all([promise1, promise2])

      // Both should return the same response
      expect(response1).toEqual(mockResponse)
      expect(response2).toEqual(mockResponse)
      expect(response1).toBe(response2)
    })

    it('should handle multiple rapid sequential calls correctly', async () => {
      mockGenerateUserToken.mockResolvedValue(createMockResponse())

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

      // Make multiple rapid calls
      const promises = Array.from({length: 5}, () =>
        result.current.generateUserToken()
      )

      // All should resolve to the same token
      const responses = await Promise.all(promises)

      responses.forEach(response => {
        expect(response.data.token).toBe(mockToken)
        // All should be the same object reference
        expect(response).toBe(responses[0])
      })

      // Should only have made one API call
      expect(mockGenerateUserToken).toHaveBeenCalledTimes(1)
    })
  })

  describe('Error Handling', () => {
    it('should clear cache on error and retry', async () => {
      mockGenerateUserToken
        .mockRejectedValueOnce(new Error('Network error'))
        .mockResolvedValueOnce(createMockResponse())

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

      // First call - should fail
      await expect(result.current.generateUserToken()).rejects.toThrow(
        'Network error'
      )

      // Second call - should succeed with new request
      let response: any
      await act(async () => {
        response = await result.current.generateUserToken()
      })

      expect(response.data.token).toBe(mockToken)
      expect(mockGenerateUserToken).toHaveBeenCalledTimes(2)
    })

    it('should clear pending request on error', async () => {
      let rejectPromise: any
      const promise = new Promise((_resolve, reject) => {
        rejectPromise = reject
      })

      mockGenerateUserToken.mockReturnValue(promise)

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

      // Make concurrent calls that will fail
      const promise1 = result.current.generateUserToken()
      const promise2 = result.current.generateUserToken()

      // Reject the promise
      rejectPromise(new Error('Network error'))

      // Both should fail with same error
      await expect(promise1).rejects.toThrow('Network error')
      await expect(promise2).rejects.toThrow('Network error')

      // Reset mock for next call
      mockGenerateUserToken.mockResolvedValue(createMockResponse())

      // Next call should make a fresh request
      let response: any
      await act(async () => {
        response = await result.current.generateUserToken()
      })

      expect(response.data.token).toBe(mockToken)
      expect(mockGenerateUserToken).toHaveBeenCalledTimes(2) // First failed call + retry
    })
  })
})
