import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
import { withTimeout, SERVICE_TIMEOUTS, ServiceTimeoutError } from '../index'

describe('withTimeout', () => {
  beforeEach(() => {
    vi.useFakeTimers()
  })

  afterEach(() => {
    vi.useRealTimers()
  })

  describe('successful completion', () => {
    it('resolves when operation completes before timeout', async () => {
      const result = await withTimeout(
        Promise.resolve('success'),
        1000,
        'TestService'
      )

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

    it('resolves with complex return types', async () => {
      const expected = { id: '123', data: [1, 2, 3] }

      const result = await withTimeout(
        Promise.resolve(expected),
        1000,
        'TestService'
      )

      expect(result).toEqual(expected)
    })

    it('clears timeout handle on successful completion', async () => {
      const clearTimeoutSpy = vi.spyOn(global, 'clearTimeout')

      const promise = withTimeout(Promise.resolve('ok'), 1000, 'Test')

      // Resolve the promise
      await promise

      expect(clearTimeoutSpy).toHaveBeenCalled()
      clearTimeoutSpy.mockRestore()
    })
  })

  describe('timeout behavior', () => {
    it('throws ServiceTimeoutError when timeout expires', async () => {
      const slowPromise = new Promise(resolve => setTimeout(() => resolve('late'), 5000))
      const timeoutPromise = withTimeout(slowPromise, 1000, 'TestService')

      vi.advanceTimersByTime(1001)

      await expect(timeoutPromise).rejects.toThrow(ServiceTimeoutError)
    })

    it('includes service name in error', async () => {
      const slowPromise = new Promise(resolve => setTimeout(() => resolve('late'), 5000))
      const timeoutPromise = withTimeout(slowPromise, 1000, 'WorkOS')

      vi.advanceTimersByTime(1001)

      try {
        await timeoutPromise
      } catch (error) {
        expect(error).toBeInstanceOf(ServiceTimeoutError)
        expect((error as ServiceTimeoutError).service).toBe('WorkOS')
      }
    })

    it('includes operation name in error when provided', async () => {
      const slowPromise = new Promise(resolve => setTimeout(() => resolve('late'), 5000))
      const timeoutPromise = withTimeout(slowPromise, 1000, 'WorkOS', 'jwt_verify')

      vi.advanceTimersByTime(1001)

      try {
        await timeoutPromise
      } catch (error) {
        expect(error).toBeInstanceOf(ServiceTimeoutError)
        expect((error as ServiceTimeoutError).operation).toBe('jwt_verify')
      }
    })

    it('includes timeout duration in error', async () => {
      const slowPromise = new Promise(resolve => setTimeout(() => resolve('late'), 5000))
      const timeoutPromise = withTimeout(slowPromise, 15000, 'TestService')

      vi.advanceTimersByTime(15001)

      try {
        await timeoutPromise
      } catch (error) {
        expect(error).toBeInstanceOf(ServiceTimeoutError)
        expect((error as ServiceTimeoutError).timeoutMs).toBe(15000)
      }
    })

    it('formats error message correctly', async () => {
      const slowPromise = new Promise(resolve => setTimeout(() => resolve('late'), 5000))
      const timeoutPromise = withTimeout(slowPromise, 1000, 'WorkOS', 'jwt_verify')

      vi.advanceTimersByTime(1001)

      await expect(timeoutPromise).rejects.toThrow('WorkOS:jwt_verify timed out after 1000ms')
    })

    it('formats error message without operation', async () => {
      const slowPromise = new Promise(resolve => setTimeout(() => resolve('late'), 5000))
      const timeoutPromise = withTimeout(slowPromise, 1000, 'WorkOS')

      vi.advanceTimersByTime(1001)

      await expect(timeoutPromise).rejects.toThrow('WorkOS timed out after 1000ms')
    })
  })

  describe('error propagation', () => {
    it('propagates original errors from the operation', async () => {
      const customError = new Error('Custom operation error')

      await expect(
        withTimeout(Promise.reject(customError), 1000, 'TestService')
      ).rejects.toThrow('Custom operation error')
    })

    it('preserves error type from operation', async () => {
      class CustomError extends Error {
        code = 'CUSTOM_CODE'
      }

      await expect(
        withTimeout(Promise.reject(new CustomError('test')), 1000, 'TestService')
      ).rejects.toBeInstanceOf(CustomError)
    })

    it('clears timeout handle when operation fails', async () => {
      const clearTimeoutSpy = vi.spyOn(global, 'clearTimeout')

      await expect(
        withTimeout(Promise.reject(new Error('fail')), 1000, 'Test')
      ).rejects.toThrow()

      expect(clearTimeoutSpy).toHaveBeenCalled()
      clearTimeoutSpy.mockRestore()
    })
  })

  describe('edge cases', () => {
    it('handles zero timeout (immediate timeout)', async () => {
      const slowPromise = new Promise(resolve => setTimeout(() => resolve('late'), 100))
      const timeoutPromise = withTimeout(slowPromise, 0, 'TestService')

      vi.advanceTimersByTime(1)

      await expect(timeoutPromise).rejects.toThrow(ServiceTimeoutError)
    })

    it('handles very short timeout', async () => {
      const slowPromise = new Promise(resolve => setTimeout(() => resolve('late'), 100))
      const timeoutPromise = withTimeout(slowPromise, 10, 'TestService')

      vi.advanceTimersByTime(11)

      await expect(timeoutPromise).rejects.toThrow(ServiceTimeoutError)
    })

    it('completes just before timeout', async () => {
      const almostSlowPromise = new Promise(resolve => setTimeout(() => resolve('just in time'), 999))
      const timeoutPromise = withTimeout(almostSlowPromise, 1000, 'TestService')

      vi.advanceTimersByTime(999)

      const result = await timeoutPromise
      expect(result).toBe('just in time')
    })
  })
})

describe('SERVICE_TIMEOUTS', () => {
  it('defines WORKOS_JWT timeout', () => {
    expect(SERVICE_TIMEOUTS.WORKOS_JWT).toBe(15_000)
  })

  it('defines WORKOS_USER timeout', () => {
    expect(SERVICE_TIMEOUTS.WORKOS_USER).toBe(10_000)
  })

  it('defines DATABASE_SIMPLE timeout', () => {
    expect(SERVICE_TIMEOUTS.DATABASE_SIMPLE).toBe(10_000)
  })

  it('defines DATABASE_COMPLEX timeout', () => {
    expect(SERVICE_TIMEOUTS.DATABASE_COMPLEX).toBe(30_000)
  })
})

describe('ServiceTimeoutError', () => {
  it('extends Error', () => {
    const error = new ServiceTimeoutError('TestService', 1000)
    expect(error).toBeInstanceOf(Error)
  })

  it('has correct name', () => {
    const error = new ServiceTimeoutError('TestService', 1000)
    expect(error.name).toBe('ServiceTimeoutError')
  })

  it('stores service, timeout, and operation', () => {
    const error = new ServiceTimeoutError('WorkOS', 15000, 'jwt_verify', { userId: '123' })

    expect(error.service).toBe('WorkOS')
    expect(error.timeoutMs).toBe(15000)
    expect(error.operation).toBe('jwt_verify')
    expect(error.context).toEqual({ userId: '123' })
  })
})
