# encoding: utf-8

require 'spec_helper'

RSpec.describe Supervision::CircuitBreaker do

  let(:dangerous_call_timeout) { sleep 1 }

  let(:dangerous_call_error) { raise StandardError }

  let(:safe_call) { 'value' }

  let(:object) { described_class }

  it "fails without a block" do
    expect { object.new }.to raise_error(Supervision::InvalidParameterError)
  end

  it "resets circuit" do
    circuit = object.new { }
    expect(circuit.control).to receive(:reset!)
    circuit.reset!
  end

  it "configures the control" do
    block = -> { }
    circuit = object.new { }
    expect(circuit.control.config).to receive(:configure).with(no_args)
    circuit.configure(&block)
  end

  context 'when closed' do
    it "successfully calls the method" do
      circuit = object.new call_timeout: 1.milli do |arg|
        arg == :danger ? dangerouse_call_error : safe_call
      end
      expect(circuit.current).to be(:closed)
      expect(circuit.call(:safe)).to eql(safe_call)
    end

    it "increments a failure counter for exceptions" do
      circuit = object.new call_timeout: 1.milli do |arg|
        arg == :danger ? dangerouse_call_error : safe_call
      end
      circuit.call(:danger)
      expect(circuit.control.failure_count).to eql(1)
    end

    it "increments a failure counter for calls exceeding :call_timeout" do
      circuit = object.new call_timeout: 1.milli do
        dangerous_call_timeout
      end
      circuit.call
      expect(circuit.control.failure_count).to eql(1)
    end
  end

  context 'when open' do
    it "fails all calls with a CircuitBreakerOpenError" do
      circuit = object.new max_failures: 2, reset_timeout: 1.sec do
        dangerous_call_error
      end
      circuit.call
      circuit.call
      expect { circuit.call }.to raise_error(Supervision::CircuitBreakerOpenError)
    end

    it "enters a :half_open state after the :reset_timeout" do
      circuit = object.new reset_timeout: 0.2.sec, max_failures: 0 do
        dangerous_call_error
      end
      expect { circuit.call }.to raise_error(Supervision::CircuitBreakerOpenError)
      expect(circuit.control.current).to eq(:open)
      sleep 0.4
      expect(circuit.control.current).to eq(:half_open)
    end
  end

  context 'when half open' do
    it "resets the breaker back to :closed state on successful call" do
      circuit = object.new reset_timeout: 0.2.sec, max_failures: 0 do |arg|
        arg == :danger ? dangerous_call_error : safe_call
      end
      expect {
        circuit.call(:danger)
      }.to raise_error(Supervision::CircuitBreakerOpenError)
      expect(circuit.control.current).to eql(:open)
      sleep 0.4
      expect(circuit.control.current).to eql(:half_open)
      circuit.call(:safe)
      expect(circuit.control.current).to eql(:closed)
    end
  end

  context 'with notification' do
    it "notifies about successful call" do
      callbacks = []
      circuit = object.new do safe_call end
      circuit.on_success { callbacks << 'on_success' }
      circuit.call
      expect(callbacks).to match_array(["on_success"])
    end

    it "notifies about failed call" do
      callbacks = []
      circuit = object.new do dangerous_call_error end
      circuit.on_failure { callbacks << 'on_failure'}
      circuit.before { callbacks << 'before'}
      circuit.call
      expect(callbacks).to match_array(['before', 'on_failure'])
    end
  end

  describe "#to_s" do
    it 'prints object info' do
      circuit = object.new(name: :danger) { }
      expect(circuit.to_s).to include("@name=danger")
    end
  end

  describe "#inspect" do
    it 'prints object info' do
      circuit = object.new(name: :danger) { }
      expect(circuit.inspect).to include("@name=danger")
    end
  end
end
