require 'spec_helper'

describe Chozo::VariaModel do
  describe "ClassMethods" do
    subject do
      Class.new do
        include Chozo::VariaModel
      end
    end

    describe "::attributes" do
      it "returns a Hashie::Mash" do
        subject.attributes.should be_a(Hashie::Mash)
      end

      it "is empty by default" do
        subject.attributes.should be_empty
      end
    end

    describe "::attribute" do
      it "adds an attribute to the attributes hash for each attribute function call" do
        subject.attribute 'jamie.winsor'
        subject.attribute 'brooke.winsor'

        subject.attributes.should have(2).items
      end

      it "adds a validation if :required option is true" do
        subject.attribute 'brooke.winsor', required: true

        subject.validations.should have(1).item
      end

      it "adds a validation if the :type option is provided" do
        subject.attribute 'brooke.winsor', type: :string

        subject.validations.should have(1).item
      end

      it "sets a default value if :default option is provided" do
        subject.attribute 'brooke.winsor', default: 'rhode island'

        subject.attributes.dig('brooke.winsor').should eql('rhode island')
      end
    end

    describe "::validations" do
      it "returns a HashWithIndifferentAccess" do
        subject.validations.should be_a(HashWithIndifferentAccess)
      end

      it "is empty by default" do
        subject.validations.should be_empty
      end
    end

    describe "::validations_for" do
      context "when an attribute is registered and has validations" do
        before(:each) do
          subject.attribute("nested.attribute", required: true, type: String)
        end

        it "returns an array of procs" do
          validations = subject.validations_for("nested.attribute")

          validations.should be_a(Array)
          validations.should each be_a(Proc)
        end
      end

      context "when an attribute is registered but has no validations" do
        before(:each) do
          subject.attribute("nested.attribute")
        end

        it "returns an empty array" do
          validations = subject.validations_for("nested.attribute")

          validations.should be_a(Array)
          validations.should be_empty
        end
      end

      context "when an attribute is not registered" do
        it "returns an empty array" do
          validations = subject.validations_for("not_existing.attribute")

          validations.should be_a(Array)
          validations.should be_empty
        end
      end
    end

    describe "::validate_kind_of" do
      let(:types) do
        [
          String,
          Boolean
        ]
      end

      let(:key) do
        'nested.one'
      end

      subject do
        Class.new do
          include Chozo::VariaModel

          attribute 'nested.one', types: [String, Boolean]
        end
      end

      let(:model) do
        subject.new
      end

      it "returns an array" do
        subject.validate_kind_of(types, model, key).should be_a(Array)
      end

      context "failure" do
        before(:each) do
          model.nested.one = nil
        end

        it "returns an array where the first element is ':error'" do
          subject.validate_kind_of(types, model, key).first.should eql(:error)
        end

        it "returns an array where the second element is an error message containing the attribute and types" do
          types.each do |type|
            subject.validate_kind_of(types, model, key)[1].should =~ /#{type}/
          end
          subject.validate_kind_of(types, model, key)[1].should =~ /#{key}/
        end
      end

      context "success" do
        before(:each) do
          model.nested.one = true
        end

        it "returns an array where the first element is ':ok'" do
          subject.validate_kind_of(types, model, key).first.should eql(:ok)
        end

        it "returns an array where the second element is a blank string" do
          subject.validate_kind_of(types, model, key)[1].should be_blank
        end
      end

      context "when given two types of the same kind" do
        let(:types) do
          [
            String,
            String
          ]
        end

        let(:key) do
          'nested.one'
        end

        subject do
          Class.new do
            include Chozo::VariaModel

            attribute 'nested.one', types: [String, Boolean]
          end
        end

        let(:model) do
          subject.new
        end

        before(:each) do
          model.nested.one = nil
        end

        it "returns a error message that contains the type error only once" do
          subject.validate_kind_of(types, model, key)[1].should eql("Expected attribute: 'nested.one' to be a type of: 'String'")
        end
      end
    end

    describe "::validate_required" do
      let(:key) do
        'nested.one'
      end

      subject do
        Class.new do
          include Chozo::VariaModel

          attribute 'nested.one', required: true
        end
      end

      let(:model) do
        subject.new
      end

      it "returns an array" do
        subject.validate_required(model, key).should be_a(Array)
      end

      it "fails validation if the value of the attribute is nil" do
        model.set_attribute(key, nil)
        
        subject.validate_required(model, key).first.should eql(:error)
      end

      it "passes validation if the value of the attribute is false" do
        model.set_attribute(key, false)

        subject.validate_required(model, key).first.should eql(:ok)
      end

      it "passes validation if the value of the attribute is not nil" do
        model.set_attribute(key, 'some_value')

        subject.validate_required(model, key).first.should eql(:ok)
      end

      context "failure" do
        before(:each) do
          model.nested.one = nil
        end

        it "returns an array where the first element is ':error'" do
          subject.validate_required(model, key).first.should eql(:error)
        end

        it "returns an array where the second element is an error message containing the attribute name" do
          subject.validate_required(model, key)[1].should =~ /#{key}/
        end
      end

      context "success" do
        before(:each) do
          model.nested.one = "hello"
        end

        it "returns an array where the first element is ':ok'" do
          subject.validate_required(model, key).first.should eql(:ok)
        end

        it "returns an array where the second element is a blank string" do
          subject.validate_required(model, key)[1].should be_blank
        end
      end
    end
  end

  subject do
    Class.new do
      include Chozo::VariaModel

      attribute 'nested.not_coerced', default: 'hello'
      attribute 'nested.no_default'
      attribute 'nested.coerced', coerce: lambda { |m| m.to_s }
      attribute 'toplevel', default: 'hello'
      attribute 'no_default'
      attribute 'coerced', coerce: lambda { |m| m.to_s }
    end.new
  end

  describe "GeneratedAccessors" do
    describe "nested getter" do
      it "returns the default value" do
        subject.nested.not_coerced.should eql('hello')
      end

      it "returns nil if there is no default value" do
        subject.nested.no_default.should be_nil
      end
    end

    describe "toplevel getter" do
      it "returns the default value" do
        subject.toplevel.should eql('hello')
      end

      it "returns nil if there is no default value" do
        subject.no_default.should be_nil
      end
    end

    describe "nested setter" do
      it "sets the value of the nested attribute" do
        subject.nested.not_coerced = 'world'

        subject.nested.not_coerced.should eql('world')
      end
    end

    describe "toplevel setter" do
      it "sets the value of the top level attribute" do
        subject.toplevel = 'world'

        subject.toplevel.should eql('world')
      end
    end

    describe "nested coerced setter" do
      it "sets the value of the nested coerced attribute" do
        subject.nested.coerced = 1

        subject.nested.coerced.should eql("1")
      end
    end

    describe "toplevel coerced setter" do
      it "sets the value of the top level coerced attribute" do
        subject.coerced = 1

        subject.coerced.should eql('1')
      end
    end

    context "given two nested attributes with a common parent and default values" do
      subject do
        Class.new do
          include Chozo::VariaModel

          attribute 'nested.one', default: 'val_one'
          attribute 'nested.two', default: 'val_two'
        end.new
      end

      it "sets a default value for each nested attribute" do
        subject.nested.one.should eql('val_one')
        subject.nested.two.should eql('val_two')
      end
    end

    context "given two nested attributes with a common parent and coercions" do
      subject do
        Class.new do
          include Chozo::VariaModel

          attribute 'nested.one', coerce: lambda { |m| m.to_s }
          attribute 'nested.two', coerce: lambda { |m| m.to_s }
        end.new
      end

      it "coerces each value if both have a coercion" do
        subject.nested.one = 1
        subject.nested.two = 2

        subject.nested.one.should eql("1")
        subject.nested.two.should eql("2")
      end
    end
  end

  describe "Validations" do
    describe "validate required" do
      subject do
        Class.new do
          include Chozo::VariaModel

          attribute 'brooke.winsor', required: true
        end.new
      end

      it "is not valid if it fails validation" do
        subject.should_not be_valid
      end

      it "adds an error for each attribute that fails validations" do
        subject.validate

        subject.errors.should have(1).item
      end

      it "adds a message for each failed validation" do
        subject.validate

        subject.errors['brooke.winsor'].should have(1).item
        subject.errors['brooke.winsor'][0].should eql("A value is required for attribute: 'brooke.winsor'")
      end
    end

    describe "validate type" do
      subject do
        Class.new do
          include Chozo::VariaModel

          attribute 'brooke.winsor', type: String
        end.new
      end

      before(:each) do
        subject.brooke.winsor = false
      end

      it "returns false if it fails validation" do
        subject.should_not be_valid
      end

      it "adds an error if it fails validation" do
        subject.validate

        subject.errors.should have(1).item
        subject.errors['brooke.winsor'].should have(1).item
        subject.errors['brooke.winsor'][0].should eql("Expected attribute: 'brooke.winsor' to be a type of: 'String', 'NilClass'")
      end
    end
  end

  describe "#set_attribute" do
    subject do
      Class.new do
        include Chozo::VariaModel

        attribute 'brooke.winsor', type: String, default: 'sister'
        attribute 'brooke.costantini', type: String, default: 'sister'
      end.new
    end

    it "sets the value of the given attribute" do
      subject.set_attribute('brooke.winsor', 'rhode island')

      subject.brooke.winsor.should eql('rhode island')
    end

    it "does not disturb the other attributes" do
      subject.set_attribute('brooke.winsor', 'rhode island')

      subject.brooke.costantini.should eql('sister')
    end
  end

  describe "#get_attribute" do
    subject do
      Class.new do
        include Chozo::VariaModel

        attribute 'brooke.winsor', type: String, default: 'sister'
      end.new
    end

    it "returns the value of the given dotted path" do
      subject.get_attribute('brooke.winsor').should eql('sister')
    end

    it "returns nil if the dotted path matches no attributes" do
      subject.get_attribute('brooke.costantini').should be_nil
    end
  end

  describe "#mass_assign" do
    subject do
      Class.new do
        include Chozo::VariaModel

        attribute 'brooke.winsor', type: String, default: 'sister'
        attribute 'jamie.winsor', type: String, default: 'brother'
        attribute 'gizmo', type: String, default: 'dog'
      end.new
    end

    it "sets the values of all matching defined attributes" do
      new_attrs = {
        brooke: {
          winsor: "other"
        },
        jamie: {
          winsor: "other_two"
        }
      }

      subject.mass_assign(new_attrs)
      subject.brooke.winsor.should eql("other")
      subject.jamie.winsor.should eql("other_two")
    end

    it "leaves the values of untouched attributes" do
      new_attrs = {
        brooke: {
          winsor: "other"
        },
        jamie: {
          winsor: "other_two"
        }
      }

      subject.mass_assign(new_attrs)
      subject.gizmo.should eql("dog")
    end

    it "ignores values which are not defined attributes" do
      new_attrs = {
        undefined_attribute: "value"
      }

      subject.mass_assign(new_attrs)
      subject.should_not respond_to(:undefined_attribute)
    end
  end

  describe "#from_json" do
    subject do
      Class.new do
        include Chozo::VariaModel

        attribute 'first_name', type: String
        attribute 'nick', type: String
      end.new
    end

    it "returns self" do
      subject.from_json(MultiJson.encode(first_name: "jamie", nick: "reset")).should be_a(described_class)
    end

    it "updates self from JSON data" do
      subject.from_json(MultiJson.encode(first_name: "jamie", nick: "reset"))

      subject.first_name.should eql("jamie")
      subject.nick.should eql("reset")
    end
  end

  describe "#from_hash" do
    subject do
      Class.new do
        include Chozo::VariaModel

        attribute 'first_name', type: String
        attribute 'nick', type: String
      end.new
    end

    it "returns self" do
      subject.from_hash(first_name: "jamie", nick: "reset").should be_a(described_class)
    end

    it "updates and returns self from a Hash" do
      subject.from_hash(first_name: "jamie", nick: "reset")

      subject.first_name.should eql("jamie")
      subject.nick.should eql("reset")
    end
  end

  describe "#to_json" do
    subject do
      Class.new do
        include Chozo::VariaModel

        attribute 'first_name', type: String
        attribute 'nick', type: String
      end.new
    end

    it "returns a JSON string containin the serialized attributes" do
      subject.first_name = "brooke"
      subject.nick = "leblanc"

      subject.to_json.should eql(MultiJson.encode(first_name: "brooke", nick: "leblanc"))
    end
  end

  describe "#to_hash" do
    it "returns the attributes" do
      subject.to_hash.should eql(subject.attributes)
    end
  end
end