import { describe, it, expect, vi, beforeEach } from 'vitest';
import { CircuitBreaker, CircuitBreakerError, CircuitState } from '../src/circuit-breaker';

const fail = async () => { throw new Error('fail'); };
const success = async () => 'ok';

async function executeOrFail<T>(breaker: CircuitBreaker, fn: () => Promise<T>, fallback?: () => T): Promise<T | Error> {
  try {
    return await breaker.execute(fn, fallback);
  } catch (e) {
    return e as Error;
  }
}

describe('CircuitBreaker', () => {
  let stateChanges: Array<{ state: CircuitState; previous: CircuitState }>;

  beforeEach(() => {
    stateChanges = [];
  });

  describe('initial state', () => {
    it('starts as CLOSED', () => {
      const breaker = new CircuitBreaker();
      expect(breaker.getState()).toBe('CLOSED');
    });

    it('uses default thresholds', () => {
      const breaker = new CircuitBreaker();
      const metrics = breaker.getMetrics();
      expect(metrics.failureCount).toBe(0);
      expect(metrics.successCount).toBe(0);
      expect(metrics.totalExecutions).toBe(0);
    });
  });

  describe('custom configuration', () => {
    it('accepts custom failure threshold', async () => {
      const breaker = new CircuitBreaker({ failureThreshold: 3 });
      for (let i = 0; i < 3; i++) {
        await executeOrFail(breaker, fail);
      }
      expect(breaker.getState()).toBe('OPEN');
    });

    it('accepts custom timeout', () => {
      const breaker = new CircuitBreaker({ timeout: 1000 });
      expect(breaker.getState()).toBe('CLOSED');
    });

    it('calls onStateChange callback on transitions', async () => {
      const breaker = new CircuitBreaker({
        failureThreshold: 2,
        onStateChange: (state, previous) => {
          stateChanges.push({ state, previous });
        },
      });

      await executeOrFail(breaker, fail);
      await executeOrFail(breaker, fail);

      expect(stateChanges).toHaveLength(1);
      expect(stateChanges[0]).toEqual({ state: 'OPEN', previous: 'CLOSED' });
    });
  });

  describe('state transitions', () => {
    it('transitions to OPEN after reaching failure threshold', async () => {
      const breaker = new CircuitBreaker({ failureThreshold: 3 });

      await executeOrFail(breaker, fail);
      await executeOrFail(breaker, fail);
      expect(breaker.getState()).toBe('CLOSED');

      await executeOrFail(breaker, fail);
      expect(breaker.getState()).toBe('OPEN');
    });

    it('transitions from OPEN to HALF_OPEN after timeout', async () => {
      const breaker = new CircuitBreaker({
        failureThreshold: 2,
        timeout: 200,
      });

      await executeOrFail(breaker, fail);
      await executeOrFail(breaker, fail);
      expect(breaker.getState()).toBe('OPEN');

      await new Promise((resolve) => setTimeout(resolve, 250));
      expect(breaker.getState()).toBe('HALF_OPEN');
    });

    it('transitions from HALF_OPEN to CLOSED after success threshold', async () => {
      const breaker = new CircuitBreaker({
        failureThreshold: 2,
        successThreshold: 3,
        timeout: 100,
      });

      await executeOrFail(breaker, fail);
      await executeOrFail(breaker, fail);
      expect(breaker.getState()).toBe('OPEN');

      await new Promise((resolve) => setTimeout(resolve, 150));

      const r1 = await breaker.execute(success);
      const r2 = await breaker.execute(success);
      expect(r1).toBe('ok');
      expect(r2).toBe('ok');
      expect(breaker.getState()).toBe('HALF_OPEN');

      const r3 = await breaker.execute(success);
      expect(r3).toBe('ok');
      expect(breaker.getState()).toBe('CLOSED');
    });

    it('transitions from HALF_OPEN back to OPEN on failure', async () => {
      const breaker = new CircuitBreaker({
        failureThreshold: 2,
        timeout: 100,
      });

      await executeOrFail(breaker, fail);
      await executeOrFail(breaker, fail);

      await new Promise((resolve) => setTimeout(resolve, 150));
      expect(breaker.getState()).toBe('HALF_OPEN');

      await executeOrFail(breaker, fail);
      expect(breaker.getState()).toBe('OPEN');
    });
  });

  describe('execute with fallback', () => {
    it('returns fallback value when circuit is OPEN', async () => {
      const breaker = new CircuitBreaker({ failureThreshold: 2 });

      await executeOrFail(breaker, fail);
      await executeOrFail(breaker, fail);

      const result = await breaker.execute(
        async () => { throw new Error('should not reach'); },
        () => 'fallback-value'
      );

      expect(result).toBe('fallback-value');
    });

    it('returns fallback value when API throws in OPEN state', async () => {
      const breaker = new CircuitBreaker({ failureThreshold: 1 });

      await executeOrFail(breaker, fail);

      const originalFn = vi.fn(() => { throw new Error('api error'); });
      const fallbackFn = vi.fn(() => 0.5);

      const result = await breaker.execute(originalFn, fallbackFn);
      expect(result).toBe(0.5);
      expect(fallbackFn).toHaveBeenCalled();
    });

    it('executes function normally when circuit is CLOSED', async () => {
      const breaker = new CircuitBreaker({ failureThreshold: 3 });
      const fn = vi.fn(async () => 'success');

      const result = await breaker.execute(fn, () => 'fallback');
      expect(result).toBe('success');
      expect(fn).toHaveBeenCalled();
    });

    it('uses fallback when circuit is OPEN', async () => {
      const breaker = new CircuitBreaker({ failureThreshold: 1 });
      await executeOrFail(breaker, fail);

      const fn = vi.fn(async () => 'original');
      const fallback = vi.fn(() => 'fallback-value');

      const result = await breaker.execute(fn, fallback);
      expect(result).toBe('fallback-value');
      expect(fallback).toHaveBeenCalled();
    });

    it('throws CircuitBreakerError when OPEN and no fallback', async () => {
      const breaker = new CircuitBreaker({ failureThreshold: 1 });
      await executeOrFail(breaker, fail);

      const result = await executeOrFail(breaker, async () => 'value');
      expect(result).toBeInstanceOf(CircuitBreakerError);
      expect((result as CircuitBreakerError).state).toBe('OPEN');
    });

    it('throws original error when fallback also fails in CLOSED state', async () => {
      const breaker = new CircuitBreaker({ failureThreshold: 5 });

      const originalError = new Error('api error');
      const result = await executeOrFail(
        breaker,
        async () => { throw originalError; },
        () => { throw new Error('fallback error'); }
      );
      expect(result).toBe(originalError);
    });
  });

  describe('metrics', () => {
    it('tracks total executions', async () => {
      const breaker = new CircuitBreaker();
      await breaker.execute(success);
      await breaker.execute(success);

      const metrics = breaker.getMetrics();
      expect(metrics.totalExecutions).toBe(2);
      expect(metrics.totalSuccesses).toBe(2);
      expect(metrics.totalFailures).toBe(0);
    });

    it('tracks failures', async () => {
      const breaker = new CircuitBreaker({ failureThreshold: 5 });
      await executeOrFail(breaker, fail);

      const metrics = breaker.getMetrics();
      expect(metrics.totalExecutions).toBe(1);
      expect(metrics.totalFailures).toBe(1);
      expect(metrics.failureCount).toBe(1);
    });

    it('includes state change timestamp', () => {
      const breaker = new CircuitBreaker();
      const metrics = breaker.getMetrics();
      expect(metrics.stateChangedAt).toBeDefined();
      expect(metrics.stateChangedAt!.getTime()).toBeGreaterThan(Date.now() - 1000);
    });

    it('tracks last failure and success times', async () => {
      const breaker = new CircuitBreaker({ failureThreshold: 5 });
      const before = Date.now();

      await breaker.execute(success);
      await new Promise((resolve) => setTimeout(resolve, 10));
      await executeOrFail(breaker, fail);

      const metrics = breaker.getMetrics();
      expect(metrics.lastSuccessTime!.getTime()).toBeGreaterThanOrEqual(before);
      expect(metrics.lastFailureTime!.getTime()).toBeGreaterThanOrEqual(metrics.lastSuccessTime!.getTime());
    });
  });

  describe('reset', () => {
    it('resets circuit to CLOSED state', async () => {
      const breaker = new CircuitBreaker({ failureThreshold: 2 });

      await executeOrFail(breaker, fail);
      await executeOrFail(breaker, fail);
      expect(breaker.getState()).toBe('OPEN');

      breaker.reset();
      expect(breaker.getState()).toBe('CLOSED');

      const metrics = breaker.getMetrics();
      expect(metrics.failureCount).toBe(0);
      expect(metrics.successCount).toBe(0);
    });

    it('allows execution after reset', async () => {
      const breaker = new CircuitBreaker({ failureThreshold: 1 });
      await executeOrFail(breaker, fail);

      breaker.reset();
      const result = await breaker.execute(success);
      expect(result).toBe('ok');
    });
  });

  describe('CircuitBreakerError', () => {
    it('includes circuit state in error', async () => {
      const breaker = new CircuitBreaker({ failureThreshold: 1 });
      await executeOrFail(breaker, fail);

      const result = await executeOrFail(breaker, success);
      expect(result).toBeInstanceOf(CircuitBreakerError);
      expect((result as CircuitBreakerError).state).toBe('OPEN');
      expect((result as CircuitBreakerError).message).toContain('OPEN');
    });
  });
});
