require_relative '../spec_helper'
require_relative 'fixtures/ensure'

describe "An ensure block inside a begin block" do
  before :each do
    ScratchPad.record []
  end

  it "is executed when an exception is raised in it's corresponding begin block" do
    -> {
      begin
        ScratchPad << :begin
        raise EnsureSpec::Error
      ensure
        ScratchPad << :ensure
      end
    }.should raise_error(EnsureSpec::Error)

    ScratchPad.recorded.should == [:begin, :ensure]
  end

  it "is executed when an exception is raised and rescued in it's corresponding begin block" do
    begin
      ScratchPad << :begin
      raise "An exception occurred!"
    rescue
      ScratchPad << :rescue
    ensure
      ScratchPad << :ensure
    end

    ScratchPad.recorded.should == [:begin, :rescue, :ensure]
  end

  it "is executed even when a symbol is thrown in it's corresponding begin block" do
    catch(:symbol) do
      begin
        ScratchPad << :begin
        throw(:symbol)
      rescue
        ScratchPad << :rescue
      ensure
        ScratchPad << :ensure
      end
    end

    NATFIXME "it is executed even when a symbol is thrown in it's corresponding begin block", exception: SpecFailedException do
      ScratchPad.recorded.should == [:begin, :ensure]
    end
  end

  it "is executed when nothing is raised or thrown in it's corresponding begin block" do
    begin
      ScratchPad << :begin
    rescue
      ScratchPad << :rescue
    ensure
      ScratchPad << :ensure
    end

    ScratchPad.recorded.should == [:begin, :ensure]
  end

  it "has no return value" do
    begin
      :begin
    ensure
      :ensure
    end.should == :begin
  end

  it "sets exception cause if raises exception in block and in ensure" do
    -> {
      begin
        raise "from block"
      ensure
        raise "from ensure"
      end
    }.should raise_error(RuntimeError, "from ensure") { |e|
      e.cause.message.should == "from block"
    }
  end
end

describe "The value of an ensure expression," do
  it "in no-exception scenarios, is the value of the last statement of the protected body" do
    begin
      v = 1
      eval('x=1') # to prevent opts from triggering
      v
    ensure
      v = 2
    end.should == 1
  end

  it "when an exception is rescued, is the value of the rescuing block" do
    begin
      raise 'foo'
    rescue
      v = 3
    ensure
      v = 2
    end.should == 3
  end
end

describe "An ensure block inside a method" do
  before :each do
    @obj = EnsureSpec::Container.new
  end

  it "is executed when an exception is raised in the method" do
    -> { @obj.raise_in_method_with_ensure }.should raise_error(EnsureSpec::Error)
    @obj.executed.should == [:method, :ensure]
  end

  it "is executed when an exception is raised and rescued in the method" do
    @obj.raise_and_rescue_in_method_with_ensure
    @obj.executed.should == [:method, :rescue, :ensure]
  end

  it "is executed even when a symbol is thrown in the method" do
    catch(:symbol) { @obj.throw_in_method_with_ensure }
    NATFIXME 'it is executed even when a symbol is thrown in the method', exception: SpecFailedException do
      @obj.executed.should == [:method, :ensure]
    end
  end

  it "has no impact on the method's implicit return value" do
    @obj.implicit_return_in_method_with_ensure.should == :method
  end

  it "has an impact on the method's explicit return value" do
    NATFIXME "it has an impact on the method's explicit return value", exception: SpecFailedException do
      @obj.explicit_return_in_method_with_ensure.should == :ensure
    end
  end

  it "has an impact on the method's explicit return value from rescue if returns explicitly" do
    NATFIXME "it has an impact on the method's explicit return value from rescue if returns explicitly", exception: SpecFailedException do
      @obj.explicit_return_in_rescue_and_explicit_return_in_ensure.should == "returned in ensure"
    end
  end

  it "has no impact on the method's explicit return value from rescue if returns implicitly" do
    @obj.explicit_return_in_rescue_and_implicit_return_in_ensure.should == "returned in rescue"
  end

  it "suppresses exception raised in method if returns value explicitly" do
    @obj.raise_and_explicit_return_in_ensure.should == "returned in ensure"
  end

  it "suppresses exception raised in rescue if returns value explicitly" do
    @obj.raise_in_rescue_and_explicit_return_in_ensure.should == "returned in ensure"
  end

  it "overrides exception raised in rescue if raises exception itself" do
    -> {
      @obj.raise_in_rescue_and_raise_in_ensure
    }.should raise_error(RuntimeError, "raised in ensure")
  end

  it "suppresses exception raised in method if raises exception itself" do
    -> {
      @obj.raise_in_method_and_raise_in_ensure
    }.should raise_error(RuntimeError, "raised in ensure")
  end
end

describe "An ensure block inside a class" do
  before :each do
    ScratchPad.record []
  end

  it "is executed when an exception is raised" do
    -> {
      eval <<-ruby
        class EnsureInClassExample
          ScratchPad << :class
          raise EnsureSpec::Error
        ensure
          ScratchPad << :ensure
        end
      ruby
    }.should raise_error(EnsureSpec::Error)

    ScratchPad.recorded.should == [:class, :ensure]
  end

  it "is executed when an exception is raised and rescued" do
    eval <<-ruby
      class EnsureInClassExample
        ScratchPad << :class
        raise
      rescue
        ScratchPad << :rescue
      ensure
        ScratchPad << :ensure
      end
    ruby

    ScratchPad.recorded.should == [:class, :rescue, :ensure]
  end

  it "is executed even when a symbol is thrown" do
    catch(:symbol) do
      eval <<-ruby
        class EnsureInClassExample
          ScratchPad << :class
          throw(:symbol)
        rescue
          ScratchPad << :rescue
        ensure
          ScratchPad << :ensure
        end
      ruby
    end

    NATFIXME 'it is executed even when a symbol is thrown', exception: SpecFailedException do
      ScratchPad.recorded.should == [:class, :ensure]
    end
  end

  it "is executed when nothing is raised or thrown" do
    eval <<-ruby
      class EnsureInClassExample
        ScratchPad << :class
      rescue
        ScratchPad << :rescue
      ensure
        ScratchPad << :ensure
      end
    ruby

    ScratchPad.recorded.should == [:class, :ensure]
  end

  it "has no return value" do
    result = eval <<-ruby
      class EnsureInClassExample
        :class
      ensure
        :ensure
      end
    ruby

    result.should == :class
  end
end

describe "An ensure block inside {} block" do
  it "is not allowed" do
    -> {
      eval <<-ruby
        lambda {
          raise
        ensure
        }
      ruby
    }.should raise_error(SyntaxError)
  end
end

describe "An ensure block inside 'do end' block" do
  before :each do
    ScratchPad.record []
  end

  it "is executed when an exception is raised in it's corresponding begin block" do
    -> {
      eval(<<-ruby).call
        lambda do
          ScratchPad << :begin
          raise EnsureSpec::Error
        ensure
          ScratchPad << :ensure
        end
      ruby
    }.should raise_error(EnsureSpec::Error)

    ScratchPad.recorded.should == [:begin, :ensure]
  end

  it "is executed when an exception is raised and rescued in it's corresponding begin block" do
    eval(<<-ruby).call
      lambda do
        ScratchPad << :begin
        raise "An exception occurred!"
      rescue
        ScratchPad << :rescue
      ensure
        ScratchPad << :ensure
      end
    ruby

    ScratchPad.recorded.should == [:begin, :rescue, :ensure]
  end

  it "is executed even when a symbol is thrown in it's corresponding begin block" do
    catch(:symbol) do
      eval(<<-ruby).call
        lambda do
          ScratchPad << :begin
          throw(:symbol)
        rescue
          ScratchPad << :rescue
        ensure
          ScratchPad << :ensure
        end
      ruby
    end

    NATFIXME "it is executed even when a symbol is thrown in it's corresponding begin block", exception: SpecFailedException do
      ScratchPad.recorded.should == [:begin, :ensure]
    end
  end

  it "is executed when nothing is raised or thrown in it's corresponding begin block" do
    eval(<<-ruby).call
      lambda do
        ScratchPad << :begin
      rescue
        ScratchPad << :rescue
      ensure
        ScratchPad << :ensure
      end
    ruby

    ScratchPad.recorded.should == [:begin, :ensure]
  end

  it "has no return value" do
    result = eval(<<-ruby).call
      lambda do
        :begin
      ensure
        :ensure
      end
    ruby

    result.should == :begin
  end

  ruby_version_is "3.4" do
    it "does not introduce extra backtrace entries" do
      def foo
        begin
          raise "oops"
        ensure
          return caller(0, 2)
        end
      end
      line = __LINE__
      foo[0].should =~ /#{__FILE__}:#{line-3}:in 'foo'/
      foo[1].should =~ /#{__FILE__}:#{line+2}:in 'block/
    end
  end
end
