# encoding: utf-8
require 'spec_helper'

describe CouchRest::Model::Property do

  before(:each) do
    reset_test_db!
    @card = Card.new(:first_name => "matt")
  end

  it "should be accessible from the object" do
    @card.properties.should be_an_instance_of(Array)
    @card.properties.map{|p| p.name}.should include("first_name")
  end

  it "should list object properties with values" do
    @card.properties_with_values.should be_an_instance_of(Hash)
    @card.properties_with_values["first_name"].should == "matt"
  end

  it "should let you access a property value (getter)" do
    @card.first_name.should == "matt"
  end

  it "should let you set a property value (setter)" do
    @card.last_name = "Aimonetti"
    @card.last_name.should == "Aimonetti"
  end

  it "should not let you set a property value if it's read only" do
    lambda{@card.read_only_value = "test"}.should raise_error
  end

  it "should let you use an alias for an attribute" do
    @card.last_name = "Aimonetti"
    @card.family_name.should == "Aimonetti"
    @card.family_name.should == @card.last_name
  end

  it "should let you use an alias for a casted attribute" do
    @card.cast_alias = Person.new(:name => ["Aimonetti"])
    @card.cast_alias.name.should == ["Aimonetti"]
    @card.calias.name.should == ["Aimonetti"]
    card = Card.new(:first_name => "matt", :cast_alias => {:name => ["Aimonetti"]})
    card.cast_alias.name.should == ["Aimonetti"]
    card.calias.name.should == ["Aimonetti"]
  end

  it "should raise error if property name coincides with model type key" do
    lambda { Cat.property(Cat.model_type_key) }.should raise_error(/already used/)
  end

  it "should not raise error if property name coincides with model type key on non-model" do
    lambda { Person.property(Article.model_type_key) }.should_not raise_error
  end

  it "should be auto timestamped" do
    @card.created_at.should be_nil
    @card.updated_at.should be_nil
    @card.save.should be_true
    @card.created_at.should_not be_nil
    @card.updated_at.should_not be_nil
  end

  describe "#as_couch_json" do

    it "should provide a simple hash from model" do
      @card.as_couch_json.class.should eql(Hash)
    end

    it "should remove properties from Hash if value is nil" do
      @card.last_name = nil
      @card.as_couch_json.keys.include?('last_name').should be_false
    end

  end

  describe "#as_json" do

    it "should provide a simple hash from model" do
      @card.as_json.class.should eql(Hash)
    end

    it "should pass options to Active Support's as_json" do
      @card.last_name = "Aimonetti"
      @card.as_json(:only => 'last_name').should eql('last_name' => 'Aimonetti')
    end

  end

  describe '#read_attribute' do
    it "should let you use read_attribute method" do
      @card.last_name = "Aimonetti"
      @card.read_attribute(:last_name).should eql('Aimonetti')
      @card.read_attribute('last_name').should eql('Aimonetti')
      last_name_prop = @card.properties.find{|p| p.name == 'last_name'}
      @card.read_attribute(last_name_prop).should eql('Aimonetti')
    end

    it 'should raise an error if the property does not exist' do
      expect { @card.read_attribute(:this_property_should_not_exist) }.to raise_error(ArgumentError)
    end
  end

  describe '#write_attribute' do
    it "should let you use write_attribute method" do
      @card.write_attribute(:last_name, 'Aimonetti 1')
      @card.last_name.should eql('Aimonetti 1')
      @card.write_attribute('last_name', 'Aimonetti 2')
      @card.last_name.should eql('Aimonetti 2')
      last_name_prop = @card.properties.find{|p| p.name == 'last_name'}
      @card.write_attribute(last_name_prop, 'Aimonetti 3')
      @card.last_name.should eql('Aimonetti 3')
    end

    it 'should raise an error if the property does not exist' do
      expect { @card.write_attribute(:this_property_should_not_exist, 823) }.to raise_error(ArgumentError)
    end


    it "should let you use write_attribute on readonly properties" do
      lambda {
        @card.read_only_value = "foo"
      }.should raise_error
      @card.write_attribute(:read_only_value, "foo")
      @card.read_only_value.should == 'foo'
    end

    it "should cast via write_attribute" do
      @card.write_attribute(:cast_alias, {:name => ["Sam", "Lown"]})
      @card.cast_alias.class.should eql(Person)
      @card.cast_alias.name.last.should eql("Lown")
    end

    it "should not cast via write_attribute if property not casted" do
      @card.write_attribute(:first_name, {:name => "Sam"})
      @card.first_name.class.should eql(Hash)
      @card.first_name[:name].should eql("Sam")
    end
  end

  describe "mass updating attributes without property" do
    
    describe "when mass_assign_any_attribute false" do
      
      it "should not allow them to be set" do
        @card.attributes = {:test => 'fooobar'}
        @card['test'].should be_nil
      end

     it 'should not allow them to be updated with update_attributes' do
       @card.update_attributes(:test => 'fooobar')
       @card['test'].should be_nil
     end

     it 'should not have a different revision after update_attributes' do
       @card.save
       rev = @card.rev
       @card.update_attributes(:test => 'fooobar')
       @card.rev.should eql(rev)
     end

     it 'should not have a different revision after save' do
       @card.save
       rev = @card.rev
       @card.attributes = {:test => 'fooobar'}
       @card.save
       @card.rev.should eql(rev)
     end

    end

    describe "when mass_assign_any_attribute true" do
      before(:each) do
        # dup Card class so that no other tests are effected
        card_class = Card.dup
        card_class.class_eval do
          mass_assign_any_attribute true
        end
        @card = card_class.new(:first_name => 'Sam')
      end

      it 'should allow them to be updated' do
        @card.attributes = {:testing => 'fooobar'}
        @card['testing'].should eql('fooobar')
      end

      it 'should allow them to be updated with update_attributes' do
        @card.update_attributes(:testing => 'fooobar')
        @card['testing'].should eql('fooobar')
      end

      it 'should have a different revision after update_attributes' do
        @card.save
        rev = @card.rev
        @card.update_attributes(:testing => 'fooobar')
        @card.rev.should_not eql(rev)
      end

      it 'should have a different revision after save' do
        @card.save
        rev = @card.rev
        @card.attributes = {:testing => 'fooobar'}
        @card.save
        @card.rev.should_not eql(rev)
      end

    end
  end

  describe "mass assignment protection" do

    it "should not store protected attribute using mass assignment" do
      cat_toy = CatToy.new(:name => "Zorro")
      cat = Cat.create(:name => "Helena", :toys => [cat_toy], :favorite_toy => cat_toy, :number => 1)
      cat.number.should be_nil
      cat.number = 1
      cat.save
      cat.number.should == 1
    end

    it "should not store protected attribute when 'declare accessible poperties, assume all the rest are protected'" do
      user = User.create(:name => "Marcos Tapajós", :admin => true)
      user.admin.should be_nil
    end

    it "should not store protected attribute when 'declare protected properties, assume all the rest are accessible'" do
      user = SpecialUser.create(:name => "Marcos Tapajós", :admin => true)
      user.admin.should be_nil
    end

  end

  describe "validation" do
    before(:each) do
      @invoice = Invoice.new(:client_name => "matt", :employee_name => "Chris", :location => "San Diego, CA")
    end

    it "should be able to be validated" do
      @card.valid?.should == true
    end

    it "should let you validate the presence of an attribute" do
      @card.first_name = nil
      @card.should_not be_valid
      @card.errors.should_not be_empty
      @card.errors[:first_name].should == ["can't be blank"]
    end

    it "should let you look up errors for a field by a string name" do
      @card.first_name = nil
      @card.should_not be_valid
      @card.errors['first_name'].should == ["can't be blank"]
    end

    it "should validate the presence of 2 attributes" do
      @invoice.clear
      @invoice.should_not be_valid
      @invoice.errors.should_not be_empty
      @invoice.errors[:client_name].should == ["can't be blank"]
      @invoice.errors[:employee_name].should_not be_empty
    end

    it "should let you set an error message" do
      @invoice.location = nil
      @invoice.valid?
      @invoice.errors[:location].should == ["Hey stupid!, you forgot the location"]
    end

    it "should validate before saving" do
      @invoice.location = nil
      @invoice.should_not be_valid
      @invoice.save.should be_false
      @invoice.should be_new
    end
  end

end

describe "properties of hash of casted models" do
  it "should be able to assign a casted hash to a hash property" do
    chain = KeyChain.new
    keys = {"House" => "8==$", "Office" => "<>==U"}
    chain.keys = keys
    chain.keys = chain.keys
    chain.keys.should == keys
  end
end

describe "properties of array of casted models" do

  before(:each) do
    @course = Course.new :title => 'Test Course'
  end

  it "should allow attribute to be set from an array of objects" do
    @course.questions = [Question.new(:q => "works?"), Question.new(:q => "Meaning of Life?")]
    @course.questions.length.should eql(2)
  end

  it "should allow attribute to be set from an array of hashes" do
    @course.questions = [{:q => "works?"}, {:q => "Meaning of Life?"}]
    @course.questions.length.should eql(2)
    @course.questions.last.q.should eql("Meaning of Life?")
    @course.questions.last.class.should eql(Question) # typecasting
  end

  it "should allow attribute to be set from hash with ordered keys and objects" do
    @course.questions = { '0' => Question.new(:q => "Test1"), '1' => Question.new(:q => 'Test2') }
    @course.questions.length.should eql(2)
    @course.questions.last.q.should eql('Test2')
    @course.questions.last.class.should eql(Question)
  end

  it "should allow attribute to be set from hash with ordered keys and sub-hashes" do
    @course.questions = { '10' => {:q => 'Test10'}, '0' => {:q => "Test1"}, '1' => {:q => 'Test2'} }
    @course.questions.length.should eql(3)
    @course.questions.last.q.should eql('Test10')
    @course.questions.last.class.should eql(Question)
  end

  it "should allow attribute to be set from hash with ordered keys and HashWithIndifferentAccess" do
    # This is similar to what you'd find in an HTML POST parameters
    hash = HashWithIndifferentAccess.new({ '0' => {:q => "Test1"}, '1' => {:q => 'Test2'} })
    @course.questions = hash
    @course.questions.length.should eql(2)
    @course.questions.last.q.should eql('Test2')
    @course.questions.last.class.should eql(Question)
  end

  it "should raise an error if attempting to set single value for array type" do
    lambda {
      @course.questions = Question.new(:q => 'test1')
    }.should raise_error(/Expecting an array/)
  end


end

describe "a casted model retrieved from the database" do
  before(:each) do
    reset_test_db!
    @cat = Cat.new(:name => 'Stimpy')
    @cat.favorite_toy = CatToy.new(:name => 'Stinky')
    @cat.toys << CatToy.new(:name => 'Feather')
    @cat.toys << CatToy.new(:name => 'Mouse')
    @cat.save
    @cat = Cat.get(@cat.id)
  end

  describe "as a casted property" do
    it "should already be casted_by its parent" do
      @cat.favorite_toy.casted_by.should === @cat
    end
  end

  describe "from a casted collection" do
    it "should already be casted_by its parent" do
      @cat.toys[0].casted_by.should === @cat
      @cat.toys[1].casted_by.should === @cat
    end
  end
end

describe "nested models (not casted)" do
  before(:each) do
    reset_test_db!
    @cat = ChildCat.new(:name => 'Stimpy')
    @cat.mother = {:name => 'Stinky'}
    @cat.siblings = [{:name => 'Feather'}, {:name => 'Felix'}]
    @cat.save
    @cat = ChildCat.get(@cat.id)
  end

  it "should correctly save single relation" do
    @cat.mother.name.should eql('Stinky')
    @cat.mother.casted_by.should eql(@cat)
  end

  it "should correctly save collection" do
    @cat.siblings.first.name.should eql("Feather")
    @cat.siblings.last.casted_by.should eql(@cat)
  end
end

describe "Property Class" do

  let :klass do
    CouchRest::Model::Property
  end

  it "should provide name as string" do
    property = CouchRest::Model::Property.new(:test, :type => String)
    property.name.should eql('test')
    property.to_s.should eql('test')
  end

  it "should provide name as a symbol" do 
    property = CouchRest::Model::Property.new(:test, :type => String)
    property.name.to_sym.should eql(:test)
    property.to_sym.should eql(:test)
  end

  it "should provide class from type" do
    property = CouchRest::Model::Property.new(:test, :type => String)
    property.type.should eql(String)
    property.array.should be_false
  end

  it "should provide base class from type in array" do
    property = CouchRest::Model::Property.new(:test, :type => [String])
    property.type.should eql(String)
    property.array.should be_true
  end

  it "should provide base class and set array type" do
    property = CouchRest::Model::Property.new(:test, :type => String, :array => true)
    property.type.should eql(String)
    property.array.should be_true
  end

  it "should raise error if type as string requested" do
    lambda {
      property = CouchRest::Model::Property.new(:test, :type => 'String')
    }.should raise_error
  end

  it "should leave type nil and return class as nil also" do
    property = CouchRest::Model::Property.new(:test, :type => nil)
    property.type.should be_nil
  end

  it "should convert empty type array to [Object]" do
    property = CouchRest::Model::Property.new(:test, :type => [])
    property.type.should eql(Object)
  end

  it "should set init method option or leave as 'new'" do
    # (bad example! Time already typecast)
    property = CouchRest::Model::Property.new(:test, :type => Time)
    property.init_method.should eql('new')
    property = CouchRest::Model::Property.new(:test, :type => Time, :init_method => 'parse')
    property.init_method.should eql('parse')
  end

  it "should set the allow_blank option to true by default" do
    property = CouchRest::Model::Property.new(:test, :type => String)
    property.allow_blank.should be_true
  end

  it "should allow setting of the allow_blank option to false" do
    property = CouchRest::Model::Property.new(:test, :type => String, :allow_blank => false)
    property.allow_blank.should be_false
  end

  it "should convert block to type" do
    prop = klass.new(:test) do
      property :testing
    end
    prop.array.should be_false
    prop.type.should_not be_nil
    prop.type.class.should eql(Class)
    obj = prop.type.new
    obj.should respond_to(:testing)
  end

  it "should convert block to type with array" do
    prop = klass.new(:test, :array => true) do
      property :testing
    end
    prop.type.should_not be_nil
    prop.type.class.should eql(Class)
    prop.array.should be_true
  end

  describe "#build" do
    it "should allow instantiation of new object" do
      property = CouchRest::Model::Property.new(:test, :type => Date)
      obj = property.build(2011, 05, 21)
      obj.should eql(Date.new(2011, 05, 21))
    end
    it "should use init_method if provided" do
      property = CouchRest::Model::Property.new(:test, :type => Date, :init_method => 'parse')
      obj = property.build("2011-05-21")
      obj.should eql(Date.new(2011, 05, 21))
    end
    it "should use init_method Proc if provided" do
      property = CouchRest::Model::Property.new(:test, :type => Date, :init_method => Proc.new{|v| Date.parse(v)})
      obj = property.build("2011-05-21")
      obj.should eql(Date.new(2011, 05, 21))
    end
    it "should raise error if no class" do
      property = CouchRest::Model::Property.new(:test)
      lambda { property.build }.should raise_error(StandardError, /Cannot build/)
    end
  end

  ## Property Casting method. More thoroughly tested in typecast_spec.

  describe "casting" do
    it "should cast a value" do
      property = CouchRest::Model::Property.new(:test, :type => Date)
      parent = mock("FooObject")
      property.cast(parent, "2010-06-16").should eql(Date.new(2010, 6, 16))
      property.cast_value(parent, "2010-06-16").should eql(Date.new(2010, 6, 16))
    end

    it "should cast an array of values" do
      property = CouchRest::Model::Property.new(:test, :type => [Date])
      parent = mock("FooObject")
      property.cast(parent, ["2010-06-01", "2010-06-02"]).should eql([Date.new(2010, 6, 1), Date.new(2010, 6, 2)])
    end

    it "should cast an array of values with array option" do
      property = CouchRest::Model::Property.new(:test, :type => Date, :array => true)
      parent = mock("FooObject")
      property.cast(parent, ["2010-06-01", "2010-06-02"]).should eql([Date.new(2010, 6, 1), Date.new(2010, 6, 2)])
    end

    context "when allow_blank is false" do
      let :parent do
        mock("FooObject")
      end

      it "should convert blank to nil" do
        property = CouchRest::Model::Property.new(:test, :type => String, :allow_blank => false)
        property.cast(parent, "").should be_nil
      end

      it "should remove blank array entries" do
        property = CouchRest::Model::Property.new(:test, :type => [String], :allow_blank => false)
        property.cast(parent, ["", "foo"]).should eql(["foo"])
      end
    end

    it "should set a CastedArray on array of Objects" do
      property = CouchRest::Model::Property.new(:test, :type => [Object])
      parent = mock("FooObject")
      property.cast(parent, ["2010-06-01", "2010-06-02"]).class.should eql(CouchRest::Model::CastedArray)
    end

    it "should set a CastedArray on array of Strings" do
      property = CouchRest::Model::Property.new(:test, :type => [String])
      parent = mock("FooObject")
      property.cast(parent, ["2010-06-01", "2010-06-02"]).class.should eql(CouchRest::Model::CastedArray)
    end

    it "should allow instantion of model via CastedArray#build" do
      property = CouchRest::Model::Property.new(:dates, :type => [Date])
      parent = Article.new
      ary = property.cast(parent, [])
      obj = ary.build(2011, 05, 21)
      ary.length.should eql(1)
      ary.first.should eql(Date.new(2011, 05, 21))
      obj = ary.build(2011, 05, 22)
      ary.length.should eql(2)
      ary.last.should eql(Date.new(2011, 05, 22))
    end

    it "should cast an object that provides an array" do
      prop = Class.new do
        attr_accessor :ary
        def initialize(val); self.ary = val; end
        def as_json; ary; end
      end
      property = CouchRest::Model::Property.new(:test, :type => prop)
      parent = mock("FooClass")
      cast = property.cast(parent, [1, 2])
      cast.ary.should eql([1, 2])
    end

    it "should set parent as casted_by object in CastedArray" do
      property = CouchRest::Model::Property.new(:test, :type => [Object])
      parent = mock("FooObject")
      property.cast(parent, ["2010-06-01", "2010-06-02"]).casted_by.should eql(parent)
    end

    it "should set casted_by on new value" do
      property = CouchRest::Model::Property.new(:test, :type => CatToy)
      parent = mock("CatObject")
      cast = property.cast(parent, {:name => 'catnip'})
      cast.casted_by.should eql(parent)
    end

  end

end