RSpec.describe Patterns::Calculation do
  before(:all) do
    class Rails
      def self.cache
        @cache ||= ActiveSupport::Cache::MemoryStore.new
      end
    end
  end

  after(:all) do
    Object.send(:remove_const, :Rails)
  end

  after do
    Object.send(:remove_const, :CustomCalculation) if defined?(CustomCalculation)
    Rails.cache.clear
    ActiveSupport::Cache::RedisCacheStore.new.clear
  end

  describe ".result" do
    it "returns a result of the calculation within a #result method" do
      CustomCalculation = Class.new(Patterns::Calculation) do
        private

        def result
          50
        end
      end

      expect(CustomCalculation.result).to eq 50
    end

    it "#result, #result_for and #calculate are aliases" do
      CustomCalculation = Class.new(Patterns::Calculation)

      expect(CustomCalculation.method(:result)).to eq CustomCalculation.method(:result_for)
      expect(CustomCalculation.method(:result)).to eq CustomCalculation.method(:calculate)
    end

    it "exposes the first argument as a subject" do
      CustomCalculation = Class.new(Patterns::Calculation) do
        private

        def result
          subject
        end
      end

      expect(CustomCalculation.result('test')).to eq 'test'
    end

    it "exposes all keyword arguments using #options" do
      CustomCalculation = Class.new(Patterns::Calculation) do
        private

        def result
          [options[:arg_1], options[:arg_2]]
        end
      end

      expect(CustomCalculation.result(nil, arg_1: 20, arg_2: 30)).to eq([20, 30])
    end

    it 'executes calculation with given block' do
      CustomCalculation = Class.new(Patterns::Calculation) do
        private

        def result
          yield(subject)
        end
      end
      
      expect(CustomCalculation.result(5) { |a| a * 3 }).to eq(15)
    end
  end

  describe "caching" do
    it "caches result for 'set_cache_expiry_every' period" do
      travel_to DateTime.new(2017, 1, 1, 12, 0) do
        CustomCalculation = Class.new(Patterns::Calculation) do
          set_cache_expiry_every 1.hour

          class_attribute :counter
          self.counter = 0

          private

          def result
            self.class.counter += 1
          end
        end

        expect(CustomCalculation.result).to eq 1
        expect(CustomCalculation.result).to eq 1
      end

      travel_to DateTime.new(2017, 1, 1, 13, 1) do
        expect(CustomCalculation.result).to eq 2
        expect(CustomCalculation.result).to eq 2
      end
    end

    it "caches result for every option passed" do
      CustomCalculation = Class.new(Patterns::Calculation) do
        set_cache_expiry_every 1.hour

        class_attribute :counter
        self.counter = 0

        private

        def result
          self.class.counter += 1
        end
      end

      expect(CustomCalculation.result(123)).to eq 1
      expect(CustomCalculation.result(123)).to eq 1
      expect(CustomCalculation.result(1024)).to eq 2
      expect(CustomCalculation.result(1024)).to eq 2
      expect(CustomCalculation.result(1024, arg: 1)).to eq 3
      expect(CustomCalculation.result(1024, arg: 1)).to eq 3
    end

    it "caches result for every option passed dependant on the class" do
      CustomCalculation = Class.new(Patterns::Calculation) do
        set_cache_expiry_every 1.hour

        class_attribute :counter
        self.counter = 0

        private

        def result
          self.class.counter += 1
        end
      end

      DifferentCalculation = Class.new(Patterns::Calculation) do
        set_cache_expiry_every 1.hour

        class_attribute :counter
        self.counter = 100

        private

        def result
          self.class.counter += 1
        end
      end

      expect(CustomCalculation.result(123)).to eq 1
      expect(CustomCalculation.result(123)).to eq 1
      expect(DifferentCalculation.result(123)).to eq 101
      expect(DifferentCalculation.result(123)).to eq 101

      Object.send(:remove_const, :DifferentCalculation)
    end

    it "does not cache result if 'set_cache_expiry_every' is not set" do
      CustomCalculation = Class.new(Patterns::Calculation) do
        class_attribute :counter
        self.counter = 0

        private

        def result
          self.class.counter += 1
        end
      end

      expect(CustomCalculation.result).to eq 1
      expect(CustomCalculation.result).to eq 2
    end

    describe "when RedisCacheStore is used" do
      it "does not store data in cache if 'cache_expiry_period' is not set" do
        client = Redis.new
        class Rails
          def self.cache
            @cache ||= ActiveSupport::Cache::RedisCacheStore.new
          end
        end

        CustomCalculation = Class.new(Patterns::Calculation) do
          class_attribute :counter
          self.counter = 0

          private

          def result
            self.class.counter += 1
          end
        end

        expect(CustomCalculation.result).to eq 1
        expect(CustomCalculation.result).to eq 2
        expect(client.keys).to be_empty
      end
    end

    it "uses cache keys consistent between processes" do
      `bundle exec ruby spec/helpers/custom_calculation.rb`
      Process.spawn('bundle exec ruby spec/helpers/custom_calculation_script.rb')
      Process.spawn('bundle exec ruby spec/helpers/custom_calculation_script.rb')
      Process.spawn('bundle exec ruby spec/helpers/custom_calculation_script.rb')
      Process.waitall

      expect(Redis.new.keys.length).to eq 1
    end
  end
end