<?php

namespace CirrusSearch;

use MediaWiki\Config\HashConfig;
use MediaWiki\Context\RequestContext;
use Wikimedia\Assert\Assert;

/**
 * Reports UserTesting bucketing decision
 *
 * See UserTestingEngine for initialization.
 *
 * @license GPL-2.0-or-later
 */
class UserTestingStatus {
	/** Bucketing decision for the main request context */
	private static ?self $instance = null;

	/** @var ?string The name of the active test, or null if none */
	private $testName;

	/** @var ?string The name of the active bucket, or null if no active test */
	private $bucket;

	/**
	 * @return bool True when a bucketing decision has been made for the main
	 *  request context
	 */
	public static function hasInstance(): bool {
		return self::$instance !== null;
	}

	/**
	 * Reports bucketing decision for the main request context
	 *
	 * If not created yet, uses configuration and query string from request context
	 * to make a bucketing decision and activate that decision. This must be
	 * called as early in the request as is sensible to ensure the test configuration
	 * is applied.
	 */
	public static function getInstance(): self {
		if ( self::$instance === null ) {
			$context = RequestContext::getMain();
			$trigger = $context->getRequest()->getVal( 'cirrusUserTesting' );
			// The current method of ensuring user testing is always initialized is
			// sloppy, if we used the context config everything that touches
			// ElasticsearchIntermediary would fail unit testing.
			if ( defined( 'MW_PHPUNIT_TEST' ) ) {
				$config = new HashConfig( [
					'CirrusSearchUserTesting' => [],
					'CirrusSearchActiveTest' => false,
				] );
			} else {
				$config = $context->getConfig();
			}
			$engine = UserTestingEngine::fromConfig( $config );
			self::$instance = $engine->decideActiveTest( $trigger );
			// The singleton here also doubles as a marker for if we've already
			// applied the test configuration. This should be the only place
			// to activate a test outside maintenance scripts.
			$engine->activateTest( self::$instance );
		}
		return self::$instance;
	}

	/**
	 * @param string $testName
	 * @param string $bucket
	 * @return self status representing a test bucket active for this request
	 */
	public static function active( string $testName, string $bucket ): self {
		return new self( $testName, $bucket );
	}

	/**
	 * @return self status representing user testing as inactive
	 */
	public static function inactive(): self {
		return new self( null, null );
	}

	private function __construct( ?string $testName, ?string $bucket ) {
		Assert::precondition( ( $testName === null ) === ( $bucket === null ),
			'Either testName and bucket are both null or both strings' );
		$this->testName = $testName;
		$this->bucket = $bucket;
	}

	/**
	 * @return bool True when a test is currently active
	 */
	public function isActive(): bool {
		return $this->testName !== null;
	}

	/**
	 * @return string
	 * @throws NoActiveTestException
	 */
	public function getTestName(): string {
		if ( $this->testName === null ) {
			throw new NoActiveTestException();
		}
		return $this->testName;
	}

	/**
	 * @return string
	 * @throws NoActiveTestException
	 */
	public function getBucket(): string {
		if ( $this->bucket === null ) {
			throw new NoActiveTestException();
		}
		return $this->bucket;
	}

	/**
	 * @return string When active returns a string that will enable the same
	 *  test configuration when provided to UserTestingEngine.
	 * @throws NoActiveTestException
	 */
	public function getTrigger(): string {
		if ( $this->testName === null || $this->bucket == null ) {
			throw new NoActiveTestException();
		}
		return "{$this->testName}:{$this->bucket}";
	}
}
