import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
import { calculateRetryDelay, sleep, withRetry } from '../retry'
import { ToolingError, rateLimitExceeded, apiError, validationError, credentialsMissing } from '../../../execution/engine/tools/types'
import type { RetryPolicy } from '../rate-limiter-types'

describe('calculateRetryDelay', () => {
  const policy: RetryPolicy = {
    maxRetries: 3,
    initialDelayMs: 1000,
    maxDelayMs: 10000,
    jitterFactor: 0.3
  }

  it('calculates exponential backoff for attempt 0', () => {
    const delay = calculateRetryDelay(0, policy)

    // First attempt: 1000ms * 2^0 = 1000ms
    // With jitter (0.3): 700ms to 1300ms
    expect(delay).toBeGreaterThanOrEqual(700)
    expect(delay).toBeLessThanOrEqual(1300)
  })

  it('calculates exponential backoff for attempt 1', () => {
    const delay = calculateRetryDelay(1, policy)

    // Second attempt: 1000ms * 2^1 = 2000ms
    // With jitter (0.3): 1400ms to 2600ms
    expect(delay).toBeGreaterThanOrEqual(1400)
    expect(delay).toBeLessThanOrEqual(2600)
  })

  it('calculates exponential backoff for attempt 2', () => {
    const delay = calculateRetryDelay(2, policy)

    // Third attempt: 1000ms * 2^2 = 4000ms
    // With jitter (0.3): 2800ms to 5200ms
    expect(delay).toBeGreaterThanOrEqual(2800)
    expect(delay).toBeLessThanOrEqual(5200)
  })

  it('caps delay at maxDelayMs', () => {
    const delay = calculateRetryDelay(5, policy)

    // Sixth attempt: 1000ms * 2^5 = 32000ms, capped at 10000ms
    // With jitter (0.3): 7000ms to 13000ms
    // But since we cap first: 7000ms to 10000ms (capped)
    expect(delay).toBeGreaterThanOrEqual(7000)
    expect(delay).toBeLessThanOrEqual(13000)
  })

  it('applies jitter to prevent thundering herd', () => {
    const delays = Array.from({ length: 100 }, () => calculateRetryDelay(0, policy))

    // All delays should be within jitter range
    delays.forEach((delay) => {
      expect(delay).toBeGreaterThanOrEqual(700)
      expect(delay).toBeLessThanOrEqual(1300)
    })

    // Delays should be varied (not all the same)
    const uniqueDelays = new Set(delays)
    expect(uniqueDelays.size).toBeGreaterThan(10)
  })

  it('handles zero jitter factor', () => {
    const noJitterPolicy: RetryPolicy = {
      ...policy,
      jitterFactor: 0
    }

    const delay = calculateRetryDelay(0, noJitterPolicy)

    // Should be exactly 1000ms with no jitter
    expect(delay).toBe(1000)
  })

  it('returns integer delay (floors the result)', () => {
    const delay = calculateRetryDelay(0, policy)

    expect(Number.isInteger(delay)).toBe(true)
  })
})

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

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

  it('resolves after specified milliseconds', async () => {
    const promise = sleep(1000)

    vi.advanceTimersByTime(999)
    expect(promise).not.toHaveProperty('resolved')

    vi.advanceTimersByTime(1)
    await promise

    expect(true).toBe(true) // Promise resolved
  })

  it('handles zero delay', async () => {
    const promise = sleep(0)
    await vi.advanceTimersByTimeAsync(0)
    await promise

    expect(true).toBe(true)
  })
})

describe('withRetry', () => {
  const policy: RetryPolicy = {
    maxRetries: 3,
    initialDelayMs: 100,
    maxDelayMs: 1000,
    jitterFactor: 0
  }

  beforeEach(() => {
    vi.clearAllTimers()
  })

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

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

    it('returns result on first success', async () => {
      const fn = vi.fn().mockResolvedValue('success')

      const result = await withRetry(fn, policy)

      expect(result).toBe('success')
      expect(fn).toHaveBeenCalledTimes(1)
    })

    it('returns result on second success after retryable error', async () => {
      const fn = vi
        .fn()
        .mockRejectedValueOnce(rateLimitExceeded('test'))
        .mockResolvedValueOnce('success')

      const promise = withRetry(fn, policy)
      await vi.advanceTimersByTimeAsync(100)
      const result = await promise

      expect(result).toBe('success')
      expect(fn).toHaveBeenCalledTimes(2)
    })

    it('returns result on final retry', async () => {
      const fn = vi
        .fn()
        .mockRejectedValueOnce(apiError('test'))
        .mockRejectedValueOnce(apiError('test'))
        .mockRejectedValueOnce(apiError('test'))
        .mockResolvedValueOnce('success')

      const promise = withRetry(fn, policy)
      await vi.advanceTimersByTimeAsync(100)
      await vi.advanceTimersByTimeAsync(200)
      await vi.advanceTimersByTimeAsync(400)
      const result = await promise

      expect(result).toBe('success')
      expect(fn).toHaveBeenCalledTimes(4) // Initial + 3 retries
    })
  })

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

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

    it('retries on rate_limit_exceeded', async () => {
      const error = rateLimitExceeded('test')
      const fn = vi
        .fn()
        .mockRejectedValueOnce(error)
        .mockResolvedValueOnce('success')

      const promise = withRetry(fn, policy)
      await vi.advanceTimersByTimeAsync(100)
      const result = await promise

      expect(result).toBe('success')
      expect(fn).toHaveBeenCalledTimes(2)
    })

    it('retries on api_error', async () => {
      const error = apiError('Server error')
      const fn = vi
        .fn()
        .mockRejectedValueOnce(error)
        .mockResolvedValueOnce('success')

      const promise = withRetry(fn, policy)
      await vi.advanceTimersByTimeAsync(100)
      const result = await promise

      expect(result).toBe('success')
      expect(fn).toHaveBeenCalledTimes(2)
    })

    it('retries on all retryable error types', async () => {
      const retryableErrors = [
        new ToolingError('service_unavailable', 'test'),
        new ToolingError('rate_limit_exceeded', 'test'),
        new ToolingError('api_error', 'test'),
        new ToolingError('network_error', 'test'),
        new ToolingError('timeout_error', 'test'),
        new ToolingError('server_unavailable', 'test')
      ]

      for (const error of retryableErrors) {
        const fn = vi
          .fn()
          .mockRejectedValueOnce(error)
          .mockResolvedValueOnce('success')

        const promise = withRetry(fn, policy)
        await vi.advanceTimersByTimeAsync(100)
        const result = await promise

        expect(result).toBe('success')
        expect(fn).toHaveBeenCalledTimes(2)
      }
    })
  })

  describe('non-retryable errors', () => {
    it('does not retry on validation_error', async () => {
      const error = validationError('Invalid input')
      const fn = vi.fn().mockRejectedValue(error)

      await expect(withRetry(fn, policy)).rejects.toThrow(error)
      expect(fn).toHaveBeenCalledTimes(1) // No retries
    })

    it('does not retry on credentials_missing', async () => {
      const error = credentialsMissing('gmail')
      const fn = vi.fn().mockRejectedValue(error)

      await expect(withRetry(fn, policy)).rejects.toThrow(error)
      expect(fn).toHaveBeenCalledTimes(1)
    })

    it('does not retry on all non-retryable error types', async () => {
      const nonRetryableErrors = [
        new ToolingError('permission_denied', 'test'),
        new ToolingError('adapter_not_found', 'test'),
        new ToolingError('method_not_found', 'test'),
        new ToolingError('tool_not_found', 'test'),
        new ToolingError('credentials_missing', 'test'),
        new ToolingError('credentials_invalid', 'test'),
        new ToolingError('auth_error', 'test'),
        new ToolingError('validation_error', 'test'),
        new ToolingError('unknown_error', 'test')
      ]

      for (const error of nonRetryableErrors) {
        const fn = vi.fn().mockRejectedValue(error)

        await expect(withRetry(fn, policy)).rejects.toThrow(error)
        expect(fn).toHaveBeenCalledTimes(1)
      }
    })

    it('does not retry on generic Error', async () => {
      const error = new Error('Generic error')
      const fn = vi.fn().mockRejectedValue(error)

      await expect(withRetry(fn, policy)).rejects.toThrow(error)
      expect(fn).toHaveBeenCalledTimes(1) // No retries -- programming bugs throw immediately
    })
  })

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

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

    it('throws ToolingError after max retries exhausted', async () => {
      const originalError = apiError('Persistent failure')
      const fn = vi.fn().mockRejectedValue(originalError)

      const promise = withRetry(fn, policy)

      // Run timers and catch the rejection simultaneously
      const result = Promise.all([
        vi.runAllTimersAsync(),
        expect(promise).rejects.toThrow(ToolingError)
      ])

      await result

      expect(fn).toHaveBeenCalledTimes(4) // Initial + 3 retries
    })
  })

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

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

    it('respects maxRetries setting', async () => {
      const customPolicy: RetryPolicy = {
        maxRetries: 1,
        initialDelayMs: 100,
        maxDelayMs: 1000,
        jitterFactor: 0
      }

      const fn = vi.fn().mockRejectedValue(apiError('test'))

      const promise = withRetry(fn, customPolicy)

      // Run timers and catch the rejection simultaneously
      const result = Promise.all([
        vi.runAllTimersAsync(),
        expect(promise).rejects.toThrow()
      ])

      await result

      // Initial + 1 retry = 2 calls
      expect(fn).toHaveBeenCalledTimes(2)
    })

    it('handles zero maxRetries', async () => {
      const customPolicy: RetryPolicy = {
        maxRetries: 0,
        initialDelayMs: 100,
        maxDelayMs: 1000,
        jitterFactor: 0
      }

      const fn = vi.fn().mockRejectedValue(apiError('test'))

      await expect(withRetry(fn, customPolicy)).rejects.toThrow()

      // Only initial call, no retries
      expect(fn).toHaveBeenCalledTimes(1)
    })

    it('applies exponential backoff between retries', async () => {
      const fn = vi
        .fn()
        .mockRejectedValueOnce(apiError('test'))
        .mockRejectedValueOnce(apiError('test'))
        .mockResolvedValueOnce('success')

      const promise = withRetry(fn, policy)
      await vi.advanceTimersByTimeAsync(100)
      await vi.advanceTimersByTimeAsync(200)
      await promise

      expect(fn).toHaveBeenCalledTimes(3)
    })
  })
})
