$: << File.dirname(__FILE__) + '/../lib'
require 'remcached'
require 'em-spec/rspec'

describe Memcached do
  def run(&block)
    EM.run do
      Memcached.servers = %w(127.0.0.2 localhost:11212 localhost localhost)

      @timer = EM::PeriodicTimer.new(0.01) do
        # at least localhost & localhost
        if Memcached.usable_clients.length >= 2
          @timer.cancel
          block.call
        end
      end

    end
  end
  def stop
    Memcached.servers = []
    EM.stop
  end


  context "when doing a simple operation" do
    it "should add a value" do
      run do
        Memcached.add(:key => 'Hello',
                :value => 'World') do |result|
          result.should be_kind_of(Memcached::Response)
          result[:status].should == Memcached::Errors::NO_ERROR
          result[:cas].should_not == 0
          stop
        end
      end
    end

    it "should get a value" do
      run do
        Memcached.get(:key => 'Hello') do |result|
          result.should be_kind_of(Memcached::Response)
          result[:status].should == Memcached::Errors::NO_ERROR
          result[:value].should == 'World'
          result[:cas].should_not == 0
          @old_cas = result[:cas]
          stop
        end
      end
    end

    it "should set a value" do
      run do
        Memcached.set(:key => 'Hello',
                :value => 'Planet') do |result|
          result.should be_kind_of(Memcached::Response)
          result[:status].should == Memcached::Errors::NO_ERROR
          result[:cas].should_not == 0
          result[:cas].should_not == @old_cas
          stop
        end
      end
    end

    it "should get a value" do
      run do
        Memcached.get(:key => 'Hello') do |result|
          result.should be_kind_of(Memcached::Response)
          result[:status].should == Memcached::Errors::NO_ERROR
          result[:value].should == 'Planet'
          result[:cas].should_not == @old_cas
          stop
        end
      end
    end

    it "should delete a value" do
      run do
        Memcached.delete(:key => 'Hello') do |result|
          result.should be_kind_of(Memcached::Response)
          result[:status].should == Memcached::Errors::NO_ERROR
          stop
        end
      end
    end

    it "should not get a value" do
      run do
        Memcached.get(:key => 'Hello') do |result|
          result.should be_kind_of(Memcached::Response)
          result[:status].should == Memcached::Errors::KEY_NOT_FOUND
          stop
        end
      end
    end

    $n = 100
    context "when incrementing a counter #{$n} times" do
      it "should initialize the counter" do
        run do
          Memcached.set(:key => 'counter',
                  :value => '0') do |result|
            stop
          end
        end
      end

      it "should count #{$n} times" do
        @counted = 0
        def count
          Memcached.get(:key => 'counter') do |result|
            result[:status].should == Memcached::Errors::NO_ERROR
            value = result[:value].to_i
            Memcached.set(:key => 'counter',
                    :value => (value + 1).to_s,
                    :cas => result[:cas]) do |result|
              if result[:status] == Memcached::Errors::KEY_EXISTS
                count # again
              else
                result[:status].should == Memcached::Errors::NO_ERROR
                @counted += 1
                stop if @counted >= $n
              end
            end
          end
        end
        run do
          $n.times { count }
        end
      end

      it "should have counted up to #{$n}" do
        run do
          Memcached.get(:key => 'counter') do |result|
            result[:status].should == Memcached::Errors::NO_ERROR
            result[:value].to_i.should == $n
            stop
          end
        end
      end
    end
  end

  context "when using multiple servers" do
    it "should not return the same hash for the succeeding key" do
      run do
        Memcached.hash_key('0').should_not == Memcached.hash_key('1')
        stop
      end
    end

    it "should not return the same client for the succeeding key" do
      run do
        # wait for 2nd client to be connected
        EM::Timer.new(0.1) do
          Memcached.client_for_key('0').should_not == Memcached.client_for_key('1')
          stop
        end
      end
    end

    it "should spread load (observe from outside :-)" do
      run do

        n = 10000
        replies = 0
        n.times do |i|
          Memcached.set(:key => "#{i % 100}",
                        :value => rand(1 << 31).to_s) {
            replies += 1
            stop if replies >= n
          }
        end
      end

    end
  end

  context "when manipulating multiple records at once" do
    before :all do
      @n = 10
    end

    def key(n)
      "test:item:#{n}"
    end

    it "should add some items" do
      run do
        items = []
        @n.times { |i|
          items << { :key => key(i),
                     :value => 'Foo',
                     :expiration => 20 } if i % 2 ==  0
        }
        Memcached.multi_add(items) { |responses|
          stop
          @n.times { |i|
            if i % 2 == 0 && (response_i = responses[key(i)])
              response_i[:status].should == Memcached::Errors::NO_ERROR
            end
          }
        }
      end
    end

    it "should get all items" do
      run do
        items = []
        @n.times { |i|
          items << { :key => key(i) }
        }
        Memcached.multi_get(items) { |responses|
          stop
          @n.times { |i|
            if i % 2 == 0
              responses.should have_key(key(i))
              responses[key(i)][:status].should == Memcached::Errors::NO_ERROR
              responses[key(i)][:value].should == 'Foo'
            else
              # either no response because request was quiet, or not
              # found in case of last response
              if (response_i = responses[key(i)])
                response_i[:status].should == Memcached::Errors::KEY_NOT_FOUND
              end
            end
          }
        }
      end
    end

    it "should delete all items" do
      run do
        items = []
        @n.times { |i|
          items << { :key => key(i) }
        }
        Memcached.multi_delete(items) { |responses|
          stop
          @n.times { |i|
            if i % 2 == 0
              # either no response because request was quiet, or ok in
              # case of last response
              if (response_i = responses[key(i)])
                response_i[:status].should == Memcached::Errors::NO_ERROR
              end
            else
              responses[key(i)][:status].should == Memcached::Errors::KEY_NOT_FOUND
            end
          }
        }
      end
    end

    context "when the multi operation is empty" do
      it "should return immediately" do
        @results = []
        @calls = 0
        Memcached.multi_add([]) { |responses|
          @results += responses
          @calls += 1
        }
        @results.should be_empty
        @calls.should == 1
      end
    end

    context "when servers are disconnected" do
      include EM::SpecHelper

      it "should return immediately with response" do
        em(1) do
          keys = [ { :key => key(0) } ]
          Memcached.multi_get(keys) { |responses|
            responses[key(0)][:status].should == Memcached::Errors::DISCONNECTED
            stop
          }
        end
      end
    end
  end

end
