require 'spec_helper'
require 'thread'

describe Consul::Power do

  before :each do
    @user = User.create!
    @deleted_client = Client.create!(:deleted => true)
    @client1 = Client.create!
    @client1_note1 = @client1.notes.create!
    @client1_note2 = @client1.notes.create!
    @client2 = Client.create!
    @client2_note1 = @client2.notes.create!
    @client2_note2 = @client2.notes.create!
  end

  describe 'integration scenario' do

    it 'should work with real records' do
      Client.active.to_a.should =~ [@client1, @client2]
      @client1.notes.to_a.should =~ [@client1_note1, @client1_note2]
    end

  end

  context 'nil powers' do

    describe 'query methods' do

      context 'when no record is given' do

        it 'should return false' do
          @user.role = 'guest'
          @user.power.clients.should be_nil
          @user.power.clients?.should == false
        end

      end

      context 'with a given record' do

        it 'should return false' do
          client = Client.create!
          @user.role = 'guest'
          @user.power.clients.should be_nil
          @user.power.client?(client).should == false
        end

      end

    end

    describe 'bang methods' do

      context 'when no record is given' do

        it 'should raise Consul::Powerless when the power returns nil' do
          @user.role = 'guest'
          @user.power.clients.should be_nil
          expect { @user.power.clients! }.to raise_error(Consul::Powerless)
        end

      end

      context 'with a given record' do

        it 'should raise Consul::Powerless when argument is not in the power scope' do
          client = Client.create!
          @user.role = 'guest'
          @user.power.clients.should be_nil
          expect { @user.power.client!(client) }.to raise_error(Consul::Powerless)
        end

      end

    end

  end

  context 'scope powers' do

    it 'should return the registered scope' do
      @user.power.clients.all.to_a.should =~ [@client1, @client2]
    end

    it 'should allow to register scopes with arguments' do
      @user.power.client_notes(@client1).to_a.should =~ [@client1_note1, @client1_note2]
    end

    it 'should preserve the arity of a power block with arguments' do
      @user.power.method(:client_notes).arity.should == 1
    end

    describe 'query methods' do

      context 'when no record is given' do

        it 'should return true if the power returns a scope (which might or might not match records)' do
          @user.power.clients?.should == true
        end

        it 'should not trigger a query if the power returns a has many association' do
          Note.count.should > 0 # show that we have records in the database
          @client1.reload
          @user.power.client_notes?(@client1)
          @client1.notes.loaded?.should == false
        end

      end

      context 'with a given record' do

        it 'should return true if the record belongs to the scope' do
          @user.power.client?(@client1).should == true
        end

        it 'should return false if the record does not belong to the scope' do
          @user.power.client?(@deleted_client).should == false
        end

        it 'should only trigger a single query for multiple checks on the same scope' do
          @user.power.should_receive(:database_touched).once
          @user.power.client?(@client1)
          @user.power.client?(@deleted_client)
        end

        it 'should trigger a query only if the scope defines a condition' do
          @user.power.should_receive(:database_touched).once
          @user.power.client?(@client1)
        end

        it 'should not trigger a query if the scope defines no conditions' do
          @user.power.should_not_receive(:database_touched)
          @user.power.all_client?(@client1)
        end

        it 'should always trigger a query if a returned model has a default scope, even if it defines no additional conditions' do
          @user.power.should_receive(:database_touched).once
          @user.power.song?(Song.new)
        end

        it 'should trigger query if a returned model has a default scope and defines additional conditions' do
          @user.power.should_receive(:database_touched).once
          @user.power.recent_song?(Song.new)
        end

        it 'should work with powers that have arguments' do
          @user.power.client_note?(@client1, @client1_note1).should == true
          @user.power.client_note?(@client1, @client2_note1).should == false
        end

        it 'should raise an error with helpful message when powers are called with wrong arguments' do
          expect { @user.power.client_note?(@client1_note1) }.to raise_error(ArgumentError, 'wrong number of arguments (given 1, expected 2)')
        end

        context 'optimization through additional definition of a Ruby method' do

          it 'should not affect collection access' do
            @user.power.fast_clients.to_a.should =~ [@client1, @client2]
            @user.power.fast_client_ids.to_a.should =~ [@client1.id, @client2.id]
            @user.power.fast_clients?.should == true
          end

          it 'should no longer access the database when checking inclusion of a single record' do
            @user.power.should_not_receive(:database_touched)
            @user.power.fast_client?(@client1).should == true
            @user.power.fast_client?(@deleted_client).should == false
          end

          it 'is used by and bang methods' do
            @user.power.should_not_receive(:database_touched)
            expect { @user.power.fast_client!(@client1) }.to_not raise_error
            expect { @user.power.fast_client!(@deleted_client) }.to raise_error(Consul::Powerless)
          end

          it 'returns a true for the bang method if the record is in the power scope' do
            @user.power.fast_client!(@client1).should == true
          end

          it 'works with parametrized powers' do
            @user.power.should_not_receive(:database_touched)
            @user.power.fast_client_note?(@client1, @client1_note1).should == true
            @user.power.fast_client_note?(@client1, @client2_note1).should == false
            expect { @user.power.fast_client_note!(@client1, @client1_note1) }.to_not raise_error
            expect { @user.power.fast_client_note!(@client1, @client2_note1) }.to raise_error(Consul::Powerless)
          end

          it 'works when only the optimized power is defined, without a collection power' do
            @user.power.should_not_receive(:database_touched)
            @user.power.fast_client_note_without_collection?(@client1, @client1_note1).should == true
            @user.power.fast_client_note_without_collection?(@client1, @client2_note1).should == false
          end

        end

      end

      #context 'when a power with arguments is given insufficient context' do
      #
      #  it 'should raise an error' do
      #    expect { @user.power.client_notes? }.to raise_error(ArgumentError)
      #  end
      #
      #end

    end

    describe 'bang methods' do

      context 'when no record is given' do

        it 'should not raise Consul::Powerless when the power returns a scope (which might or might not match records)' do
          expect { @user.power.clients! }.to_not raise_error
        end

        it 'should return the scope' do
          @user.power.clients!.to_a.should =~ [@client1, @client2]
        end

      end

      context 'with a given record' do

        it 'should raise Consul::Powerless when the record is inside the scope' do
          expect { @user.power.client!(@deleted_client) }.to raise_error(Consul::Powerless)
        end

        it 'should not raise Consul::Powerless when the record is inside the scope' do
          expect { @user.power.client!(@client1) }.to_not raise_error
          @user.power.client!(@client1).should == true
        end

        it 'should work with scopes that have arguments' do
          expect { @user.power.client_note!(@client1, @client1_note1) }.to_not raise_error
          @user.power.client_note!(@client1, @client1_note1).should == true
          expect { @user.power.client_note!(@client1, @client2_note1) }.to raise_error(Consul::Powerless)
        end

      end

    end

    describe 'retrieving scope_ids' do

      it 'should return record ids that match the registered scope' do
        @user.power.client_ids.should =~ [@client1.id, @client2.id]
      end

      it 'should cache scope ids' do
        @user.power.should_receive(:clients).once.and_return(double('scope', :construct_finder_sql => 'SELECT 1', :to_sql => 'SELECT 1').as_null_object)
        2.times { @user.power.client_ids }
      end

      it 'should return ids when the scope joins another table (bugfix)' do
        expect { @user.power.note_ids }.to_not raise_error
      end

      it 'should work with scopes that have arguments' do
        @user.power.client_note_ids(@client1).should =~ [@client1_note1.id, @client1_note2.id]
      end

    end

  end

  context 'collection powers' do

    it 'should return the registered collection' do
      @user.power.key_figures.should =~ %w[amount working_costs]
    end

    it 'should memoize the collection' do
      @user.power.should_receive(:compute_key_figures).once
      2.times { @user.power.key_figures }
    end

    describe 'query methods' do

      context 'when no record is given' do

        it 'should return true if the returns an enumerable (which might or might not be empty)' do
          @user.power.key_figures?.should == true
        end

        it 'should return false if the power returns nil' do
          @user.role = 'guest'
          @user.power.key_figures.should be_nil
          @user.power.key_figures?.should == false
        end

      end

      context 'with a given record' do

        it 'should return true if the power contains the given record' do
          @user.power.key_figure?('amount').should == true
        end

        it 'should return false if the power does not contain the given record' do
          @user.power.key_figure?('xyz').should == false
        end

      end

    end

    describe 'bang methods' do

      context 'when no record is given' do

        it 'should not raise Consul::Powerless if the power returns an enumerable (which might or might not be empty)' do
          expect { @user.power.key_figures! }.to_not raise_error
        end

        it 'should return the registered collection' do
          @user.power.key_figures!.should =~ %w[amount working_costs]
        end

        it 'should raise Consul::Powerless if the power returns nil' do
          @user.role = 'guest'
          @user.power.key_figures.should be_nil
          expect { @user.power.key_figures! }.to raise_error(Consul::Powerless)
        end

      end

      context 'with a given record' do

        it 'should not raise Consul::Powerless if the power contains the given record' do
          expect { @user.power.key_figure!('amount') }.to_not raise_error
        end

        it 'returns true if the power contains the given record' do
          @user.power.key_figure!('amount').should == true
        end

        it 'should raise Consul::Powerless if the power does not contain the given record' do
          expect { @user.power.key_figure!('xyz') }.to raise_error(Consul::Powerless)
        end
      end

    end

  end

  context 'boolean powers' do

    it 'should return the registered value' do
      @user.power.always_true.should == true
      @user.power.always_false.should == false
    end

    describe 'query methods' do

      context 'when no record is given' do

        it 'should return true when the queried power returns true' do
          @user.power.always_true?.should == true
        end

        it 'should return false when the queried power returns false' do
          @user.power.always_false?.should == false
        end

        it 'should return false when the queried power returns nil' do
          @user.power.always_nil?.should == false
        end

      end

      context 'with a given record' do

        #it 'should raise Consul::NoCollection' do
        #  expect { @user.power.always_true?('foo') }.to raise_error(Consul::NoCollection)
        #end

      end

    end

    describe 'bang methods' do

      context 'when no record is given' do

        it 'should not raise Consul::Powerless when the power returns true' do
          expect { @user.power.always_true! }.to_not raise_error
        end

        it 'should return the power value when the power returns true' do
          @user.power.always_true!.should == true
        end

        it 'should raise Consul::Powerless when the power returns false' do
          expect { @user.power.always_false! }.to raise_error(Consul::Powerless)
        end

        it 'should raise Consul::Powerless when the power returns nil' do
          expect { @user.power.always_nil! }.to raise_error(Consul::Powerless)
        end

      end

      context 'with a given record' do

        #it 'should raise Consul::NoCollection' do
        #  expect { @user.power.always_true!('foo') }.to raise_error(Consul::NoCollection)
        #end

      end

    end


  end

  context 'powers of other types' do

    it 'should return the registered value' do
      @user.power.api_key.should == 'secret-api-key'
    end

    describe 'query methods' do

      context 'when no record is given' do

        it 'should return true if the power is not nil' do
          @user.power.api_key?.should == true
        end

        it 'should return false if the power is nil' do
          @user.role = 'guest'
          @user.power.api_key.should be_nil
          @user.power.api_key?.should == false
        end

      end

      context 'with a given record' do

        #it 'should raise Consul::NoCollection' do
        #  expect { @user.power.api_key?('foo') }.to raise_error(Consul::NoCollection)
        #end

      end

    end

    describe 'bang methods' do

      context 'when no record is given' do

        it 'should not raise Consul::Powerless if the power is not nil' do
          expect { @user.power.api_key! }.to_not raise_error
        end

        it 'should return the power value if the power is not nil' do
          @user.power.api_key!.should =='secret-api-key'
        end

        it 'should raise Consul::powerless if the power is nil' do
          @user.role = 'guest'
          @user.power.api_key.should be_nil
          expect { @user.power.api_key! }.to raise_error(Consul::Powerless)
        end

      end

      context 'with a given record' do

        #it 'should raise Consul::NoCollection' do
        #  expect { @user.power.api_key!('foo') }.to raise_error(Consul::NoCollection)
        #end

      end

    end

  end

  describe '.power' do

    it 'should allow to define multiple powers at once' do
      @user.power.shorthand1.should == 'shorthand'
      @user.power.shorthand2.should == 'shorthand'
      @user.power.shorthand3.should == 'shorthand'
    end

  end

  describe '.current' do

    it 'should provide a class method to set and get the current Power' do
      Power.current = 'current power'
      Power.current.should == 'current power'
      Power.current = nil
      Power.current.should be_nil
    end

  end

  describe '.current=' do

    it 'should set the given Power for the current thread only' do
      Thread.new do
        Power.current = 'some power'
      end.join
      Power.current.should be_nil
    end

    it 'should keep multiple powers separated' do
      FrontendPower = Class.new(Power)
      BackendPower = Class.new(Power)
      frontend_power = FrontendPower.new
      backend_power = BackendPower.new
      FrontendPower.current = frontend_power
      BackendPower.current = backend_power

      FrontendPower.current.should == frontend_power
      BackendPower.current.should == backend_power
    end

  end

  describe '.with_power' do

    it 'should provide the given power as current power for the duration of the block' do
      spy = double
      inner_power = Power.new('inner')
      Power.current = 'outer power'
      spy.should_receive(:observe).with(inner_power)
      Power.with_power(inner_power) do
        spy.observe(Power.current)
      end
      Power.current.should == 'outer power'
      Power.current = nil # clean up for subsequent specs -- too bad we can't use .with_power :)
    end

    it 'should restore an existing power even if the block raises an error' do
      begin
        inner_power = Power.new('inner')
        Power.current = 'outer power'
        Power.with_power(inner_power) do
          raise ZeroDivisionError
        end
      rescue ZeroDivisionError
        # do nothing
      end
      Power.current.should == 'outer power'
      Power.current = nil # clean up for subsequent specs -- too bad we can't use .with_power :)
    end

    it 'should instantiate a new Power if the given argument is not already a power' do
      spy = double
      Power.should_receive(:new).with('argument').and_return('instantiated power')
      spy.should_receive(:observe).with('instantiated power')
      Power.with_power('argument') do
        spy.observe(Power.current)
      end
    end

    it 'should instantiate a new Power with all given arguments if it receives more than one regular argument' do
      spy = double
      Power.should_receive(:new).with('more', 'than', 'one').and_return('instantiated power')
      spy.should_receive(:observe).with('instantiated power')
      Power.with_power('more', 'than', 'one') do
        spy.observe(Power.current)
      end
    end

    it 'should instantiate a new Power with all given arguments if it receives any keyword argument' do
      spy = double
      Power.should_receive(:new).with(keyword: 'argument').and_return('instantiated power')
      spy.should_receive(:observe).with('instantiated power')
      Power.with_power(keyword: 'argument') do
        spy.observe(Power.current)
      end
    end

    it 'should instantiate a new Power with all given arguments if it receives regular AND keyword argument' do
      spy = double
      Power.should_receive(:new).with('regular', 'and', keyword: 'arguments').and_return('instantiated power')
      spy.should_receive(:observe).with('instantiated power')
      Power.with_power('regular', 'and', keyword: 'arguments') do
        spy.observe(Power.current)
      end
    end

    it 'should not instantiate a new Power if the given argument is nil' do
      spy = double
      Power.should_not_receive(:new)
      spy.should_receive(:observe).with(nil)
      Power.with_power(nil) do
        spy.observe(Power.current)
      end
    end

  end

  describe '#for_model' do

    it 'should return the power corresponding to the given model' do
      @user.power.for_model(Deal).should == 'deals power'
    end

    it 'should return the correct power for a namespaced model' do
      @user.power.for_model(Deal::Item).should == 'deal_items power'
    end

    it 'should allow to prefix the power with an adjective' do
      @user.power.for_model(:updatable, Deal).should == 'updatable_deals power'
    end

  end

  describe '.for_model' do

    context 'when Power.current is present' do

      it 'should return the power corresponding to the given model' do
        Power.with_power(@user.power) do
          Power.for_model(Deal).should == 'deals power'
        end
      end

      it 'should allow to prefix the power with an adjective' do
        Power.with_power(@user.power) do
          Power.for_model(:updatable, Deal).should == 'updatable_deals power'
        end
      end

    end

    context 'when Power.current is nil' do

      it 'should return the given model' do
        Power.for_model(Deal).should == Deal
      end

      it 'should return the given model even if the model was prefixed with an adjective' do
        Power.for_model(:updatable, Deal).should == Deal
      end

    end

  end

  describe '#include_model?' do

    it 'should return if the given model corresponds to a non-nil power' do
      @user.role = 'guest'
      Power.new(@user).include_model?(Client).should == false
      @user.role = 'admin'
      Power.new(@user).include_model?(Client).should == true
    end

  end

  describe '.include_model?' do

    context 'when Power.current is present' do

      it 'should return whether the given model corresponds to a non-nil power' do
        @user.role = 'guest'
        Power.with_power(Power.new(@user)) do
          Power.include_model?(Deal).should == false
        end

        @user.role = 'admin'
        Power.with_power(Power.new(@user)) do
          Power.include_model?(Deal).should == true
        end
      end
    end

    context 'when Power.current is nil' do

      it 'should return true' do
        Power.include_model?(Deal).should == true
      end

    end

  end

  describe '#include_record?' do

    it 'should return if the given record is included in the power corresponding to the class of the given record' do
      @user.power.include_record?(@deleted_client).should == false
      @user.power.include_record?(@client1).should == true
    end

  end

  describe '.include_record?' do

    context 'when Power.current is present' do

      it 'should return whether the given record is included in the the power corresponding to the class of the given record' do
        Power.with_power(@user.power) do
          Power.include_record?(@deleted_client).should == false
          Power.include_record?(@client1).should == true
        end
      end
    end

    context 'when Power.current is nil' do

      it 'should return true' do
        Power.include_record?(Deal.new).should == true
      end

    end

  end


end
