# Copyright (C) 2012-2024 Zammad Foundation, https://zammad-foundation.org/

require 'rails_helper'

RSpec.describe KnowledgeBase::PermissionsUpdate do
  describe '#update!' do
    include_context 'basic Knowledge Base'

    let(:role_editor)    { create(:role, permission_names: %w[knowledge_base.editor]) }
    let(:role_another)   { create(:role, permission_names: %w[knowledge_base.editor]) }
    let(:role_reader)    { create(:role, permission_names: %w[knowledge_base.reader]) }
    let(:child_category) { create(:knowledge_base_category, parent: category) }

    describe 'updating itself' do
      shared_examples 'updating itself' do |object_name:|
        let(:object) { send(object_name) }
        it 'adds role permission for self' do
          described_class.new(object).update! role_editor => 'editor'

          expect(object.permissions)
            .to contain_exactly have_attributes(permissionable: object, role: role_editor, access: 'editor')
        end

        it 'adds additional role permission for self' do
          described_class.new(object).update! role_editor => 'reader'
          described_class.new(object).update! role_editor => 'reader', role_another => 'reader'

          expect(object.permissions)
            .to contain_exactly(have_attributes(role: role_editor), have_attributes(role: role_another))
        end

        it 'does not update when re-adding an existing permission' do
          described_class.new(object).update! role_editor => 'reader'

          expect { described_class.new(object).update! role_editor => 'reader' }
            .not_to change(object, :updated_at)
        end

        it 'throws error when role does not allow given access' do
          expect { described_class.new(object).update! role_reader => 'editor' }
            .to raise_error(%r{Validation failed})
        end
      end

      context 'when saving role on KB itself' do
        include_context 'updating itself', object_name: :knowledge_base
      end

      context 'when saving role on KB category' do
        include_context 'updating itself', object_name: :category
      end
    end

    describe 'updating descendants' do
      context 'when saving role on KB itself' do
        it 'adds effective permissions to descendant categories' do
          described_class.new(knowledge_base).update! role_editor => 'reader'

          expect(category.permissions_effective)
            .to contain_exactly have_attributes(role: role_editor, access: 'reader', permissionable: knowledge_base)
        end

        it 'removing permission opens up access to descendants' do
          described_class.new(knowledge_base).update! role_editor => 'editor'
          described_class.new(knowledge_base).update!(**{})

          expect(category.permissions_effective).to be_blank
        end

        context 'when category has editor role has editor role with editor permission' do
          before do
            described_class.new(category).update! role_editor => 'editor'
            category.reload
          end

          it 'removes identical permissions on descendant roles' do
            described_class.new(knowledge_base).update! role_editor => 'editor'
            category.reload

            expect(category.permissions_effective)
              .to contain_exactly have_attributes(role: role_editor, access: 'editor', permissionable: knowledge_base)
          end
        end

        context 'when child category has another role permission' do
          before do
            create(:knowledge_base_permission, permissionable: category, role: role_another, access: 'reader')
          end

          it 'removes conflicting permissions on descendant role but keeps another role' do
            described_class.new(knowledge_base).update! role_editor => 'editor'
            category.reload

            expect(category.permissions_effective)
              .to contain_exactly(
                have_attributes(role: role_editor,  access: 'editor', permissionable: knowledge_base),
                have_attributes(role: role_another, access: 'reader', permissionable: category),
              )
          end
        end

        context 'when limiting sub-category in category with editor permission' do
          before do
            create(:knowledge_base_permission, permissionable: category, role: role_editor, access: 'editor')
          end

          it 'ignores redundant editor permission' do
            described_class.new(child_category).update! role_editor => 'editor'
            child_category.reload

            expect(child_category.permissions_effective)
              .to contain_exactly(
                have_attributes(role: role_editor,  access: 'editor', permissionable: category),
              )
          end

          it 'raises error on updating to reader permission' do
            expect { described_class.new(child_category).update! role_editor => 'reader' }
              .to raise_error(Exceptions::UnprocessableEntity)
          end

          it 'raises error on updating to none permission' do
            expect { described_class.new(child_category).update! role_editor => 'none' }
              .to raise_error(Exceptions::UnprocessableEntity)
          end
        end

        context 'when limiting sub-category in category with none permission' do
          before do
            create(:knowledge_base_permission, permissionable: category, role: role_editor, access: 'none')
          end

          it 'ignores redundant none permission' do
            described_class.new(child_category).update! role_editor => 'none'
            child_category.reload

            expect(child_category.permissions_effective)
              .to contain_exactly(
                have_attributes(role: role_editor,  access: 'none', permissionable: category),
              )
          end

          it 'raises error on updating to reader permission' do
            expect { described_class.new(child_category).update! role_editor => 'reader' }
              .to raise_error(Exceptions::UnprocessableEntity)
          end

          it 'raises error on updating to editor permission' do
            expect { described_class.new(child_category).update! role_editor => 'editor' }
              .to raise_error(Exceptions::UnprocessableEntity)
          end
        end
      end

      context 'when saving role on KB category' do
        it 'adds effective permissions to descendant roles' do
          described_class.new(category).update! role_editor => 'reader'

          expect(child_category.permissions_effective)
            .to contain_exactly have_attributes(role: role_editor, access: 'reader', permissionable: category)
        end

        context 'when child category has editor role with editor permission' do
          before do
            described_class.new(child_category).update! role_editor => 'editor'
            category.reload
            child_category.reload
          end

          it 'removes conflicting permissions on descendant roles' do
            described_class.new(category).update! role_editor => 'none'
            category.reload
            child_category.reload

            expect(child_category.permissions_effective)
              .to contain_exactly have_attributes(role: role_editor, access: 'none', permissionable: category)
          end

          it 'removes identical permissions on descendant roles' do
            described_class.new(category).update! role_editor => 'editor'
            category.reload
            child_category.reload

            expect(child_category.permissions_effective)
              .to contain_exactly have_attributes(role: role_editor, access: 'editor', permissionable: category)
          end

          context 'when child category has another role permission' do
            before do
              create(:knowledge_base_permission, permissionable: child_category, role: role_another, access: 'reader')
            end

            it 'removes conflicting permissions on descendant role but keeps another role' do
              described_class.new(category).update! role_editor => 'none'
              category.reload
              child_category.reload

              expect(child_category.permissions_effective)
                .to contain_exactly(
                  have_attributes(role: role_editor,  access: 'none',   permissionable: category),
                  have_attributes(role: role_another, access: 'reader', permissionable: child_category),
                )
            end
          end
        end

        context 'when category has role editor with none permission' do
          before do
            described_class.new(category).update! role_editor => 'none'
            category.reload
          end

          it 'removing permission opens up access to descendants' do
            described_class.new(category).update!(**{})
            category.reload

            expect(child_category.permissions_effective).to be_blank
          end
        end
      end
    end

    describe 'preventing user lockout' do
      let(:user) { create(:admin) }
      let(:role) { user.roles.first }

      shared_examples 'preventing user lockout' do |object_name:|
        let(:object) { send(object_name) }

        it 'raises an error when saving a lockout change for a given user' do
          expect { described_class.new(object, user).update! role => 'reader' }
            .to raise_error(Exceptions::UnprocessableEntity)
        end

        it 'allows to save same change without a user' do
          expect { described_class.new(object).update! role => 'reader' }.not_to raise_error
        end
      end

      context 'when saving role on KB itself' do
        include_context 'preventing user lockout', object_name: 'knowledge_base'
      end

      context 'when saving role on KB category' do
        include_context 'preventing user lockout', object_name: 'category'
      end
    end
  end

  describe '#update_using_params!' do
    subject(:updater) { described_class.new(category) }

    let(:role)     { create(:role, permission_names: %w[knowledge_base.editor]) }
    let(:category) { create(:knowledge_base_category) }

    it 'calls update! with given roles' do
      updater.update_using_params!({ permissions: { role.id => 'editor' } })
      expect(category.permissions.first).to have_attributes(role: role, access: 'editor', permissionable: category)
    end

    it 'raises an error when given a non existant role' do
      expect { updater.update_using_params!({ permissions: { (role.id + 1) => 'editor' } }) }
        .to raise_error(ActiveRecord::RecordNotFound)
    end
  end
end