require 'spec_helper'

module Draper
  describe Factory do
    describe "#initialize" do
      it "accepts valid options" do
        valid_options = {with: Decorator, context: {foo: "bar"}}
        expect{Factory.new(valid_options)}.not_to raise_error
      end

      it "rejects invalid options" do
        expect{Factory.new(foo: "bar")}.to raise_error ArgumentError, /Unknown key/
      end
    end

    describe "#decorate" do
      context "when object is nil" do
        it "returns nil" do
          factory = Factory.new

          expect(factory.decorate(nil)).to be_nil
        end
      end

      it "calls a worker" do
        factory = Factory.new
        worker = ->(*){ :decorated }

        expect(Factory::Worker).to receive(:new).and_return(worker)
        expect(factory.decorate(double)).to be :decorated
      end

      it "passes the object to the worker" do
        factory = Factory.new
        object = double

        expect(Factory::Worker).to receive(:new).with(anything(), object).and_return(->(*){})
        factory.decorate(object)
      end

      context "when the :with option was given" do
        it "passes the decorator class to the worker" do
          decorator_class = double
          factory = Factory.new(with: decorator_class)

          expect(Factory::Worker).to receive(:new).with(decorator_class, anything()).and_return(->(*){})
          factory.decorate(double)
        end
      end

      context "when the :with option was omitted" do
        it "passes nil to the worker" do
          factory = Factory.new

          expect(Factory::Worker).to receive(:new).with(nil, anything()).and_return(->(*){})
          factory.decorate(double)
        end
      end

      it "passes options to the call" do
        factory = Factory.new
        worker = ->(*){}
        allow(Factory::Worker).to receive(:new).and_return(worker)
        options = {foo: "bar"}

        allow(worker).to receive(:call).with(options)
        factory.decorate(double, options)
      end

      context "when the :context option was given" do
        it "sets the passed context" do
          factory = Factory.new(context: {foo: "bar"})
          worker = ->(*){}
          allow(Factory::Worker).to receive_messages new: worker

          expect(worker).to receive(:call).with({baz: "qux", context: {foo: "bar"}})
          factory.decorate(double, {baz: "qux"})
        end

        it "is overridden by explicitly-specified context" do
          factory = Factory.new({context: {foo: "bar"}})
          worker = ->(*){}
          allow(Factory::Worker).to receive_messages new: worker

          expect(worker).to receive(:call).with({context: {baz: "qux"}})
          factory.decorate(double, {context: {baz: "qux"}})
        end
      end
    end
  end

  describe Factory::Worker do
    describe "#call" do
      it "calls the decorator method" do
        object = double
        options = {foo: "bar"}
        worker = Factory::Worker.new(double, object)
        decorator = ->(*){}
        allow(worker).to receive(:decorator){ decorator }

        allow(decorator).to receive(:call).with(object, options).and_return(:decorated)
        expect(worker.call(options)).to be :decorated
      end

      context "when the :context option is callable" do
        it "calls it" do
          worker = Factory::Worker.new(double, double)
          decorator = ->(*){}
          allow(worker).to receive_messages decorator: decorator
          context = {foo: "bar"}

          expect(decorator).to receive(:call).with(anything(), {context: context})
          worker.call(context: ->{ context })
        end

        it "receives arguments from the :context_args option" do
          worker = Factory::Worker.new(double, double)
          allow(worker).to receive_messages decorator: ->(*){}
          context = ->{}

          expect(context).to receive(:call).with(:foo, :bar)
          worker.call(context: context, context_args: [:foo, :bar])
        end

        it "wraps non-arrays passed to :context_args" do
          worker = Factory::Worker.new(double, double)
          allow(worker).to receive_messages decorator: ->(*){}
          context = ->{}
          hash = {foo: "bar"}

          expect(context).to receive(:call).with(hash)
          worker.call(context: context, context_args: hash)
        end
      end

      context "when the :context option is not callable" do
        it "doesn't call it" do
          worker = Factory::Worker.new(double, double)
          decorator = ->(*){}
          allow(worker).to receive_messages decorator: decorator
          context = {foo: "bar"}

          expect(decorator).to receive(:call).with(anything(), {context: context})
          worker.call(context: context)
        end
      end

      it "does not pass the :context_args option to the decorator" do
        worker = Factory::Worker.new(double, double)
        decorator = ->(*){}
        allow(worker).to receive_messages decorator: decorator

        expect(decorator).to receive(:call).with(anything(), {foo: "bar"})
        worker.call(foo: "bar", context_args: [])
      end
    end

    describe "#decorator" do
      context "for a singular object" do
        context "when decorator_class is specified" do
          it "returns the .decorate method from the decorator" do
            decorator_class = Class.new(Decorator)
            worker = Factory::Worker.new(decorator_class, double)

            expect(worker.decorator).to eq decorator_class.method(:decorate)
          end
        end

        context "when decorator_class is unspecified" do
          context "and the object is decoratable" do
            it "returns the object's #decorate method" do
              object = double
              options = {foo: "bar"}
              worker = Factory::Worker.new(nil, object)

              expect(object).to receive(:decorate).with(options).and_return(:decorated)
              expect(worker.decorator.call(object, options)).to be :decorated
            end
          end

          context "and the object is not decoratable" do
            it "raises an error" do
              object = double
              worker = Factory::Worker.new(nil, object)

              expect{worker.decorator}.to raise_error UninferrableDecoratorError
            end
          end
        end

        context "when the object is a struct" do
          it "returns a singular decorator" do
            object = Struct.new(:stuff).new("things")

            decorator_class = Class.new(Decorator)
            worker = Factory::Worker.new(decorator_class, object)

            expect(worker.decorator).to eq decorator_class.method(:decorate)
          end
        end
      end

      context "for a collection object" do
        context "when decorator_class is a CollectionDecorator" do
          it "returns the .decorate method from the collection decorator" do
            decorator_class = Class.new(CollectionDecorator)
            worker = Factory::Worker.new(decorator_class, [])

            expect(worker.decorator).to eq decorator_class.method(:decorate)
          end
        end

        context "when decorator_class is a Decorator" do
          it "returns the .decorate_collection method from the decorator" do
            decorator_class = Class.new(Decorator)
            worker = Factory::Worker.new(decorator_class, [])

            expect(worker.decorator).to eq decorator_class.method(:decorate_collection)
          end
        end

        context "when decorator_class is unspecified" do
          context "and the object is decoratable" do
            it "returns the .decorate_collection method from the object's decorator" do
              object = []
              decorator_class = Class.new(Decorator)
              allow(object).to receive(:decorator_class){ decorator_class }
              allow(object).to receive(:decorate){ nil }
              worker = Factory::Worker.new(nil, object)

              expect(decorator_class).to receive(:decorate_collection).with(object, {foo: "bar", with: nil}).and_return(:decorated)
              expect(worker.decorator.call(object, foo: "bar")).to be :decorated
            end
          end

          context "and the object is not decoratable" do
            it "returns the .decorate method from CollectionDecorator" do
              worker = Factory::Worker.new(nil, [])

              expect(worker.decorator).to eq CollectionDecorator.method(:decorate)
            end
          end
        end
      end
    end
  end
end
