# frozen_string_literal: true

#
# Copyright (C) 2011 - 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/>.
#

describe GroupMembership do
  it "ensures a mutually exclusive relationship" do
    category = Account.default.group_categories.create!(name: "blah")
    group1 = category.groups.create!(context: Account.default)
    group2 = category.groups.create!(context: Account.default)
    user_model

    # start with one active membership
    gm1 = group1.group_memberships.create!(user: @user, workflow_state: "accepted")
    expect(gm1.reload).to be_accepted

    # adding another should mark the first as deleted
    gm2 = group2.group_memberships.create!(user: @user, workflow_state: "accepted")
    expect(gm2.reload).to be_accepted
    expect(gm1.reload).to be_deleted

    # restoring the first should mark the second as deleted
    gm1.workflow_state = "accepted"
    gm1.save!
    expect(gm1.reload).to be_accepted
    expect(gm2.reload).to be_deleted

    # should work even if we start with bad data (two accepted memberships)
    GroupMembership.where(id: gm2).update_all(workflow_state: "accepted")
    gm1.save!
    expect(gm1.reload).to be_accepted
    expect(gm2.reload).to be_deleted
  end

  it "is not valid if the group is full" do
    course_factory
    category = @course.group_categories.build(name: "category 1")
    category.group_limit = 2
    category.save!
    group = category.groups.create!(context: @course)
    # when the group is full
    group.group_memberships.create!(user: user_model, workflow_state: "accepted")
    group.group_memberships.create!(user: user_model, workflow_state: "accepted")
    # expect
    membership = group.reload.group_memberships.build(user: user_model, workflow_state: "accepted")
    expect(membership).not_to be_valid
    expect(membership.errors[:group_id]).to eq ["The group is full."]
  end

  context "section homogeneity" do
    # can't use 'course' because it is defined in spec_helper, so use 'course1'
    let_once(:course1) do
      course_with_teacher(active_all: true)
      @course
    end
    let_once(:student) do
      student = user_model
      course1.enroll_student(student)
      student
    end
    let_once(:group_category) { GroupCategory.student_organized_for(course1) }
    let_once(:group) { course1.groups.create(group_category:) }
    let_once(:group_membership) { group.group_memberships.create(user: student) }

    it "has a validation error on new record" do
      membership = GroupMembership.new

      allow(membership).to receive_messages(user: double(name: "test user"),
                                            group: double(name: "test group"),
                                            restricted_self_signup?: true,
                                            has_common_section_with_me?: false)
      expect(membership.save).not_to be_truthy
      expect(membership.errors.size).to eq 1
      expect(membership.errors[:user_id].to_s).to match(/test user does not share a section/)
    end

    it "passes validation on update" do
      expect do
        group_membership.save!
      end.not_to raise_error
    end
  end

  context "Notifications" do
    context "in published course" do
      before do
        course_with_teacher(active_all: true)
        @student1 = student_in_course(active_all: true).user
        @group1 = @course.groups.create(group_category: GroupCategory.student_organized_for(@course))
      end

      it "sends message if the first membership in a student organized group", priority: "1" do
        Notification.create(name: "New Student Organized Group", category: "TestImmediately")
        communication_channel(@teacher, { username: "test_channel_email_#{@teacher.id}@test.com", active_cc: true })

        group_membership = @group1.group_memberships.create(user: @student1)
        expect(group_membership.messages_sent["New Student Organized Group"]).not_to be_empty
      end

      it "sends message when a new student is invited to group and auto-joins", priority: "1" do
        Notification.create!(name: "New Context Group Membership", category: "TestImmediately")
        student2 = student_in_course(active_all: true).user
        communication_channel(student2, { username: "test_channel_email_#{student2.id}@test.com", active_cc: true })
        group_membership = @group1.group_memberships.create(user: @student1)
        @group1.add_user(student2)
        expect(group_membership.messages_sent["New Context Group Membership"]).not_to be_empty
      end

      it "does not dispatch a message if the membership has been created with SIS" do
        membership = @group1.group_memberships.build(user: @student1)
        Notification.create!(name: "New Context Group Membership", category: "TestImmediately")
        Notification.create!(name: "New Context Group Membership Invitation", category: "TestImmediately")
        batch = @course.root_account.sis_batches.create!
        membership.sis_batch_id = batch.id
        membership.save!
        expect(membership.messages_sent).to be_empty
      end

      it "dispatches a message if the course is available and has started" do
        membership = @group1.group_memberships.build(user: @student1)
        Notification.create!(name: "New Context Group Membership", category: "TestImmediately")
        membership.save!
        expect(membership.messages_sent["New Context Group Membership"]).not_to be_empty
      end

      it "does not dispatch a message if the course is available and has not started yet" do
        course = course_factory(active_all: true)
        course.start_at = 1.day.from_now
        course.restrict_enrollments_to_course_dates = true
        course.save!
        student_in_course(active_all: true, course:)
        group1 = course.groups.create(group_category: GroupCategory.student_organized_for(course))
        membership = group1.group_memberships.build(user: @student)
        Notification.create!(name: "New Context Group Membership", category: "TestImmediately")
        Notification.create!(name: "New Context Group Membership Invitation", category: "TestImmediately")
        membership.save!
        expect(membership.messages_sent).to be_empty
      end
    end

    it "does not dispatch a message if the course is unpublished" do
      course_with_teacher
      student = user_model
      group = @course.groups.create(group_category: GroupCategory.student_organized_for(@course))
      membership = group.group_memberships.build(user: student)
      @course.enroll_student(student)
      Notification.create!(name: "New Context Group Membership", category: "TestImmediately")
      membership.save!
      expect(membership.messages_sent).to be_empty
    end

    it "does not dispatch a message if the course is soft-concluded" do
      course_with_teacher(active_all: true)
      @course.soft_conclude!
      @course.save!
      student = user_model
      group = @course.groups.create(group_category: GroupCategory.student_organized_for(@course))
      membership = group.group_memberships.build(user: student)
      @course.enroll_student(student)
      Notification.create!(name: "New Context Group Membership", category: "TestImmediately")
      membership.save!
      expect(membership.messages_sent).to be_empty
    end
  end

  it "is invalid if group wants a common section, but doesn't have one with the user" do
    course_with_teacher(active_all: true)
    section1 = @course.course_sections.create
    section2 = @course.course_sections.create
    user1 = section1.enroll_user(user_model, "StudentEnrollment").user
    user2 = section2.enroll_user(user_model, "StudentEnrollment").user
    group_category = @course.group_categories.build(name: "My Category")
    group_category.configure_self_signup(true, true)
    group_category.save
    group = group_category.groups.create(context: @course)
    group.add_user(user1)
    membership = group.group_memberships.build(user: user2)
    expect(membership).not_to be_valid
    expect(membership.errors[:user_id]).not_to be_nil
  end

  context "active_given_enrollments?" do
    before :once do
      @enrollment = course_with_student(active_all: true)
      @course_group = @course.groups.create!
      @membership = @course_group.add_user(@student)
    end

    it "is false if the membership is pending (requested)" do
      @membership.workflow_state = "requested"
      expect(@membership.active_given_enrollments?([@enrollment])).to be_falsey
    end

    it "is false if the membership is terminated (deleted)" do
      @membership.workflow_state = "deleted"
      expect(@membership.active_given_enrollments?([@enrollment])).to be_falsey
    end

    it "is false given a course group without an enrollment in the list" do
      expect(@membership.active_given_enrollments?([])).to be_falsey
    end

    it "is true for other course groups" do
      expect(@membership.active_given_enrollments?([@enrollment])).to be_truthy
    end

    it "is true for account groups regardless of enrollments" do
      @account_group = Account.default.groups.create!
      @membership = @account_group.add_user(@student)
      expect(@membership.active_given_enrollments?([])).to be_truthy
    end

    it "is not deleted when the enrollment is destroyed" do
      @enrollment.destroy
      @membership.reload
      expect(@membership.workflow_state).to eq "deleted"
    end

    it "softs delete when membership destroyed" do
      @membership.destroy
      @membership.reload
      expect(@membership.workflow_state).to eq "deleted"
    end
  end

  it "auto_joins for backwards compatibility" do
    user_model
    group_model
    group_membership_model(workflow_state: "invited")
    expect(@group_membership.workflow_state).to eq "accepted"
  end

  it "does not auto_join for communities" do
    user_model
    @communities = GroupCategory.communities_for(Account.default)
    group_model(name: "Algebra Teachers", group_category: @communities, join_level: "parent_context_request")
    group_membership_model(user: @user, workflow_state: "requested")
    expect(@group_membership.workflow_state).to eq "requested"
  end

  context "permissions" do
    before :once do
      course_with_teacher(active_all: true)
    end

    it "allows someone to join an open, non-community group" do
      student_in_course(active_all: true)
      student_organized = GroupCategory.student_organized_for(@course)
      student_group = student_organized.groups.create!(context: @course, join_level: "parent_context_auto_join")
      expect(GroupMembership.new(user: @student, group: student_group).grants_right?(@student, :create)).to be_truthy

      course_groups = group_category
      course_groups.configure_self_signup(true, false)
      course_groups.save!
      course_group = course_groups.groups.create!(context: @course, join_level: "invitation_only")
      expect(GroupMembership.new(user: @student, group: course_group).grants_right?(@student, :create)).to be_truthy
    end

    it "allows someone to be added to a non-community group" do
      student_in_course(active_all: true)
      course_groups = group_category
      course_group = course_groups.groups.create!(context: @course, join_level: "invitation_only")
      expect(GroupMembership.new(user: @student, group: course_group).grants_right?(@teacher, :create)).to be_truthy

      @account = @course.root_account
      account_admin_user(active_all: true, account: @account)
      account_groups = group_category(context: @account)
      account_group = account_groups.groups.create!(context: @account)
      expect(GroupMembership.new(user: @student, group: account_group).grants_right?(@admin, :create)).to be_truthy
    end

    it "allows a teacher to join a student to a group in an unpublished course" do
      @course.claim!
      student_in_course(active_all: true)
      course_groups = group_category
      course_group = course_groups.groups.create!(context: @course, join_level: "invitation_only")
      expect(GroupMembership.new(user: @student, group: course_group).grants_right?(@teacher, :create)).to be_truthy
    end

    it "allows someone to join an open community group" do
      @account = @course.root_account
      community_groups = GroupCategory.communities_for(@account)
      community_group = community_groups.groups.create!(context: @account, join_level: "parent_context_auto_join")
      expect(GroupMembership.new(user: @teacher, group: community_group).grants_right?(@teacher, :create)).to be_truthy
    end

    it "does not allow someone to be added to a community group" do
      @account = @course.root_account
      account_admin_user(active_all: true, account: @account)
      community_groups = GroupCategory.communities_for(@account)
      community_group = community_groups.groups.create!(context: @account, join_level: "parent_context_auto_join")
      expect(GroupMembership.new(user: @teacher, group: community_group).grants_right?(@admin, :create)).to be_falsey
    end

    it "allows a moderator to kick someone from a community" do
      @account = @course.root_account
      account_admin_user(active_all: true, account: @account)
      community_groups = GroupCategory.communities_for(@account)
      community_group = community_groups.groups.create!(context: @account, join_level: "parent_context_auto_join")
      community_group.add_user(@admin, "accepted", true)
      community_group.add_user(@teacher, "accepted", false)
      expect(GroupMembership.where(group_id: community_group.id, user_id: @teacher.id).first.grants_right?(@admin, :delete)).to be_truthy
    end

    it "does not allow students in group to have any membership permissions" do
      student_in_course(active_all: true)
      @category = @course.group_categories.build(name: "category 1", non_collaborative: true)
      @category.save!
      @group = @category.groups.create!(context: @course)
      @membership = @group.add_user(@student)

      expect(@membership.grants_right?(@student, :read)).to be_falsey
      expect(@membership.grants_right?(@student, :delete)).to be_falsey
      expect(@membership.grants_right?(@student, :update)).to be_falsey
      expect(@membership.grants_right?(@student, :create)).to be_falsey
      expect(@membership.check_policy(@student)).to be_empty

      expect(@membership.user_id).to eq @student.id
    end

    it "does not allow ta without permissions to add to a tag" do
      Account.default.settings[:allow_assign_to_differentiation_tags] = { value: true }
      Account.default.save!
      Account.default.reload
      teacher_in_course(active_all: true)
      student_in_course(active_all: true)
      ta_in_course(active_all: true)
      @category = @course.group_categories.build(name: "category 1", non_collaborative: true)
      @category.save!

      @group = @category.groups.create!(context: @course)
      @membership = @group.add_user(@student)

      # By default TAs don't have Dif tag permissions
      expect(@membership.grants_right?(@ta, :read)).to be_falsey
      expect(@membership.grants_right?(@ta, :delete)).to be_falsey
      expect(@membership.grants_right?(@ta, :update)).to be_falsey
      expect(@membership.grants_right?(@ta, :create)).to be_falsey
      expect(@membership.check_policy(@ta)).to be_empty

      # Students shouldn't have these permissions for dif tags
      expect(@membership.grants_right?(@student, :read)).to be_falsey
      expect(@membership.grants_right?(@student, :delete)).to be_falsey
      expect(@membership.grants_right?(@student, :update)).to be_falsey
      expect(@membership.grants_right?(@student, :create)).to be_falsey
      expect(@membership.check_policy(@student)).to be_empty

      # Teachers have these permissions by default
      expect(@membership.grants_right?(@teacher, :read)).to be_truthy
      expect(@membership.grants_right?(@teacher, :delete)).to be_truthy
      expect(@membership.grants_right?(@teacher, :update)).to be_truthy
      expect(@membership.grants_right?(@teacher, :create)).to be_truthy
    end
  end

  it "updates group leadership as membership changes" do
    course_factory
    @category = @course.group_categories.build(name: "category 1")
    @category.save!
    @group = @category.groups.create!(context: @course)
    @category.auto_leader = "first"
    @category.save!
    leader = user_model
    @group.group_memberships.create!(user: leader, workflow_state: "accepted")
    expect(@group.reload.leader).to eq leader
  end

  describe "updating cached due dates" do
    before :once do
      course_factory
      @group_category = @course.group_categories.create!(name: "category")
      @membership = group_with_user(group_context: @course, group_category: @group_category)

      # back-populate associations so we don't need to reload
      @membership.group = @group
      @group.group_category = @group_category

      @assignments = Array.new(3) { assignment_model(course: @course) }
      @assignments.last.group_category = nil
      @assignments.last.save!
    end

    it "triggers a batch when membership is created" do
      new_user = user_factory

      expect(SubmissionLifecycleManager).not_to receive(:recompute)
      expect(SubmissionLifecycleManager).to receive(:recompute_users_for_course).with(
        new_user.id,
        @course.id,
        match_array(@assignments[0..1].map(&:id))
      )

      @group.group_memberships.create(user: new_user)
    end

    it "triggers a batch when membership is deleted" do
      expect(SubmissionLifecycleManager).not_to receive(:recompute)
      expect(SubmissionLifecycleManager).to receive(:recompute_users_for_course).with(
        @membership.user.id,
        @course.id,
        match_array(@assignments[0..1].map(&:id))
      )
      @membership.destroy
    end

    it "does not trigger when nothing changed" do
      expect(SubmissionLifecycleManager).not_to receive(:recompute)
      expect(SubmissionLifecycleManager).not_to receive(:recompute_course)
      @membership.save
    end

    it "does not trigger when it's an account group" do
      expect(SubmissionLifecycleManager).not_to receive(:recompute)
      expect(SubmissionLifecycleManager).not_to receive(:recompute_course)
      @group = Account.default.groups.create!(name: "Group!")
      @group.group_memberships.create!(user: user_factory)
    end

    context "non-collaborative group" do
      before do
        account = @course.account
        account.tap do |a|
          a.settings[:allow_assign_to_differentiation_tags] = { value: true }
          a.save!
        end
        student1 = user_factory
        diff_tag_category = @course.group_categories.create!(name: "Differentiation Tag Category", non_collaborative: true)
        @diff_tag = @course.groups.create!(name: "Diff Tag 1", group_category: diff_tag_category, non_collaborative: true)
        @dt_membership = @diff_tag.group_memberships.create(user: student1)

        @da = assignment_model(course: @course)
        diff_tag_override = assignment_override_model(assignment: @da)
        diff_tag_override.set_type = "Group"
        diff_tag_override.set_id = @diff_tag.id
        diff_tag_override.save!
        @da.update!(only_visible_to_overrides: true)
      end

      it "triggers a batch when membership is created" do
        new_user = user_factory

        expect(SubmissionLifecycleManager).not_to receive(:recompute)
        expect(SubmissionLifecycleManager).to receive(:recompute_users_for_course).with(
          new_user.id,
          @course.id,
          match_array([@da.id])
        )

        @diff_tag.group_memberships.create(user: new_user)
      end

      it "triggers a batch when membership is deleted" do
        expect(SubmissionLifecycleManager).not_to receive(:recompute)
        expect(SubmissionLifecycleManager).to receive(:recompute_users_for_course).with(
          @dt_membership.user.id,
          @course.id,
          match_array([@da.id])
        )
        @dt_membership.destroy
      end
    end
  end

  it "runs due date updates for discussion assignments" do
    group_discussion_assignment
    @assignment.update_attribute(:only_visible_to_overrides, true)
    @assignment.assignment_overrides.create!(set: @group1)
    @student1 = student_in_course(course: @course, active_all: true).user
    membership = @group1.add_user(@student1)
    @topic.child_topic_for(@student1).reply_from(user: @student1, text: "sup")
    sub = @assignment.submission_for_student(@student1)
    expect(sub).to be_submitted
    membership.destroy
    expect(sub.reload).to be_deleted # no longer part of the group so the assignment no longer applies to them
    membership.update_attribute(:workflow_state, "accepted")
    expect(sub.reload).to be_submitted # back to the way it was
  end

  describe "root_account_id" do
    let(:category) { course.group_categories.create!(name: "category 1") }
    let(:course) { course_factory && @course }
    let(:group) { category.groups.create!(context: course) }
    let(:user) { user_model }

    it "assigns it on save if it is not set" do
      membership = group.group_memberships.create!(user:)
      membership.root_account_id = nil

      expect do
        membership.save!
      end.to change {
        membership.root_account_id
      }.from(nil).to(group.root_account_id)
    end

    it "preserves it on save if it was already set" do
      membership = group.group_memberships.create!(user:)

      expect(membership.group).not_to receive(:root_account_id)

      expect do
        membership.save!
      end.not_to change {
        GroupMembership.find(membership.id).root_account_id
      }
    end
  end
end
