# frozen_string_literal: true

RSpec.describe TopicHotScore do
  describe ".update_scores" do
    fab!(:user)
    fab!(:user2, :user)
    fab!(:user3, :user)

    after { Discourse.cache.delete(described_class::CACHE_KEY) }

    it "also always updates based on recent activity" do
      freeze_time

      # this will come in with a score
      topic = Fabricate(:topic, created_at: 1.hour.ago, bumped_at: 2.minutes.ago)
      post = Fabricate(:post, topic: topic, created_at: 2.minutes.ago)
      PostActionCreator.like(user, post)

      TopicHotScore.update_scores

      # this will come in in the batch in score 0
      topic = Fabricate(:topic, created_at: 1.minute.ago, bumped_at: 1.minute.ago)
      post = Fabricate(:post, topic: topic, created_at: 1.minute.ago)
      PostActionCreator.like(user, post)

      # batch size is 1 so if we do not do something special we only update
      # the high score topic and skip new
      TopicHotScore.update_scores(1)

      expect(TopicHotScore.find_by(topic_id: topic.id).score).to be_within(0.001).of(0.861)
    end

    it "can correctly update like counts and post counts and account for activity" do
      freeze_time_safe

      TopicHotScore.create!(topic_id: -1, score: 0.0, recent_likes: 99, recent_posters: 0)

      old_post = Fabricate(:post, created_at: 10.months.ago)
      topic = old_post.topic

      new_reply = Fabricate(:post, user: user, topic: topic, created_at: 4.hours.ago)
      newer_reply = Fabricate(:post, user: user2, topic: topic, created_at: 1.hour.ago)
      Fabricate(:post, user: user2, topic: topic, created_at: 1.minute.ago)

      freeze_time(1.year.ago)
      PostActionCreator.like(user, old_post)
      freeze_time(1.year.from_now)

      PostActionCreator.like(user2, new_reply)
      PostActionCreator.like(user, newer_reply)

      # user 3 likes two posts, but we should only count 1
      # this avoids a single user from trivially inflating hot scores
      PostActionCreator.like(user3, new_reply)
      PostActionCreator.like(user3, newer_reply)

      TopicHotScore.update_scores

      hot_scoring = TopicHotScore.find_by(topic_id: topic.id)

      expect(hot_scoring.recent_posters).to eq(2)
      expect(hot_scoring.recent_likes).to eq(3)
      expect(hot_scoring.recent_first_bumped_at).to eq_time(new_reply.created_at)
      expect(hot_scoring.score).to be_within(0.001).of(1.771)

      expect(TopicHotScore.find_by(topic_id: -1).recent_likes).to eq(0)

      # make sure we exclude whispers, deleted posts, small posts, etc
      whisper =
        Fabricate(:post, topic: topic, created_at: 1.hour.ago, post_type: Post.types[:whisper])
      PostActionCreator.like(Fabricate(:admin), whisper)

      TopicHotScore.update_scores

      hot_scoring = TopicHotScore.find_by(topic_id: topic.id)

      expect(hot_scoring.recent_posters).to eq(2)
      expect(hot_scoring.recent_likes).to eq(3)
    end

    it "prefers recent_likes to topic like count for recent topics" do
      freeze_time

      topic = Fabricate(:topic, created_at: 1.hour.ago)
      post = Fabricate(:post, topic: topic, created_at: 1.minute.ago)
      PostActionCreator.like(user, post)

      TopicHotScore.update_scores
      score = TopicHotScore.find_by(topic_id: topic.id).score

      topic.update!(like_count: 100)

      TopicHotScore.update_scores

      expect(TopicHotScore.find_by(topic_id: topic.id).score).to be_within(0.001).of(score)
    end

    it "can correctly set scores for topics" do
      freeze_time

      topic1 = Fabricate(:topic, like_count: 3, created_at: 2.weeks.ago)
      topic2 = Fabricate(:topic, like_count: 10, created_at: 2.weeks.ago)

      TopicHotScore.update_scores

      expect(TopicHotScore.find_by(topic_id: topic1.id).score).to be_within(0.001).of(0.002)
      expect(TopicHotScore.find_by(topic_id: topic2.id).score).to be_within(0.001).of(0.009)

      freeze_time(6.weeks.from_now)

      TopicHotScore.update_scores

      expect(TopicHotScore.find_by(topic_id: topic1.id).score).to be_within(0.0001).of(0.0005)
      expect(TopicHotScore.find_by(topic_id: topic2.id).score).to be_within(0.001).of(0.001)
    end

    it "caches 10% of the hottest topic IDs" do
      freeze_time

      9.times do
        Fabricate(:topic, like_count: 3, created_at: 2.weeks.ago, last_posted_at: 10.minutes.ago)
      end
      hottest_topic =
        Fabricate(:topic, like_count: 10, created_at: 2.weeks.ago, last_posted_at: 10.minutes.ago)

      TopicHotScore.update_scores

      expect(TopicHotScore.hottest_topic_ids).to contain_exactly(hottest_topic.id)

      hottest_topic_2 =
        Fabricate(:topic, like_count: 100, created_at: 2.weeks.ago, last_posted_at: 10.minutes.ago)

      TopicHotScore.update_scores

      expect(TopicHotScore.hottest_topic_ids).to contain_exactly(hottest_topic_2.id)
    end

    it "ignores topics in the future" do
      freeze_time

      topic1 = Fabricate(:topic, like_count: 3, created_at: 2.days.from_now)
      post1 = Fabricate(:post, topic: topic1, created_at: 1.minute.ago)
      PostActionCreator.like(user, post1)
      TopicHotScore.create!(topic_id: topic1.id, score: 0.0, recent_likes: 0, recent_posters: 0)

      expect { TopicHotScore.update_scores }.not_to change {
        TopicHotScore.where(topic_id: topic1.id).pluck(:recent_likes)
      }
    end

    it "triggers an event after updating" do
      triggered = false
      blk = Proc.new { triggered = true }

      begin
        DiscourseEvent.on(:topic_hot_scores_updated, &blk)

        TopicHotScore.update_scores

        expect(triggered).to eq(true)
      ensure
        DiscourseEvent.off(:topic_hot_scores_updated, &blk)
      end
    end

    it "excludes category description topics from hot topics list" do
      freeze_time

      category = Fabricate(:category_with_definition)
      category_topic = category.topic
      category_topic.update!(
        like_count: 100,
        created_at: 2.weeks.ago,
        last_posted_at: 10.minutes.ago,
      )

      9.times do
        Fabricate(:topic, like_count: 3, created_at: 2.weeks.ago, last_posted_at: 10.minutes.ago)
      end

      regular_topic =
        Fabricate(:topic, like_count: 10, created_at: 2.weeks.ago, last_posted_at: 10.minutes.ago)

      TopicHotScore.update_scores

      category_score = TopicHotScore.find_by(topic_id: category_topic.id)
      regular_score = TopicHotScore.find_by(topic_id: regular_topic.id)

      expect(category_score).to be_present
      expect(category_score.score).to be > regular_score.score

      hottest_ids = TopicHotScore.hottest_topic_ids

      expect(hottest_ids).not_to include(category_topic.id)
    end

    it "applies faster decay to closed topics" do
      freeze_time

      open_topic = Fabricate(:topic, like_count: 10, created_at: 2.weeks.ago)
      closed_topic = Fabricate(:topic, like_count: 10, created_at: 2.weeks.ago, closed: true)

      TopicHotScore.update_scores

      open_score = TopicHotScore.find_by(topic_id: open_topic.id).score
      closed_score = TopicHotScore.find_by(topic_id: closed_topic.id).score

      expect(closed_score).to be < open_score
      expect(closed_score).to be_within(0.0001).of(open_score * 0.5)
    end

    it "does not create hot scores for unlisted topics" do
      freeze_time

      unlisted_topic = Fabricate(:topic, visible: false, like_count: 10, created_at: 1.day.ago)

      TopicHotScore.update_scores

      expect(TopicHotScore.find_by(topic_id: unlisted_topic.id)).to be_nil
    end

    it "excludes unlisted topics from hottest_topic_ids cache" do
      freeze_time

      unlisted_topic =
        Fabricate(
          :topic,
          visible: false,
          like_count: 100,
          created_at: 1.day.ago,
          last_posted_at: 10.minutes.ago,
        )
      TopicHotScore.create!(topic_id: unlisted_topic.id, score: 999.0)

      TopicHotScore.recreate_hottest_topic_ids_cache

      expect(TopicHotScore.hottest_topic_ids).not_to include(unlisted_topic.id)
    end

    it "sets score to 0 for unlisted topics during update" do
      freeze_time

      topic = Fabricate(:topic, like_count: 10, created_at: 2.weeks.ago)
      TopicHotScore.update_scores

      expect(TopicHotScore.find_by(topic_id: topic.id).score).to be > 0

      topic.update!(visible: false)
      TopicHotScore.update_scores

      expect(TopicHotScore.find_by(topic_id: topic.id).score).to eq(0)
    end
  end
end
