
require File.expand_path(File.dirname(__FILE__) + '/spec_helper')

require 'redis/objects'
Redis::Objects.redis = REDIS_HANDLE

class Roster
  include Redis::Objects
  counter :available_slots, :start => 10
  counter :pitchers, :limit => :max_pitchers
  counter :basic
  hash_key :contact_information, :marshal_keys=>{'updated_at'=>true}
  redis_lock :resort, :timeout => 2
  value :starting_pitcher, :marshal => true
  list :player_stats, :marshal => true
  set :outfielders, :marshal => true
  sorted_set :rank
  redis_lock :per_field

  # global class counters
  counter :total_players_online, :global => true
  set :all_players_online, :global => true
  value :last_player, :global => true
  redis_lock :nasty_global_mutex, :global => true  # remember it appends "_lock"
  sorted_set :global_player_leaderboard, :global => true
  hash_key :global_player_online_status, :global => true

  # custom keys
  counter :player_totals, :key => 'players/#{username}/total'
  list :all_player_stats, :key => 'players:all_stats', :global => true
  set :total_wins, :key => 'players:#{id}:all_stats'
  value :my_rank, :key => 'players:my_rank:#{username}'

  # now support class interpolation as well. not sure why not previously
  def self.jimmyhat; 350; end
  value :weird_key, :key => 'players:weird_key:#{jimmyhat}', :global => true

  #callable as key
  counter :daily, :global => true, :key => Proc.new { |roster| "#{roster.name}:#{Time.now.strftime('%Y-%m-%dT%H')}:daily" }

  # set default expiration
  value :value_with_expiration, :expiration => 10
  value :value_with_expireat, :expireat => Time.now + 10.seconds
  set :set_with_expiration, :expiration => 10
  set :set_with_expireat, :expireat => Time.now + 10.seconds
  list :list_with_expiration, :expiration => 10
  list :list_with_expireat, :expireat => Time.now + 10.seconds
  hash_key :hash_with_expiration, :expiration => 10
  hash_key :hash_with_expireat, :expireat => Time.now + 10.seconds
  counter :counter_with_expiration, :expiration => 10
  counter :counter_with_expireat, :expireat => Time.now + 10.seconds
  sorted_set :sorted_set_with_expiration,:expiration => 10
  sorted_set :sorted_set_with_expireat, :expireat => Time.now + 10.seconds

  value :value_with_expireat_with_lambda, :expireat => lambda { Time.now + 10.seconds }

  def initialize(id=1) @id = id end
  def id; @id; end
  def username; "user#{id}"; end
  def max_pitchers; 3; end
end

class VanillaRoster < Roster
  # No explicit Redis::Objects
end

class CustomRoster < Roster
  include Redis::Objects

  counter :basic # Override
  counter :special # New
end

class MethodRoster
  def increment(attribute, by=1)
    42
  end

  def initialize(id=1) @id = id end
  def id; @id; end
end

class CustomMethodRoster < MethodRoster
  include Redis::Objects

  attr_accessor :counter
  counter :basic
end

class UidRoster < Roster
  attr_accessor :uid
  def initialize(uid=123) @uid = uid end
end

class CustomIdFieldRoster < UidRoster
  redis_id_field :uid
  include Redis::Objects
  counter :basic
end

describe Redis::Objects do
  before do
    @roster  = Roster.new
    @roster2 = Roster.new

    @roster_1 = Roster.new(1)
    @roster_2 = Roster.new(2)
    @roster_3 = Roster.new(3)

    @vanilla_roster = VanillaRoster.new
    @custom_roster  = CustomRoster.new

    @roster.available_slots.reset
    @roster.pitchers.reset
    @roster.basic.reset
    @roster.resort_lock.clear
    @roster.starting_pitcher.delete
    @roster.player_stats.clear
    @roster.outfielders.clear
    @roster.contact_information.clear
    @roster_1.outfielders.clear
    @roster_2.outfielders.clear
    @roster_3.outfielders.clear
    @roster.redis.del(UNIONSTORE_KEY)
    @roster.redis.del(INTERSTORE_KEY)
    @roster.redis.del(DIFFSTORE_KEY)

    Roster.total_players_online.reset
    Roster.all_player_stats.clear
    Roster.all_players_online.clear
    Roster.last_player.delete
    Roster.weird_key.clear

    @roster.player_totals.clear
    @roster.all_player_stats.clear
    @roster.total_wins.clear
    @roster.my_rank.clear

    @roster.daily.clear

    @custom_roster.basic.reset
    @custom_roster.special.reset
  end

  it "should provide a connection method" do
    Roster.redis.should == Redis::Objects.redis
    # Roster.redis.should.be.kind_of(Redis)
  end

  it "should support interpolation of key names" do
    @roster.player_totals.incr
    @roster.redis.get('players/user1/total').should == '1'
    @roster.redis.get('players/#{username}/total').should.be.nil
    @roster.all_player_stats << 'a'
    @roster.redis.lindex('players:all_stats', 0).should == 'a'
    @roster.total_wins << 'a'
    # test for interpolation of key names
    @roster.redis.smembers('players:#{id}:all_stats').should == []
    @roster.redis.smembers('players:1:all_stats').should == ['a']
    @roster.my_rank = 'a'
    @roster.redis.get('players:my_rank:user1').should == 'a'
    Roster.weird_key = 'tuka'
    Roster.redis.get("players:weird_key:#{Roster.jimmyhat}").should == 'tuka'

    k = "Roster:#{Time.now.strftime('%Y-%m-%dT%H')}:daily"
    @roster.daily.incr
    @roster.redis.get(k).should == '1'
  end

  it "should be able to directly assign value of hash" do
    @roster.contact_information['John_Name'] = 'John Doe'
    @roster.contact_information = { 'John_Phone' => '12345678', 'John_Address' => '321 LANE' }
    @roster.contact_information['John_Phone'].should == '12345678'
    @roster.contact_information['John_Address'].should == '321 LANE'
    @roster.contact_information['John_Name'].should.be.nil
  end

  it "should be able to get/set contact info" do
    @roster.contact_information['John_Phone'] = '123415352'
    @roster.contact_information['John_Address'] = '123 LANE'
    @roster.contact_information['John_Phone'].should == '123415352'
    @roster.contact_information['John_Address'].should == '123 LANE'
    @roster.contact_information['asdasd'].should.be.nil
    @roster.contact_information.size.should == 2
  end

  it 'should be able to expire keys and then persist them' do
    # on a hash_key
    @roster.contact_information['Jenny_Phone'] = '8675309'
    @roster.contact_information.expire 30
    @roster.contact_information.ttl.should > -1
    @roster.contact_information.ttl.should <= 30
    @roster.contact_information.persist
    @roster.contact_information.ttl.should == -1
    @roster.contact_information['Jenny_Phone'].should == '8675309'

    # on a value
    @roster.my_rank = 42
    @roster.my_rank.expire 30
    @roster.my_rank.ttl.should > -1
    @roster.my_rank.ttl.should <= 30
    @roster.my_rank.persist
    @roster.my_rank.ttl.should == -1
    @roster.my_rank.to_i.should == 42
  end

  it "should be marshalling hash keys" do
    @roster.contact_information['updated_at'] = Time.now
    @roster.contact_information['updated_at'].class.should == Time
  end

  it "should create counter accessors" do
    [:available_slots, :pitchers, :basic].each do |m|
       @roster.respond_to?(m).should == true
     end
  end

  it "should handle obtaining / clearing locks" do
    # class-level calls for Admin reasons
    # this is a really weird API and I wonder if anyone actually uses it
    id = 88
    roster = Roster.new(id)
    roster.per_field_lock.exists?.should == false
    Roster.obtain_lock(:per_field, id) do
      roster.per_field_lock.exists?.should == true
    end
    roster.per_field_lock.exists?.should == false

    Roster.clear_lock(:per_field, id)
    roster.per_field_lock.exists?.should == false
  end

  it "should support increment/decrement of counters" do
    @roster.available_slots.key.should == 'roster:1:available_slots'
    @roster.available_slots.should == 10

    # math proxy ops
    (@roster.available_slots == 10).should.be.true
    (@roster.available_slots <= 10).should.be.true
    (@roster.available_slots < 11).should.be.true
    (@roster.available_slots > 9).should.be.true
    (@roster.available_slots >= 10).should.be.true
    "#{@roster.available_slots}".should == "10"

    @roster.available_slots.increment.should == 11
    @roster.available_slots.increment.should == 12
    @roster2.available_slots.increment.should == 13
    @roster2.available_slots.increment(2).should == 15
    @roster.available_slots.decrement.should == 14
    @roster2.available_slots.decrement.should == 13
    @roster.available_slots.decrement.should == 12
    @roster2.available_slots.decrement(4).should == 8
    @roster.available_slots.should == 8
    @roster.available_slots.reset.should.be.true
    @roster.available_slots.should == 10
    @roster.available_slots.reset(15).should.be.true
    @roster.available_slots.should == 15
    @roster.pitchers.increment.should == 1
    @roster.basic.increment.should == 1
    @roster2.basic.decrement.should == 0
    @roster.basic.get.should == 0
  end

  it "should support class-level increment/decrement of counters" do
    Roster.get_counter(:available_slots, @roster.id).should == 10
    Roster.increment_counter(:available_slots, @roster.id).should == 11
    Roster.increment_counter(:available_slots, @roster.id, 3).should == 14
    Roster.decrement_counter(:available_slots, @roster.id, 2).should == 12
    Roster.decrement_counter(:available_slots, @roster.id).should == 11
    Roster.reset_counter(:available_slots, @roster.id).should == true
    Roster.get_counter(:available_slots, @roster.id).should == 10
    Roster.getset_counter(:available_slots, @roster.id, 555).should == 10
    Roster.get_counter(:available_slots, @roster.id).should == 555
  end

  it "should support class-level increment/decrement of global counters" do
    Roster.total_players_online.should == 0
    Roster.total_players_online.increment.should == 1
    Roster.total_players_online.decrement.should == 0
    Roster.total_players_online.increment(3).should == 3
    Roster.total_players_online.decrement(2).should == 1
    Roster.total_players_online.reset.should.be.true
    Roster.total_players_online.should == 0

    Roster.get_counter(:total_players_online).should == 0
    Roster.increment_counter(:total_players_online).should == 1
    Roster.increment_counter(:total_players_online, nil, 3).should == 4
    Roster.decrement_counter(:total_players_online, nil, 2).should == 2
    Roster.decrement_counter(:total_players_online).should == 1
    Roster.reset_counter(:total_players_online).should == true
    Roster.get_counter(:total_players_online).should == 0
    Roster.getset_counter(:total_players_online, nil, 111).should == 0
    Roster.get_counter(:total_players_online).should == 111
  end

  it "should support class-level manipulation of global objects" do
    Roster.nasty_global_mutex_lock.exists?.should == false
    Roster.nasty_global_mutex_lock do
      Roster.nasty_global_mutex_lock.exists?.should == true
    end
    Roster.nasty_global_mutex_lock.exists?.should == false

    Roster.global_player_leaderboard.exists?.should == false
    Roster.global_player_leaderboard.add('nate', 22)
    Roster.global_player_leaderboard.add('jim', 11)
    Roster.global_player_leaderboard.rank('nate').should == 1 # 0-based

    Roster.global_player_online_status.exists?.should == false
    Roster.global_player_online_status['nate'] = 'online'
    Roster.global_player_online_status['jeff'] = 'offline'
    Roster.global_player_online_status['nate'].should == 'online'
    Roster.global_player_online_status['jeff'].should == 'offline'
    Roster.global_player_online_status['bobby'].should.be.nil
  end

  it "should take an atomic block for increment/decrement" do
    a = false
    @roster.available_slots.should == 10
    @roster.available_slots.decr do |cnt|
      if cnt >= 0
        a = true
      end
    end
    @roster.available_slots.should == 9
    a.should.be.true

    @roster.available_slots.should == 9
    @roster.available_slots.decr do |cnt|
      @roster.available_slots.should == 8
      false
    end
    @roster.available_slots.should == 8

    @roster.available_slots.should == 8
    @roster.available_slots.decr do |cnt|
      @roster.available_slots.should == 7
      nil  # should rewind
    end
    @roster.available_slots.should == 8

    @roster.available_slots.should == 8
    @roster.available_slots.decr(4) do |cnt|
      @roster.available_slots.should == 4
      nil  # should rewind
    end
    @roster.available_slots.should == 8

    @roster.available_slots.should == 8
    @roster.available_slots.incr do |cnt|
      if 1 == 2  # should rewind
        true
      end
    end
    @roster.available_slots.should == 8

    @roster.available_slots.should == 8
    @roster.available_slots.incr(5) do |cnt|
      if 1 == 2  # should rewind
        true
      end
    end
    @roster.available_slots.should == 8

    @roster.available_slots.should == 8
    @roster.available_slots.incr do |cnt|
      @roster.available_slots.should == 9
      []
    end
    @roster.available_slots.should == 9

    @roster.available_slots.should == 9
    begin
      @roster.available_slots.decr do |cnt|
        @roster.available_slots.should == 8
        raise 'oops'
      end
    rescue
    end
    @roster.available_slots.should == 9

    # check return value from the block
    value =
      @roster.available_slots.decr do |cnt|
        @roster.available_slots.should == 8
        42
      end
    value.should == 42
    @roster.available_slots.should == 8
  end

  it "should take an atomic block for increment/decrement class methods" do
    a = false
    Roster.get_counter(:available_slots, @roster.id).should == 10
    Roster.decrement_counter(:available_slots, @roster.id) do |cnt|
      if cnt >= 0
        a = true
      end
    end
    Roster.get_counter(:available_slots, @roster.id).should == 9
    a.should.be.true

    Roster.get_counter(:available_slots, @roster.id).should == 9
    Roster.decrement_counter(:available_slots, @roster.id) do |cnt|
      Roster.get_counter(:available_slots, @roster.id).should == 8
      false
    end
    Roster.get_counter(:available_slots, @roster.id).should == 8

    Roster.get_counter(:available_slots, @roster.id).should == 8
    Roster.decrement_counter(:available_slots, @roster.id) do |cnt|
      Roster.get_counter(:available_slots, @roster.id).should == 7
      nil  # should rewind
    end
    Roster.get_counter(:available_slots, @roster.id).should == 8

    Roster.get_counter(:available_slots, @roster.id).should == 8
    Roster.decrement_counter(:available_slots, @roster.id, 4) do |cnt|
      Roster.get_counter(:available_slots, @roster.id).should == 4
      nil  # should rewind
    end
    Roster.get_counter(:available_slots, @roster.id).should == 8

    Roster.get_counter(:available_slots, @roster.id).should == 8
    Roster.increment_counter(:available_slots, @roster.id) do |cnt|
      if 1 == 2  # should rewind
        true
      end
    end
    Roster.get_counter(:available_slots, @roster.id).should == 8

    Roster.get_counter(:available_slots, @roster.id).should == 8
    Roster.increment_counter(:available_slots, @roster.id, 4) do |cnt|
      if 1 == 2  # should rewind
        true
      end
    end
    Roster.get_counter(:available_slots, @roster.id).should == 8

    Roster.get_counter(:available_slots, @roster.id).should == 8
    Roster.increment_counter(:available_slots, @roster.id) do |cnt|
      Roster.get_counter(:available_slots, @roster.id).should == 9
      []
    end
    Roster.get_counter(:available_slots, @roster.id).should == 9

    Roster.get_counter(:available_slots, @roster.id).should == 9
    begin
      Roster.decrement_counter(:available_slots, @roster.id) do |cnt|
        Roster.get_counter(:available_slots, @roster.id).should == 8
        raise 'oops'
      end
    rescue
    end
    Roster.get_counter(:available_slots, @roster.id).should == 9

    # check return value from the block
    value =
      Roster.decrement_counter(:available_slots, @roster.id) do |cnt|
        Roster.get_counter(:available_slots, @roster.id).should == 8
        42
      end
    value.should == 42
    Roster.get_counter(:available_slots, @roster.id).should == 8
  end

  it "should properly throw errors on bad counters" do
    error = nil
    begin
      Roster.increment_counter(:badness, 2)
    rescue => error
    end
    error.should.be.kind_of(NoMethodError)

    error = nil
    begin
      Roster.increment_counter(:available_slots, nil)
    rescue => error
    end
    error.should.be.kind_of(Redis::Objects::MissingID)

    error = nil
    begin
      Roster.obtain_lock(:badness, 2){}
    rescue => error
    end
    error.should.be.kind_of(Redis::Objects::UndefinedLock)

    error = nil
    begin
      @roster.available_slots = 42
    rescue => error
    end
    error.should.be.kind_of(NoMethodError)

    error = nil
    begin
      @roster.available_slots += 69
    rescue => error
    end
    error.should.be.kind_of(NoMethodError)

    error = nil
    begin
      @roster.available_slots -= 15
    rescue => error
    end
    error.should.be.kind_of(NoMethodError)
  end

  it "should support obtain_lock as a class method" do
    error = nil
    begin
      Roster.obtain_lock(:resort, 2) do
        Roster.redis.get("roster:2:resort_lock").should.not.be.nil
      end
    rescue => error
    end

    error.should.be.nil
    Roster.redis.get("roster:2:resort_lock").should.be.nil
  end

  it "should handle simple values" do
    @roster.starting_pitcher.should == nil
    @roster.starting_pitcher = 'Trevor Hoffman'
    @roster.starting_pitcher.should == 'Trevor Hoffman'
    @roster.starting_pitcher.get.should == 'Trevor Hoffman'
    @roster.starting_pitcher = 'Tom Selleck'
    @roster.starting_pitcher.should == 'Tom Selleck'
    @roster.starting_pitcher.del.should == 1
    @roster.starting_pitcher.should.be.nil
  end

  it "should handle complex marshaled values" do
    @roster.starting_pitcher.should == nil
    @roster.starting_pitcher = {:json => 'data'}
    @roster.starting_pitcher.should == {:json => 'data'}
    @roster.starting_pitcher.get.should == {:json => 'data'}
    @roster.starting_pitcher.del.should == 1
    @roster.starting_pitcher.should.be.nil
  end

  it "should be able to directly assign value of list" do
    @roster.player_stats << 'c'
    @roster.player_stats = ['a', 'b']
    @roster.player_stats.get.should == ['a', 'b']
  end

  it "should handle lists of simple values" do
    @roster.player_stats.should.be.empty
    @roster.player_stats << 'a'
    @roster.player_stats.should == ['a']
    @roster.player_stats.get.should == ['a']
    @roster.player_stats.unshift 'b'
    @roster.player_stats.to_s.should == 'b, a'
    @roster.player_stats.should == ['b','a']
    @roster.player_stats.get.should == ['b','a']
    @roster.player_stats.push 'c'
    @roster.player_stats.should == ['b','a','c']
    @roster.player_stats.get.should == ['b','a','c']
    @roster.player_stats.first.should == 'b'
    @roster.player_stats.last.should == 'c'
    @roster.player_stats << 'd'
    @roster.player_stats.should == ['b','a','c','d']
    @roster.player_stats[1].should == 'a'
    @roster.player_stats[0].should == 'b'
    @roster.player_stats[2].should == 'c'
    @roster.player_stats[3].should == 'd'
    @roster.player_stats.include?('c').should.be.true
    @roster.player_stats.include?('no').should.be.false
    @roster.player_stats.pop.should == 'd'
    @roster.player_stats[0].should == @roster.player_stats.at(0)
    @roster.player_stats[1].should == @roster.player_stats.at(1)
    @roster.player_stats[2].should == @roster.player_stats.at(2)
    @roster.player_stats.should == ['b','a','c']
    @roster.player_stats.get.should == ['b','a','c']
    @roster.player_stats.shift.should == 'b'
    @roster.player_stats.should == ['a','c']
    @roster.player_stats.get.should == ['a','c']
    @roster.player_stats << 'e' << 'f' << 'e'
    @roster.player_stats.should == ['a','c','e','f','e']
    @roster.player_stats.get.should == ['a','c','e','f','e']
    @roster.player_stats.delete('e').should == 2
    @roster.player_stats.should == ['a','c','f']
    @roster.player_stats.get.should == ['a','c','f']
    @roster.player_stats << 'j'
    @roster.player_stats.should == ['a','c','f','j']
    @roster.player_stats[0..2].should == ['a','c','f']
    @roster.player_stats[1, 3].should == ['c','f','j']
    @roster.player_stats.length.should == 4
    @roster.player_stats.size.should == 4
    @roster.player_stats.should == ['a','c','f','j']
    @roster.player_stats.get.should == ['a','c','f','j']
    @roster.player_stats.push *['h','i']
    @roster.player_stats.should == ['a','c','f','j','h','i']
    @roster.player_stats.get.should == ['a','c','f','j','h','i']

    i = -1
    @roster.player_stats.each do |st|
      st.should == @roster.player_stats[i += 1]
    end
    @roster.player_stats.should == ['a','c','f','j','h','i']
    @roster.player_stats.get.should == ['a','c','f','j','h','i']

    @roster.player_stats.each_with_index do |st,i|
      st.should == @roster.player_stats[i]
    end
    @roster.player_stats.should == ['a','c','f','j','h','i']
    @roster.player_stats.get.should == ['a','c','f','j','h','i']

    coll = @roster.player_stats.collect{|st| st}
    coll.should == ['a','c','f','j','h','i']
    @roster.player_stats.should == ['a','c','f','j','h','i']
    @roster.player_stats.get.should == ['a','c','f','j','h','i']

    @roster.player_stats << 'a'
    coll = @roster.player_stats.select{|st| st == 'a'}
    coll.should == ['a','a']
    @roster.player_stats.should == ['a','c','f','j','h','i','a']
    @roster.player_stats.get.should == ['a','c','f','j','h','i','a']
  end

  it "should be able to directly assign values of set" do
    @roster.outfielders << 'c'
    @roster.outfielders = ['a', 'b']
    @roster.outfielders.member?('a').should.be.true
    @roster.outfielders.member?('b').should.be.true
    @roster.outfielders.member?('c').should.be.false
  end

  it "should handle sets of simple values" do
    @roster.outfielders.should.be.empty
    @roster.outfielders << 'a' << 'a' << 'a'
    @roster.outfielders.should == ['a']
    @roster.outfielders.get.should == ['a']
    @roster.outfielders << 'b' << 'b'
    @roster.outfielders.sort.should == ['a','b']
    @roster.outfielders.members.sort.should == ['a','b']
    @roster.outfielders.get.sort.should == ['a','b']
    @roster.outfielders << 'c'
    @roster.outfielders.sort.should == ['a','b','c']
    @roster.outfielders.get.sort.should == ['a','b','c']
    @roster.outfielders.delete('c')
    @roster.outfielders.sort.should == ['a','b']
    @roster.outfielders.get.sort.should == ['a','b']
    @roster.outfielders.length.should == 2
    @roster.outfielders.size.should == 2

    i = 0
    @roster.outfielders.each do |st|
      i += 1
    end
    i.should == @roster.outfielders.length

    coll = @roster.outfielders.collect{|st| st}
    coll.sort.should == ['a','b']
    @roster.outfielders.sort.should == ['a','b']
    @roster.outfielders.get.sort.should == ['a','b']

    @roster.outfielders << 'c'
    @roster.outfielders.member?('c').should.be.true
    @roster.outfielders.include?('c').should.be.true
    @roster.outfielders.member?('no').should.be.false
    coll = @roster.outfielders.select{|st| st == 'c'}
    coll.should == ['c']
    @roster.outfielders.sort.should == ['a','b','c']
  end

  it "should handle set intersections and unions" do
    @roster_1.outfielders << 'a' << 'b' << 'c' << 'd' << 'e'
    @roster_2.outfielders << 'c' << 'd' << 'e' << 'f' << 'g'
    @roster_3.outfielders << 'a' << 'd' << 'g' << 'l' << 'm'
    @roster_1.outfielders.sort.should == %w(a b c d e)
    @roster_2.outfielders.sort.should == %w(c d e f g)
    @roster_3.outfielders.sort.should == %w(a d g l m)
    (@roster_1.outfielders & @roster_2.outfielders).sort.should == ['c','d','e']
    @roster_1.outfielders.intersection(@roster_2.outfielders).sort.should == ['c','d','e']
    @roster_1.outfielders.intersection(@roster_2.outfielders, @roster_3.outfielders).sort.should == ['d']
    @roster_1.outfielders.intersect(@roster_2.outfielders).sort.should == ['c','d','e']
    @roster_1.outfielders.inter(@roster_2.outfielders, @roster_3.outfielders).sort.should == ['d']

    @roster_1.outfielders.interstore(INTERSTORE_KEY, @roster_2.outfielders).should == 3
    @roster_1.redis.smembers(INTERSTORE_KEY).sort.map{|v| Marshal.restore(v)}.should == ['c','d','e']

    @roster_1.outfielders.interstore(INTERSTORE_KEY, @roster_2.outfielders, @roster_3.outfielders).should == 1
    @roster_1.redis.smembers(INTERSTORE_KEY).sort.map{|v| Marshal.restore(v)}.should == ['d']

    (@roster_1.outfielders | @roster_2.outfielders).sort.should == ['a','b','c','d','e','f','g']
    (@roster_1.outfielders + @roster_2.outfielders).sort.should == ['a','b','c','d','e','f','g']
    @roster_1.outfielders.union(@roster_2.outfielders).sort.should == ['a','b','c','d','e','f','g']
    @roster_1.outfielders.union(@roster_2.outfielders, @roster_3.outfielders).sort.should == ['a','b','c','d','e','f','g','l','m']

    @roster_1.outfielders.unionstore(UNIONSTORE_KEY, @roster_2.outfielders).should == 7
    @roster_1.redis.smembers(UNIONSTORE_KEY).map{|v| Marshal.restore(v)}.sort.should == ['a','b','c','d','e','f','g']

    @roster_1.outfielders.unionstore(UNIONSTORE_KEY, @roster_2.outfielders, @roster_3.outfielders).should == 9
    @roster_1.redis.smembers(UNIONSTORE_KEY).map{|v| Marshal.restore(v)}.sort.should == ['a','b','c','d','e','f','g','l','m']
  end

  it "should handle class-level global lists of simple values" do
    Roster.all_player_stats.should.be.empty
    Roster.all_player_stats << 'a'
    Roster.all_player_stats.should == ['a']
    Roster.all_player_stats.get.should == ['a']
    Roster.all_player_stats.unshift 'b'
    Roster.all_player_stats.to_s.should == 'b, a'
    Roster.all_player_stats.should == ['b','a']
    Roster.all_player_stats.get.should == ['b','a']
    Roster.all_player_stats.push 'c'
    Roster.all_player_stats.should == ['b','a','c']
    Roster.all_player_stats.get.should == ['b','a','c']
    Roster.all_player_stats.first.should == 'b'
    Roster.all_player_stats.last.should == 'c'
    Roster.all_player_stats << 'd'
    Roster.all_player_stats.should == ['b','a','c','d']
    Roster.all_player_stats[1].should == 'a'
    Roster.all_player_stats[0].should == 'b'
    Roster.all_player_stats[2].should == 'c'
    Roster.all_player_stats[3].should == 'd'
    Roster.all_player_stats.include?('c').should.be.true
    Roster.all_player_stats.include?('no').should.be.false
    Roster.all_player_stats.pop.should == 'd'
    Roster.all_player_stats[0].should == Roster.all_player_stats.at(0)
    Roster.all_player_stats[1].should == Roster.all_player_stats.at(1)
    Roster.all_player_stats[2].should == Roster.all_player_stats.at(2)
    Roster.all_player_stats.should == ['b','a','c']
    Roster.all_player_stats.get.should == ['b','a','c']
    Roster.all_player_stats.shift.should == 'b'
    Roster.all_player_stats.should == ['a','c']
    Roster.all_player_stats.get.should == ['a','c']
    Roster.all_player_stats << 'e' << 'f' << 'e'
    Roster.all_player_stats.should == ['a','c','e','f','e']
    Roster.all_player_stats.get.should == ['a','c','e','f','e']
    Roster.all_player_stats.delete('e').should == 2
    Roster.all_player_stats.should == ['a','c','f']
    Roster.all_player_stats.get.should == ['a','c','f']
    Roster.all_player_stats << 'j'
    Roster.all_player_stats.should == ['a','c','f','j']
    Roster.all_player_stats[0..2].should == ['a','c','f']
    Roster.all_player_stats[1, 3].should == ['c','f','j']
    Roster.all_player_stats.length.should == 4
    Roster.all_player_stats.size.should == 4
    Roster.all_player_stats.should == ['a','c','f','j']
    Roster.all_player_stats.get.should == ['a','c','f','j']

    i = -1
    Roster.all_player_stats.each do |st|
      st.should == Roster.all_player_stats[i += 1]
    end
    Roster.all_player_stats.should == ['a','c','f','j']
    Roster.all_player_stats.get.should == ['a','c','f','j']

    Roster.all_player_stats.each_with_index do |st,i|
      st.should == Roster.all_player_stats[i]
    end
    Roster.all_player_stats.should == ['a','c','f','j']
    Roster.all_player_stats.get.should == ['a','c','f','j']

    coll = Roster.all_player_stats.collect{|st| st}
    coll.should == ['a','c','f','j']
    Roster.all_player_stats.should == ['a','c','f','j']
    Roster.all_player_stats.get.should == ['a','c','f','j']

    Roster.all_player_stats << 'a'
    coll = Roster.all_player_stats.select{|st| st == 'a'}
    coll.should == ['a','a']
    Roster.all_player_stats.should == ['a','c','f','j','a']
    Roster.all_player_stats.get.should == ['a','c','f','j','a']
  end

  it "should handle class-level global sets of simple values" do
    Roster.all_players_online.should.be.empty
    Roster.all_players_online << 'a' << 'a' << 'a'
    Roster.all_players_online.should == ['a']
    Roster.all_players_online.get.should == ['a']
    Roster.all_players_online << 'b' << 'b'
    Roster.all_players_online.sort.should == ['a','b']
    Roster.all_players_online.members.sort.should == ['a','b']
    Roster.all_players_online.get.sort.should == ['a','b']
    Roster.all_players_online << 'c'
    Roster.all_players_online.sort.should == ['a','b','c']
    Roster.all_players_online.get.sort.should == ['a','b','c']
    Roster.all_players_online.delete('c')
    Roster.all_players_online.sort.should == ['a','b']
    Roster.all_players_online.get.sort.should == ['a','b']
    Roster.all_players_online.length.should == 2
    Roster.all_players_online.size.should == 2

    i = 0
    Roster.all_players_online.each do |st|
      i += 1
    end
    i.should == Roster.all_players_online.length

    coll = Roster.all_players_online.collect{|st| st}
    coll.sort.should == ['a','b']
    Roster.all_players_online.sort.should == ['a','b']
    Roster.all_players_online.get.sort.should == ['a','b']

    Roster.all_players_online << 'c'
    Roster.all_players_online.member?('c').should.be.true
    Roster.all_players_online.include?('c').should.be.true
    Roster.all_players_online.member?('no').should.be.false
    coll = Roster.all_players_online.select{|st| st == 'c'}
    coll.should == ['c']
    Roster.all_players_online.sort.should == ['a','b','c']
  end

  it "should handle class-level global values" do
    Roster.last_player.should == nil
    Roster.last_player = 'Trevor Hoffman'
    Roster.last_player.should == 'Trevor Hoffman'
    Roster.last_player.get.should == 'Trevor Hoffman'
    Roster.last_player = 'Tom Selleck'
    Roster.last_player.should == 'Tom Selleck'
    Roster.last_player.del.should == 1
    Roster.last_player.should.be.nil
  end

  it "should easily enable @object.class.global_objects" do
    @roster.class.all_players_online.should.be.empty
    @roster.class.all_players_online << 'a' << 'a' << 'a'
    @roster.class.all_players_online.should == ['a']
    @roster2.class.all_players_online.should == ['a']

    @roster.all_players_online.should == ['a']
    @roster2.all_players_online.should == ['a']

    @roster.class.all_player_stats.should.be.empty
    @roster.class.all_player_stats << 'a'
    @roster.class.all_player_stats.should == ['a']
    @roster.class.all_player_stats.get.should == ['a']
    @roster.class.all_player_stats.unshift 'b'
    @roster.class.all_player_stats.to_s.should == 'b, a'
    @roster.class.all_player_stats.should == ['b','a']
    @roster2.class.all_player_stats.should == ['b','a']

    @roster.all_player_stats.should == ['b','a']
    @roster2.all_player_stats.should == ['b','a']
    @roster2.all_player_stats << 'b'
    @roster.all_player_stats.should == ['b','a','b']

    @roster.last_player.should == nil
    @roster.class.last_player = 'Trevor Hoffman'
    @roster.last_player.should == 'Trevor Hoffman'
    @roster.last_player.get.should == 'Trevor Hoffman'
    @roster2.last_player.get.should == 'Trevor Hoffman'
    @roster2.last_player = 'Tom Selleck'
    @roster.last_player.should == 'Tom Selleck'
    @roster.last_player.del.should == 1
    @roster.last_player.should.be.nil
    @roster2.last_player.should.be.nil
  end

  it "should handle lists of complex data types" do
    @roster.player_stats << {:json => 'data'}
    @roster.player_stats << {:json2 => 'data2'}
    @roster.player_stats.first.should == {:json => 'data'}
    @roster.player_stats.last.should == {:json2 => 'data2'}
    @roster.player_stats << [1,2,3,[4,5]]
    @roster.player_stats.last.should == [1,2,3,[4,5]]
    @roster.player_stats.shift.should == {:json => 'data'}
  end

  it "should handle sets of complex data types" do
    @roster.outfielders << {:a => 1}
    @roster.outfielders.members.should == [{:a => 1}]
    @roster.outfielders << {:b => 2}
    @roster.outfielders.member?({:b => 2})
    @roster_1.outfielders << {:a => 1} << {:b => 2}
    @roster_2.outfielders << {:b => 2} << {:c => 3}
    (@roster_1.outfielders & @roster_2.outfielders).should == [{:b => 2}]
    #(@roster_1.outfielders | @roster_2.outfielders).members.should ==
  end

  it "should provide a lock method that accepts a block" do
    @roster.resort_lock.key.should == 'roster:1:resort_lock'
    a = false
    @roster.resort_lock.lock do
      a = true
    end
    a.should.be.true
  end

  it "should raise an exception if the timeout is exceeded" do
    @roster.redis.set(@roster.resort_lock.key, 1)
    error = nil
    begin
      @roster.resort_lock.lock {}
    rescue => error
    end
    error.should.not.be.nil
    error.should.be.kind_of(Redis::Lock::LockTimeout)
  end

  it "should pick up objects from superclass automatically" do
    @vanilla_roster.available_slots.should.be.kind_of(Redis::Counter)
    @vanilla_roster.pitchers.should.be.kind_of(Redis::Counter)
    @vanilla_roster.basic.should.be.kind_of(Redis::Counter)
    @vanilla_roster.resort_lock.should.be.kind_of(Redis::Lock)
    @vanilla_roster.starting_pitcher.should.be.kind_of(Redis::Value)
    @vanilla_roster.player_stats.should.be.kind_of(Redis::List)
    @vanilla_roster.outfielders.should.be.kind_of(Redis::Set)
    @vanilla_roster.rank.should.be.kind_of(Redis::SortedSet)

    # custom keys
    @vanilla_roster.player_totals.should.be.kind_of(Redis::Counter)
    @vanilla_roster.all_player_stats.should.be.kind_of(Redis::List)
    @vanilla_roster.total_wins.should.be.kind_of(Redis::Set)
    @vanilla_roster.my_rank.should.be.kind_of(Redis::Value)
    @vanilla_roster.weird_key.should.be.kind_of(Redis::Value)

    # globals via class
    @vanilla_roster.total_players_online.should.be.kind_of(Redis::Counter)
    @vanilla_roster.all_player_stats.should.be.kind_of(Redis::List)
    @vanilla_roster.all_players_online.should.be.kind_of(Redis::Set)
    @vanilla_roster.last_player.should.be.kind_of(Redis::Value)

    VanillaRoster.total_players_online.should.be.kind_of(Redis::Counter)
    VanillaRoster.all_player_stats.should.be.kind_of(Redis::List)
    VanillaRoster.all_players_online.should.be.kind_of(Redis::Set)
    VanillaRoster.last_player.should.be.kind_of(Redis::Value)
  end

  it "should allow subclass overrides of the same redis object" do
    @roster.basic.should == 0
    @custom_roster.basic.increment.should == 1
    @roster2.basic.should == 0
    CustomRoster.new.basic.should == 1
    @custom_roster.basic.decrement.should == 0
  end

  it "should handle new subclass objects" do
    @custom_roster.special.increment.should == 1
  end

  it "should allow passing of increment/decrement to super class" do
    @custom_method_roster = CustomMethodRoster.new
    @custom_method_roster.counter.should.be.nil

    @custom_method_roster.increment(:counter).should == 42

    @custom_method_roster.increment(:basic).should == 1
    @custom_method_roster.basic.increment.should == 2
    @custom_method_roster.decrement(:basic).should == 1
    @custom_method_roster.basic.decrement.should == 0
    @custom_method_roster.basic.reset.should.be.true
    @custom_method_roster.basic.should == 0
    @custom_method_roster.basic.should.be.kind_of(Redis::Counter)
  end

  it "should respond to #to_json" do
    @roster = Roster.new
    @roster.player_totals.increment
    json = JSON.parse(@roster.player_totals.to_json)
    json['value'].should == 1
  end

  it "should persist object with custom id field name" do
    @custom_id_field_roster = CustomIdFieldRoster.new()
    @custom_id_field_roster.uid.should == 123 # sanity
    @custom_id_field_roster.increment(:basic).should == 1
    @custom_id_field_roster.basic.increment.should == 2
    @custom_id_field_roster.basic.reset
    @custom_id_field_roster.basic.should == 0
  end

  it "should pick up class methods from superclass automatically" do
    Roster.redis_prefix.should == 'roster'
    CounterRoster = Class.new(Roster)
    CounterRoster.redis_prefix.should == 'counter_roster'
    CounterRoster.counter :extended_counter
    extended_roster = CounterRoster.new
    Roster.redis_prefix.should == 'roster'
    extended_roster.class.redis_prefix.should == 'counter_roster'
    extended_roster.basic.should.be.kind_of(Redis::Counter)
    extended_roster.extended_counter.should.be.kind_of(Redis::Counter)
    @roster.respond_to?(:extended_counter).should == false

    HashKeyRoster = Class.new(Roster)
    HashKeyRoster.hash_key :extended_hash_key
    extended_roster = HashKeyRoster.new
    extended_roster.contact_information.should.be.kind_of(Redis::HashKey)
    extended_roster.extended_hash_key.should.be.kind_of(Redis::HashKey)
    @roster.respond_to?(:extended_hash_key).should == false

    LockRoster = Class.new(Roster)
    LockRoster.redis_lock :extended
    extended_roster = LockRoster.new
    extended_roster.resort_lock.should.be.kind_of(Redis::Lock)
    extended_roster.extended_lock.should.be.kind_of(Redis::Lock)
    @roster.respond_to?(:extended_lock).should == false

    ValueRoster = Class.new(Roster)
    ValueRoster.value :extended_value
    extended_roster = ValueRoster.new
    extended_roster.starting_pitcher.should.be.kind_of(Redis::Value)
    extended_roster.extended_value.should.be.kind_of(Redis::Value)
    @roster.respond_to?(:extended_value).should == false

    ListRoster = Class.new(Roster)
    ListRoster.list :extended_list
    extended_roster = ListRoster.new
    extended_roster.player_stats.should.be.kind_of(Redis::List)
    extended_roster.extended_list.should.be.kind_of(Redis::List)
    @roster.respond_to?(:extended_list).should == false

    SetRoster = Class.new(Roster)
    SetRoster.set :extended_set
    extended_roster = SetRoster.new
    extended_roster.outfielders.should.be.kind_of(Redis::Set)
    extended_roster.extended_set.should.be.kind_of(Redis::Set)
    @roster.respond_to?(:extended_set).should == false

    SortedSetRoster = Class.new(Roster)
    SortedSetRoster.sorted_set :extended_sorted_set
    extended_roster = SortedSetRoster.new
    extended_roster.rank.should.be.kind_of(Redis::SortedSet)
    extended_roster.extended_sorted_set.should.be.kind_of(Redis::SortedSet)
    @roster.respond_to?(:extended_sorted_set).should == false
  end

  it "should set time to live in seconds when expiration option assigned" do
    @roster.value_with_expiration.value = 'val'
    @roster.value_with_expiration.ttl.should > 0
    @roster.value_with_expiration.ttl.should <= 10

    @roster.set_with_expiration << 'val'
    @roster.set_with_expiration.ttl.should > 0
    @roster.set_with_expiration.ttl.should <= 10

    @roster.list_with_expiration << 'val'
    @roster.list_with_expiration.ttl.should > 0
    @roster.list_with_expiration.ttl.should <= 10

    @roster.hash_with_expiration[:foo] = :bar
    @roster.hash_with_expiration.ttl.should > 0
    @roster.hash_with_expiration.ttl.should <= 10

    @roster.counter_with_expiration.increment
    @roster.counter_with_expiration.ttl.should > 0
    @roster.counter_with_expiration.ttl.should <= 10

    @roster.sorted_set_with_expiration[:foo] = 1
    @roster.sorted_set_with_expiration.ttl.should > 0
    @roster.sorted_set_with_expiration.ttl.should <= 10
  end

  it "should set expiration when expireat option assigned" do
    @roster.value_with_expireat.value = 'val'
    @roster.value_with_expireat.ttl.should > 0
    @roster.value_with_expireat.ttl.should <= 10

    @roster.set_with_expireat << 'val'
    @roster.set_with_expireat.ttl.should > 0
    @roster.set_with_expireat.ttl.should <= 10

    @roster.list_with_expireat << 'val'
    @roster.list_with_expireat.ttl.should > 0
    @roster.list_with_expireat.ttl.should <= 10

    @roster.hash_with_expireat[:foo] = :bar
    @roster.hash_with_expireat.ttl.should > 0
    @roster.hash_with_expireat.ttl.should <= 10

    @roster.sorted_set_with_expireat[:foo] = 1
    @roster.sorted_set_with_expireat.ttl.should > 0
    @roster.sorted_set_with_expireat.ttl.should <= 10
  end

  it "should set expiration when expireat option assigned with lambda" do
    travel(1.minute) do
      # non-proc expireat is not affected by time travel
      @roster.value_with_expireat.value = 'val'
      @roster.value_with_expireat.ttl.should > 0
      @roster.value_with_expireat.ttl.should <= 10

      @roster.value_with_expireat_with_lambda.value = 'val'
      @roster.value_with_expireat_with_lambda.ttl.should > 60
      @roster.value_with_expireat_with_lambda.ttl.should <= 70
    end
  end

  it "should allow deleting the entire object" do
    (@roster.redis.keys & @roster.redis_instance_keys).count.should > 0
    @roster.redis_delete_objects.should > 0
    (@roster.redis.keys & @roster.redis_instance_keys).count.should == 0
  end

  it "should be able to return all instance keys" do
    @roster.redis_instance_keys.include?('roster:1:player_stats').should == true
    @roster.redis_instance_keys.include?('players:my_rank:user1').should == true
    @roster.redis_instance_keys.include?('roster:1:player_stats').should == true
    @roster.redis_instance_keys.include?('players:all_stats').should == false
  end

  it "should handle per-class connection handles" do
    redis = Roster.redis
    Roster.redis = redis
    Roster.redis.should == redis
  end
end
