# frozen_string_literal: true

require 'rails_helper'

RSpec.describe ExercisePolicy do
  subject(:policy) { described_class }

  let(:exercise) { build(:dummy, public: true) }

  %i[batch_update? programming_groups_for_exercise?].each do |action|
    permissions(action) do
      it 'grants access to admins only' do
        expect(policy).to permit(build(:admin), exercise)
        %i[external_user teacher].each do |factory_name|
          expect(policy).not_to permit(create(factory_name), exercise)
        end
      end
    end
  end

  %i[create? index? new? statistics? feedback? rfcs_for_exercise?].each do |action|
    permissions(action) do
      it 'grants access to admins' do
        expect(policy).to permit(build(:admin), exercise)
      end

      it 'grants access to teachers' do
        expect(policy).to permit(create(:teacher), exercise)
      end

      it 'does not grant access to external users' do
        expect(policy).not_to permit(build(:external_user), exercise)
      end
    end
  end

  %i[clone? destroy? edit? update?].each do |action|
    permissions(action) do
      it 'grants access to admins' do
        expect(policy).to permit(build(:admin), exercise)
      end

      it 'grants access to authors' do
        expect(policy).to permit(exercise.author, exercise)
      end

      it 'does not grant access to all other users' do
        %i[external_user teacher].each do |factory_name|
          expect(policy).not_to permit(create(factory_name), exercise)
        end
      end
    end
  end

  %i[export_external_check? export_external_confirm?].each do |action|
    permissions(action) do
      context 'when CodeHarbor is disabled' do
        before do
          stub_const('CodeharborLinkPolicy::CODEHARBOR_CONFIG', {enabled: false})
        end

        %i[external_user teacher admin].each do |factory_name|
          context "when user is #{factory_name}" do
            let(:user) { create(factory_name) }

            it 'does not grant access' do
              expect(policy).not_to permit(user, exercise)
            end

            context 'when user has codeharbor_link' do
              before { user.codeharbor_link = build(:codeharbor_link) }

              it 'does not grant access' do
                expect(policy).not_to permit(user, exercise)
              end
            end
          end
        end
      end

      context 'when CodeHarbor is enabled' do
        before do
          stub_const('CodeharborLinkPolicy::CODEHARBOR_CONFIG', {enabled: true})
        end

        context 'when user is author' do
          let(:user) { exercise.author }

          it 'does not grant access' do
            expect(policy).not_to permit(user, exercise)
          end

          context 'when user has codeharbor_link' do
            before { user.codeharbor_link = build(:codeharbor_link) }

            it 'grants access' do
              expect(policy).to permit(user, exercise)
            end
          end
        end

        context 'when user is admin' do
          let(:user) { build(:admin) }

          it 'does not grant access' do
            expect(policy).not_to permit(user, exercise)
          end

          context 'when user has codeharbor_link' do
            before { user.codeharbor_link = build(:codeharbor_link) }

            it 'grants access' do
              expect(policy).to permit(user, exercise)
            end
          end
        end

        %i[external_user teacher].each do |factory_name|
          context "when user is #{factory_name}" do
            let(:user) { create(factory_name) }

            it 'does not grant access' do
              expect(policy).not_to permit(user, exercise)
            end

            context 'when user has codeharbor_link' do
              before { user.codeharbor_link = build(:codeharbor_link) }

              it 'does not grant access' do
                expect(policy).not_to permit(user, exercise)
              end
            end
          end
        end
      end
    end
  end

  permissions :show? do
    let(:teacher) { create(:teacher) }
    let(:exercise_not_public) { build(:dummy, public: false) }

    it 'does not grant access to external users' do
      expect(policy).not_to permit(build(:external_user), exercise_not_public)
    end

    context 'when a teacher is not a member in the same study group as the exercise author' do
      it 'not grants access to the user' do
        expect(policy).not_to permit(teacher, exercise_not_public)
      end
    end

    context "when a teacher is only a member of type 'learner' in the same study group as the exercise author" do
      it 'not grants access to the user' do
        exercise_not_public.author.study_groups << teacher.study_groups.first
        expect(policy).not_to permit(teacher, exercise_not_public)
      end
    end

    context 'when a teacher and the exercise author are teaching team members of the same study group' do
      it 'grants access to the user' do
        exercise_not_public.author.study_groups << teacher.study_groups.first
        exercise_not_public.author.study_group_memberships.last.update(role: 'teacher')
        expect(policy).to permit(teacher, exercise_not_public)
      end
    end
  end

  %i[implement? working_times? intervention? reload?].each do |action|
    permissions(action) do
      context 'when the exercise has no visible files' do
        let(:exercise) { create(:dummy) }

        it 'does not grant access to anyone' do
          %i[admin external_user teacher].each do |factory_name|
            expect(policy).not_to permit(create(factory_name), exercise)
          end
        end
      end

      context 'when the exercise has visible files' do
        let(:exercise) { create(:fibonacci) }

        it 'grants access to anyone' do
          %i[admin external_user teacher].each do |factory_name|
            expect(policy).to permit(create(factory_name), exercise)
          end
        end
      end

      context 'when the exercise is published' do
        let(:exercise) { create(:fibonacci, unpublished: false) }

        it 'grants access to anyone' do
          %i[admin external_user teacher].each do |factory_name|
            expect(policy).to permit(create(factory_name), exercise)
          end
        end
      end

      context 'when the exercise is unpublished' do
        let(:exercise) { create(:fibonacci, unpublished: true) }

        it 'grants access to admins' do
          expect(policy).to permit(build(:admin), exercise)
        end

        it 'grants access to the author' do
          expect(policy).to permit(exercise.author, exercise)
        end

        it 'grants access to teachers of the same study group' do
          # By default, the exercise author and the teacher are internal users and share the same study group
          expect(policy).to permit(create(:teacher), exercise)
        end

        it 'does not grant access to teachers of another consumer' do
          # With another consumer, the teacher is in another study group, too.
          expect(policy).not_to permit(create(:external_teacher), exercise)
        end

        it 'does not grant access to external users' do
          expect(policy).not_to permit(create(:external_user), exercise)
        end
      end
    end
  end

  describe ExercisePolicy::Scope do
    describe '#resolve' do
      let(:admin) { create(:admin) }
      let(:external_user) { create(:external_user) }
      let(:teacher) { create(:external_teacher) }

      before do
        [admin, teacher].each do |user|
          [true, false].each do |public|
            create(:dummy, public:, user:)
          end
        end
      end

      context 'when being an admin' do
        let(:scope) { Pundit.policy_scope!(admin, Exercise) }

        it 'returns all exercises' do
          expect(scope.map(&:id)).to include(*Exercise.all.map(&:id))
        end
      end

      context 'when being an external users' do
        let(:scope) { Pundit.policy_scope!(external_user, Exercise) }

        it 'returns nothing' do
          expect(scope.count).to be 0
        end
      end

      context 'when being a teacher' do
        let(:scope) { Pundit.policy_scope!(teacher, Exercise) }

        it 'includes all public exercises' do
          expect(scope.map(&:id)).to include(*Exercise.where(public: true).map(&:id))
        end

        it 'includes all authored non-public exercises' do
          expect(scope.map(&:id)).to include(*Exercise.where(public: false, user: teacher).map(&:id))
        end

        it "does not include other authors' non-public exercises" do
          expect(scope.map(&:id)).not_to include(*Exercise.where(public: false).where.not(user: teacher).map(&:id))
        end
      end
    end
  end
end
