# frozen_string_literal: true

#
# Copyright (C) 2018 - present Instructure, Inc.
#
# This file is part of Canvas.
#
# Canvas is free software: you can redistribute it and/or modify it under
# the terms of the GNU Affero General Public License as published by the Free
# Software Foundation, version 3 of the License.
#
# Canvas is distributed in the hope that it will be useful, but WITHOUT ANY
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
# A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
# details.
#
# You should have received a copy of the GNU Affero General Public License along
# with this program. If not, see <http://www.gnu.org/licenses/>.

require_relative "concerns/advantage_services_shared_context"
require_relative "concerns/advantage_services_shared_examples"
require_relative "concerns/lti_services_shared_examples"

describe Lti::IMS::NamesAndRolesController do
  include Lti::IMS::NamesAndRolesMatchers

  include_context "advantage services context"

  let(:group_record) { group(context: course) } # _record suffix to avoid conflict with group() factory mtd
  let(:context_param_name) { raise "Override in spec" }
  let(:scope_to_remove) { "https://purl.imsglobal.org/spec/lti-nrps/scope/contextmembership.readonly" }

  # rubocop:disable RSpec/LetSetup
  shared_context "assignment context" do
    let!(:student_enrollment_1) { student_in_course(course:, active_all: true) }
    let!(:student_enrollment_2) { student_in_course(course:, active_all: true) }
    let!(:student_enrollment_3) { student_in_course(course:, active_all: true) }
    let!(:student_enrollment_4) { student_in_course(course:, active_all: true) }
    let!(:student_enrollment_5) { student_in_course(course:, active_all: true) }
    let!(:student_enrollments) do
      [student_enrollment_1, student_enrollment_2, student_enrollment_3, student_enrollment_4, student_enrollment_5]
    end
    let!(:enrollments) do
      super().push(student_enrollment_2, student_enrollment_3, student_enrollment_4, student_enrollment_5)
    end
    let(:rlid_param_1) { SecureRandom.hex }
    let(:rlid_param_2) { SecureRandom.hex }
    let!(:assignment_with_rlid_1) { assignment_with_rlid(rlid_param_1) }
    let!(:assignment_with_rlid_2) { assignment_with_rlid(rlid_param_2) }
    let(:rlid_param) { raise "Override in example" }
    let(:pass_thru_params) { super().merge(rlid: rlid_param) }
  end

  shared_context "assignment assigned to specific students" do
    let!(:assignment_with_rlid_1) do
      assignment = super()
      assignment = assign(assignment, student_enrollment_1, student_enrollment_3, student_enrollment_5)
      assignment
    end
  end
  # rubocop:enable RSpec/LetSetup

  shared_examples "page size check" do
    # Needs all the same variables as 'multiple enrollments check', plus:
    #
    #   page_size_param_name - symbol naming the page size query param
    #
    let(:pagination_overrides) { { page_size_param_name => rqst_page_size } }
    let(:params_overrides) { super().merge(pagination_overrides) }

    it "returns the first page of results, with the correct size and proper navigation links" do
      send_request
      expect_enrollment_response_page
      expect(response_links).to have_correct_pagination_urls
    end

    context "when the page size parameter is too large" do
      let(:rqst_page_size) { 4_611_686_018_427_387_903 }
      let(:effective_page_size) { Lti::IMS::Providers::MembershipsProvider::MAX_PAGE_SIZE } # system max
      let(:rsp_page_size) { total_items }

      it "defaults to the system maximum page size" do
        send_request
        expect_enrollment_response_page
        expect(response_links).to have_correct_pagination_urls
      end
    end

    context "when a page index parameter is specified" do
      let(:rqst_page) { 2 }
      let(:rsp_page) { 2 }
      let(:pagination_overrides) { super().merge(page: rqst_page) }

      it "returns the specified page of results, with the correct size and proper navigation links" do
        send_request
        expect_enrollment_response_page
        expect(response_links).to have_correct_pagination_urls
      end

      context "and when the page index parameter is too large" do
        let(:rqst_page) { total_items + 1 } # cant have more pages than there are items
        let(:rsp_page) { rqst_page }
        let(:effective_page_size) { 30 } # don't know why, Api just does this
        let(:rsp_page_size) { 0 }

        it "returns an empty members array, with the correct size and proper navigation links" do
          send_request
          expect_empty_members_array
          expect(response_links).to have_correct_pagination_urls
        end
      end
    end
  end

  shared_examples "multiple enrollments check" do
    # When including, define the following:
    #   enrollments: array of context enrollments
    #   total_items: context's active enrollment count
    #   rsp_page: expected response page index
    #   effective_page_size: either user- or system-enforced max page size,
    #   rsp_page_size: expected actual response page size, i.e. actual items on the page
    #   pass_through_param: hash of query params to be written through to pagination links
    #   params_overrides: hash of all request params

    it "returns all enrollments sorted by user id" do
      send_request
      expect_enrollment_response_page
      expect(response_links).to have_correct_pagination_urls
    end

    context "when a limit page size parameter is specified" do
      let(:rqst_page_size) { 2 }
      let(:effective_page_size) { rqst_page_size }
      let(:rsp_page_size) { rqst_page_size }
      let(:page_size_param_name) { :limit }

      it_behaves_like "page size check"
    end

    context "when a per_page parameter is specified" do
      let(:rqst_page_size) { 2 }
      let(:effective_page_size) { rqst_page_size }
      let(:rsp_page_size) { rqst_page_size }
      let(:page_size_param_name) { :per_page }

      it_behaves_like "page size check"
    end

    context "when a limit param overrides a per_page param" do
      let(:rqst_page_size) { 2 }
      let(:effective_page_size) { rqst_page_size }
      let(:rsp_page_size) { rqst_page_size }
      let(:page_size_param_name) { :limit }

      # limit=2, per_page=3 ... latter should be ignored
      let(:params_overrides) { super().merge(per_page: 3) }

      it_behaves_like "page size check"
    end
  end

  shared_examples "rlid check" do
    let(:rlid_param) { expected_lti_id(course) }
    let(:params_overrides) { super().merge(rlid: rlid_param) }
    let(:user_full_name) { "Marta Perkins" }

    context "when the rlid param specifies the course context LTI ID" do
      it "behaves just like a 'normal' NRPS course membership lookup" do
        send_request
        expect_single_member(enrollment)
      end

      context "and the tool is configured with a mix of supported and unsupported custom parameters" do
        let!(:tool) do
          tool = super()
          tool.settings[:custom_fields] = {
            person_name_full: "$Person.name.full",
            person_name_display: "$Person.name.display",
            person_name_family: "$Person.name.family",
            person_name_given: "$Person.name.given",
            user_image: "$User.image",
            user_id: "$User.id",
            canvas_user_id: "$Canvas.user.id",
            vnd_instructure_user_uuid: "$vnd.instructure.User.uuid",
            canvas_user_globalid: "$Canvas.user.globalId",
            canvas_user_sissourceid: "$Canvas.user.sisSourceId",
            person_sourced_id: "$Person.sourcedId",
            message_locale: "$Message.locale",
            vnd_canvas_person_email_sis: "$vnd.Canvas.Person.email.sis",
            person_email_primary: "$Person.email.primary",
            person_address_timezone: "$Person.address.timezone",
            user_username: "$User.username",
            canvas_user_sisintegrationid: "$Canvas.user.sisIntegrationId",
            canvas_user_loginid: "$Canvas.user.loginId",
            canvas_xapi_url: "$Canvas.xapi.url",
            caliper_url: "$Caliper.url",
            unsupported_param_1: "$unsupported.param.1",
            unsupported_param_2: "$unsupported.param.2"
          }
          tool.save!
          tool
        end
        let!(:user) do
          user = enrollment.user
          user.email = "marta.perkins@school.edu"
          user.avatar_image_url = "http://school.edu/image/url.png"
          user.locale = :de
          user.time_zone = "Europe/Berlin"
          user.save!
          sis_email = "sis@example.com"
          cc = user.communication_channels.email.create!(path: sis_email)
          cc.user = user
          cc.save!
          @cc_pseud = user.pseudonyms.create!({
                                                account: course.account,
                                                unique_id: cc.path,
                                                sis_communication_channel_id: cc.id,
                                                communication_channel_id: cc.id,
                                                workflow_state: "active",
                                                sis_user_id: "user-1-sis-user-id-1",
                                                integration_id: "user-1-sis-integration-id-1",
                                              })
          user.pseudonyms.create!({
                                    account: course.account,
                                    unique_id: "user1@example.com",
                                    password: "asdfasdf",
                                    password_confirmation: "asdfasdf",
                                    workflow_state: "active",
                                    sis_user_id: "user-1-sis-user-id-2",
                                    integration_id: "user-1-sis-integration-id-2",
                                  })
          user
        end

        def be_xapi_url
          if context.is_a?(Course)
            return be_analytics_url(controller.lti_xapi_url(Lti::AnalyticsService.create_token(tool, user, context)))
          end

          eq "$Canvas.xapi.url"
        end

        def be_caliper_url
          if context.is_a?(Course)
            return be_analytics_url(controller.lti_caliper_url(Lti::AnalyticsService.create_token(tool, user, context)))
          end

          eq "$Caliper.url"
        end

        # analytics URLs end with a time value, a random value, and a signature... we really dont care about testing
        # all the details of how those URLs are created, just that they are. chop off the problematic substrings.
        def be_analytics_url(url)
          start_with(url.gsub(/-.+-.+-.+$/, ""))
        end

        it "expands the supported parameters and echoes the rest" do
          send_request
          expect(json[:members][0]).to match_enrollment_for_rlid(
            {
              "https://purl.imsglobal.org/spec/lti/claim/custom" => {
                "person_name_full" => "Marta Perkins",
                "person_name_display" => "Marta Perkins",
                "person_name_family" => "Perkins",
                "person_name_given" => "Marta",
                "user_image" => "http://school.edu/image/url.png",
                "user_id" => user.id.to_s,
                "canvas_user_id" => user.id.to_s,
                "vnd_instructure_user_uuid" => user.uuid,
                "canvas_user_globalid" => user.global_id.to_s,
                "canvas_user_sissourceid" => @cc_pseud.sis_user_id,
                "person_sourced_id" => @cc_pseud.sis_user_id,
                "message_locale" => "de",
                "vnd_canvas_person_email_sis" => "sis@example.com",
                "person_email_primary" => "marta.perkins@school.edu",
                "person_address_timezone" => "Europe/Berlin",
                "user_username" => "sis@example.com",
                "canvas_user_loginid" => "sis@example.com",
                "canvas_user_sisintegrationid" => @cc_pseud.integration_id,
                "canvas_xapi_url" => be_xapi_url,
                "caliper_url" => be_caliper_url,
                "unsupported_param_1" => "$unsupported.param.1",
                "unsupported_param_2" => "$unsupported.param.2"
              }
            },
            enrollment
          )
          expect_member_count(1)
        end
      end

      context "when the rlid param specifies a non-assignment resource link" do
        let(:resource_link) do
          Lti::ResourceLink.create!(
            context: course,
            context_external_tool: tool,
            url: "https://www.example.com/launch"
          )
        end

        let(:course_module) { ContextModule.create!(context: course) }

        let(:rlid_param) { resource_link.resource_link_uuid }

        before do
          course_module.content_tags.create!(
            context: course,
            context_module: course_module,
            tag_type: "context_module",
            content_type: "ContextExternalTool",
            title: "Test Title",
            url: tool.url,
            content: tool,
            associated_asset: resource_link
          )
        end

        it "responds a NRPS course membership lookup" do
          send_request
          expect_single_member(enrollment)
        end

        it "does not match against content tags with the wrong associated_asset_type" do
          course_module.content_tags.where(associated_asset: resource_link).update_all(associated_asset_type: "LearningOutcome")
          send_request
          expect(json).to be_lti_advantage_error_response_body("bad_request", "Requested ResourceLink was not found")
        end
      end
    end

    context "when the rlid param does not specify the course context LTI ID" do
      let(:rlid_param) { "nonsense-#{expected_lti_id(course)}" }

      # mime_type check needs the request to have already been sent
      before do
        send_request
      end

      it_behaves_like "mime_type check"

      it "returns a 400 bad_request and descriptive error message" do
        expect(json).to be_lti_advantage_error_response_body("bad_request", "Invalid 'rlid' parameter")
      end
    end
  end

  describe "#course_index" do
    let(:action) { :course_index }
    let(:context) { course }
    let(:context_param_name) { :course_id }
    let(:unknown_context_id) { course && (Course.maximum(:id) + 1) }

    it_behaves_like "advantage services"
    it_behaves_like "lti services"

    # Bunch of single-enrollment tests b/c they're just so much easier to
    # debug as compared to multi-enrollment tests

    describe "id field" do
      it "uses the Account#domain in the line item id" do
        expect_any_instance_of(Account).to receive(:environment_specific_domain).and_return("canonical.host")
        send_request
        expect(json[:id]).to eq(
          "http://canonical.host/api/lti/courses/#{course.id}/names_and_roles"
        )
      end
    end

    context "when a course has a single enrollment" do
      it "returns teacher in members array" do
        enrollment = teacher_in_course(course:, active_all: true)
        send_request
        expect_single_member(enrollment)
      end

      it "returns student in members array" do
        enrollment = student_in_course(course:, active_all: true)
        send_request
        expect_single_member(enrollment)
      end

      it "returns ta in members array" do
        enrollment = ta_in_course(course:, active_all: true)
        send_request
        expect_single_member(enrollment)
      end

      it "returns observer in members array" do
        enrollment = observer_in_course(course:, active_all: true)
        send_request
        expect_single_member(enrollment)
      end

      it "returns designer in members array" do
        enrollment = designer_in_course(course:, active_all: true)
        send_request
        expect_single_member(enrollment)
      end

      it "returns custom teacher in members array" do
        enrollment = custom_enrollment_in_course("TeacherEnrollment", course:, active_all: true)
        send_request
        expect_single_member(enrollment)
      end

      it "returns custom student in members array" do
        enrollment = custom_enrollment_in_course("StudentEnrollment", course:, active_all: true)
        send_request
        expect_single_member(enrollment)
      end

      it "returns custom ta in members array" do
        enrollment = custom_enrollment_in_course("TaEnrollment", course:, active_all: true)
        send_request
        expect_single_member(enrollment)
      end

      it "returns custom observer in members array" do
        enrollment = custom_enrollment_in_course("ObserverEnrollment", course:, active_all: true)
        send_request
        expect_single_member(enrollment)
      end

      it "returns custom designer in members array" do
        enrollment = custom_enrollment_in_course("DesignerEnrollment", course:, active_all: true)
        send_request
        expect_single_member(enrollment)
      end
    end

    context "when a course has a user with multiple active enrollments" do
      it "returns both enrollments in the same NRPS membership" do
        teacher_enrollment = teacher_in_course(course:, active_all: true)
        student_enrollment = student_in_course(course:, user: teacher_enrollment.user, active_all: true)
        send_request
        expect_single_member(teacher_enrollment, student_enrollment)
      end
    end

    context "when a course has a concluded enrollment" do
      it "does not return the concluded enrollment" do
        enrollment = teacher_in_course(course:, active_all: true)
        enrollment.conclude
        send_request
        expect_empty_members_array
      end
    end

    context "when a course has a deactivated enrollment" do
      it "does not return the deactivated enrollment" do
        enrollment = teacher_in_course(course:, active_all: true)
        enrollment.deactivate
        send_request
        expect_empty_members_array
      end
    end

    context "when a course has a deleted enrollment" do
      it "does not return the deleted enrollment" do
        enrollment = teacher_in_course(course:, active_all: true)
        enrollment.destroy # logical delete (physical wont work and will just raise a FK violation)
        send_request
        expect_empty_members_array
      end
    end

    context "when a course has a rejected enrollment" do
      it "does not return the rejected enrollment" do
        enrollment = teacher_in_course(course:, active_all: true)
        enrollment.reject!
        send_request
        expect_empty_members_array
      end
    end

    context "when a course has a invited instructor enrollment" do
      it "does not return the invited instructor enrollment" do
        teacher_in_course(course:)
        send_request
        expect_empty_members_array
      end
    end

    context "when a course has a creation_pending student enrollment" do
      it "does not return the creation_pending student enrollment" do
        student_in_course(course:)
        send_request
        expect_empty_members_array
      end
    end

    # rubocop:disable RSpec/LetSetup
    context "when the rlid param is specified" do
      let!(:enrollment) { teacher_in_course(course:, active_all: true, name: user_full_name) }

      it_behaves_like "rlid check"
    end
    # rubocop:enable RSpec/LetSetup

    context "when a course has multiple enrollments" do
      let!(:teacher_enrollment) { teacher_in_course(course:, active_all: true) }
      let!(:student_enrollment) { student_in_course(course:, active_all: true) }
      let!(:ta_enrollment) { ta_in_course(course:, active_all: true) }
      let!(:observer_enrollment) { observer_in_course(course:, active_all: true) }
      let!(:designer_enrollment) { designer_in_course(course:, active_all: true) }

      let!(:enrollments) do
        [
          teacher_enrollment,
          student_enrollment,
          ta_enrollment,
          observer_enrollment,
          designer_enrollment
        ]
      end
      let(:total_items) { enrollments.length }
      let(:rsp_page) { 1 }
      let(:rsp_page_size) { [total_items, effective_page_size].min }
      let(:effective_page_size) { Lti::IMS::Providers::MembershipsProvider::MAX_PAGE_SIZE } # system default
      let(:pass_thru_params) { { pass: "thru" } }
      let(:params_overrides) { super().merge(pass_thru_params) }

      it_behaves_like "multiple enrollments check"

      context "and the role param specifies the LIS Instructor role" do
        let(:total_items) { 2 }
        let(:pass_thru_params) { super().merge(role: "http://purl.imsglobal.org/vocab/lis/v2/membership#Instructor") }

        it "limits results to Canvas Teachers and TAs" do
          send_request
          expect_enrollment_response_page_of([teacher_enrollment, ta_enrollment])
        end
      end

      context "and the role param specifies the LIS Learner role" do
        let(:total_items) { 1 }
        let(:pass_thru_params) { super().merge(role: "http://purl.imsglobal.org/vocab/lis/v2/membership#Learner") }

        it "limits results to Canvas Students" do
          send_request
          expect_enrollment_response_page_of([student_enrollment])
        end
      end

      context "and the role param specifies the LIS TeachingAssistant role" do
        let(:total_items) { 1 }
        let(:pass_thru_params) { super().merge(role: "http://purl.imsglobal.org/vocab/lis/v2/membership/Instructor#TeachingAssistant") }

        it "limits results to Canvas TAs" do
          send_request
          expect_enrollment_response_page_of([ta_enrollment])
        end
      end

      context "and the role param specifies the LIS Mentor role" do
        let(:total_items) { 1 }
        let(:pass_thru_params) { super().merge(role: "http://purl.imsglobal.org/vocab/lis/v2/membership#Mentor") }

        it "limits results to Canvas Observers" do
          send_request
          expect_enrollment_response_page_of([observer_enrollment])
        end
      end

      context "and the role param specifies the LIS ContentDeveloper role" do
        let(:total_items) { 1 }
        let(:pass_thru_params) { super().merge(role: "http://purl.imsglobal.org/vocab/lis/v2/membership#ContentDeveloper") }

        it "limits results to Canvas Designers" do
          send_request
          expect_enrollment_response_page_of([designer_enrollment])
        end
      end

      context "and the role param specifies the LTI TestUser role" do
        let(:total_items) { 1 }
        let(:pass_thru_params) { super().merge(role: "http://purl.imsglobal.org/vocab/lti/system/person#TestUser") }

        it "limits results to test students" do
          student_view_enrollment =
            course_with_user("StudentViewEnrollment", course:, active_all: true)
          send_request
          expect_enrollment_response_page_of([student_view_enrollment])
        end
      end

      context "and the role param specifies an unknown value" do
        let(:params_overrides) { super().merge(role: "http://purl.imsglobal.org/total/nonsense") }

        it "returns no course members" do
          send_request
          expect_empty_members_array
        end
      end

      context "and a course has a user with multiple active enrollments" do
        let(:student_enrollment_2) { student_in_course(course:, user: teacher_enrollment.user, active_all: true) }
        let(:enrollments) { super().push(student_enrollment_2) }

        context "and the role param specifies the first enrollment role" do
          let(:pass_thru_params) { super().merge(role: "http://purl.imsglobal.org/vocab/lis/v2/membership#Instructor") }

          it "returns matching NRPS memberships grouped by user" do
            send_request
            expect_member(teacher_enrollment, student_enrollment_2, index: 0)
            expect_member(ta_enrollment, index: 1)
            expect_member_count(2)
          end
        end

        context "and the role param specifies the second enrollment role" do
          let(:pass_thru_params) { super().merge(role: "http://purl.imsglobal.org/vocab/lis/v2/membership#Learner") }

          it "returns matching NRPS memberships grouped by user" do
            send_request
            expect_member(teacher_enrollment, student_enrollment_2, index: 0)
            expect_member(student_enrollment, index: 1)
            expect_member_count(2)
          end
        end
      end

      context "and role and pagination params are specified" do
        let(:total_items) { 2 }
        let(:rsp_page_size) { 1 }
        let(:rqst_page_size) { 1 }
        let(:effective_page_size) { rqst_page_size }
        let(:rqst_page) { 1 }
        let(:pass_thru_params) { super().merge(role: "http://purl.imsglobal.org/vocab/lis/v2/membership#Instructor") }
        let(:params_overrides) do
          super().merge(
            limit: rqst_page_size,
            page: rqst_page
          )
        end

        context "and the first page of results are requested" do
          it "returns the correct first page of results" do
            send_request
            expect_enrollment_response_page_of([teacher_enrollment])
            expect(response_links).to have_correct_pagination_urls
          end
        end

        context "and the second page of results are requested" do
          let(:rqst_page) { 2 }
          let(:rsp_page) { 2 }

          it "returns the correct second page of results" do
            send_request
            expect_enrollment_response_page_of([ta_enrollment])
            expect(response_links).to have_correct_pagination_urls
          end
        end
      end

      # rubocop:disable RSpec/LetSetup
      context "and an assignment rlid param is specified" do
        include_context "assignment context"
        let(:rlid_param) { rlid_param_1 }
        let!(:student_enrollment_1) { student_enrollment } # no need to create an extra enrollment

        context "for an assignment unbound from its previous tool" do
          before do
            assignment_with_rlid_1.update!(submission_types: "none")
            # mime_type check needs the request to have already been sent
            send_request
          end

          it_behaves_like "mime_type check"

          it "returns a 400 bad_request and descriptive error message" do
            expect(json).to be_lti_advantage_error_response_body("bad_request", "Requested assignment not configured for external tool launches")
          end
        end

        context "and the resource link contains additional custom params" do
          let(:resource_link) do
            rl = assignment_with_rlid_1.primary_resource_link
            rl.update!(custom: custom_params)
            rl
          end
          let(:tool) do
            tool = super()
            tool.settings[:custom_fields] = tool_params
            tool.save!
            tool
          end
          let(:custom_params) { { "foo" => "bar" } }
          let(:tool_params) { { "baz" => "qux" } }
          let(:expected_params) { custom_params.merge(tool_params).with_indifferent_access }
          let(:rlid_param) { rlid_param_1 }

          it "includes the custom params from the tool and the resource link" do
            tool
            resource_link
            send_request
            expect(json[:members][0][:message][0]["https://purl.imsglobal.org/spec/lti/claim/custom"]).to eq(
              expected_params
            )
          end
        end

        context "for an assignment assigned to everyone" do
          it "returns all course members" do
            send_request
            expect_enrollment_response_page
            expect(response_links).to have_correct_pagination_urls
          end

          it "checks tool.can_access_content_tag?" do
            checked_tool = nil
            checked_tag = nil
            expect_any_instance_of(ContextExternalTool).to receive(:can_access_content_tag?) do |tool, content_tag|
              checked_tool = tool
              checked_tag = content_tag
              false
            end

            send_request

            expect(checked_tag.context).to eq(assignment_with_rlid_1)
            expect(checked_tool).to eq(tool)
            expect(response).to have_http_status(:bad_request)
          end

          context "and a student role param is specified" do
            let(:pass_thru_params) { super().merge(role: "http://purl.imsglobal.org/vocab/lis/v2/membership#Learner") }
            let(:total_items) { 5 }

            it "returns only student enrollments" do
              send_request
              expect_enrollment_response_page_of(student_enrollments)
              expect(response_links).to have_correct_pagination_urls
            end
          end

          context "and a non-student role param is specified" do
            let(:pass_thru_params) { super().merge(role: "http://purl.imsglobal.org/vocab/lis/v2/membership#Instructor") }
            let(:total_items) { 2 }

            it "returns only the non-student enrollments" do
              send_request
              expect_enrollment_response_page_of([teacher_enrollment, ta_enrollment])
              expect(response_links).to have_correct_pagination_urls
            end
          end

          context "and a nonsense role param is specified" do
            let(:pass_thru_params) { super().merge(role: "http://purl.imsglobal.org/total/nonsense") }

            it "returns no course members" do
              send_request
              expect_empty_members_array
            end
          end

          context "and pagination params are specified" do
            let(:rsp_page_size) { 5 }
            let(:rqst_page_size) { 5 }
            let(:effective_page_size) { rqst_page_size }
            let(:rqst_page) { raise "Override in example" }
            let(:params_overrides) { super().merge(limit: rqst_page_size, page: rqst_page) }

            context "and page 1 is requested" do
              let(:rqst_page) { 1 }
              let(:rsp_page) { 1 }

              it "returns page 1" do
                send_request
                expect_enrollment_response_page_of([
                                                     teacher_enrollment,
                                                     student_enrollment_1,
                                                     ta_enrollment,
                                                     observer_enrollment,
                                                     designer_enrollment
                                                   ])
                expect(response_links).to have_correct_pagination_urls
              end
            end

            context "and page 2 is requested" do
              let(:rqst_page) { 2 }
              let(:rsp_page) { 2 }
              let(:rsp_page_size) { 4 }

              it "returns page 2" do
                send_request
                expect_enrollment_response_page_of([
                                                     student_enrollment_2,
                                                     student_enrollment_3,
                                                     student_enrollment_4,
                                                     student_enrollment_5,
                                                   ])
                expect(response_links).to have_correct_pagination_urls
              end
            end
          end

          context "and a role and pagination params are specified" do
            let(:pass_thru_params) { super().merge(role: "http://purl.imsglobal.org/vocab/lis/v2/membership#Learner") }
            let(:rsp_page_size) { 3 }
            let(:rqst_page_size) { 3 }
            let(:effective_page_size) { rqst_page_size }
            let(:total_items) { 5 }
            let(:rqst_page) { raise "Override in example" }
            let(:params_overrides) { super().merge(limit: rqst_page_size, page: rqst_page) }

            context "and page 1 is requested" do
              let(:rqst_page) { 1 }
              let(:rsp_page) { 1 }

              it "returns page 1" do
                send_request
                expect_enrollment_response_page_of([student_enrollment_1, student_enrollment_2, student_enrollment_3])
                expect(response_links).to have_correct_pagination_urls
              end
            end

            context "and page 2 is requested" do
              let(:rqst_page) { 2 }
              let(:rsp_page) { 2 }
              let(:rsp_page_size) { 2 }

              it "returns page 2" do
                send_request
                expect_enrollment_response_page_of([student_enrollment_4, student_enrollment_5])
                expect(response_links).to have_correct_pagination_urls
              end
            end
          end
        end

        context "for an assignment assigned to specific students" do
          include_context "assignment assigned to specific students"

          context "and a student role param is specified" do
            let(:pass_thru_params) { super().merge(role: "http://purl.imsglobal.org/vocab/lis/v2/membership#Learner") }
            let(:total_items) { 3 }

            it "returns only assigned student enrollments" do
              send_request
              expect_enrollment_response_page_of([student_enrollment_1, student_enrollment_3, student_enrollment_5])
              expect(response_links).to have_correct_pagination_urls
            end
          end

          context "and a non-student role param is specified" do
            let(:pass_thru_params) { super().merge(role: "http://purl.imsglobal.org/vocab/lis/v2/membership#Instructor") }
            let(:total_items) { 2 }

            it "returns only the non-student enrollments" do
              send_request
              expect_enrollment_response_page_of([teacher_enrollment, ta_enrollment])
              expect(response_links).to have_correct_pagination_urls
            end
          end

          context "and rlid references an assignment assigned to everyone" do
            let(:rlid_param) { rlid_param_2 }

            it "returns all course members" do
              send_request
              expect_enrollment_response_page
              expect(response_links).to have_correct_pagination_urls
            end
          end

          context "and pagination params are specified" do
            let(:rsp_page_size) { 4 }
            let(:rqst_page_size) { 4 }
            let(:effective_page_size) { rqst_page_size }
            let(:total_items) { 7 }
            let(:rqst_page) { raise "Override in example" }
            let(:params_overrides) { super().merge(limit: rqst_page_size, page: rqst_page) }

            context "and page 1 is requested" do
              let(:rqst_page) { 1 }
              let(:rsp_page) { 1 }

              it "returns page 1" do
                send_request
                expect_enrollment_response_page_of([
                                                     teacher_enrollment,
                                                     student_enrollment_1,
                                                     ta_enrollment,
                                                     observer_enrollment,
                                                   ])
                expect(response_links).to have_correct_pagination_urls
              end
            end

            context "and page 2 is requested" do
              let(:rqst_page) { 2 }
              let(:rsp_page) { 2 }
              let(:rsp_page_size) { 3 }

              it "returns page 2" do
                send_request
                expect_enrollment_response_page_of([
                                                     designer_enrollment,
                                                     student_enrollment_3,
                                                     student_enrollment_5
                                                   ])
                expect(response_links).to have_correct_pagination_urls
              end
            end
          end

          context "and a role and pagination params are specified" do
            let(:pass_thru_params) { super().merge(role: "http://purl.imsglobal.org/vocab/lis/v2/membership#Learner") }
            let(:rsp_page_size) { 2 }
            let(:rqst_page_size) { 2 }
            let(:effective_page_size) { rqst_page_size }
            let(:total_items) { 3 }
            let(:rqst_page) { raise "Override in example" }
            let(:params_overrides) { super().merge(limit: rqst_page_size, page: rqst_page) }

            context "and page 1 is requested" do
              let(:rqst_page) { 1 }
              let(:rsp_page) { 1 }

              it "returns page 1" do
                send_request
                expect_enrollment_response_page_of([student_enrollment_1, student_enrollment_3])
                expect(response_links).to have_correct_pagination_urls
              end
            end

            context "and page 2 is requested" do
              let(:rqst_page) { 2 }
              let(:rsp_page) { 2 }
              let(:rsp_page_size) { 1 }

              it "returns page 2" do
                send_request
                expect_enrollment_response_page_of([student_enrollment_5])
                expect(response_links).to have_correct_pagination_urls
              end
            end
          end

          context "for an assignment assigned to an inactive enrollment" do
            # student 5 is assigned, but lets deactivate his enrollment
            let!(:student_enrollment_5) { student_in_course(course:, active_all: false) }

            # Really just specifying the role so we can focus on verifying the enrollments we care about
            context "and a student role param is specified" do
              let(:pass_thru_params) { super().merge(role: "http://purl.imsglobal.org/vocab/lis/v2/membership#Learner") }
              let(:total_items) { 2 }

              it "returns only assigned _active_ student enrollments" do
                send_request
                # NB student_enrollment_4 should _not_ be present
                expect_enrollment_response_page_of([student_enrollment_1, student_enrollment_3])
                expect(response_links).to have_correct_pagination_urls
              end
            end
          end
        end
      end
      # rubocop:enable RSpec/LetSetup
    end
  end

  describe "#group_index" do
    let(:action) { :group_index }
    let(:context) { group_record }
    let(:context_param_name) { :group_id }
    let(:unknown_context_id) { group_record && (Group.maximum(:id) + 1) }

    it_behaves_like "advantage services"
    it_behaves_like "lti services"

    context "when a group has a single membership" do
      let(:group_record) { group_with_user(active_all: true, context: course).group }
      let(:group_member) { group_record.group_memberships.first }

      it "returns that membership" do
        send_request
        expect_single_member(group_member)
      end
    end

    context "when a group has a deleted membership" do
      let(:group_record) { group_with_user(active_all: true, context: course).group }
      let(:group_member) { group_record.group_memberships.first }

      it "does not return the deleted membership" do
        group_member.destroy
        send_request
        expect_empty_members_array
      end
    end

    context "when a group has a pending membership" do
      let(:group_record) { group_with_user(join_level: "invitation_only", context: course).group }
      let(:group_member) { group_record.group_memberships.first }

      it "does not return the pending membership" do
        pending("group memberships are always auto-accepted so cant test \"invited\" workflow state - see GroupMembership#auto_join")
        send_request
        expect_empty_members_array
      end
    end

    # rubocop:disable RSpec/LetSetup
    context "when the rlid param is specified" do
      let(:group_record) { group_with_user(active_all: true, context: course, name: user_full_name).group }
      let(:group_member) { group_record.group_memberships.first }
      let!(:enrollment) { group_member }

      it_behaves_like "rlid check"
    end
    # rubocop:enable RSpec/LetSetup

    context "when a group has multiple memberships" do
      let!(:group_leadership) do
        leader = group_membership_model(group: group_record, user: user_model)
        group_record.leader = leader.user
        group_record.save
        leader
      end
      let!(:group_membership_1) { group_membership_model(group: group_record, user: user_model) }
      let!(:group_membership_2) { group_membership_model(group: group_record, user: user_model) }
      let!(:group_membership_3) { group_membership_model(group: group_record, user: user_model) }
      let!(:group_membership_4) { group_membership_model(group: group_record, user: user_model) }
      let!(:enrollments) do
        [
          group_leadership,
          group_membership_1,
          group_membership_2,
          group_membership_3,
          group_membership_4
        ]
      end
      let(:total_items) { enrollments.length }
      let(:rsp_page) { 1 }
      let(:rsp_page_size) { [total_items, effective_page_size].min }
      let(:effective_page_size) { Lti::IMS::Providers::MembershipsProvider::MAX_PAGE_SIZE } # system default
      let(:pass_thru_params) { { pass: "thru" } }
      let(:params_overrides) { super().merge(pass_thru_params) }

      it_behaves_like "multiple enrollments check"

      context "and the role param specifies the LIS Manager role" do
        let(:total_items) { 1 }
        let(:params_overrides) { super().merge(role: "http://purl.imsglobal.org/vocab/lis/v2/membership#Manager") }

        it "limits results to the Canvas group leader" do
          send_request
          expect_enrollment_response_page_of([group_leadership])
        end
      end

      context "and the role param specifies the LIS Member role" do
        let(:params_overrides) { super().merge(role: "http://purl.imsglobal.org/vocab/lis/v2/membership#Member") }

        it "returns all group members" do
          send_request
          expect_enrollment_response_page
        end
      end

      context "and the role param specifies an unknown value" do
        let(:params_overrides) { super().merge(role: "http://purl.imsglobal.org/total/nonsense") }

        it "returns no group members" do
          send_request
          expect_empty_members_array
        end
      end

      context "and the role param specifies a valid but non-Group LIS value" do
        let(:params_overrides) { super().merge(role: "http://purl.imsglobal.org/vocab/lis/v2/membership#Instructor") }

        it "returns no group members" do
          send_request
          expect_empty_members_array
        end
      end

      # Somewhat redundant for groups which just have two roles, one of which everyone in the group shares. But
      # still useful for anti-regression purposes to at least verify that combining role + pagination params doesn't
      # break anything
      context "and role and pagination params are specified" do
        let(:rsp_page_size) { 1 }
        let(:rqst_page_size) { 1 }
        let(:effective_page_size) { rqst_page_size }
        let(:rqst_page) { 1 }
        let(:pass_thru_params) { super().merge(role: "http://purl.imsglobal.org/vocab/lis/v2/membership#Member") }
        let(:params_overrides) do
          super().merge(
            limit: rqst_page_size,
            page: rqst_page
          )
        end

        context "and the first page of results are requested" do
          it "returns the correct first page of results" do
            send_request
            expect_enrollment_response_page_of([group_leadership])
            expect(response_links).to have_correct_pagination_urls
          end
        end

        context "and the second page of results are requested" do
          let(:rqst_page) { 2 }
          let(:rsp_page) { 2 }

          it "returns the correct second page of results" do
            send_request
            expect_enrollment_response_page_of([group_membership_1])
            expect(response_links).to have_correct_pagination_urls
          end
        end

        context "and the role param limits results to the group leader" do
          let(:total_items) { 1 }
          let(:pass_thru_params) { super().merge(role: "http://purl.imsglobal.org/vocab/lis/v2/membership#Manager") }

          it "returns the group leader" do
            send_request
            expect_enrollment_response_page_of([group_leadership])
            expect(response_links).to have_correct_pagination_urls
          end
        end
      end

      # Group Assignments can't be associated to external tools, so this is basically the same set of tests as
      # for Course contexts, except that we're just checking to see if we can narrow results even further based
      # on group memberships
      # rubocop:disable RSpec/LetSetup
      context "and an assignment rlid param is specified" do
        include_context "assignment context"
        let(:rlid_param) { rlid_param_1 }
        let!(:group_leadership) do
          leader = group_membership_model(group: group_record, user: student_enrollment_1.user)
          group_record.leader = leader.user
          group_record.save
          leader
        end
        # student 1 is our leader, add another rando student as group member 1
        let!(:group_membership_1) do
          group_membership_model(group: group_record, user: student_in_course(course:, active_all: true).user)
        end
        let!(:group_membership_2) { group_membership_model(group: group_record, user: student_enrollment_2.user) }
        # to test 'narrowing', leave student 3 out, add another rando student as group member 3
        let!(:group_membership_3) do
          group_membership_model(group: group_record, user: student_in_course(course:, active_all: true).user)
        end
        let!(:group_membership_4) { group_membership_model(group: group_record, user: student_enrollment_4.user) }
        let!(:group_membership_5) { group_membership_model(group: group_record, user: student_enrollment_5.user) }
        let!(:enrollments) do # cant just append to super() b/c this gets clobbered by 'assignment context'
          [
            group_leadership,
            group_membership_1,
            group_membership_2,
            group_membership_3,
            group_membership_4,
            group_membership_5
          ]
        end

        shared_context "returns all paginated group members" do
          let(:rsp_page_size) { 3 }
          let(:rqst_page_size) { 3 }
          let(:effective_page_size) { rqst_page_size }
          let(:rqst_page) { raise "Override in example" }
          let(:params_overrides) { super().merge(limit: rqst_page_size, page: rqst_page) }

          context "and page 1 is requested" do
            let(:rqst_page) { 1 }
            let(:rsp_page) { 1 }

            it "returns page 1" do
              send_request
              expect_enrollment_response_page
              # expect_enrollment_response_page_of([group_leadership, group_membership_1, group_membership_2])
              expect(response_links).to have_correct_pagination_urls
            end
          end

          context "and page 2 is requested" do
            let(:rqst_page) { 2 }
            let(:rsp_page) { 2 }

            it "returns page 2" do
              send_request
              expect_enrollment_response_page
              # expect_enrollment_response_page_of([group_membership_3, group_membership_4])
              expect(response_links).to have_correct_pagination_urls
            end
          end
        end

        shared_context "returns assigned paginated group members" do
          let(:rsp_page_size) { 1 }
          let(:rqst_page_size) { 1 }
          let(:effective_page_size) { rqst_page_size }
          let(:total_items) { 2 }
          let(:rqst_page) { raise "Override in example" }
          let(:params_overrides) { super().merge(limit: rqst_page_size, page: rqst_page) }

          context "and page 1 is requested" do
            let(:rqst_page) { 1 }
            let(:rsp_page) { 1 }

            it "returns page 1" do
              send_request
              expect_enrollment_response_page_of([group_leadership])
              expect(response_links).to have_correct_pagination_urls
            end
          end

          context "and page 2 is requested" do
            let(:rqst_page) { 2 }
            let(:rsp_page) { 2 }

            it "returns page 2" do
              send_request
              expect_enrollment_response_page_of([group_membership_5])
              expect(response_links).to have_correct_pagination_urls
            end
          end
        end

        context "for an assignment assigned to everyone" do
          it "returns all group members" do
            send_request
            expect_enrollment_response_page
            expect(response_links).to have_correct_pagination_urls
          end

          context "and a member role param is specified" do
            let(:pass_thru_params) { super().merge(role: "http://purl.imsglobal.org/vocab/lis/v2/membership#Member") }
            let(:total_items) { 6 }

            it "returns all group members" do
              send_request
              expect_enrollment_response_page
              expect(response_links).to have_correct_pagination_urls
            end
          end

          context "and a manager role param is specified" do
            let(:pass_thru_params) { super().merge(role: "http://purl.imsglobal.org/vocab/lis/v2/membership#Manager") }
            let(:total_items) { 1 }

            it "returns only the group leader" do
              send_request
              expect_enrollment_response_page_of([group_leadership])
              expect(response_links).to have_correct_pagination_urls
            end
          end

          context "and the role param specifies an unknown value" do
            let(:params_overrides) { super().merge(role: "http://purl.imsglobal.org/total/nonsense") }

            it "returns no group members" do
              send_request
              expect_empty_members_array
            end
          end

          context "and pagination params are specified" do
            it_behaves_like "returns all paginated group members"

            context "and a role param is specified" do
              let(:pass_thru_params) { super().merge(role: "http://purl.imsglobal.org/vocab/lis/v2/membership#Member") }

              it_behaves_like "returns all paginated group members"
            end
          end
        end

        context "for an assignment assigned to specific students" do
          # included context assigns to students 1, 3, and 5, but student 3 is not in our group
          include_context "assignment assigned to specific students"

          context "and a member role param is specified" do
            let(:pass_thru_params) { super().merge(role: "http://purl.imsglobal.org/vocab/lis/v2/membership#Member") }
            let(:total_items) { 2 }

            it "returns only assigned students further narrowed to current group" do
              send_request
              expect_enrollment_response_page_of([group_leadership, group_membership_5])
              expect(response_links).to have_correct_pagination_urls
            end
          end

          context "and a manager role param is specified" do
            let(:pass_thru_params) { super().merge(role: "http://purl.imsglobal.org/vocab/lis/v2/membership#Manager") }
            let(:total_items) { 1 }

            it "returns only assigned students further narrowed to current group" do
              send_request
              expect_enrollment_response_page_of([group_leadership])
              expect(response_links).to have_correct_pagination_urls
            end
          end

          context "and the role param specifies an unknown value" do
            let(:params_overrides) { super().merge(role: "http://purl.imsglobal.org/total/nonsense") }

            it "returns no group members" do
              send_request
              expect_empty_members_array
            end
          end

          context "and rlid references an assignment assigned to everyone" do
            let(:rlid_param) { rlid_param_2 }

            it "returns all course members" do
              send_request
              expect_enrollment_response_page
              expect(response_links).to have_correct_pagination_urls
            end
          end

          it_behaves_like "returns assigned paginated group members"

          context "and a role and pagination params are specified" do
            let(:pass_thru_params) { super().merge(role: "http://purl.imsglobal.org/vocab/lis/v2/membership#Member") }

            it_behaves_like "returns assigned paginated group members"
          end
        end

        context "and the resource link contains additional custom params" do
          let(:resource_link) do
            rl = assignment_with_rlid_1.primary_resource_link
            rl.update!(custom: custom_params)
            rl
          end
          let(:tool) do
            tool = super()
            tool.settings[:custom_fields] = tool_params
            tool.save!
            tool
          end
          let(:custom_params) { { "foo" => "bar" } }
          let(:tool_params) { { "baz" => "qux" } }
          let(:expected_params) { custom_params.merge(tool_params).with_indifferent_access }
          let(:rlid_param) { rlid_param_1 }

          it "includes the custom params from the tool and the resource link" do
            tool
            resource_link
            send_request
            expect(json[:members][0][:message][0]["https://purl.imsglobal.org/spec/lti/claim/custom"]).to eq(
              expected_params
            )
          end
        end
      end
      # rubocop:enable RSpec/LetSetup
    end
  end

  def send_http
    get action, params: { context_param_name => context_id }.merge(params_overrides)
  end

  def expect_empty_response
    expect_empty_members_array
  end

  def expect_single_member(*enrollment)
    expect_member(*enrollment)
    expect_member_count(1)
    enrollment
  end

  def expect_member(*enrollment, index: 0)
    # Not doing contain_exactly() b/c it's impossible to see which specific field is problematic in
    # any given bad element.
    expect(json[:members][index]).to match_enrollment(*enrollment)
    enrollment
  end

  def expect_empty_members_array
    expect(json[:members]).to be_empty
    expect(json[:members]).to be_a(Array)
  end

  def expect_member_count(count)
    expect(json[:members].length).to eq(count)
  end

  def custom_enrollment_in_course(base_type_name, opts = {})
    opts[:account] ||= opts[:course].account
    opts[:role] ||= custom_role(base_type_name, "Custom#{base_type_name}", opts)
    course_with_user(base_type_name, opts)
  end

  def match_enrollment(*enrollment)
    if respond_to?(:rlid_param) && rlid_param.present?
      match_enrollment_for_rlid({}, *enrollment)
    elsif enrollment.first.is_a?(Enrollment)
      be_lti_course_membership({ expected: enrollment })
    else
      be_lti_group_membership({ expected: enrollment.first })
    end
  end

  def match_enrollment_for_rlid(message_matcher, *enrollment)
    opts = { message_matcher: }
    return be_lti_course_membership(opts.merge!(expected: enrollment)) if enrollment.first.is_a?(Enrollment)

    be_lti_group_membership(opts.merge!(expected: enrollment.first))
  end

  def expect_enrollment_response_page
    expect_enrollment_response_page_of(enrollments
      .sort_by { |e| e.user.id }
      .slice(effective_page_size * (rsp_page - 1), effective_page_size))
  end

  def expect_enrollment_response_page_of(scoped_enrollments)
    scoped_enrollments.each_with_index { |e, i| expect_member(e, index: i) }
    expect_member_count([rsp_page_size, total_items].min)
  end

  def have_correct_pagination_urls
    total_pages = page_count(total_items, effective_page_size)
    pass_thrus = as_query_params(pass_thru_params)

    expected_links = [
      "<http://test.host/api/lti/#{context.class.to_s.downcase}s/#{context_id}/names_and_roles?#{pass_thrus}page=#{rsp_page}&per_page=#{effective_page_size}>; rel=\"current\"",
      "<http://test.host/api/lti/#{context.class.to_s.downcase}s/#{context_id}/names_and_roles?#{pass_thrus}page=#{rsp_page + 1}&per_page=#{effective_page_size}>; rel=\"next\"",
      "<http://test.host/api/lti/#{context.class.to_s.downcase}s/#{context_id}/names_and_roles?#{pass_thrus}page=#{rsp_page - 1}&per_page=#{effective_page_size}>; rel=\"prev\"",
      "<http://test.host/api/lti/#{context.class.to_s.downcase}s/#{context_id}/names_and_roles?#{pass_thrus}page=1&per_page=#{effective_page_size}>; rel=\"first\"",
      "<http://test.host/api/lti/#{context.class.to_s.downcase}s/#{context_id}/names_and_roles?#{pass_thrus}page=#{total_pages}&per_page=#{effective_page_size}>; rel=\"last\""
    ]

    expected_links.reject! { |el| el.include?('rel="next"') } if rsp_page == total_pages
    expected_links.reject! { |el| el.include?('rel="prev"') } if rsp_page <= 1
    expected_links.reject! { |el| el.include?('rel="last"') } if rsp_page > total_pages
    match_array(expected_links)
  end

  def as_query_params(params_hash)
    params_hash.empty? ? "" : "#{params_hash.to_query}&"
  end

  def page_count(total_items, page_size)
    (total_items / page_size) + ((total_items % page_size > 0) ? 1 : 0)
  end

  def response_links
    response.headers["Link"].split(",")
  end

  def assignment_with_rlid(rlid)
    assignment_model({
                       course:,
                       lti_context_id: rlid,
                       submission_types: "external_tool",
                       external_tool_tag_attributes: {
                         url: tool.url,
                         content_type: "context_external_tool",
                         content_id: tool.id
                       }
                     })
  end

  def assign(assignment, *users_or_user_containers)
    assignment.only_visible_to_overrides = true
    assignment.save!
    create_adhoc_override_for_assignment(assignment, users_from(*users_or_user_containers))
    assignment
  end

  def users_from(*users_or_user_containers)
    users_or_user_containers.map { |uc| uc.respond_to?(:user) ? uc.user : uc }
  end
end
