spec/integration/mentioning_spec.rb
# frozen_string_literal: true

module MentioningSpecHelpers
  def notifications_about_mentioning(user, object)
    table = object.class.table_name

    if object.is_a?(StatusMessage)
      klass = Notifications::MentionedInPost
    elsif object.is_a?(Comment)
      klass = Notifications::MentionedInComment
    end

    klass
      .where(recipient_id: user.id)
      .joins("LEFT OUTER JOIN mentions ON notifications.target_id = mentions.id AND "\
             "notifications.target_type = 'Mention'")
      .joins("LEFT OUTER JOIN #{table} ON mentions_container_id = #{table}.id AND "\
             "mentions_container_type = '#{object.class.base_class}'").where(table.to_sym => {id: object.id})
  end

  def mention_container_path(object)
    object.is_a?(Post) ? post_path(object) : post_path(object.parent, anchor: object.guid)
  end

  def mentioning_mail_notification(user, object)
    ActionMailer::Base.deliveries.select {|delivery|
      delivery.to.include?(user.email) &&
        delivery.subject.include?(I18n.t("notifier.mentioned.subject", name: "")) &&
        delivery.body.parts[0].body.include?(mention_container_path(object))
    }
  end

  def also_commented_mail_notification(user, post)
    ActionMailer::Base.deliveries.select {|delivery|
      delivery.to.include?(user.email) &&
        delivery.subject.include?(I18n.t("notifier.also_commented.limited_subject")) &&
        delivery.body.parts[0].body.include?(post_path(post))
    }
  end

  def stream_for(user)
    stream = Stream::Multi.new(user)
    stream.posts
  end

  def mention_stream_for(user)
    stream = Stream::Mention.new(user)
    stream.posts
  end

  def post_status_message(mentioned_user, aspects=nil)
    aspects = user1.aspects.first.id.to_s if aspects.nil?
    sign_in user1
    status_msg = nil
    inlined_jobs do
      post "/status_messages.json", params: {
        status_message: {text: text_mentioning(mentioned_user)},
        aspect_ids:     aspects
      }
      status_msg = StatusMessage.find(JSON.parse(response.body)["id"])
    end
    status_msg
  end

  def receive_each(entity, recipients)
    inlined_jobs do
      recipients.each do |recipient|
        DiasporaFederation.callbacks.trigger(:receive_entity, entity, entity.author, recipient.id)
      end
    end
  end

  def find_private_message(guid)
    StatusMessage.find_by(guid: guid).tap do |status_msg|
      expect(status_msg).not_to be_nil
      expect(status_msg.public?).to be false
    end
  end

  def receive_status_message_via_federation(text, *recipients)
    entity = Fabricate(
      :status_message_entity,
      author: remote_raphael.diaspora_handle,
      text:   text,
      public: false
    )

    expect {
      receive_each(entity, recipients)
    }.to change(Post, :count).by(1).and change(ShareVisibility, :count).by(recipients.count)

    find_private_message(entity.guid)
  end

  def receive_comment_via_federation(text, parent)
    entity = build_relayable_federation_entity(
      :comment,
      parent_guid: parent.guid,
      author:      remote_raphael.diaspora_handle,
      parent:      Diaspora::Federation::Entities.related_entity(parent),
      text:        text
    )

    receive_each(entity, [parent.author.owner])

    Comment.find_by(guid: entity.guid)
  end
end

describe "mentioning", type: :request do
  include MentioningSpecHelpers

  RSpec::Matchers.define :be_mentioned_in do |object|
    include Rails.application.routes.url_helpers

    def user_notified?(user, object)
      notifications_about_mentioning(user, object).any? && mentioning_mail_notification(user, object).any?
    end

    match do |user|
      object.message.markdownified.include?(person_path(id: user.person.guid)) && user_notified?(user, object)
    end

    match_when_negated do |user|
      !user_notified?(user, object)
    end
  end

  RSpec::Matchers.define :be_in_streams_of do |user|
    match do |status_message|
      stream_for(user).map(&:id).include?(status_message.id) &&
        mention_stream_for(user).map(&:id).include?(status_message.id)
    end

    match_when_negated do |status_message|
      !stream_for(user).map(&:id).include?(status_message.id) &&
        !mention_stream_for(user).map(&:id).include?(status_message.id)
    end
  end

  let(:user1) { FactoryBot.create(:user_with_aspect) }
  let(:user2) { FactoryBot.create(:user_with_aspect, friends: [user1, user3]) }
  let(:user3) { FactoryBot.create(:user_with_aspect) }

  # see: https://github.com/diaspora/diaspora/issues/4160
  it "only mentions people that are in the target aspect" do
    status_msg = nil
    expect {
      status_msg = post_status_message(user3)
    }.to change(Post, :count).by(1).and change(AspectVisibility, :count).by(1)

    expect(status_msg).not_to be_nil
    expect(status_msg.public?).to be false
    expect(status_msg.text).to include(user3.name)

    expect(user3).not_to be_mentioned_in(status_msg)
    expect(status_msg).not_to be_in_streams_of(user3)
  end

  context "in private post via federation" do
    let(:status_msg) {
      receive_status_message_via_federation(text_mentioning(user2, user3), user3)
    }

    it "receiver is mentioned in status message" do
      expect(user3).to be_mentioned_in(status_msg)
    end

    it "receiver can see status message in streams" do
      expect(status_msg).to be_in_streams_of(user3)
    end

    it "non-receiver is not mentioned in status message" do
      expect(user2).not_to be_mentioned_in(status_msg)
    end

    it "non-receiver can't see status message in streams" do
      expect(status_msg).not_to be_in_streams_of(user2)
    end
  end

  context "in private post via federation with multiple recipients" do
    let(:status_msg) {
      receive_status_message_via_federation(text_mentioning(user3, user2), user3, user2)
    }

    it "mentions all recipients in the status message" do
      [user2, user3].each do |user|
        expect(user).to be_mentioned_in(status_msg)
      end
    end

    it "all recipients can see status message in streams" do
      [user2, user3].each do |user|
        expect(status_msg).to be_in_streams_of(user)
      end
    end
  end

  it "mentions people in public posts" do
    status_msg = nil
    expect {
      status_msg = post_status_message(user3, "public")
    }.to change(Post, :count).by(1)

    expect(status_msg).not_to be_nil
    expect(status_msg.public?).to be true
    expect(status_msg.text).to include(user3.diaspora_handle)

    expect(user3).to be_mentioned_in(status_msg)
    expect(status_msg).to be_in_streams_of(user3)
  end

  it "mentions people that are in the target aspect" do
    status_msg = nil
    expect {
      status_msg = post_status_message(user2)
    }.to change(Post, :count).by(1).and change(AspectVisibility, :count).by(1)

    expect(status_msg).not_to be_nil
    expect(status_msg.public?).to be false
    expect(status_msg.text).to include(user2.diaspora_handle)

    expect(user2).to be_mentioned_in(status_msg)
    expect(status_msg).to be_in_streams_of(user2)
  end

  context "in comments" do
    let(:author) { FactoryBot.create(:user_with_aspect) }

    shared_context "commenter is author" do
      let(:commenter) { author }
    end

    shared_context "commenter is author's friend" do
      let(:commenter) { FactoryBot.create(:user_with_aspect, friends: [author]) }
    end

    shared_context "commenter is not author's friend" do
      let(:commenter) { FactoryBot.create(:user) }
    end

    shared_context "mentioned user is author" do
      let(:mentioned_user) { author }
    end

    shared_context "mentioned user is author's friend" do
      let(:mentioned_user) { FactoryBot.create(:user_with_aspect, friends: [author]) }
    end

    shared_context "mentioned user is not author's friend" do
      let(:mentioned_user) { FactoryBot.create(:user) }
    end

    context "with public post" do
      let(:status_msg) { FactoryBot.create(:status_message, author: author.person, public: true) }

      [
        ["commenter is author's friend", "mentioned user is not author's friend"],
        ["commenter is author's friend", "mentioned user is author"],
        ["commenter is not author's friend", "mentioned user is author's friend"],
        ["commenter is not author's friend", "mentioned user is not author's friend"],
        ["commenter is author", "mentioned user is author's friend"],
        ["commenter is author", "mentioned user is not author's friend"]
      ].each do |commenters_context, mentioned_context|
        context "when #{commenters_context} and #{mentioned_context}" do
          include_context commenters_context
          include_context mentioned_context

          let(:comment) {
            inlined_jobs do
              commenter.comment!(status_msg, text_mentioning(mentioned_user))
            end
          }

          subject { mentioned_user }
          it { is_expected.to be_mentioned_in(comment) }
        end
      end

      context "when comment is received via federation" do
        context "when mentioned user is remote" do
          it "relays the comment to the mentioned user" do
            mentioned_person = FactoryBot.create(:person)
            expect_any_instance_of(Diaspora::Federation::Dispatcher::Public)
              .to receive(:deliver_to_remote).with([mentioned_person])

            receive_comment_via_federation(text_mentioning(mentioned_person), status_msg)
          end
        end
      end
    end

    context "with private post" do
      [
        ["commenter is author's friend", "mentioned user is author's friend"],
        ["commenter is author", "mentioned user is author's friend"],
        ["commenter is author's friend", "mentioned user is author"]
      ].each do |commenters_context, mentioned_context|
        context "when #{commenters_context} and #{mentioned_context}" do
          include_context commenters_context
          include_context mentioned_context

          let(:parent) { FactoryBot.create(:status_message_in_aspect, author: author.person) }
          let(:comment) {
            inlined_jobs do
              commenter.comment!(parent, text_mentioning(mentioned_user))
            end
          }

          before do
            mentioned_user.like!(parent)
          end

          subject { mentioned_user }
          it { is_expected.to be_mentioned_in(comment) }
        end
      end

      context "when comment is received via federation" do
        let(:parent) { FactoryBot.create(:status_message_in_aspect, author: user2.person) }

        before do
          user3.like!(parent)
          user1.like!(parent)
        end

        let(:comment_text) { text_mentioning(user2, user3, user1) }
        let(:comment) { receive_comment_via_federation(comment_text, parent) }

        it "mentions all the recepients" do
          [user1, user2, user3].each do |user|
            expect(user).to be_mentioned_in(comment)
          end
        end

        context "with only post author mentioned" do
          let(:post_author) { parent.author.owner }
          let(:comment_text) { text_mentioning(post_author) }

          it "makes only one notification for each recipient" do
            expect {
              comment
            }.to change { Notifications::MentionedInComment.for(post_author).count }.by(1)
              .and change { Notifications::AlsoCommented.for(user1).count }.by(1)
              .and change { Notifications::AlsoCommented.for(user3).count }.by(1)

            expect(mentioning_mail_notification(post_author, comment).count).to eq(1)

            [user1, user3].each do |user|
              expect(also_commented_mail_notification(user, parent).count).to eq(1)
            end
          end
        end
      end

      context "commenter can't mention a non-participant" do
        let(:status_msg) { FactoryBot.create(:status_message_in_aspect, author: author.person) }

        [
          ["commenter is author's friend", "mentioned user is not author's friend"],
          ["commenter is not author's friend", "mentioned user is author's friend"],
          ["commenter is not author's friend", "mentioned user is not author's friend"],
          ["commenter is author", "mentioned user is author's friend"],
          ["commenter is author", "mentioned user is not author's friend"]
        ].each do |commenters_context, mentioned_context|
          context "when #{commenters_context} and #{mentioned_context}" do
            include_context commenters_context
            include_context mentioned_context

            let(:comment) {
              inlined_jobs do
                commenter.comment!(status_msg, text_mentioning(mentioned_user))
              end
            }

            subject { mentioned_user }
            it { is_expected.not_to be_mentioned_in(comment) }
          end
        end
      end

      it "only creates one notification for the mentioned person, when mentioned person commented twice before" do
        parent = FactoryBot.create(:status_message_in_aspect, author: author.person)
        mentioned_user = FactoryBot.create(:user_with_aspect, friends: [author])
        mentioned_user.comment!(parent, "test comment 1")
        mentioned_user.comment!(parent, "test comment 2")
        comment = inlined_jobs do
          author.comment!(parent, text_mentioning(mentioned_user))
        end

        expect(notifications_about_mentioning(mentioned_user, comment).count).to eq(1)
      end
    end
  end
end
