<?php
/**
 * Class DiscoursePublishTest
 *
 * @package WPDiscourse
 */

namespace WPDiscourse\Test;

use WPDiscourse\EmailNotification\EmailNotification;
use WPDiscourse\DiscoursePublish\DiscoursePublish;
use WPDiscourse\Logs\FileHandler;
use WPDiscourse\Test\UnitTest;

/**
 * DiscoursePublish test case.
 */
class DiscoursePublishTest extends UnitTest {


	/**
	 * Instance of DiscoursePublish.
	 *
	 * @access protected
	 * @var \WPDiscourse\DiscoursePublish\DiscoursePublish
	 */
	protected $publish;

	/**
	 * A partial mock instance of EmailNotification.
	 *
	 * @access protected
	 * @var \Mockery\Mock
	 */
	protected $email_notifier;

	/**
	 * Setup test class
	 */
	public static function setUpBeforeClass(): void {
		parent::setUpBeforeClass();
		self::initialize_variables();
	}

	/**
	 * Setup each test.
	 */
	public function setUp(): void {
		parent::setUp();
		$register_actions     = false;
		$this->email_notifier = \Mockery::mock( EmailNotification::class )->makePartial();
		$this->publish        = new DiscoursePublish( $this->email_notifier, $register_actions );
		$this->publish->setup_options( self::$plugin_options );
		$this->publish->setup_logger();
	}

	/**
	 * Sync_to_discourse handles new posts correctly.
	 */
	public function test_sync_to_discourse_when_creating() {
		// Set up a response body for creating a new post.
		$body                = $this->mock_remote_post_success( 'post_create', 'POST' );
		$discourse_post_id   = $body->id;
		$discourse_topic_id  = $body->topic_id;
		$discourse_permalink = self::$discourse_url . '/t/' . $body->topic_slug . '/' . $body->topic_id;
		$discourse_category  = self::$post_atts['meta_input']['publish_post_category'];

		// Add the post.
		$post_id = wp_insert_post( self::$post_atts, false, false );

		// Run the publication.
		$post = get_post( $post_id );
		$this->publish->sync_to_discourse_without_lock( $post_id, $post->title, $post->post_content );

		// Ensure the right post meta is created.
		$this->assertEquals( get_post_meta( $post_id, 'discourse_post_id', true ), $discourse_post_id );
		$this->assertEquals( get_post_meta( $post_id, 'discourse_topic_id', true ), $discourse_topic_id );
		$this->assertEquals( get_post_meta( $post_id, 'discourse_permalink', true ), $discourse_permalink );
		$this->assertEquals( get_post_meta( $post_id, 'publish_post_category', true ), $discourse_category );
		$this->assertEquals( get_post_meta( $post_id, 'wpdc_publishing_response', true ), 'success' );

		// Cleanup.
		wp_delete_post( $post_id );
	}

	/**
	 * Sync_to_discourse when creating a new post with embed error response.
	 */
	public function test_sync_to_discourse_when_creating_with_embed_error() {
		// Set up the error responses.
		$raw_response  = $this->build_response( 'unprocessable', 'embed' );
		$error_message = json_decode( $raw_response['body'] )->errors[0];
		$request       = array(
			'response' => $raw_response,
			'method'   => 'POST',
		);
		$this->mock_remote_post( $request );

		// Add the post.
		$post_id = wp_insert_post( self::$post_atts, false, false );

		// Run the publication.
		$post     = get_post( $post_id );
		$response = $this->publish->sync_to_discourse_without_lock(
			$post_id,
			$post->title,
			$post->post_content
		);

		// Ensure the right error is returned.
		$this->assertEquals( $response, $this->build_post_error() );

		// Ensure the post meta is updated correctly.
		$this->assertEquals( get_post_meta( $post_id, 'wpdc_auto_publish_overridden', true ), 1 );
		$this->assertEquals( get_post_meta( $post_id, 'publish_to_discourse', true ), '' );
		$this->assertEquals( get_post_meta( $post_id, 'wpdc_publishing_error', true ), $error_message );

		// Ensure the right log is created.
		$log = $this->get_last_log();
		$this->assertMatchesRegularExpression( '/publish.ERROR: create_post.post_error/', $log );
		$this->assertMatchesRegularExpression( '/"http_code":' . $raw_response['response']['code'] . '/', $log );
		$this->assertMatchesRegularExpression( '/"response_message":"' . $error_message . '"/', $log );

		// Cleanup.
		wp_delete_post( $post_id );
	}

	/**
	 * Sync_to_discourse when creating a new post with category error response.
	 */
	public function test_sync_to_discourse_when_creating_with_category_error() {
		// Set up the error responses.
		$raw_response  = $this->build_response( 'invalid_parameters', 'category' );
		$error_message = json_decode( $raw_response['body'] )->errors[0];
		$request       = array(
			'response' => $raw_response,
			'method'   => 'POST',
		);
		$this->mock_remote_post( $request );

		// Add the post.
		$post_id = wp_insert_post( self::$post_atts, false, false );

		// Run the publication.
		$post     = get_post( $post_id );
		$response = $this->publish->sync_to_discourse_without_lock(
			$post_id,
			$post->title,
			$post->post_content
		);

		// Ensure the right error is returned.
		$this->assertEquals( $response, $this->build_post_error() );

		// Ensure the post meta is updated correctly.
		$this->assertEquals( get_post_meta( $post_id, 'publish_to_discourse', true ), '' );
		$this->assertEquals( get_post_meta( $post_id, 'wpdc_publishing_error', true ), $error_message );

		// Ensure the right log is created.
		$log = $this->get_last_log();
		$this->assertMatchesRegularExpression( '/publish.ERROR: create_post.post_error/', $log );
		$this->assertMatchesRegularExpression( '/"http_code":' . $raw_response['response']['code'] . '/', $log );
		$this->assertMatchesRegularExpression( '/"response_message":"' . $error_message . '"/', $log );

		// cleanup.
		wp_delete_post( $post_id );
	}

	/**
	 * Sync_to_discourse when creating a new post with invalid body in response.
	 */
	public function test_sync_to_discourse_when_creating_with_response_body_error() {
		// Setup the invalid respond body.
		$response         = $this->build_response( 'success' );
		$response['body'] = '{ "invalid_body" : true }';
		$request          = array(
			'response' => $response,
			'method'   => 'POST',
		);
		$this->mock_remote_post( $request );

		// Add the post.
		$post_id = wp_insert_post( self::$post_atts, false, false );

		// Run the publication.
		$post     = get_post( $post_id );
		$response = $this->publish->sync_to_discourse_without_lock(
			$post_id,
			$post->title,
			$post->post_content
		);

		// Ensure the right error is returned.
		$this->assertEquals( $response, $this->build_body_error() );

		// Ensure the post meta is updated correctly.
		$this->assertEquals( get_post_meta( $post_id, 'publish_to_discourse', true ), '' );
		$this->assertEquals( get_post_meta( $post_id, 'wpdc_publishing_error', true ), 'OK' );
		$this->assertEquals( get_post_meta( $post_id, 'wpdc_publishing_response', true ), 'error' );

		// Ensure the right log is created.
		$log = $this->get_last_log();
		$this->assertMatchesRegularExpression( '/publish.ERROR: create_post.body_validation_error/', $log );

		// cleanup.
		wp_delete_post( $post_id );
	}

	/**
	 * Sync_to_discourse when creating a new post and post is enqueued.
	 */
	public function test_sync_to_discourse_when_creating_with_enqueued_post() {
		// Setup the enqueued response body.
		$response         = $this->build_response( 'success' );
		$response['body'] = '';
		$request          = array(
			'response' => $response,
			'method'   => 'POST',
		);
		$this->mock_remote_post( $request );

		// Add the post.
		$post_id = wp_insert_post( self::$post_atts, false, false );

		// Run the publication.
		$post     = get_post( $post_id );
		$response = $this->publish->sync_to_discourse_without_lock(
			$post_id,
			$post->title,
			$post->post_content
		);

		// Ensure the right error is returned.
		$message = __( 'The published post has been added to the Discourse approval queue', 'wp-discourse' );
		$this->assertEquals( $response, $this->build_notice( $message ) );

		// Ensure the post meta is updated correctly.
		$this->assertEquals( get_post_meta( $post_id, 'publish_to_discourse', true ), 1 );
		$this->assertEquals( get_post_meta( $post_id, 'discourse_post_id', true ), '' );
		$this->assertEquals( get_post_meta( $post_id, 'wpdc_publishing_error', true ), 'queued_topic' );

		// Ensure the right log is created.
		$log = $this->get_last_log();
		$this->assertMatchesRegularExpression( '/publish.WARNING: create_post.queued_topic_notice/', $log );

		// cleanup.
		wp_delete_post( $post_id );
	}

	/**
	 * Sync_to_discourse when creating a new post with direct-db-publication-flags.
	 */
	public function test_sync_to_discourse_when_creating_with_direct_db_publication_flags() {
		// Enable direct db pubilcation flags option.
		self::$plugin_options['direct-db-publication-flags'] = 1;
		$this->publish->setup_options( self::$plugin_options );

		// Set up a response body for creating a new post.
		$body                = $this->mock_remote_post_success( 'post_create', 'POST' );
		$discourse_post_id   = $body->id;
		$discourse_topic_id  = $body->topic_id;
		$discourse_permalink = self::$discourse_url . '/t/' . $body->topic_slug . '/' . $body->topic_id;
		$discourse_category  = self::$post_atts['meta_input']['publish_post_category'];

		// Add the post.
		$post_id = wp_insert_post( self::$post_atts, false, false );

		// Run the publication.
		$post = get_post( $post_id );
		$this->publish->sync_to_discourse_without_lock( $post_id, $post->title, $post->post_content );

		// Ensure the right post meta is created.
		$this->assertEquals( get_post_meta( $post_id, 'discourse_post_id', true ), $discourse_post_id );
		$this->assertEquals( get_post_meta( $post_id, 'discourse_topic_id', true ), $discourse_topic_id );
		$this->assertEquals( get_post_meta( $post_id, 'discourse_permalink', true ), $discourse_permalink );
		$this->assertEquals( get_post_meta( $post_id, 'publish_post_category', true ), $discourse_category );
		$this->assertEquals( get_post_meta( $post_id, 'wpdc_publishing_response', true ), 'success' );

		// Cleanup.
		wp_delete_post( $post_id );
	}

	/**
	 * Sync_to_discourse when creating a new post and pinning topics.
	 */
	public function test_sync_to_discourse_pin_topic() {
		// Set up a response body for creating a new post, with subsequent pin request.
		$discourse_post = json_decode( $this->response_body_file( 'post_create' ) );
		$pin_until      = '2021-02-17';
		$second_request = array(
			'url'      => self::$discourse_url . "/t/{$discourse_post->topic_id}/status",
			'method'   => 'PUT',
			'response' => $this->build_response( 'success' ),
		);
		$body           = $this->mock_remote_post_success( 'post_create', 'POST', $second_request );

		// Add a post that will be pinned.
		$post_atts                                 = self::$post_atts;
		$post_atts['meta_input']['wpdc_pin_until'] = $pin_until;
		$post_id                                   = wp_insert_post( $post_atts, false, false );

		// Run the publication.
		$post     = get_post( $post_id );
		$response = $this->publish->sync_to_discourse_without_lock(
			$post_id,
			$post->title,
			$post->post_content
		);

		// Ensure the right result.
		$this->assertFalse( is_wp_error( $response ) );
		$this->assertTrue( empty( get_post_meta( $post_id, 'wpdc_pin_until', true ) ) );

		// Cleanup.
		wp_delete_post( $post_id );
	}

	/**
	 * Sync_to_discourse when creating a new post with Discourse Username set.
	 */
	public function test_sync_to_discourse_discourse_username() {
		$discourse_username = 'angus2';

		// Setup the user
		$user_id = self::factory()->user->create();
		add_user_meta( $user_id, 'discourse_username', $discourse_username, true );

		// Set up a response body for creating a new post
		$raw_body         = $this->response_body_file( 'post_create' );
		$discourse_post   = json_decode( $raw_body );
		$response         = $this->build_response( 'success' );
		$response['body'] = $raw_body;
		$request          = array(
			'headers'  => array(
				'Api-Key'      => self::$connection_options['api-key'],
				'Api-Username' => $discourse_username,
				'Accept'       => 'application/json',
				'Content-Type' => 'application/json',
			),
			'method'   => 'POST',
			'response' => $response,
		);
		$body             = $this->mock_remote_post( $request );

		// Add the post.
		$post_atts                = self::$post_atts;
		$post_atts['post_author'] = $user_id;
		$post_id                  = wp_insert_post( $post_atts, false, false );

		// Run the publication.
		$post     = get_post( $post_id );
		$response = $this->publish->sync_to_discourse_without_lock(
			$post_id,
			$post->title,
			$post->post_content
		);

		// Ensure the right result.
		$this->assertFalse( is_wp_error( $response ) );

		// Cleanup.
		wp_delete_post( $post_id );
	}

	/**
	 * Sync_to_discourse when creating a new post with Discourse Username set.
	 */
	public function test_sync_to_discourse_discourse_username_with_single_user_api_key() {
		$discourse_username = 'angus';

		// Enable single user api key
		self::$plugin_options['single-user-api-key-publication'] = 1;
		$this->publish->setup_options( self::$plugin_options );

		// Setup the user
		$user_id = self::factory()->user->create();
		add_user_meta( $user_id, 'discourse_username', $discourse_username, true );

		// Set up a response body for creating a new post
		$raw_body         = $this->response_body_file( 'post_create' );
		$discourse_post   = json_decode( $raw_body );
		$response         = $this->build_response( 'success' );
		$response['body'] = $raw_body;
		$first_request    = array(
			'headers'  => array(
				'Api-Key'      => self::$connection_options['api-key'],
				'Api-Username' => self::$connection_options['publish-username'],
				'Accept'       => 'application/json',
				'Content-Type' => 'application/json',
			),
			'method'   => 'POST',
			'response' => $response,
		);
		$second_request   = array(
			'url'      => self::$discourse_url . "/t/{$discourse_post->topic_id}/change-owner",
			'method'   => 'POST',
			'body'     => json_encode(
				array(
					'username' => $discourse_username,
					'post_ids' => array( "{$discourse_post->id}" ),
				)
			),
			'response' => $this->build_response( 'success' ),
		);
		$body             = $this->mock_remote_post( $first_request, $second_request );

		// Add the post.
		$post_atts                = self::$post_atts;
		$post_atts['post_author'] = $user_id;
		$post_id                  = wp_insert_post( $post_atts, false, false );

		// Run the publication.
		$post     = get_post( $post_id );
		$response = $this->publish->sync_to_discourse_without_lock(
			$post_id,
			$post->title,
			$post->post_content
		);

		// Ensure the right result.
		$this->assertFalse( is_wp_error( $response ) );

		// Cleanup.
		wp_delete_post( $post_id );
	}

	/**
	 * Sync_to_discourse when updating a post.
	 */
	public function test_sync_to_discourse_when_updating() {
		// Set up a response body for updating an existing post.
		$body = $this->mock_remote_post_success( 'post_update', 'PUT' );
		$post = $body->post;

		$discourse_post_id   = $post->id;
		$discourse_topic_id  = $post->topic_id;
		$discourse_permalink = self::$discourse_url . '/t/' . $post->topic_slug . '/' . $post->topic_id;
		$discourse_category  = self::$post_atts['meta_input']['publish_post_category'];

		// Add a post that's already been published to Discourse.
		$post_atts                                    = self::$post_atts;
		$post_atts['meta_input']['discourse_post_id'] = $discourse_post_id;
		$post_id                                      = wp_insert_post( $post_atts, false, false );

		// Run the update.
		update_post_meta( $post_id, 'update_discourse_topic', 1 );
		$post = get_post( $post_id );
		$this->publish->sync_to_discourse_without_lock( $post_id, $post->title, $post->post_content );

		// Ensure the right post meta still exists.
		$this->assertEquals( get_post_meta( $post_id, 'discourse_post_id', true ), $discourse_post_id );
		$this->assertEquals( get_post_meta( $post_id, 'discourse_topic_id', true ), $discourse_topic_id );
		$this->assertEquals( get_post_meta( $post_id, 'discourse_permalink', true ), $discourse_permalink );
		$this->assertEquals( get_post_meta( $post_id, 'publish_post_category', true ), $discourse_category );
		$this->assertEquals( get_post_meta( $post_id, 'wpdc_publishing_response', true ), 'success' );

		// Cleanup.
		wp_delete_post( $post_id );
	}

	/**
	 * Sync_to_discourse when updating a post and post is deleted.
	 */
	public function test_sync_to_discourse_when_updating_with_deleted_topic() {
		// Setup the response body for an existing post that's been deleted.
		$response               = $this->build_response( 'success' );
		$raw_body               = $this->response_body_json( 'post_update' );
		$body                   = json_decode( $raw_body );
		$body->post->deleted_at = '2021-03-10T23:06:05.328Z';
		$response['body']       = json_encode( $body );
		$request                = array(
			'response' => $response,
			'method'   => 'PUT',
		);
		$this->mock_remote_post( $request );

		// Add a post that's already been published to Discourse.
		$discourse_post_id                            = $body->post->id;
		$post_atts                                    = self::$post_atts;
		$post_atts['meta_input']['discourse_post_id'] = $discourse_post_id;
		$post_id                                      = wp_insert_post( $post_atts, false, false );

		// Run the update.
		update_post_meta( $post_id, 'update_discourse_topic', 1 );
		$post     = get_post( $post_id );
		$response = $this->publish->sync_to_discourse_without_lock(
			$post_id,
			$post->title,
			$post->post_content
		);

		// Ensure the right error is returned.
		$message = __( 'The Discourse topic associated with this post has been deleted', 'wp-discourse' );
		$this->assertEquals( $response, $this->build_notice( $message ) );

		// Ensure the post meta is updated correctly.
		$this->assertEquals( get_post_meta( $post_id, 'publish_to_discourse', true ), 1 );
		$this->assertEquals( get_post_meta( $post_id, 'discourse_post_id', true ), $discourse_post_id );
		$this->assertEquals( get_post_meta( $post_id, 'wpdc_publishing_error', true ), 'deleted_topic' );

		// Ensure the right log is created.
		$log = $this->get_last_log();
		$this->assertMatchesRegularExpression( '/publish.WARNING: update_post.deleted_topic_notice/', $log );

		// cleanup.
		wp_delete_post( $post_id );
	}

	/**
	 * Sync_to_discourse when updating a post and adding featured link.
	 */
	public function test_sync_to_discourse_when_updating_with_featured_link() {
		// Enable featured link option.
		self::$plugin_options['add-featured-link'] = 1;
		$this->publish->setup_options( self::$plugin_options );

		// Add a post that's already been published to Discourse.
		$body           = json_decode( $this->response_body_json( 'post_update' ) );
		$discourse_post = $body->post;
		$post_atts      = self::$post_atts;
		$post_atts['meta_input']['discourse_post_id'] = $discourse_post->id;
		$post_id                                      = wp_insert_post( $post_atts, false, false );

		// Set up a response body for updating an existing post, and the featured link in the second request.
		$second_request = array(
			'url'      => self::$discourse_url . '/t/' . $discourse_post->topic_slug . '/' . $discourse_post->topic_id,
			'method'   => 'PUT',
			'response' => $this->build_response( 'success' ),
		);
		$body           = $this->mock_remote_post_success( 'post_update', 'PUT', $second_request );
		$post           = $body->post;

		// Run the update.
		update_post_meta( $post_id, 'update_discourse_topic', 1 );
		$post     = get_post( $post_id );
		$response = $this->publish->sync_to_discourse_without_lock(
			$post_id,
			$post->title,
			$post->post_content
		);

		// Ensure the right result.
		$this->assertFalse( is_wp_error( $response ) );

		// Cleanup.
		wp_delete_post( $post_id );
	}

	/**
	 * Sync_to_discourse when updating a post with direct-db-publication-flags.
	 */
	public function test_sync_to_discourse_when_updating_with_direct_db_publication_flags() {
		// Enable direct db pubilcation flags option.
		self::$plugin_options['direct-db-publication-flags'] = 1;
		$this->publish->setup_options( self::$plugin_options );

		// Set up a response body for updating an existing post.
		$body = $this->mock_remote_post_success( 'post_update', 'PUT' );
		$post = $body->post;

		$discourse_post_id   = $post->id;
		$discourse_topic_id  = $post->topic_id;
		$discourse_permalink = self::$discourse_url . '/t/' . $post->topic_slug . '/' . $post->topic_id;
		$discourse_category  = self::$post_atts['meta_input']['publish_post_category'];

		// Add a post that's already been published to Discourse.
		$post_atts                                    = self::$post_atts;
		$post_atts['meta_input']['discourse_post_id'] = $discourse_post_id;
		$post_id                                      = wp_insert_post( $post_atts, false, false );

		// Run the update.
		update_post_meta( $post_id, 'update_discourse_topic', 1 );
		$post   = get_post( $post_id );
		$result = $this->publish->sync_to_discourse_without_lock( $post_id, $post->title, $post->post_content );

		// Ensure the right post meta still exists.
		$this->assertEquals( get_post_meta( $post_id, 'discourse_post_id', true ), $discourse_post_id );
		$this->assertEquals( get_post_meta( $post_id, 'discourse_topic_id', true ), $discourse_topic_id );
		$this->assertEquals( get_post_meta( $post_id, 'discourse_permalink', true ), $discourse_permalink );
		$this->assertEquals( get_post_meta( $post_id, 'publish_post_category', true ), $discourse_category );
		$this->assertEquals( get_post_meta( $post_id, 'wpdc_publishing_response', true ), 'success' );

		// Cleanup.
		wp_delete_post( $post_id );
	}

	/**
	 * Exclude_tags prevents publication if excluded tag is present
	 */
	public function test_exclude_tags_with_exclusionary_tag() {
		if ( version_compare( get_bloginfo( 'version' ), '5.6', '<' ) ) {
			$this->markTestSkipped(
				'Not supported on WordPress version.'
			);
		}

		// Create the exclusionary tag
		$excluded_term = term_exists( 'dont_publish', 'post_tag' );
		if ( ! $excluded_term ) {
			$excluded_term = wp_insert_term( 'dont_publish', 'post_tag' );
		}
		$excluded_term      = get_tag( $excluded_term['term_id'] );
		$excluded_term_slug = $excluded_term->slug;

		// Enable exclude_tags option.
		self::$plugin_options['exclude_tags'] = array( $excluded_term_slug );
		$this->publish->setup_options( self::$plugin_options );

		// Set up a response body for creating a new post.
		$body                          = $this->mock_remote_post_success( 'post_create', 'POST' );
		self::$post_atts['tags_input'] = array( $excluded_term_slug );

		// Add the post.
		$post_id = wp_insert_post( self::$post_atts, false, false );

		// Run the publication.
		$post = get_post( $post_id );
		$this->publish->publish_post_after_save( $post_id, $post );

		// Ensure no publication occurs.
		$this->assertEquals( get_post_meta( $post_id, 'discourse_post_id', true ), null );
		$this->assertEquals( get_post_meta( $post_id, 'wpdc_publishing_response', true ), null );

		// Cleanup.
		wp_delete_post( $post_id );
		wp_delete_term( $excluded_term->ID, 'post_tags' );
	}

	/**
	 * Exclude_tags does not prevent publication if excluded tag is not present
	 */
	public function test_exclude_tags_with_non_exclusionary_tag() {
		// Create a non exclusionary tag
		$term = term_exists( 'publish', 'post_tag' );
		if ( ! $term ) {
			$term = wp_insert_term( 'publish', 'post_tag' );
		}
		$term      = get_tag( $term['term_id'] );
		$term_slug = $term->slug;

		// Create the exclusionary tag
		$excluded_term = term_exists( 'dont_publish', 'post_tag' );
		if ( ! $excluded_term ) {
			$excluded_term = wp_insert_term( 'dont_publish', 'post_tag' );
		}
		$excluded_term      = get_tag( $excluded_term['term_id'] );
		$excluded_term_slug = $excluded_term->slug;

		// Enable excluded tags option.
		self::$plugin_options['exclude_tags'] = array( $excluded_term_slug );
		$this->publish->setup_options( self::$plugin_options );

		// Set up a response body for creating a new post.
		$body                          = $this->mock_remote_post_success( 'post_create', 'POST' );
		$discourse_post_id             = $body->id;
		self::$post_atts['tags_input'] = array( $term_slug );

		// Add the post.
		$post_id = wp_insert_post( self::$post_atts, false, false );

		// Run the publication.
		$post = get_post( $post_id );
		$this->publish->publish_post_after_save( $post_id, $post );

		// Ensure publication occurs.
		$this->assertEquals( get_post_meta( $post_id, 'discourse_post_id', true ), $discourse_post_id );
		$this->assertEquals( get_post_meta( $post_id, 'wpdc_publishing_response', true ), 'success' );

		// Cleanup.
		wp_delete_post( $post_id );
		wp_delete_term( $term->ID, 'post_tags' );
		wp_delete_term( $excluded_term->ID, 'post_tags' );
	}

	/**
	 * wpdc_publish_options filters publish_options
	 */
	public function test_wpdc_publish_options() {
		$tags = array( 'tag1', 'tag2' );

		// Add filter.
		add_filter(
			'wpdc_publish_body',
			function ( $body, $remote_post_type ) use ( $tags ) {
				if ( 'create_post' === $remote_post_type ) {
					$body['tags'] = $tags;
				}
				return $body;
			},
			10,
			2
		);

		$raw_body             = $this->response_body_json( 'post_create' );
			$response         = $this->build_response( 'success' );
			$response['body'] = $raw_body;
		$body                 = json_decode( $raw_body );

		// Check tags are in the body passed to request.
		add_filter(
			'pre_http_request',
			function ( $prempt, $args, $url ) use ( $tags, $response ) {
				$body = json_decode( $args['body'] );

				if ( ! isset( $body->tags ) || ! ( $tags === $body->tags ) ) {
						return new \WP_Error( 'http_request_failed', 'Failed to add tags' );
				} else {
					return $response;
				}
			},
			10,
			3
		);

		// Setup post.
		$post_id           = wp_insert_post( self::$post_atts, false, false );
		$discourse_post_id = $body->id;

		// Run the publication.
		$post = get_post( $post_id );
		$this->publish->publish_post_after_save( $post_id, $post );

		// Ensure publication occurs.
		$this->assertEquals( get_post_meta( $post_id, 'discourse_post_id', true ), $discourse_post_id );
		$this->assertEquals( get_post_meta( $post_id, 'wpdc_publishing_response', true ), 'success' );

		// Cleanup.
		wp_delete_post( $post_id );
	}

	/**
	 * Test that the default value of the force_publish_allowed property prevents force-publishing.
	 */
	public function test_force_publish_allowed_property() {
		// Enable the force-publish option, but don't override default value of the force_publish_allowed property.
		$plugin_options                  = self::$plugin_options;
		$plugin_options['force-publish'] = 1;
		$this->publish->setup_options( $plugin_options );

		// Set up a response body for creating a new post.
		$body              = $this->mock_remote_post_success( 'post_create', 'POST' );
		$discourse_post_id = $body->id;

		// Set publish_to_discourse to 0 to test the force-publish option.
		$post_atts                                       = self::$post_atts;
		$post_atts['meta_input']['publish_to_discourse'] = 0;
		$post_id = wp_insert_post( $post_atts, false, false );

		// Trigger the publish_post_after_save method.
		$post = get_post( $post_id );
		$this->publish->publish_post_after_save( $post_id, $post );

		// Ensure that publication has not occurred.
		$this->assertEmpty( get_post_meta( $post_id, 'discourse_post_id', true ) );

		// Set force_publish_allowed to true.
		$this->publish->force_publish_allowed = true;

		// Trigger the publish_post_after_save method.
		$this->publish->publish_post_after_save( $post_id, $post );

		// Ensure that publication has occurred.
		$this->assertEquals( get_post_meta( $post_id, 'discourse_post_id', true ), $discourse_post_id );
		$this->assertEquals( get_post_meta( $post_id, 'wpdc_publishing_response', true ), 'success' );

		// Cleanup.
		wp_delete_post( $post_id );
	}

	/**
	 * Test that enabling the force-publish overrides the publish_to_discourse meta-data.
	 */
	public function test_force_publish_option() {
		$this->publish->force_publish_allowed = true;
		// Explicitly disable the force-publish option.
		$plugin_options                  = self::$plugin_options;
		$plugin_options['force-publish'] = 0;
		$this->publish->setup_options( $plugin_options );

		// Set up a response body for creating a new post.
		$body              = $this->mock_remote_post_success( 'post_create', 'POST' );
		$discourse_post_id = $body->id;

		// Set publish_to_discourse to 0 to test the force-publish option.
		$post_atts                                       = self::$post_atts;
		$post_atts['meta_input']['publish_to_discourse'] = 0;
		$post_id = wp_insert_post( $post_atts, false, false );

		// Trigger the publish_post_after_save method.
		$post = get_post( $post_id );
		$this->publish->publish_post_after_save( $post_id, $post );

		// Ensure that publication has not occurred.
		$this->assertEmpty( get_post_meta( $post_id, 'discourse_post_id', true ) );

		// Enable the force-publish option.
		$plugin_options                  = self::$plugin_options;
		$plugin_options['force-publish'] = 1;
		$this->publish->setup_options( $plugin_options );

		// Trigger the publish_post_after_save method.
		$this->publish->publish_post_after_save( $post_id, $post );

		// Ensure that publication has occurred.
		$this->assertEquals( get_post_meta( $post_id, 'discourse_post_id', true ), $discourse_post_id );
		$this->assertEquals( get_post_meta( $post_id, 'wpdc_publishing_response', true ), 'success' );

		// Cleanup.
		wp_delete_post( $post_id );
	}

	/**
	 * Test that the force-publish-max-age option prevents older posts from being published.
	 */
	public function test_force_publish_max_age_prevents_older_posts_from_being_published() {
		$this->publish->force_publish_allowed = true;
		$plugin_options                       = self::$plugin_options;
		$plugin_options['force-publish']      = 1;
		// Don't publish posts that were created greater than 2 days ago.
		$plugin_options['force-publish-max-age'] = 2;
		$this->publish->setup_options( $plugin_options );

		// Set up a response hook for creating a new post.
		$body              = $this->mock_remote_post_success( 'post_create', 'POST' );
		$discourse_post_id = $body->id;

		// Set publish_to_discourse to 0 to test the force-publish option.
		// Set post_date to 7 days in the past.
		$post_atts                                       = self::$post_atts;
		$post_atts['meta_input']['publish_to_discourse'] = 0;
		$post_atts['post_date']                          = gmdate( 'Y-m-d H:i:s', strtotime( '-7 days' ) );
		$post_id = wp_insert_post( $post_atts, false, false );

		// Trigger the publish_post_after_save method.
		$post = get_post( $post_id );
		$this->publish->publish_post_after_save( $post_id, $post );

		// Ensure that publication has not occurred.
		$this->assertEmpty( get_post_meta( $post_id, 'discourse_post_id', true ) );

		// Change force-publish-max-age to 20.
		$plugin_options['force-publish-max-age'] = 20;
		$this->publish->setup_options( $plugin_options );

		// Trigger the publish_post_after_save method.
		$this->publish->publish_post_after_save( $post_id, $post );

		// Ensure that publication has occurred.
		$this->assertEquals( get_post_meta( $post_id, 'discourse_post_id', true ), $discourse_post_id );
		$this->assertEquals( get_post_meta( $post_id, 'wpdc_publishing_response', true ), 'success' );

		// Cleanup.
		wp_delete_post( $post_id );
	}

    /**
     * Test that HTML entities are converted to their special characters.
     */
    public function test_conversion_of_html_entities_in_title() {
		$title_with_entities           = 'Title with &amp; and &#8211;';
		$title_with_decoded_entities   = 'Title with & and –';
		self::$post_atts['post_title'] = $title_with_entities;

		$response         = $this->build_response( 'success' );
		$response['body'] = $this->response_body_json( 'post_create' );

		add_filter(
			'pre_http_request',
			function ( $prempt, $args, $url ) use ( $response, $title_with_decoded_entities ) {
				$body = json_decode( $args['body'] );

				if ( $body->title !== $title_with_decoded_entities ) {
						return new \WP_Error( 'http_request_failed', 'Failed to decode title' );
				} else {
					return $response;
				}
				},
			10,
			3
		);

		// Setup post.
		$post_id = wp_insert_post( self::$post_atts, false, false );

		// Run the publication.
		$post = get_post( $post_id );
		$this->publish->publish_post_after_save( $post_id, $post );

		// Ensure publication occurs.
		$this->assertEquals( get_post_meta( $post_id, 'wpdc_publishing_response', true ), 'success' );

		// Cleanup.
		wp_delete_post( $post_id );
    }

	/**
	 * Posts can only be published via XMLRPC by hooking into the wp_discourse_before_xmlrpc_publish filter with a function
	 * that returns `true`.
	 */
	public function test_wp_discourse_before_xmlrpc_publish_filter() {
		$body              = $this->mock_remote_post_success( 'post_create', 'POST' );
		$discourse_post_id = $body->id;
		// Note: `self::$post_atts['meta_input']['publish_to_discourse'] === 1`.
		$post_atts = self::$post_atts;
		$post_id   = wp_insert_post( $post_atts, false, false );

		// Call xmlrpc_publish_post_to_discourse without hooking into the wp_discourse_before_xmlrpc_publish filter.
		$this->publish->xmlrpc_publish_post_to_discourse( $post_id );

		// Ensure that publication has not occurred.
		$this->assertEmpty( get_post_meta( $post_id, 'discourse_post_id', true ) );

		// Hook into the filter to allow for xmlrpc publishing.
		add_filter( 'wp_discourse_before_xmlrpc_publish', '__return_true' );

		$this->publish->xmlrpc_publish_post_to_discourse( $post_id );

		// Ensure that publication has occurred.
		$this->assertEquals( get_post_meta( $post_id, 'discourse_post_id', true ), $discourse_post_id );

		// Cleanup.
		wp_delete_post( $post_id );
		remove_filter( 'wp_discourse_before_xmlrpc_publish', '__return_true' );
	}

	/**
	 * When the auto-publish option is enabled, posts published via XMLRPC will trigger a publish_failure_notification.
	 */
	public function test_xmlrpc_publish_failure_notification() {
		$plugin_options = self::$plugin_options;
		// The  xmlrpc failure notification will only be triggered if the auto-publish option is set.
		$plugin_options['auto-publish'] = 1;
		$this->publish->setup_options( $plugin_options );

		$post_atts = self::$post_atts;
		$post_id   = wp_insert_post( $post_atts, false, false );

		$this->email_notifier->shouldReceive( 'publish_failure_notification' )->withSomeOfArgs( array( 'location' => 'after_xmlrpc_publish' ) )->andReturn( true );

		$this->assertTrue( $this->publish->xmlrpc_publish_post_to_discourse( $post_id ) );

		// Cleanup.
		wp_delete_post( $post_id );
	}

    /**
     * When the auto-publish option is enabled, quick edits of un-published posts are not published
     */
    public function test_quick_edits_of_unpublished_posts() {
		$plugin_options                 = self::$plugin_options;
		$plugin_options['auto-publish'] = 1;

		$publish = \Mockery::mock( $this->publish )->makePartial();
		$publish->setup_options( $plugin_options );

		$post_atts = self::$post_atts;
		$post_id   = wp_insert_post( $post_atts, false, false );

		$publish->shouldReceive( 'quick_edit' )->andReturn( true );

		$post = get_post( $post_id );
		$publish->publish_post_after_save( $post_id, $post );

		$publish->shouldNotReceive( 'sync_to_discourse' );

		$this->assertEmpty( get_post_meta( $post_id, 'discourse_post_id', true ) );

		wp_delete_post( $post_id );
	}

	/**
	 * Successful remote_post request returns original response.
	 */
	public function test_remote_post_success() {
		$success_response = $this->build_response( 'success' );
		$request          = array(
			'response' => $success_response,
			'method'   => 'POST',
		);
		$this->mock_remote_post( $request );
		$response = $this->publish->remote_post( ...self::$remote_post_params );
		$this->assertEquals( $response, $success_response );
	}

	/**
	 * Forbidden remote_post request returns standardised WP_Error and creates correct log.
	 */
	public function test_remote_post_forbidden() {
		$raw_response = $this->build_response( 'forbidden' );
		$request      = array(
			'response' => $raw_response,
			'method'   => 'POST',
		);
		$this->mock_remote_post( $request );

		$response = $this->publish->remote_post( ...self::$remote_post_params );
		$this->assertEquals( $response, $this->build_post_error() );

		$log = $this->get_last_log();
		$this->assertMatchesRegularExpression( '/publish.ERROR: create_post.post_error/', $log );
		$this->assertMatchesRegularExpression( '/"http_code":' . $raw_response['response']['code'] . '/', $log );
	}

	/**
	 * Unprocessable remote_post request returns standardised WP_Error and creates correct log.
	 */
	public function test_remote_post_unprocessable() {
		$raw_response = $this->build_response( 'unprocessable', 'title' );
		$request      = array(
			'response' => $raw_response,
			'method'   => 'POST',
		);
		$this->mock_remote_post( $request );

		$response = $this->publish->remote_post( ...self::$remote_post_params );
		$this->assertEquals( $response, $this->build_post_error() );

		$log = $this->get_last_log();
		$this->assertMatchesRegularExpression( '/publish.ERROR: create_post.post_error/', $log );
		$this->assertMatchesRegularExpression( '/"http_code":' . $raw_response['response']['code'] . '/', $log );
	}

	/**
	 * Forbidden remote_post request returns standardised WP_Error and creates correct log.
	 */
	public function test_remote_post_failed_to_connect() {
		$request = array(
			'method'   => 'POST',
			'response' => new \WP_Error(
				'http_request_failed',
				'cURL error 7: Failed to connect to localhost port 3000: Connection refused'
			),
		);
		$this->mock_remote_post( $request );

		$response = $this->publish->remote_post( ...self::$remote_post_params );
		$this->assertEquals( $response, $this->build_post_error() );

		$log = $this->get_last_log();
		$this->assertMatchesRegularExpression( '/publish.ERROR: create_post.post_error/', $log );
	}

	/**
	 * Build error returned by discourse-publish when post request fails.
	 */
	protected function build_post_error() {
		$message = __( 'An error occurred when communicating with Discourse', 'wp-discourse' );
		return new \WP_Error( 'discourse_publishing_response_error', $message );
	}

	/**
	 * Build error returned by discourse-publish when response body is invalid.
	 */
	protected function build_body_error() {
		$message = __( 'An invalid response was returned from Discourse', 'wp-discourse' );
		return new \WP_Error( 'discourse_publishing_response_error', $message );
	}

	/**
	 * Build an error notice returned by discourse-publish when post queued or topic deleted.
	 */
	protected function build_notice( $message ) {
		return new \WP_Error( 'discourse_publishing_response_notice', $message );
	}

	/**
	 * Initialize static variables used by test class.
	 */
	public static function initialize_variables() {
		self::$remote_post_params = array(
			self::$discourse_url,
			array(
				'timeout' => 30,
				'method'  => 'POST',
				'headers' => array(
					'Api-Key'      => self::$connection_options['api-key'],
					'Api-Username' => self::$connection_options['publish-username'],
				),
				'body'    => http_build_query(
					array(
						'embed_url'        => 'https://wordpress.org/post.php',
						'featured_link'    => null,
						'title'            => 'New Topic Title',
						'raw'              => 'Post content',
						'category'         => 3,
						'skip_validations' => 'true',
						'auto_track'       => 'false',
						'visible'          => 'true',
					)
				),
			),
			'create_post',
			1,
		);
	}
}
