require 'spec_helper'

describe Bogus::MockingDSL do
  class ExampleFoo
    def foo(bar)
    end

    def hello(greeting, name)
    end

    def with_optional_args(x, y = 1)
    end

    def self.bar(baz)
      "Hello #{baz}"
    end
  end

  class Stubber
    extend Bogus::MockingDSL
  end

  before do
    Bogus.reset!
  end

  describe "#stub" do
    let(:baz) { ExampleFoo.new }

    it "allows stubbing the existing methods" do
      Stubber.stub(baz).foo("bar") { :return_value }

      expect(baz.foo("bar")).to eq :return_value
    end

    it "can stub method with any parameters" do
      Stubber.stub(baz).foo(Stubber.any_args) { :default_value }
      Stubber.stub(baz).foo("bar") { :foo_value }

      expect(baz.foo("a")).to eq :default_value
      expect(baz.foo("b")).to eq :default_value
      expect(baz.foo("bar")).to eq :foo_value
    end

    it "can stub method with some wildcard parameters" do
      Stubber.stub(baz).hello(Stubber.any_args) { :default_value }
      Stubber.stub(baz).hello("welcome", Stubber.anything) { :greeting_value }

      expect(baz.hello("hello", "adam")).to eq :default_value
      expect(baz.hello("welcome", "adam")).to eq :greeting_value
      expect(baz.hello("welcome", "rudy")).to eq :greeting_value
    end

    it "does not allow stubbing non-existent methods" do
      baz = ExampleFoo.new
      expect do
        Stubber.stub(baz).does_not_exist("bar") { :return_value }
      end.to raise_error(NameError)
    end

    it "unstubs methods after each test" do
      Stubber.stub(ExampleFoo).bar("John") { "something else" }

      Bogus.after_each_test

      expect(ExampleFoo.bar("John")).to eq "Hello John"
    end
  end

  describe "#have_received" do
    context "with a fake object" do
      let(:the_fake) { Bogus.fake_for(:example_foo) }

      it "allows verifying that fakes have correct interfaces" do
        the_fake.foo("test")

        expect(the_fake).to Stubber.have_received.foo("test")
      end

      it "does not allow verifying on non-existent methods" do
        expect {
          expect(the_fake).to Stubber.have_received.bar("test")
        }.to raise_error(NameError)
      end

      it "does not allow verifying on methods with a wrong argument count" do
        expect {
          expect(the_fake).to Stubber.have_received.foo("test", "test 2")
        }.to raise_error(ArgumentError)
      end

      it "allows spying on methods with optional parameters" do
        the_fake.with_optional_args(123)

        expect(the_fake).to Stubber.have_received.with_optional_args(123)
      end
    end

    it "can be used with plain old Ruby objects" do
      object = ExampleFoo.new
      Stubber.stub(object).foo(Stubber.any_args)

      object.foo('test')

      expect(object).to Stubber.have_received.foo("test")
    end

    it "allows spying on methods with optional parameters" do
      object = ExampleFoo.new
      Stubber.stub(object).with_optional_args(123) { 999 }

      expect(object.with_optional_args(123)).to eq 999

      expect(object).to Stubber.have_received.with_optional_args(123)
    end

    class ClassWithMatches
      def matches?(something)
      end
    end

    it "can be used with objects that have same methods as an RSpec expectation" do
      fake = Bogus.fake_for(:class_with_matches)

      fake.matches?("foo")

      expect(fake).to Bogus.have_received.matches?("foo")
    end

    class PassesSelfToCollaborator
      def hello(example)
        example.foo(self)
      end
    end

    it "can be used with self references" do
      Bogus.record_calls_for(:passes_self_to_collaborator, PassesSelfToCollaborator)

      fake = Bogus.fake_for(:example_foo)
      object = PassesSelfToCollaborator.new

      object.hello(fake)

      expect(fake).to Bogus.have_received.foo(object)
    end
  end

  class Mocker
    extend Bogus::MockingDSL
  end

  describe "#mock" do
    let(:object) { ExampleFoo.new }
    let(:fake) { Bogus.fake_for(:example_foo) { ExampleFoo } }

    shared_examples_for "mocking dsl" do
      it "allows mocking on methods with optional parameters" do
        Mocker.mock(baz).with_optional_args(1) { :return }

        expect(baz.with_optional_args(1)).to eq :return

        expect { Bogus.after_each_test }.not_to raise_error
      end

      it "allows mocking with anything" do
        Mocker.mock(baz).hello(1, Bogus::Anything) { :return }

        expect(baz.hello(1, 2)).to eq :return

        expect { Bogus.after_each_test }.not_to raise_error
      end

      it "allows mocking the existing methods" do
        Mocker.mock(baz).foo("bar") { :return_value }

        expect(baz.foo("bar")).to eq :return_value

        expect { Bogus.after_each_test }.not_to raise_error
      end

      it "verifies that the methods mocked exist" do
        expect {
          Mocker.mock(baz).does_not_exist { "whatever" }
        }.to raise_error(NameError)
      end

      it "raises errors when mocks were not called" do
        Mocker.mock(baz).foo("bar")

        expect {
          Bogus.after_each_test
        }.to raise_error(Bogus::NotAllExpectationsSatisfied)
      end

      it "clears the data between tests" do
        Mocker.mock(baz).foo("bar")

        Bogus.send(:clear_expectations)

        expect {
          Bogus.after_each_test
        }.not_to raise_error
      end
    end

    class ExampleForMockingOnConstants
      def self.bar(foo)
      end

      def self.baz
      end
    end

    it "clears expected interactions from constants" do
      Mocker.mock(ExampleForMockingOnConstants).bar("foo")

      expect {
        Bogus.after_each_test
      }.to raise_error(Bogus::NotAllExpectationsSatisfied)

      Mocker.stub(ExampleForMockingOnConstants).baz

      expect {
        Bogus.after_each_test
      }.not_to raise_error
    end

    context "with fakes" do
      it_behaves_like "mocking dsl" do
        let(:baz) { fake }
      end
    end

    context "with regular objects" do
      it_behaves_like "mocking dsl" do
        let(:baz) { object }
      end
    end
  end

  describe "#fake" do
    include Bogus::MockingDSL

    it "creates objects that can be stubbed" do
      greeter = fake

      stub(greeter).greet("Jake") { "Hello Jake" }

      expect(greeter.greet("Jake")).to eq "Hello Jake"
    end

    it "creates objects that can be mocked" do
      greeter = fake

      mock(greeter).greet("Jake") { "Hello Jake" }

      expect(greeter.greet("Jake")).to eq "Hello Jake"
    end

    it "creates objects with some methods stubbed by default" do
      greeter = fake(greet: "Hello Jake")

      expect(greeter.greet("Jake")).to eq "Hello Jake"
    end

    it "creates objects that can be spied upon" do
      greeter = fake

      greeter.greet("Jake")

      expect(greeter).to have_received.greet("Jake")
      expect(greeter).to_not have_received.greet("Bob")
    end

    it "verifies mock expectations set on anonymous fakes" do
      greeter = fake
      mock(greeter).greet("Jake") { "Hello Jake" }

      expect {
        Bogus.after_each_test
      }.to raise_error(Bogus::NotAllExpectationsSatisfied)
    end
  end

  class SampleOfConfiguredFake
    def self.foo(x)
    end

    def self.bar(x, y)
    end

    def self.baz
    end
  end

  describe "globally configured fakes" do
    before do
      Bogus.fakes do
        fake(:globally_configured_fake, as: :class, class: proc{SampleOfConfiguredFake}) do
          foo "foo"
          bar { "bar" }
          baz { raise "oh noes!" }
        end
      end
    end

    it "returns configured fakes" do
      the_fake = Stubber.fake(:globally_configured_fake)

      expect(the_fake.foo('a')).to eq "foo"
      expect(the_fake.bar('a', 'b')).to eq "bar"
    end

    it "allows overwriting stubbed methods" do
      the_fake = Stubber.fake(:globally_configured_fake, bar: "baz")

      expect(the_fake.foo('a')).to eq "foo"
      expect(the_fake.bar('a', 'b')).to eq "baz"
    end

    it "evaluates the block passed to method in configuration when method is called" do
      the_fake = Stubber.fake(:globally_configured_fake)

      expect{ the_fake.baz }.to raise_error("oh noes!")
    end
  end

  describe "faking classes" do
    class ThisClassWillBeReplaced
      def self.hello(name)
        "Hello, #{name}"
      end
    end
    TheOriginalClass = ThisClassWillBeReplaced

    after do
      Bogus.reset_overwritten_classes
    end

    it "replaces the class for the duration of the test" do
      Stubber.fake_class(ThisClassWillBeReplaced, hello: "replaced!")

      expect(ThisClassWillBeReplaced.hello("foo")).to eq "replaced!"
      expect(ThisClassWillBeReplaced).to_not equal(TheOriginalClass)
    end

    it "makes it possible to spy on classes" do
      Stubber.fake_class(ThisClassWillBeReplaced)

      ThisClassWillBeReplaced.hello("foo")

      expect(ThisClassWillBeReplaced).to Bogus.have_received.hello("foo")
      expect(ThisClassWillBeReplaced).to_not Bogus.have_received.hello("bar")
    end

    it "restores the class after the test has finished" do
      Stubber.fake_class(ThisClassWillBeReplaced)
      Bogus.reset_overwritten_classes

      expect(ThisClassWillBeReplaced).to equal(TheOriginalClass)
    end
  end

  class SampleForContracts
    def initialize(name)
      @name = name
    end

    def greet(greeting = "Hello")
      "#{greeting}, #{@name}!"
    end
  end

  describe "contracts" do
    let(:sample) { SampleForContracts.new("John") }

    before do
      Bogus.reset!

      Stubber.fake(:sample_for_contracts, greet: "Welcome, John!")

      Bogus.after_each_test

      Bogus.record_calls_for(:sample_for_contracts, SampleForContracts)
    end

    it "passes when all the mocked interactions were executed" do
      expect(sample.greet("Welcome")).to eq "Welcome, John!"

      Bogus.after_each_test

      expect {
        Bogus.verify_contract!(:sample_for_contracts)
      }.not_to raise_error
    end

    it "fails when contracts are fullfilled" do
      expect(sample.greet("Hello")).to eq "Hello, John!"

      Bogus.after_each_test

      expect {
        Bogus.verify_contract!(:sample_for_contracts)
      }.to raise_error(Bogus::ContractNotFulfilled)
    end
  end
end
