# Copyright 2010-present Basho Technologies, Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

require 'spec_helper'
require 'riak'
require 'riak/errors/failed_request'

describe "CRDTs", integration: true, test_client: true do
  let(:bucket) { random_bucket }

  describe 'Riak-assigned names' do
    describe 'an anonymous counter' do
      subject { Riak::Crdt::Counter.new bucket, nil }
      it 'accepts a Riak-assigned name' do
        subject.increment
        expect(subject.key).to be
        expect(subject.value).to eq 1
      end
    end

    describe 'an anonymous set' do
      subject { Riak::Crdt::Set.new bucket, nil }
      it 'accepts a Riak-assigned name' do
        subject.add 'sandwich'
        expect(subject.key).to be
        expect(subject).to include 'sandwich'
      end
    end

    describe 'an anonymous map' do
      subject { Riak::Crdt::Map.new bucket, nil }
      it 'accepts a Riak-assigned name' do
        subject.registers['coat_pattern'] = 'tabby'
        expect(subject.key).to be
        expect(subject.registers['coat_pattern']).to eq 'tabby'
      end
    end
  end

  describe 'counters' do
    subject { Riak::Crdt::Counter.new bucket, random_key }
    it 'allows straightforward counter ops' do
      start = subject.value
      subject.increment
      expect(subject.value).to eq(start + 1)
      subject.increment
      expect(subject.value).to eq(start + 2)
      subject.increment -1
      expect(subject.value).to eq(start + 1)
      subject.decrement
      expect(subject.value).to eq(start)
    end

    it 'allows batched counter ops' do
      start = subject.value
      subject.batch do |s|
        s.increment
        s.increment 2
        s.increment
        s.increment
      end
      expect(subject.value).to eq(start + 5)
    end

    it 'asks for and accepts a returned body by default' do
      other = Riak::Crdt::Counter.new subject.bucket, subject.key

      start = subject.value

      expect(subject.value).to eq start

      other.increment 10

      subject.increment 1

      expect(subject.dirty?).to_not be
      expect(subject.value).to eq(start + 10 + 1)
    end

    describe 'equality' do
      let(:same){ Riak::Crdt::Counter.new subject.bucket, subject.key }
      let(:same_bucket){ Riak::Bucket.new test_client, bucket.name }
      let(:similar){ Riak::Crdt::Counter.new same_bucket, subject.key }
      it { is_expected.to eq subject }
      it { is_expected.to eq same }
      it { is_expected.to eq similar }
    end
  end

  describe 'sets' do

    subject { Riak::Crdt::Set.new bucket, random_key }

    it 'allows straightforward set ops' do
      start = subject.members
      addition = random_key

      subject.add addition
      expect(subject.include? addition).to be
      expect(subject.members).to include(addition)

      subject.remove addition
      expect(subject.include? addition).to_not be
      expect(subject.members).to_not include(addition)
      expect(subject.members).to eq(start)
    end

    it 'lets Riak silently accept removals after reload' do
      addition = random_key
      subject.add addition

      other = Riak::Crdt::Set.new subject.bucket, subject.key
      expect{ other.remove addition }.to raise_error(Riak::CrdtError::SetRemovalWithoutContextError)
      other.reload
      expect{ other.remove addition }.to_not raise_error
      other.reload
      expect{ other.remove 'an element not in the set' }.to_not raise_error
    end

    it 'allows batched set ops' do
      subject.add 'zero'
      subject.reload

      subject.batch do |s|
        s.add 'first'
        s.remove 'zero'
      end

      expect(subject.members.to_a).to eq %w{first}
    end

    it 'asks for and accepts a returned body by default' do
      other = Riak::Crdt::Set.new subject.bucket, subject.key

      expect(subject.include? 'coffee').to_not be
      expect(other.include? 'coffee').to_not be

      other.add 'coffee'
      subject.add 'tea'

      expect(subject.dirty?).to_not be

      expect(other.include? 'coffee').to be
      expect(subject.include? 'coffee').to be
    end
  end

  describe 'maps' do
    subject { Riak::Crdt::Map.new bucket, random_key }

    it 'allows straightforward map ops' do
      subject.registers['first'] = 'hello'
      expect(subject.registers['first']).to eq('hello')

      subject.sets['arnold'].add 'commando'
      subject.sets['arnold'].add 'terminator'
      expect(subject.sets['arnold'].members).to include('commando')
      subject.sets['arnold'].remove 'commando'
      expect(subject.sets['arnold'].members).to_not include('commando')
      expect(subject.sets['arnold'].members).to include('terminator')

      subject.maps['first'].registers['second'] = 'good evening'
      subject.maps['first'].maps['third'].counters['fourth'].increment

      expect(subject.maps['first'].registers['second']).to eq('good evening')
      expect(subject.maps['first'].maps['third'].counters['fourth'].value).to eq(1)

      subject.counters['hits'].increment
      expect(subject.counters['hits'].value).to eq 1

      subject.flags['yes'] = true
      expect(subject.flags['yes']).to eq true

      expect do
        subject.registers.delete 'first'
        subject.sets.delete 'arnold'
        subject.maps.delete 'first'
        subject.counters.delete 'hits'
        subject.flags.delete 'yes'
      end.to_not raise_error
    end

    it 'allows batched map ops' do
      subject.batch do |s|
        s.registers['condiment'] = 'ketchup'
        s.counters['banana'].increment
      end

      expect(subject.registers['condiment']).to eq 'ketchup'
      expect(subject.counters['banana'].value).to eq 1
    end

    it 'asks for and accepts a returned body by default' do
      other = Riak::Crdt::Map.new subject.bucket, subject.key

      expect(subject.sets['bees'].include? 'honey').to_not be
      expect(other.sets['bees'].include? 'honey').to_not be

      other.sets['bees'].add 'honey'
      subject.counters['stings'].increment

      expect(subject.dirty?).to_not be

      expect(other.sets['bees'].include? 'honey').to be
      expect(subject.sets['bees'].include? 'honey').to be
    end

    describe 'containing a map' do
      it 'bubbles straightforward map ops up' do
        street_map = subject.maps['street']

        street_map.registers['bird'] = 'avenue'
        street_map.flags['traffic_light'] = false

        expect(subject.maps['street'])
      end

      it 'includes inner-map ops in the outer-map batch' do
        subject.batch do |m|
          m.maps['road'].counters['speedbumps'].increment 4
          m.maps['road'].sets['signs'].add 'yield'
        end

        expect(subject.maps['road'].counters['speedbumps'].value).to eq 4
        expect(subject.maps['road'].sets['signs'].include? 'yield').to be
      end

      it 'deletes nested inner-map' do
        bag_map = subject.maps['bag']
        inner_map = bag_map.maps['123']
        inner_map.registers['name'] = 'f1'

        expect(subject.maps['bag'].maps['123'].registers['name']).to eq 'f1'

        expect{ bag_map.maps.delete('123') }.to_not raise_error
        expect(bag_map.maps.include? '123').to_not be
        expect(bag_map.maps['123'].registers['name']).to_not be
      end
    end

    describe 'containing a register' do
      it 'bubbles straightforward register ops up' do
        subject.registers['hkey_local_machine'] = 'registry'

        expect(subject.registers['hkey_local_machine']).to eq 'registry'
      end

      it "doesn't error on an unset register" do
        expect{ subject.registers['unset'] }.to_not raise_error
        expect(subject.registers['other_unset']).to_not be
      end
    end

    describe 'containing a flag' do
      it 'bubbles straightforward flag ops up' do
        subject.flags['enable_magic'] = true

        expect(subject.flags['enable_magic']).to be
      end

      it "doesn't error on an unset flag" do
        expect{ subject.flags['unset'] }.to_not raise_error
        expect(subject.flags['other_unset']).to_not be
      end
    end
  end

  describe 'HLLs', hll: true do
    before(:each) do
      ensure_datatype_exists :hll
    end

    subject { Riak::Crdt::HyperLogLog.new bucket, random_key }

    it 'allows straightforward HLL ops' do
      addition = random_key

      subject.add addition

      expect(subject.value).to be_a(Integer)
      expect(subject.value).to eq 1
    end

    it 'asks for and accepts a returned body by default' do
      other = Riak::Crdt::HyperLogLog.new subject.bucket, subject.key

      other.add 'coffee'

      expect(other.value).to be_a(Integer)
      expect(other.value).to eq 1

      expect(subject.value).to eq 1

      subject.add 'tea'

      expect(subject.value).to be_a(Integer)
      expect(subject.value).to eq 2

      other.reload
      expect(other.value).to eq 2

      other.add 'juice'

      expect(other.value).to be_a(Integer)
      expect(other.value).to eq 3

      # repeat input to verify the HLL value doesn't increment
      other.add 'juice'

      expect(other.value).to be_a(Integer)
      expect(other.value).to eq 3

      expect(subject.dirty?).to_not be
    end

    it 'allows batched HLL ops' do
      subject.add 'zero'
      subject.reload

      subject.batch do |s|
        s.add 'first'
        s.add 'second'
        s.add 'second'
      end

      expect(subject.value).to be_a(Integer)
      expect(subject.value).to eq 3
    end
  end
end
