require 'spec_helper'

RSpec.describe JSON::SchemaBuilder::Entity, type: :unit do
  subject{ described_class }

  it{ is_expected.to define_attribute :title }
  it{ is_expected.to define_attribute :description }
  it{ is_expected.to define_attribute :type }
  it{ is_expected.to define_attribute :default }
  it{ is_expected.to define_attribute :enum }
  it{ is_expected.to define_attribute :all_of }
  it{ is_expected.to define_attribute :any_of }
  it{ is_expected.to define_attribute :one_of }
  it{ is_expected.to define_attribute :not_a }
  it{ is_expected.to define_attribute :ref }
  it{ is_expected.to define_attribute :definitions }

  describe ".disable_attributes!" do
    let(:disabled_attributes) { [:default, :ref, :definitions] }

    around(:each) do |example|
      described_class.disable_attributes! *disabled_attributes
      example.call
      disabled_attributes.each { |attr| described_class.attribute(attr) }
    end

    it "should remove the attribute methods" do
      entity = described_class.new "test"
      disabled_attributes.each do |attr|
        expect(entity).to_not respond_to(attr)
        expect(entity).to_not respond_to("#{attr}=")
      end
    end
  end

  describe '.attribute' do
    include_context 'an entity'

    it 'should define the attribute' do
      expect(klass).to define_attribute :test
    end

    it 'should delegate the reader to the schema' do
      subject.schema[:test] = 1
      expect(subject.test).to eql 1
    end

    it 'should delegate the writer to the schema' do
      subject.schema[:test] = 1
      expect(subject.schema[:test]).to eql 1
    end

    it 'should accept function-style writes' do
      subject.test 1
      expect(subject.test).to eql 1
    end

    it 'should snakeCase attribute reads' do
      subject.schema[:testName] = 1
      expect(subject.test_name).to eql 1
    end

    it 'should snakeCase attribute writes' do
      subject.test_name = 1
      expect(subject.schema[:testName]).to eql 1
    end

    it 'should handle array argument reads' do
      subject.schema[:testList] = [1, 2, 3]
      expect(subject.test_list).to eql [1, 2, 3]
    end

    it 'should handle array argument writes' do
      subject.test_list = [1, 2, 3]
      expect(subject.schema[:testList]).to eql [1, 2, 3]
    end

    it 'should handle array argument lists' do
      subject.test_list 1, 2, 3
      expect(subject.schema[:testList]).to eql [1, 2, 3]
    end

    it 'should handle arbitrary key writes' do
      subject.test_as 1
      expect(subject.schema[:testAs]).to be_nil
      expect(subject.schema[:testOther]).to eql 1
    end

    it 'should handle arbitrary key reads' do
      subject.schema[:testOther] = 1
      expect(subject.test_as).to eql 1
    end
  end

  describe '#initialize' do
    include_context 'an entity with a parent'

    its(:name){ is_expected.to eql 'name' }
    its(:title){ is_expected.to eql 'test' }
    its(:parent){ is_expected.to eql parent }
    its('schema.data'){ is_expected.to include evaluated_block: true }
    its('parent.children'){ is_expected.to include subject }
  end

  describe '#required=' do
    include_context 'an entity with a parent'

    it 'should delegate to the parent entity' do
      expect{
        subject.required = 'name'
      }.to change{
        parent.required
      }.from([]).to ['name']
    end
  end

  %w(validate validate! fully_validate).each do |validator|
    describe "##{ validator }" do
      let(:klass) do
        Class.new do
          include JSON::SchemaBuilder
          configure{ |opts| opts.working = true }

          def example
            object{ string :name }
          end
        end
      end
      subject{ klass.new.example }

      it "should #{ validator }" do
        expect(JSON::Validator).to receive(validator)
          .with subject.as_json, { }, working: true, opts: true
        subject.send validator, { }, opts: true
      end
    end
  end

  describe '#merge_children!' do
    include_context 'an entity with a parent'

    let(:child_schema){ double 'JSON::SchemaBuilder::Schema' }
    let(:child){ double 'JSON::SchemaBuilder::Entity', schema: child_schema }

    it 'should merge children schemas' do
      expect(child).to receive :schema
      expect(subject).to receive(:children).and_return [child]
      expect(subject.schema).to receive(:merge!).with child_schema
      subject.merge_children!
    end
  end

  describe '#as_json' do
    include_context 'an entity'

    it 'should delegate to schema' do
      expect(subject.schema).to receive(:as_json)
      subject.as_json
    end
  end

  describe "array attributes" do
    let(:klass) do
      Class.new do
        include JSON::SchemaBuilder

        def example
          object do
            object :parent_name do
              any_of [
                string(:name)
              ]
            end
          end
        end
      end
    end
    subject(:schema){ klass.new }

    it "prevents elements from being named" do
      expect(schema.example.fragments.keys).to match_array(%w(#/ #/parent_name))
    end
  end

  describe "#extend" do
    let(:object) do
      obj = JSON::SchemaBuilder::Object.new(nil) do
        string :test
        object :sub_object do
          string :a_string
        end
      end

      obj.extend(:sub_object) do
        string :another_string
        number :a_number, required: true
      end

      obj
    end
    let(:schema) { object.schema }
    let(:properties) { schema[:properties] }
    let(:sub_object) { properties[:sub_object] }
    let(:sub_properties) { sub_object[:properties] }

    it "does not remove properties" do
      expect(properties[:test]).to eq "type" => "string"
      expect(sub_object).to include "type" => "object"
      expect(sub_properties).to include "a_string" => {"type" => "string"}
    end

    it "adds new properties" do
      expect(sub_properties).to include "another_string" => {"type" => "string"}
      expect(sub_properties).to include "a_number" => {"type" => "number"}
      expect(sub_object[:required]).to match_array ["a_number"]
    end

    it "raises on unknown properties" do
      expect do
        object.extend(:foo) do
          raise "doesn't work"
        end
      end.to raise_error "Property foo does not exist"
    end

    it "returns the child" do
      child = object.extend(:sub_object)
      expect(child).to be_a(JSON::SchemaBuilder::Object)
      expect(child.name).to eq(:sub_object)
    end
  end
end