'use strict';

/**
 * Unit tests for circuit-breaker.js
 */

const { describe, it, beforeEach } = require('node:test');
const assert = require('node:assert/strict');

const { CircuitBreaker, CircuitOpenError, STATE } = require('../../src/circuit-breaker');

function makeBreaker(opts = {}) {
  return new CircuitBreaker({ threshold: 3, cooldownMs: 100, ...opts });
}

describe('CircuitBreaker', () => {
  let breaker;

  beforeEach(() => {
    breaker = makeBreaker();
  });

  it('starts in CLOSED state', () => {
    assert.equal(breaker.getState(), STATE.CLOSED);
    assert.equal(breaker.failures, 0);
  });

  it('executes successfully in CLOSED state', async () => {
    const result = await breaker.execute(async () => 42);
    assert.equal(result, 42);
    assert.equal(breaker.getState(), STATE.CLOSED);
    assert.equal(breaker.failures, 0);
  });

  it('tracks failures below threshold', async () => {
    const failFn = async () => {
      throw new Error('fail');
    };
    await assert.rejects(() => breaker.execute(failFn));
    await assert.rejects(() => breaker.execute(failFn));
    assert.equal(breaker.getState(), STATE.CLOSED);
    assert.equal(breaker.failures, 2);
  });

  it('transitions to OPEN after threshold failures', async () => {
    const failFn = async () => {
      throw new Error('fail');
    };
    for (let i = 0; i < 3; i++) {
      await assert.rejects(() => breaker.execute(failFn));
    }
    assert.equal(breaker.getState(), STATE.OPEN);
  });

  it('rejects calls immediately when OPEN', async () => {
    const failFn = async () => {
      throw new Error('fail');
    };
    for (let i = 0; i < 3; i++) {
      await assert.rejects(() => breaker.execute(failFn));
    }
    assert.equal(breaker.getState(), STATE.OPEN);

    await assert.rejects(() => breaker.execute(async () => 'should not run'), CircuitOpenError);
  });

  it('transitions to HALF_OPEN after cooldown', async () => {
    const failFn = async () => {
      throw new Error('fail');
    };
    for (let i = 0; i < 3; i++) {
      await assert.rejects(() => breaker.execute(failFn));
    }
    assert.equal(breaker.getState(), STATE.OPEN);

    // Wait for cooldown
    await sleep(150);

    // Next call transitions to HALF_OPEN and executes
    const result = await breaker.execute(async () => 'probe');
    assert.equal(result, 'probe');
    assert.equal(breaker.getState(), STATE.CLOSED);
  });

  it('transitions HALF_OPEN -> OPEN if probe fails', async () => {
    const failFn = async () => {
      throw new Error('fail');
    };
    for (let i = 0; i < 3; i++) {
      await assert.rejects(() => breaker.execute(failFn));
    }
    assert.equal(breaker.getState(), STATE.OPEN);

    await sleep(150);

    // Probe fails
    await assert.rejects(() => breaker.execute(failFn));
    assert.equal(breaker.getState(), STATE.OPEN);
  });

  it('resets on success after HALF_OPEN', async () => {
    const failFn = async () => {
      throw new Error('fail');
    };
    for (let i = 0; i < 3; i++) {
      await assert.rejects(() => breaker.execute(failFn));
    }
    await sleep(150);
    await breaker.execute(async () => 'ok');
    assert.equal(breaker.getState(), STATE.CLOSED);
    assert.equal(breaker.failures, 0);
  });

  it('calls onStateChange callback on transitions', async () => {
    const changes = [];
    breaker = makeBreaker({
      onStateChange: (newState, oldState) => changes.push({ newState, oldState }),
    });

    const failFn = async () => {
      throw new Error('fail');
    };
    for (let i = 0; i < 3; i++) {
      await assert.rejects(() => breaker.execute(failFn));
    }

    assert.equal(changes.length, 1);
    assert.equal(changes[0].newState, STATE.OPEN);
    assert.equal(changes[0].oldState, STATE.CLOSED);
  });

  it('reset() returns to CLOSED', async () => {
    const failFn = async () => {
      throw new Error('fail');
    };
    for (let i = 0; i < 3; i++) {
      await assert.rejects(() => breaker.execute(failFn));
    }
    assert.equal(breaker.getState(), STATE.OPEN);

    breaker.reset();
    assert.equal(breaker.getState(), STATE.CLOSED);
    assert.equal(breaker.failures, 0);
  });

  it('getMetrics() returns correct data', () => {
    const metrics = breaker.getMetrics();
    assert.equal(metrics.state, STATE.CLOSED);
    assert.equal(metrics.failures, 0);
    assert.equal(metrics.threshold, 3);
    assert.equal(metrics.openedAt, null);
    assert.equal(metrics.lastError, null);
  });

  it('getMetrics() reflects open state', async () => {
    const failFn = async () => {
      throw new Error('test error');
    };
    for (let i = 0; i < 3; i++) {
      await assert.rejects(() => breaker.execute(failFn));
    }
    const metrics = breaker.getMetrics();
    assert.equal(metrics.state, STATE.OPEN);
    assert.ok(metrics.openedAt > 0);
    assert.equal(metrics.lastError, 'test error');
  });
});

function sleep(ms) {
  return new Promise((resolve) => setTimeout(resolve, ms));
}
