<?php

declare( strict_types = 1 );

namespace MediaWiki\Composer\PhpUnitSplitter;

/**
 * @license GPL-2.0-or-later
 */
class PhpUnitXmlManager {

	private string $rootDir;

	private string $testsListFile;

	/**
	 * The `SkippedTestCase` is generated dynamically by PHPUnit for tests
	 * that are marked as skipped. We don't need to find a matching filesystem
	 * file for these.
	 *
	 * The `ParserIntegrationTest` is a special case - it's a single test class
	 * that generates very many tests. To balance out the test suites, we exclude
	 * the class from the scan, and add it back in PhpUnitXml::addSpecialCaseTests
	 */
	private const EXPECTED_MISSING_CLASSES = [
		"PHPUnit\\Framework\\SkippedTestCase",
		"MediaWiki\\Extension\\Scribunto\\Tests\\Engines\\LuaCommon\\LuaEngineTestSkip",
		"\\ParserIntegrationTest",
	];

	public function __construct( string $rootDir, string $testsListFile ) {
		$this->rootDir = $rootDir;
		$this->testsListFile = $testsListFile;
	}

	private function getPhpUnitXmlTarget(): string {
		return $this->rootDir . DIRECTORY_SEPARATOR . PhpUnitXml::PHP_UNIT_XML_FILE;
	}

	private function getPhpUnitXmlDist(): string {
		return $this->rootDir . DIRECTORY_SEPARATOR . "phpunit.xml.dist";
	}

	private function getTestsList(): string {
		return $this->rootDir . DIRECTORY_SEPARATOR . $this->testsListFile;
	}

	private function loadPhpUnitXmlDist(): PhpUnitXml {
		return $this->loadPhpUnitXml( $this->getPhpUnitXmlDist() );
	}

	private function loadPhpUnitXml( string $targetFile ): PhpUnitXml {
		return new PhpUnitXml( $targetFile );
	}

	private function loadTestClasses(): array {
		if ( !file_exists( $this->getTestsList() ) ) {
			throw new TestListMissingException( $this->getTestsList() );
		}
		return ( new PhpUnitTestListProcessor( $this->getTestsList() ) )->getTestClasses();
	}

	private function scanForTestFiles(): array {
		return ( new PhpUnitTestFileScanner( $this->rootDir ) )->scanForFiles();
	}

	private static function extractNamespaceFromFile( $filename ): array {
		$contents = file_get_contents( $filename );
		$matches = [];
		if ( preg_match( '/^namespace\s+([^\s;]+)/m', $contents, $matches ) ) {
			return explode( '\\', $matches[1] );
		}
		return [];
	}

	/**
	 * @param TestDescriptor $testDescriptor
	 * @param array $phpFiles
	 * @return ?string
	 * @throws MissingNamespaceMatchForTestException
	 * @throws UnlocatedTestException
	 */
	private function resolveFileForTest( TestDescriptor $testDescriptor, array $phpFiles ): ?string {
		$filename = $testDescriptor->getClassName() . ".php";
		if ( !array_key_exists( $filename, $phpFiles ) ) {
			if ( !in_array( $testDescriptor->getFullClassname(), self::EXPECTED_MISSING_CLASSES ) ) {
				throw new UnlocatedTestException( $testDescriptor );
			} else {
				return null;
			}
		}
		if ( count( $phpFiles[$filename] ) === 1 ) {
			return $phpFiles[$filename][0];
		}
		$possibleNamespaces = [];
		foreach ( $phpFiles[$filename] as $file ) {
			$namespace = self::extractNamespaceFromFile( $file );
			if ( $namespace === $testDescriptor->getNamespace() ) {
				return $file;
			}
			$possibleNamespaces[] = $namespace;
		}
		throw new MissingNamespaceMatchForTestException( $testDescriptor );
	}

	private function buildSuites( array $testClasses, int $groups ): array {
		return ( new TestSuiteBuilder() )->buildSuites( $testClasses, $groups );
	}

	public function isPhpUnitXmlPrepared(): bool {
		return PhpUnitXml::isPhpUnitXmlPrepared( $this->rootDir . DIRECTORY_SEPARATOR . "phpunit.xml" );
	}

	/**
	 * @return void
	 * @throws MissingNamespaceMatchForTestException
	 * @throws TestListMissingException
	 * @throws UnlocatedTestException
	 * @throws SuiteGenerationException
	 */
	public function createPhpUnitXml( int $groups ) {
		$unitFile = $this->loadPhpUnitXmlDist();
		$testFiles = $this->scanForTestFiles();
		$testClasses = $this->loadTestClasses();
		$seenFiles = [];
		foreach ( $testClasses as $testDescriptor ) {
			$file = $this->resolveFileForTest( $testDescriptor, $testFiles );
			if ( is_string( $file ) && !array_key_exists( $file, $seenFiles ) ) {
				$testDescriptor->setFilename( $file );
				$seenFiles[$file] = 1;
			}
		}
		$suites = $this->buildSuites( $testClasses, $groups - 1 );
		$unitFile->addSplitGroups( $suites );
		$unitFile->addSpecialCaseTests( $groups );
		$unitFile->saveToDisk( $this->getPhpUnitXmlTarget() );
	}

	public static function listTestsNotice() {
		print( PHP_EOL );
		print( 'Running `phpunit --list-tests-xml` to get a list of expected tests ... ' . PHP_EOL );
		print( PHP_EOL );
	}

	/**
	 * @throws TestListMissingException
	 * @throws UnlocatedTestException
	 * @throws MissingNamespaceMatchForTestException
	 * @throws SuiteGenerationException
	 */
	public static function splitTestsList( string $testListFile ) {
		/**
		 * We split into 8 groups here, because experimentally that generates 100% CPU load
		 * on developer machines and results in groups that are similar in size to the
		 * Parser tests (which we have to run in a group on their own - see T345481)
		 */
		( new PhpUnitXmlManager( getcwd(), $testListFile ) )->createPhpUnitXml( 8 );
		print( PHP_EOL . 'Created modified `phpunit.xml` with test suite groups' . PHP_EOL );
	}

	/**
	 * @throws TestListMissingException
	 * @throws UnlocatedTestException
	 * @throws MissingNamespaceMatchForTestException
	 * @throws SuiteGenerationException
	 */
	public static function splitTestsListExtensions() {
		self::splitTestsList( 'tests-list-extensions.xml' );
	}

	/**
	 * @throws TestListMissingException
	 * @throws UnlocatedTestException
	 * @throws MissingNamespaceMatchForTestException
	 * @throws SuiteGenerationException
	 */
	public static function splitTestsListDefault() {
		self::splitTestsList( 'tests-list-default.xml' );
	}

	/**
	 * @throws TestListMissingException
	 * @throws UnlocatedTestException
	 * @throws MissingNamespaceMatchForTestException
	 * @throws SuiteGenerationException
	 */
	public static function splitTestsCustom() {
		if ( $_SERVER["argc"] < 3 ) {
			print( 'Specify a filename to split' . PHP_EOL );
			exit( 1 );
		}
		$filename = $_SERVER["argv"][2];
		self::splitTestsList( $filename );
	}
}
