require 'spec_helper'

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

    describe "::attributes" do
      it "returns a VariaModel::Attributes" do
        expect(subject.attributes).to be_a(described_class::Attributes)
      end

      it "is empty by default" do
        expect(subject.attributes).to 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'

        expect(subject.attributes.size).to eq(2)
      end

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

        expect(subject.validations.size).to eq(1)
      end

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

        expect(subject.validations.size).to eq(1)
      end

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

        expect(subject.attributes.berks_dig('brooke.winsor')).to eql('rhode island')
      end

      it "allows an attribute called 'attributes'" do
        subject.attribute 'attributes', default: 'bag of junk'

        expect(subject.attributes.berks_dig('attributes')).to eql('bag of junk')
      end

      it "allows an attribute called 'attribute'" do
        subject.attribute 'attribute', default: 'some value'

        expect(subject.attributes.berks_dig('attribute')).to eql('some value')
      end

      it "allows an attribute that has a lambda default value" do
        subject.attribute 'brooke', default: ->{ "winsor".upcase }

        expect(subject.attributes.berks_dig('brooke')).to be_a(Proc)
      end
    end

    describe "::validations" do
      it "returns a Hashie::Mash" do
        expect(subject.validations).to be_a(Hashie::Mash)
      end

      it "is empty by default" do
        expect(subject.validations).to 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")

          expect(validations).to be_a(Array)
          expect(validations).to 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")

          expect(validations).to be_a(Array)
          expect(validations).to 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")

          expect(validations).to be_a(Array)
          expect(validations).to be_empty
        end
      end

      describe "#assignment_mode" do
        it "returns the default assignment mode :whitelist" do
          expect(subject.assignment_mode).to eql(:whitelist)
        end
      end

      describe "#set_assignment_mode" do
        it "sets the assignment_mode to whitelist" do
          subject.set_assignment_mode(:whitelist)

          expect(subject.assignment_mode).to eql(:whitelist)
        end

        it "sets the assignment_mode to carefree" do
          subject.set_assignment_mode(:carefree)

          expect(subject.assignment_mode).to eql(:carefree)
        end

        it "raises if given an invalid assignment mode" do
          expect { subject.set_assignment_mode(:not_a_real_mode) }.to raise_error(ArgumentError)
        end
      end
    end

    describe "::validate_kind_of" do
      let(:types) { [ String, Buff::Boolean ] }
      let(:key) { 'nested.one' }

      subject do
        Class.new do
          include VariaModel

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

      let(:model) { subject.new }

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

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

        it "returns an array where the first element is ':error'" do
          expect(subject.validate_kind_of(types, model, key).first).to 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|
            expect(subject.validate_kind_of(types, model, key)[1]).to match(/#{type}/)
          end
          expect(subject.validate_kind_of(types, model, key)[1]).to match(/#{key}/)
        end
      end

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

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

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

      context "when given two types of the same kind" do
        let(:types) { [ String, String ] }
        let(:key) { 'nested.one' }

        subject do
          Class.new do
            include VariaModel

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

        let(:model) { subject.new }
        before(:each) { model.nested.one = nil }

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

    describe "::validate_required" do
      subject do
        Class.new do
          include VariaModel

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

      let(:key) { 'nested.one' }
      let(:model) { subject.new }

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

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

        expect(subject.validate_required(model, key).first).to eql(:error)
      end

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

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

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

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

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

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

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

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

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

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

  subject do
    Class.new do
      include 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
        expect(subject.nested.not_coerced).to eql('hello')
      end

      it "returns nil if there is no default value" do
        expect(subject.nested.no_default).to be_nil
      end
    end

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

      it "returns nil if there is no default value" do
        expect(subject.no_default).to be_nil
      end
    end

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

        expect(subject.nested.not_coerced).to eql('world')
      end
    end

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

        expect(subject.toplevel).to eql('world')
      end
    end

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

        expect(subject.nested.coerced).to eql("1")
      end
    end

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

        expect(subject.coerced).to eql('1')
      end
    end

    context "given two nested attributes with a common parent and default values" do
      subject do
        Class.new do
          include 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
        expect(subject.nested.one).to eql('val_one')
        expect(subject.nested.two).to eql('val_two')
      end
    end

    context "given two nested attributes with a common parent and coercions" do
      subject do
        Class.new do
          include 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

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

    context "given an attribute called 'attributes'" do
      subject do
        Class.new do
          include VariaModel

          attribute 'attributes', default: Hash.new
        end.new
      end

      it "allows the setting and getting of the 'attributes' mimic methods" do
        expect(subject.attributes).to be_a(Hash)
        expect(subject.attributes).to be_empty

        new_hash = { something: "here" }
        subject.attributes = new_hash
        expect(subject.attributes[:something]).to eql("here")
      end
    end
  end

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

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

      it "is not valid if it fails validation" do
        expect(subject).not_to be_valid
      end

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

        expect(subject.errors.size).to eq(1)
      end

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

        expect(subject.errors['brooke.winsor'].size).to eq(1)
        expect(subject.errors['brooke.winsor'][0]).to eql("A value is required for attribute: 'brooke.winsor'")
      end
    end

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

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

      before(:each) { subject.brooke.winsor = false }

      it "returns false if it fails validation" do
        expect(subject).not_to be_valid
      end

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

        expect(subject.errors.size).to eq(1)
        expect(subject.errors['brooke.winsor'].size).to eq(1)
        expect(subject.errors['brooke.winsor'][0]).to 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 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')

      expect(subject.brooke.winsor).to eql('rhode island')
    end

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

      expect(subject.brooke.costantini).to eql('sister')
    end
  end

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

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

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

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

    it "returns the current value of the Proc" do
      subject.brooke.winsor = ->{ "bacon".upcase }
      expect(subject.get_attribute("brooke.winsor")).to eql("BACON")
    end

    it "returns the current value of the Proc each time" do
      @magic = "ponies"
      subject.brooke.winsor = -> { @magic }
      expect(subject.get_attribute("brooke.winsor")).to eql("ponies")
      @magic = "unicorns"
      expect(subject.get_attribute("brooke.winsor")).to eql("unicorns")
    end
  end

  describe "#[]" do
    subject do
      Class.new do
        include VariaModel
        attribute 'foo', default: ->{ String.new("Bacon").upcase }
      end.new
    end

    it "returns the current value of the Proc" do
      expect(subject['foo']).to eql("BACON")
    end

    it "returns the updated value of the Proc" do
      subject['foo']
      subject['foo'] = 'hello'
      expect(subject['foo']).to eql('hello')
    end
  end

  describe "#mass_assign" do
    subject do
      Class.new do
        include 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)
      expect(subject.brooke.winsor).to eql("other")
      expect(subject.jamie.winsor).to 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)
      expect(subject.gizmo).to eql("dog")
    end

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

      subject.mass_assign(new_attrs)
      expect(subject.get_attribute(:undefined_attribute)).to be_nil
      expect(subject).not_to respond_to(:undefined_attribute)
    end

    context "when in carefree assignment mode" do
      subject do
        Class.new do
          include VariaModel

          set_assignment_mode :carefree
        end.new
      end

      it "does not ignore values which are not defined" do
        new_attrs = {
          undefined_attribute: "value"
        }

        subject.mass_assign(new_attrs)
        expect(subject.get_attribute(:undefined_attribute)).to eql("value")
      end
    end
  end

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

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

    it "returns self" do
      expect(subject.from_json(JSON.dump(first_name: "jamie", nick: "reset"))).to be_a(described_class)
    end

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

      expect(subject.first_name).to eql("jamie")
      expect(subject.nick).to eql("reset")
    end
  end

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

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

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

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

      expect(subject.first_name).to eql("jamie")
      expect(subject.nick).to eql("reset")
    end
  end

  describe "#to_json" do
    class Playa
      include VariaModel

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

    subject do
      Playa.new
    end

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

      expect(subject.to_json).to eql(JSON.dump(first_name: "brooke", nick: "leblanc", json_class: "Playa"))
    end

    it "includes the most recent value for any Procs" do
      subject.first_name = ->{ "seth".capitalize }
      subject.nick = ->{ "name".upcase }

      expect(subject.to_json).to eql(JSON.dump(first_name: "Seth", nick: "NAME", json_class: "Playa"))
    end

    describe "when JSON.create_id is nil" do
      before do
        @_old_create_id = JSON.create_id
        JSON.create_id = nil
      end

      after do
        JSON.create_id = @_old_create_id
      end

      it "does not include a nil key" do
        skip ("This won't work on Ruby 3.0") unless RUBY_VERSION < '3'
        subject.first_name = "brooke"
        subject.nick = "leblanc"

        expect(subject.to_json).to eql(JSON.dump(first_name: "brooke", nick: "leblanc"))
      end
    end
  end

  describe "#to_hash" do
    it "returns all of the varia dattributes" do
      expect(subject.to_hash).to eql(subject.send(:_attributes_))
    end
  end
end
