<?php

namespace Automattic\VIP\Search;

use PHPUnit\Framework\MockObject\MockObject;
use WP_UnitTestCase;
use Automattic\Test\Constant_Mocker;
use Automattic\VIP\Utils\Alerts;
use ElasticPress\Elasticsearch;
use ElasticPress\Features;
use ElasticPress\Indexable;
use ElasticPress\Indexables;
use stdClass;
use WP_Error;
use WP_Post;

require_once __DIR__ . '/mock-header.php';
require_once __DIR__ . '/../../../../search/search.php';
require_once __DIR__ . '/../../../../search/includes/classes/class-versioning.php';
require_once __DIR__ . '/../../../../search/elasticpress/elasticpress.php';
require_once __DIR__ . '/../../../../prometheus.php';

class Search_Test extends WP_UnitTestCase {
	public static $mock_global_functions;
	public $test_index_name = 'vip-1234-post-0-v3';

	/** @var Search */
	private $search_instance;

	public function setUp(): void {
		parent::setUp();

		\Automattic\VIP\Prometheus\Plugin::get_instance()->init_registry();
		$this->search_instance = new Search();
		$this->search_instance->load_collector();
		\Automattic\VIP\Prometheus\Plugin::get_instance()->load_collectors();

		self::$mock_global_functions = $this->getMockBuilder( self::class )
			->addMethods( [ 'mock_vip_safe_wp_remote_request', 'mock_wp_remote_request' ] )
			->getMock();

		header_remove();

		// As of PHPUnit 10.x, expectWarning() is removed. We'll use a custom error handler to test for warnings.
		// phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_set_error_handler
		set_error_handler( static function ( int $errno, string $errstr ): never {
			// phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped -- CLI
			throw new \Exception( $errstr, $errno ); // NOSONAR
		}, E_USER_WARNING );
	}

	public function tearDown(): void {
		restore_error_handler();

		Constant_Mocker::clear();
		parent::tearDown();
	}

	public function test_query_es_with_invalid_type() {
		$this->init_es();

		$result = $this->search_instance->query_es( 'foo' );

		$this->assertTrue( is_wp_error( $result ) );
		$this->assertEquals( 'indexable-not-found', $result->get_error_code() );
	}

	/**
	 * Test `ep_index_name` filter for ElasticPress + VIP Search
	 */
	public function test__vip_search_filter_ep_index_name() {
		$this->init_es();

		$indexable = Indexables::factory()->get( 'post' );

		$index_name = apply_filters( 'ep_index_name', 'index-name', 1, $indexable );

		$this->assertEquals( 'vip-123-post-1', $index_name );
	}

	/**
	 * Test `ep_index_name` filter for ElasticPress + VIP Search for global indexes
	 *
	 * On "global" indexes, such as users, no blog id will be present
	 */
	public function test__vip_search_filter_ep_index_name_global_index() {
		$this->init_es();

		$indexable = Indexables::factory()->get( 'post' );

		$index_name = apply_filters( 'ep_index_name', 'index-name', null, $indexable );

		$this->assertEquals( 'vip-123-post', $index_name );
	}

	/**
	 * Test `ep_index_name` filter for ElasticPress + VIP Search
	 *
	 * USE_VIP_ELASTICSEARCH not defined (Elasticseach class doesn't load)
	 *
	 * @runInSeparateProcess -- necessary because the Elasticpress class should not be loaded
	 * @preserveGlobalState disabled
	 */
	public function test__vip_search_filter_ep_index_name__no_constant() {
		self::assertFalse( defined( 'USE_VIP_ELASTICSEARCH' ) );
		self::assertFalse( class_exists( Elasticsearch::class, false ) );

		$mock_indexable = (object) [ 'slug' => 'slug' ];

		$index_name = apply_filters( 'ep_index_name', 'index-name', 1, $mock_indexable );

		$this->assertEquals( 'index-name', $index_name );
	}

	public function vip_search_filter_ep_index_name_with_versions_data() {
		return array(
			array(
				// Active index number
				1,
				// Blog id
				null,
				// Expected index name
				'vip-123-post',
			),
			array(
				// Active index number
				2,
				// Blog id
				null,
				// Expected index name
				'vip-123-post-v2',
			),
			array(
				// Active index number
				1,
				// Blog id
				2,
				// Expected index name
				'vip-123-post-2',
			),
			array(
				// Active index number
				2,
				// Blog id
				2,
				// Expected index name
				'vip-123-post-2-v2',
			),
			array(
				// Active index number
				null,
				// Blog id
				null,
				// Expected index name
				'vip-123-post',
			),
			array(
				// Active index number
				0,
				// Blog id
				null,
				// Expected index name
				'vip-123-post',
			),
		);
	}

	public function vip_search_is_url_query_cacheable_data() {
		return array(
			// Regular search
			array(
				// The $query object
				array(
					'url' => 'https://foo.com/index/_search',
				),
				[
					'method' => 'POST',
				],
				// The expected result
				true,
			),
			// Regular multiget
			array(
				// The $query object
				array(
					'url' => 'https://foo.com/index/_mget',
				),
				[
					'method' => 'POST',
				],

				// The expected result
				true,
			),
			// Regular entity multiget
			array(
				// The $query object
				array(
					'url' => 'https://foo.com/index/type/_doc/_mget',
				),
				[
					'method' => 'POST',
				],
				// The expected result
				true,
			),
			// Regular entity multiget
			array(
				// The $query object
				array(
					'url' => 'https://foo.com/index/type/_doc/123456',
				),
				[
					'method' => 'DELETE',
				],
				// The expected result
				false,
			),
			// Bulk index
			array(
				// The $query object
				array(
					'url' => 'https://foo.com/index/_bulk',
				),
				[
					'method' => 'POST',
				],
				// The expected result
				false,
			),
			// Url containing _bulk
			array(
				// The $query object
				array(
					'url' => 'https://foo.com/index/_bulk/bar?_mget',
				),
				[
					'method' => 'POST',
				],
				// The expected result
				false,
			),
			// Random other url
			array(
				// The $query object
				array(
					'url' => 'https://foo.com/index/type/_anything',
				),
				[
					'method' => 'GET',
				],
				// The expected result
				false,
			),
		);
	}

	/**
	 * Test that we correctly calculate the HTTP request timeout value for ES requests
	 *
	 * @dataProvider vip_search_is_url_query_cacheable_data()
	 */
	public function test__is_url_query_cacheable( $query, $args, $expected_is_cacheable ) {
		$is_cacheable = $this->search_instance->is_url_query_cacheable( $query['url'], $args );

		$this->assertEquals( $expected_is_cacheable, $is_cacheable );
	}

	/**
	 * Test `ep_index_name` filter with versioning
	 *
	 * When current version is 1, the index name should not have a version applied to it
	 *
	 * @dataProvider vip_search_filter_ep_index_name_with_versions_data
	 */
	public function test__vip_search_filter_ep_index_name_with_versions( $current_version, $blog_id, $expected_index_name ) {
		$this->init_es();

		$indexable = Indexables::factory()->get( 'post' );

		// Mock the Versioning class so we can control which version it returns
		$stub = $this->getMockBuilder( Versioning::class )
				->onlyMethods( [ 'get_current_version_number' ] )
				->getMock();

		$stub->expects( $this->once() )
				->method( 'get_current_version_number' )
				->with( $indexable )
				->will( $this->returnValue( $current_version ) );

		$this->search_instance->versioning = $stub;

		$index_name = apply_filters( 'ep_index_name', 'index-name', $blog_id, $indexable );

		$this->assertEquals( $expected_index_name, $index_name );
	}

	public function test__vip_search_filter_ep_index_name_with_overridden_version() {
		Constant_Mocker::define( 'VIP_ORIGIN_DATACENTER', 'dfw' );
		$this->init_es();

		Constant_Mocker::define( 'FILES_CLIENT_SITE_ID', 123 );

		$indexable = Indexables::factory()->get( 'post' );

		add_filter( 'ep_do_intercept_request', [ $this, 'filter_ok_es_requests' ], PHP_INT_MAX, 5 );

		$new_version = $this->search_instance->versioning->add_version( $indexable );

		remove_filter( 'ep_do_intercept_request', [ $this, 'filter_ok_es_requests' ], PHP_INT_MAX );

		$this->assertNotFalse( $new_version, 'Failed to add new version of index' );
		$this->assertNotInstanceOf( WP_Error::class, $new_version, 'Got WP_Error when adding new index version' );

		// Override the version
		$override_result = $this->search_instance->versioning->set_current_version_number( $indexable, 2 );

		$this->assertTrue( $override_result, 'Setting current version number failed' );

		$index_name = apply_filters( 'ep_index_name', 'index-name', null, $indexable );

		$this->assertEquals( 'vip-123-post-v2', $index_name, 'Overridden index name is not correct' );

		// Reset
		$this->search_instance->versioning->reset_current_version_number( $indexable );

		$index_name = apply_filters( 'ep_index_name', 'index-name', null, $indexable );

		$this->assertEquals( 'vip-123-post', $index_name );

		delete_option( Versioning::INDEX_VERSIONS_OPTION );
	}

	public function test__vip_search_sends_http_requests_via_helper_functions() {
		Constant_Mocker::define( 'VIP_ELASTICSEARCH_ENDPOINTS', array(
			'https://es-endpoint1:9235',
		) );

		Constant_Mocker::define( 'VIP_ELASTICSEARCH_USERNAME', 'foo' );
		Constant_Mocker::define( 'VIP_ELASTICSEARCH_PASSWORD', 'bar' );

		$test_user_id = $this->factory()->user->create( array( 'role' => 'administrator' ) );
		wp_set_current_user( $test_user_id );

		self::$mock_global_functions->expects( $this->exactly( 2 ) )
			->method( 'mock_wp_remote_request' )
			->with( $this->callback( function ( $url ) {
				return in_array( $url, [
					'https://es-endpoint1:9235/vip-123-post-1',
					'https://es-endpoint1:9235/vip-123-post-1/_bulk',
				] );
			} ) )
			->willReturn([
				'response' => [ 'code' => 200 ],
				'body'     => '',
			]);

		$this->init_es();
		$indexable = Indexables::factory()->get( 'post' );

		$post_id = $this->factory()->post->create( array(
			'post_title'  => 'Test Post',
			'post_status' => 'publish',
		) );

		$indexable->bulk_index( [ $post_id ] );
	}

	public function test__vip_search_sends_double_writes_when_upgrading() {
		Constant_Mocker::define( 'VIP_ELASTICSEARCH_ENDPOINTS', array(
			'https://es-endpoint:9235',
		) );

		Constant_Mocker::define( 'VIP_ELASTICSEARCH_MIGRATION_IN_PROGRESS', true );

		Constant_Mocker::define( 'VIP_ELASTICSEARCH_USERNAME', 'foo' );
		Constant_Mocker::define( 'VIP_ELASTICSEARCH_PASSWORD', 'bar' );

		$test_user_id = $this->factory()->user->create( array( 'role' => 'administrator' ) );
		wp_set_current_user( $test_user_id );

		self::$mock_global_functions->expects( $this->exactly( 3 ) )
			->method( 'mock_wp_remote_request' )
			->with( $this->callback( function ( $url ) {
				return in_array( $url, [
					'https://es-endpoint:9235/vip-123-post-1',
					'https://es-endpoint:9235/vip-123-post-1/_bulk',
					'https://es-endpoint:9245/vip-123-post-1/_bulk',
				] );
			} ) )
			->willReturn([
				'response' => [ 'code' => 200 ],
				'body'     => '',
			]);

		$this->init_es();
		$indexable = Indexables::factory()->get( 'post' );

		$post_id = $this->factory()->post->create( array(
			'post_title'  => 'Test Post',
			'post_status' => 'publish',
		) );

		$indexable->bulk_index( [ $post_id ] );
	}

	public function test__vip_search_sends_queries_to_next_version_host_when_is_testing_next_version() {
		Constant_Mocker::define( 'VIP_ELASTICSEARCH_ENDPOINTS', array(
			'https://es-endpoint:9235/weirdpath9235',
		) );

		Constant_Mocker::define( 'VIP_ELASTICSEARCH_MIGRATION_IN_PROGRESS', true );
		Constant_Mocker::define( 'VIP_ELASTICSEARCH_TEST_ES_NEXT', true );

		Constant_Mocker::define( 'VIP_ELASTICSEARCH_USERNAME', 'foo' );
		Constant_Mocker::define( 'VIP_ELASTICSEARCH_PASSWORD', 'bar' );

		$test_user_id = $this->factory()->user->create( array( 'role' => 'administrator' ) );
		wp_set_current_user( $test_user_id );

		self::$mock_global_functions->expects( $this->exactly( 3 ) )
			->method( 'mock_wp_remote_request' )
			->with( $this->callback( function ( $url ) {
				return in_array( $url, [
					'https://es-endpoint:9245/weirdpath9235/vip-123-post-1',       // Next version host
					'https://es-endpoint:9245/weirdpath9235/vip-123-post-1/_bulk', // Next version host
					'https://es-endpoint:9235/weirdpath9235/vip-123-post-1/_bulk', // Mirror to the current host
				] );
			} ) )
			->willReturn([
				'response' => [ 'code' => 200 ],
				'body'     => '',
			]);

		$this->init_es();
		$indexable = Indexables::factory()->get( 'post' );

		$post_id = $this->factory()->post->create( array(
			'post_title'  => 'Test Post',
			'post_status' => 'publish',
		) );

		$indexable->bulk_index( [ $post_id ] );
	}

	public function test__vip_search_filter__ep_global_alias() {
		$this->init_es();

		$indexable = Indexables::factory()->get( 'post' );

		$alias_name = $indexable->get_network_alias();

		$this->assertEquals( 'vip-123-post-all', $alias_name );
	}

	public function test__vip_search_filter_ep_default_index_number_of_shards() {
		$this->init_es();

		$shards = apply_filters( 'ep_default_index_number_of_shards', 5 );

		$this->assertEquals( 1, $shards );
	}

	public function test__vip_search_filter_filter__ep_post_mapping__large_site() {
		Constant_Mocker::define( 'VIP_ORIGIN_DATACENTER', 'foo' );
		Constant_Mocker::define( 'VIP_GO_ENV', 'production' );
		$this->init_es();

		// Simulate a large site
		$return_big_count = function ( $counts ) {
			$counts->publish = 2000000;

			return $counts;
		};

		$indexable = Indexables::factory()->get( 'post' );

		add_filter( 'wp_count_posts', $return_big_count );

		if ( method_exists( $indexable, 'build_settings' ) ) {
			$settings = $indexable->build_settings();
		} else {
			$mapping  = $indexable->generate_mapping();
			$settings = $mapping['settings'];
		}

		$this->assertEquals( 4, $settings['index.number_of_shards'] );

		remove_filter( 'wp_count_posts', $return_big_count );
	}

	public function test__vip_search_filter_filter__ep_user_mapping__large_site() {
		Constant_Mocker::define( 'VIP_ORIGIN_DATACENTER', 'foo' );
		Constant_Mocker::define( 'VIP_GO_ENV', 'production' );
		$this->init_es();

		// Activate and set-up the feature
		Features::factory()->activate_feature( 'users' );
		Features::factory()->setup_features();

		// Simulate a large site
		$return_big_count = fn () => [
			'avail_roles' => 100,
			'total_users' => 3000000,
		];

		add_filter( 'pre_count_users', $return_big_count );

		$indexable = Indexables::factory()->get( 'user' );
		if ( method_exists( $indexable, 'build_settings' ) ) {
			$settings = $indexable->build_settings();
		} else {
			$mapping  = $indexable->generate_mapping();
			$settings = $mapping['settings'];
		}
		$this->assertEquals( 4, $settings['index.number_of_shards'] );
	}

	public function test__vip_search_filter_ep_default_index_number_of_replicas() {
		$this->init_es();

		$replicas = apply_filters( 'ep_default_index_number_of_replicas', 2 );

		$this->assertEquals( 1, $replicas );
	}

	public function vip_search_enforces_disabled_features_data() {
		return array(
			array( 'documents' ),
		);
	}

	/**
	 * Test that given an EP Feature slug, that feature is always disabled
	 *
	 * @dataProvider vip_search_enforces_disabled_features_data
	 */
	public function test__vip_search_enforces_disabled_features( $slug ) {
		$this->init_es();

		// Activate the feature
		Features::factory()->activate_feature( $slug );

		// And attempt to force-enable it via filter
		add_filter( 'ep_feature_active', '__return_true' );

		$active = Features::factory()->get_registered_feature( $slug );

		$this->assertFalse( $active );
	}

	/**
	 * Test that we set a default bulk index chunk size limit
	 */
	public function test__vip_search_bulk_chunk_size_default() {
		$this->init_es();

		$this->assertEquals( Constant_Mocker::constant( 'EP_SYNC_CHUNK_LIMIT' ), 500 );
	}

	/**
	 * Test that the default bulk index chunk size limit is not applied if constant is already defined
	 */
	public function test__vip_search_bulk_chunk_size_already_defined() {
		Constant_Mocker::define( 'EP_SYNC_CHUNK_LIMIT', 500 );

		$this->init_es();

		$this->assertEquals( Constant_Mocker::constant( 'EP_SYNC_CHUNK_LIMIT' ), 500 );
	}

	/**
	 * Test that the default bulk index chunk size limit is not defined if we're not using VIP Search
	 */
	public function test__vip_search_bulk_chunk_size_not_defined_when_not_using_vip_search() {
		$this->markTestSkipped( 'Revisit this test' );
		$this->assertEquals( defined( 'EP_SYNC_CHUNK_LIMIT' ), false );
	}

	/**
	 * Test that the ES config constants are set automatically when not already defined and VIP-provided configs are present
	 */
	public function test__vip_search_connection_constants() {
		Constant_Mocker::define( 'VIP_ELASTICSEARCH_ENDPOINTS', array(
			'https://es-endpoint1',
			'https://es-endpoint2',
		) );

		Constant_Mocker::define( 'VIP_ELASTICSEARCH_USERNAME', 'foo' );
		Constant_Mocker::define( 'VIP_ELASTICSEARCH_PASSWORD', 'bar' );

		$this->init_es();

		$this->assertContains( Constant_Mocker::constant( 'EP_HOST' ), Constant_Mocker::constant( 'VIP_ELASTICSEARCH_ENDPOINTS' ) );
		$this->assertEquals( Constant_Mocker::constant( 'ES_SHIELD' ), 'foo:bar' );
	}

	/**
	 * Test that the ES config constants are _not_ set automatically when already defined and VIP-provided configs are present
	 *
	 */
	public function test__vip_search_connection_constants_with_overrides() {
		Constant_Mocker::define( 'VIP_ELASTICSEARCH_ENDPOINTS', array(
			'https://es-endpoint1',
			'https://es-endpoint2',
		) );

		Constant_Mocker::define( 'VIP_ELASTICSEARCH_USERNAME', 'foo' );
		Constant_Mocker::define( 'VIP_ELASTICSEARCH_PASSWORD', 'bar' );

		// Client over-rides - don't fatal
		Constant_Mocker::define( 'EP_HOST', 'https://somethingelse' );
		Constant_Mocker::define( 'ES_SHIELD', 'bar:baz' );

		$this->init_es();

		$this->assertEquals( Constant_Mocker::constant( 'EP_HOST' ), 'https://somethingelse' );
		$this->assertEquals( Constant_Mocker::constant( 'ES_SHIELD' ), 'bar:baz' );
	}

	/**
	 * Test that we are sending HTTP requests through the VIP helper functions
	 */
	public function test__vip_search_has_http_layer_filters() {
		$this->init_es();

		$this->assertEquals( true, has_filter( 'ep_intercept_remote_request', '__return_true' ) );
		$this->assertEquals( true, has_filter( 'ep_do_intercept_request', [ $this->search_instance, 'filter__ep_do_intercept_request' ] ) );
	}

	public function vip_search_get_http_timeout_for_query_data() {
		return array(
			// Regular search
			array(
				// The $query object
				array(
					'url' => 'https://foo.com/index/type/_search',
				),
				// The expected timeout
				2,
			),
			// Bulk index
			array(
				// The $query object
				array(
					'url' => 'https://foo.com/index/type/_bulk',
				),
				// The expected timeout
				30,
			),
			// Url containing _bulk
			array(
				// The $query object
				array(
					'url' => 'https://foo.com/index/type/_bulk/bar?_bulk',
				),
				// The expected timeout
				2,
			),
			// Random other url
			array(
				// The $query object
				array(
					'url' => 'https://foo.com/index/type/_anything',
				),
				// The expected timeout
				2,
			),
			// Opening an index
			array(
				// The $query object
				array(
					'url' => 'https://foo.com/index/type/_open',
				),
				// The expected timeout
				30,
			),
			// Closing an index
			array(
				// The $query object
				array(
					'url' => 'https://foo.com/index/type/_close',
				),
				// The expected timeout
				30,
			),
			// Updating index settings
			array(
				// The $query object
				array(
					'url' => 'https://foo.com/index/type/_settings',
				),
				// The expected timeout
				30,
			),
		);
	}

	/**
	 * Test that we correctly calculate the HTTP request timeout value for ES requests
	 *
	 * @dataProvider vip_search_get_http_timeout_for_query_data()
	 */
	public function test__vip_search_get_http_timeout_for_query( $query, $expected_timeout ) {
		Constant_Mocker::define( 'EP_DASHBOARD_SYNC', 'test' );

		$timeout = $this->search_instance->get_http_timeout_for_query( $query, [ 'method' => 'POST' ] );

		$this->assertEquals( $expected_timeout, $timeout );
	}

	/**
	 * Test that instantiating the HealthJob works as expected (files are properly included, init is hooked)
	 */
	public function test__vip_search_setup_healthchecks_with_enabled() {
		// Need to filter to enable the HealthJob
		add_filter( 'enable_vip_search_healthchecks', '__return_true' );

		$this->init_es();

		$this->search_instance->setup_cron_jobs();
		// Should not have fataled (class was included)

		// Ensure it returns the priority set. Easiest way to to ensure it's not false
		$this->assertTrue( false !== has_action( 'wp_loaded', [ $this->search_instance->healthcheck, 'init' ] ) );
	}

	/**
	 * Test that instantiating the HealthJob does not happen when not in production
	 */
	public function test__vip_search_setup_healthchecks_disabled_in_non_production_env() {
		Constant_Mocker::define( 'VIP_GO_ENV', '999' );
		$this->init_es();

		$this->search_instance->setup_cron_jobs();

		// Should not have fataled (class was included)

		// Should not have instantiated and registered the init action to setup the health check
		$this->assertEquals( false, $this->search_instance->healthcheck->is_enabled() );
	}

	/**
	 * Test that checks both single and multi-host retries
	 */
	public function test__vip_search_filter__ep_pre_request_host() {
		$this->init_es();

		// If VIP_ELASTICSEARCH_ENDPOINTS is not defined, just hand the last host back
		$this->assertEquals( 'test', $this->search_instance->filter__ep_pre_request_host( 'test', 0 ), 'filter__ep_pre_request_host() did\'t just hand the last host back when VIP_ELASTICSEARCH_ENDPOINTS was undefined' );

		Constant_Mocker::define(
			'VIP_ELASTICSEARCH_ENDPOINTS',
			array(
				'endpoint1',
				'endpoint2',
				'endpoint3',
				'endpoint4',
				'endpoint5',
				'endpoint6',
			)
		);

		$this->assertContains( $this->search_instance->filter__ep_pre_request_host( 'endpoint1', 0 ), Constant_Mocker::constant( 'VIP_ELASTICSEARCH_ENDPOINTS' ), 'filter__ep_pre_request_host() didn\'t return a value that exists in VIP_ELASTICSEARCH_ENDPOINTS with 0 total failures' );
		$this->assertContains( $this->search_instance->filter__ep_pre_request_host( 'endpoint1', 107 ), Constant_Mocker::constant( 'VIP_ELASTICSEARCH_ENDPOINTS' ), 'filter__ep_pre_request_host() didn\'t return a value that exists in VIP_ELASTICSEARCH_ENDPOINTS with 107 failures' );
	}

	/*
	 * Test for making sure filter__ep_pre_request_host handles empty endpoint lists
	 */
	public function test__vip_search_filter__ep_pre_request_host_empty_endpoint() {
		$this->init_es();

		Constant_Mocker::define( 'VIP_ELASTICSEARCH_ENDPOINTS', array() );

		$this->assertEquals( 'test', $this->search_instance->filter__ep_pre_request_host( 'test', 0 ) );
	}

	/*
	 * Test for making sure filter__ep_pre_request_host handles endpoint lists that aren't arrays
	 */
	public function test__vip_search_filter__ep_pre_request_host_endpoint_not_array() {
		$this->init_es();

		Constant_Mocker::define( 'VIP_ELASTICSEARCH_ENDPOINTS', 'Random string' );

		$this->assertEquals( 'test', $this->search_instance->filter__ep_pre_request_host( 'test', 0 ) );
	}

	/**
	 * Ensure that we're allowing querying during bulk re-index, via the ep_enable_query_integration_during_indexing filter
	 */
	public function test__vip_search_filter__ep_enable_query_integration_during_indexing() {
		$this->init_es();

		$allowed = apply_filters( 'ep_enable_query_integration_during_indexing', false );

		$this->assertTrue( $allowed );
	}

	/*
	 * Test for making sure the round robin function returns the next array value
	 */
	public function test__vip_search_get_next_host() {
		Constant_Mocker::define( 'VIP_ELASTICSEARCH_ENDPOINTS',
			array(
				'test0',
				'test1',
				'test2',
				'test3',
			)
		);

		$this->assertEquals( 'test0', $this->search_instance->get_next_host( 0 ), 'get_next_host() didn\'t use the same host with 0 total failures and 4 hosts with a starting index of 0' );
		$this->assertEquals( 'test1', $this->search_instance->get_next_host( 1 ), 'get_next_host() didn\'t get the correct host with 1 total failures and 4 hosts with a starting index of 0' );
		$this->assertEquals( 'test0', $this->search_instance->get_next_host( 3 ), 'get_next_host() didn\'t restart at the beginning of the list upon reaching the end with 4 total failures and 4 hosts with a starting index of 1' );
		$this->assertEquals( 'test1', $this->search_instance->get_next_host( 17 ), 'get_next_host() didn\'t match expected result with 21 total failures and 4 hosts. and a starting index of 0' );
	}

	/*
	 * Test for making sure the load balance functionality works
	 */
	public function test__vip_search_get_random_host() {
		$hosts = array(
			'test0',
			'test1',
			'test2',
			'test3',
		);

		$this->assertContains( $this->search_instance->get_random_host( $hosts ), $hosts );
	}

	public function test__send_vary_headers__sent_for_group() {
		$this->init_es();
		$_GET['ep_debug'] = true;

		apply_filters( 'ep_valid_response', array(), array(), array(), array(), null );

		do_action( 'send_headers' );

		// phpcs:ignore WordPress.Security.NonceVerification.Recommended
		unset( $_GET['ep_debug'] );

		$headers = headers_list();
		$this->assertContains( 'X-ElasticPress-Search-Valid-Response: true', $headers, '', true );
	}

	public function test__vip_search_filter__ep_facet_taxonomies_size() {
		$this->init_es();

		$this->assertEquals( 5, $this->search_instance->filter__ep_facet_taxonomies_size( 10000, 'category' ) );
	}

	public function vip_search_filter__jetpack_active_modules() {
		return array(
			// No modules, no change
			array(
				// Input
				array(),

				// Expected
				array(),
			),

			// Search not enabled, no change
			array(
				// Input
				array(
					'foo',
				),

				// Expected
				array(
					'foo',
				),
			),

			// Search enabled, should be removed from list
			array(
				// Input
				array(
					'foo',
					'search',
				),

				// Expected
				array(
					'foo',
				),
			),

			// Search-like module enabled, should not be removed from list
			array(
				// Input
				array(
					'foo',
					'searchbar',
				),

				// Expected
				array(
					'foo',
					'searchbar',
				),
			),

			// Search enabled multiple times, should be removed from list
			array(
				// Input
				array(
					'search',
					'foo',
					'search',
				),

				// Expected
				array(
					'foo',
				),
			),
		);
	}

	/**
	 * Test that our active modules filter works as expected
	 *
	 * @dataProvider vip_search_filter__jetpack_active_modules
	 */
	public function test__vip_search_filter__jetpack_active_modules( $input, $expected ) {
		$this->init_es();

		$result = $this->search_instance->filter__jetpack_active_modules( $input );

		$this->assertEquals( $expected, $result );
	}

	public function vip_search_filter__jetpack_widgets_to_include_data() {
		return array(
			array(
				// Input
				array(
					'/path/to/jetpack/modules/widgets/file.php',
					'/path/to/jetpack/modules/widgets/other.php',
				),

				// Expected
				array(
					'/path/to/jetpack/modules/widgets/file.php',
					'/path/to/jetpack/modules/widgets/other.php',
				),
			),

			array(
				// Input
				array(
					'/path/to/jetpack/modules/widgets/file.php',
					'/path/to/jetpack/modules/widgets/search.php',
					'/path/to/jetpack/modules/widgets/other.php',
				),

				// Expected
				array(
					'/path/to/jetpack/modules/widgets/file.php',
					'/path/to/jetpack/modules/widgets/other.php',
				),
			),

			array(
				// Input
				12345, // non-array

				// Expected
				12345,
			),
		);
	}

	/**
	 * Test that the widgets filter works as expected
	 *
	 * @dataProvider vip_search_filter__jetpack_widgets_to_include_data
	 */
	public function test__vip_search_filter__jetpack_widgets_to_include( $input, $expected ) {
		$this->init_es();

		$result = $this->search_instance->filter__jetpack_widgets_to_include( $input );

		$this->assertEquals( $expected, $result );
	}

	/**
	 * Test that the track_total_hits arg exists
	 */
	public function test__vip_filter__ep_post_formatted_args() {
		$this->init_es();

		$result = $this->search_instance->filter__ep_post_formatted_args( array(), '', '' );

		$this->assertTrue( array_key_exists( 'track_total_hits', $result ), 'track_total_hits doesn\'t exist in fortmatted args' );
		if ( array_key_exists( 'track_total_hits', $result ) ) {
			$this->assertTrue( $result['track_total_hits'], 'track_total_hits isn\'t set to true' );
		}
	}

	public function get_index_name_for_url_data() {
		return array(
			// Search
			array(
				'https://host.com/_search',
				null,
			),
			array(
				'https://host.com/index-name/_search',
				'index-name',
			),
			array(
				'https://host.com/index-name,index-name-2/_search',
				'index-name,index-name-2',
			),
			// Other misc operations
			array(
				'https://host.com/index-name/_bulk',
				'index-name',
			),
			array(
				'https://host.com/index-name/_doc',
				'index-name',
			),
			array(
				'  https://host.com/index-name/_doc  ',
				'index-name',
			),
		);
	}

	/**
	 * Test that we correctly determine the index name from an ES API url for stats purposes
	 *
	 * @dataProvider get_index_name_for_url_data()
	 */
	public function test_get_index_name_for_url( $url, $expected_index_name ) {
		$index_name = $this->search_instance->get_index_name_for_url( $url );

		$this->assertEquals( $expected_index_name, $index_name );
	}

	/**
	 * Ensure we disable indexing of filtered content by default
	 */
	public function test__vip_search_filter__ep_allow_post_content_filtered_index() {
		$this->init_es();

		$enabled = apply_filters( 'ep_allow_post_content_filtered_index', true );

		$this->assertFalse( $enabled );
	}

	/*
	 * Ensure that is_query_integration_enabled() is false by default with no options/constants
	 */
	public function test__is_query_integration_enabled_default() {
		$this->assertFalse( Search::is_query_integration_enabled() );
	}

	/*
	 * Ensure is_query_integration_enabled() option works properly with the vip_enable_vip_search_query_integration option
	 */
	public function test__is_query_integration_enabled_via_option() {
		update_option( 'vip_enable_vip_search_query_integration', true );

		self::assertFalse( defined( 'VIP_ENABLE_ELASTICSEARCH_QUERY_INTEGRATION' ) );
		self::assertFalse( defined( 'VIP_ENABLE_VIP_SEARCH_QUERY_INTEGRATION' ) );
		self::assertTrue( Search::is_query_integration_enabled() );

		delete_option( 'vip_enable_vip_search_query_integration' );
	}

	/*
	 * Ensure is_query_integration_enabled() properly considers VIP_ENABLE_ELASTICSEARCH_QUERY_INTEGRATION
	 */
	public function test__is_query_integration_enabled_via_legacy_constant() {
		Constant_Mocker::define( 'VIP_ENABLE_ELASTICSEARCH_QUERY_INTEGRATION', true );

		$this->assertTrue( Search::is_query_integration_enabled() );
	}

	/*
	 * Ensure is_query_integration_enabled() properly considers VIP_ENABLE_VIP_SEARCH_QUERY_INTEGRATION
	 */
	public function test__is_query_integration_enabled_via_constant() {
		Constant_Mocker::define( 'VIP_ENABLE_VIP_SEARCH_QUERY_INTEGRATION', true );

		$this->assertTrue( Search::is_query_integration_enabled() );
	}

	/**
	 * Ensure query integration is enabled when the 'es' query param is set
	 */
	public function test__is_query_integration_enabled_via_query_param() {
		// Set es query string to test override
		$_GET[ Search::QUERY_INTEGRATION_FORCE_ENABLE_KEY ] = true;

		try {
			$this->assertTrue( Search::is_query_integration_enabled() );
		} finally {
			unset( $_GET[ Search::QUERY_INTEGRATION_FORCE_ENABLE_KEY ] );
		}
	}

	public function test_is_network_mode_default() {
		$this->assertFalse( Search::is_network_mode() );
	}

	public function test_is_network_mode_with_constant() {
		Constant_Mocker::define( 'EP_IS_NETWORK', true );

		$this->assertTrue( Search::is_network_mode() );
	}

	public function test_is_network_mode_with_constant_false() {
		Constant_Mocker::define( 'EP_IS_NETWORK', false );

		$this->assertFalse( Search::is_network_mode() );
	}

	/*
	 * Ensure that filters disabling query integration are honored
	 */
	public function test__ep_skip_query_integration_filter() {
		// Set constants to enable query integration
		Constant_Mocker::define( 'VIP_ENABLE_VIP_SEARCH_QUERY_INTEGRATION', true );

		// We pass in `true` as the starting value for the filter, indicating it should be skipped. We expect that `true` comes back out,
		// even though query integration is enabled, which indicates that we're properly respecting other filters that have already decided
		// this query should be skipped
		$this->assertTrue( Search::ep_skip_query_integration( true ) );
	}

	/*
	 * Ensure that EP query integration is disabled by default
	 */
	public function test__ep_skip_query_integration_default() {
		$this->assertTrue( Search::ep_skip_query_integration( false ) );
	}

	/*
	 * Ensure ratelimiting works properly with ep_skip_query_integration filter
	 */
	public function test__rate_limit_ep_query_integration__triggers() {
		$es = new Search();
		$es->init();

		add_option( 'vip_enable_vip_search_query_integration', true );
		Constant_Mocker::define( 'VIP_ENABLE_VIP_SEARCH_QUERY_INTEGRATION', true );
		$_GET[ Search::QUERY_INTEGRATION_FORCE_ENABLE_KEY ] = true;

		$this->assertFalse( $es->rate_limit_ep_query_integration( false ), 'the default value should be false' );
		$this->assertTrue( $es->rate_limit_ep_query_integration( true ), 'should honor filters that skip query integrations' );

		// Force ratelimiting to apply
		$es::$max_query_count = 0;

		// Force this request to be ratelimited
		$es::$query_db_fallback_value = 11;
		wp_cache_set( $this->search_instance::QUERY_COUNT_CACHE_KEY, 1, $this->search_instance::SEARCH_CACHE_GROUP );

		// ep_skip_query_integration should be true if ratelimited
		$this->assertTrue( $es->rate_limit_ep_query_integration( false ), 'should return true if the query is rate-limited' );
		wp_cache_delete( $this->search_instance::QUERY_COUNT_CACHE_KEY, $this->search_instance::SEARCH_CACHE_GROUP );
	}

	public function test__rate_limit_ep_query_integration__handles_start_correctly() {
		/** @var MockObject&Search */
		$partially_mocked_search = $this->getMockBuilder( Search::class )
			->onlyMethods( [ 'handle_query_limiting_start_timestamp', 'maybe_alert_for_prolonged_query_limiting' ] )
			->getMock();
		$partially_mocked_search->init();

		// Force rate-limiting to apply
		$partially_mocked_search::$max_query_count = 0;

		// Force this request to be rate-limited
		$partially_mocked_search::$query_db_fallback_value = 11;
		wp_cache_set( $this->search_instance::QUERY_COUNT_CACHE_KEY, 1, $this->search_instance::SEARCH_CACHE_GROUP );

		$partially_mocked_search->expects( $this->once() )->method( 'handle_query_limiting_start_timestamp' );
		$partially_mocked_search->expects( $this->once() )->method( 'maybe_alert_for_prolonged_query_limiting' );

		$partially_mocked_search->rate_limit_ep_query_integration( false );
		wp_cache_delete( $this->search_instance::QUERY_COUNT_CACHE_KEY, $this->search_instance::SEARCH_CACHE_GROUP );
	}

	/**
	 * Ensure we don't load es-wp-query by default (if it's not enabled)
	 */
	public function test__should_load_es_wp_query_default() {
		$should = Search::should_load_es_wp_query();

		$this->assertFalse( $should );
	}

	/**
	 * Ensure we don't load es-wp-query if it is already loaded
	 *
	 * @runInSeparateProcess
	 * @preserveGlobalState disabled
	 */
	public function test__should_load_es_wp_query_already_loaded() {
		require_once __DIR__ . '/../../../../search/es-wp-query/es-wp-query.php';

		$this->setExpectedIncorrectUsage( 'Automattic\VIP\Search\Search::should_load_es_wp_query' );

		$should = Search::should_load_es_wp_query();

		$this->assertFalse( $should );
	}

	/**
	 * Ensure we do load es-wp-query when query integration is enabled
	 */
	public function test__should_load_es_wp_query_query_integration() {
		Constant_Mocker::define( 'VIP_ENABLE_VIP_SEARCH_QUERY_INTEGRATION', true );

		$should = Search::should_load_es_wp_query();

		$this->assertTrue( $should );
	}

	/**
	 * Ensure the incrementor for tracking request counts behaves properly
	 */
	public function test__query_count_incr() {
		$query_count_incr = self::get_method( 'query_count_incr' );

		// Reset cache key
		wp_cache_delete( $this->search_instance::QUERY_COUNT_CACHE_KEY, $this->search_instance::SEARCH_CACHE_GROUP );

		$this->assertEquals( 1, $query_count_incr->invokeArgs( $this->search_instance, [] ), 'initial value should be 1' );

		for ( $i = 2; $i < 10; $i++ ) {
			$this->assertEquals( $i, $query_count_incr->invokeArgs( $this->search_instance, [] ), 'value should increment with loop' );
		}
	}

	public function test__truncate_search_string_length__user_no_cap() {
		$expected_search_string = '1nAtu5t4QRo9XmU5VeKFOCTfQN62FrbvvoQXkU1782KOThAlt50NipM7V4dZNGG4eO54HsOQlJaBPStX';
		$provided_search_string = '1nAtu5t4QRo9XmU5VeKFOCTfQN62FrbvvoQXkU1782KOThAlt50NipM7V4dZNGG4eO54HsOQlJaBPStXPRoxWPHqdrHGsGkNQJJshYseaePxCJuGmY7kYp941TUoNF3GhSBEzjajNu0iwdCWrPMLxSJ5XXBltNM9of2LKvwa1hNPOXLka1tyAi8PSZlS53RbGhv7egKOYPyyPpR6mZlzJhx6nXXlZ5t3BtRdQOIvGho6HjdYwdd1hMyHHv1qpggg5oMk1nWsx5fJ0B3bAFYKt1Y5dOA0Q4lQUqj8mf1LjcmR73wQwujc1GQfgCKj9X9Ktr6LrDtN5zAJFQboAJa7fZ9AiGxbJqUrLFs1nAtu5t4QRo9XmU5VeKFOCTfQN62FrbvvoQXkU1782KOThAlt50NipM7V4dZNGG4eO54HsOQlJaBPStXPRoxWPHqdrHGsGkNQJJshYseaePxCJuGmY7kYp941TUoNF3GhSBEzjajNu0iwdCWrPMLxSJ5XXBltNM9of2LKvwa1hNPOXLka1tyAi8PSZlS53RbGhv7egKOYPyyPpR6mZlzJhx6nXXlZ5t3BtRdQOIvGho6HjdYwdd1hMyHHv1qpggg5oMk1nWsx5fJ0B3bAFYKt1Y5dOA0Q4lQUqj8mf1LjcmR73wQwujc1GQfgCKj9X9Ktr6LrDtN5zAJFQboAJa7fZ9AiGxbJqUrLFs88';

		$wp_query_mock = new \WP_Query();

		$wp_query_mock->set( 's', $provided_search_string );
		$wp_query_mock->is_search = true;

		$this->search_instance->truncate_search_string_length( $wp_query_mock );

		$this->assertEquals( $expected_search_string, $wp_query_mock->get( 's' ) );
	}

	public function test__truncate_search_string_length__user_with_cap() {
		$admin_user = $this->factory()->user->create( [
			'user_email' => 'admin@automattic.com',
			'user_login' => 'vip_admin',
			'role'       => 'administrator',
		] );

		wp_set_current_user( $admin_user );

		$expected_search_string = '1nAtu5t4QRo9XmU5VeKFOCTfQN62FrbvvoQXkU1782KOThAlt50NipM7V4dZNGG4eO54HsOQlJaBPStXPRoxWPHqdrHGsGkNQJJshYseaePxCJuGmY7kYp941TUoNF3GhSBEzjajNu0iwdCWrPMLxSJ5XXBltNM9of2LKvwa1hNPOXLka1tyAi8PSZlS53RbGhv7egKOYPyyPpR6mZlzJhx6nXXlZ5t3BtRdQOIvGho6HjdYwdd1hMyHHv1qpggg5oMk1nWsx5fJ0B3bAFYKt1Y5dOA0Q4lQUqj8mf1LjcmR73wQwujc1GQfgCKj9X9Ktr6LrDtN5zAJFQboAJa7fZ9AiGxbJqUrLFs1nAtu5t4QRo9XmU5VeKFOCTfQN62FrbvvoQXkU1782KOThAlt50NipM7V4dZNGG4eO54HsOQlJaBPStXPRoxWPHqdrHGsGkNQJJshYseaePxCJuGmY7kYp941TUoNF3GhSBEzjajNu0iwdCWrPMLxSJ5XXB';
		$provided_search_string = '1nAtu5t4QRo9XmU5VeKFOCTfQN62FrbvvoQXkU1782KOThAlt50NipM7V4dZNGG4eO54HsOQlJaBPStXPRoxWPHqdrHGsGkNQJJshYseaePxCJuGmY7kYp941TUoNF3GhSBEzjajNu0iwdCWrPMLxSJ5XXBltNM9of2LKvwa1hNPOXLka1tyAi8PSZlS53RbGhv7egKOYPyyPpR6mZlzJhx6nXXlZ5t3BtRdQOIvGho6HjdYwdd1hMyHHv1qpggg5oMk1nWsx5fJ0B3bAFYKt1Y5dOA0Q4lQUqj8mf1LjcmR73wQwujc1GQfgCKj9X9Ktr6LrDtN5zAJFQboAJa7fZ9AiGxbJqUrLFs1nAtu5t4QRo9XmU5VeKFOCTfQN62FrbvvoQXkU1782KOThAlt50NipM7V4dZNGG4eO54HsOQlJaBPStXPRoxWPHqdrHGsGkNQJJshYseaePxCJuGmY7kYp941TUoNF3GhSBEzjajNu0iwdCWrPMLxSJ5XXBltNM9of2LKvwa1hNPOXLka1tyAi8PSZlS53RbGhv7egKOYPyyPpR6mZlzJhx6nXXlZ5t3BtRdQOIvGho6HjdYwdd1hMyHHv1qpggg5oMk1nWsx5fJ0B3bAFYKt1Y5dOA0Q4lQUqj8mf1LjcmR73wQwujc1GQfgCKj9X9Ktr6LrDtN5zAJFQboAJa7fZ9AiGxbJqUrLFs88';

		$wp_query_mock = new \WP_Query();

		$wp_query_mock->set( 's', $provided_search_string );
		$wp_query_mock->is_search = true;

		$this->search_instance->truncate_search_string_length( $wp_query_mock );

		$this->assertEquals( $expected_search_string, $wp_query_mock->get( 's' ) );
	}

	public function test__limit_field_limit_absolute_maximum_is_20000() {
		$this->setExpectedIncorrectUsage( 'limit_field_limit' );

		$this->assertEquals( 20000, $this->search_instance->limit_field_limit( 1000000 ) );
	}

	public function test__limit_field_limit_should_respect_values_under_maximum() {
		$this->assertEquals( 777, $this->search_instance->limit_field_limit( 777 ) );
	}

	public function test__ep_total_field_limit_should_limit_total_fields() {
		$this->setExpectedIncorrectUsage( 'limit_field_limit' );

		$this->init_es();

		add_filter( 'ep_total_field_limit', fn() => 1000000 );
		$this->assertEquals( 20000, apply_filters( 'ep_total_field_limit', 5000 ) );
	}

	public function test__ep_total_field_limit_should_respect_values_under_the_limit() {
		$this->init_es();

		add_filter( 'ep_total_field_limit', fn() => 787 );

		$this->assertEquals( 787, apply_filters( 'ep_total_field_limit', 5000 ) );
	}

	public function get_filter__ep_sync_taxonomies_default_data() {
		return array(
			array(
				array(),
			),
			array(
				array(
					(object) array(
						'name' => 'category',
					),
				),
			),
			array(
				array(
					(object) array(
						'name' => 'category',
					),
					(object) array(
						'name' => 'post_tag',
					),
				),
			),
		);
	}

	/**
	 * @dataProvider get_filter__ep_sync_taxonomies_default_data
	 */
	public function test__filter__ep_sync_taxonomies_default( $input_taxonomies ) {
		$this->init_es( false );

		$post = new stdClass();

		$filtered_taxonomies = apply_filters( 'ep_sync_taxonomies', $input_taxonomies, $post );

		$input_taxonomy_names    = wp_list_pluck( $input_taxonomies, 'name' );
		$filtered_taxonomy_names = wp_list_pluck( $filtered_taxonomies, 'name' );

		// No change expected
		$this->assertEquals( $input_taxonomy_names, $filtered_taxonomy_names );
	}

	public function test__filter__ep_sync_taxonomies_added() {
		$this->init_es( false );

		$post = new stdClass();

		$start_taxonomies = array(
			(object) array(
				'name' => 'category',
			),
		);

		add_filter(
			'vip_search_post_taxonomies_allow_list',
			function ( $taxonomies ) {
				$taxonomies[] = 'post_tag';
				$taxonomies[] = 'post_tag';

				return $taxonomies;
			}
		);

		$filtered_taxonomies = apply_filters( 'ep_sync_taxonomies', $start_taxonomies, $post );

		// Pull out just the names, for easier comparison
		$filtered_taxonomy_names = wp_list_pluck( $filtered_taxonomies, 'name' );

		$expected_taxonomy_names = array(
			'category',
			'post_tag',
		);

		// Should now include the additional taxonomies
		$this->assertEquals( $expected_taxonomy_names, $filtered_taxonomy_names );
	}

	public function test__filter__ep_sync_taxonomies_removed() {
		$this->init_es();

		$post = new stdClass();

		$start_taxonomies = array(
			(object) array(
				'name' => 'category',
			),
			(object) array(
				'name' => 'post_tag',
			),
		);

		add_filter( 'vip_search_post_taxonomies_allow_list', fn() => [ 'post_tag' ] );

		$filtered_taxonomies = apply_filters( 'ep_sync_taxonomies', $start_taxonomies, $post );

		// Pull out just the names, for easier comparison
		$filtered_taxonomy_names = wp_list_pluck( $filtered_taxonomies, 'name' );
		$expected_taxonomy_names = [ 'post_tag' ];

		// Should now not include the removed taxonomies
		$this->assertEquals( $expected_taxonomy_names, $filtered_taxonomy_names );
	}

	public function test__is_jetpack_migration() {
		Constant_Mocker::define( 'VIP_SEARCH_MIGRATION_SOURCE', 'jetpack' );

		$this->assertTrue( $this->search_instance->is_jetpack_migration() );
	}

	public function test__is_jetpack_migration__no_constant() {
		$this->assertFalse( $this->search_instance->is_jetpack_migration() );
	}

	public function test__is_jetpack_migration__different_value() {
		Constant_Mocker::define( 'VIP_SEARCH_MIGRATION_SOURCE', 'foo' );

		$this->assertFalse( $this->search_instance->is_jetpack_migration() );
	}

	public function test__filter__ep_prepare_meta_data_allow_list_should_be_respected_by_default() {
		add_filter(
			'vip_search_post_meta_allow_list',
			function () {
				return array(
					'random_post_meta',
					'another_one',
					'third',
				);
			}
		);

		// Matches allow list
		$post_meta = array(
			'random_post_meta' => array(
				'Random value',
			),
			'another_one'      => array(
				'4656784',
			),
			'third'            => array(
				'true',
			),
		);

		$post_meta['random_thing_not_allow_listed'] = array( 'Missing' );

		$post     = new WP_Post( new stdClass() );
		$post->ID = 0;

		$meta = $this->search_instance->filter__ep_prepare_meta_data( $post_meta, $post );

		unset( $post_meta['random_thing_not_allow_listed'] ); // Remove last added value that should have been excluded by the filter

		$this->assertEquals( $meta, $post_meta );
	}

	public function test__filter__ep_prepare_meta_data_allow_list_should_be_respected_by_default_assoc() {
		$es = new Search();

		add_filter(
			'vip_search_post_meta_allow_list',
			function () {
				return array(
					'random_post_meta' => true,
					'another_one'      => true,
					'skipped'          => false,
					'skipped_another'  => 4,
					'skipped_string'   => 'Wooo',
					'third'            => true,
				);
			}
		);

		// Matches allow list
		$post_meta = array(
			'random_post_meta' => array(
				'Random value',
			),
			'another_one'      => array(
				'4656784',
			),
			'skipped'          => array(
				'Skip',
			),
			'skipped_another'  => array(
				'Skip',
			),
			'skipped_string'   => array(
				'Skip',
			),
			'third'            => array(
				'true',
			),
		);

		$post_meta['random_thing_not_allow_listed'] = array( 'Missing' );

		$post     = new WP_Post( new stdClass() );
		$post->ID = 0;

		$meta = $es->filter__ep_prepare_meta_data( $post_meta, $post );

		$this->assertEquals(
			$meta,
			array(
				'random_post_meta' => array(
					'Random value',
				),
				'another_one'      => array(
					'4656784',
				),
				'third'            => array(
					'true',
				),
			)
		);
	}

	/**
	 * This tests the correct implementaton of the ep_$indexable_mapping filters, but note that these filters
	 * operate on the mapping and settings together - EP doesn't yet distinguish between them
	 */
	public function test__filter__ep_indexable_mapping() {
		Constant_Mocker::define( 'VIP_ORIGIN_DATACENTER', 'dfw' );
		$this->init_es();

		// Should apply to all indexables
		$indexables = Indexables::factory()->get_all();

		// Make sure the above worked
		$this->assertNotEmpty( $indexables, 'Indexables array was empty' );

		foreach ( $indexables as $indexable ) {
			if ( method_exists( $indexable, 'build_settings' ) ) {
				$settings = $indexable->build_settings();
			} else {
				$mapping  = $indexable->generate_mapping();
				$settings = $mapping['settings'];
			}

			$this->assertEquals( 'dfw', $settings['index.routing.allocation.include.dc'], 'Indexable ' . $indexable->slug . ' has the wrong routing allocation' );
		}
	}

	public function test__filter__ep_indexable_mapping_invalid_datacenter() {
		Constant_Mocker::define( 'VIP_ORIGIN_DATACENTER', 'foo' );
		$this->init_es();

		// Should apply to all indexables
		$indexables = Indexables::factory()->get_all();

		// Make sure the above worked
		$this->assertNotEmpty( $indexables, 'Indexables array was empty' );

		foreach ( $indexables as $indexable ) {
			if ( method_exists( $indexable, 'build_settings' ) ) {
				$settings = $indexable->build_settings();
			} else {
				$mapping  = $indexable->generate_mapping();
				$settings = $mapping['settings'];
			}

			// Datacenter was invalid, so it should not have added the allocation settings
			$this->assertArrayNotHasKey( 'index.routing.allocation.include.dc', $settings, 'Indexable ' . $indexable->slug . ' incorrectly defined the allocation settings' );
		}
	}

	public function test__get_index_routing_allocation_include_dc_from_constant() {
		Constant_Mocker::define( 'VIP_ORIGIN_DATACENTER', 'dca' );
		$this->init_es();

		$origin_dc = $this->search_instance->get_index_routing_allocation_include_dc();

		$this->assertEquals( 'dca', $origin_dc );
	}

	public function get_index_routing_allocation_include_dc_from_endpoints_data() {
		return array(
			// Valid
			array(
				// Endpoints to define in VIP_ELASTICSEARCH_ENDPOINTS
				array(
					'https://es-ha.dfw.vipv2.net:1234',
				),
				// Expected datacenter
				'dfw',
			),
			array(
				// Endpoints to define in VIP_ELASTICSEARCH_ENDPOINTS
				array(
					'https://es-ha.bur.vipv2.net/some/path',
				),
				// Expected datacenter
				'bur',
			),
			// Unknown dc
			array(
				// Endpoints to define in VIP_ELASTICSEARCH_ENDPOINTS
				array(
					'https://es-ha.bar.vipv2.net:1234',
				),
				// Expected datacenter
				null,
			),
			// Weird format
			array(
				// Endpoints to define in VIP_ELASTICSEARCH_ENDPOINTS
				array(
					'https://test:test@foo.com/bar/baz',
				),
				// Expected datacenter
				null,
			),
		);
	}

	/**
	 * @dataProvider get_index_routing_allocation_include_dc_from_endpoints_data
	 */
	public function test__get_index_routing_allocation_include_dc_from_endpoints( $endpoints, $expected ) {
		Constant_Mocker::define( 'VIP_ELASTICSEARCH_ENDPOINTS', $endpoints );
		Constant_Mocker::define( 'EP_DASHBOARD_SYNC', 'test' );
		$this->search_instance->init();

		$origin_dc = $this->search_instance->get_index_routing_allocation_include_dc();

		$this->assertEquals( $expected, $origin_dc );
	}

	public function get_origin_dc_from_es_endpoint_data() {
		return array(
			array(
				'https://es-ha.bur.vipv2.net:1234',
				'bur',
			),
			array(
				'https://es-ha.dca.vipv2.net:4321',
				'dca',
			),
			array(
				'https://es-ha.DCA.vipv2.net:4321',
				'dca',
			),
			array(
				'https://es-ha.dfw.vipv2.net:4321',
				'dfw',
			),
		);
	}

	/**
	 * @dataProvider get_origin_dc_from_es_endpoint_data
	 */
	public function test__get_origin_dc_from_es_endpoint( $host, $expected ) {
		Constant_Mocker::define( 'EP_DASHBOARD_SYNC', 'test' );
		$this->search_instance->init();

		$origin_dc = $this->search_instance->get_origin_dc_from_es_endpoint( $host );

		$this->assertEquals( $expected, $origin_dc );
	}

	public function get_post_meta_allow_list__combinations_for_jetpack_migration_data() {
		return [
			[
				null, // VIP search
				null, // Jetpack filter added
				array_merge( Search::POST_META_DEFAULT_ALLOW_LIST, Search::JETPACK_POST_META_DEFAULT_ALLOW_LIST ), // expected
			],
			[
				[ 'foo' ], // VIP search
				null, // Jetpack filter added
				array_merge( Search::POST_META_DEFAULT_ALLOW_LIST, Search::JETPACK_POST_META_DEFAULT_ALLOW_LIST, [ 'foo' ] ), // expected
			],
			[
				// keys provided by VIP and JP filters
				[ 'foo' ], // VIP search
				[ 'bar' ], // Jetpack filter added
				array_merge( Search::POST_META_DEFAULT_ALLOW_LIST, Search::JETPACK_POST_META_DEFAULT_ALLOW_LIST, [ 'bar', 'foo' ] ), // expected
			],
			[
				// keys from empty VIP filter, JP filter
				[], // VIP search
				[ 'bar' ], // Jetpack filter added
				array_merge( Search::POST_META_DEFAULT_ALLOW_LIST, Search::JETPACK_POST_META_DEFAULT_ALLOW_LIST, [ 'bar' ] ), // expected
			],
			[
				// No VIP filter, JP filter
				null, // VIP search
				[ 'bar' ], // Jetpack filter added
				array_merge( Search::POST_META_DEFAULT_ALLOW_LIST, Search::JETPACK_POST_META_DEFAULT_ALLOW_LIST, [ 'bar' ] ), // expected
			],
		];
	}

	/**
	 * @dataProvider get_post_meta_allow_list__combinations_for_jetpack_migration_data
	 */
	public function test__get_post_meta_allow_list__combinations_for_jetpack_migration( $vip_search_keys, $jetpack_added, $expected ) {
		Constant_Mocker::define( 'VIP_SEARCH_MIGRATION_SOURCE', 'jetpack' );

		remove_all_filters( 'vip_search_post_meta_allow_list' );
		remove_all_filters( 'jetpack_sync_post_meta_whitelist' );
		$this->init_es();

		$post     = new WP_Post( new stdClass() );
		$post->ID = 0;

		if ( is_array( $vip_search_keys ) ) {
			add_filter( 'vip_search_post_meta_allow_list', function ( $post_meta ) use ( $vip_search_keys ) {
				return array_merge( $post_meta, $vip_search_keys );
			});
		}

		if ( is_array( $jetpack_added ) ) {
			add_filter( 'jetpack_sync_post_meta_whitelist', function ( $post_meta ) use ( $jetpack_added ) {
				return array_merge( $post_meta, $jetpack_added );
			});
		}

		$result = $this->search_instance->get_post_meta_allow_list( $post );

		$this->assertEquals( $expected, $result );
	}

	public function get_post_meta_allow_list__combinations_not_jetpack_migration_data() {
		return [
			[
				null, // VIP search
				null, // Jetpack filter added
				Search::POST_META_DEFAULT_ALLOW_LIST, // expected
			],
			[
				[ 'foo' ], // VIP search
				null, // Jetpack filter added
				array_merge( Search::POST_META_DEFAULT_ALLOW_LIST, [ 'foo' ] ), // expected
			],
			[
				// keys provided by VIP and JP filters
				[ 'foo' ], // VIP search
				[ 'bar' ], // Jetpack filter added
				array_merge( Search::POST_META_DEFAULT_ALLOW_LIST, [ 'foo' ] ), // expected
			],
			[
				// keys from empty VIP filter, JP filter
				[], // VIP search
				[ 'bar' ], // Jetpack filter added
				Search::POST_META_DEFAULT_ALLOW_LIST, // expected
			],
			[
				// No VIP filter, JP filter
				null, // VIP search
				[ 'bar' ], // Jetpack filter added
				Search::POST_META_DEFAULT_ALLOW_LIST, // expected
			],
		];
	}

	/**
	 * @dataProvider get_post_meta_allow_list__combinations_not_jetpack_migration_data
	 */
	public function test__get_post_meta_allow_list__combinations_not_jetpack_migration( $vip_search_keys, $jetpack_added, $expected ) {
		$this->init_es();

		$post     = new WP_Post( new stdClass() );
		$post->ID = 0;

		if ( is_array( $vip_search_keys ) ) {
			add_filter( 'vip_search_post_meta_allow_list', function ( $post_meta ) use ( $vip_search_keys ) {
				return array_merge( $post_meta, $vip_search_keys );
			});
		}

		if ( is_array( $jetpack_added ) ) {
			add_filter( 'jetpack_sync_post_meta_whitelist', function ( $post_meta ) use ( $jetpack_added ) {
				return array_merge( $post_meta, $jetpack_added );
			});
		}

		$result = $this->search_instance->get_post_meta_allow_list( $post );

		$this->assertEquals( $expected, $result );
	}

	public function get_post_meta_allow_list__processing_array_data() {
		return [
			[
				[ 'foo' ], // input
				[ 'foo' ],  // expected
			],
			[
				'non-array', // input
				[],  // expected
			],
			[
				// assoc array -> only true goes
				[
					'foo'         => true,
					'bar'         => false,
					'string-true' => 'true',
					'number'      => 1,
				],
				[ 'foo' ],  // expected
			],
		];
	}

	/**
	 * @dataProvider get_post_meta_allow_list__processing_array_data
	 */
	public function test__get_post_meta_allow_list__processing_array( $returned_by_filter, $expected ) {
		$this->init_es();

		$post     = new WP_Post( new stdClass() );
		$post->ID = 0;

		// clearing up jetpack values as those are put by default to vip_search_post_meta_allow_list but are not the object of testing here
		add_filter( 'jetpack_sync_post_meta_whitelist', '__return_empty_array' );

		add_filter( 'vip_search_post_meta_allow_list', function () use ( $returned_by_filter ) {
			return $returned_by_filter;
		}, 0);

		$result = $this->search_instance->get_post_meta_allow_list( $post );

		$this->assertEquals( $expected, $result );
	}

	public function test__filter__ep_skip_post_meta_sync_should_return_true_if_meta_not_in_allow_list() {
		$post_id = $this->factory()->post->create( array( 'post_title' => 'Test Post' ) );

		$post = get_post( $post_id );

		$this->init_es();

		$this->assertTrue( $this->search_instance->filter__ep_skip_post_meta_sync( false, $post, 40, 'random_key', 'random_value' ) );
	}

	public function test__filter__ep_skip_post_meta_sync_should_return_false_if_meta_is_in_allow_list() {
		$post_id = $this->factory()->post->create( array( 'post_title' => 'Test Post' ) );

		$post = get_post( $post_id );

		add_filter( 'vip_search_post_meta_allow_list', fn() => [ 'random_key' ] );

		$this->init_es();

		$this->assertFalse( $this->search_instance->filter__ep_skip_post_meta_sync( false, $post, 40, 'random_key', 'random_value' ) );
	}

	public function test__filter__ep_skip_post_meta_sync_should_return_true_if_a_previous_filter_is_true() {
		$post_id = $this->factory()->post->create( array( 'post_title' => 'Test Post' ) );

		$post = get_post( $post_id );

		add_filter( 'vip_search_post_meta_allow_list', fn() => [ 'random_key' ] );

		$this->init_es();

		$this->assertTrue( $this->search_instance->filter__ep_skip_post_meta_sync( true, $post, 40, 'random_key', 'random_value' ) );
	}

	public function test__ep_skip_post_meta_sync_filter_should_return_true_if_meta_not_in_allow_list() {
		$post_id = $this->factory()->post->create( array( 'post_title' => 'Test Post' ) );

		$post = get_post( $post_id );

		$this->init_es();

		$this->assertTrue( apply_filters( 'ep_skip_post_meta_sync', false, $post, 40, 'random_key', 'random_value' ) );
	}

	public function test__ep_skip_post_meta_sync_filter_should_return_false_if_meta_is_in_allow_list() {
		$post_id = $this->factory()->post->create( array( 'post_title' => 'Test Post' ) );

		$post = get_post( $post_id );

		add_filter( 'vip_search_post_meta_allow_list', fn() => [ 'random_key' ] );

		$this->init_es();

		$this->assertFalse( apply_filters( 'ep_skip_post_meta_sync', false, $post, 40, 'random_key', 'random_value' ) );
	}

	public function test__ep_skip_post_meta_sync_filter_should_return_true_if_a_previous_filter_is_true() {
		$post_id = $this->factory()->post->create( array( 'post_title' => 'Test Post' ) );

		$post = get_post( $post_id );

		add_filter( 'vip_search_post_meta_allow_list', fn() => [ 'random_key' ] );

		$this->init_es();

		$this->assertTrue( apply_filters( 'ep_skip_post_meta_sync', true, $post, 40, 'random_key', 'random_value' ) );
	}

	public function filter__ep_prepare_meta_allowed_protected_keys__should_use_post_meta_allow_list_data() {
		return [
			[
				[], // default
				[], // new
				[], // expected
			],
			[
				[ 'foo' ], // default
				[ 'bar' ], // new
				[ 'foo', 'bar' ], // expected
			],
			[
				// should handle assoc array
				[], // default
				[
					'foo' => true,
					'bar' => false,
				],
				[ 'foo' ], // expected
			],
		];
	}

	/**
	 * @dataProvider filter__ep_prepare_meta_allowed_protected_keys__should_use_post_meta_allow_list_data
	 */
	public function test__filter__ep_prepare_meta_allowed_protected_keys__should_use_post_meta_allow_list( $default_ep_protected_keys, $added_keys, $expected ) {
		self::assertFalse( defined( 'VIP_SEARCH_MIGRATION_SOURCE' ) );

		$post     = new WP_Post( new stdClass() );
		$post->ID = 0;

		// clearing up jetpack values as those are put by default to vip_search_post_meta_allow_list but are not the object of testing here
		add_filter( 'jetpack_sync_post_meta_whitelist', '__return_empty_array' );

		add_filter( 'vip_search_post_meta_allow_list', function ( $meta_keys ) use ( $added_keys ) {
			return array_merge( $meta_keys, $added_keys );
		}, 0);

		$this->init_es();

		$result = \apply_filters( 'ep_prepare_meta_allowed_protected_keys', $default_ep_protected_keys, $post );

		$this->assertEquals( $expected, $result );
	}

	public function test__maybe_alert_for_average_queue_time__sends_notification() {
		$application_id      = 123;
		$application_url     = 'http://example.org';
		$average_queue_value = 3601;
		$queue_count_value   = 1;
		$longest_queue_value = $average_queue_value;
		$expected_message    = "Average index queue wait time for application {$application_id} - {$application_url} is currently {$average_queue_value} seconds. There are {$queue_count_value} items in the queue and the oldest item is {$longest_queue_value} seconds old";
		$expected_level      = 2;

		$this->search_instance->init();

		$alerts_mocked   = $this->createMock( Alerts::class );
		$queue_mocked    = $this->createMock( Queue::class );
		$indexables_mock = $this->createMock( Indexables::class );

		$this->search_instance->queue      = $queue_mocked;
		$this->search_instance->indexables = $indexables_mock;
		$this->search_instance->alerts     = $alerts_mocked;

		$indexables_mock->method( 'get' )
			->willReturn( $this->createMock( Indexable::class ) );

		$queue_mocked
			->method( 'get_queue_stats' )
			->willReturn( (object) [
				'average_wait_time' => $average_queue_value,
				'queue_count'       => $queue_count_value,
				'longest_wait_time' => $longest_queue_value,
			] );

		$alerts_mocked->expects( $this->once() )
			->method( 'send_to_chat' )
			->with( '#vip-go-es-alerts', $expected_message, $expected_level );

		$this->search_instance->maybe_alert_for_average_queue_time();
	}

	public function maybe_alert_for_field_count_data() {
		return [
			[ 5000, false ],
			[ 5001, true ],
		];
	}

	/**
	 * @dataProvider maybe_alert_for_field_count_data
	 */
	public function test__maybe_alert_for_field_count( $field_count, $should_alert ) {
		$application_id   = 123;
		$application_url  = 'http://example.org';
		$expected_message = "The field count for post index for application $application_id - $application_url is too damn high - $field_count";
		$expected_level   = 2;

		/** @var MockObject&Search */
		$partially_mocked_search = $this->getMockBuilder( Search::class )
			->onlyMethods( [ 'get_current_field_count' ] )
			->getMock();
		$partially_mocked_search->init();

		$alerts_mocked   = $this->createMock( Alerts::class );
		$indexables_mock = $this->createMock( Indexables::class );

		$partially_mocked_search->indexables = $indexables_mock;
		$partially_mocked_search->alerts     = $alerts_mocked;

		$indexables_mock->method( 'get' )
			->willReturn( $this->createMock( \ElasticPress\Indexable::class ) );

		$partially_mocked_search->method( 'get_current_field_count' )->willReturn( $field_count );

		$alerts_mocked->expects( $should_alert ? $this->once() : $this->never() )
			->method( 'send_to_chat' )
			->with( '#vip-go-es-alerts', $expected_message, $expected_level );

		$partially_mocked_search->maybe_alert_for_field_count();
	}

	public function maybe_alert_for_prolonged_query_limiting_data() {
		return [
			[ false, false ],
			[ 0, false ],
			[ 12, false ],
			[ 7201, true ],
		];
	}

	/**
	 * @dataProvider maybe_alert_for_prolonged_query_limiting_data
	 */
	public function test__maybe_alert_for_prolonged_query_limiting( $difference, $should_alert ) {
		$expected_level = 2;

		$time = time();

		if ( false !== $difference ) {
			$query_limited_start = $time - $difference;
			wp_cache_set( Search::QUERY_RATE_LIMITED_START_CACHE_KEY, $query_limited_start, Search::SEARCH_CACHE_GROUP );
		}

		$this->search_instance->init();
		$this->search_instance->set_time( $time );

		$alerts_mocked = $this->createMock( Alerts::class );

		$this->search_instance->alerts = $alerts_mocked;

		$alerts_mocked->expects( $should_alert ? $this->once() : $this->never() )
			->method( 'send_to_chat' )
			->with( '#vip-go-es-alerts', $this->anything(), $expected_level );

		// trigger_error is only called if an alert should happen
		if ( $should_alert ) {
			$this->expectException( \Exception::class );
			$this->expectExceptionMessage(
				sprintf(
					'Application 123 - http://example.org has had its Elasticsearch queries rate-limited for %d seconds. Half of traffic is diverted to the database when queries are rate-limited.',
					$difference
				)
			);
		}

		$this->search_instance->maybe_alert_for_prolonged_query_limiting();
		$this->search_instance->reset_time();
	}

	/* Format:
	 * [
	 *      [
	 *          $filter,
	 *          $too_low_message,
	 *          $too_high_message,
	 *      ]
	 * ]
	 */
	public function vip_search_ratelimiting_filter_data() {
		return array(
			[
				'vip_search_ratelimit_period',
				'vip_search_ratelimit_period should not be set below 60 seconds.',
				'vip_search_ratelimit_period should not be set above 7200 seconds.',
			],
			[
				'vip_search_max_query_count',
				'vip_search_max_query_count should not be below 10 queries per second.',
				'vip_search_max_query_count should not exceed 500 queries per second.',
			],
			[
				'vip_search_query_db_fallback_value',
				'vip_search_query_db_fallback_value should be between 1 and 10.',
				'vip_search_query_db_fallback_value should be between 1 and 10.',
			],
		);
	}

	/**
	 * @dataProvider vip_search_ratelimiting_filter_data
	 */
	public function test__filter__vip_search_ratelimiting_numeric_validation( $filter, $too_low_message, $too_high_message ) {
		add_filter(
			$filter,
			function () {
				return '30.ffr';
			}
		);

		$this->setExpectedIncorrectUsage( 'add_filter' );
		$this->search_instance->apply_settings();
	}

	/**
	 * @dataProvider vip_search_ratelimiting_filter_data
	 */
	public function test__filter__vip_search_ratelimiting_too_low_validation( $filter, $too_low_message, $too_high_message ) {
		add_filter(
			$filter,
			function () {
				return 0;
			}
		);

		$this->setExpectedIncorrectUsage( 'add_filter' );
		$this->search_instance->apply_settings();
	}

	/**
	 * @dataProvider vip_search_ratelimiting_filter_data
	 */
	public function test__filter__vip_search_ratelimiting_too_high_validation( $filter, $too_low_message, $too_high_message ) {
		add_filter(
			$filter,
			function () {
				return PHP_INT_MAX;
			}
		);

		$this->setExpectedIncorrectUsage( 'add_filter' );
		$this->search_instance->apply_settings();
	}

	public function stat_sampling_invalid_stat_param_data() {
		return [
			[ array() ],
			[ null ],
			[ new stdClass() ],
			[ 5 ],
			[ 8.6 ],
		];
	}

	public function stat_sampling_invalid_value_param_data() {
		return [
			[ array() ],
			[ null ],
			[ new stdClass() ],
			[ 'random' ],
		];
	}

	public function ep_handle_failed_request_data() {
		return [
			[
				[
					'body' => '{ "error": { "reason": "error text"} }',
				],
				'error text',
			],
			[
				[
					'body'     => '{ "error": {} }',
					'response' => [
						'code'    => 401,
						'message' => 'Unauthorized',
					],
				],
				'401 Unauthorized',
			],
			[
				[
					'body' => '{}',
				],
				'Unknown Elasticsearch query error',
			],
			[
				[],
				'Unknown Elasticsearch query error',
			],
		];
	}

	/**
	 * @dataProvider ep_handle_failed_request_data
	 */
	public function test__ep_handle_failed_request__log_message( $response, $expected_message ) {
		$this->init_es();

		$this->search_instance->logger = $this->getMockBuilder( \Automattic\VIP\Logstash\Logger::class )
			->onlyMethods( [ 'log' ] )
			->getMock();

		$this->search_instance->logger->expects( $this->once() )
			->method( 'log' )
			->with(
				$this->equalTo( 'error' ),
				$this->equalTo( 'search_query_error' ),
				$this->equalTo( $expected_message ),
				$this->anything()
			);

		$this->search_instance->ep_handle_failed_request( null, $response, [], null, null, '' );
	}

	/**
	 * Ensure when actions from the skiplist are called, they do not get logged as a failed request.
	 */
	public function test__ep_handle_failed_request__skiplist() {
		$this->init_es();

		$this->search_instance->logger = $this->getMockBuilder( \Automattic\VIP\Logstash\Logger::class )
			->onlyMethods( [ 'log' ] )
			->getMock();

		$this->search_instance->logger->expects( $this->never() )->method( 'log' );

		$skiplist = [
			'index_exists',
			'get',
		];

		foreach ( $skiplist as $item ) {
			$this->search_instance->ep_handle_failed_request( null, 404, [], $item, null, '' );
		}
	}

	public function get_sanitize_ep_query_for_logging_data() {
		return array(
			// No Auth header present
			array(
				// The "query" from ElasticPress
				array(
					'args' => array(
						'headers' => array(
							'some' => 'header',
						),
					),
				),
				// Expected sanitized value
				array(
					'args' => array(
						'headers' => array(
							'some' => 'header',
						),
					),
				),
			),
			// Auth header present, should be sanitized
			array(
				array(
					'args' => array(
						'headers' => array(
							'Authorization' => 'foo',
							'some'          => 'header',
						),
					),
				),
				array(
					'args' => array(
						'headers' => array(
							'Authorization' => '<redacted>',
							'some'          => 'header',
						),
					),
				),
			),
		);
	}

	/**
	 * @dataProvider get_sanitize_ep_query_for_logging_data
	 */
	public function test__sanitize_ep_query_for_logging( $input, $expected ) {
		$sanitized = $this->search_instance->sanitize_ep_query_for_logging( $input );

		$this->assertEquals( $expected, $sanitized );
	}

	public function test__maybe_log_query_ratelimiting_start_should_do_nothing_if_ratelimiting_already_started() {
		$this->init_es();

		wp_cache_set( $this->search_instance::QUERY_RATE_LIMITED_START_CACHE_KEY, time(), $this->search_instance::SEARCH_CACHE_GROUP );

		$this->search_instance->logger = $this->getMockBuilder( \Automattic\VIP\Logstash\Logger::class )
			->onlyMethods( [ 'log' ] )
			->getMock();

		$this->search_instance->logger->expects( $this->never() )->method( 'log' );

		$this->search_instance->maybe_log_query_ratelimiting_start();
	}

	public function test__maybe_log_query_ratelimiting_start_should_log_if_ratelimiting_not_already_started() {
		$this->init_es();

		$this->search_instance->logger = $this->getMockBuilder( \Automattic\VIP\Logstash\Logger::class )
			->onlyMethods( [ 'log' ] )
			->getMock();

		$this->search_instance->logger->expects( $this->once() )
			->method( 'log' )
			->with(
				$this->equalTo( 'warning' ),
				$this->equalTo( 'search_query_rate_limiting' ),
				$this->equalTo(
					'Application 123 - http://example.org has triggered Elasticsearch query rate-limiting, which will last up to 300 seconds. Subsequent or repeat occurrences are possible. Half of traffic is diverted to the database when queries are rate-limited.'
				),
				$this->anything()
			);

		$this->search_instance->maybe_log_query_ratelimiting_start();
	}

	public function test__add_attachment_to_ep_indexable_post_types_should_return_the_passed_value_if_not_array() {
		Constant_Mocker::define( 'EP_DASHBOARD_SYNC', 'test' );
		$this->search_instance->init();

		$this->assertEquals( 'testing', $this->search_instance->add_attachment_to_ep_indexable_post_types( 'testing' ) );
		$this->assertEquals( 65, $this->search_instance->add_attachment_to_ep_indexable_post_types( 65 ) );
		$this->assertEquals( null, $this->search_instance->add_attachment_to_ep_indexable_post_types( null ) );
		$this->assertEquals( new stdClass(), $this->search_instance->add_attachment_to_ep_indexable_post_types( new stdClass() ) );
	}

	public function test__add_attachment_to_ep_indexable_post_types_should_append_attachment_to_array() {
		$this->init_es();

		$this->assertEquals( array( 'attachment' => 'attachment' ), $this->search_instance->add_attachment_to_ep_indexable_post_types( array() ) );
		$this->assertEquals(
			array(
				'test'       => 'test',
				'one'        => 'one',
				'attachment' => 'attachment',
			),
			$this->search_instance->add_attachment_to_ep_indexable_post_types(
				array(
					'test' => 'test',
					'one'  => 'one',
				)
			)
		);
	}

	public function test__ep_indexable_post_types_should_return_the_passed_value_if_not_array() {
		$this->init_es();

		Features::factory()->activate_feature( 'protected_content' );

		$this->assertEquals( 'testing', apply_filters( 'ep_indexable_post_types', 'testing' ) );
		$this->assertEquals( 65, apply_filters( 'ep_indexable_post_types', 65 ) );
		$this->assertEquals( null, apply_filters( 'ep_indexable_post_types', null ) );
		$this->assertEquals( new stdClass(), apply_filters( 'ep_indexable_post_types', new stdClass() ) );
	}

	public function test__ep_indexable_post_types_should_append_attachment_to_array() {
		// Ensure ElasticPress is ready
		do_action( 'plugins_loaded' );

		Features::factory()->activate_feature( 'protected_content' );

		$es = new Search();
		$es->init();

		$this->assertEquals( array( 'attachment' => 'attachment' ), apply_filters( 'ep_indexable_post_types', array() ) );
		$this->assertEquals(
			array(
				'test'       => 'test',
				'one'        => 'one',
				'attachment' => 'attachment',
			),
			apply_filters(
				'ep_indexable_post_types',
				array(
					'test' => 'test',
					'one'  => 'one',
				)
			)
		);
	}

	public function test__is_protected_content_enabled_should_return_false_if_protected_content_not_enabled() {
		$this->init_es();

		$this->assertFalse( $this->search_instance->is_protected_content_enabled() );
	}

	public function test__is_protected_content_enabled_should_return_true_if_protected_content_enabled() {
		$this->init_es();

		Features::factory()->activate_feature( 'protected_content' );

		$this->assertTrue( $this->search_instance->is_protected_content_enabled() );
	}

	public function test__get_random_host_return_null_if_no_host() {
		$this->init_es();

		$this->assertSame( null, $this->search_instance->get_random_host( array() ) );
	}

	public function test__get_random_host_return_null_if_hosts_is_not_array() {
		$this->init_es();

		$this->assertSame( null, $this->search_instance->get_random_host( false ) );
	}

	public function test__maybe_enable_ep_query_logging_no_cap() {
		wp_set_current_user( 0 );

		$this->init_es();

		$this->assertFalse( Constant_Mocker::defined( 'WP_EP_DEBUG' ) );
	}

	public function test__maybe_enable_ep_query_logging_has_cap() {
		$super_admin = $this->factory()->user->create_and_get( array( 'role' => 'administrator' ) );
		wp_set_current_user( $super_admin->ID );

		$this->init_es();

		$this->assertTrue( Constant_Mocker::defined( 'WP_EP_DEBUG' ) );
		$this->assertTrue( Constant_Mocker::constant( 'WP_EP_DEBUG' ) );
	}

	public function test__maybe_enable_ep_query_logging_filtered_cap() {
		add_filter( 'vip_search_dev_tools_cap', function () {
			return 'edit_posts';
		} );
		$editor = $this->factory()->user->create_and_get( array( 'role' => 'editor' ) );
		wp_set_current_user( $editor->ID );

		$this->init_es();

		$this->assertTrue( Constant_Mocker::defined( 'WP_EP_DEBUG' ) );
		$this->assertTrue( Constant_Mocker::constant( 'WP_EP_DEBUG' ) );
	}

	public function limit_max_result_window_data() {
		return [
			[
				'input'    => 500,
				'expected' => 500,
			],
			[
				'input'    => 10000,
				'expected' => 10000,
			],
		];
	}

	/**
	 * @dataProvider limit_max_result_window_data
	 */
	public function test__limit_max_result_window( $input, $expected ) {
		$result = $this->search_instance->limit_max_result_window( $input );

		$this->assertEquals( $expected, $result );
	}

	public function test__are_es_constants_defined__no_constatns() {
		$result = Search::are_es_constants_defined();

		$this->assertFalse( $result );
	}

	public function test__are_es_constants_defined__all_constatns() {
		Constant_Mocker::define( 'VIP_ELASTICSEARCH_ENDPOINTS', [ 'endpoint' ] );
		Constant_Mocker::define( 'VIP_ELASTICSEARCH_USERNAME', 'foo' );
		Constant_Mocker::define( 'VIP_ELASTICSEARCH_PASSWORD', 'bar' );

		$result = Search::are_es_constants_defined();

		$this->assertTrue( $result );
	}

	public function test__are_es_constants_defined__empty_password() {
		Constant_Mocker::define( 'VIP_ELASTICSEARCH_ENDPOINTS', [ 'endpoint' ] );
		Constant_Mocker::define( 'VIP_ELASTICSEARCH_USERNAME', 'foo' );
		Constant_Mocker::define( 'VIP_ELASTICSEARCH_PASSWORD', '' );

		$result = Search::are_es_constants_defined();

		$this->assertFalse( $result );
	}

	public function test__are_es_constants_defined__no_username() {
		Constant_Mocker::define( 'VIP_ELASTICSEARCH_ENDPOINTS', [ 'endpoint' ] );
		Constant_Mocker::define( 'VIP_ELASTICSEARCH_PASSWORD', 'bar' );

		$result = Search::are_es_constants_defined();

		$this->assertFalse( $result );
	}

	public function test__are_es_constants_defined__no_endpoints() {
		Constant_Mocker::define( 'VIP_ELASTICSEARCH_ENDPOINTS', [] );
		Constant_Mocker::define( 'VIP_ELASTICSEARCH_USERNAME', 'foo' );
		Constant_Mocker::define( 'VIP_ELASTICSEARCH_PASSWORD', 'bar' );

		$result = Search::are_es_constants_defined();

		$this->assertFalse( $result );
	}

	public function test__filter_ep_enable_do_weighting__default_no_weighting() {
		$this->search_instance->init();

		$this->assertFalse( apply_filters( 'ep_enable_do_weighting', true, [], [], [] ) );
	}

	public function test__filter_ep_enable_do_weighting__anonymous_function() {
		$this->search_instance->init();

		add_filter(
			'ep_weighting_configuration_for_search',
			function ( $weight_config ) {
				return $weight_config;
			}
		);

		$this->assertTrue( apply_filters( 'ep_enable_do_weighting', true, [], [], [] ) );
	}

	public function test__filter_ep_enable_do_weighting__class_function() {
		$this->search_instance->init();

		add_filter( 'ep_weighting_configuration_for_search', [ $this, 'some_function' ] );

		$this->assertTrue( apply_filters( 'ep_enable_do_weighting', true, [], [], [] ) );
	}

	public function test__filter_ep_enable_do_weighting__weight_config() {
		$this->search_instance->init();

		$this->assertTrue( apply_filters( 'ep_enable_do_weighting', true, [ 'foo' => 'bar' ], [], [] ) );
	}

	public function test__filter_ep_enable_do_weighting__no_custom_search_results() {
		// Ensure ElasticPress is ready
		do_action( 'plugins_loaded' );

		$this->search_instance->init();

		Features::factory()->activate_feature( 'searchordering' );
		update_option( 'vip_custom_results_existence', '0' );

		$this->assertFalse( apply_filters( 'ep_enable_do_weighting', true, [], [], [] ) );
	}

	public function test__filter_ep_enable_do_weighting__custom_search_results() {
		// Ensure ElasticPress is ready
		do_action( 'plugins_loaded' );

		$this->search_instance->init();

		Features::factory()->activate_feature( 'searchordering' );
		update_option( 'vip_custom_results_existence', '1' );

		$this->assertTrue( apply_filters( 'ep_enable_do_weighting', true, [], [], [] ) );
	}

	public function test__set_custom_results_existence_cache() {
		// Ensure ElasticPress is ready
		do_action( 'plugins_loaded' );

		$this->search_instance->init();

		Features::factory()->activate_feature( 'searchordering' );

		$post = wp_insert_post( [
			'post_type'   => 'ep-pointer',
			'post_status' => 'publish',
			'post_title'  => 'Test CSR',
		] );

		$this->assertEquals( '1', get_option( 'vip_custom_results_existence' ) );

		wp_trash_post( $post );

		$this->assertEquals( '0', get_option( 'vip_custom_results_existence' ) );

		wp_insert_post( [
			'post_type'   => 'ep-pointer',
			'post_status' => 'publish',
			'post_title'  => 'Test CSR 2',
		] );
		$post3 = wp_insert_post( [
			'post_type'   => 'ep-pointer',
			'post_status' => 'publish',
			'post_title'  => 'Test CSR 3',
		] );
		wp_trash_post( $post3 );

		$this->assertEquals( '1', get_option( 'vip_custom_results_existence' ) );
	}

	/**
	 * Helper function for accessing protected methods.
	 */
	protected static function get_method( $name ) {
		$class  = new \ReflectionClass( __NAMESPACE__ . '\Search' );
		$method = $class->getMethod( $name );
		$method->setAccessible( true ); // NOSONAR
		return $method;
	}

	/**
	 * Helper function for accessing protected properties.
	 */
	protected static function get_property( $name ) {
		$class = new \ReflectionClass( __NAMESPACE__ . '\Search' );

		$property = $class->getProperty( $name );
		$property->setAccessible( true ); // NOSONAR

		return $property;
	}

	/**
	 * Helper function to set required constant, initialize the search instance, and do required action for setting up EP indexables.
	 *
	 * @return void
	 */
	private function init_es( $run_init = true ) {
		Constant_Mocker::undefine( 'EP_DASHBOARD_SYNC' );
		Constant_Mocker::define( 'EP_DASHBOARD_SYNC', false );

		if ( $run_init ) {
			remove_all_actions( 'init' );
		}

		$this->search_instance->init();

		do_action( 'plugins_loaded' );

		if ( $run_init ) {
			do_action( 'init' );
		}
	}

	/**
	 * We need to fake the OK response from the ES server to avoid the actual failing request.
	 */
	public function filter_ok_es_requests( $request, $query, $args, $failures, $type ) {
		if ( 'put_mapping' === $type ) {
			return [
				'response' => [ 'code' => 200 ],
				'body'     => '',
			];
		}

		if ( 'index_exists' === $type ) {
			return [
				'response' => [ 'code' => 200 ],
				'body'     => [],
			];
		}

		if ( 'get_mapping' === $type ) {
			return [
				'response' => [ 'code' => 200 ],
				'body'     => '{"vip-123-post-1-v2":{"aliases":{},"mappings":{"_meta":{"mapping_version":"7-0.php"}}}}', // phpcs:ignore WordPressVIPMinimum.Security.Mustache.OutputNotation
			];
		}

		return $request;
	}

	/**
	 * We need to fake the OK response from the ES server to avoid the actual get_mapping request.
	 */
	public function filter_index_exists_request_ok( $request, $query, $args, $failures, $type ) {
		if ( 'index_exists' === $type ) {
			return [
				'response' => [ 'code' => 200 ],
				'body'     => [],
			];
		}
		return $request;
	}
}

/**
 * Overwriting global function so that no real remote request is called
 */
function vip_safe_wp_remote_request( ...$args ) {
	return is_null( Search_Test::$mock_global_functions ) ? null : Search_Test::$mock_global_functions->mock_vip_safe_wp_remote_request( ...$args );
}

/**
 * Overwriting global function so that no real remote request is called
 */
function wp_remote_request( ...$args ) {
	return is_null( Search_Test::$mock_global_functions ) ? null : Search_Test::$mock_global_functions->mock_wp_remote_request( ...$args );
}
