import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
import {
  createCircuitBreaker,
  CircuitState,
  ServiceUnavailableError
} from '../index'

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

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

  describe('initial state', () => {
    it('starts in CLOSED state', () => {
      const breaker = createCircuitBreaker('test-service')
      expect(breaker.getState()).toBe(CircuitState.CLOSED)
    })

    it('starts with zero failure count', () => {
      const breaker = createCircuitBreaker('test-service')
      expect(breaker.getFailureCount()).toBe(0)
    })
  })

  describe('CLOSED state behavior', () => {
    it('allows requests when closed', async () => {
      const breaker = createCircuitBreaker('test-service')

      const result = await breaker.execute(() => Promise.resolve('success'))

      expect(result).toBe('success')
      expect(breaker.getState()).toBe(CircuitState.CLOSED)
    })

    it('increments failure count on error', async () => {
      const breaker = createCircuitBreaker('test-service')

      await expect(
        breaker.execute(() => Promise.reject(new Error('fail')))
      ).rejects.toThrow('fail')

      expect(breaker.getFailureCount()).toBe(1)
    })

    it('resets failure count on success', async () => {
      const breaker = createCircuitBreaker('test-service', { failureThreshold: 5 })

      // Two failures
      await expect(breaker.execute(() => Promise.reject(new Error('fail')))).rejects.toThrow()
      await expect(breaker.execute(() => Promise.reject(new Error('fail')))).rejects.toThrow()
      expect(breaker.getFailureCount()).toBe(2)

      // Success resets count
      await breaker.execute(() => Promise.resolve('ok'))
      expect(breaker.getFailureCount()).toBe(0)
    })

    it('opens after failureThreshold consecutive failures', async () => {
      const breaker = createCircuitBreaker('test-service', { failureThreshold: 3 })

      for (let i = 0; i < 3; i++) {
        await expect(
          breaker.execute(() => Promise.reject(new Error('fail')))
        ).rejects.toThrow()
      }

      expect(breaker.getState()).toBe(CircuitState.OPEN)
    })

    it('uses default failureThreshold of 5', async () => {
      const breaker = createCircuitBreaker('test-service')

      // 4 failures should keep circuit closed
      for (let i = 0; i < 4; i++) {
        await expect(breaker.execute(() => Promise.reject(new Error('fail')))).rejects.toThrow()
      }
      expect(breaker.getState()).toBe(CircuitState.CLOSED)

      // 5th failure opens circuit
      await expect(breaker.execute(() => Promise.reject(new Error('fail')))).rejects.toThrow()
      expect(breaker.getState()).toBe(CircuitState.OPEN)
    })
  })

  describe('OPEN state behavior', () => {
    it('throws ServiceUnavailableError when circuit is open', async () => {
      const breaker = createCircuitBreaker('test-service', { failureThreshold: 1 })

      // Force circuit open
      await expect(breaker.execute(() => Promise.reject(new Error('fail')))).rejects.toThrow()
      expect(breaker.getState()).toBe(CircuitState.OPEN)

      // Subsequent requests should fail immediately
      await expect(
        breaker.execute(() => Promise.resolve('should not execute'))
      ).rejects.toThrow(ServiceUnavailableError)
    })

    it('includes circuit_open reason in error', async () => {
      const breaker = createCircuitBreaker('test-service', { failureThreshold: 1 })

      await expect(breaker.execute(() => Promise.reject(new Error('fail')))).rejects.toThrow()

      try {
        await breaker.execute(() => Promise.resolve('x'))
      } catch (error) {
        expect(error).toBeInstanceOf(ServiceUnavailableError)
        expect((error as ServiceUnavailableError).reason).toBe('circuit_open')
      }
    })

    it('transitions to HALF_OPEN after reset timeout', async () => {
      const breaker = createCircuitBreaker('test-service', {
        failureThreshold: 1,
        resetTimeoutMs: 1000
      })

      // Force circuit open
      await expect(breaker.execute(() => Promise.reject(new Error('fail')))).rejects.toThrow()
      expect(breaker.getState()).toBe(CircuitState.OPEN)

      // Advance time past reset timeout
      vi.advanceTimersByTime(1001)

      // Next execute should transition to HALF_OPEN and allow request
      const result = await breaker.execute(() => Promise.resolve('success'))
      expect(result).toBe('success')
      // After success in HALF_OPEN, may transition to CLOSED depending on successThreshold
    })

    it('uses default resetTimeoutMs of 60000', async () => {
      const breaker = createCircuitBreaker('test-service', { failureThreshold: 1 })

      await expect(breaker.execute(() => Promise.reject(new Error('fail')))).rejects.toThrow()

      // 59 seconds - still open
      vi.advanceTimersByTime(59000)
      await expect(breaker.execute(() => Promise.resolve('x'))).rejects.toThrow(ServiceUnavailableError)

      // 61 seconds - should allow attempt
      vi.advanceTimersByTime(2000)
      const result = await breaker.execute(() => Promise.resolve('success'))
      expect(result).toBe('success')
    })
  })

  describe('HALF_OPEN state behavior', () => {
    it('limits concurrent requests in HALF_OPEN state', async () => {
      const breaker = createCircuitBreaker('test-service', {
        failureThreshold: 1,
        resetTimeoutMs: 100,
        halfOpenMaxRequests: 2
      })

      // Force circuit open
      await expect(breaker.execute(() => Promise.reject(new Error('fail')))).rejects.toThrow()
      vi.advanceTimersByTime(101)

      // First two requests should be allowed (transition to HALF_OPEN)
      const pending1 = breaker.execute(() => new Promise(resolve => setTimeout(() => resolve('1'), 1000)))
      const pending2 = breaker.execute(() => new Promise(resolve => setTimeout(() => resolve('2'), 1000)))

      // Third request should be rejected
      await expect(
        breaker.execute(() => Promise.resolve('3'))
      ).rejects.toThrow(ServiceUnavailableError)

      // Clean up pending promises
      vi.advanceTimersByTime(1000)
      await pending1
      await pending2
    })

    it('closes circuit after successThreshold successes', async () => {
      const breaker = createCircuitBreaker('test-service', {
        failureThreshold: 1,
        resetTimeoutMs: 100,
        successThreshold: 2
      })

      // Force circuit open
      await expect(breaker.execute(() => Promise.reject(new Error('fail')))).rejects.toThrow()
      vi.advanceTimersByTime(101)

      // First success
      await breaker.execute(() => Promise.resolve('ok'))

      // Second success should close circuit
      await breaker.execute(() => Promise.resolve('ok'))

      expect(breaker.getState()).toBe(CircuitState.CLOSED)
      expect(breaker.getFailureCount()).toBe(0)
    })

    it('reopens circuit on any failure in HALF_OPEN', async () => {
      const breaker = createCircuitBreaker('test-service', {
        failureThreshold: 1,
        resetTimeoutMs: 100,
        successThreshold: 3
      })

      // Force circuit open
      await expect(breaker.execute(() => Promise.reject(new Error('fail')))).rejects.toThrow()
      vi.advanceTimersByTime(101)

      // One success in HALF_OPEN
      await breaker.execute(() => Promise.resolve('ok'))

      // Failure should reopen circuit
      await expect(breaker.execute(() => Promise.reject(new Error('fail again')))).rejects.toThrow()

      expect(breaker.getState()).toBe(CircuitState.OPEN)
    })

    it('uses default successThreshold of 2', async () => {
      const breaker = createCircuitBreaker('test-service', {
        failureThreshold: 1,
        resetTimeoutMs: 100
      })

      // Force circuit open then wait for HALF_OPEN
      await expect(breaker.execute(() => Promise.reject(new Error('fail')))).rejects.toThrow()
      vi.advanceTimersByTime(101)

      // First success - still HALF_OPEN
      await breaker.execute(() => Promise.resolve('ok'))

      // Second success - should close
      await breaker.execute(() => Promise.resolve('ok'))

      expect(breaker.getState()).toBe(CircuitState.CLOSED)
    })
  })

  describe('reset()', () => {
    it('resets circuit to CLOSED state', async () => {
      const breaker = createCircuitBreaker('test-service', { failureThreshold: 1 })

      // Force circuit open
      await expect(breaker.execute(() => Promise.reject(new Error('fail')))).rejects.toThrow()
      expect(breaker.getState()).toBe(CircuitState.OPEN)

      breaker.reset()

      expect(breaker.getState()).toBe(CircuitState.CLOSED)
      expect(breaker.getFailureCount()).toBe(0)
    })

    it('allows requests after reset', async () => {
      const breaker = createCircuitBreaker('test-service', { failureThreshold: 1 })

      // Force circuit open
      await expect(breaker.execute(() => Promise.reject(new Error('fail')))).rejects.toThrow()

      breaker.reset()

      const result = await breaker.execute(() => Promise.resolve('success'))
      expect(result).toBe('success')
    })
  })

  describe('error propagation', () => {
    it('propagates original error from executed function', async () => {
      const breaker = createCircuitBreaker('test-service')
      const customError = new Error('Custom error message')

      await expect(
        breaker.execute(() => Promise.reject(customError))
      ).rejects.toThrow('Custom error message')
    })

    it('preserves error type', async () => {
      const breaker = createCircuitBreaker('test-service')

      class CustomError extends Error {
        constructor(message: string) {
          super(message)
          this.name = 'CustomError'
        }
      }

      await expect(
        breaker.execute(() => Promise.reject(new CustomError('test')))
      ).rejects.toBeInstanceOf(CustomError)
    })
  })
})
