# coding: utf-8
# frozen_string_literal: true

require 'rails_helper'

RSpec.describe TopicsController do
  fab!(:topic) { Fabricate(:topic) }

  fab!(:user) { Fabricate(:user) }
  fab!(:moderator) { Fabricate(:moderator) }
  fab!(:admin) { Fabricate(:admin) }
  fab!(:trust_level_4) { Fabricate(:trust_level_4) }

  fab!(:category) { Fabricate(:category) }
  fab!(:staff_category) {
    Fabricate(:category).tap { |staff_category|
      staff_category.set_permissions(staff: :full)
      staff_category.save!
    }
  }

  fab!(:group_user) { Fabricate(:group_user) }

  describe '#wordpress' do
    let!(:user) { sign_in(moderator) }
    fab!(:p1) { Fabricate(:post, user: moderator) }
    fab!(:topic) { p1.topic }
    fab!(:p2) { Fabricate(:post, topic: topic, user: moderator) }

    it "returns the JSON in the format our wordpress plugin needs" do
      SiteSetting.external_system_avatars_enabled = false

      get "/t/#{topic.id}/wordpress.json", params: { best: 3 }

      expect(response.status).to eq(200)
      json = response.parsed_body

      # The JSON has the data the wordpress plugin needs
      expect(json['id']).to eq(topic.id)
      expect(json['posts_count']).to eq(2)
      expect(json['filtered_posts_count']).to eq(2)

      # Posts
      expect(json['posts'].size).to eq(1)
      post = json['posts'][0]
      expect(post['id']).to eq(p2.id)
      expect(post['username']).to eq(user.username)
      expect(post['avatar_template']).to eq("#{Discourse.base_url_no_prefix}#{user.avatar_template}")
      expect(post['name']).to eq(user.name)
      expect(post['created_at']).to be_present
      expect(post['cooked']).to eq(p2.cooked)

      # Participants
      expect(json['participants'].size).to eq(1)
      participant = json['participants'][0]
      expect(participant['id']).to eq(user.id)
      expect(participant['username']).to eq(user.username)
      expect(participant['avatar_template']).to eq("#{Discourse.base_url_no_prefix}#{user.avatar_template}")
    end
  end

  describe '#move_posts' do
    before do
      SiteSetting.min_topic_title_length = 2
      SiteSetting.tagging_enabled = true
    end

    it 'needs you to be logged in' do
      post "/t/111/move-posts.json", params: {
        title: 'blah',
        post_ids: [1, 2, 3]
      }
      expect(response.status).to eq(403)
    end

    describe 'moving to a new topic' do
      fab!(:p1) { Fabricate(:post, user: user, post_number: 1) }
      let(:p2) { Fabricate(:post, user: user, post_number: 2, topic: p1.topic) }
      let(:topic) { p1.topic }

      it "raises an error without post_ids" do
        sign_in(moderator)
        post "/t/#{topic.id}/move-posts.json", params: { title: 'blah' }
        expect(response.status).to eq(400)
      end

      it "raises an error when the user doesn't have permission to move the posts" do
        sign_in(user)

        post "/t/#{topic.id}/move-posts.json", params: {
          title: 'blah', post_ids: [p1.post_number, p2.post_number]
        }

        expect(response).to be_forbidden
      end

      it "raises an error when the OP is not a regular post" do
        sign_in(moderator)
        p2 = Fabricate(:post, topic: topic, post_number: 2, post_type: Post.types[:whisper])
        p3 = Fabricate(:post, topic: topic, post_number: 3)

        post "/t/#{topic.id}/move-posts.json", params: {
          title: 'blah', post_ids: [p2.id, p3.id]
        }
        expect(response.status).to eq(422)

        result = response.parsed_body

        expect(result['errors']).to be_present
      end

      context 'success' do

        before { sign_in(admin) }

        it "returns success" do
          expect do
            post "/t/#{topic.id}/move-posts.json", params: {
              title: 'Logan is a good movie',
              post_ids: [p2.id],
              category_id: category.id,
              tags: ["tag1", "tag2"]
            }
          end.to change { Topic.count }.by(1)

          expect(response.status).to eq(200)

          result = response.parsed_body

          expect(result['success']).to eq(true)

          new_topic = Topic.last
          expect(result['url']).to eq(new_topic.relative_url)
          expect(new_topic.excerpt).to eq(p2.excerpt_for_topic)
          expect(Tag.all.pluck(:name)).to contain_exactly("tag1", "tag2")
        end

        describe 'when topic has been deleted' do
          it 'should still be able to move posts' do
            PostDestroyer.new(admin, topic.first_post).destroy

            expect(topic.reload.deleted_at).to_not be_nil

            expect do
              post "/t/#{topic.id}/move-posts.json", params: {
                title: 'Logan is a good movie',
                post_ids: [p2.id],
                category_id: category.id
              }
            end.to change { Topic.count }.by(1)

            expect(response.status).to eq(200)

            result = response.parsed_body

            expect(result['success']).to eq(true)
            expect(result['url']).to eq(Topic.last.relative_url)
          end
        end
      end

      context 'failure' do
        it "returns JSON with a false success" do
          sign_in(moderator)
          post "/t/#{topic.id}/move-posts.json", params: {
            post_ids: [p2.id]
          }
          expect(response.status).to eq(200)
          result = response.parsed_body
          expect(result['success']).to eq(false)
          expect(result['url']).to be_blank
        end
      end

      describe "moving replied posts" do
        context 'success' do
          it "moves the child posts too" do
            user = sign_in(moderator)
            p1 = Fabricate(:post, topic: topic, user: user)
            p2 = Fabricate(:post, topic: topic, user: user, reply_to_post_number: p1.post_number)
            PostReply.create(post_id: p1.id, reply_post_id: p2.id)

            post "/t/#{topic.id}/move-posts.json", params: {
              title: 'new topic title',
              post_ids: [p1.id],
              reply_post_ids: [p1.id]
            }
            expect(response.status).to eq(200)

            p1.reload
            p2.reload

            new_topic_id = response.parsed_body["url"].split("/").last.to_i
            new_topic = Topic.find(new_topic_id)
            expect(p1.topic.id).to eq(new_topic.id)
            expect(p2.topic.id).to eq(new_topic.id)
            expect(p2.reply_to_post_number).to eq(p1.post_number)
          end
        end
      end
    end

    describe "moving to a new topic as a group moderator" do
      fab!(:category) { Fabricate(:category, reviewable_by_group: group_user.group) }
      fab!(:topic) { Fabricate(:topic, category: category) }
      fab!(:p1) { Fabricate(:post, user: group_user.user, post_number: 1, topic: topic) }
      fab!(:p2) { Fabricate(:post, user: group_user.user, post_number: 2, topic: topic) }
      let!(:user) { group_user.user }

      before do
        sign_in(user)
        SiteSetting.enable_category_group_moderation = true
      end

      it "moves the posts" do
        expect do
          post "/t/#{topic.id}/move-posts.json", params: {
            title: 'Logan is a good movie',
            post_ids: [p2.id],
            category_id: category.id
          }
        end.to change { Topic.count }.by(1)

        expect(response.status).to eq(200)
        result = response.parsed_body
        expect(result['success']).to eq(true)
        expect(result['url']).to eq(Topic.last.relative_url)
      end

      it "does not allow posts to be moved to a private category" do
        post "/t/#{topic.id}/move-posts.json", params: {
          title: 'Logan is a good movie',
          post_ids: [p2.id],
          category_id: staff_category.id
        }

        expect(response).to be_forbidden
      end

      it "does not allow posts outside of the category to be moved" do
        topic.update!(category: nil)

        post "/t/#{topic.id}/move-posts.json", params: {
          title: 'blah', post_ids: [p1.post_number, p2.post_number]
        }

        expect(response).to be_forbidden
      end
    end

    describe 'moving to an existing topic' do
      let!(:user) { sign_in(moderator) }
      fab!(:p1) { Fabricate(:post, user: moderator) }
      fab!(:topic) { p1.topic }
      fab!(:dest_topic) { Fabricate(:topic) }
      fab!(:p2) { Fabricate(:post, user: moderator, topic: topic) }

      context 'success' do
        it "returns success" do
          post "/t/#{topic.id}/move-posts.json", params: {
            post_ids: [p2.id],
            destination_topic_id: dest_topic.id
          }

          expect(response.status).to eq(200)
          result = response.parsed_body
          expect(result['success']).to eq(true)
          expect(result['url']).to be_present
        end

        it "triggers an event on merge" do
          begin
            called = false

            assert = -> (original_topic, destination_topic) do
              called = true
              expect(original_topic).to eq(topic)
              expect(destination_topic).to eq(dest_topic)
            end

            DiscourseEvent.on(:topic_merged, &assert)

            post "/t/#{topic.id}/move-posts.json", params: {
              post_ids: [p2.id],
              destination_topic_id: dest_topic.id
            }

            expect(called).to eq(true)
            expect(response.status).to eq(200)
          ensure
            DiscourseEvent.off(:topic_merged, &assert)
          end
        end
      end

      context 'failure' do
        fab!(:p2) { Fabricate(:post, user: moderator) }
        it "returns JSON with a false success" do
          post "/t/#{topic.id}/move-posts.json", params: {
            post_ids: [p2.id]
          }

          expect(response.status).to eq(200)
          result = response.parsed_body
          expect(result['success']).to eq(false)
          expect(result['url']).to be_blank
        end
      end
    end

    describe "moving to an existing topic as a group moderator" do
      fab!(:category) { Fabricate(:category, reviewable_by_group: group_user.group) }
      fab!(:topic) { Fabricate(:topic, category: category) }
      fab!(:p1) { Fabricate(:post, user: group_user.user, post_number: 1, topic: topic) }
      fab!(:p2) { Fabricate(:post, user: group_user.user, post_number: 2, topic: topic) }
      fab!(:dest_topic) { Fabricate(:topic) }

      let!(:user) { group_user.user }

      before do
        sign_in(user)
        SiteSetting.enable_category_group_moderation = true
      end

      it "moves the posts" do
        post "/t/#{topic.id}/move-posts.json", params: {
          post_ids: [p2.id],
          destination_topic_id: dest_topic.id
        }

        expect(response.status).to eq(200)
        result = response.parsed_body
        expect(result['success']).to eq(true)
        expect(result['url']).to be_present
      end

      it "does not allow posts to be moved to a private category" do
        dest_topic.update!(category: staff_category)

        post "/t/#{topic.id}/move-posts.json", params: {
          post_ids: [p2.id],
          destination_topic_id: dest_topic.id
        }

        expect(response).to be_forbidden
      end

      it "does not allow posts outside of the category to be moved" do
        topic.update!(category: nil)

        post "/t/#{topic.id}/move-posts.json", params: {
          post_ids: [p1.post_number, p2.post_number],
          destination_topic_id: dest_topic.id
        }

        expect(response).to be_forbidden
      end
    end

    describe 'moving to a new message' do
      fab!(:message) { Fabricate(:private_message_topic) }
      fab!(:p1) { Fabricate(:post, user: user, post_number: 1, topic: message) }
      fab!(:p2) { Fabricate(:post, user: user, post_number: 2, topic: message) }

      it "raises an error without post_ids" do
        sign_in(moderator)
        post "/t/#{message.id}/move-posts.json", params: { title: 'blah', archetype: 'private_message' }
        expect(response.status).to eq(400)
      end

      it "raises an error when the user doesn't have permission to move the posts" do
        sign_in(trust_level_4)

        post "/t/#{message.id}/move-posts.json", params: {
          title: 'blah', post_ids: [p1.post_number, p2.post_number], archetype: 'private_message'
        }

        expect(response.status).to eq(403)
        result = response.parsed_body
        expect(result['errors']).to be_present
      end

      context 'success' do
        before { sign_in(admin) }

        it "returns success" do
          SiteSetting.allow_staff_to_tag_pms = true

          expect do
            post "/t/#{message.id}/move-posts.json", params: {
              title: 'Logan is a good movie',
              post_ids: [p2.id],
              archetype: 'private_message',
              tags: ["tag1", "tag2"]
            }
          end.to change { Topic.count }.by(1)

          expect(response.status).to eq(200)

          result = response.parsed_body

          expect(result['success']).to eq(true)
          expect(result['url']).to eq(Topic.last.relative_url)
          expect(Tag.all.pluck(:name)).to contain_exactly("tag1", "tag2")
        end

        describe 'when message has been deleted' do
          it 'should still be able to move posts' do
            PostDestroyer.new(admin, message.first_post).destroy

            expect(message.reload.deleted_at).to_not be_nil

            expect do
              post "/t/#{message.id}/move-posts.json", params: {
                title: 'Logan is a good movie',
                post_ids: [p2.id],
                archetype: 'private_message'
              }
            end.to change { Topic.count }.by(1)

            expect(response.status).to eq(200)

            result = response.parsed_body

            expect(result['success']).to eq(true)
            expect(result['url']).to eq(Topic.last.relative_url)
          end
        end
      end

      context 'failure' do
        it "returns JSON with a false success" do
          sign_in(moderator)
          post "/t/#{message.id}/move-posts.json", params: {
            post_ids: [p2.id],
            archetype: 'private_message'
          }
          expect(response.status).to eq(200)
          result = response.parsed_body
          expect(result['success']).to eq(false)
          expect(result['url']).to be_blank
        end
      end
    end

    describe 'moving to an existing message' do
      let!(:user) { sign_in(admin) }
      fab!(:evil_trout) { Fabricate(:evil_trout) }
      fab!(:message) { Fabricate(:private_message_topic) }
      fab!(:p2) { Fabricate(:post, user: evil_trout, post_number: 2, topic: message) }

      fab!(:dest_message) do
        Fabricate(:private_message_topic, user: trust_level_4, topic_allowed_users: [
          Fabricate.build(:topic_allowed_user, user: evil_trout)
        ])
      end

      context 'success' do
        it "returns success" do
          post "/t/#{message.id}/move-posts.json", params: {
            post_ids: [p2.id],
            destination_topic_id: dest_message.id,
            archetype: 'private_message'
          }

          expect(response.status).to eq(200)
          result = response.parsed_body
          expect(result['success']).to eq(true)
          expect(result['url']).to be_present
        end
      end

      context 'failure' do
        it "returns JSON with a false success" do
          post "/t/#{message.id}/move-posts.json", params: {
            post_ids: [p2.id],
            archetype: 'private_message'
          }

          expect(response.status).to eq(200)
          result = response.parsed_body
          expect(result['success']).to eq(false)
          expect(result['url']).to be_blank
        end
      end
    end
  end

  describe '#merge_topic' do
    it 'needs you to be logged in' do
      post "/t/111/merge-topic.json", params: {
        destination_topic_id: 345
      }
      expect(response.status).to eq(403)
    end

    describe 'merging into another topic' do
      fab!(:p1) { Fabricate(:post, user: user) }
      fab!(:topic) { p1.topic }
      fab!(:dest_topic) { Fabricate(:topic) }

      it "raises an error without destination_topic_id" do
        sign_in(moderator)
        post "/t/#{topic.id}/merge-topic.json"
        expect(response.status).to eq(400)
      end

      it "raises an error when the user doesn't have permission to merge" do
        sign_in(user)
        post "/t/111/merge-topic.json", params: { destination_topic_id: 345 }
        expect(response).to be_forbidden
      end

      context 'moves all the posts to the destination topic' do
        it "returns success" do
          sign_in(moderator)
          post "/t/#{topic.id}/merge-topic.json", params: {
            destination_topic_id: dest_topic.id
          }

          expect(response.status).to eq(200)
          result = response.parsed_body
          expect(result['success']).to eq(true)
          expect(result['url']).to be_present
        end
      end
    end

    describe "merging into another topic as a group moderator" do
      fab!(:category) { Fabricate(:category, reviewable_by_group: group_user.group) }
      fab!(:topic) { Fabricate(:topic, category: category) }
      fab!(:p1) { Fabricate(:post, post_number: 1, topic: topic) }
      fab!(:p2) { Fabricate(:post, post_number: 2, topic: topic) }
      fab!(:dest_topic) { Fabricate(:topic) }
      let!(:user) { group_user.user }

      before do
        sign_in(user)
        SiteSetting.enable_category_group_moderation = true
      end

      it "moves the posts" do
        post "/t/#{topic.id}/merge-topic.json", params: {
          destination_topic_id: dest_topic.id
        }

        expect(response.status).to eq(200)
        result = response.parsed_body
        expect(result['success']).to eq(true)
        expect(result['url']).to be_present
      end

      it "does not allow posts to be moved to a private category" do
        dest_topic.update!(category: staff_category)

        post "/t/#{topic.id}/merge-topic.json", params: {
          destination_topic_id: dest_topic.id
        }

        expect(response).to be_forbidden
      end

      it "does not allow posts outside of the category to be moved" do
        topic.update!(category: nil)

        post "/t/#{topic.id}/merge-topic.json", params: {
          destination_topic_id: dest_topic.id
        }

        expect(response).to be_forbidden
      end
    end

    describe 'merging into another message' do
      fab!(:message) { Fabricate(:private_message_topic, user: user) }
      fab!(:p1) { Fabricate(:post, topic: message, user: trust_level_4) }
      fab!(:p2) { Fabricate(:post, topic: message, reply_to_post_number: p1.post_number, user: user) }

      fab!(:dest_message) do
        Fabricate(:private_message_topic, user: trust_level_4, topic_allowed_users: [
          Fabricate.build(:topic_allowed_user, user: moderator)
        ])
      end

      it "raises an error without destination_topic_id" do
        sign_in(moderator)
        post "/t/#{message.id}/merge-topic.json", params: {
          archetype: 'private_message'
        }
        expect(response.status).to eq(400)
      end

      it "raises an error when the user doesn't have permission to merge" do
        sign_in(trust_level_4)
        post "/t/#{message.id}/merge-topic.json", params: {
          destination_topic_id: 345,
          archetype: 'private_message'
        }
        expect(response).to be_forbidden
      end

      context 'moves all the posts to the destination message' do
        it "returns success" do
          sign_in(moderator)
          post "/t/#{message.id}/merge-topic.json", params: {
            destination_topic_id: dest_message.id,
            archetype: 'private_message'
          }

          expect(response.status).to eq(200)
          result = response.parsed_body
          expect(result['success']).to eq(true)
          expect(result['url']).to be_present
        end
      end
    end
  end

  describe '#change_post_owners' do
    it 'needs you to be logged in' do
      post "/t/111/change-owner.json", params: {
        username: 'user_a',
        post_ids: [1, 2, 3]
      }
      expect(response).to be_forbidden
    end

    describe 'forbidden to trust_level_4s' do
      before do
        sign_in(trust_level_4)
      end

      it 'correctly denies' do
        post "/t/111/change-owner.json", params: {
          topic_id: 111, username: 'user_a', post_ids: [1, 2, 3]
        }
        expect(response).to be_forbidden
      end
    end

    describe 'changing ownership' do
      fab!(:topic) { Fabricate(:topic) }
      fab!(:user_a) { Fabricate(:user) }
      fab!(:p1) { Fabricate(:post, topic: topic) }
      fab!(:p2) { Fabricate(:post, topic: topic) }

      describe 'moderator signed in' do
        let!(:editor) { sign_in(moderator) }

        it "returns 200 when moderators_change_post_ownership is true" do
          SiteSetting.moderators_change_post_ownership = true

          post "/t/#{topic.id}/change-owner.json", params: {
            username: user_a.username_lower, post_ids: [p1.id]
          }
          expect(response.status).to eq(200)
        end

        it "returns 403 when moderators_change_post_ownership is false" do
          SiteSetting.moderators_change_post_ownership = false

          post "/t/#{topic.id}/change-owner.json", params: {
            username: user_a.username_lower, post_ids: [p1.id]
          }
          expect(response.status).to eq(403)
        end
      end
      describe 'admin signed in' do
        let!(:editor) { sign_in(admin) }

        it "raises an error with a parameter missing" do
          [
            { post_ids: [1, 2, 3] },
            { username: 'user_a' }
          ].each do |params|
            post "/t/111/change-owner.json", params: params
            expect(response.status).to eq(400)
          end
        end

        it "changes the topic and posts ownership" do
          post "/t/#{topic.id}/change-owner.json", params: {
            username: user_a.username_lower, post_ids: [p1.id]
          }
          topic.reload
          p1.reload
          expect(response.status).to eq(200)
          expect(topic.user.username).to eq(user_a.username)
          expect(p1.user.username).to eq(user_a.username)
        end

        it "changes multiple posts" do
          post "/t/#{topic.id}/change-owner.json", params: {
            username: user_a.username_lower, post_ids: [p1.id, p2.id]
          }

          expect(response.status).to eq(200)

          p1.reload
          p2.reload

          expect(p1.user).to_not eq(nil)
          expect(p1.reload.user).to eq(p2.reload.user)
        end

        it "works with deleted users" do
          deleted_user = user
          t2 = Fabricate(:topic, user: deleted_user)
          p3 = Fabricate(:post, topic: t2, user: deleted_user)

          UserDestroyer.new(editor).destroy(deleted_user, delete_posts: true, context: 'test', delete_as_spammer: true)

          post "/t/#{t2.id}/change-owner.json", params: {
            username: user_a.username_lower, post_ids: [p3.id]
          }

          expect(response.status).to eq(200)
          t2.reload
          p3.reload
          expect(t2.deleted_at).to be_nil
          expect(p3.user).to eq(user_a)
        end

        it "removes likes by new owner" do
          now = Time.zone.now
          freeze_time(now - 1.day)
          PostActionCreator.like(user_a, p1)
          p1.reload
          freeze_time(now)
          post "/t/#{topic.id}/change-owner.json", params: {
            username: user_a.username_lower, post_ids: [p1.id]
          }
          topic.reload
          p1.reload
          expect(response.status).to eq(200)
          expect(topic.user.username).to eq(user_a.username)
          expect(p1.user.username).to eq(user_a.username)
          expect(p1.like_count).to eq(0)
        end
      end
    end
  end

  describe '#change_timestamps' do
    let!(:params) { { timestamp: Time.zone.now } }

    it 'needs you to be logged in' do
      put "/t/1/change-timestamp.json", params: params
      expect(response.status).to eq(403)
    end

    describe "forbidden to trust_level_4" do
      before do
        sign_in(trust_level_4)
      end

      it 'correctly denies' do
        put "/t/1/change-timestamp.json", params: params
        expect(response).to be_forbidden
      end
    end

    describe 'changing timestamps' do
      before do
        freeze_time
        sign_in(moderator)
      end

      let!(:old_timestamp) { Time.zone.now }
      let!(:new_timestamp) { old_timestamp - 1.day }
      let!(:topic) { Fabricate(:topic, created_at: old_timestamp) }
      let!(:p1) { Fabricate(:post, topic: topic, created_at: old_timestamp) }
      let!(:p2) { Fabricate(:post, topic: topic, created_at: old_timestamp + 1.day) }

      it 'should update the timestamps of selected posts' do
        # try to see if we fail with invalid first
        put "/t/1/change-timestamp.json"
        expect(response.status).to eq(400)

        put "/t/#{topic.id}/change-timestamp.json", params: {
          timestamp: new_timestamp.to_f
        }

        expect(response.status).to eq(200)
        expect(topic.reload.created_at).to eq_time(new_timestamp)
        expect(p1.reload.created_at).to eq_time(new_timestamp)
        expect(p2.reload.created_at).to eq_time(old_timestamp)
      end

      it 'should create a staff log entry' do
        put "/t/#{topic.id}/change-timestamp.json", params: {
          timestamp: new_timestamp.to_f
        }

        log = UserHistory.last
        expect(log.acting_user_id).to eq(moderator.id)
        expect(log.topic_id).to eq(topic.id)
        expect(log.new_value).to eq(new_timestamp.utc.to_s)
        expect(log.previous_value).to eq(old_timestamp.utc.to_s)
      end
    end
  end

  describe '#clear_pin' do
    it 'needs you to be logged in' do
      put "/t/1/clear-pin.json"
      expect(response.status).to eq(403)
    end

    context 'when logged in' do
      fab!(:topic) { Fabricate(:topic) }
      fab!(:pm) { Fabricate(:private_message_topic) }

      before do
        sign_in(user)
      end

      it "fails when the user can't see the topic" do
        put "/t/#{pm.id}/clear-pin.json"
        expect(response).to be_forbidden
      end

      describe 'when the user can see the topic' do
        it "succeeds" do
          expect do
            put "/t/#{topic.id}/clear-pin.json"
          end.to change { TopicUser.where(topic_id: topic.id, user_id: user.id).count }.by(1)
          expect(response.status).to eq(200)
        end
      end
    end
  end

  describe '#status' do
    it 'needs you to be logged in' do
      put "/t/1/status.json", params: {
        status: 'visible', enabled: true
      }
      expect(response.status).to eq(403)
    end

    describe 'when logged in as a moderator' do
      fab!(:topic) { Fabricate(:topic) }
      before do
        sign_in(moderator)
      end

      it "raises an exception if you can't change it" do
        sign_in(user)
        put "/t/#{topic.id}/status.json", params: {
          status: 'visible', enabled: 'true'
        }
        expect(response).to be_forbidden
      end

      it 'requires the status parameter' do
        put "/t/#{topic.id}/status.json", params: { enabled: true }
        expect(response.status).to eq(400)
      end

      it 'requires the enabled parameter' do
        put "/t/#{topic.id}/status.json", params: { status: 'visible' }
        expect(response.status).to eq(400)
      end

      it 'raises an error with a status not in the allowlist' do
        put "/t/#{topic.id}/status.json", params: {
          status: 'title', enabled: 'true'
        }
        expect(response.status).to eq(400)
      end

      it 'should update the status of the topic correctly' do
        topic = Fabricate(:topic, user: user, closed: true)
        Fabricate(:topic_timer, topic: topic, status_type: TopicTimer.types[:open])

        put "/t/#{topic.id}/status.json", params: {
          status: 'closed', enabled: 'false'
        }

        expect(response.status).to eq(200)
        expect(topic.reload.closed).to eq(false)
        expect(topic.topic_timers).to eq([])

        body = response.parsed_body

        expect(body['topic_status_update']).to eq(nil)
      end
    end

    describe 'when logged in as a group member with reviewable status' do
      fab!(:category) { Fabricate(:category, reviewable_by_group: group_user.group) }
      fab!(:topic) { Fabricate(:topic, category: category) }
      let!(:user) { group_user.user }

      before do
        sign_in(user)
        SiteSetting.enable_category_group_moderation = true
      end

      it 'should allow a group moderator to close a topic' do
        put "/t/#{topic.id}/status.json", params: {
          status: 'closed', enabled: 'true'
        }

        expect(response.status).to eq(200)
        expect(topic.reload.closed).to eq(true)
        expect(topic.posts.last.action_code).to eq('closed.enabled')
      end

      it 'should allow a group moderator to open a closed topic' do
        topic.update!(closed: true)

        expect do
          put "/t/#{topic.id}/status.json", params: {
            status: 'closed', enabled: 'false'
          }
        end.to change { topic.reload.posts.count }.by(1)

        expect(response.status).to eq(200)
        expect(topic.reload.closed).to eq(false)
        expect(topic.posts.last.action_code).to eq('closed.disabled')
      end

      it 'should allow a group moderator to archive a topic' do
        expect do
          put "/t/#{topic.id}/status.json", params: {
            status: 'archived', enabled: 'true'
          }
        end.to change { topic.reload.posts.count }.by(1)

        expect(response.status).to eq(200)
        expect(topic.reload.archived).to eq(true)
        expect(topic.posts.last.action_code).to eq('archived.enabled')
      end

      it 'should allow a group moderator to unarchive an archived topic' do
        topic.update!(archived: true)

        put "/t/#{topic.id}/status.json", params: {
          status: 'archived', enabled: 'false'
        }

        expect(response.status).to eq(200)
        expect(topic.reload.archived).to eq(false)
        expect(topic.posts.last.action_code).to eq('archived.disabled')
      end

      it 'should allow a group moderator to pin a topic' do
        put "/t/#{topic.id}/status.json", params: {
          status: 'pinned', enabled: 'true', until: 2.weeks.from_now
        }

        expect(response.status).to eq(200)
        expect(topic.reload.pinned_at).to_not eq(nil)
      end

      it 'should allow a group moderator to unpin a topic' do
        put "/t/#{topic.id}/status.json", params: {
          status: 'pinned', enabled: 'false'
        }

        expect(response.status).to eq(200)
        expect(topic.reload.pinned_at).to eq(nil)
      end

      it 'should allow a group moderator to unlist a topic' do
        put "/t/#{topic.id}/status.json", params: {
          status: 'visible', enabled: 'false'
        }

        expect(response.status).to eq(200)
        expect(topic.reload.visible).to eq(false)
        expect(topic.posts.last.action_code).to eq('visible.disabled')
      end

      it 'should allow a group moderator to list an unlisted topic' do
        topic.update!(visible: false)

        put "/t/#{topic.id}/status.json", params: {
          status: 'visible', enabled: 'true'
        }

        expect(response.status).to eq(200)
        expect(topic.reload.visible).to eq(true)
        expect(topic.posts.last.action_code).to eq('visible.enabled')
      end
    end
  end

  describe '#destroy_timings' do
    it 'needs you to be logged in' do
      delete "/t/1/timings.json"
      expect(response.status).to eq(403)
    end

    def topic_user_post_timings_count(user, topic)
      [TopicUser, PostTiming].map do |klass|
        klass.where(user: user, topic: topic).count
      end
    end

    context 'for last post only' do
      it 'should allow you to retain topic timing but remove last post only' do
        freeze_time

        post1 = create_post
        user = post1.user

        topic = post1.topic

        post2 = create_post(topic_id: topic.id)

        PostTiming.create!(topic: topic, user: user, post_number: 2, msecs: 100)

        user.user_stat.update!(first_unread_at: Time.now + 1.week)

        topic_user = TopicUser.find_by(
          topic_id: topic.id,
          user_id: user.id,
        )

        topic_user.update!(
          last_read_post_number: 2,
        )

        # ensure we have 2 notifications
        # fake notification on topic but it is read
        first_notification = Notification.create!(
          user_id: user.id,
          topic_id: topic.id,
          data: "{}",
          read: true,
          notification_type: 1
        )

        freeze_time 1.minute.from_now
        PostAlerter.post_created(post2)

        second_notification = user.notifications.where(topic_id: topic.id).order(created_at: :desc).first
        second_notification.update!(read: true)

        sign_in(user)

        delete "/t/#{topic.id}/timings.json?last=1"

        expect(PostTiming.where(topic: topic, user: user, post_number: 2).exists?).to eq(false)
        expect(PostTiming.where(topic: topic, user: user, post_number: 1).exists?).to eq(true)

        expect(TopicUser.where(topic: topic, user: user, last_read_post_number: 1).exists?).to eq(true)

        user.user_stat.reload
        expect(user.user_stat.first_unread_at).to eq_time(topic.updated_at)

        first_notification.reload
        second_notification.reload
        expect(first_notification.read).to eq(true)
        expect(second_notification.read).to eq(false)

        PostDestroyer.new(admin, post2).destroy

        delete "/t/#{topic.id}/timings.json?last=1"

        expect(PostTiming.where(topic: topic, user: user, post_number: 1).exists?).to eq(false)
        expect(TopicUser.where(topic: topic, user: user, last_read_post_number: nil).exists?).to eq(true)
      end
    end

    context 'when logged in' do
      before do
        @user = sign_in(user)
        @topic = Fabricate(:topic, user: @user)
        Fabricate(:post, user: @user, topic: @topic, post_number: 2)
        TopicUser.create!(topic: @topic, user: @user)
        PostTiming.create!(topic: @topic, user: @user, post_number: 2, msecs: 1000)
      end

      it 'deletes the forum topic user and post timings records' do
        expect do
          delete "/t/#{@topic.id}/timings.json"
        end.to change { topic_user_post_timings_count(@user, @topic) }.from([1, 1]).to([0, 0])
      end
    end
  end

  describe '#mute/unmute' do
    it 'needs you to be logged in' do
      put "/t/99/mute.json"
      expect(response.status).to eq(403)
    end

    it 'needs you to be logged in' do
      put "/t/99/unmute.json"
      expect(response.status).to eq(403)
    end
  end

  describe '#recover' do
    it "won't allow us to recover a topic when we're not logged in" do
      put "/t/1/recover.json"
      expect(response.status).to eq(403)
    end

    describe 'when logged in' do
      let!(:topic) { Fabricate(:topic, user: user, deleted_at: Time.now, deleted_by: moderator) }
      let!(:post) { Fabricate(:post, user: user, topic: topic, post_number: 1, deleted_at: Time.now, deleted_by: moderator) }

      describe 'without access' do
        it "raises an exception when the user doesn't have permission to delete the topic" do
          sign_in(user)
          put "/t/#{topic.id}/recover.json"
          expect(response).to be_forbidden
        end
      end

      context 'with permission' do
        before do
          sign_in(moderator)
        end

        it 'succeeds' do
          put "/t/#{topic.id}/recover.json"
          topic.reload
          post.reload
          expect(response.status).to eq(200)
          expect(topic.trashed?).to be_falsey
          expect(post.trashed?).to be_falsey
        end
      end
    end
  end

  describe '#delete' do
    it "won't allow us to delete a topic when we're not logged in" do
      delete "/t/1.json"
      expect(response.status).to eq(403)
    end

    describe 'when logged in' do
      fab!(:topic) { Fabricate(:topic, user: user, created_at: 48.hours.ago) }
      fab!(:post) { Fabricate(:post, topic: topic, user: user, post_number: 1) }

      describe 'without access' do
        it "raises an exception when the user doesn't have permission to delete the topic" do
          sign_in(user)
          delete "/t/#{topic.id}.json"
          expect(response.status).to eq(422)
        end
      end

      describe 'with permission' do
        before do
          sign_in(moderator)
        end

        it 'succeeds' do
          delete "/t/#{topic.id}.json"
          expect(response.status).to eq(200)
          topic.reload
          expect(topic.trashed?).to be_truthy
        end
      end
    end
  end

  describe '#id_for_slug' do
    fab!(:topic) { Fabricate(:post).topic }
    fab!(:pm) { Fabricate(:private_message_topic) }

    it "returns JSON for the slug" do
      get "/t/id_for/#{topic.slug}.json"
      expect(response.status).to eq(200)
      json = response.parsed_body
      expect(json['topic_id']).to eq(topic.id)
      expect(json['url']).to eq(topic.url)
      expect(json['slug']).to eq(topic.slug)
    end

    it "returns invalid access if the user can't see the topic" do
      get "/t/id_for/#{pm.slug}.json"
      expect(response).to be_forbidden
    end
  end

  describe '#update' do
    it "won't allow us to update a topic when we're not logged in" do
      put "/t/1.json", params: { slug: 'xyz' }
      expect(response.status).to eq(403)
    end

    describe 'when logged in' do
      fab!(:topic) { Fabricate(:topic, user: user) }

      before do
        Fabricate(:post, topic: topic)
        SiteSetting.editing_grace_period = 0
        sign_in(user)
      end

      it 'can not change category to a disallowed category' do
        category.set_permissions(staff: :full)
        category.save!

        put "/t/#{topic.id}.json", params: { category_id: category.id }

        expect(response.status).to eq(403)
        expect(topic.reload.category_id).not_to eq(category.id)
      end

      it 'can not move to a category that requires topic approval' do
        category.custom_fields[Category::REQUIRE_TOPIC_APPROVAL] = true
        category.save!

        put "/t/#{topic.id}.json", params: { category_id: category.id }

        expect(response.status).to eq(403)
        expect(topic.reload.category_id).not_to eq(category.id)
      end

      context 'updating shared drafts' do
        fab!(:shared_drafts_category) { Fabricate(:category) }
        fab!(:topic) { Fabricate(:topic, category: shared_drafts_category) }
        fab!(:shared_draft) { Fabricate(:shared_draft, topic: topic, category: Fabricate(:category)) }

        it 'changes destination category' do
          put "/t/#{topic.id}.json", params: { category_id: category.id }

          expect(response.status).to eq(403)
          expect(topic.shared_draft.category_id).not_to eq(category.id)
        end
      end

      describe 'without permission' do
        it "raises an exception when the user doesn't have permission to update the topic" do
          topic.update!(archived: true)
          put "/t/#{topic.slug}/#{topic.id}.json"

          expect(response.status).to eq(403)
        end
      end

      describe 'with permission' do
        fab!(:post_hook) { Fabricate(:post_web_hook) }
        fab!(:topic_hook) { Fabricate(:topic_web_hook) }

        it 'succeeds' do
          put "/t/#{topic.slug}/#{topic.id}.json"

          expect(response.status).to eq(200)
          expect(response.parsed_body['basic_topic']).to be_present
        end

        it "throws an error if it could not be saved" do
          PostRevisor.any_instance.stubs(:should_revise?).returns(false)
          put "/t/#{topic.slug}/#{topic.id}.json", params: { title: "brand new title" }

          expect(response.status).to eq(422)
          expect(response.parsed_body['errors'].first).to eq(
            I18n.t("activerecord.errors.models.topic.attributes.base.unable_to_update")
          )
        end

        it "can update a topic to an uncategorized topic" do
          topic.update!(category: category)

          put "/t/#{topic.slug}/#{topic.id}.json", params: {
            category_id: ""
          }

          expect(response.status).to eq(200)
          expect(topic.reload.category_id).to eq(SiteSetting.uncategorized_category_id)
        end

        it 'allows a change of title' do
          put "/t/#{topic.slug}/#{topic.id}.json", params: {
            title: 'This is a new title for the topic'
          }

          topic.reload
          expect(topic.title).to eq('This is a new title for the topic')

          # emits a topic_edited event but not a post_edited web hook event
          expect(Jobs::EmitWebHookEvent.jobs.length).to eq(1)
          job_args = Jobs::EmitWebHookEvent.jobs[0]["args"].first

          expect(job_args["event_name"]).to eq("topic_edited")
          payload = JSON.parse(job_args["payload"])
          expect(payload["title"]).to eq('This is a new title for the topic')
        end

        it 'allows a change of then updating the OP' do
          topic.update(user: user)
          topic.first_post.update(user: user)

          put "/t/#{topic.slug}/#{topic.id}.json", params: {
            title: 'This is a new title for the topic'
          }

          topic.reload
          expect(topic.title).to eq('This is a new title for the topic')

          update_params = {
            post: { raw: 'edited body', edit_reason: 'typo' },
          }
          put "/posts/#{topic.first_post.id}.json", params: update_params

          # emits a topic_edited event and a post_edited web hook event
          expect(Jobs::EmitWebHookEvent.jobs.length).to eq(2)
          job_args = Jobs::EmitWebHookEvent.jobs[0]["args"].first

          expect(job_args["event_name"]).to eq("topic_edited")
          payload = JSON.parse(job_args["payload"])
          expect(payload["title"]).to eq('This is a new title for the topic')

          job_args = Jobs::EmitWebHookEvent.jobs[1]["args"].first

          expect(job_args["event_name"]).to eq("post_edited")
          payload = JSON.parse(job_args["payload"])
          expect(payload["raw"]).to eq("edited body")
        end

        it "returns errors with invalid titles" do
          put "/t/#{topic.slug}/#{topic.id}.json", params: {
            title: 'asdf'
          }

          expect(response.status).to eq(422)
          expect(response.parsed_body['errors']).to match_array([/Title is too short/, /Title seems unclear/])
        end

        it "returns errors when the rate limit is exceeded" do
          EditRateLimiter.any_instance.expects(:performed!).raises(RateLimiter::LimitExceeded.new(60))

          put "/t/#{topic.slug}/#{topic.id}.json", params: {
            title: 'This is a new title for the topic'
          }

          expect(response.status).to eq(429)
        end

        it "returns errors with invalid categories" do
          put "/t/#{topic.slug}/#{topic.id}.json", params: {
            category_id: -1
          }

          expect(response.status).to eq(422)
        end

        it "doesn't call the PostRevisor when there is no changes" do
          expect do
            put "/t/#{topic.slug}/#{topic.id}.json", params: {
              category_id: topic.category_id
            }
          end.not_to change(PostRevision.all, :count)

          expect(response.status).to eq(200)
        end

        describe "when first post is locked" do
          it "blocks non-staff from editing even if 'trusted_users_can_edit_others' is true" do
            SiteSetting.trusted_users_can_edit_others = true
            user.update!(trust_level: 3)
            topic.first_post.update!(locked_by_id: admin.id)

            put "/t/#{topic.slug}/#{topic.id}.json", params: {
              title: topic.title + " hello"
            }

            expect(response.status).to eq(403)
          end

          it "allows staff to edit" do
            sign_in(Fabricate(:admin))
            topic.first_post.update!(locked_by_id: admin.id)

            put "/t/#{topic.slug}/#{topic.id}.json", params: {
              title: topic.title + " hello"
            }
            expect(response.status).to eq(200)
          end
        end

        context 'tags' do
          fab!(:tag) { Fabricate(:tag) }

          before do
            SiteSetting.tagging_enabled = true
          end

          it "can add a tag to topic" do
            expect do
              put "/t/#{topic.slug}/#{topic.id}.json", params: {
                tags: [tag.name]
              }
            end.to change { topic.reload.first_post.revisions.count }.by(1)

            expect(response.status).to eq(200)
            expect(topic.tags.pluck(:id)).to contain_exactly(tag.id)
          end

          it "can create a tag" do
            SiteSetting.min_trust_to_create_tag = 0
            expect do
              put "/t/#{topic.slug}/#{topic.id}.json", params: {
                tags: ["newtag"]
              }
            end.to change { topic.reload.first_post.revisions.count }.by(1)

            expect(response.status).to eq(200)
            expect(topic.reload.tags.pluck(:name)).to contain_exactly("newtag")
          end

          it "can change the category and create a new tag" do
            SiteSetting.min_trust_to_create_tag = 0
            expect do
              put "/t/#{topic.slug}/#{topic.id}.json", params: {
                tags: ["newtag"],
                category_id: category.id
              }
            end.to change { topic.reload.first_post.revisions.count }.by(1)

            expect(response.status).to eq(200)
            expect(topic.reload.tags.pluck(:name)).to contain_exactly("newtag")
          end

          it "can add a tag to wiki topic" do
            SiteSetting.min_trust_to_edit_wiki_post = 2
            topic.first_post.update!(wiki: true)
            user = Fabricate(:user)
            sign_in(user)

            expect do
              put "/t/#{topic.id}/tags.json", params: {
                tags: [tag.name]
              }
            end.not_to change { topic.reload.first_post.revisions.count }

            expect(response.status).to eq(403)
            user.update!(trust_level: 2)

            expect do
              put "/t/#{topic.id}/tags.json", params: {
                tags: [tag.name]
              }
            end.to change { topic.reload.first_post.revisions.count }.by(1)

            expect(response.status).to eq(200)
            expect(topic.tags.pluck(:id)).to contain_exactly(tag.id)
          end

          it 'does not remove tag if no params is given' do
            topic.tags << tag

            expect do
              put "/t/#{topic.slug}/#{topic.id}.json"
            end.to_not change { topic.reload.tags.count }

            expect(response.status).to eq(200)
          end

          it 'can remove a tag' do
            topic.tags << tag

            expect do
              put "/t/#{topic.slug}/#{topic.id}.json", params: {
                tags: [""]
              }
            end.to change { topic.reload.first_post.revisions.count }.by(1)

            expect(response.status).to eq(200)
            expect(topic.tags).to eq([])
          end
        end

        context 'when topic is private' do
          before do
            topic.update!(
              archetype: Archetype.private_message,
              category: nil,
              allowed_users: [topic.user]
            )
          end

          context 'when there are no changes' do
            it 'does not call the PostRevisor' do
              expect do
                put "/t/#{topic.slug}/#{topic.id}.json", params: {
                  category_id: topic.category_id
                }
              end.not_to change(PostRevision.all, :count)

              expect(response.status).to eq(200)
            end
          end
        end

        context 'updating to a category with restricted tags' do
          fab!(:restricted_category) { Fabricate(:category) }
          fab!(:tag1) { Fabricate(:tag) }
          fab!(:tag2) { Fabricate(:tag) }
          fab!(:tag3) { Fabricate(:tag) }
          fab!(:tag_group_1) { Fabricate(:tag_group, tag_names: [tag1.name]) }
          fab!(:tag_group_2) { Fabricate(:tag_group) }

          before do
            SiteSetting.tagging_enabled = true
            topic.update!(tags: [tag1])
          end

          it 'can’t change to a category disallowing this topic current tags' do
            restricted_category.allowed_tags = [tag2.name]

            put "/t/#{topic.slug}/#{topic.id}.json", params: { category_id: restricted_category.id }

            result = response.parsed_body

            expect(response.status).to eq(422)
            expect(result['errors']).to be_present
            expect(topic.reload.category_id).not_to eq(restricted_category.id)
          end

          it 'can’t change to a category disallowing this topic current tag (through tag_group)' do
            tag_group_2.tags = [tag2]
            restricted_category.allowed_tag_groups = [tag_group_2.name]

            put "/t/#{topic.slug}/#{topic.id}.json", params: { category_id: restricted_category.id }

            result = response.parsed_body

            expect(response.status).to eq(422)
            expect(result['errors']).to be_present
            expect(topic.reload.category_id).not_to eq(restricted_category.id)
          end

          it 'can change to a category allowing this topic current tags' do
            restricted_category.allowed_tags = [tag1.name]

            put "/t/#{topic.slug}/#{topic.id}.json", params: { category_id: restricted_category.id }

            expect(response.status).to eq(200)
          end

          it 'can change to a category allowing this topic current tags (through tag_group)' do
            tag_group_1.tags = [tag1]
            restricted_category.allowed_tag_groups = [tag_group_1.name]

            put "/t/#{topic.slug}/#{topic.id}.json", params: { category_id: restricted_category.id }

            expect(response.status).to eq(200)
          end

          it 'can change to a category allowing any tag' do
            put "/t/#{topic.slug}/#{topic.id}.json", params: { category_id: category.id }

            expect(response.status).to eq(200)
          end

          it 'can’t add a category-only tags from another category to a category' do
            restricted_category.allowed_tags = [tag2.name]

            put "/t/#{topic.slug}/#{topic.id}.json", params: {
              tags: [tag2.name],
              category_id: category.id
            }

            result = response.parsed_body
            expect(response.status).to eq(422)
            expect(result['errors']).to be_present
            expect(result['errors'][0]).to include(tag2.name)
            expect(topic.reload.category_id).not_to eq(restricted_category.id)
          end

          it 'allows category change when topic has a hidden tag' do
            Fabricate(:tag_group, permissions: { "staff" => 1 }, tag_names: [tag1.name])

            put "/t/#{topic.slug}/#{topic.id}.json", params: {
              category_id: category.id
            }

            result = response.parsed_body
            expect(response.status).to eq(200)
            expect(topic.reload.tags).to include(tag1)
          end

          it 'allows category change when topic has a read-only tag' do
            Fabricate(:tag_group, permissions: { "staff" => 1, "everyone" => 3 }, tag_names: [tag3.name])
            topic.update!(tags: [tag3])

            put "/t/#{topic.slug}/#{topic.id}.json", params: {
              category_id: category.id
            }

            result = response.parsed_body
            expect(response.status).to eq(200)
            expect(topic.reload.tags).to contain_exactly(tag3)
          end

          it 'does not leak tag name when trying to use a staff tag' do
            Fabricate(:tag_group, permissions: { "staff" => 1 }, tag_names: [tag3.name])

            put "/t/#{topic.slug}/#{topic.id}.json", params: {
              tags: [tag3.name],
              category_id: category.id
            }

            result = response.parsed_body
            expect(response.status).to eq(422)
            expect(result['errors']).to be_present
            expect(result['errors'][0]).not_to include(tag3.name)
          end

          it 'will clean tag params' do
            restricted_category.allowed_tags = [tag2.name]

            put "/t/#{topic.slug}/#{topic.id}.json", params: {
              tags: [""],
              category_id: restricted_category.id
            }

            result = response.parsed_body
            expect(response.status).to eq(200)
          end
        end

        context "allow_uncategorized_topics is false" do
          before do
            SiteSetting.allow_uncategorized_topics = false
          end

          it "can add a category to an uncategorized topic" do
            put "/t/#{topic.slug}/#{topic.id}.json", params: {
              category_id: category.id
            }

            expect(response.status).to eq(200)
            expect(topic.reload.category).to eq(category)
          end
        end
      end
    end
  end

  describe '#show' do
    fab!(:private_topic) { Fabricate(:private_message_topic) }
    let!(:topic) { Fabricate(:post).topic }

    let!(:p1) { Fabricate(:post, user: topic.user) }
    let!(:p2) { Fabricate(:post, user: topic.user) }

    describe 'when topic is not allowed' do
      it 'should return the right response' do
        SiteSetting.detailed_404 = true
        sign_in(user)

        get "/t/#{private_topic.id}.json"

        expect(response.status).to eq(403)
        expect(response.body).to include(I18n.t('invalid_access'))
      end
    end

    describe 'when topic is allowed to a group' do
      fab!(:group) { Fabricate(:group, public_admission: true) }
      fab!(:category) do
        Fabricate(:category_with_definition).tap do |category|
          category.set_permissions(group => :full)
          category.save!
        end
      end
      fab!(:topic) { Fabricate(:topic, category: category) }

      before do
        SiteSetting.detailed_404 = true
      end

      it 'shows a descriptive error message containing the group name' do
        get "/t/#{topic.id}.json"

        html = CGI.unescapeHTML(response.parsed_body["extras"]["html"])
        expect(response.status).to eq(403)
        expect(html).to include(I18n.t('not_in_group.title_topic', group: group.name))
        expect(html).to include(I18n.t('not_in_group.join_group'))
      end
    end

    it 'correctly renders canonicals' do
      get "/t/#{topic.id}", params: { slug: topic.slug }

      expect(response.status).to eq(200)
      expect(css_select("link[rel=canonical]").length).to eq(1)
      expect(response.headers["Cache-Control"]).to eq("no-cache, no-store")
    end

    it 'returns 301 even if slug does not match URL' do
      # in the past we had special logic for unlisted topics
      # we would require slug unless you made a json call
      # this was not really providing any security
      #
      # we no longer require a topic be visible to perform url correction
      # if you need to properly hide a topic for users use a secure category
      # or a PM
      topic = Fabricate(:topic, visible: false)
      Fabricate(:post, topic: topic)

      get "/t/#{topic.id}.json", params: { slug: topic.slug }
      expect(response.status).to eq(200)

      get "/t/#{topic.id}.json", params: { slug: "just-guessing" }
      expect(response.status).to eq(301)

      get "/t/#{topic.slug}.json"
      expect(response.status).to eq(301)
    end

    it 'shows a topic correctly' do
      get "/t/#{topic.slug}/#{topic.id}.json"
      expect(response.status).to eq(200)
    end

    it 'return 404 for an invalid page' do
      get "/t/#{topic.slug}/#{topic.id}.json", params: { page: 2 }
      expect(response.status).to eq(404)
    end

    it 'can find a topic given a slug in the id param' do
      get "/t/#{topic.slug}"
      expect(response).to redirect_to(topic.relative_url)
    end

    it 'can find a topic when a slug has a number in front' do
      another_topic = Fabricate(:post).topic

      topic.update_column(:slug, "#{another_topic.id}-reasons-discourse-is-awesome")
      get "/t/#{another_topic.id}-reasons-discourse-is-awesome"

      expect(response).to redirect_to(topic.relative_url)
    end

    it 'keeps the post_number parameter around when redirecting' do
      get "/t/#{topic.slug}", params: { post_number: 42 }
      expect(response).to redirect_to(topic.relative_url + "/42")
    end

    it 'keeps the page around when redirecting' do
      get "/t/#{topic.slug}", params: {
        post_number: 42, page: 123
      }

      expect(response).to redirect_to(topic.relative_url + "/42?page=123")
    end

    it 'does not accept page params as an array' do
      get "/t/#{topic.slug}", params: {
        post_number: 42, page: [2]
      }

      expect(response).to redirect_to("#{topic.relative_url}/42?page=1")
    end

    it 'returns 404 when an invalid slug is given and no id' do
      get "/t/nope-nope.json"

      expect(response.status).to eq(404)
    end

    it 'returns a 404 when slug and topic id do not match a topic' do
      get "/t/made-up-topic-slug/123456.json"
      expect(response.status).to eq(404)
    end

    it 'returns a 404 for an ID that is larger than postgres limits' do
      get "/t/made-up-topic-slug/5014217323220164041.json"

      expect(response.status).to eq(404)
    end

    context 'a topic with nil slug exists' do
      before do
        nil_slug_topic = Fabricate(:topic)
        Topic.connection.execute("update topics set slug=null where id = #{nil_slug_topic.id}") # can't find a way to set slug column to null using the model
      end

      it 'returns a 404 when slug and topic id do not match a topic' do
        get "/t/made-up-topic-slug/123123.json"
        expect(response.status).to eq(404)
      end
    end

    context 'permission errors' do
      fab!(:allowed_user) { Fabricate(:user) }
      fab!(:allowed_group) { Fabricate(:group) }
      fab!(:accessible_group) { Fabricate(:group, public_admission: true) }
      fab!(:secure_category) do
        c = Fabricate(:category)
        c.permissions = [[allowed_group, :full]]
        c.save
        allowed_user.groups = [allowed_group]
        allowed_user.save
        c
      end
      fab!(:accessible_category) do
        Fabricate(:category).tap do |c|
          c.set_permissions(accessible_group => :full)
          c.save!
        end
      end
      fab!(:normal_topic) { Fabricate(:topic) }
      fab!(:secure_topic) { Fabricate(:topic, category: secure_category) }
      fab!(:private_topic) { Fabricate(:private_message_topic, user: allowed_user) }
      let(:deleted_topic) { Fabricate(:deleted_topic) }
      let(:deleted_secure_topic) { Fabricate(:topic, category: secure_category, deleted_at: 1.day.ago) }
      let(:deleted_private_topic) { Fabricate(:private_message_topic, user: allowed_user, deleted_at: 1.day.ago) }
      let!(:nonexistent_topic_id) { Topic.last.id + 10000 }
      fab!(:secure_accessible_topic) { Fabricate(:topic, category: accessible_category) }

      shared_examples "various scenarios" do |expected|
        expected.each do |key, value|
          it "returns #{value} for #{key}" do
            slug = key == :nonexistent ? "garbage-slug" : send(key.to_s).slug
            topic_id = key == :nonexistent ? nonexistent_topic_id : send(key.to_s).id
            get "/t/#{slug}/#{topic_id}.json"
            expect(response.status).to eq(value)
          end
        end

        expected_slug_response = expected[:secure_topic] == 200 ? 301 : expected[:secure_topic]
        it "will return a #{expected_slug_response} when requesting a secure topic by slug" do
          get "/t/#{secure_topic.slug}"
          expect(response.status).to eq(expected_slug_response)
        end
      end

      context 'without detailed error pages' do
        before do
          SiteSetting.detailed_404 = false
        end

        context 'anonymous' do
          expected = {
            normal_topic: 200,
            secure_topic: 404,
            private_topic: 404,
            deleted_topic: 404,
            deleted_secure_topic: 404,
            deleted_private_topic: 404,
            nonexistent: 404,
            secure_accessible_topic: 404
          }
          include_examples "various scenarios", expected
        end

        context 'anonymous with login required' do
          before do
            SiteSetting.login_required = true
          end
          expected = {
            normal_topic: 302,
            secure_topic: 302,
            private_topic: 302,
            deleted_topic: 302,
            deleted_secure_topic: 302,
            deleted_private_topic: 302,
            nonexistent: 302,
            secure_accessible_topic: 302
          }
          include_examples "various scenarios", expected
        end

        context 'normal user' do
          before do
            sign_in(user)
          end

          expected = {
            normal_topic: 200,
            secure_topic: 404,
            private_topic: 404,
            deleted_topic: 404,
            deleted_secure_topic: 404,
            deleted_private_topic: 404,
            nonexistent: 404,
            secure_accessible_topic: 404
          }
          include_examples "various scenarios", expected
        end

        context 'allowed user' do
          before do
            sign_in(allowed_user)
          end

          expected = {
            normal_topic: 200,
            secure_topic: 200,
            private_topic: 200,
            deleted_topic: 404,
            deleted_secure_topic: 404,
            deleted_private_topic: 404,
            nonexistent: 404,
            secure_accessible_topic: 404
          }
          include_examples "various scenarios", expected
        end

        context 'moderator' do
          before do
            sign_in(moderator)
          end

          expected = {
            normal_topic: 200,
            secure_topic: 404,
            private_topic: 404,
            deleted_topic: 200,
            deleted_secure_topic: 404,
            deleted_private_topic: 404,
            nonexistent: 404,
            secure_accessible_topic: 404
          }
          include_examples "various scenarios", expected
        end

        context 'admin' do
          before do
            sign_in(admin)
          end

          expected = {
            normal_topic: 200,
            secure_topic: 200,
            private_topic: 200,
            deleted_topic: 200,
            deleted_secure_topic: 200,
            deleted_private_topic: 200,
            nonexistent: 404,
            secure_accessible_topic: 200
          }
          include_examples "various scenarios", expected
        end
      end

      context 'with detailed error pages' do
        before do
          SiteSetting.detailed_404 = true
        end

        context 'anonymous' do
          expected = {
            normal_topic: 200,
            secure_topic: 403,
            private_topic: 403,
            deleted_topic: 410,
            deleted_secure_topic: 403,
            deleted_private_topic: 403,
            nonexistent: 404,
            secure_accessible_topic: 403
          }
          include_examples "various scenarios", expected
        end

        context 'anonymous with login required' do
          before do
            SiteSetting.login_required = true
          end
          expected = {
            normal_topic: 302,
            secure_topic: 302,
            private_topic: 302,
            deleted_topic: 302,
            deleted_secure_topic: 302,
            deleted_private_topic: 302,
            nonexistent: 302,
            secure_accessible_topic: 302
          }
          include_examples "various scenarios", expected
        end

        context 'normal user' do
          before do
            sign_in(user)
          end

          expected = {
            normal_topic: 200,
            secure_topic: 403,
            private_topic: 403,
            deleted_topic: 410,
            deleted_secure_topic: 403,
            deleted_private_topic: 403,
            nonexistent: 404,
            secure_accessible_topic: 403
          }
          include_examples "various scenarios", expected
        end

        context 'allowed user' do
          before do
            sign_in(allowed_user)
          end

          expected = {
            normal_topic: 200,
            secure_topic: 200,
            private_topic: 200,
            deleted_topic: 410,
            deleted_secure_topic: 410,
            deleted_private_topic: 410,
            nonexistent: 404,
            secure_accessible_topic: 403
          }
          include_examples "various scenarios", expected
        end

        context 'moderator' do
          before do
            sign_in(moderator)
          end

          expected = {
            normal_topic: 200,
            secure_topic: 403,
            private_topic: 403,
            deleted_topic: 200,
            deleted_secure_topic: 403,
            deleted_private_topic: 403,
            nonexistent: 404,
            secure_accessible_topic: 403
          }
          include_examples "various scenarios", expected
        end

        context 'admin' do
          before do
            sign_in(admin)
          end

          expected = {
            normal_topic: 200,
            secure_topic: 200,
            private_topic: 200,
            deleted_topic: 200,
            deleted_secure_topic: 200,
            deleted_private_topic: 200,
            nonexistent: 404,
            secure_accessible_topic: 200
          }
          include_examples "various scenarios", expected
        end
      end

    end

    it 'records a view' do
      expect do
        get "/t/#{topic.slug}/#{topic.id}.json"
      end.to change(TopicViewItem, :count).by(1)
    end

    it 'records a view to invalid post_number' do
      expect do
        get "/t/#{topic.slug}/#{topic.id}/#{256**4}", params: {
          u: user.username
        }
        expect(response.status).to eq(200)
      end.to change { IncomingLink.count }.by(1)

    end

    it 'records incoming links' do
      expect do
        get "/t/#{topic.slug}/#{topic.id}", params: {
          u: user.username
        }
      end.to change { IncomingLink.count }.by(1)
    end

    context 'print' do
      it "doesn't renders the print view when disabled" do
        SiteSetting.max_prints_per_hour_per_user = 0

        get "/t/#{topic.slug}/#{topic.id}/print"

        expect(response).to be_forbidden
      end

      it 'renders the print view when enabled' do
        SiteSetting.max_prints_per_hour_per_user = 10
        get "/t/#{topic.slug}/#{topic.id}/print", headers: { HTTP_USER_AGENT: "Rails Testing" }

        expect(response.status).to eq(200)
        body = response.body

        expect(body).to have_tag(:body, class: 'crawler')
        expect(body).to_not have_tag(:meta, with: { name: 'fragment' })
      end

      it "uses the application layout when there's no param" do
        SiteSetting.max_prints_per_hour_per_user = 10
        get "/t/#{topic.slug}/#{topic.id}", headers: { HTTP_USER_AGENT: "Rails Testing" }

        body = response.body

        expect(body).to have_tag(:script, src: '/assets/application.js')
        expect(body).to have_tag(:meta, with: { name: 'fragment' })
      end
    end

    it 'records redirects' do
      get "/t/#{topic.id}", headers: { HTTP_REFERER: "http://twitter.com" }
      get "/t/#{topic.slug}/#{topic.id}", headers: { HTTP_REFERER: nil }

      link = IncomingLink.first
      expect(link.referer).to eq('http://twitter.com')
    end

    it 'tracks a visit for all html requests' do
      sign_in(user)
      get "/t/#{topic.slug}/#{topic.id}"
      topic_user = TopicUser.where(user: user, topic: topic).first
      expect(topic_user.last_visited_at).to eq_time(topic_user.first_visited_at)
    end

    context 'consider for a promotion' do
      before do
        SiteSetting.tl1_requires_topics_entered = 0
        SiteSetting.tl1_requires_read_posts = 0
        SiteSetting.tl1_requires_time_spent_mins = 0
        SiteSetting.tl1_requires_time_spent_mins = 0
      end

      it "reviews the user for a promotion if they're new" do
        sign_in(user)
        user.update_column(:trust_level, TrustLevel[0])
        get "/t/#{topic.slug}/#{topic.id}.json"
        user.reload
        expect(user.trust_level).to eq(1)
      end
    end

    context 'filters' do
      def extract_post_stream
        json = response.parsed_body
        json["post_stream"]["posts"].map { |post| post["id"] }
      end

      before do
        TopicView.stubs(:chunk_size).returns(2)
        @post_ids = topic.posts.pluck(:id)
        3.times do
          @post_ids << Fabricate(:post, topic: topic).id
        end
      end

      it 'grabs the correct set of posts' do
        get "/t/#{topic.slug}/#{topic.id}.json"
        expect(response.status).to eq(200)
        expect(extract_post_stream).to eq(@post_ids[0..1])

        get "/t/#{topic.slug}/#{topic.id}.json", params: { page: 1 }
        expect(response.status).to eq(200)
        expect(extract_post_stream).to eq(@post_ids[0..1])

        get "/t/#{topic.slug}/#{topic.id}.json", params: { page: 2 }
        expect(response.status).to eq(200)
        expect(extract_post_stream).to eq(@post_ids[2..3])

        post_number = topic.posts.pluck(:post_number).sort[3]
        get "/t/#{topic.slug}/#{topic.id}/#{post_number}.json"
        expect(response.status).to eq(200)
        expect(extract_post_stream).to eq(@post_ids[-2..-1])

        TopicView.stubs(:chunk_size).returns(3)

        get "/t/#{topic.slug}/#{topic.id}.json", params: { page: 1 }
        expect(response.status).to eq(200)
        expect(extract_post_stream).to eq(@post_ids[0..2])

        get "/t/#{topic.slug}/#{topic.id}.json", params: { page: 2 }
        expect(response.status).to eq(200)
        expect(extract_post_stream).to eq(@post_ids[3..3])

        get "/t/#{topic.slug}/#{topic.id}.json", params: { page: 3 }
        expect(response.status).to eq(404)

        TopicView.stubs(:chunk_size).returns(4)

        get "/t/#{topic.slug}/#{topic.id}.json", params: { page: 1 }
        expect(response.status).to eq(200)
        expect(extract_post_stream).to eq(@post_ids[0..3])

        get "/t/#{topic.slug}/#{topic.id}.json", params: { page: 2 }
        expect(response.status).to eq(404)
      end
    end

    describe '#show filters' do
      fab!(:post) { Fabricate(:post) }
      fab!(:topic) { post.topic }
      fab!(:post2) { Fabricate(:post, topic: topic) }

      describe 'filter by replies to a post' do
        fab!(:post3) { Fabricate(:post, topic: topic, reply_to_post_number: post2.post_number) }
        fab!(:post4) { Fabricate(:post, topic: topic, reply_to_post_number: post2.post_number) }
        fab!(:post5) { Fabricate(:post, topic: topic) }
        fab!(:quote_reply) { Fabricate(:basic_reply, user: user, topic: topic) }
        fab!(:post_reply) { PostReply.create(post_id: post2.id, reply_post_id: quote_reply.id) }

        it 'should return the right posts' do
          get "/t/#{topic.id}.json", params: {
            replies_to_post_number: post2.post_number
          }

          expect(response.status).to eq(200)

          body = response.parsed_body

          expect(body.has_key?("suggested_topics")).to eq(false)
          expect(body.has_key?("related_messages")).to eq(false)

          ids = body["post_stream"]["posts"].map { |p| p["id"] }
          expect(ids).to eq([post.id, post2.id, post3.id, post4.id, quote_reply.id])
        end
      end

      describe 'filter upwards by post id' do
        fab!(:post3) { Fabricate(:post, topic: topic) }
        fab!(:post4) { Fabricate(:post, topic: topic, reply_to_post_number: post3.post_number) }
        fab!(:post5) { Fabricate(:post, topic: topic, reply_to_post_number: post4.post_number) }
        fab!(:post6) { Fabricate(:post, topic: topic) }

        it 'should return the right posts' do
          get "/t/#{topic.id}.json", params: {
            filter_upwards_post_id: post5.id
          }

          expect(response.status).to eq(200)

          body = response.parsed_body

          expect(body.has_key?("suggested_topics")).to eq(false)
          expect(body.has_key?("related_messages")).to eq(false)

          ids = body["post_stream"]["posts"].map { |p| p["id"] }
          # includes topic OP, current post and subsequent posts
          # but only one level of parents, respecting default max_reply_history = 1
          expect(ids).to eq([post.id, post4.id, post5.id, post6.id])
        end

        it 'should respect max_reply_history site setting' do
          SiteSetting.max_reply_history = 2

          get "/t/#{topic.id}.json", params: {
            filter_upwards_post_id: post5.id
          }

          expect(response.status).to eq(200)

          body = response.parsed_body
          ids = body["post_stream"]["posts"].map { |p| p["id"] }

          # includes 2 levels of replies (post3 and post4)
          expect(ids).to eq([post.id, post3.id, post4.id, post5.id, post6.id])
        end
      end

    end

    context "when 'login required' site setting has been enabled" do
      before { SiteSetting.login_required = true }

      context 'and the user is logged in' do
        before { sign_in(Fabricate(:coding_horror)) }

        it 'shows the topic' do
          get "/t/#{topic.slug}/#{topic.id}.json"
          expect(response.status).to eq(200)
        end
      end

      context 'and the user is not logged in' do
        let(:api_key) { Fabricate(:api_key, user: topic.user) }

        it 'redirects to the login page' do
          get "/t/#{topic.slug}/#{topic.id}.json"

          expect(response).to redirect_to login_path
        end

        it 'shows the topic if valid api key is provided' do
          get "/t/#{topic.slug}/#{topic.id}.json", headers: { "HTTP_API_KEY" => api_key.key }

          expect(response.status).to eq(200)
          topic.reload
          expect(topic.views).to eq(1)
        end

        it 'returns 403 for an invalid key' do
          [:json, :html].each do |format|
            get "/t/#{topic.slug}/#{topic.id}.#{format}", headers: { "HTTP_API_KEY" => "bad" }

            expect(response.code.to_i).to eq(403)
            expect(response.body).to include(I18n.t("invalid_access"))
          end
        end
      end
    end

    it "is included for unlisted topics" do
      topic = Fabricate(:topic, visible: false)
      get "/t/#{topic.slug}/#{topic.id}.json"

      expect(response.headers['X-Robots-Tag']).to eq('noindex')
    end

    it "is not included for normal topics" do
      get "/t/#{topic.slug}/#{topic.id}.json"

      expect(response.headers['X-Robots-Tag']).to eq(nil)
    end

    it "is included when allow_index_in_robots_txt is set to false" do
      SiteSetting.allow_index_in_robots_txt = false

      get "/t/#{topic.slug}/#{topic.id}.json"

      expect(response.headers['X-Robots-Tag']).to eq('noindex, nofollow')
    end

    it "doesn't store an incoming link when there's no referer" do
      expect {
        get "/t/#{topic.id}.json"
      }.not_to change(IncomingLink, :count)
      expect(response.status).to eq(200)
    end

    it "doesn't raise an error on a very long link" do
      get "/t/#{topic.id}.json", headers: { HTTP_REFERER: "http://#{'a' * 2000}.com" }
      expect(response.status).to eq(200)
    end

    describe "has_escaped_fragment?" do
      context "when the SiteSetting is disabled" do
        it "uses the application layout even with an escaped fragment param" do
          SiteSetting.enable_escaped_fragments = false

          get "/t/#{topic.slug}/#{topic.id}", params: {
            _escaped_fragment_: 'true'
          }

          body = response.body

          expect(response.status).to eq(200)
          expect(body).to have_tag(:script, with: { src: '/assets/application.js' })
          expect(body).to_not have_tag(:meta, with: { name: 'fragment' })
        end
      end

      context "when the SiteSetting is enabled" do
        before do
          SiteSetting.enable_escaped_fragments = true
        end

        it "uses the application layout when there's no param" do
          get "/t/#{topic.slug}/#{topic.id}"

          body = response.body

          expect(body).to have_tag(:script, with: { src: '/assets/application.js' })
          expect(body).to have_tag(:meta, with: { name: 'fragment' })
        end

        it "uses the crawler layout when there's an _escaped_fragment_ param" do
          get "/t/#{topic.slug}/#{topic.id}", params: {
            _escaped_fragment_: true
          }, headers: { HTTP_USER_AGENT: "Rails Testing" }

          body = response.body

          expect(response.status).to eq(200)
          expect(body).to have_tag(:body, with: { class: 'crawler' })
          expect(body).to_not have_tag(:meta, with: { name: 'fragment' })
        end
      end
    end

    describe 'clear_notifications' do
      it 'correctly clears notifications if specified via cookie' do
        set_subfolder "/eviltrout"

        notification = Fabricate(:notification)
        sign_in(notification.user)

        cookies['cn'] = "2828,100,#{notification.id}"

        get "/t/#{topic.id}.json"

        expect(response.status).to eq(200)
        expect(response.cookies['cn']).to eq(nil)
        expect(response.headers['Set-Cookie']).to match(/^cn=;.*path=\/eviltrout/)

        notification.reload
        expect(notification.read).to eq(true)
      end

      it 'correctly clears notifications if specified via header' do
        notification = Fabricate(:notification)
        sign_in(notification.user)

        get "/t/#{topic.id}.json", headers: { "Discourse-Clear-Notifications" => "2828,100,#{notification.id}" }

        expect(response.status).to eq(200)
        notification.reload
        expect(notification.read).to eq(true)
      end
    end

    describe "read only header" do
      it "returns no read only header by default" do
        get "/t/#{topic.id}.json"
        expect(response.status).to eq(200)
        expect(response.headers['Discourse-Readonly']).to eq(nil)
      end

      it "returns a readonly header if the site is read only" do
        Discourse.received_postgres_readonly!
        get "/t/#{topic.id}.json"
        expect(response.status).to eq(200)
        expect(response.headers['Discourse-Readonly']).to eq('true')
      end
    end

    describe "image only topic" do
      it "uses image alt tag for meta description" do
        post = Fabricate(:post, raw: "![image_description|690x405](upload://sdtr5O5xaxf0iEOxICxL36YRj86.png)")

        get post.topic.url

        body = response.body
        expect(body).to have_tag(:meta, with: { name: 'description', content: '[image_description]' })
      end
    end
  end

  describe '#post_ids' do
    fab!(:post) { Fabricate(:post) }
    fab!(:topic) { post.topic }

    before do
      TopicView.stubs(:chunk_size).returns(1)
    end

    it 'returns the right post ids' do
      post2 = Fabricate(:post, topic: topic)
      post3 = Fabricate(:post, topic: topic)

      get "/t/#{topic.id}/post_ids.json", params: {
        post_number: post.post_number
      }

      expect(response.status).to eq(200)

      body = response.parsed_body

      expect(body["post_ids"]).to eq([post2.id, post3.id])
    end

    describe 'filtering by post number with filters' do
      describe 'username filters' do
        fab!(:post) { Fabricate(:post, user: user) }
        fab!(:post2) { Fabricate(:post, topic: topic, user: user) }
        fab!(:post3) { Fabricate(:post, topic: topic) }

        it 'should return the right posts' do
          get "/t/#{topic.id}/post_ids.json", params: {
            post_number: post.post_number,
            username_filters: post2.user.username
          }

          expect(response.status).to eq(200)

          body = response.parsed_body

          expect(body["post_ids"]).to eq([post2.id])
        end
      end

      describe 'summary filter' do
        fab!(:post2) { Fabricate(:post, topic: topic, percent_rank: 0.2) }
        fab!(:post3) { Fabricate(:post, topic: topic) }

        it 'should return the right posts' do
          get "/t/#{topic.id}/post_ids.json", params: {
            post_number: post.post_number,
            filter: 'summary'
          }

          expect(response.status).to eq(200)

          body = response.parsed_body

          expect(body["post_ids"]).to eq([post2.id])
        end
      end

      describe 'custom filters' do
        fab!(:post2) { Fabricate(:post, topic: topic, percent_rank: 0.2) }
        fab!(:post3) { Fabricate(:post, topic: topic, percent_rank: 0.5) }
        it 'should return the right posts' do
          TopicView.add_custom_filter("percent") do |posts, topic_view|
            posts.where(percent_rank: 0.5)
          end

          get "/t/#{topic.id}.json", params: {
            post_number: post.post_number,
            filter: 'percent'
          }

          expect(response.status).to eq(200)

          body = response.parsed_body

          expect(body["post_stream"]["posts"].map { |p| p["id"] }).to eq([post3.id])
        ensure
          TopicView.instance_variable_set(:@custom_filters, {})
        end
      end
    end
  end

  describe '#posts' do
    fab!(:post) { Fabricate(:post) }
    fab!(:topic) { post.topic }

    after do
      Discourse.redis.flushdb
    end

    it 'returns first post of the topic' do
      # we need one for suggested
      create_post

      get "/t/#{topic.id}/posts.json"

      expect(response.status).to eq(200)

      body = response.parsed_body

      expect(body["post_stream"]["posts"].first["id"]).to eq(post.id)

      expect(body["suggested_topics"]).to eq(nil)

      get "/t/#{topic.id}/posts.json?include_suggested=true"
      body = response.parsed_body

      expect(body["suggested_topics"]).not_to eq(nil)
    end

    describe 'filtering by post number with filters' do
      describe 'username filters' do
        fab!(:post2) { Fabricate(:post, topic: topic, user: user) }
        fab!(:post3) { Fabricate(:post, topic: topic) }

        it 'should return the right posts' do
          TopicView.stubs(:chunk_size).returns(2)

          get "/t/#{topic.id}/posts.json", params: {
            post_number: post.post_number,
            username_filters: post2.user.username,
            asc: true
          }

          expect(response.status).to eq(200)

          body = response.parsed_body

          expect(body["post_stream"]["posts"].first["id"]).to eq(post2.id)
        end
      end

      describe 'summary filter' do
        fab!(:post2) { Fabricate(:post, topic: topic, percent_rank: 0.2) }
        fab!(:post3) { Fabricate(:post, topic: topic) }

        it 'should return the right posts' do
          TopicView.stubs(:chunk_size).returns(2)

          get "/t/#{topic.id}/posts.json", params: {
            post_number: post.post_number,
            filter: 'summary',
            asc: true
          }

          expect(response.status).to eq(200)

          body = response.parsed_body

          expect(body["post_stream"]["posts"].first["id"]).to eq(post2.id)
        end
      end
    end
  end

  describe '#feed' do
    fab!(:topic) { Fabricate(:post).topic }

    it 'renders rss of the topic' do
      get "/t/foo/#{topic.id}.rss"
      expect(response.status).to eq(200)
      expect(response.media_type).to eq('application/rss+xml')
    end

    it 'renders rss of the topic correctly with subfolder' do
      set_subfolder "/forum"
      get "/t/foo/#{topic.id}.rss"
      expect(response.status).to eq(200)
      expect(response.body).to_not include("/forum/forum")
      expect(response.body).to include("http://test.localhost/forum/t/#{topic.slug}")
    end

    it 'returns 404 when posts are deleted' do
      topic.posts.each(&:trash!)
      get "/t/foo/#{topic.id}.rss"
      expect(response.status).to eq(404)
    end

    it 'returns 404 when the topic is deleted' do
      topic.trash!
      get "/t/foo/#{topic.id}.rss"
      expect(response.status).to eq(404)
    end
  end

  describe '#invite_notify' do
    let(:user2) { Fabricate(:user) }

    it 'does not notify same user multiple times' do
      sign_in(user)

      expect { post "/t/#{topic.id}/invite-notify.json", params: { usernames: [user2.username] } }
        .to change { Notification.count }.by(1)
      expect(response.status).to eq(200)

      expect { post "/t/#{topic.id}/invite-notify.json", params: { usernames: [user2.username] } }
        .to change { Notification.count }.by(0)
      expect(response.status).to eq(200)

      freeze_time 1.day.from_now

      expect { post "/t/#{topic.id}/invite-notify.json", params: { usernames: [user2.username] } }
        .to change { Notification.count }.by(1)
      expect(response.status).to eq(200)
    end

    it 'does not let regular users to notify multiple users' do
      sign_in(user)

      expect { post "/t/#{topic.id}/invite-notify.json", params: { usernames: [admin.username, user2.username] } }
        .to change { Notification.count }.by(0)
      expect(response.status).to eq(400)
    end

    it 'lets staff to notify multiple users' do
      sign_in(admin)

      expect { post "/t/#{topic.id}/invite-notify.json", params: { usernames: [user.username, user2.username] } }
        .to change { Notification.count }.by(2)
      expect(response.status).to eq(200)
    end
  end

  describe '#invite_group' do
    let!(:admins) { Group[:admins] }

    before do
      sign_in(admin)
      admins.messageable_level = Group::ALIAS_LEVELS[:everyone]
      admins.save!
    end

    it "disallows inviting a group to a topic" do
      topic = Fabricate(:topic)
      post "/t/#{topic.id}/invite-group.json", params: {
        group: 'admins'
      }

      expect(response.status).to eq(422)
    end

    it "allows inviting a group to a PM" do
      topic = Fabricate(:private_message_topic)
      post "/t/#{topic.id}/invite-group.json", params: {
        group: 'admins'
      }

      expect(response.status).to eq(200)
      expect(topic.allowed_groups.first.id).to eq(admins.id)
    end
  end

  describe '#make_banner' do
    it 'needs you to be a staff member' do
      topic = Fabricate(:topic, user: sign_in(trust_level_4))
      put "/t/#{topic.id}/make-banner.json"
      expect(response).to be_forbidden
    end

    describe 'when logged in' do
      it "changes the topic archetype to 'banner'" do
        topic = Fabricate(:topic, user: sign_in(admin))

        put "/t/#{topic.id}/make-banner.json"
        expect(response.status).to eq(200)
        topic.reload
        expect(topic.archetype).to eq(Archetype.banner)
      end
    end
  end

  describe '#remove_banner' do
    it 'needs you to be a staff member' do
      topic = Fabricate(:topic, user: sign_in(trust_level_4), archetype: Archetype.banner)
      put "/t/#{topic.id}/remove-banner.json"
      expect(response).to be_forbidden
    end

    describe 'when logged in' do
      it "resets the topic archetype" do
        topic = Fabricate(:topic, user: sign_in(admin), archetype: Archetype.banner)

        put "/t/#{topic.id}/remove-banner.json"
        expect(response.status).to eq(200)
        topic.reload
        expect(topic.archetype).to eq(Archetype.default)
      end
    end
  end

  describe '#remove_allowed_user' do
    it 'admin can be removed from a pm' do
      sign_in(admin)
      pm = create_post(user: user, archetype: 'private_message', target_usernames: [user.username, admin.username])

      put "/t/#{pm.topic_id}/remove-allowed-user.json", params: {
        username: admin.username
      }

      expect(response.status).to eq(200)
      expect(TopicAllowedUser.where(topic_id: pm.topic_id, user_id: admin.id).first).to eq(nil)
    end
  end

  describe '#bulk' do
    it 'needs you to be logged in' do
      put "/topics/bulk.json"
      expect(response.status).to eq(403)
    end

    describe "when logged in" do
      fab!(:tag) { Fabricate(:tag) }

      before { sign_in(user) }
      let!(:operation) { { type: 'change_category', category_id: '1' } }
      let!(:topic_ids) { [1, 2, 3] }

      it "requires a list of topic_ids or filter" do
        put "/topics/bulk.json", params: { operation: operation }
        expect(response.status).to eq(400)
      end

      it "requires an operation param" do
        put "/topics/bulk.json", params: { topic_ids: topic_ids }
        expect(response.status).to eq(400)
      end

      it "requires a type field for the operation param" do
        put "/topics/bulk.json", params: { topic_ids: topic_ids, operation: {} }
        expect(response.status).to eq(400)
      end

      it "can mark sub-categories unread" do
        sub = Fabricate(:category, parent_category_id: category.id)

        topic.update!(category_id: sub.id)

        post1 = create_post(user: user, topic_id: topic.id)
        create_post(topic_id: topic.id)

        put "/topics/bulk.json", params: {
          category_id: category.id,
          include_subcategories: true,
          filter: 'unread',
          operation: { type: 'dismiss_posts' }
        }

        expect(response.status).to eq(200)
        expect(TopicUser.get(post1.topic, post1.user).last_read_post_number).to eq(2)
      end

      it "can mark tag topics unread" do
        TopicTag.create!(
          topic_id: topic.id,
          tag_id: tag.id
        )

        post1 = create_post(user: user, topic_id: topic.id)
        create_post(topic_id: topic.id)

        put "/topics/bulk.json", params: {
          tag_name: tag.name,
          filter: 'unread',
          operation: { type: 'dismiss_posts' }
        }

        expect(response.status).to eq(200)
        expect(TopicUser.get(post1.topic, post1.user).last_read_post_number).to eq(2)
      end

      it "can find unread" do
        # mark all unread muted
        put "/topics/bulk.json", params: {
          filter: 'unread', operation: { type: :change_notification_level, notification_level_id: 0 }
        }

        expect(response.status).to eq(200)
      end

      it "delegates work to `TopicsBulkAction`" do
        topics_bulk_action = mock
        TopicsBulkAction.expects(:new).with(user, topic_ids, operation, group: nil).returns(topics_bulk_action)
        topics_bulk_action.expects(:perform!)

        put "/topics/bulk.json", params: {
          topic_ids: topic_ids, operation: operation
        }
      end

      it "raises an error if topic_ids is provided and it is not an array" do
        put "/topics/bulk.json", params: {
          topic_ids: "1", operation: operation
        }
        expect(response.parsed_body["errors"].first).to match(/Expecting topic_ids to contain a list/)
        put "/topics/bulk.json", params: {
          topic_ids: [1], operation: operation
        }
        expect(response.parsed_body["errors"]).to eq(nil)
      end

      it "respects the tracked parameter" do
        # untracked topic
        CategoryUser.set_notification_level_for_category(user,
                                                     NotificationLevels.all[:regular],
                                                     category.id)
        create_post(user: user, topic_id: topic.id)
        topic.update!(category_id: category.id)
        create_post(topic_id: topic.id)

        # tracked topic
        tracked_category = Fabricate(:category)
        CategoryUser.set_notification_level_for_category(user,
                                                     NotificationLevels.all[:tracking],
                                                     tracked_category.id)
        tracked_topic = create_post(user: user).topic
        tracked_topic.update!(category_id: tracked_category.id)
        create_post(topic_id: tracked_topic.id)

        put "/topics/bulk.json", params: {
          filter: 'unread',
          operation: { type: 'dismiss_posts' },
          tracked: true
        }

        expect(response.status).to eq(200)
        expect(TopicUser.get(topic, user).last_read_post_number).to eq(topic.posts.count - 1)
        expect(TopicUser.get(tracked_topic, user).last_read_post_number).to eq(tracked_topic.posts.count)
      end
    end
  end

  describe '#remove_bookmarks' do
    it "should remove bookmarks properly from non first post" do
      sign_in(user)

      post = create_post
      post2 = create_post(topic_id: post.topic_id)
      Fabricate(:bookmark, user: user, post: post)
      Fabricate(:bookmark, user: user, post: post2)

      put "/t/#{post.topic_id}/remove_bookmarks.json"
      expect(Bookmark.where(user: user).count).to eq(0)
    end

    it "should disallow bookmarks on posts you have no access to" do
      sign_in(Fabricate(:user))
      pm = create_post(user: user, archetype: 'private_message', target_usernames: [user.username])

      put "/t/#{pm.topic_id}/bookmark.json"
      expect(response).to be_forbidden
    end

    context "bookmarks with reminders" do
      it "deletes all the bookmarks for the user in the topic" do
        sign_in(user)
        post = create_post
        Fabricate(:bookmark, post: post, topic: post.topic, user: user)
        put "/t/#{post.topic_id}/remove_bookmarks.json"
        expect(Bookmark.where(user: user, topic: topic).count).to eq(0)
      end
    end
  end

  describe "#bookmark" do
    before do
      sign_in(user)
    end

    it "should create a new bookmark on the first post of the topic" do
      post = create_post
      post2 = create_post(topic_id: post.topic_id)
      put "/t/#{post.topic_id}/bookmark.json"

      expect(Bookmark.find_by(user_id: user.id).post_id).to eq(post.id)
    end

    it "errors if the topic is already bookmarked for the user" do
      post = create_post
      Bookmark.create(post: post, user: user, topic: post.topic)

      put "/t/#{post.topic_id}/bookmark.json"
      expect(response.status).to eq(400)
    end

    context "bookmarks with reminders" do
      it "should create a new bookmark on the first post of the topic" do
        post = create_post
        post2 = create_post(topic_id: post.topic_id)
        put "/t/#{post.topic_id}/bookmark.json"
        expect(response.status).to eq(200)

        bookmarks_for_topic = Bookmark.where(topic: post.topic, user: user)
        expect(bookmarks_for_topic.count).to eq(1)
        expect(bookmarks_for_topic.first.post_id).to eq(post.id)
      end

      it "errors if the topic is already bookmarked for the user" do
        post = create_post
        Bookmark.create(post: post, topic: post.topic, user: user)

        put "/t/#{post.topic_id}/bookmark.json"
        expect(response.status).to eq(400)
      end
    end
  end

  describe '#reset_new' do
    it 'needs you to be logged in' do
      put "/topics/reset-new.json"
      expect(response.status).to eq(403)
    end

    it "updates the `new_since` date" do
      sign_in(user)

      old_date = 2.years.ago
      user.user_stat.update_column(:new_since, old_date)
      user.update_column(:created_at, old_date)

      TopicTrackingState.expects(:publish_dismiss_new).with(user.id, topic_ids: [topic.id])

      put "/topics/reset-new.json"
      expect(response.status).to eq(200)
      user.reload
      expect(user.user_stat.new_since.to_date).not_to eq(old_date.to_date)
    end

    describe "when tracked param is true" do
      it "does not update user_stat.new_since" do
        sign_in(user)

        old_date = 2.years.ago
        user.user_stat.update_column(:new_since, old_date)

        put "/topics/reset-new.json?tracked=true"
        expect(response.status).to eq(200)
        user.reload
        expect(user.user_stat.new_since.to_date).to eq(old_date.to_date)
      end

      it "creates dismissed topic user records for each new topic" do
        sign_in(user)
        user.user_stat.update_column(:new_since, 2.years.ago)

        tracked_category = Fabricate(:category)
        CategoryUser.set_notification_level_for_category(user,
                                                     NotificationLevels.all[:tracking],
                                                     tracked_category.id)
        tracked_topic = create_post(category: tracked_category).topic

        create_post # This is a new post, but is not tracked so a record will not be created for it
        expect do
          put "/topics/reset-new.json?tracked=true"
        end.to change {
          DismissedTopicUser.where(user_id: user.id, topic_id: tracked_topic.id).count
        }.by(1)
      end

      it "creates dismissed topic user records if there are > 30 (default pagination) topics" do
        sign_in(user)
        tracked_category = Fabricate(:category)
        CategoryUser.set_notification_level_for_category(user,
                                                     NotificationLevels.all[:tracking],
                                                     tracked_category.id)

        topic_ids = []
        5.times do
          topic_ids << create_post(category: tracked_category).topic.id
        end

        expect do
          stub_const(TopicQuery, "DEFAULT_PER_PAGE_COUNT", 2) do
            put "/topics/reset-new.json?tracked=true"
          end
        end.to change {
          DismissedTopicUser.where(user_id: user.id, topic_id: topic_ids).count
        }.by(5)
      end

      it "creates dismissed topic user records if there are > 30 (default pagination) topics and topic_ids are provided" do
        sign_in(user)
        tracked_category = Fabricate(:category)
        CategoryUser.set_notification_level_for_category(user,
                                                     NotificationLevels.all[:tracking],
                                                     tracked_category.id)

        topic_ids = []
        5.times do
          topic_ids << create_post(category: tracked_category).topic.id
        end
        dismissing_topic_ids = topic_ids.sample(4)

        expect do
          stub_const(TopicQuery, "DEFAULT_PER_PAGE_COUNT", 2) do
            put "/topics/reset-new.json?tracked=true", params: { topic_ids: dismissing_topic_ids }
          end
        end.to change {
          DismissedTopicUser.where(user_id: user.id, topic_id: topic_ids).count
        }.by(4)
      end
    end

    context 'category' do
      fab!(:subcategory) { Fabricate(:category, parent_category_id: category.id) }
      fab!(:category_topic) { Fabricate(:topic, category: category) }
      fab!(:subcategory_topic) { Fabricate(:topic, category: subcategory) }

      it 'dismisses topics for main category' do
        sign_in(user)

        TopicTrackingState.expects(:publish_dismiss_new).with(user.id, topic_ids: [category_topic.id])

        put "/topics/reset-new.json?category_id=#{category.id}"

        expect(DismissedTopicUser.where(user_id: user.id).pluck(:topic_id)).to eq([category_topic.id])
      end

      it 'dismisses topics for main category and subcategories' do
        sign_in(user)

        TopicTrackingState.expects(:publish_dismiss_new).with(user.id, topic_ids: [category_topic.id, subcategory_topic.id])

        put "/topics/reset-new.json?category_id=#{category.id}&include_subcategories=true"

        expect(DismissedTopicUser.where(user_id: user.id).pluck(:topic_id).sort).to eq([category_topic.id, subcategory_topic.id].sort)
      end
    end

    context 'tag' do
      fab!(:tag) { Fabricate(:tag) }
      fab!(:tag_topic) { Fabricate(:topic) }
      fab!(:topic_tag) { Fabricate(:topic_tag, topic: tag_topic, tag: tag) }
      fab!(:topic) { Fabricate(:topic) }

      it 'dismisses topics for tag' do
        sign_in(user)
        TopicTrackingState.expects(:publish_dismiss_new).with(user.id, topic_ids: [tag_topic.id])
        put "/topics/reset-new.json?tag_id=#{tag.name}"
        expect(DismissedTopicUser.where(user_id: user.id).pluck(:topic_id)).to eq([tag_topic.id])
      end
    end

    context 'tag and category' do
      fab!(:tag) { Fabricate(:tag) }
      fab!(:tag_topic) { Fabricate(:topic) }
      fab!(:tag_and_category_topic) { Fabricate(:topic, category: category) }
      fab!(:topic_tag) { Fabricate(:topic_tag, topic: tag_topic, tag: tag) }
      fab!(:topic_tag2) { Fabricate(:topic_tag, topic: tag_and_category_topic, tag: tag) }

      it 'dismisses topics for tag' do
        sign_in(user)
        TopicTrackingState.expects(:publish_dismiss_new).with(user.id, topic_ids: [tag_and_category_topic.id])
        put "/topics/reset-new.json?tag_id=#{tag.name}&category_id=#{category.id}"
        expect(DismissedTopicUser.where(user_id: user.id).pluck(:topic_id)).to eq([tag_and_category_topic.id])
      end
    end

    context "specific topics" do
      fab!(:topic2) { Fabricate(:topic) }
      fab!(:topic3) { Fabricate(:topic) }

      it "updates the `new_since` date" do
        sign_in(user)

        old_date = 2.years.ago
        user.user_stat.update_column(:new_since, old_date)
        user.update_column(:created_at, old_date)

        TopicTrackingState.expects(:publish_dismiss_new).with(user.id, topic_ids: [topic2.id, topic3.id]).at_least_once

        put "/topics/reset-new.json", { params: { topic_ids: [topic2.id, topic3.id] } }
        expect(response.status).to eq(200)
        user.reload
        expect(user.user_stat.new_since.to_date).not_to eq(old_date.to_date)
        expect(DismissedTopicUser.where(user_id: user.id).pluck(:topic_id)).to match_array([topic2.id, topic3.id])
      end

      it "raises an error if topic_ids is provided and it is not an array" do
        sign_in(user)
        put "/topics/reset-new.json", params: { topic_ids: topic2.id }
        expect(response.parsed_body["errors"].first).to match(/Expecting topic_ids to contain a list/)
        put "/topics/reset-new.json", params: { topic_ids: [topic2.id] }
        expect(response.parsed_body["errors"]).to eq(nil)
      end

      describe "when tracked param is true" do
        it "does not update user_stat.new_since and does not dismiss untracked topics" do
          sign_in(user)

          old_date = 2.years.ago
          user.user_stat.update_column(:new_since, old_date)

          put "/topics/reset-new.json?tracked=true", { params: { topic_ids: [topic2.id, topic3.id] } }
          expect(response.status).to eq(200)
          user.reload
          expect(user.user_stat.new_since.to_date).to eq(old_date.to_date)
          expect(DismissedTopicUser.where(user_id: user.id).pluck(:topic_id)).to be_empty
        end

        it "creates topic user records for each unread topic" do
          sign_in(user)
          user.user_stat.update_column(:new_since, 2.years.ago)

          tracked_category = Fabricate(:category)
          CategoryUser.set_notification_level_for_category(user,
                                                           NotificationLevels.all[:tracking],
                                                           tracked_category.id)
          tracked_topic = create_post.topic
          tracked_topic.update!(category_id: tracked_category.id)
          topic2.update!(category_id: tracked_category.id)

          create_post # This is a new post, but is not tracked so a record will not be created for it
          expect do
            put "/topics/reset-new.json?tracked=true", { params: { topic_ids: [tracked_topic.id, topic2.id, topic3.id] } }
          end.to change { DismissedTopicUser.where(user_id: user.id).count }.by(2)
          expect(DismissedTopicUser.where(user_id: user.id).pluck(:topic_id)).to match_array([tracked_topic.id, topic2.id])
        end
      end
    end
  end

  describe '#feature_stats' do
    it "works" do
      get "/topics/feature_stats.json", params: { category_id: 1 }

      expect(response.status).to eq(200)
      json = response.parsed_body
      expect(json["pinned_in_category_count"]).to eq(0)
      expect(json["pinned_globally_count"]).to eq(0)
      expect(json["banner_count"]).to eq(0)
    end

    it "allows unlisted banner topic" do
      Fabricate(:topic, category_id: 1, archetype: Archetype.banner, visible: false)

      get "/topics/feature_stats.json", params: { category_id: 1 }
      json = response.parsed_body
      expect(json["banner_count"]).to eq(1)
    end
  end

  describe '#excerpts' do
    it "can correctly get excerpts" do
      first_post = create_post(raw: 'This is the first post :)', title: 'This is a test title I am making yay')
      second_post = create_post(raw: 'This is second post', topic: first_post.topic)

      random_post = Fabricate(:post)

      get "/t/#{first_post.topic_id}/excerpts.json", params: {
        post_ids: [first_post.id, second_post.id, random_post.id]
      }

      json = response.parsed_body
      json.sort! { |a, b| a["post_id"] <=> b["post_id"] }

      # no random post
      expect(json.length).to eq(2)
      # keep emoji images
      expect(json[0]["excerpt"]).to match(/emoji/)
      expect(json[0]["excerpt"]).to match(/first post/)
      expect(json[0]["username"]).to eq(first_post.user.username)
      expect(json[0]["post_id"]).to eq(first_post.id)

      expect(json[1]["excerpt"]).to match(/second post/)
    end
  end

  describe '#convert_topic' do
    it 'needs you to be logged in' do
      put "/t/111/convert-topic/private.json"
      expect(response.status).to eq(403)
    end

    describe 'converting public topic to private message' do
      fab!(:topic) { Fabricate(:topic, user: user) }
      fab!(:post) { Fabricate(:post, topic: topic) }

      it "raises an error when the user doesn't have permission to convert topic" do
        sign_in(user)
        put "/t/#{topic.id}/convert-topic/private.json"
        expect(response).to be_forbidden
      end

      context "success" do
        it "returns success" do
          sign_in(admin)
          put "/t/#{topic.id}/convert-topic/private.json"

          topic.reload
          expect(topic.archetype).to eq(Archetype.private_message)
          expect(response.status).to eq(200)

          result = response.parsed_body
          expect(result['success']).to eq(true)
          expect(result['url']).to be_present
        end
      end
    end

    describe 'converting private message to public topic' do
      fab!(:topic) { Fabricate(:private_message_topic, user: user) }
      fab!(:post) { Fabricate(:post, topic: topic) }

      it "raises an error when the user doesn't have permission to convert topic" do
        sign_in(user)
        put "/t/#{topic.id}/convert-topic/public.json"
        expect(response).to be_forbidden
      end

      context "success" do
        it "returns success" do
          sign_in(admin)
          put "/t/#{topic.id}/convert-topic/public.json?category_id=#{category.id}"

          topic.reload
          expect(topic.archetype).to eq(Archetype.default)
          expect(topic.category_id).to eq(category.id)
          expect(response.status).to eq(200)

          result = response.parsed_body
          expect(result['success']).to eq(true)
          expect(result['url']).to be_present
        end
      end
    end
  end

  describe '#timings' do
    fab!(:post_1) { Fabricate(:post, topic: topic) }

    it 'should record the timing' do
      sign_in(user)

      post "/topics/timings.json", params: {
        topic_id: topic.id,
        topic_time: 5,
        timings: { post_1.post_number => 2 }
      }

      expect(response.status).to eq(200)

      post_timing = PostTiming.first

      expect(post_timing.topic).to eq(topic)
      expect(post_timing.user).to eq(user)
      expect(post_timing.msecs).to eq(2)
    end

    it 'caps post read time at the max integer value (2^31 - 1)' do
      PostTiming.create!(
        topic_id: post_1.topic.id,
        post_number: post_1.post_number,
        user_id: user.id,
        msecs: 2**31 - 10
      )
      sign_in(user)

      post "/topics/timings.json", params: {
        topic_id: topic.id,
        topic_time: 5,
        timings: { post_1.post_number => 100 }
      }

      expect(response.status).to eq(200)
      post_timing = PostTiming.first

      expect(post_timing.topic).to eq(topic)
      expect(post_timing.user).to eq(user)
      expect(post_timing.msecs).to eq(2**31 - 1)
    end
  end

  describe '#timer' do
    context 'when a user is not logged in' do
      it 'should return the right response' do
        post "/t/#{topic.id}/timer.json", params: {
          time: '24',
          status_type: TopicTimer.types[1]
        }
        expect(response.status).to eq(403)
      end
    end

    context 'when does not have permission' do
      it 'should return the right response' do
        sign_in(user)

        post "/t/#{topic.id}/timer.json", params: {
          time: '24',
          status_type: TopicTimer.types[1]
        }

        expect(response.status).to eq(403)
        expect(response.parsed_body["error_type"]).to eq('invalid_access')
      end
    end

    context 'when time is in the past' do
      it 'returns an error' do
        freeze_time
        sign_in(admin)

        post "/t/#{topic.id}/timer.json", params: {
          time: Time.current - 1.day,
          status_type: TopicTimer.types[1]
        }
        expect(response.status).to eq(400)
      end
    end

    context 'when logged in as an admin' do
      before do
        freeze_time
        sign_in(admin)
      end

      it 'should be able to create a topic status update' do
        post "/t/#{topic.id}/timer.json", params: {
          time: 24,
          status_type: TopicTimer.types[1]
        }

        expect(response.status).to eq(200)

        topic_status_update = TopicTimer.last

        expect(topic_status_update.topic).to eq(topic)
        expect(topic_status_update.execute_at).to eq_time(24.hours.from_now)

        json = response.parsed_body

        expect(DateTime.parse(json['execute_at']))
          .to eq_time(DateTime.parse(topic_status_update.execute_at.to_s))

        expect(json['duration_minutes']).to eq(topic_status_update.duration_minutes)
        expect(json['closed']).to eq(topic.reload.closed)
      end

      it 'should be able to delete a topic status update' do
        Fabricate(:topic_timer, topic: topic)

        post "/t/#{topic.id}/timer.json", params: {
          time: nil,
          status_type: TopicTimer.types[1]
        }

        expect(response.status).to eq(200)
        expect(topic.reload.public_topic_timer).to eq(nil)

        json = response.parsed_body

        expect(json['execute_at']).to eq(nil)
        expect(json['duration_minutes']).to eq(nil)
        expect(json['closed']).to eq(topic.closed)
      end

      it 'should be able to create a topic status update with duration' do
        post "/t/#{topic.id}/timer.json", params: {
          duration_minutes: 7200,
          status_type: TopicTimer.types[7]
        }

        expect(response.status).to eq(200)

        topic_status_update = TopicTimer.last

        expect(topic_status_update.topic).to eq(topic)
        expect(topic_status_update.execute_at).to eq_time(5.days.from_now)
        expect(topic_status_update.duration_minutes).to eq(7200)

        json = response.parsed_body

        expect(DateTime.parse(json['execute_at']))
          .to eq_time(DateTime.parse(topic_status_update.execute_at.to_s))

        expect(json['duration_minutes']).to eq(topic_status_update.duration_minutes)
      end

      it 'should be able to delete a topic status update for delete_replies type' do
        Fabricate(:topic_timer, topic: topic, status_type: TopicTimer.types[:delete_replies])

        post "/t/#{topic.id}/timer.json", params: {
          time: nil,
          status_type: TopicTimer.types[7]
        }

        expect(response.status).to eq(200)
        expect(topic.reload.public_topic_timer).to eq(nil)

        json = response.parsed_body

        expect(json['execute_at']).to eq(nil)
        expect(json['duration']).to eq(nil)
        expect(json['closed']).to eq(topic.closed)
      end

      describe 'publishing topic to category in the future' do
        it 'should be able to create the topic status update' do
          post "/t/#{topic.id}/timer.json", params: {
            time: 24,
            status_type: TopicTimer.types[3],
            category_id: topic.category_id
          }

          expect(response.status).to eq(200)

          topic_status_update = TopicTimer.last

          expect(topic_status_update.topic).to eq(topic)
          expect(topic_status_update.execute_at).to eq_time(24.hours.from_now)
          expect(topic_status_update.status_type)
            .to eq(TopicTimer.types[:publish_to_category])

          json = response.parsed_body

          expect(json['category_id']).to eq(topic.category_id)
        end
      end

      describe 'invalid status type' do
        it 'should raise the right error' do
          post "/t/#{topic.id}/timer.json", params: {
            time: 10,
            status_type: 'something'
          }
          expect(response.status).to eq(400)
          expect(response.body).to include('status_type')
        end
      end
    end

    context 'when logged in as a TL4 user' do
      it "raises an error if the user can't see the topic" do
        user.update!(trust_level: TrustLevel[4])
        sign_in(user)

        pm_topic = Fabricate(:private_message_topic)

        post "/t/#{pm_topic.id}/timer.json", params: {
          time: '24',
          status_type: TopicTimer.types[1]
        }

        expect(response.status).to eq(403)
        expect(response.parsed_body["error_type"]).to eq('invalid_access')
      end
    end
  end

  describe '#set_slow_mode' do
    context 'when not logged in' do
      it 'returns a forbidden response' do
        put "/t/#{topic.id}/slow_mode.json", params: {
          seconds: '3600'
        }

        expect(response.status).to eq(403)
      end
    end

    context 'logged in as an admin' do
      it 'allows admins to set the slow mode interval' do
        sign_in(admin)

        put "/t/#{topic.id}/slow_mode.json", params: {
          seconds: '3600'
        }

        topic.reload
        expect(response.status).to eq(200)
        expect(topic.slow_mode_seconds).to eq(3600)
      end
    end

    context 'logged in as a regular user' do
      it 'does nothing if the user is not TL4' do
        user.update!(trust_level: TrustLevel[3])
        sign_in(user)

        put "/t/#{topic.id}/slow_mode.json", params: {
          seconds: '3600'
        }

        expect(response.status).to eq(403)
      end

      it 'allows TL4 users to set the slow mode interval' do
        user.update!(trust_level: TrustLevel[4])
        sign_in(user)

        put "/t/#{topic.id}/slow_mode.json", params: {
          seconds: '3600'
        }

        topic.reload
        expect(response.status).to eq(200)
        expect(topic.slow_mode_seconds).to eq(3600)
      end
    end

    context 'auto-disable slow mode' do
      before { sign_in(admin) }

      let!(:timestamp) { 1.week.from_now.to_formatted_s(:iso8601) }

      it 'sets a topic timer to clear the slow mode automatically' do
        put "/t/#{topic.id}/slow_mode.json", params: {
          seconds: '3600', enabled_until: timestamp
        }

        created_timer = TopicTimer.find_by(topic: topic)
        execute_at = created_timer.execute_at.to_formatted_s(:iso8601)

        expect(execute_at).to eq(timestamp)
      end

      it 'deletes the topic timer' do
        put "/t/#{topic.id}/slow_mode.json", params: {
          seconds: '3600', enabled_until: timestamp
        }

        put "/t/#{topic.id}/slow_mode.json", params: {
          seconds: '0', enabled_until: timestamp
        }

        created_timer = TopicTimer.find_by(topic: topic)

        expect(created_timer).to be_nil
      end

      it 'updates the existing timer' do
        put "/t/#{topic.id}/slow_mode.json", params: {
          seconds: '3600', enabled_until: timestamp
        }

        updated_timestamp = 1.hour.from_now.to_formatted_s(:iso8601)

        put "/t/#{topic.id}/slow_mode.json", params: {
          seconds: '3600', enabled_until: updated_timestamp
        }

        created_timer = TopicTimer.find_by(topic: topic)
        execute_at = created_timer.execute_at.to_formatted_s(:iso8601)

        expect(execute_at).to eq(updated_timestamp)
      end
    end
  end

  describe '#invite' do
    describe 'when not logged in' do
      it "should return the right response" do
        post "/t/#{topic.id}/invite.json", params: {
          email: 'jake@adventuretime.ooo'
        }

        expect(response.status).to eq(403)
      end
    end

    describe 'when logged in' do
      before do
        sign_in(user)
      end

      describe 'as a valid user' do
        fab!(:topic) { Fabricate(:topic, user: user) }

        it 'should return the right response' do
          user.update!(trust_level: TrustLevel[2])

          expect do
            post "/t/#{topic.id}/invite.json", params: {
              email: 'someguy@email.com'
            }
          end.to change { Invite.where(invited_by_id: user.id).count }.by(1)

          expect(response.status).to eq(200)
          expect(Jobs::InviteEmail.jobs.first['args'].first['invite_to_topic']).to be_truthy
        end
      end

      describe 'when user is a group manager' do
        fab!(:group) { Fabricate(:group).tap { |g| g.add_owner(user) } }
        fab!(:private_category)  { Fabricate(:private_category, group: group) }

        fab!(:group_private_topic) do
          Fabricate(:topic, category: private_category, user: user)
        end

        let!(:recipient) { 'jake@adventuretime.ooo' }

        it "should attach group to the invite" do
          post "/t/#{group_private_topic.id}/invite.json", params: {
            user: recipient,
            group_ids: "#{group.id},123"
          }

          expect(response.status).to eq(200)
          expect(Invite.find_by(email: recipient).groups).to eq([group])
        end

        describe 'when group is available to automatic groups only' do
          before do
            group.update!(automatic: true)
          end

          it 'should return the right response' do
            post "/t/#{group_private_topic.id}/invite.json", params: {
              user: user
            }

            expect(response.status).to eq(403)
          end
        end

        describe 'when user is not part of the required group' do
          it 'should return the right response' do
            post "/t/#{group_private_topic.id}/invite.json", params: {
              user: user
            }

            expect(response.status).to eq(422)

            response_body = response.parsed_body

            expect(response_body["errors"]).to eq([
              I18n.t("topic_invite.failed_to_invite",
                group_names: group.name
              )
            ])
          end
        end
      end

      describe 'when topic id is invalid' do
        it 'should return the right response' do
          post "/t/999/invite.json", params: {
            email: user.email
          }

          expect(response.status).to eq(400)
        end
      end

      it 'requires an email parameter' do
        post "/t/#{topic.id}/invite.json"
        expect(response.status).to eq(400)
      end

      describe "when PM has reached maximum allowed numbers of recipients" do
        fab!(:user2) { Fabricate(:user) }
        fab!(:pm) { Fabricate(:private_message_topic, user: user) }

        fab!(:moderator_pm) { Fabricate(:private_message_topic, user: moderator) }

        before do
          SiteSetting.max_allowed_message_recipients = 2
        end

        it "doesn't allow normal users to invite" do
          post "/t/#{pm.id}/invite.json", params: {
            user: user2.username
          }
          expect(response.status).to eq(422)
          expect(response.parsed_body["errors"]).to contain_exactly(
            I18n.t("pm_reached_recipients_limit", recipients_limit: SiteSetting.max_allowed_message_recipients)
          )
        end

        it "allows staff to bypass limits" do
          sign_in(moderator)
          post "/t/#{moderator_pm.id}/invite.json", params: {
            user: user2.username
          }
          expect(response.status).to eq(200)
          expect(moderator_pm.reload.topic_allowed_users.count).to eq(3)
        end
      end

      describe 'when user does not have permission to invite to the topic' do
        fab!(:topic) { Fabricate(:private_message_topic) }

        it "should return the right response" do
          post "/t/#{topic.id}/invite.json", params: {
            user: user.username
          }

          expect(response.status).to eq(403)
        end
      end
    end

    describe "when inviting a group to a topic" do
      fab!(:group) { Fabricate(:group) }

      before do
        sign_in(admin)
      end

      it "should work correctly" do
        email = 'hiro@from.heros'

        post "/t/#{topic.id}/invite.json", params: {
          email: email, group_ids: group.id
        }

        expect(response.status).to eq(200)

        groups = Invite.find_by(email: email).groups
        expect(groups.count).to eq(1)
        expect(groups.first.id).to eq(group.id)
      end
    end
  end

  describe 'invite_group' do
    let!(:admins) { Group[:admins] }
    fab!(:pm) { Fabricate(:private_message_topic) }

    def invite_group(topic, expected_status)
      post "/t/#{topic.id}/invite-group.json", params: { group: admins.name }
      expect(response.status).to eq(expected_status)
    end

    before do
      admins.update!(messageable_level: Group::ALIAS_LEVELS[:everyone])
    end

    describe 'as an anon user' do
      it 'should be forbidden' do
        invite_group(pm, 403)
      end
    end

    describe 'as a normal user' do
      let!(:user) { sign_in(Fabricate(:user)) }

      describe 'when user does not have permission to view the topic' do
        it 'should be forbidden' do
          invite_group(pm, 403)
        end
      end

      describe 'when user has permission to view the topic' do
        before do
          pm.allowed_users << user
        end

        it 'should allow user to invite group to topic' do
          invite_group(pm, 200)
          expect(pm.allowed_groups.first.id).to eq(admins.id)
        end
      end
    end

    describe 'as an admin user' do
      before do
        sign_in(admin)
      end

      it "disallows inviting a group to a topic" do
        topic = Fabricate(:topic)
        invite_group(topic, 422)
      end

      it "allows inviting a group to a PM" do
        invite_group(pm, 200)
        expect(pm.allowed_groups.first.id).to eq(admins.id)
      end
    end

    context "when PM has reached maximum allowed numbers of recipients" do
      fab!(:group) { Fabricate(:group, messageable_level: 99) }
      fab!(:pm) { Fabricate(:private_message_topic, user: user) }

      fab!(:moderator_pm) { Fabricate(:private_message_topic, user: moderator) }

      before do
        SiteSetting.max_allowed_message_recipients = 2
      end

      it "doesn't allow normal users to invite" do
        post "/t/#{pm.id}/invite-group.json", params: {
          group: group.name
        }
        expect(response.status).to eq(422)
        expect(response.parsed_body["errors"]).to contain_exactly(
          I18n.t("pm_reached_recipients_limit", recipients_limit: SiteSetting.max_allowed_message_recipients)
        )
      end

      it "allows staff to bypass limits" do
        sign_in(moderator)
        post "/t/#{moderator_pm.id}/invite-group.json", params: {
          group: group.name
        }
        expect(response.status).to eq(200)
        expect(moderator_pm.reload.topic_allowed_users.count + moderator_pm.topic_allowed_groups.count).to eq(3)
      end
    end
  end

  describe 'shared drafts' do
    fab!(:shared_drafts_category) { Fabricate(:category) }

    before do
      SiteSetting.shared_drafts_category = shared_drafts_category.id
    end

    describe "#update_shared_draft" do
      fab!(:other_cat) { Fabricate(:category) }
      fab!(:topic) { Fabricate(:topic, category: shared_drafts_category, visible: false) }

      context "anonymous" do
        it "doesn't allow staff to update the shared draft" do
          put "/t/#{topic.id}/shared-draft.json", params: { category_id: other_cat.id }
          expect(response.code.to_i).to eq(403)
        end
      end

      context "as a moderator" do
        before do
          sign_in(moderator)
        end

        context "with a shared draft" do
          fab!(:shared_draft) { Fabricate(:shared_draft, topic: topic, category: category) }
          it "allows staff to update the category id" do
            put "/t/#{topic.id}/shared-draft.json", params: { category_id: other_cat.id }
            expect(response.status).to eq(200)
            topic.reload
            expect(topic.shared_draft.category_id).to eq(other_cat.id)
          end
        end

        context "without a shared draft" do
          it "allows staff to update the category id" do
            put "/t/#{topic.id}/shared-draft.json", params: { category_id: other_cat.id }
            expect(response.status).to eq(200)
            topic.reload
            expect(topic.shared_draft.category_id).to eq(other_cat.id)
          end
        end
      end
    end

    describe "#publish" do
      fab!(:topic) { Fabricate(:topic, category: shared_drafts_category, visible: false) }
      fab!(:post) { Fabricate(:post, topic: topic) }

      it "fails for anonymous users" do
        put "/t/#{topic.id}/publish.json", params: { destination_category_id: category.id }
        expect(response.status).to eq(403)
      end

      it "fails as a regular user" do
        sign_in(user)
        put "/t/#{topic.id}/publish.json", params: { destination_category_id: category.id }
        expect(response.status).to eq(403)
      end

      context "as staff" do
        before do
          sign_in(moderator)
        end

        it "will publish the topic" do
          put "/t/#{topic.id}/publish.json", params: { destination_category_id: category.id }
          expect(response.status).to eq(200)
          json = response.parsed_body['basic_topic']

          result = Topic.find(json['id'])
          expect(result.category_id).to eq(category.id)
          expect(result.visible).to eq(true)
        end

        it 'fails if the destination category is the shared drafts category' do
          put "/t/#{topic.id}/publish.json", params: { destination_category_id: shared_drafts_category.id }
          expect(response.status).to eq(400)
        end
      end
    end
  end

  describe "crawler" do
    context "when not a crawler" do
      it "renders with the application layout" do
        get topic.url

        body = response.body

        expect(body).to have_tag(:script, with: { src: '/assets/application.js' })
        expect(body).to have_tag(:meta, with: { name: 'fragment' })
      end
    end

    context "when a crawler" do
      it "renders with the crawler layout, and handles proper pagination" do
        page1_time = 3.months.ago
        page2_time = 2.months.ago
        page3_time = 1.month.ago

        freeze_time page1_time

        topic = Fabricate(:topic)
        Fabricate(:post, topic: topic)
        Fabricate(:post, topic: topic)

        freeze_time page2_time
        Fabricate(:post, topic: topic)
        Fabricate(:post, topic: topic)

        freeze_time page3_time
        Fabricate(:post, topic: topic)

        # ugly, but no interface to set this and we don't want to create
        # 100 posts to test this thing
        TopicView.stubs(:chunk_size).returns(2)

        user_agent = "Mozilla/5.0 (compatible; Googlebot/2.1; +http://www.google.com/bot.html)"

        get topic.url, env: { "HTTP_USER_AGENT" => user_agent }

        body = response.body

        expect(body).to have_tag(:body, with: { class: 'crawler' })
        expect(body).to_not have_tag(:meta, with: { name: 'fragment' })
        expect(body).to include('<link rel="next" href="' + topic.relative_url + "?page=2")

        expect(response.headers['Last-Modified']).to eq(page1_time.httpdate)

        get topic.url + "?page=2", env: { "HTTP_USER_AGENT" => user_agent }
        body = response.body

        expect(response.headers['Last-Modified']).to eq(page2_time.httpdate)

        expect(body).to include('<link rel="prev" href="' + topic.relative_url)
        expect(body).to include('<link rel="next" href="' + topic.relative_url + "?page=3")

        get topic.url + "?page=3", env: { "HTTP_USER_AGENT" => user_agent }
        body = response.body

        expect(response.headers['Last-Modified']).to eq(page3_time.httpdate)
        expect(body).to include('<link rel="prev" href="' + topic.relative_url + "?page=2")
      end

      context "canonical_url" do
        fab!(:topic_embed) { Fabricate(:topic_embed, embed_url: "https://markvanlan.com") }
        let!(:user_agent) { "Mozilla/5.0 (compatible; Googlebot/2.1; +http://www.google.com/bot.html)" }

        it "set to topic.url when embed_set_canonical_url is false" do
          get topic_embed.topic.url, env: { "HTTP_USER_AGENT" => user_agent }
          expect(response.body).to include('<link rel="canonical" href="' + topic_embed.topic.url)
        end

        it "set to topic_embed.embed_url when embed_set_canonical_url is true" do
          SiteSetting.embed_set_canonical_url = true
          get topic_embed.topic.url, env: { "HTTP_USER_AGENT" => user_agent }
          expect(response.body).to include('<link rel="canonical" href="' + topic_embed.embed_url)
        end
      end

      context "wayback machine" do
        it "renders crawler layout" do
          get topic.url, env: { "HTTP_USER_AGENT" => "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/74.0.3729.169 Safari/537.36", "HTTP_VIA" => "HTTP/1.0 web.archive.org (Wayback Save Page)" }
          body = response.body

          expect(body).to have_tag(:body, with: { class: 'crawler' })
          expect(body).to_not have_tag(:meta, with: { name: 'fragment' })
        end
      end
    end
  end

  describe "#reset_bump_date" do
    context "errors" do
      fab!(:topic) { Fabricate(:topic) }

      it "needs you to be logged in" do
        put "/t/#{topic.id}/reset-bump-date.json"
        expect(response.status).to eq(403)
      end

      [:user].each do |user|
        it "denies access for #{user}" do
          sign_in(Fabricate(user))
          put "/t/#{topic.id}/reset-bump-date.json"
          expect(response.status).to eq(403)
        end
      end

      it "should fail for non-existent topic" do
        max_id = Topic.maximum(:id)
        sign_in(admin)
        put "/t/#{max_id + 1}/reset-bump-date.json"
        expect(response.status).to eq(404)
      end
    end

    [:admin, :moderator, :trust_level_4].each do |user|
      it "should reset bumped_at as #{user}" do
        sign_in(Fabricate(user))
        topic = Fabricate(:topic, bumped_at: 1.hour.ago)
        timestamp = 1.day.ago
        Fabricate(:post, topic: topic, created_at: timestamp)

        put "/t/#{topic.id}/reset-bump-date.json"
        expect(response.status).to eq(200)
        expect(topic.reload.bumped_at).to eq_time(timestamp)
      end
    end
  end
end