import { describe, it, expect, beforeEach } from 'vitest';
import {
  assertCircuitAvailable,
  recordCircuitFailure,
  recordCircuitSuccess,
  resetCircuitBreaker,
  configureCircuitBreaker,
  isCircuitOpen,
  CircuitOpenError,
  DEFAULT_CIRCUIT_FAILURE_THRESHOLD,
} from '../../../../octocode-tools-core/src/utils/http/circuitBreaker.js';

const URL_A = 'https://api.github.com/repos/x';
const URL_B = 'https://registry.npmjs.org/x';

describe('circuit breaker (#T13)', () => {
  beforeEach(() => resetCircuitBreaker());

  it('stays closed below the failure threshold', () => {
    for (let i = 0; i < DEFAULT_CIRCUIT_FAILURE_THRESHOLD - 1; i++) {
      recordCircuitFailure(URL_A, 1000);
    }
    expect(() => assertCircuitAvailable(URL_A, 1000)).not.toThrow();
  });

  it('opens after threshold consecutive failures and fails fast', () => {
    for (let i = 0; i < DEFAULT_CIRCUIT_FAILURE_THRESHOLD; i++) {
      recordCircuitFailure(URL_A, 1000);
    }
    expect(() => assertCircuitAvailable(URL_A, 1000)).toThrowError(
      CircuitOpenError
    );
  });

  it('is per-host (one host opening does not affect another)', () => {
    for (let i = 0; i < DEFAULT_CIRCUIT_FAILURE_THRESHOLD; i++) {
      recordCircuitFailure(URL_A, 1000);
    }
    expect(() => assertCircuitAvailable(URL_A, 1000)).toThrow();
    expect(() => assertCircuitAvailable(URL_B, 1000)).not.toThrow();
  });

  it('half-opens after the cooldown, then closes on a successful trial', () => {
    configureCircuitBreaker({ failureThreshold: 2, cooldownMs: 5000 });
    recordCircuitFailure(URL_A, 0);
    recordCircuitFailure(URL_A, 0);
    expect(() => assertCircuitAvailable(URL_A, 1000)).toThrow();
    expect(() => assertCircuitAvailable(URL_A, 6000)).not.toThrow();
    recordCircuitSuccess(URL_A);
    expect(() => assertCircuitAvailable(URL_A, 7000)).not.toThrow();
  });

  it('re-opens if the half-open trial fails', () => {
    configureCircuitBreaker({ failureThreshold: 2, cooldownMs: 5000 });
    recordCircuitFailure(URL_A, 0);
    recordCircuitFailure(URL_A, 0);
    assertCircuitAvailable(URL_A, 6000);
    recordCircuitFailure(URL_A, 6000);
    expect(() => assertCircuitAvailable(URL_A, 6500)).toThrow();
  });

  it('a success resets the failure count', () => {
    recordCircuitFailure(URL_A, 0);
    recordCircuitFailure(URL_A, 0);
    recordCircuitSuccess(URL_A);
    for (let i = 0; i < DEFAULT_CIRCUIT_FAILURE_THRESHOLD - 1; i++) {
      recordCircuitFailure(URL_A, 0);
    }
    expect(() => assertCircuitAvailable(URL_A, 0)).not.toThrow();
  });

  it('isCircuitOpen returns false when no circuit exists', () => {
    expect(isCircuitOpen(URL_A)).toBe(false);
  });

  it('handles non-URL strings in hostKey (isCircuitOpen with raw string)', () => {
    expect(isCircuitOpen('not-a-valid-url')).toBe(false);
  });

  it('hostKey falls back to raw url when host is empty (mailto: scheme)', () => {
    expect(isCircuitOpen('mailto:x@example.com')).toBe(false);
  });

  it('assertCircuitAvailable returns early when circuit exists but is closed', () => {
    for (let i = 0; i < DEFAULT_CIRCUIT_FAILURE_THRESHOLD - 1; i++) {
      recordCircuitFailure(URL_A, 0);
    }
    expect(() => assertCircuitAvailable(URL_A, 0)).not.toThrow();
  });

  it('configureCircuitBreaker ignores non-numeric / absent options (false branches)', () => {
    configureCircuitBreaker({});
    for (let i = 0; i < DEFAULT_CIRCUIT_FAILURE_THRESHOLD; i++) {
      recordCircuitFailure(URL_A, 0);
    }
    expect(() => assertCircuitAvailable(URL_A, 0)).toThrow(CircuitOpenError);
  });

  it('isCircuitOpen returns true when circuit is open within cooldown', () => {
    configureCircuitBreaker({ failureThreshold: 2, cooldownMs: 5000 });
    recordCircuitFailure(URL_A, 0);
    recordCircuitFailure(URL_A, 0);
    expect(isCircuitOpen(URL_A, 1000)).toBe(true);
  });

  it('isCircuitOpen returns false when circuit is in half-open state', () => {
    configureCircuitBreaker({ failureThreshold: 2, cooldownMs: 5000 });
    recordCircuitFailure(URL_A, 0);
    recordCircuitFailure(URL_A, 0);
    assertCircuitAvailable(URL_A, 6000);
    expect(isCircuitOpen(URL_A, 6000)).toBe(false);
  });

  it('assertCircuitAvailable no-ops when circuit is already half-open', () => {
    configureCircuitBreaker({ failureThreshold: 2, cooldownMs: 5000 });
    recordCircuitFailure(URL_A, 0);
    recordCircuitFailure(URL_A, 0);
    expect(() => assertCircuitAvailable(URL_A, 6000)).not.toThrow();
    expect(() => assertCircuitAvailable(URL_A, 6001)).not.toThrow();
  });

  it('isCircuitOpen returns false when circuit is open but cooldown has elapsed', () => {
    configureCircuitBreaker({ failureThreshold: 2, cooldownMs: 5000 });
    recordCircuitFailure(URL_A, 0);
    recordCircuitFailure(URL_A, 0);
    expect(isCircuitOpen(URL_A, 10000)).toBe(false);
  });
});
