<?php

namespace CirrusSearch\Profile;

use CirrusSearch\CirrusSearchHookRunner;
use CirrusSearch\CirrusTestCase;
use CirrusSearch\HashSearchConfig;
use CirrusSearch\InterwikiResolverFactory;
use MediaWiki\Interwiki\NullInterwikiLookup;
use MediaWiki\Registration\ExtensionRegistry;
use MediaWiki\Request\FauxRequest;
use MediaWiki\User\Options\StaticUserOptionsLookup;
use MediaWiki\User\User;
use MediaWiki\User\UserIdentityValue;
use Wikimedia\ObjectCache\EmptyBagOStuff;
use Wikimedia\ObjectCache\WANObjectCache;

/**
 * @group CirrusSearch
 * @covers \CirrusSearch\Profile\SearchProfileServiceFactory
 */
class SearchProfileServiceFactoryTest extends CirrusTestCase {
	/**
	 * @dataProvider provideTypeAndContext
	 * @param string $type
	 * @param string $context
	 */
	public function testSaneDefaults( $type, $context ) {
		// Even with an empty search config we should have default profiles
		// available
		$factory = $this->getFactory( [], null, [] );
		$service = $factory->loadService( new HashSearchConfig( [] ), null,
			new UserIdentityValue( 1, 'test' ) );
		$this->assertNotNull( $service->getProfileName( $type, $context ) );
		$this->assertNotNull( $service->loadProfile( $type, $context ) );
	}

	public static function provideTypeAndContext() {
		return [
			'rescore fulltext' => [ SearchProfileService::RESCORE, SearchProfileService::CONTEXT_DEFAULT ],
			'rescore prefix' => [ SearchProfileService::RESCORE, SearchProfileService::CONTEXT_PREFIXSEARCH ],
			'similarity prefix' => [ SearchProfileService::SIMILARITY, SearchProfileService::CONTEXT_DEFAULT ],
			'crossproject block order' => [ SearchProfileService::CROSS_PROJECT_BLOCK_SCORER, SearchProfileService::CONTEXT_DEFAULT ],
			'completion' => [ SearchProfileService::COMPLETION, SearchProfileService::CONTEXT_COMPLETION ],
			'fallback' => [ SearchProfileService::FALLBACKS, SearchProfileService::CONTEXT_DEFAULT ],
			'fulltext query builder' => [ SearchProfileService::FT_QUERY_BUILDER, SearchProfileService::CONTEXT_DEFAULT ],
			'document size limiter' => [ SearchProfileService::DOCUMENT_SIZE_LIMITER, SearchProfileService::CONTEXT_DEFAULT ],
			'namespace matcher' => [ SearchProfileService::NAMESPACE_MATCHER, SearchProfileService::CONTEXT_DEFAULT ],
		];
	}

	/**
	 * @dataProvider provideOverrides
	 * @param string $type
	 * @param string $context
	 * @param string $overrideType
	 * @param string $overrideKey
	 * @param array[] $profiles
	 */
	public function testOverrides( $type, $context, $overrideType, $overrideKey, $profiles ) {
		$cirrusSearchHookRunner = $this->createCirrusSearchHookRunner( [
			'CirrusSearchProfileService' => static function ( SearchProfileService $service ) use ( $type, $profiles ) {
				$service->registerArrayRepository( $type, 'unit_test', $profiles );
			}
		] );

		$profileName = key( $profiles );
		$userOption = [];

		// Don't use TestUser it may have been polluted with default config from other tests.
		$user = $this->createMock( User::class );

		if ( $overrideType === 'uri' ) {
			$request = new FauxRequest( [ $overrideKey => $profileName ] );
			$config = new HashSearchConfig( [] );
		} elseif ( $overrideType === 'pref' ) {
			$request = new FauxRequest();
			$username = 'test';
			$user = new UserIdentityValue( 1, $username );
			$userOption = [ $username => [ $overrideKey => $profileName ] ];
			$config = new HashSearchConfig( [] );
		} elseif ( $overrideType === 'config' ) {
			$request = new FauxRequest();
			$config = new HashSearchConfig( [ $overrideKey => $profileName ] );
		} else {
			throw new \RuntimeException( "Unknown override type $overrideType" );
		}
		$factory = $this->getFactory( [], $cirrusSearchHookRunner, $userOption );
		$service = $factory->loadService( $config, $request, $user, true );
		$this->assertEquals( key( $profiles ), $service->getProfileName( $type, $context ) );
		$this->assertEquals( reset( $profiles ), $service->loadProfile( $type, $context ) );
	}

	public static function provideOverrides() {
		return [
			'rescore fulltext by uri' => [
				SearchProfileService::RESCORE, SearchProfileService::CONTEXT_DEFAULT,
				'uri', 'cirrusRescoreProfile', [ 'unittest' => [] ]
			],
			'rescore fulltext by config' => [
				SearchProfileService::RESCORE, SearchProfileService::CONTEXT_DEFAULT,
				'config', 'CirrusSearchRescoreProfile', [ 'unittest' => [] ]
			],
			'rescore prefix by uri' => [
				SearchProfileService::RESCORE, SearchProfileService::CONTEXT_PREFIXSEARCH,
				'uri', 'cirrusRescoreProfile', [ 'unittest' => [] ]
			],
			'rescore prefix by config' => [
				SearchProfileService::RESCORE, SearchProfileService::CONTEXT_PREFIXSEARCH,
				'config', 'CirrusSearchPrefixSearchRescoreProfile', [ 'unittest' => [] ]
			],
			'similarity by config' => [
				SearchProfileService::SIMILARITY, SearchProfileService::CONTEXT_DEFAULT,
				'config', 'CirrusSearchSimilarityProfile', [ 'unittest' => [] ]
			],
			'crossproject block scorer by config' => [
				SearchProfileService::CROSS_PROJECT_BLOCK_SCORER, SearchProfileService::CONTEXT_DEFAULT,
				'config', 'CirrusSearchCrossProjectOrder', [ 'unittest' => [] ]
			],
			'completion by user pref' => [
				SearchProfileService::COMPLETION, SearchProfileService::CONTEXT_COMPLETION,
				'pref', 'cirrussearch-pref-completion-profile', [ 'unittest' => [] ]
			],
			'completion by config' => [
				SearchProfileService::COMPLETION, SearchProfileService::CONTEXT_COMPLETION,
				'config', 'CirrusSearchCompletionSettings', [ 'unittest' => [] ],
			],
			'fallbacks by config' => [
				SearchProfileService::FALLBACKS, SearchProfileService::CONTEXT_DEFAULT,
				'config', 'CirrusSearchFallbackProfile', [ 'unittest' => [] ],
			],
			'fallbacks by uri' => [
				SearchProfileService::FALLBACKS, SearchProfileService::CONTEXT_DEFAULT,
				'uri', 'cirrusFallbackProfile', [ 'unittest' => [] ],
			],
			'fulltext query builder by uri' => [
				SearchProfileService::FT_QUERY_BUILDER, SearchProfileService::CONTEXT_DEFAULT,
				'uri', 'cirrusFTQBProfile', [ 'unittest' => [] ],
			],
			'fulltext query builder by config' => [
				SearchProfileService::FT_QUERY_BUILDER, SearchProfileService::CONTEXT_DEFAULT,
				'config', 'CirrusSearchFullTextQueryBuilderProfile', [ 'unittest' => [] ],
			],
			'second try prefix search by config' => [
				SearchProfileService::SECOND_TRY, SearchProfileService::CONTEXT_COMPLETION,
				'config', 'CirrusSearchCompletionUseSecondTryProfile', [ 'unittest' => [] ],
			],
			'second try prefix search by uri' => [
				SearchProfileService::SECOND_TRY, SearchProfileService::CONTEXT_COMPLETION,
				'uri', 'cirrusUseSecondTryProfile', [ 'unittest' => [] ],
			],
			'namespace matcher by config' => [
				SearchProfileService::NAMESPACE_MATCHER, SearchProfileService::CONTEXT_DEFAULT,
				'config', 'CirrusSearchNamespaceResolutionMethod', [ 'unittest' => [] ],
			],
			'namespace matcher by uri' => [
				SearchProfileService::NAMESPACE_MATCHER, SearchProfileService::CONTEXT_DEFAULT,
				'uri', 'cirrusUseNamespaceMatcherProfile', [ 'unittest' => [] ],
			],
		];
	}

	public static function provideTestInterwikiOverrides() {
		$baseConfig = [
			'CirrusSearchInterwikiSources' => [
				'my' => 'mywiki',
			],
		];
		return [
			'rescore' => [
				$baseConfig + [
					'CirrusSearchCrossProjectProfiles' => [
						'my' => [
							'rescore' => 'overridden'
						]
					]
				],
				[
					'_wikiID' => 'mywiki',
					'CirrusSearchRescoreProfiles' => [
						'default' => [],
						'overridden' => [ 'INTERWIKI' ]
					],
					'CirrusSearchRescoreProfile' => 'default',
				],
				SearchProfileService::RESCORE,
				'overridden'
			],
			'ftbuilder' => [
				$baseConfig + [
					'CirrusSearchCrossProjectProfiles' => [
						'my' => [
							'ftbuilder' => 'overridden'
						]
					]
				],
				[
					'_wikiID' => 'mywiki',
					'CirrusSearchFullTextQueryBuilderProfiles' => [
						'default' => [],
						'overridden' => [ 'INTERWIKI' ]
					],
					'CirrusSearchFullTextQueryBuilderProfile' => 'test',
				],
				SearchProfileService::FT_QUERY_BUILDER,
				'overridden'
			]
		];
	}

	/**
	 * @dataProvider provideTestInterwikiOverrides
	 */
	public function testInterwikiOverrides( array $hostWikiConfig, array $targetWikiConfig, $profileType, $overridden ) {
		$factory = $this->getFactory( $hostWikiConfig, null, [] );
		$service = $factory->loadService( new HashSearchConfig( $targetWikiConfig ) );
		$this->assertEquals( $overridden,
			$service->getProfileName( $profileType, SearchProfileService::CONTEXT_DEFAULT ) );
		$this->assertEquals( [ 'INTERWIKI' ], $service->loadProfile( $profileType ) );
	}

	public function testSemanticSearchNotLoadedWhenConfigAbsent() {
		$factory = $this->getFactory( [], null, [] );
		$service = $factory->loadService( new HashSearchConfig( [] ) );
		$this->expectException( SearchProfileException::class );
		$service->getProfileName( SearchProfileService::FT_QUERY_BUILDER, SearchProfileService::CONTEXT_SEMANTIC );
	}

	public function testSemanticSearchDefaults() {
		$cirrusSearchHookRunner = $this->createCirrusSearchHookRunner( [
			'CirrusSearchProfileService' => static function ( SearchProfileService $service ) {
				$service->registerArrayRepository( SearchProfileService::FT_QUERY_BUILDER,
					'unit_test', [ 'default_semantic' => [] ] );
			}
		] );
		$factory = $this->getFactory( [], $cirrusSearchHookRunner, [] );
		$config = new HashSearchConfig( [ 'CirrusSearchDefaultSemanticProfile' => 'default_semantic' ] );
		$service = $factory->loadService( $config, null, null, true );
		$this->assertEquals( 'default_semantic',
			$service->getProfileName( SearchProfileService::FT_QUERY_BUILDER, SearchProfileService::CONTEXT_SEMANTIC ) );
		$this->assertEquals( 'empty',
			$service->getProfileName( SearchProfileService::RESCORE, SearchProfileService::CONTEXT_SEMANTIC ) );
	}

	public function testSemanticSearchRouteRegistered() {
		$cirrusSearchHookRunner = $this->createCirrusSearchHookRunner( [
			'CirrusSearchProfileService' => static function ( SearchProfileService $service ) {
				$service->registerArrayRepository( SearchProfileService::FT_QUERY_BUILDER,
					'unit_test', [ 'default_semantic' => [] ] );
			}
		] );
		$factory = $this->getFactory( [], $cirrusSearchHookRunner, [] );
		$config = new HashSearchConfig( [ 'CirrusSearchDefaultSemanticProfile' => 'default_semantic' ] );
		$request = new FauxRequest( [ 'cirrusSemanticSearch' => '1' ] );
		$service = $factory->loadService( $config, $request, null, true );

		$dispatch = $service->getDispatchService();
		$query = $this->getNewFTSearchQueryBuilder( $config, 'foo' )
			->setInitialNamespaces( [ NS_MAIN ] )
			->build();
		$this->assertEquals( SearchProfileService::CONTEXT_SEMANTIC, $dispatch->bestRoute( $query )->getProfileContext() );
	}

	private function getFactory( array $hostWikiConfig = [],
								 ?CirrusSearchHookRunner $cirrusSearchHookRunner = null,
								 $userOption = []
	) {
		$config = new HashSearchConfig( $hostWikiConfig );
		$httpClient = new \NullMultiHttpClient( [] );
		$interWikiLookup = new NullInterwikiLookup();
		$registry = new ExtensionRegistry();

		$resolver = InterwikiResolverFactory::build( $config, WANObjectCache::newEmpty(),
			$interWikiLookup, $registry, $httpClient );

		return new SearchProfileServiceFactory( $resolver, $config, new EmptyBagOStuff(),
			$cirrusSearchHookRunner ?: $this->createCirrusSearchHookRunner(),
			new StaticUserOptionsLookup( $userOption ), $registry
		);
	}
}
