require 'rails_helper'
require_dependency 'post_destroyer'

describe PostAction do
  it { is_expected.to rate_limit }

  let(:moderator) { Fabricate(:moderator) }
  let(:codinghorror) { Fabricate(:coding_horror) }
  let(:eviltrout) { Fabricate(:evil_trout) }
  let(:admin) { Fabricate(:admin) }
  let(:post) { Fabricate(:post) }
  let(:second_post) { Fabricate(:post, topic_id: post.topic_id) }
  let(:bookmark) { PostAction.new(user_id: post.user_id, post_action_type_id: PostActionType.types[:bookmark] , post_id: post.id) }

  def value_for(user_id, dt)
    GivenDailyLike.find_for(user_id, dt).pluck(:likes_given)[0] || 0
  end

  describe "rate limits" do

    it "limits redo/undo" do

      RateLimiter.stubs(:disabled?).returns(false)

      PostAction.act(eviltrout, post, PostActionType.types[:like])
      PostAction.remove_act(eviltrout, post, PostActionType.types[:like])
      PostAction.act(eviltrout, post, PostActionType.types[:like])
      PostAction.remove_act(eviltrout, post, PostActionType.types[:like])

      expect {
        PostAction.act(eviltrout, post, PostActionType.types[:like])
      }.to raise_error(RateLimiter::LimitExceeded)

    end
  end

  describe "messaging" do

    it "doesn't generate title longer than 255 characters" do
      topic = create_topic(title: "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nunc sit amet rutrum neque. Pellentesque suscipit vehicula facilisis. Phasellus lacus sapien, aliquam nec convallis sit amet, vestibulum laoreet ante. Curabitur et pellentesque tortor. Donec non.")
      post = create_post(topic: topic)
      expect { PostAction.act(admin, post, PostActionType.types[:notify_user], message: "WAT") }.not_to raise_error
    end

    it "notify moderators integration test" do
      post = create_post
      mod = moderator
      Group.refresh_automatic_groups!

      action = PostAction.act(codinghorror, post, PostActionType.types[:notify_moderators], message: "this is my special long message")

      posts = Post.joins(:topic)
                  .select('posts.id, topics.subtype, posts.topic_id')
                  .where('topics.archetype' => Archetype.private_message)
                  .to_a

      expect(posts.count).to eq(1)
      expect(action.related_post_id).to eq(posts[0].id.to_i)
      expect(posts[0].subtype).to eq(TopicSubtype.notify_moderators)

      topic = posts[0].topic

      # Moderators should be invited to the private topic, otherwise they're not permitted to see it
      topic_user_ids = topic.topic_users(true).map {|x| x.user_id}
      expect(topic_user_ids).to include(codinghorror.id)
      expect(topic_user_ids).to include(mod.id)

      expect(topic.topic_users.where(user_id: mod.id)
              .pluck(:notification_level).first).to eq(TopicUser.notification_levels[:tracking])

      expect(topic.topic_users.where(user_id: codinghorror.id)
              .pluck(:notification_level).first).to eq(TopicUser.notification_levels[:watching])

      # reply to PM should not clear flag
      PostCreator.new(mod, topic_id: posts[0].topic_id, raw: "This is my test reply to the user, it should clear flags").create
      action.reload
      expect(action.deleted_at).to eq(nil)

      # Acting on the flag should not post an automated status message (since a moderator already replied)
      expect(topic.posts.count).to eq(2)
      PostAction.agree_flags!(post, admin)
      topic.reload
      expect(topic.posts.count).to eq(2)

      # Clearing the flags should not post an automated status message
      PostAction.act(mod, post, PostActionType.types[:notify_moderators], message: "another special message")
      PostAction.clear_flags!(post, admin)
      topic.reload
      expect(topic.posts.count).to eq(2)

      # Acting on the flag should post an automated status message
      another_post = create_post
      action = PostAction.act(codinghorror, another_post, PostActionType.types[:notify_moderators], message: "foobar")
      topic = action.related_post.topic

      expect(topic.posts.count).to eq(1)
      PostAction.agree_flags!(another_post, admin)
      topic.reload
      expect(topic.posts.count).to eq(2)
      expect(topic.posts.last.post_type).to eq(Post.types[:moderator_action])
    end

    describe 'notify_moderators' do
      before do
        PostAction.stubs(:create)
      end

      it "creates a pm if selected" do
        post = build(:post, id: 1000)
        PostCreator.any_instance.expects(:create).returns(post)
        PostAction.act(build(:user), build(:post), PostActionType.types[:notify_moderators], message: "this is my special message")
      end
    end

    describe "notify_user" do
      before do
        PostAction.stubs(:create)
        post = build(:post)
        post.user = build(:user)
      end

      it "sends an email to user if selected" do
        PostCreator.any_instance.expects(:create).returns(build(:post))
        PostAction.act(build(:user), post, PostActionType.types[:notify_user], message: "this is my special message")
      end
    end
  end

  describe "flag counts" do
    before do
      PostAction.update_flagged_posts_count
    end

    it "increments the numbers correctly" do
      expect(PostAction.flagged_posts_count).to eq(0)

      PostAction.act(codinghorror, post, PostActionType.types[:off_topic])
      expect(PostAction.flagged_posts_count).to eq(1)

      PostAction.clear_flags!(post, Discourse.system_user)
      expect(PostAction.flagged_posts_count).to eq(0)
    end

    it "should reset counts when a topic is deleted" do
      PostAction.act(codinghorror, post, PostActionType.types[:off_topic])
      post.topic.trash!
      expect(PostAction.flagged_posts_count).to eq(0)
    end

    it "should ignore flags on non-human users" do
      post = create_post(user: Discourse.system_user)
      PostAction.act(codinghorror, post, PostActionType.types[:off_topic])
      expect(PostAction.flagged_posts_count).to eq(0)
    end

    it "should ignore validated flags" do
      post = create_post

      PostAction.act(codinghorror, post, PostActionType.types[:off_topic])
      expect(post.hidden).to eq(false)
      expect(post.hidden_at).to be_blank
      PostAction.defer_flags!(post, admin)
      expect(PostAction.flagged_posts_count).to eq(0)

      post.reload
      expect(post.hidden).to eq(false)
      expect(post.hidden_at).to be_blank

      PostAction.hide_post!(post, PostActionType.types[:off_topic])

      post.reload
      expect(post.hidden).to eq(true)
      expect(post.hidden_at).to be_present
    end

  end

  describe "update_counters" do

    it "properly updates topic counters" do
      Timecop.freeze(Date.today) do
        # we need this to test it
        TopicUser.change(codinghorror, post.topic, posted: true)

        expect(value_for(moderator.id, Date.today)).to eq(0)

        PostAction.act(moderator, post, PostActionType.types[:like])
        PostAction.act(codinghorror, second_post, PostActionType.types[:like])

        post.topic.reload
        expect(post.topic.like_count).to eq(2)

        expect(value_for(moderator.id, Date.today)).to eq(1)

        tu = TopicUser.get(post.topic, codinghorror)
        expect(tu.liked).to be true
        expect(tu.bookmarked).to be false
      end
    end

  end

  describe "when a user bookmarks something" do
    it "increases the post's bookmark count when saved" do
      expect { bookmark.save; post.reload }.to change(post, :bookmark_count).by(1)
    end

    describe 'when deleted' do

      before do
        bookmark.save
        post.reload
        @topic = post.topic
        @topic.reload
        bookmark.deleted_at = DateTime.now
        bookmark.save
      end

      it 'reduces the bookmark count of the post' do
        expect { post.reload }.to change(post, :bookmark_count).by(-1)
      end

    end
  end

  describe 'when a user likes something' do

    it 'should generate notifications correctly' do

      PostActionNotifier.enable

      PostAction.act(codinghorror, post, PostActionType.types[:like])
      expect(Notification.count).to eq(1)

      mutee = Fabricate(:user)

      post = Fabricate(:post)
      MutedUser.create!(user_id: post.user.id, muted_user_id: mutee.id)
      PostAction.act(mutee, post, PostActionType.types[:like])

      expect(Notification.count).to eq(1)

      # you can not mute admin, sorry
      MutedUser.create!(user_id: post.user.id, muted_user_id: admin.id)
      PostAction.act(admin, post, PostActionType.types[:like])

      expect(Notification.count).to eq(2)

    end

    it 'should increase the `like_count` and `like_score` when a user likes something' do
      Timecop.freeze(Date.today) do
        PostAction.act(codinghorror, post, PostActionType.types[:like])
        post.reload
        expect(post.like_count).to eq(1)
        expect(post.like_score).to eq(1)
        post.topic.reload
        expect(post.topic.like_count).to eq(1)
        expect(value_for(codinghorror.id, Date.today)).to eq(1)

        # When a staff member likes it
        PostAction.act(moderator, post, PostActionType.types[:like])
        post.reload
        expect(post.like_count).to eq(2)
        expect(post.like_score).to eq(4)

        # Removing likes
        PostAction.remove_act(codinghorror, post, PostActionType.types[:like])
        post.reload
        expect(post.like_count).to eq(1)
        expect(post.like_score).to eq(3)
        expect(value_for(codinghorror.id, Date.today)).to eq(0)

        PostAction.remove_act(moderator, post, PostActionType.types[:like])
        post.reload
        expect(post.like_count).to eq(0)
        expect(post.like_score).to eq(0)
      end
    end
  end

  describe "undo/redo repeatedly" do
    it "doesn't create a second action for the same user/type" do
      PostAction.act(codinghorror, post, PostActionType.types[:like])
      PostAction.remove_act(codinghorror, post, PostActionType.types[:like])
      PostAction.act(codinghorror, post, PostActionType.types[:like])
      expect(PostAction.where(post: post).with_deleted.count).to eq(1)
      PostAction.remove_act(codinghorror, post, PostActionType.types[:like])

      # Check that we don't lose consistency into negatives
      expect(post.reload.like_count).to eq(0)
    end
  end

  describe 'when a user likes something' do
    it 'should increase the like counts when a user votes' do
      expect {
        PostAction.act(codinghorror, post, PostActionType.types[:like])
        post.reload
      }.to change(post, :like_count).by(1)
    end

    it 'should increase the forum topic vote count when a user votes' do
      expect {
        PostAction.act(codinghorror, post, PostActionType.types[:like])
        post.topic.reload
      }.to change(post.topic, :like_count).by(1)

      expect {
        PostAction.remove_act(codinghorror, post, PostActionType.types[:like])
        post.topic.reload
      }.to change(post.topic, :like_count).by(-1)
    end
  end

  describe 'flagging' do

    context "flag_counts_for" do
      it "returns the correct flag counts" do
        post = create_post

        SiteSetting.stubs(:flags_required_to_hide_post).returns(7)

        # A post with no flags has 0 for flag counts
        expect(PostAction.flag_counts_for(post.id)).to eq([0, 0])

        _flag = PostAction.act(eviltrout, post, PostActionType.types[:spam])
        expect(PostAction.flag_counts_for(post.id)).to eq([0, 1])

        # If staff takes action, it is ranked higher
        PostAction.act(admin, post, PostActionType.types[:spam], take_action: true)
        expect(PostAction.flag_counts_for(post.id)).to eq([0, 8])

        # If a flag is dismissed
        PostAction.clear_flags!(post, admin)
        expect(PostAction.flag_counts_for(post.id)).to eq([8, 0])
      end
    end

    it 'does not allow you to flag stuff with the same reason more than once' do
      post = Fabricate(:post)
      PostAction.act(eviltrout, post, PostActionType.types[:spam])
      expect { PostAction.act(eviltrout, post, PostActionType.types[:off_topic]) }.to raise_error(PostAction::AlreadyActed)
    end

    it 'allows you to flag stuff with another reason' do
      post = Fabricate(:post)
      PostAction.act(eviltrout, post, PostActionType.types[:spam])
      PostAction.remove_act(eviltrout, post, PostActionType.types[:spam])
      expect { PostAction.act(eviltrout, post, PostActionType.types[:off_topic]) }.not_to raise_error()
    end

    it 'should update counts when you clear flags' do
      post = Fabricate(:post)
      PostAction.act(eviltrout, post, PostActionType.types[:spam])

      post.reload
      expect(post.spam_count).to eq(1)

      PostAction.clear_flags!(post, Discourse.system_user)

      post.reload
      expect(post.spam_count).to eq(0)
    end

    it 'should follow the rules for automatic hiding workflow' do
      post = create_post
      walterwhite = Fabricate(:walter_white)

      SiteSetting.stubs(:flags_required_to_hide_post).returns(2)
      Discourse.stubs(:site_contact_user).returns(admin)

      PostAction.act(eviltrout, post, PostActionType.types[:spam])
      PostAction.act(walterwhite, post, PostActionType.types[:spam])

      post.reload

      expect(post.hidden).to eq(true)
      expect(post.hidden_at).to be_present
      expect(post.hidden_reason_id).to eq(Post.hidden_reasons[:flag_threshold_reached])
      expect(post.topic.visible).to eq(false)

      post.revise(post.user, { raw: post.raw + " ha I edited it " })

      post.reload

      expect(post.hidden).to eq(false)
      expect(post.hidden_reason_id).to eq(Post.hidden_reasons[:flag_threshold_reached]) # keep most recent reason
      expect(post.hidden_at).to be_present # keep the most recent hidden_at time
      expect(post.topic.visible).to eq(true)

      PostAction.act(eviltrout, post, PostActionType.types[:spam])
      PostAction.act(walterwhite, post, PostActionType.types[:off_topic])

      post.reload

      expect(post.hidden).to eq(true)
      expect(post.hidden_at).to be_present
      expect(post.hidden_reason_id).to eq(Post.hidden_reasons[:flag_threshold_reached_again])
      expect(post.topic.visible).to eq(false)

      post.revise(post.user, { raw: post.raw + " ha I edited it again " })

      post.reload

      expect(post.hidden).to eq(true)
      expect(post.hidden_at).to be_present
      expect(post.hidden_reason_id).to eq(Post.hidden_reasons[:flag_threshold_reached_again])
      expect(post.topic.visible).to eq(false)
    end

    it "hide tl0 posts that are flagged as spam by a tl3 user" do
      newuser = Fabricate(:newuser)
      post = create_post(user: newuser)

      Discourse.stubs(:site_contact_user).returns(admin)

      PostAction.act(Fabricate(:leader), post, PostActionType.types[:spam])

      post.reload

      expect(post.hidden).to eq(true)
      expect(post.hidden_at).to be_present
      expect(post.hidden_reason_id).to eq(Post.hidden_reasons[:flagged_by_tl3_user])
    end

    it "can flag the topic instead of a post" do
      post1 = create_post
      _post2 = create_post(topic: post1.topic)
      post_action = PostAction.act(Fabricate(:user), post1, PostActionType.types[:spam], { flag_topic: true })
      expect(post_action.targets_topic).to eq(true)
    end

    it "will flag the first post if you flag a topic but there is only one post in the topic" do
      post = create_post
      post_action = PostAction.act(Fabricate(:user), post, PostActionType.types[:spam], { flag_topic: true })
      expect(post_action.targets_topic).to eq(false)
      expect(post_action.post_id).to eq(post.id)
    end

    it "will unhide the post when a moderator undos the flag on which s/he took action" do
      Discourse.stubs(:site_contact_user).returns(admin)

      post = create_post
      PostAction.act(moderator, post, PostActionType.types[:spam], { take_action: true })

      post.reload
      expect(post.hidden).to eq(true)

      PostAction.remove_act(moderator, post, PostActionType.types[:spam])

      post.reload
      expect(post.hidden).to eq(false)
    end

    it "will automatically pause a topic due to large community flagging" do
      SiteSetting.flags_required_to_hide_post = 0
      SiteSetting.num_flags_to_close_topic = 3
      SiteSetting.num_flaggers_to_close_topic = 2
      SiteSetting.num_hours_to_close_topic = 1

      topic = Fabricate(:topic)
      post1 = create_post(topic: topic)
      post2 = create_post(topic: topic)
      post3 = create_post(topic: topic)

      flagger1 = Fabricate(:user)
      flagger2 = Fabricate(:user)

      # reaching `num_flaggers_to_close_topic` isn't enough
      [flagger1, flagger2].each do |flagger|
        PostAction.act(flagger, post1, PostActionType.types[:inappropriate])
      end

      expect(topic.reload.closed).to eq(false)

      # clean up
      PostAction.where(post: post1).delete_all

      # reaching `num_flags_to_close_topic` isn't enough
      [post1, post2, post3].each do |post|
        PostAction.act(flagger1, post, PostActionType.types[:inappropriate])
      end

      expect(topic.reload.closed).to eq(false)

      # clean up
      PostAction.where(post: [post1, post2, post3]).delete_all

      # reaching both should close the topic
      [flagger1, flagger2].each do |flagger|
        [post1, post2, post3].each do |post|
          PostAction.act(flagger, post, PostActionType.types[:inappropriate])
        end
      end

      expect(topic.reload.closed).to eq(true)

      topic_status_update = TopicTimer.last

      expect(topic_status_update.topic).to eq(topic)
      expect(topic_status_update.execute_at).to be_within(1.second).of(1.hour.from_now)
      expect(topic_status_update.status_type).to eq(TopicTimer.types[:open])
    end

  end

  it "prevents user to act twice at the same time" do
    # flags are already being tested
    all_types_except_flags = PostActionType.types.except(PostActionType.flag_types)
    all_types_except_flags.values.each do |action|
      expect do
        PostAction.act(eviltrout, post, action)
        PostAction.act(eviltrout, post, action)
      end.to raise_error(PostAction::AlreadyActed)
    end
  end

  describe "#create_message_for_post_action" do
    it "does not create a message when there is no message" do
      message_id = PostAction.create_message_for_post_action(Discourse.system_user, post, PostActionType.types[:spam], {})
      expect(message_id).to be_nil
    end

    [:notify_moderators, :notify_user, :spam].each do |post_action_type|
      it "creates a message for #{post_action_type}" do
        message_id = PostAction.create_message_for_post_action(Discourse.system_user, post, PostActionType.types[post_action_type], message: "WAT")
        expect(message_id).to be_present
      end
    end

  end

  describe ".lookup_for" do
    it "returns the correct map" do
      user = Fabricate(:user)
      post = Fabricate(:post)
      post_action = PostAction.create(user_id: user.id, post_id: post.id, post_action_type_id: 1)

      map = PostAction.lookup_for(user, [post.topic], post_action.post_action_type_id)

      expect(map).to eq({post.topic_id => [post.post_number]})
    end
  end

  describe ".add_moderator_post_if_needed" do

    it "should not add a moderator post when it's disabled" do
      post = create_post

      action = PostAction.act(moderator, post, PostActionType.types[:spam], message: "WAT")
      action.reload
      topic = action.related_post.topic
      expect(topic.posts.count).to eq(1)

      SiteSetting.expects(:auto_respond_to_flag_actions).returns(false)
      PostAction.agree_flags!(post, admin)

      topic.reload
      expect(topic.posts.count).to eq(1)
    end

    it "should create a notification in the related topic" do
      post = Fabricate(:post)
      user = Fabricate(:user)
      action = PostAction.act(user, post, PostActionType.types[:spam], message: "WAT")
      topic = action.reload.related_post.topic
      expect(user.notifications.count).to eq(0)

      SiteSetting.expects(:auto_respond_to_flag_actions).returns(true)
      PostAction.agree_flags!(post, admin)

      user_notifications = user.notifications
      expect(user_notifications.count).to eq(1)
      expect(user_notifications.last.topic).to eq(topic)
    end

  end

  describe "rate limiting" do

    def limiter(tl)
      user = Fabricate.build(:user)
      user.trust_level = tl
      action = PostAction.new(user: user, post_action_type_id: PostActionType.types[:like])
      action.post_action_rate_limiter
    end

    it "should scale up like limits depending on liker" do
      expect(limiter(0).max).to eq SiteSetting.max_likes_per_day
      expect(limiter(1).max).to eq SiteSetting.max_likes_per_day
      expect(limiter(2).max).to eq (SiteSetting.max_likes_per_day * SiteSetting.tl2_additional_likes_per_day_multiplier).to_i
      expect(limiter(3).max).to eq (SiteSetting.max_likes_per_day * SiteSetting.tl3_additional_likes_per_day_multiplier).to_i
      expect(limiter(4).max).to eq (SiteSetting.max_likes_per_day * SiteSetting.tl4_additional_likes_per_day_multiplier).to_i

      SiteSetting.tl2_additional_likes_per_day_multiplier = -1
      expect(limiter(2).max).to eq SiteSetting.max_likes_per_day

      SiteSetting.tl2_additional_likes_per_day_multiplier = 0.8
      expect(limiter(2).max).to eq SiteSetting.max_likes_per_day

      SiteSetting.tl2_additional_likes_per_day_multiplier = "bob"
      expect(limiter(2).max).to eq SiteSetting.max_likes_per_day
    end

  end

end