# frozen_string_literal: true

RSpec.describe 'prevent_all with exceptions' do
  let(:policy_class) do
    Class.new(DeclarativePolicy::Base) do
      condition(:blocked) { @subject.blocked }

      rule { blocked }.prevent_all do
        except :read
        except :list
      end

      rule { default }.policy do
        enable :read
        enable :list
        enable :edit
        enable :admin
      end
    end
  end

  before do
    stub_const('Resource', Struct.new(:blocked))
    stub_const('ResourcePolicy', policy_class)
  end

  subject(:policy) { DeclarativePolicy.policy_for(nil, policy_subject) }

  context 'when the subject is blocked' do
    let(:policy_subject) { Resource.new(true) }

    it 'allows excepted abilities' do
      expect(policy).to be_allowed(:read)
      expect(policy).to be_allowed(:list)
    end

    it 'prevents non-excepted abilities' do
      expect(policy).not_to be_allowed(:edit)
      expect(policy).not_to be_allowed(:admin)
    end

    it 'is not fully banned (excepted abilities can still pass)' do
      expect(policy.banned?).to be false
    end
  end

  context 'when the subject is not blocked' do
    let(:policy_subject) { Resource.new(false) }

    it 'allows all abilities' do
      expect(policy).to be_allowed(:read)
      expect(policy).to be_allowed(:list)
      expect(policy).to be_allowed(:edit)
      expect(policy).to be_allowed(:admin)
    end
  end

  context 'with multiple except calls on one line' do
    let(:policy_class) do
      Class.new(DeclarativePolicy::Base) do
        condition(:blocked) { @subject.blocked }

        rule { blocked }.prevent_all do
          except :read, :list
        end

        rule { default }.policy do
          enable :read
          enable :list
          enable :edit
        end
      end
    end

    let(:thing) { Resource.new(true) }

    subject(:policy) { DeclarativePolicy.policy_for(nil, thing) }

    it 'excepts all listed abilities' do
      expect(policy).to be_allowed(:read)
      expect(policy).to be_allowed(:list)
    end

    it 'prevents non-excepted abilities' do
      expect(policy).not_to be_allowed(:edit)
    end
  end

  context 'with delegated policies' do
    let(:delegating_policy) do
      Class.new(DeclarativePolicy::Base) do
        delegate { @subject.parent }

        condition(:delegating_active) { @subject.active }

        rule { delegating_active }.policy do
          enable :comment
        end
      end
    end

    before do
      stub_const('Delegate', Struct.new(:parent, :active))
      stub_const('DelegatePolicy', delegating_policy)
    end

    context 'when the delegated subject is blocked' do
      let(:parent) { Resource.new(true) }
      let(:policy_subject) { Delegate.new(parent, true) }

      it 'allows excepted abilities from the delegated policy' do
        expect(policy).to be_allowed(:read)
      end

      it 'prevents non-excepted abilities from the delegated policy' do
        expect(policy).not_to be_allowed(:edit)
        expect(policy).not_to be_allowed(:admin)
      end

      it 'prevents abilities defined on the delegating that are not excepted' do
        expect(policy).not_to be_allowed(:comment)
      end
    end

    context 'when the delegated subject is not blocked' do
      let(:parent) { Resource.new(false) }
      let(:policy_subject) { Delegate.new(parent, true) }

      it 'allows all delegated abilities' do
        expect(policy).to be_allowed(:read)
        expect(policy).to be_allowed(:edit)
        expect(policy).to be_allowed(:admin)
      end

      it 'allows abilities directly enabled' do
        expect(policy).to be_allowed(:comment)
      end
    end

    context 'with overrides on the delegating policy' do
      let(:delegating_policy) do
        Class.new(DeclarativePolicy::Base) do
          delegate { @subject.parent }

          overrides :edit

          condition(:active) { @subject.active }

          rule { active }.policy do
            enable :edit
            enable :comment
          end
        end
      end

      let(:parent) { Resource.new(true) }
      let(:policy_subject) { Delegate.new(parent, true) }

      it 'allows excepted abilities from the delegated policy' do
        expect(policy).to be_allowed(:read)
      end

      it 'allows overridden abilities regardless of delegated prevent_all' do
        expect(policy).to be_allowed(:edit)
      end

      it 'prevents non-excepted, non-overridden abilities from delegated policy' do
        expect(policy).not_to be_allowed(:admin)
        expect(policy).not_to be_allowed(:comment)
      end
    end
  end

  context 'with inherited policies' do
    let(:sub_policy) do
      Class.new(policy_class) do
        condition(:premium) { @subject.premium }

        rule { premium }.policy do
          enable :export
        end
      end
    end

    before do
      stub_const('SubResource', Struct.new(:blocked, :premium))
      stub_const('SubResourcePolicy', sub_policy)
    end

    context 'when the subject is blocked' do
      let(:policy_subject) { SubResource.new(true, true) }

      it 'inherits prevent_all exceptions from the parent policy' do
        expect(policy).to be_allowed(:read)
      end

      it 'prevents non-excepted inherited abilities' do
        expect(policy).not_to be_allowed(:edit)
        expect(policy).not_to be_allowed(:admin)
      end

      it 'prevents abilities defined in the subclass' do
        expect(policy).not_to be_allowed(:export)
      end
    end

    context 'when the subject is not blocked' do
      let(:policy_subject) { SubResource.new(false, true) }

      it 'allows all abilities' do
        expect(policy).to be_allowed(:read)
        expect(policy).to be_allowed(:edit)
        expect(policy).to be_allowed(:admin)
        expect(policy).to be_allowed(:export)
      end
    end

    context 'when the subclass adds its own prevent_all with different exceptions' do
      let(:sub_policy) do
        Class.new(policy_class) do
          condition(:premium) { @subject.premium }
          condition(:restricted) { @subject.blocked }

          rule { restricted }.prevent_all do
            except :read, :edit
          end

          rule { premium }.policy do
            enable :export
          end
        end
      end

      let(:policy_subject) { SubResource.new(true, true) }

      it 'combines exceptions from both parent and subclass prevent_all rules' do
        # :read is excepted by both parent and subclass
        expect(policy).to be_allowed(:read)

        # :edit is excepted by subclass but not by parent. The parent still prevents it
        expect(policy).not_to be_allowed(:edit)
      end

      it 'prevents abilities not excepted by either' do
        expect(policy).not_to be_allowed(:admin)
        expect(policy).not_to be_allowed(:export)
      end
    end
  end

  context 'when prevent_all has no block' do
    let(:policy_class) do
      Class.new(DeclarativePolicy::Base) do
        condition(:blocked) { @subject.blocked }

        rule { blocked }.prevent_all

        rule { default }.policy do
          enable :read
          enable :edit
        end
      end
    end

    let(:policy_subject) { Resource.new(true) }

    it 'prevents all abilities' do
      expect(policy).not_to be_allowed(:read)
      expect(policy).not_to be_allowed(:edit)
    end

    it 'is fully banned' do
      expect(policy.banned?).to be true
    end
  end
end
