diff --git a/app/models/post.rb b/app/models/post.rb
index a88b52da9..98842c916 100644
--- a/app/models/post.rb
+++ b/app/models/post.rb
@@ -317,6 +317,10 @@ class Post < ActiveRecord::Base
     result
   end
 
+  def is_first_post?
+    post_number == 1
+  end
+
   def is_flagged?
     post_actions.where(post_action_type_id: PostActionType.flag_types.values, deleted_at: nil).count != 0
   end
diff --git a/app/models/post_mover.rb b/app/models/post_mover.rb
new file mode 100644
index 000000000..11f15de30
--- /dev/null
+++ b/app/models/post_mover.rb
@@ -0,0 +1,117 @@
+class PostMover
+  attr_reader :original_topic, :destination_topic, :user, :post_ids
+
+  def initialize(original_topic, user, post_ids)
+    @original_topic = original_topic
+    @user = user
+    @post_ids = post_ids
+  end
+
+  def to_topic(id)
+    Topic.transaction do
+      move_posts_to Topic.find_by_id(id)
+    end
+  end
+
+  def to_new_topic(title)
+    Topic.transaction do
+      move_posts_to Topic.create!(
+        user: user,
+        title: title,
+        category: original_topic.category
+      )
+    end
+  end
+
+  private
+
+  def move_posts_to(topic)
+    Guardian.new(user).ensure_can_see! topic
+    @destination_topic = topic
+
+    move_posts_to_destination_topic
+    destination_topic
+  end
+
+  def move_posts_to_destination_topic
+    move_each_post
+    notify_users_that_posts_have_moved
+    update_statistics
+  end
+
+  def move_each_post
+    with_max_post_number do |max_post_number|
+      posts.each_with_index do |post, offset|
+        post.is_first_post? ? copy(post) : move(post, offset + max_post_number)
+      end
+    end
+  end
+
+  def copy(post)
+    PostCreator.create(
+      post.user,
+      raw: post.raw,
+      topic_id: destination_topic.id,
+      acting_user: user
+    )
+  end
+
+  def move(post, post_number)
+    @first_post_number_moved ||= post.post_number
+
+    Post.update_all(
+      [
+        ['post_number = :post_number',
+         'topic_id    = :topic_id',
+         'sort_order  = :post_number'
+        ].join(', '),
+        post_number: post_number,
+        topic_id: destination_topic.id
+      ],
+      id: post.id,
+      topic_id: original_topic.id
+    )
+  end
+
+  def update_statistics
+    destination_topic.update_statistics
+    original_topic.update_statistics
+  end
+
+  def notify_users_that_posts_have_moved
+    enqueue_notification_job
+    create_moderator_post_in_original_topic
+  end
+
+  def enqueue_notification_job
+    Jobs.enqueue(
+      :notify_moved_posts,
+      post_ids: post_ids,
+      moved_by_id: user.id
+    )
+  end
+
+  def create_moderator_post_in_original_topic
+    original_topic.add_moderator_post(
+      user,
+      I18n.t(
+        "move_posts.moderator_post",
+        count: post_ids.count,
+        topic_link: "[#{destination_topic.title}](#{destination_topic.url})"
+      ),
+      post_number: @first_post_number_moved
+    )
+  end
+
+  def with_max_post_number
+    yield destination_topic.max_post_number + 1
+  end
+
+  def posts
+    @posts ||= begin
+      Post.where(id: post_ids).order(:created_at).tap do |posts|
+        raise Discourse::InvalidParameters.new(:post_ids) if posts.empty?
+      end
+    end
+  end
+end
diff --git a/app/models/topic.rb b/app/models/topic.rb
index 9563ddc2e..176168957 100644
--- a/app/models/topic.rb
+++ b/app/models/topic.rb
@@ -447,70 +447,18 @@ class Topic < ActiveRecord::Base
     invite
   end
 
-  def move_posts_to_topic(moved_by, post_ids, destination_topic)
-    to_move = posts.where(id: post_ids).order(:created_at)
-    raise Discourse::InvalidParameters.new(:post_ids) if to_move.blank?
-
-    first_post_number = nil
-    Topic.transaction do
-      # Find the max post number in the topic
-      max_post_number = destination_topic.posts.maximum(:post_number) || 0
-
-      to_move.each_with_index do |post, i|
-        if post.post_number == 1
-          # We have a special case for the OP, we copy it instead of deleting it.
-          result = PostCreator.new(post.user,
-                                  raw: post.raw,
-                                  topic_id: destination_topic.id,
-                                  acting_user: moved_by).create
-        else
-          first_post_number ||= post.post_number
-          # Move the post and raise an error if it couldn't be moved
-          row_count = Post.update_all ["post_number = :post_number, topic_id = :topic_id, sort_order = :post_number", post_number: max_post_number+i+1, topic_id: destination_topic.id], id: post.id, topic_id: id
-          raise Discourse::InvalidParameters.new(:post_ids) if row_count == 0
-        end
-      end
-    end
-
-
-    first_post_number
+  def max_post_number
+    posts.maximum(:post_number).to_i
   end
 
   def move_posts(moved_by, post_ids, opts)
+    post_mover = PostMover.new(self, moved_by, post_ids)
 
-    topic = nil
-    first_post_number = nil
-
-    if opts[:title].present?
-      # If we're moving to a new topic...
-      Topic.transaction do
-        topic = Topic.create(user: moved_by, title: opts[:title], category: category)
-        first_post_number = move_posts_to_topic(moved_by, post_ids, topic)
-      end
-
-    elsif opts[:destination_topic_id].present?
-      # If we're moving to an existing topic...
-
-      topic = Topic.where(id: opts[:destination_topic_id]).first
-      Guardian.new(moved_by).ensure_can_see!(topic)
-      first_post_number = move_posts_to_topic(moved_by, post_ids, topic)
-
+    if opts[:destination_topic_id]
+      post_mover.to_topic opts[:destination_topic_id]
+    elsif opts[:title]
+      post_mover.to_new_topic opts[:title]
     end
-
-    # Add a moderator post explaining that the post was moved
-    if topic.present?
-      topic_url = "#{Discourse.base_url}#{topic.relative_url}"
-      topic_link = "[#{topic.title}](#{topic_url})"
-
-      add_moderator_post(moved_by, I18n.t("move_posts.moderator_post", count: post_ids.size, topic_link: topic_link), post_number: first_post_number)
-      Jobs.enqueue(:notify_moved_posts, post_ids: post_ids, moved_by_id: moved_by.id)
-
-      topic.update_statistics
-      update_statistics
-    end
-
-
-    topic
   end
 
   # Updates the denormalized statistics of a topic including featured posters. They shouldn't
@@ -678,6 +626,10 @@ class Topic < ActiveRecord::Base
     url
   end
 
+  def url(post_number = nil)
+    self.class.url id, slug, post_number
+  end
+
   def relative_url(post_number=nil)
     url = "/t/#{slug}/#{id}"
     url << "/#{post_number}" if post_number.to_i > 1
diff --git a/spec/models/topic_spec.rb b/spec/models/topic_spec.rb
index 27e9e127a..262a0064d 100644
--- a/spec/models/topic_spec.rb
+++ b/spec/models/topic_spec.rb
@@ -204,10 +204,15 @@ describe Topic do
         lambda { topic.move_posts(user, [1003], title: "new testing topic name") }.should raise_error(Discourse::InvalidParameters)
       end
 
-      it "raises an error if no posts were moved" do
-        lambda { topic.move_posts(user, [], title: "new testing topic name") }.should raise_error(Discourse::InvalidParameters)
-      end
+      it "raises an error and does not create a topic if no posts were moved" do
+        Topic.count.tap do |original_topic_count|
+          lambda {
+            topic.move_posts(user, [], title: "new testing topic name")
+          }.should raise_error(Discourse::InvalidParameters)
 
+          expect(Topic.count).to eq original_topic_count
+        end
+      end
     end
 
     context "successfully moved" do