# frozen_string_literal: true
# rubocop:todo all

require 'spec_helper'
require 'json'

describe BSON::DBRef do

  let(:object_id) do
    BSON::ObjectId.new
  end

  describe '#as_json' do

    context 'when the database is not provided' do

      let(:dbref) do
        described_class.new({ '$ref' => 'users', '$id' => object_id })
      end

      it 'returns the json document without database' do
        expect(dbref.as_json).to eq({ '$ref' => 'users', '$id' => object_id })
      end
    end

    context 'when the database is provided' do

      let(:dbref) do
        described_class.new({ '$ref' => 'users', '$id' => object_id, '$db' => 'database' })
      end

      it 'returns the json document with database' do
        expect(dbref.as_json).to eq({
          '$ref' => 'users',
          '$id' => object_id,
          '$db' => 'database'
        })
      end
    end

    context 'when other keys are provided' do

      let(:dbref) do
        described_class.new({ '$ref' => 'users', '$id' => object_id, '$db' => 'database', 'x' => 'y' })
      end

      it 'returns the json document with the other keys' do
        expect(dbref.as_json).to eq({
          '$ref' => 'users',
          '$id' => object_id,
          '$db' => 'database',
          'x' => 'y'
        })
      end
    end
  end

  describe '#initialize' do

    let(:dbref) do
      described_class.new(hash)
    end

    let(:hash) do
      { '$ref' => 'users', '$id' => object_id }
    end

    it 'sets the collection' do
      expect(dbref.collection).to eq('users')
    end

    it 'sets the id' do
      expect(dbref.id).to eq(object_id)
    end

    context 'when first argument is a hash and two arguments are provided' do

      let(:dbref) do
        described_class.new({:$ref => 'users', :$id => object_id}, object_id)
      end

      it 'raises InvalidDBRefArgument' do
        lambda do
          dbref
        end.should raise_error(BSON::Error::InvalidDBRefArgument)
      end
    end

    context 'when first argument is a hash and three arguments are provided' do

      let(:dbref) do
        described_class.new({:$ref => 'users', :$id => object_id}, object_id, 'db')
      end

      it 'raises InvalidDBRefArgument' do
        lambda do
          dbref
        end.should raise_error(BSON::Error::InvalidDBRefArgument)
      end
    end

    context 'when a database is provided' do

      let(:hash) do
        { '$ref' => 'users', '$id' => object_id, '$db' => 'db' }
      end

      it 'sets the database' do
        expect(dbref.database).to eq('db')
      end
    end

    context 'when not providing a collection' do
      let(:hash) do
        { '$id' => object_id, '$db' => 'db' }
      end

      it 'raises an error' do
        expect do
          dbref
        end.to raise_error(BSON::Error::InvalidDBRefArgument, /DBRef must have \$ref/)
      end
    end

    context 'when not providing an id' do
      let(:hash) do
        { '$ref' => 'users', '$db' => 'db' }
      end

      it 'raises an error' do
        expect do
          dbref
        end.to raise_error(BSON::Error::InvalidDBRefArgument, /DBRef must have \$id/)
      end
    end

    context 'when providing an invalid type for ref' do
      let(:hash) do
        { '$ref' => 1, '$id' => object_id }
      end

      it 'raises an error' do
        expect do
          dbref
        end.to raise_error(BSON::Error::InvalidDBRefArgument, /The value for key \$ref must be a string/)
      end
    end

    context 'when providing an invalid type for database' do
      let(:hash) do
        { '$ref' => 'users', '$id' => object_id, '$db' => 1 }
      end

      it 'raises an error' do
        expect do
          dbref
        end.to raise_error(BSON::Error::InvalidDBRefArgument, /The value for key \$db must be a string/)
      end
    end

    context 'when providing the fieds as symbols' do
      let(:hash) do
        { :$ref => 'users', :$id => object_id, :$db => 'db' }
      end

      it 'does not raise an error' do
        expect do
          dbref
        end.to_not raise_error
      end
    end

    context 'when testing the ordering of the fields' do
      context 'when the fields are in order' do
        let(:hash) do
          { '$ref' => 'users', '$id' => object_id, '$db' => 'db' }
        end

        it 'has the correct order' do
          expect(dbref.keys).to eq(['$ref', '$id', '$db'])
        end
      end

      context 'when the fields are out of order' do
        let(:hash) do
          { '$db' => 'db', '$id' => object_id, '$ref' => 'users' }
        end

        it 'has the correct order' do
          expect(dbref.keys).to eq(['$ref', '$id', '$db'])
        end
      end

      context 'when there is no db' do
        let(:hash) do
          { '$id' => object_id, '$ref' => 'users' }
        end

        it 'has the correct order' do
          expect(dbref.keys).to eq(['$ref', '$id'])
        end
      end

      context 'when the there are other fields in order' do
        let(:hash) do
          { '$ref' => 'users', '$id' => object_id, '$db' => 'db', 'x' => 'y', 'y' => 'z' }
        end

        it 'has the correct order' do
          expect(dbref.keys).to eq(['$ref', '$id', '$db', 'x', 'y'])
        end
      end

      context 'when the there are other fields out of order' do
        let(:hash) do
          { 'y' => 'z', '$db' => 'db', '$id' => object_id, 'x' => 'y', '$ref' => 'users' }
        end

        it 'has the correct order' do
          expect(dbref.keys).to eq(['$ref', '$id', '$db', 'y', 'x'])
        end
      end
    end
  end

  describe '#to_bson' do

    let(:dbref) do
      described_class.new({ '$ref' => 'users', '$id' => object_id, '$db' => 'database' })
    end

    it 'converts the underlying document to bson' do
      expect(dbref.to_bson.to_s).to eq(dbref.as_json.to_bson.to_s)
    end
  end

  describe '#to_json' do

    context 'when the database is not provided' do

      let(:dbref) do
        described_class.new({ '$ref' => 'users', '$id' => object_id })
      end

      it 'returns the json document without database' do
        expect(dbref.to_json).to eq("{\"$ref\":\"users\",\"$id\":#{object_id.to_json}}")
      end
    end

    context 'when the database is provided' do

      let(:dbref) do
        described_class.new({ '$ref' => 'users', '$id' => object_id, '$db' => 'database' })
      end

      it 'returns the json document with database' do
        expect(dbref.to_json).to eq("{\"$ref\":\"users\",\"$id\":#{object_id.to_json},\"$db\":\"database\"}")
      end
    end

    context 'when other keys are provided' do

      let(:dbref) do
        described_class.new({ '$ref' => 'users', '$id' => object_id, '$db' => 'database', 'x' => 'y' })
      end

      it 'returns the json document with the other keys' do
        expect(dbref.to_json).to eq("{\"$ref\":\"users\",\"$id\":#{object_id.to_json},\"$db\":\"database\",\"x\":\"y\"}")
      end
    end
  end

  describe '#from_bson' do

    let(:buffer) do
      hash.to_bson
    end

    let(:decoded) do
      BSON::Document.from_bson(BSON::ByteBuffer.new(buffer.to_s))
    end

    context 'when a database exists' do

      let(:hash) do
        { '$ref' => 'users', '$id' => object_id, '$db' => 'database' }
      end

      it 'decodes the ref' do
        expect(decoded.collection).to eq('users')
      end

      it 'decodes the id' do
        expect(decoded.id).to eq(object_id)
      end

      it 'decodes the database' do
        expect(decoded.database).to eq('database')
      end

      it 'is of class DBRef' do
        expect(decoded).to be_a described_class
      end
    end

    context 'when no database exists' do

      let(:hash) do
        { '$ref' => 'users', '$id' => object_id }
      end

      it 'decodes the ref' do
        expect(decoded.collection).to eq('users')
      end

      it 'decodes the id' do
        expect(decoded.id).to eq(object_id)
      end

      it 'sets the database to nil' do
        expect(decoded.database).to be_nil
      end

      it 'is of class DBRef' do
        expect(decoded).to be_a described_class
      end
    end

    context 'when other keys exist' do

      let(:hash) do
        { '$ref' => 'users', '$id' => object_id, 'x' => 'y' }
      end

      it 'decodes the key' do
        expect(decoded['x']).to eq('y')
      end

      it 'is of class DBRef' do
        expect(decoded).to be_a described_class
      end
    end

    context 'when it is an invalid dbref' do

      shared_examples 'bson document' do
        it 'should not raise' do
          expect do
            decoded
          end.to_not raise_error
        end

        it 'has the correct class' do
          expect(decoded).to be_a BSON::Document
          expect(decoded).to_not be_a described_class
        end
      end

      context 'when the hash has invalid collection type' do
        let(:hash) do
          { '$ref' => 1, '$id' => object_id }
        end
        include_examples 'bson document'
      end

      context 'when the hash has an invalid database type' do
        let(:hash) do
          { '$ref' => 'users', '$id' => object_id, '$db' => 1 }
        end
        include_examples 'bson document'
      end

      context 'when the hash is missing a collection' do
        let(:hash) do
          { '$id' => object_id }
        end
        include_examples 'bson document'
      end

      context 'when the hash is missing an id' do
        let(:hash) do
          { '$ref' => 'users' }
        end
        include_examples 'bson document'
      end
    end

    context 'when nesting the dbref' do

      context 'when it is a valid dbref' do
        let(:hash) do
          { 'dbref' => { '$ref' => 'users', '$id' => object_id } }
        end

        it 'should not raise' do
          expect do
            buffer
          end.to_not raise_error
        end

        it 'has the correct class' do
          expect(decoded['dbref']).to be_a described_class
        end
      end

      context 'when it is an invalid dbref' do

        shared_examples 'nested bson document' do
          it 'should not raise' do
            expect do
              decoded
            end.to_not raise_error
          end

          it 'has the correct class' do
            expect(decoded['dbref']).to be_a BSON::Document
            expect(decoded['dbref']).to_not be_a described_class
          end
        end

        context 'when the hash has invalid collection type' do
          let(:hash) do
            { 'dbref' => { '$ref' => 1, '$id' => object_id } }
          end
          include_examples 'nested bson document'
        end

        context 'when the hash has an invalid database type' do
          let(:hash) do
            { 'dbref' => { '$ref' => 'users', '$id' => object_id, '$db' => 1 } }
          end
          include_examples 'nested bson document'
        end

        context 'when the hash is missing a collection' do
          let(:hash) do
            { 'dbref' => { '$id' => object_id } }
          end
          include_examples 'nested bson document'
        end

        context 'when the hash is missing an id' do
          let(:hash) do
            { 'dbref' => { '$ref' => 'users' } }
          end
          include_examples 'nested bson document'
        end
      end
    end

    context 'when nesting a dbref inside a dbref' do
      context 'when it is a valid dbref' do
        let(:hash) do
          { 'dbref1' => { '$ref' => 'users', '$id' => object_id, 'dbref2' => { '$ref' => 'users', '$id' => object_id } } }
        end

        it 'should not raise' do
          expect do
            buffer
          end.to_not raise_error
        end

        it 'has the correct class' do
          expect(decoded['dbref1']).to be_a described_class
          expect(decoded['dbref1']['dbref2']).to be_a described_class
        end
      end

      context 'when it is an invalid dbref' do
        let(:hash) do
          { 'dbref' => { '$ref' => 'users', '$id' => object_id, 'dbref' => { '$ref' => 1, '$id' => object_id } } }
        end

        it 'should not raise' do
          expect do
            decoded
          end.to_not raise_error
        end

        it 'has the correct class' do
          expect(decoded['dbref']).to be_a described_class
          expect(decoded['dbref']['dbref']).to be_a BSON::Document
        end
      end
    end
  end
end
