Index: src/wp-admin/includes/ms.php
===================================================================
--- src/wp-admin/includes/ms.php	(revision 41233)
+++ src/wp-admin/includes/ms.php	(working copy)
@@ -53,6 +53,7 @@
  * Delete a site.
  *
  * @since 3.0.0
+ * @since 4.9.0 Now uses `wp_delete_site()` to delete the site row from the database.
  *
  * @global wpdb $wpdb WordPress database abstraction object.
  *
@@ -127,7 +128,7 @@
 			$wpdb->query( "DROP TABLE IF EXISTS `$table`" );
 		}
 
-		$wpdb->delete( $wpdb->blogs, array( 'blog_id' => $blog_id ) );
+		wp_delete_site( $blog_id );
 
 		/**
 		 * Filters the upload base directory to delete when the site is deleted.
@@ -169,8 +170,6 @@
 			if ( $dir != $top_dir)
 			@rmdir( $dir );
 		}
-
-		clean_blog_cache( $blog );
 	}
 
 	/**
Index: src/wp-includes/ms-blogs.php
===================================================================
--- src/wp-includes/ms-blogs.php	(revision 41233)
+++ src/wp-includes/ms-blogs.php	(working copy)
@@ -295,6 +295,7 @@
  * Update the details for a blog. Updates the blogs table for a given blog id.
  *
  * @since MU (3.0.0)
+ * @since 4.9.0 Now wraps `wp_update_site()`.
  *
  * @global wpdb $wpdb WordPress database abstraction object.
  *
@@ -311,126 +312,9 @@
 	if ( is_object($details) )
 		$details = get_object_vars($details);
 
-	$current_details = get_site( $blog_id );
-	if ( empty($current_details) )
+	$site = wp_update_site( $blog_id, $details );
+	if ( is_wp_error( $site ) ) {
 		return false;
-
-	$current_details = get_object_vars($current_details);
-
-	$details = array_merge($current_details, $details);
-	$details['last_updated'] = current_time('mysql', true);
-
-	$update_details = array();
-	$fields = array( 'site_id', 'domain', 'path', 'registered', 'last_updated', 'public', 'archived', 'mature', 'spam', 'deleted', 'lang_id');
-	foreach ( array_intersect( array_keys( $details ), $fields ) as $field ) {
-		if ( 'path' === $field ) {
-			$details[ $field ] = trailingslashit( '/' . trim( $details[ $field ], '/' ) );
-		}
-
-		$update_details[ $field ] = $details[ $field ];
-	}
-
-	$result = $wpdb->update( $wpdb->blogs, $update_details, array('blog_id' => $blog_id) );
-
-	if ( false === $result )
-		return false;
-
-	// If spam status changed, issue actions.
-	if ( $details['spam'] != $current_details['spam'] ) {
-		if ( $details['spam'] == 1 ) {
-			/**
-			 * Fires when the 'spam' status is added to a blog.
-			 *
-			 * @since MU (3.0.0)
-			 *
-			 * @param int $blog_id Blog ID.
-			 */
-			do_action( 'make_spam_blog', $blog_id );
-		} else {
-			/**
-			 * Fires when the 'spam' status is removed from a blog.
-			 *
-			 * @since MU (3.0.0)
-			 *
-			 * @param int $blog_id Blog ID.
-			 */
-			do_action( 'make_ham_blog', $blog_id );
-		}
-	}
-
-	// If mature status changed, issue actions.
-	if ( $details['mature'] != $current_details['mature'] ) {
-		if ( $details['mature'] == 1 ) {
-			/**
-			 * Fires when the 'mature' status is added to a blog.
-			 *
-			 * @since 3.1.0
-			 *
-			 * @param int $blog_id Blog ID.
-			 */
-			do_action( 'mature_blog', $blog_id );
-		} else {
-			/**
-			 * Fires when the 'mature' status is removed from a blog.
-			 *
-			 * @since 3.1.0
-			 *
-			 * @param int $blog_id Blog ID.
-			 */
-			do_action( 'unmature_blog', $blog_id );
-		}
-	}
-
-	// If archived status changed, issue actions.
-	if ( $details['archived'] != $current_details['archived'] ) {
-		if ( $details['archived'] == 1 ) {
-			/**
-			 * Fires when the 'archived' status is added to a blog.
-			 *
-			 * @since MU (3.0.0)
-			 *
-			 * @param int $blog_id Blog ID.
-			 */
-			do_action( 'archive_blog', $blog_id );
-		} else {
-			/**
-			 * Fires when the 'archived' status is removed from a blog.
-			 *
-			 * @since MU (3.0.0)
-			 *
-			 * @param int $blog_id Blog ID.
-			 */
-			do_action( 'unarchive_blog', $blog_id );
-		}
-	}
-
-	// If deleted status changed, issue actions.
-	if ( $details['deleted'] != $current_details['deleted'] ) {
-		if ( $details['deleted'] == 1 ) {
-			/**
-			 * Fires when the 'deleted' status is added to a blog.
-			 *
-			 * @since 3.5.0
-			 *
-			 * @param int $blog_id Blog ID.
-			 */
-			do_action( 'make_delete_blog', $blog_id );
-		} else {
-			/**
-			 * Fires when the 'deleted' status is removed from a blog.
-			 *
-			 * @since 3.5.0
-			 *
-			 * @param int $blog_id Blog ID.
-			 */
-			do_action( 'make_undelete_blog', $blog_id );
-		}
-	}
-
-	if ( isset( $details['public'] ) ) {
-		switch_to_blog( $blog_id );
-		update_option( 'blog_public', $details['public'] );
-		restore_current_blog();
 	}
 
 	refresh_blog_details($blog_id);
@@ -498,6 +382,199 @@
 }
 
 /**
+ * Inserts a new site into the database.
+ *
+ * @since 4.9.0
+ *
+ * @global wpdb $wpdb WordPress database abstraction object.
+ *
+ * @param array $data {
+ *     Data for the new site that should be inserted.
+ *
+ *     @type string $domain       Site domain. Must always be provided.
+ *     @type string $path         Site path. Default '/'.
+ *     @type int    $site_id      The site's network ID. Default is the main network ID.
+ *     @type string $registered   When the site was registered, in SQL datetime format. Default is
+ *                                the current time.
+ *     @type string $last_updated When the site was last updated, in SQL datetime format. Default is
+ *                                the value of $registered.
+ *     @type int    $public       Whether the site is public. Default 1.
+ *     @type int    $archived     Whether the site is archived. Default 0.
+ *     @type int    $mature       Whether the site is mature. Default 0.
+ *     @type int    $spam         Whether the site is spam. Default 0.
+ *     @type int    $deleted      Whether the site is deleted. Default 0.
+ *     @type int    $lang_id      The site's language ID. Currently unused. Default 0.
+ * }
+ * @return int|WP_Error The new site's ID on success, or error object on failure.
+ */
+function wp_insert_site( $data ) {
+	global $wpdb;
+
+	$now = current_time( 'mysql' );
+
+	$defaults = array(
+		'domain'       => '',
+		'path'         => '',
+		'site_id'      => 0,
+		'registered'   => $now,
+		'last_updated' => $now,
+		'public'       => 1,
+		'archived'     => 0,
+		'mature'       => 0,
+		'spam'         => 0,
+		'deleted'      => 0,
+		'lang_id'      => 0,
+	);
+
+	$compat_keys = array(
+		'network_id' => 'site_id',
+	);
+
+	foreach ( $compat_keys as $compat_key => $original_compat_key ) {
+		if ( ! empty( $data[ $compat_key ] ) && empty( $data[ $original_compat_key ] ) ) {
+			$data[ $original_compat_key ] = $data[ $compat_key ];
+		}
+	}
+
+	$data = array_intersect_key( wp_parse_args( $data, $defaults ), $defaults );
+
+	// A domain must always be present.
+	if ( empty( $data['domain'] ) ) {
+		return new WP_Error( 'site_empty_domain', __( 'Site domain must not be empty.' ) );
+	}
+
+	$data['path'] = trailingslashit( '/' . trim( $data['path'], '/' ) );
+
+	// Use the default network if none is set.
+	if ( empty( $data['site_id'] ) ) {
+		$data['site_id'] = get_main_network_id();
+	}
+
+	if ( false === $wpdb->insert( $wpdb->blogs, $data ) ) {
+		return new WP_Error( 'db_insert_error', __( 'Could not insert site into the database.' ), $wpdb->last_error );
+	}
+
+	$new_site = get_site( $wpdb->insert_id );
+
+	clean_blog_cache( $new_site );
+
+	/**
+	 * Fires once a site has been inserted into the database.
+	 *
+	 * @since 4.9.0
+	 *
+	 * @param WP_Site $new_site New site object.
+	 */
+	do_action( 'wp_insert_site', $new_site );
+
+	return (int) $new_site->id;
+}
+
+/**
+ * Updates a site in the database.
+ *
+ * @since 4.9.0
+ *
+ * @global wpdb $wpdb WordPress database abstraction object.
+ *
+ * @param int   $site_id ID of the site that should be updated.
+ * @param array $data    Site data to update. See wp_insert_site() for the list of supported keys.
+ * @return int|WP_Error The updated site's ID on success, or error object on failure.
+ */
+function wp_update_site( $site_id, $data ) {
+	global $wpdb;
+
+	$old_site = get_site( $site_id );
+	if ( ! $old_site ) {
+		return new WP_Error( 'site_not_exist', __( 'Site does not exist.' ) );
+	}
+
+	$defaults = $old_site->to_array();
+	$defaults['last_updated'] = current_time( 'mysql' );
+	unset( $defaults['blog_id'] );
+
+	$compat_keys = array(
+		'network_id' => 'site_id',
+	);
+
+	foreach ( $compat_keys as $compat_key => $original_compat_key ) {
+		if ( ! empty( $data[ $compat_key ] ) && empty( $data[ $original_compat_key ] ) ) {
+			$data[ $original_compat_key ] = $data[ $compat_key ];
+		}
+	}
+
+	$data = array_intersect_key( wp_parse_args( $data, $defaults ), $defaults );
+
+	// A domain must always be present.
+	if ( empty( $data['domain'] ) ) {
+		return new WP_Error( 'site_empty_domain', __( 'Site domain must not be empty.' ) );
+	}
+
+	$data['path'] = trailingslashit( '/' . trim( $data['path'], '/' ) );
+
+	// Use the default network if none is set.
+	if ( empty( $data['site_id'] ) ) {
+		$data['site_id'] = get_main_network_id();
+	}
+
+	if ( false === $wpdb->update( $wpdb->blogs, $data, array( 'blog_id' => $old_site->id ) ) ) {
+		return new WP_Error( 'db_update_error', __( 'Could not update site in the database.' ), $wpdb->last_error );
+	}
+
+	clean_blog_cache( $old_site );
+
+	$new_site = get_site( $old_site->id );
+
+	/**
+	 * Fires once a site has been updated in the database.
+	 *
+	 * @since 4.9.0
+	 *
+	 * @param WP_Site $new_site New site object.
+	 * @param WP_Site $old_site Old site object.
+	 */
+	do_action( 'wp_update_site', $new_site, $old_site );
+
+	return (int) $new_site->id;
+}
+
+/**
+ * Deletes a site from the database.
+ *
+ * @since 4.9.0
+ *
+ * @global wpdb $wpdb WordPress database abstraction object.
+ *
+ * @param int $site_id ID of the site that should be deleted.
+ * @return WP_Site|WP_Error The deleted site object on success, or error object on failure.
+ */
+function wp_delete_site( $site_id ) {
+	global $wpdb;
+
+	$old_site = get_site( $site_id );
+	if ( ! $old_site ) {
+		return new WP_Error( 'site_not_exist', __( 'Site does not exist.' ) );
+	}
+
+	if ( false === $wpdb->delete( $wpdb->blogs, array( 'blog_id' => $old_site->id ) ) ) {
+		return new WP_Error( 'db_delete_error', __( 'Could not delete site from the database.' ), $wpdb->last_error );
+	}
+
+	clean_blog_cache( $old_site );
+
+	/**
+	 * Fires once a site has been deleted from the database.
+	 *
+	 * @since 4.9.0
+	 *
+	 * @param WP_Site $old_site Deleted site object.
+	 */
+	do_action( 'wp_delete_site', $old_site );
+
+	return $old_site;
+}
+
+/**
  * Retrieves site data given a site ID or site object.
  *
  * Site data will be cached and returned after being passed through a filter.
@@ -1305,3 +1382,178 @@
 
 	update_posts_count();
 }
+
+/**
+ * Updates the count of sites for a network based on a changed site.
+ *
+ * @since 4.9.0
+ *
+ * @param WP_Site      $new_site The site object that has been inserted, updated or deleted.
+ * @param WP_Site|null $old_site Optional. If $new_site has been updated, this must be the previous
+ *                               state of that site. Default null.
+ */
+function wp_maybe_update_network_site_counts_on_update( $new_site, $old_site = null ) {
+	if ( null === $old_site ) {
+		wp_maybe_update_network_site_counts( $new_site->network_id );
+		return;
+	}
+
+	if ( $new_site->network_id != $old_site->network_id ) {
+		wp_maybe_update_network_site_counts( $new_site->network_id );
+		wp_maybe_update_network_site_counts( $old_site->network_id );
+	}
+}
+
+/**
+ * Triggers actions on site status updates.
+ *
+ * @since 4.9.0
+ *
+ * @param WP_Site $new_site The site object after the update.
+ * @param WP_Site $old_site The site obejct prior to the update.
+ */
+function wp_maybe_transition_site_statuses_on_update( $new_site, $old_site ) {
+	$site_id = $new_site->id;
+
+	if ( $new_site->spam != $old_site->spam ) {
+		if ( $new_site->spam == 1 ) {
+
+			/**
+			 * Fires when the 'spam' status is added to a site.
+			 *
+			 * @since MU (3.0.0)
+			 *
+			 * @param int $site_id Site ID.
+			 */
+			do_action( 'make_spam_blog', $site_id );
+		} else {
+
+			/**
+			 * Fires when the 'spam' status is removed from a site.
+			 *
+			 * @since MU (3.0.0)
+			 *
+			 * @param int $site_id Site ID.
+			 */
+			do_action( 'make_ham_blog', $site_id );
+		}
+	}
+
+	if ( $new_site->mature != $old_site->mature ) {
+		if ( $new_site->mature == 1 ) {
+
+			/**
+			 * Fires when the 'mature' status is added to a site.
+			 *
+			 * @since 3.1.0
+			 *
+			 * @param int $site_id Site ID.
+			 */
+			do_action( 'mature_blog', $site_id );
+		} else {
+
+			/**
+			 * Fires when the 'mature' status is removed from a site.
+			 *
+			 * @since 3.1.0
+			 *
+			 * @param int $site_id Site ID.
+			 */
+			do_action( 'unmature_blog', $site_id );
+		}
+	}
+
+	if ( $new_site->archived != $old_site->archived ) {
+		if ( $new_site->archived == 1 ) {
+
+			/**
+			 * Fires when the 'archived' status is added to a site.
+			 *
+			 * @since MU (3.0.0)
+			 *
+			 * @param int $site_id Site ID.
+			 */
+			do_action( 'archive_blog', $site_id );
+		} else {
+
+			/**
+			 * Fires when the 'archived' status is removed from a site.
+			 *
+			 * @since MU (3.0.0)
+			 *
+			 * @param int $site_id Site ID.
+			 */
+			do_action( 'unarchive_blog', $site_id );
+		}
+	}
+
+	if ( $new_site->deleted != $old_site->deleted ) {
+		if ( $new_site->deleted == 1 ) {
+
+			/**
+			 * Fires when the 'deleted' status is added to a site.
+			 *
+			 * @since 3.5.0
+			 *
+			 * @param int $site_id Site ID.
+			 */
+			do_action( 'make_delete_blog', $site_id );
+		} else {
+
+			/**
+			 * Fires when the 'deleted' status is removed from a site.
+			 *
+			 * @since 3.5.0
+			 *
+			 * @param int $site_id Site ID.
+			 */
+			do_action( 'make_undelete_blog', $site_id );
+		}
+	}
+
+	if ( $new_site->public != $old_site->public ) {
+		if ( $new_site->public == 1 ) {
+
+			/**
+			 * Fires when the 'public' status is added to a site.
+			 *
+			 * @since 4.9.0
+			 *
+			 * @param int $site_id Site ID.
+			 */
+			do_action( 'make_public_blog', $site_id );
+		} else {
+
+			/**
+			 * Fires when the 'public' status is removed from a site.
+			 *
+			 * @since 4.9.0
+			 *
+			 * @param int $site_id Site ID.
+			 */
+			do_action( 'make_private_blog', $site_id );
+		}
+	}
+}
+
+/**
+ * Sets the `blog_public` option to 1 for a given site ID.
+ *
+ * @since 4.9.0
+ *
+ * @param int $site_id Site ID.
+ */
+function wp_make_blog_public_on_site_update( $site_id ) {
+	update_blog_option( $site_id, 'blog_public', 1 );
+}
+
+/**
+ * Sets the `blog_public` option to 0 for a given site ID.
+ *
+ * @since 4.9.0
+ *
+ * @param int $site_id Site ID.
+ */
+function wp_make_blog_private_on_site_update( $site_id ) {
+	update_blog_option( $site_id, 'blog_public', 0 );
+}
Index: src/wp-includes/ms-default-filters.php
===================================================================
--- src/wp-includes/ms-default-filters.php	(revision 41233)
+++ src/wp-includes/ms-default-filters.php	(working copy)
@@ -38,6 +38,12 @@
 add_action( 'wpmu_new_blog', 'newblog_notify_siteadmin', 10, 2 );
 add_action( 'wpmu_activate_blog', 'wpmu_welcome_notification', 10, 5 );
 add_action( 'after_signup_site', 'wpmu_signup_blog_notification', 10, 7 );
+add_action( 'wp_insert_site', 'wp_maybe_update_network_site_counts_on_update', 10, 1 );
+add_action( 'wp_update_site', 'wp_maybe_update_network_site_counts_on_update', 10, 2 );
+add_action( 'wp_delete_site', 'wp_maybe_update_network_site_counts_on_update', 10, 1 );
+add_action( 'wp_update_site', 'wp_maybe_transition_site_statuses_on_update', 10, 2 );
+add_action( 'make_public_blog', 'wp_make_blog_public_on_site_update', 10, 1 );
+add_action( 'make_private_blog', 'wp_make_blog_private_on_site_update', 10, 1 );
 
 // Register Nonce
 add_action( 'signup_hidden_fields', 'signup_nonce_fields' );
Index: src/wp-includes/ms-deprecated.php
===================================================================
--- src/wp-includes/ms-deprecated.php	(revision 41233)
+++ src/wp-includes/ms-deprecated.php	(working copy)
@@ -516,3 +516,37 @@
 
 	return $results;
 }
+
+/**
+ * Store basic site info in the blogs table.
+ *
+ * This function creates a row in the wp_blogs table and returns
+ * the new blog's ID. It is the first step in creating a new blog.
+ *
+ * @since MU (3.0.0)
+ * @deprecated 4.9.0 Use `wp_insert_site()`
+ * @see wp_insert_site()
+ *
+ * @param string $domain  The domain of the new site.
+ * @param string $path    The path of the new site.
+ * @param int    $site_id Unless you're running a multi-network install, be sure to set this value to 1.
+ * @return int|false The ID of the new row
+ */
+function insert_blog($domain, $path, $site_id) {
+	_deprecated_function( __FUNCTION__, '4.9.0', 'wp_insert_site()' );
+
+	$data = array(
+		'domain'  => $domain,
+		'path'    => $path,
+		'site_id' => $site_id,
+	);
+
+	$site_id = wp_insert_site( $data );
+	if ( is_wp_error( $site_id ) ) {
+		return false;
+	}
+
+	refresh_blog_details( $site_id );
+
+	return $site_id;
+}
Index: src/wp-includes/ms-functions.php
===================================================================
--- src/wp-includes/ms-functions.php	(revision 41233)
+++ src/wp-includes/ms-functions.php	(working copy)
@@ -1209,8 +1209,15 @@
 		wp_installing( true );
 	}
 
-	if ( ! $blog_id = insert_blog($domain, $path, $site_id) )
+	$blog_id = wp_insert_site( array(
+		'domain'  => $domain,
+		'path'    => $path,
+		'site_id' => $site_id,
+	) );
+
+	if ( is_wp_error( $blog_id ) ) {
 		return new WP_Error('insert_blog', __('Could not create site.'));
+	}
 
 	switch_to_blog($blog_id);
 	install_blog($blog_id, $title);
@@ -1380,39 +1387,6 @@
 }
 
 /**
- * Store basic site info in the blogs table.
- *
- * This function creates a row in the wp_blogs table and returns
- * the new blog's ID. It is the first step in creating a new blog.
- *
- * @since MU (3.0.0)
- *
- * @global wpdb $wpdb WordPress database abstraction object.
- *
- * @param string $domain  The domain of the new site.
- * @param string $path    The path of the new site.
- * @param int    $site_id Unless you're running a multi-network install, be sure to set this value to 1.
- * @return int|false The ID of the new row
- */
-function insert_blog($domain, $path, $site_id) {
-	global $wpdb;
-
-	$path = trailingslashit($path);
-	$site_id = (int) $site_id;
-
-	$result = $wpdb->insert( $wpdb->blogs, array('site_id' => $site_id, 'domain' => $domain, 'path' => $path, 'registered' => current_time('mysql')) );
-	if ( ! $result )
-		return false;
-
-	$blog_id = $wpdb->insert_id;
-	refresh_blog_details( $blog_id );
-
-	wp_maybe_update_network_site_counts( $site_id );
-
-	return $blog_id;
-}
-
-/**
  * Install an empty blog.
  *
  * Creates the new blog tables and options. If calling this function
@@ -1424,7 +1398,7 @@
  * @global wpdb     $wpdb
  * @global WP_Roles $wp_roles
  *
- * @param int    $blog_id    The value returned by insert_blog().
+ * @param int    $blog_id    The value returned by wp_insert_site().
  * @param string $blog_title The title of the new site.
  */
 function install_blog( $blog_id, $blog_title = '' ) {
Index: tests/phpunit/tests/multisite/site.php
===================================================================
--- tests/phpunit/tests/multisite/site.php	(revision 41233)
+++ tests/phpunit/tests/multisite/site.php	(working copy)
@@ -1028,6 +1028,240 @@
 	function filter_allow_unavailable_languages( $value, $option, $original_value ) {
 		return $original_value;
 	}
+
+	/**
+	 * @ticket 40364
+	 * @dataProvider data_wp_insert_site
+	 */
+	function test_wp_insert_site( $site_data, $expected_data ) {
+		$site_id = wp_insert_site( $site_data );
+
+		$this->assertInternalType( 'integer', $site_id );
+
+		$site = get_site( $site_id );
+		foreach ( $expected_data as $key => $value ) {
+			$this->assertEquals( $value, $site->$key );
+		}
+	}
+
+	function data_wp_insert_site() {
+		return array(
+			array(
+				array(
+					'domain' => 'example.com',
+				),
+				array(
+					'domain'     => 'example.com',
+					'path'       => '/',
+					'network_id' => 1,
+					'public'     => 1,
+					'archived'   => 0,
+					'mature'     => 0,
+					'spam'       => 0,
+					'deleted'    => 0,
+					'lang_id'    => 0,
+				),
+			),
+			array(
+				array(
+					'domain'     => 'example.com',
+					'path'       => '/foo',
+					'network_id' => 2,
+				), array(
+					'domain'     => 'example.com',
+					'path'       => '/foo/',
+					'network_id' => 2,
+				),
+			),
+			array(
+				array(
+					'domain'  => 'example.com',
+					'path'    => '/bar/',
+					'site_id' => 2,
+					'public'  => 0,
+				), array(
+					'domain'     => 'example.com',
+					'path'       => '/bar/',
+					'network_id' => 2,
+					'public'     => 0,
+				),
+			),
+			array(
+				array(
+					'domain'   => 'example.com',
+					'path'     => 'foobar',
+					'public'   => 0,
+					'archived' => 1,
+					'mature'   => 1,
+					'spam'     => 1,
+					'deleted'  => 1,
+					'lang_id'  => 1,
+				), array(
+					'domain'   => 'example.com',
+					'path'     => '/foobar/',
+					'public'   => 0,
+					'archived' => 1,
+					'mature'   => 1,
+					'spam'     => 1,
+					'deleted'  => 1,
+					'lang_id'  => 1,
+				),
+			),
+		);
+	}
+
+	/**
+	 * @ticket 40364
+	 */
+	function test_wp_insert_site_empty_domain() {
+		$site_id = wp_insert_site( array( 'public' => 0 ) );
+
+		$this->assertWPError( $site_id );
+		$this->assertSame( 'site_empty_domain', $site_id->get_error_code() );
+	}
+
+	/**
+	 * @ticket 40364
+	 * @dataProvider data_wp_update_site
+	 */
+	function test_wp_update_site( $site_data, $expected_data ) {
+		$site_id = self::factory()->blog->create();
+
+		$old_site = get_site( $site_id );
+
+		$result = wp_update_site( $site_id, $site_data );
+
+		$this->assertSame( $site_id, $result );
+
+		$new_site = get_site( $site_id );
+		foreach ( $new_site->to_array() as $key => $value ) {
+			if ( isset( $expected_data[ $key ] ) ) {
+				$this->assertEquals( $expected_data[ $key ], $value );
+			} elseif ( 'last_updated' === $key ) {
+				$this->assertTrue( $old_site->last_updated <= $value );
+			} else {
+				$this->assertEquals( $old_site->$key, $value );
+			}
+		}
+	}
+
+	function data_wp_update_site() {
+		return array(
+			array(
+				array(
+					'domain'     => 'example.com',
+					'network_id' => 2,
+				),
+				array(
+					'domain'  => 'example.com',
+					'site_id' => 2,
+				),
+			),
+			array(
+				array(
+					'path' => 'foo',
+				),
+				array(
+					'path' => '/foo/'
+				),
+			),
+			array(
+				array(
+					'public'   => 0,
+					'archived' => 1,
+					'mature'   => 1,
+					'spam'     => 1,
+					'deleted'  => 1,
+					'lang_id'  => 1,
+				),
+				array(
+					'public'   => 0,
+					'archived' => 1,
+					'mature'   => 1,
+					'spam'     => 1,
+					'deleted'  => 1,
+					'lang_id'  => 1,
+				),
+			),
+		);
+	}
+
+	/**
+	 * @ticket 40364
+	 */
+	function test_wp_update_site_empty_domain() {
+		$site_id = self::factory()->blog->create();
+
+		$result = wp_update_site( $site_id, array( 'domain' => '' ) );
+
+		$this->assertWPError( $result );
+		$this->assertSame( 'site_empty_domain', $result->get_error_code() );
+	}
+
+	/**
+	 * @ticket 40364
+	 */
+	function test_wp_update_site_invalid_id() {
+		$result = wp_update_site( 444444, array( 'domain' => 'example.com' ) );
+
+		$this->assertWPError( $result );
+		$this->assertSame( 'site_not_exist', $result->get_error_code() );
+	}
+
+	/**
+	 * @ticket 40364
+	 */
+	function test_wp_update_site_cleans_cache() {
+		$site_id = self::factory()->blog->create();
+		$site1 = get_site( $site_id );
+
+		$result = wp_update_site( $site_id, array( 'public' => 0 ) );
+		$site2 = get_site( $site_id );
+
+		$result = wp_update_site( $site_id, array( 'public' => 1 ) );
+		$site3 = get_site( $site_id );
+
+		$this->assertEquals( 1, $site1->public );
+		$this->assertEquals( 0, $site2->public );
+		$this->assertEquals( 1, $site3->public );
+	}
+
+	/**
+	 * @ticket 40364
+	 */
+	function test_wp_delete_site() {
+		$site_id = self::factory()->blog->create();
+
+		$site = get_site( $site_id );
+
+		$result = wp_delete_site( $site_id );
+
+		$this->assertInstanceOf( 'WP_Site', $result );
+		$this->assertEquals( $result->to_array(), $site->to_array() );
+	}
+
+	/**
+	 * @ticket 40364
+	 */
+	function test_wp_delete_site_invalid_id() {
+		$result = wp_delete_site( 444444 );
+
+		$this->assertWPError( $result );
+		$this->assertSame( 'site_not_exist', $result->get_error_code() );
+	}
+
+	/**
+	 * @ticket 40364
+	 */
+	function test_wp_delete_site_cleans_cache() {
+		$site_id = self::factory()->blog->create();
+
+		get_site( $site_id );
+
+		wp_delete_site( $site_id );
+
+		$this->assertNull( get_site( $site_id ) );
+	}
 }
 
 endif;
