require 'woyo/world/attributes'

describe Woyo::Attributes do

  before :all do
    class AttrTest
      include Woyo::Attributes
      attributes :attr1, :attr2, :attr3
    end
  end
  
  it 'names can be listed for class' do
    attrs = AttrTest.attributes
    attrs.should be_instance_of Array
    attrs.count.should eq 3
    attrs.all? { |a| a.is_a? Symbol }.should be_true
  end                       

  it 'hash of names and values can be retrieved for instance' do
    attr_test = AttrTest.new
    attr_test.attributes.should be_instance_of Woyo::Attributes::AttributesHash
    AttrTest.attributes.each do |attr|
      attr_test.send(attr, attr.to_s.upcase)
    end
    attr_test.attributes.keys.should eq [ :attr1, :attr2, :attr3 ]
    attr_test.attributes.values.should eq [ 'ATTR1', 'ATTR2', 'ATTR3' ]
  end

  it 'hash of names and nil values can be retrieved for instance before populating' do
    attr_test = AttrTest.new
    attr_test.attributes.should be_instance_of Woyo::Attributes::AttributesHash
    attr_test.attributes.keys.should eq [ :attr1, :attr2, :attr3 ]  
    attr_test.attributes.values.should eq [ nil, nil, nil ]
  end

  it 'hash of names and default values can be retrieved for instance before populating' do
    expect { 
      class DefTest
        include Woyo::Attributes
        attributes one: 1, two: 2, three: proc { 3 } 
      end
    }.to_not raise_error
    def_test = DefTest.new
    def_test.attributes.keys.should eq [ :one, :two, :three ]  
    def_test.attributes.values.should eq [ 1, 2, 3 ]
  end

  it 'have convenience accessor :names for :keys' do
    attr_test = AttrTest.new
    attr_test.attributes.names.should eq [ :attr1, :attr2, :attr3 ]  
  end

  it 'can be written via method with =' do
    attr_test = AttrTest.new
    AttrTest.attributes.each do |attr|
      attr_test.send("#{attr}=", attr.to_s.upcase)
    end
    attr_test.attributes.count.should eq AttrTest.attributes.count
    attr_test.attributes.each do |name,value|
      value.should eq name.to_s.upcase
    end
  end

  it 'can be written via method without =' do
    attr_test = AttrTest.new
    AttrTest.attributes.each do |attr|
      attr_test.send(attr, attr.to_s.upcase)
    end
    attr_test.attributes.count.should eq AttrTest.attributes.count
    attr_test.attributes.each do |name,value|
      value.should eq name.to_s.upcase
    end
  end

  it 'can be read via method' do
    attr_test = AttrTest.new
    AttrTest.attributes.each do |attr|
      attr_test.send(attr, attr.to_s.upcase)
    end
    attr_test.attributes.count.should eq AttrTest.attributes.count
    attr_test.attributes.each do |name,value|
      eval("attr_test.#{name}").should eq value
    end
  end

  it 'list can be added to' do
    expect {
      class AttrTest
        attributes :attr4, :attr5, :attr6
      end
    }.to_not raise_error
    AttrTest.attributes.count.should eq 6
    attr_test = AttrTest.new
    AttrTest.attributes.each do |attr|
      attr_test.send(attr, attr.to_s.upcase)
    end
    attr_test.attributes.count.should eq 6
  end

  it 'can be defined with "attribute"' do
    expect {
      class AttrTest
        attribute :open
      end
    }.to_not raise_error
    attr_test = AttrTest.new
    attr_test.open.should be_nil
    attr_test.open = true
    attr_test.open.should be_true
  end
      
  it 'can have a default value' do
    expect { 
      class AttrTest
        attributes attr_with_array___default: [ 1, 2, 3 ]
        attributes attr_with_hash____default: { a: 1, b: 2, c: 3 }
        attributes attr_with_number__default: 12345
        attributes attr_with_string__default: "abcde"
        attributes attr_with_boolean_default: true
      end
    }.to_not raise_error
    attr_test = AttrTest.new
    attr_test.attr_with_array___default.should eq [ 1, 2, 3 ]
    attr_test.attr_with_array___default = :array
    attr_test.attr_with_array___default.should eq :array
    attr_test.attr_with_hash____default.should eq ( { a: 1, b: 2, c: 3 } )
    attr_test.attr_with_hash____default = :hash
    attr_test.attr_with_hash____default.should eq :hash
    attr_test.attr_with_number__default.should eq 12345
    attr_test.attr_with_number__default = :number
    attr_test.attr_with_number__default.should eq :number
    attr_test.attr_with_string__default.should eq "abcde"
    attr_test.attr_with_string__default = :string
    attr_test.attr_with_string__default.should eq :string
    attr_test.attr_with_boolean_default.should eq true
    attr_test.attr_with_boolean_default = :boolean
    attr_test.attr_with_boolean_default.should eq :boolean
  end

  it 'can have a default proc' do
    expect {
      class AttrTest
        attributes attr_with_proc_default: proc { Time.now }
      end
    }.to_not raise_error
    attr_test = AttrTest.new
    attr_test.attr_with_proc_default.should be < Time.now
  end

  it 'default proc runs in instance scope' do
    expect {
      class AttrTest
        attributes attr_with_proc_default: proc { |this| this.my_method }
        def my_method
          "okay"
        end
      end
    }.to_not raise_error
    attr_test = AttrTest.new
    attr_test.attr_with_proc_default.should eq "okay"
  end

  it 'can have a default lambda' do
    expect {
      class AttrTest
        attributes attr_with_lambda_default: lambda { Time.now }
      end
    }.to_not raise_error
    attr_test = AttrTest.new
    attr_test.attr_with_lambda_default.should be < Time.now
  end

  it 'default lambda runs in instance scope' do
    expect {
      class AttrTest
        attributes attr_with_lambda_default: lambda { |this| this.my_method }
        def my_method
          "okay"
        end
      end
    }.to_not raise_error
    attr_test = AttrTest.new
    attr_test.attr_with_lambda_default.should eq "okay"
  end

  context 'that are boolean' do

    context 'have convenient instance accessors' do

      before :all do
        expect {
          class BooleanAttrTest
            include Woyo::Attributes
            attribute open: true
            attribute light: false 
          end
        }.to_not raise_error
      end

      it '#attr?' do
        attr_test = BooleanAttrTest.new
        attr_test.open.should eq true
        attr_test.open?.should eq true
      end

      it '#attr!' do
        attr_test = BooleanAttrTest.new
        attr_test.open = false
        attr_test.open.should eq false
        attr_test.open!.should eq true
        attr_test.open.should eq true
      end

      it '#is? :attr' do
        attr_test = BooleanAttrTest.new
        attr_test.is?(:open).should eq true
        attr_test.open = false
        attr_test.is?(:open).should eq false
      end

      it '#is :attr' do
        attr_test = BooleanAttrTest.new
        attr_test.light.should eq false
        attr_test.is(:light)
        attr_test.light.should eq true
      end
    end

    context 'have class accessor ::is' do

      # I was confused here, this may not be needed as a class method at all!!!
      
      it 'that defines new attribute with true default' do 
        expect {
          class BooleanIsTest1
            include Woyo::Attributes
            is :attr1
          end
        }.to_not raise_error
        attr_test = BooleanIsTest1.new
        attr_test.attr1.should eq true 
      end

      it 'that sets true default for existing attribute' do
        expect {
          class BooleanIsTest2
            include Woyo::Attributes
            attribute :attr2
            is :attr2
          end
        }.to_not raise_error
        attr_test = BooleanIsTest2.new
        attr_test.attr2.should eq true 
      end

      it 'that changes default to true for existing attribute' do
        expect {
          class BooleanIsTest3
            include Woyo::Attributes
            attribute :attr3 => false
            is :attr3
          end
        }.to_not raise_error
        attr_test = BooleanIsTest3.new
        attr_test.attr3.should eq true 
      end

      it 'that works for attribute in BooleanGroup' do
        expect {
          class BooleanIsTest4
            include Woyo::Attributes
            group! :temp, :hot, :warm, :cool, :cold
            # :hot is true by default
            is :cold
            # :cold is now true
          end
        }.to_not raise_error
        attr_test = BooleanIsTest4.new
        attr_test.hot.should eq false
        attr_test.warm.should eq false
        attr_test.cool.should eq false
        attr_test.cold.should eq true
      end

    end

  end

  context 'can be re-defined' do

    it 'without duplication' do
      expect {
        class AttrReDef1
          include Woyo::Attributes
          attribute :attr1
        end
        class AttrReDef1
          attribute :attr1
        end
      }.to_not raise_error
      AttrReDef1.attributes.count.should eq 1
      AttrReDef1.attributes.should eq [:attr1] 
      attr_rd1 = AttrReDef1.new
      attr_rd1.attr1.should be_nil
    end

    it 'to set default' do
      expect {
        class AttrReDef2
          include Woyo::Attributes
          attribute :attr2
        end
        class AttrReDef2
          attribute attr2: 'two'
        end
      }.to_not raise_error
      AttrReDef2.attributes.count.should eq 1
      AttrReDef2.attributes.should eq [:attr2] 
      attr_rd2 = AttrReDef2.new
      attr_rd2.attr2.should eq 'two'
    end

    it 'to change default'  do
      expect {
        class AttrReDef3
          include Woyo::Attributes
          attribute attr3: '333'
        end
        class AttrReDef3
          attribute attr3: 'three'
        end
      }.to_not raise_error
      AttrReDef3.attributes.count.should eq 1
      AttrReDef3.attributes.should eq [:attr3] 
      attr_rd3 = AttrReDef3.new
      attr_rd3.attr3.should eq 'three'
    end

  end

  context 'groups' do

    before :all do
      class AT
        include Woyo::Attributes
        group :stooges, :larry, :curly, :moe
        group :cars,    :mustang, :ferarri, :mini 
      end
      @at = AT.new
    end

    it 'can be listed for class' do
      groups = AT.groups
      groups.should be_instance_of Hash
      groups.count.should eq 2
      groups.keys.should eq [ :stooges, :cars ]
      groups[:stooges].should eq [ :larry, :curly, :moe ]
      groups[:cars].should eq [ :mustang, :ferarri, :mini ]
    end

    it 'have convenience accessor :names for :keys' do
      @at.stooges.names.should eq [ :larry, :curly, :moe ]  
    end

    it 'names and nil values can be retrieved from instance without populating' do
      @at.stooges.should be_instance_of Woyo::Attributes::Group
      @at.stooges.count.should eq 3
      @at.stooges.names.should eq [ :larry, :curly, :moe ] 
      @at.stooges.values.should eq [ nil, nil, nil ] 
    end

    it 'names and default values can be retrieved from instance without populating' do
      expect { 
        class GroupDefTest
          include Woyo::Attributes
          group :numbers, one: 1, two: 2, three: proc { 3 } 
        end
      }.to_not raise_error
      def_test = GroupDefTest.new
      groups = def_test.groups
      groups.should be_instance_of Hash
      groups.count.should eq 1
      groups.names.should eq [ :numbers ]
      groups[:numbers].should be_instance_of Woyo::Attributes::Group
      groups[:numbers].names.should eq [ :one, :two, :three ]
      groups[:numbers].values.should eq [ 1, 2, 3 ]
      def_test.numbers.names.should eq [ :one, :two, :three ]  
      def_test.numbers.values.should eq [ 1, 2, 3 ]
    end

    it 'members can be accessed via group' do
      @at.stooges[:curly].should eq nil
      @at.stooges[:curly] = 'bald'
      @at.stooges[:curly].should eq 'bald'
    end

    it 'members are also attributes' do
      all_attrs = [ :larry, :curly, :moe, :mustang, :ferarri, :mini ]  
      @at.attributes.keys.should eq all_attrs 
      all_attrs.each do |attr|
        @at.should respond_to attr
      end
    end

    it 'members are attributes' do
      @at.stooges[:moe] = 'knucklehead'
      @at.stooges[:moe].should eq 'knucklehead'
      @at.moe.should eq 'knucklehead' 
    end

    it 'attributes are members' do
      @at.ferarri = 'fast'
      @at.ferarri.should eq 'fast'
      @at.cars[:ferarri].should eq 'fast'
    end

  end

  context 'boolean groups' do

    before :all do
      class ExGroupTest
        include Woyo::Attributes
        group! :temp, :hot, :warm, :cool, :cold
        group! :light, :dark, :dim, :bright
      end
    end

    it 'are listed for a class' do
      groups = ExGroupTest.boolean_groups
      groups.should be_instance_of Hash
      groups.count.should eq 2
      groups.keys.should eq [ :temp, :light ]
      groups[:temp].should eq [ :hot, :warm, :cool, :cold ] 
      groups[:light].should eq [ :dark, :dim, :bright ]
    end

    it 'accessor returns BooleanGroup instance' do
      egt = ExGroupTest.new
      egt.temp.should be_instance_of Woyo::Attributes::BooleanGroup
    end

    it 'first member is true, rest are false' do
      egt = ExGroupTest.new
      egt.light[:dark].should eq true
      egt.light[:dim].should eq false
      egt.light[:bright].should eq false
    end

    it 'making group member true affects member attributes' do
      egt = ExGroupTest.new
      egt.temp[:cold] = true
      egt.cold.should be true
      egt.cool.should be false
      egt.warm.should be false
      egt.hot.should be false
    end

    it 'making attribute true affects group members' do
      egt = ExGroupTest.new
      egt.cold = true
      egt.light[:cold].should be true
      egt.light[:cool].should be false
      egt.light[:warm].should be false
      egt.light[:hot].should be false
    end

  end

  context 'assigned a Hash as a value' do

    before :all do
      class AttrHashTest
        include Woyo::Attributes
        attributes :reaction, :hot, :cold
      end
      @aht = AttrHashTest.new
    end

    it 'accepts the hash as a value' do
      expect { @aht.reaction hot: 'Sweat', cold: 'Shiver' }.to_not raise_error
    end

    it 'returns the value of the first key that evaluates as a true attribute' do
      @aht.cold = true
      @aht.reaction.should eq 'Shiver'
      @aht.hot = true
      @aht.reaction.should eq 'Sweat'
    end

    it 'otherwise it returns the hash' do
      reactions = { :hot => 'Sweat', :cold => 'Shiver' }
      @aht.cold = false
      @aht.hot = false
      @aht.reaction.should eq reactions
    end

  end

end