require "rails_helper"

RSpec.describe ReactionHandler, type: :service do
  # existing reaction by other user
  # existing reaction by same user, other category
  # no existing reaction = create
  # existing reaction = no-op
  # existing contradictory mod reaction

  let(:user) { create(:user) }
  let(:article) { create(:article) }
  let(:category) { "like" }

  let!(:other_category) { article.reactions.create! user: user, category: "hands" }
  let!(:other_existing) { article.reactions.create! user: create(:user), category: "like" }

  let(:moderator) { create(:user, :trusted) }
  let!(:contradictory_mod) { article.reactions.create! user: moderator, category: "thumbsup" }

  let(:params) do
    {
      reactable_id: article.id,
      reactable_type: article.class.polymorphic_name,
      category: category
    }
  end

  let(:reactable_data) do
    {
      reactable_id: article.id,
      reactable_type: "Article",
      reactable_user_id: article.user.id,
      reactable_subforem_id: article.subforem_id
    }
  end

  describe "#create" do
    subject(:result) { described_class.new(params, current_user: user).create }

    context "when no existing/matching reaction by user" do
      it "justs create" do
        expect(result).to be_success
        expect(result.action).to eq("create")
      end

      it "ignores other existing reactions" do
        expect(result).to be_success
        expect(Reaction.ids).to include(other_category.id, other_existing.id, contradictory_mod.id)
      end

      it "sends a notification to the author" do
        receiver = { klass: "User", id: article.user.id }
        sidekiq_assert_enqueued_with(job: Notifications::NewReactionWorker, args: [reactable_data, receiver]) do
          expect(result).to be_success
        end
      end

      it "records a feed event for articles reached through a feed" do
        create(:feed_event, category: :click, article: article, user: user)

        expect { result }.to change(FeedEvent, :count).by(1)
        expect(user.feed_events.last).to have_attributes(
          category: "reaction",
          article_id: article.id,
          user_id: user.id,
        )
      end

      it "does not record a feed event for articles that were not reached through a feed" do
        # activity by a different user!
        create(:feed_event, category: :click, article: article, user: moderator)

        expect { result }.not_to change(FeedEvent, :count)
        expect(user.feed_events).to be_empty
      end
    end

    context "when the article is written for an organization" do
      let(:org_author) { create(:user, :org_member) }
      let(:organization) { org_author.organizations.first }
      let(:article) { create(:article, organization: organization, user: org_author) }

      it "sends a notification to both the author and the organization" do
        author = { klass: "User", id: article.user.id }
        org = { klass: "Organization", id: organization.id }

        expect(result).to be_success
        sidekiq_assert_enqueued_with(job: Notifications::NewReactionWorker, args: [reactable_data, author])
        sidekiq_assert_enqueued_with(job: Notifications::NewReactionWorker, args: [reactable_data, org])
      end
    end

    context "when the reaction is not a public reaction or bookmark" do
      let(:user) { moderator }
      let(:category) { "vomit" }

      it "does not record a feed event" do
        click = create(:feed_event, category: :click, article: article, user: moderator)

        expect { result }.not_to change(FeedEvent, :count)
        expect(moderator.feed_events).to contain_exactly(click)
      end
    end

    context "when there's an existing/matching reaction by user" do
      let!(:existing) { article.reactions.create! user: user, category: "like" }

      it "does nothing" do
        expect(result).to be_success
        expect(result.action).to eq("none")
      end

      it "ignores other existing reactions" do
        expect(result).to be_success
        expect(Reaction.ids).to include(other_category.id, other_existing.id, contradictory_mod.id, existing.id)
      end

      it "does not send a notification to the author" do
        sidekiq_assert_no_enqueued_jobs(only: Notifications::NewReactionWorker) do
          expect(result).to be_success
        end
      end
    end

    context "when the reaction is a bookmark" do
      let(:category) { "readinglist" }

      it "does not send a notification to the author" do
        sidekiq_assert_no_enqueued_jobs(only: Notifications::NewReactionWorker) do
          expect(result).to be_success
          expect(result.action).to eq("create")
        end
      end

      it "records a feed event if reached through a feed" do
        create(:feed_event, category: :click, article: article, user: user)

        expect { result }.to change(FeedEvent, :count).by(1)
        expect(user.feed_events.last).to have_attributes(
          category: "reaction",
          article_id: article.id,
          user_id: user.id,
        )
      end
    end

    context "when there's an existing, contradictory mod reaction" do
      let(:user) { moderator }
      let(:category) { "vomit" }

      it "creates" do
        expect(result).to be_success
        expect(result.action).to eq("create")
      end

      it "destroys the other reaction as a side-effect" do
        expect(result).to be_success
        expect(Reaction.ids).not_to include(contradictory_mod.id)
      end
    end

    it "updates the last_reacted_at field" do
      Timecop.freeze(Time.current) do
        reaction_handler = described_class.new(params, current_user: user).create
        expect(reaction_handler.reaction.user.last_reacted_at).to eq Time.current
      end
    end

    context "when the reactable is a comment" do
      let(:user) { moderator }
      let(:comment) { create(:comment) }
      let!(:contradictory_mod) { comment.reactions.create! user: moderator, category: "thumbsup" }
      let(:params) do
        {
          reactable_id: comment.id,
          reactable_type: comment.class.polymorphic_name,
          category: "vomit"
        }
      end

      it "also destroys contradictory mod reactions" do
        expect(result).to be_success
        expect(result.action).to eq("create")
        expect(Reaction.ids).not_to include(contradictory_mod.id)
      end

      it "does not record a feed event" do
        click = create(:feed_event, category: :click, article: comment.commentable, user: moderator)

        expect { result }.not_to change(FeedEvent, :count)
        expect(moderator.feed_events).to contain_exactly(click)
      end
    end
  end

  describe "#toggle" do
    subject(:result) { described_class.new(params, current_user: user).toggle }

    context "when no existing/matching reaction by user" do
      it "justs create" do
        expect(result).to be_success
        expect(result.action).to eq("create")
      end

      it "ignores other existing reactions" do
        expect(result).to be_success
        expect(Reaction.ids).to include(other_category.id, other_existing.id, contradictory_mod.id)
      end

      it "sends a notification to the author" do
        receiver = { klass: "User", id: article.user.id }
        sidekiq_assert_enqueued_with(job: Notifications::NewReactionWorker, args: [reactable_data, receiver]) do
          expect(result).to be_success
        end
      end

      it "records a feed event for articles reached through a feed" do
        create(:feed_event, category: :click, article: article, user: user)

        expect { result }.to change(FeedEvent, :count).by(1)
        expect(user.feed_events.last).to have_attributes(
          category: "reaction",
          article_id: article.id,
          user_id: user.id,
        )
      end

      it "does not record a feed event for articles that were not reached through a feed" do
        # activity by a different user!
        create(:feed_event, category: :click, article: article, user: moderator)

        expect { result }.not_to change(FeedEvent, :count)
        expect(user.feed_events).to be_empty
      end
    end

    context "when the reaction is not in a notifiable category" do
      let(:category) { "readinglist" }

      it "does not send a notification to the author" do
        sidekiq_assert_no_enqueued_jobs(only: Notifications::NewReactionWorker) do
          expect(result).to be_success
          expect(result.action).to eq("create")
        end
      end
    end

    context "when there's an existing/matching reaction by user" do
      let!(:existing) { article.reactions.create! user: user, category: "like" }

      it "un-likes" do
        expect(result).to be_success
        expect(result.action).to eq("destroy")
        expect(Reaction.ids).not_to include(existing.id)
      end

      it "ignores other existing reactions" do
        expect(result).to be_success
        expect(Reaction.ids).to include(other_category.id, other_existing.id, contradictory_mod.id)
      end

      it "immediately sends a notification to the author" do
        allow(Notifications::Reactions::Send).to receive(:call)

        expect(result).to be_success

        expect(Notifications::Reactions::Send).to have_received(:call).with(reactable_data, article.user)
      end
    end

    context "when the article is written for an organization" do
      let(:org_author) { create(:user, :org_member) }
      let(:organization) { org_author.organizations.first }
      let(:article) { create(:article, organization: organization, user: org_author) }

      it "sends a notification to both the author and the organization" do
        author = { klass: "User", id: article.user.id }
        org = { klass: "Organization", id: organization.id }

        expect(result).to be_success
        sidekiq_assert_enqueued_with(job: Notifications::NewReactionWorker, args: [reactable_data, author])
        sidekiq_assert_enqueued_with(job: Notifications::NewReactionWorker, args: [reactable_data, org])
      end

      it "sends the notifications immediately if there was an existing reaction" do
        allow(Notifications::Reactions::Send).to receive(:call)

        article.reactions.create! user: user, category: "like"
        expect(result).to be_success

        expect(Notifications::Reactions::Send).to have_received(:call).with(reactable_data, org_author)
        expect(Notifications::Reactions::Send).to have_received(:call).with(reactable_data, organization)
      end
    end

    context "when there's an existing, contradictory mod reaction" do
      let(:user) { moderator }
      let(:category) { "vomit" }

      it "creates" do
        expect(result).to be_success
        expect(result.action).to eq("create")
      end

      it "destroys the other reaction as a side-effect" do
        expect(result).to be_success
        expect(Reaction.ids).not_to include(contradictory_mod.id)
      end
    end

    context "when the reactable is a comment" do
      let(:user) { moderator }
      let(:comment) { create(:comment) }
      let!(:contradictory_mod) { comment.reactions.create! user: moderator, category: "thumbsup" }
      let(:params) do
        {
          reactable_id: comment.id,
          reactable_type: comment.class.polymorphic_name,
          category: "vomit"
        }
      end

      it "also destroys contradictory mod reactions" do
        expect(result).to be_success
        expect(result.action).to eq("create")
        expect(Reaction.ids).not_to include(contradictory_mod.id)
      end
    end
  end
end
