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

require 'rails_helper'

RSpec.describe Auth::Permissions, type: :request do
  shared_examples 'verify permissions checking' do
    let(:permission)       { create(:permission, name: permission_name) }
    let(:permission_names) { [permission.name] }
    let(:role)             { create(:role, permission_names:) }

    context 'with privileges for a root permission (e.g., "foo", not "foo.bar")' do
      let(:permission_name) { 'foo' }

      context 'when given that exact permission' do
        it 'returns true' do
          expect(described_class).to be_authorized(object, 'foo')
        end
      end

      context 'when given an active sub-permission' do
        before { create(:permission, name: 'foo.bar') }

        it 'returns true' do
          expect(described_class).to be_authorized(object, 'foo.bar')
        end
      end

      describe 'chain-of-ancestry quirk' do
        context 'when given an inactive sub-permission' do
          before { create(:permission, name: 'foo.bar.baz', active: false) }

          it 'returns false, even with active ancestors' do
            expect(described_class).not_to be_authorized(object, 'foo.bar.baz')
          end
        end

        context 'when given a sub-permission that does not exist' do
          before { create(:permission, name: 'foo.bar', active: false) }

          it 'can return true, even with inactive ancestors' do
            expect(described_class).to be_authorized(object, 'foo.bar.baz')
          end
        end
      end

      context 'when given a glob' do
        context 'when matching that permission' do
          it 'returns true' do
            expect(described_class).to be_authorized(object, 'foo.*')
          end
        end

        context 'when NOT matching that permission' do
          it 'returns false' do
            expect(described_class).not_to be_authorized(object, 'bar.*')
          end
        end
      end
    end

    context 'with privileges for a sub-permission (e.g., "foo.bar", not "foo")' do
      let(:permission_name) { 'foo.bar' }

      context 'when given that exact sub-permission' do
        it 'returns true' do
          expect(described_class).to be_authorized(object, 'foo.bar')
        end

        context 'when the permission is inactive' do
          before { permission.update(active: false) }

          it 'returns false' do
            expect(described_class).not_to be_authorized(object, 'foo.bar')
          end
        end
      end

      context 'when given a sibling sub-permission' do
        let(:sibling_permission) { create(:permission, name: 'foo.baz') }

        context 'when sibling exists' do
          before { sibling_permission }

          it 'returns false' do
            expect(described_class).not_to be_authorized(object, 'foo.baz')
          end
        end

        context 'when sibling does not exist' do
          it 'returns false' do
            expect(described_class).not_to be_authorized(object, 'foo.baz')
          end
        end
      end

      context 'when given the parent permission' do
        it 'returns false' do
          expect(described_class).not_to be_authorized(object, 'foo')
        end
      end

      context 'when given a glob' do
        context 'when matching that sub-permission' do
          it 'returns true' do
            expect(described_class).to be_authorized(object, 'foo.*')
          end

          context 'when the permission is inactive' do
            before { permission.update(active: false) }

            it 'returns false' do
              expect(described_class).not_to be_authorized(object, 'foo.*')
            end
          end
        end

        context 'when NOT matching that sub-permission' do
          it 'returns false' do
            expect(described_class).not_to be_authorized(object, 'bar.*')
          end
        end
      end

      context 'when given a plus' do
        let(:permission_names) { [permission.name, 'ticket.agent'] }

        context 'when matching both permissions' do
          it 'returns true' do
            expect(described_class).to be_authorized(object, 'foo.bar+ticket.agent')
          end

          it 'returns true if vice versa order given' do
            expect(described_class).to be_authorized(object, 'ticket.agent+foo.bar')
          end

          it 'returns true if given a glob' do
            expect(described_class).to be_authorized(object, 'foo.*+ticket.agent')
          end

          context 'when one of the permissions is inactive' do
            before { permission.update(active: false) }

            it 'returns false' do
              expect(described_class).not_to be_authorized(object, 'ticket.agent+foo.bar')
            end
          end
        end

        context 'when not matching one of permissions' do
          it 'returns false' do
            expect(described_class).not_to be_authorized(object, 'bar+ticket.agent')
          end

          it 'returns false if vice versa order given' do
            expect(described_class).not_to be_authorized(object, 'ticket.agent+bar')
          end
        end
      end
    end
  end

  describe 'user handling' do
    let(:object) { create(:user, roles: [role]) }

    include_examples 'verify permissions checking'
  end

  describe 'token handling' do
    let(:user)   { create(:user, roles: [role]) }
    let(:object) { create(:token, user:, preferences: { permission: permission_names }) }

    include_examples 'verify permissions checking'
  end

  describe 'caching' do
    let(:user) { create(:agent) }

    before do
      allow(described_class).to receive(:new).and_call_original
    end

    it 'caches response with same arguments' do
      described_class.authorized?(user, 'ticket.agent')
      described_class.authorized?(user, 'ticket.agent')

      expect(described_class).to have_received(:new).once
    end

    it 'does not cache response with different queries' do
      described_class.authorized?(user, 'ticket.agent')
      described_class.authorized?(user, 'other')

      expect(described_class).to have_received(:new).twice
    end

    it 'does not cache response with different objects' do
      described_class.authorized?(user, 'ticket.agent')
      described_class.authorized?(create(:user), 'ticket.agent')

      expect(described_class).to have_received(:new).twice
    end
  end
end