<?php declare(strict_types=1);
/*
 * This file is part of PHPUnit.
 *
 * (c) Sebastian Bergmann <sebastian@phpunit.de>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */
namespace PHPUnit\TextUI;

use const PHP_EOL;
use const PHP_VERSION;
use const SIGINT;
use function array_reverse;
use function assert;
use function class_exists;
use function count;
use function defined;
use function dirname;
use function explode;
use function function_exists;
use function getmypid;
use function is_array;
use function is_file;
use function is_string;
use function method_exists;
use function pcntl_async_signals;
use function pcntl_signal;
use function printf;
use function realpath;
use function sprintf;
use function str_contains;
use function str_starts_with;
use function trim;
use function unlink;
use PHPUnit\Event\EventFacadeIsSealedException;
use PHPUnit\Event\Facade as EventFacade;
use PHPUnit\Event\UnknownSubscriberTypeException;
use PHPUnit\Framework\TestCase;
use PHPUnit\Framework\TestSuite;
use PHPUnit\Logging\EventLogger;
use PHPUnit\Logging\JUnit\JunitXmlLogger;
use PHPUnit\Logging\OpenTestReporting\CannotOpenUriForWritingException;
use PHPUnit\Logging\OpenTestReporting\OtrXmlLogger;
use PHPUnit\Logging\TeamCity\TeamCityLogger;
use PHPUnit\Logging\TestDox\HtmlRenderer as TestDoxHtmlRenderer;
use PHPUnit\Logging\TestDox\PlainTextRenderer as TestDoxTextRenderer;
use PHPUnit\Logging\TestDox\TestResultCollector as TestDoxResultCollector;
use PHPUnit\Runner\Baseline\CannotLoadBaselineException;
use PHPUnit\Runner\Baseline\Generator as BaselineGenerator;
use PHPUnit\Runner\Baseline\Reader;
use PHPUnit\Runner\Baseline\Writer;
use PHPUnit\Runner\CodeCoverage;
use PHPUnit\Runner\CodeCoverageInitializationStatus;
use PHPUnit\Runner\DeprecationCollector\Facade as DeprecationCollector;
use PHPUnit\Runner\DeprecationFilter;
use PHPUnit\Runner\DirectoryDoesNotExistException;
use PHPUnit\Runner\ErrorHandler;
use PHPUnit\Runner\Extension\ExtensionBootstrapper;
use PHPUnit\Runner\Extension\ExtensionCapabilities;
use PHPUnit\Runner\Extension\ExtensionFacade;
use PHPUnit\Runner\Extension\PharLoader;
use PHPUnit\Runner\GarbageCollection\GarbageCollectionHandler;
use PHPUnit\Runner\IssueTriggerResolver\Resolver;
use PHPUnit\Runner\PhpConfiguration\PhpConfigurationChecker;
use PHPUnit\Runner\Phpt\TestCase as PhptTestCase;
use PHPUnit\Runner\TestRunHistory\DefaultTestRunHistory;
use PHPUnit\Runner\TestRunHistory\NullTestRunHistory;
use PHPUnit\Runner\TestRunHistory\TestRunHistory;
use PHPUnit\Runner\TestRunHistory\TestRunHistoryHandler;
use PHPUnit\Runner\TestSuiteSorter;
use PHPUnit\Runner\Version;
use PHPUnit\TestRunner\IssueFilter;
use PHPUnit\TestRunner\TestResult\Facade as TestResultFacade;
use PHPUnit\TextUI\CliArguments\Builder;
use PHPUnit\TextUI\CliArguments\Configuration as CliConfiguration;
use PHPUnit\TextUI\CliArguments\Exception as ArgumentsException;
use PHPUnit\TextUI\CliArguments\XmlConfigurationFileFinder;
use PHPUnit\TextUI\Command\AtLeastVersionCommand;
use PHPUnit\TextUI\Command\CheckPhpConfigurationCommand;
use PHPUnit\TextUI\Command\GenerateConfigurationCommand;
use PHPUnit\TextUI\Command\ListGroupsCommand;
use PHPUnit\TextUI\Command\ListTestFilesCommand;
use PHPUnit\TextUI\Command\ListTestIdsCommand;
use PHPUnit\TextUI\Command\ListTestsAsTextCommand;
use PHPUnit\TextUI\Command\ListTestsAsXmlCommand;
use PHPUnit\TextUI\Command\ListTestSuitesCommand;
use PHPUnit\TextUI\Command\MigrateConfigurationCommand;
use PHPUnit\TextUI\Command\Result;
use PHPUnit\TextUI\Command\ShowHelpCommand;
use PHPUnit\TextUI\Command\ShowVersionCommand;
use PHPUnit\TextUI\Command\ValidateConfigurationCommand;
use PHPUnit\TextUI\Command\VersionCheckCommand;
use PHPUnit\TextUI\Command\WarmCodeCoverageCacheCommand;
use PHPUnit\TextUI\Configuration\BootstrapLoader;
use PHPUnit\TextUI\Configuration\BootstrapScriptDoesNotExistException;
use PHPUnit\TextUI\Configuration\BootstrapScriptException;
use PHPUnit\TextUI\Configuration\CodeCoverageFilterRegistry;
use PHPUnit\TextUI\Configuration\Configuration;
use PHPUnit\TextUI\Configuration\PhpHandler;
use PHPUnit\TextUI\Configuration\Registry;
use PHPUnit\TextUI\Configuration\TestSuiteBuilder;
use PHPUnit\TextUI\Output\DefaultPrinter;
use PHPUnit\TextUI\Output\Facade as OutputFacade;
use PHPUnit\TextUI\Output\Printer;
use PHPUnit\TextUI\XmlConfiguration\Configuration as XmlConfiguration;
use PHPUnit\TextUI\XmlConfiguration\DefaultConfiguration;
use PHPUnit\TextUI\XmlConfiguration\Loader;
use PHPUnit\Util\DifferBuilder;
use PHPUnit\Util\Http\PhpDownloader;
use SebastianBergmann\Timer\Timer;
use Throwable;

/**
 * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit
 *
 * @internal This class is not covered by the backward compatibility promise for PHPUnit
 */
final readonly class Application
{
    /**
     * @param list<string> $argv
     */
    public function run(array $argv): int
    {
        try {
            EventFacade::emitter()->applicationStarted();

            $cliConfiguration           = $this->buildCliConfiguration($argv);
            $pathToXmlConfigurationFile = (new XmlConfigurationFileFinder)->find($cliConfiguration);

            $this->executeCommandsThatOnlyRequireCliConfiguration($cliConfiguration, $pathToXmlConfigurationFile);

            // the commands above end the process; preloading is therefore only
            // worthwhile once it is known that tests are going to be run
            $this->preload();

            $xmlConfiguration = $this->loadXmlConfiguration($pathToXmlConfigurationFile);

            $configuration = Registry::init(
                $cliConfiguration,
                $xmlConfiguration,
            );

            DifferBuilder::configureComparatorFactory();

            (new PhpHandler)->handle($configuration->php());

            try {
                (new BootstrapLoader)->handle($configuration);
            } catch (BootstrapScriptDoesNotExistException|BootstrapScriptException $e) {
                $this->exitWithErrorMessage($e->getMessage());
            }

            $this->executeCommandsThatDoNotRequireTheTestSuite($configuration, $cliConfiguration);

            $pharExtensions        = null;
            $extensionCapabilities = ExtensionCapabilities::none();

            if (!$configuration->noExtensions()) {
                if ($configuration->hasPharExtensionDirectory()) {
                    $pharExtensions = (new PharLoader)->loadPharExtensionsInDirectory(
                        $configuration->pharExtensionDirectory(),
                    );
                }

                $extensionCapabilities = $this->bootstrapExtensions($configuration);
            }

            $printer = OutputFacade::init(
                $configuration,
                $extensionCapabilities,
            );

            if ($configuration->debug()) {
                EventFacade::instance()->registerTracer(
                    new EventLogger(
                        'php://stdout',
                        $configuration->withTelemetry(),
                    ),
                );
            }

            TestResultFacade::init();
            DeprecationCollector::init();

            $this->registerLogfileWriters($configuration);

            $testDoxResultCollector = $this->testDoxResultCollector($configuration);

            $testRunHistory = $this->initializeTestRunHistory($configuration);

            if ($configuration->controlGarbageCollector()) {
                new GarbageCollectionHandler(
                    EventFacade::instance(),
                    $configuration->numberOfTestsBeforeGarbageCollection(),
                );
            }

            $baselineGenerator = $this->configureBaseline($configuration);

            $this->checkPhpConfiguration($configuration);

            EventFacade::instance()->seal();

            $this->configureDeprecationTriggers($configuration);
            $this->configureIssueTriggerResolvers($configuration);
            $this->configureDeprecationFilters($configuration);

            ErrorHandler::instance()->registerForNonTestCaseContext();

            $testSuite = $this->buildTestSuite($configuration);

            if ($configuration->hasTestIdFilterFile() && !is_file($configuration->testIdFilterFile())) {
                $this->exitWithErrorMessage(
                    sprintf(
                        'Test ID filter file "%s" not found',
                        $configuration->testIdFilterFile(),
                    ),
                );
            }

            ErrorHandler::instance()->restoreForNonTestCaseContext();

            $this->executeCommandsThatRequireTheTestSuite($configuration, $cliConfiguration, $testSuite);

            if ($testSuite->isEmpty() && !$configuration->hasCliArguments() && $configuration->testSuite()->isEmpty()) {
                $this->execute(new ShowHelpCommand(Result::FAILURE));
            }

            $coverageInitializationStatus = CodeCoverage::instance()->init(
                $configuration,
                CodeCoverageFilterRegistry::instance(),
                $extensionCapabilities->requiresCodeCoverageCollection(),
            );

            if (!$configuration->debug() && !$extensionCapabilities->replacesOutput()) {
                $this->writeRuntimeInformation($printer, $configuration);
                $this->writePharExtensionInformation($printer, $pharExtensions);
                $this->writeRandomSeedInformation($printer, $configuration);

                $printer->print(PHP_EOL);
            }

            $this->registerInterruptHandler();

            $timer = new Timer;
            $timer->start();

            if ($coverageInitializationStatus === CodeCoverageInitializationStatus::NOT_REQUESTED ||
                $coverageInitializationStatus === CodeCoverageInitializationStatus::SUCCEEDED) {
                $runner = new TestRunner;

                $runner->run(
                    $configuration,
                    $testRunHistory,
                    $testSuite,
                );
            }

            $duration = $timer->stop();

            $testDoxResult = null;

            if (isset($testDoxResultCollector)) {
                $testDoxResult = $testDoxResultCollector->testMethodsGroupedByClass();
            }

            if ($testDoxResult !== null &&
                $configuration->hasLogfileTestdoxHtml()) {
                try {
                    OutputFacade::printerFor($configuration->logfileTestdoxHtml())->print(
                        (new TestDoxHtmlRenderer)->render($testDoxResult),
                    );
                } catch (DirectoryDoesNotExistException|InvalidSocketException $e) {
                    EventFacade::emitter()->testRunnerTriggeredPhpunitWarning(
                        sprintf(
                            'Cannot log test results in TestDox HTML format to "%s": %s',
                            $configuration->logfileTestdoxHtml(),
                            $e->getMessage(),
                        ),
                    );
                }
            }

            if ($testDoxResult !== null &&
                $configuration->hasLogfileTestdoxText()) {
                try {
                    OutputFacade::printerFor($configuration->logfileTestdoxText())->print(
                        (new TestDoxTextRenderer)->render($testDoxResult),
                    );
                } catch (DirectoryDoesNotExistException|InvalidSocketException $e) {
                    EventFacade::emitter()->testRunnerTriggeredPhpunitWarning(
                        sprintf(
                            'Cannot log test results in TestDox plain text format to "%s": %s',
                            $configuration->logfileTestdoxText(),
                            $e->getMessage(),
                        ),
                    );
                }
            }

            CodeCoverage::instance()->warnAboutFilesThatCouldNotBeParsed();

            $result = TestResultFacade::result();

            if (TestResultFacade::wasInterrupted()) {
                if (!$extensionCapabilities->replacesResultOutput() && !$configuration->debug()) {
                    $printer->print(PHP_EOL . PHP_EOL);
                }

                $printer->print('Test execution was interrupted by a signal.');

                if ($extensionCapabilities->replacesResultOutput() || $configuration->debug()) {
                    $printer->print(PHP_EOL);
                }
            }

            if (!$extensionCapabilities->replacesResultOutput() && !$configuration->debug()) {
                OutputFacade::printResult(
                    $result,
                    $testDoxResult,
                    $duration,
                    $configuration->hasSpecificDeprecationToStopOn(),
                );
            }

            if (!TestResultFacade::wasInterrupted()) {
                CodeCoverage::instance()->generateReports($printer, $configuration);

                if (isset($baselineGenerator)) {
                    (new Writer)->write(
                        $configuration->generateBaseline(),
                        $baselineGenerator->baseline(),
                    );

                    $printer->print(
                        sprintf(
                            PHP_EOL . 'Baseline written to %s.' . PHP_EOL,
                            realpath($configuration->generateBaseline()),
                        ),
                    );
                }
            }

            $shellExitCode = (new ShellExitCodeCalculator)->calculate(
                $configuration,
                $result,
            );

            EventFacade::emitter()->applicationFinished($shellExitCode);

            return $shellExitCode;
            // @codeCoverageIgnoreStart
        } catch (Throwable $t) {
            $this->exitWithCrashMessage($t);
        }
        // @codeCoverageIgnoreEnd
    }

    private function execute(Command\Command $command, bool $requiresResultCollectedFromEvents = false): never
    {
        $errored = false;

        if ($requiresResultCollectedFromEvents) {
            try {
                TestResultFacade::init();
                EventFacade::instance()->seal();

                $resultCollectedFromEvents = TestResultFacade::result();

                $errored = $resultCollectedFromEvents->hasTestTriggeredPhpunitErrorEvents();
                // @codeCoverageIgnoreStart
            } catch (EventFacadeIsSealedException|UnknownSubscriberTypeException) {
            }
            // @codeCoverageIgnoreEnd
        }

        print Version::getVersionString() . PHP_EOL . PHP_EOL;

        if (!$errored) {
            $result = $command->execute();

            print $result->output();

            exit($result->shellExitCode());
        }

        assert(isset($resultCollectedFromEvents));

        print 'There were errors:' . PHP_EOL;

        foreach ($resultCollectedFromEvents->testTriggeredPhpunitErrorEvents() as $events) {
            foreach ($events as $event) {
                print PHP_EOL . trim($event->message()) . PHP_EOL;
            }
        }

        exit(Result::EXCEPTION);
    }

    /**
     * @param list<string> $argv
     */
    private function buildCliConfiguration(array $argv): CliConfiguration
    {
        try {
            $cliConfiguration = (new Builder)->fromParameters($argv);
        } catch (ArgumentsException $e) {
            $this->exitWithErrorMessage($e->getMessage());
        }

        return $cliConfiguration;
    }

    private function loadXmlConfiguration(false|string $configurationFile): XmlConfiguration
    {
        if ($configurationFile === false) {
            return DefaultConfiguration::create();
        }

        try {
            return (new Loader)->load($configurationFile);
        } catch (Throwable $e) {
            $this->exitWithErrorMessage($e->getMessage());
        }
    }

    private function buildTestSuite(Configuration $configuration): TestSuite
    {
        try {
            return (new TestSuiteBuilder)->build($configuration);
        } catch (Exception $e) {
            $this->exitWithErrorMessage($e->getMessage());
        }
    }

    private function bootstrapExtensions(Configuration $configuration): ExtensionCapabilities
    {
        $facade = new ExtensionFacade;

        $extensionBootstrapper = new ExtensionBootstrapper(
            $configuration,
            $facade,
        );

        foreach ($configuration->extensionBootstrappers() as $bootstrapper) {
            $extensionBootstrapper->bootstrap(
                $bootstrapper['className'],
                $bootstrapper['parameters'],
            );
        }

        return $facade->capabilities();
    }

    private function executeCommandsThatOnlyRequireCliConfiguration(CliConfiguration $cliConfiguration, false|string $configurationFile): void
    {
        if ($cliConfiguration->generateConfiguration()) {
            $this->execute(new GenerateConfigurationCommand);
        }

        if ($cliConfiguration->migrateConfiguration()) {
            if ($configurationFile === false) {
                $this->exitWithErrorMessage('No configuration file found to migrate');
            }

            $resolved = realpath($configurationFile);

            // @codeCoverageIgnoreStart
            if ($resolved === false) {
                $this->exitWithErrorMessage('Configuration file cannot be migrated');
            }
            // @codeCoverageIgnoreEnd

            $this->execute(new MigrateConfigurationCommand($resolved));
        }

        if ($cliConfiguration->validateConfiguration()) {
            if ($configurationFile === false) {
                $this->exitWithErrorMessage('No configuration file found to validate');
            }

            $resolved = realpath($configurationFile);

            // @codeCoverageIgnoreStart
            if ($resolved === false) {
                $this->exitWithErrorMessage('Configuration file cannot be validated');
            }
            // @codeCoverageIgnoreEnd

            $this->execute(new ValidateConfigurationCommand($resolved));
        }

        if ($cliConfiguration->hasAtLeastVersion()) {
            $this->execute(new AtLeastVersionCommand($cliConfiguration->atLeastVersion()));
        }

        if ($cliConfiguration->version()) {
            $this->execute(new ShowVersionCommand);
        }

        if ($cliConfiguration->checkPhpConfiguration()) {
            $this->execute(new CheckPhpConfigurationCommand);
        }

        if ($cliConfiguration->checkVersion()) {
            $this->execute(new VersionCheckCommand(new PhpDownloader, Version::majorVersionNumber(), Version::id()));
        }

        if ($cliConfiguration->help()) {
            $this->execute(new ShowHelpCommand(Result::SUCCESS));
        }
    }

    private function executeCommandsThatDoNotRequireTheTestSuite(Configuration $configuration, CliConfiguration $cliConfiguration): void
    {
        if ($cliConfiguration->warmCoverageCache()) {
            $this->execute(new WarmCodeCoverageCacheCommand($configuration, CodeCoverageFilterRegistry::instance()));
        }
    }

    private function executeCommandsThatRequireTheTestSuite(Configuration $configuration, CliConfiguration $cliConfiguration, TestSuite $testSuite): void
    {
        if ($cliConfiguration->listSuites()) {
            $this->execute(new ListTestSuitesCommand($testSuite));
        }

        if ($cliConfiguration->listGroups()) {
            $this->execute(
                new ListGroupsCommand(
                    $this->filteredTests(
                        $configuration,
                        $testSuite,
                    ),
                ),
                true,
            );
        }

        if ($cliConfiguration->listTestIds()) {
            $this->execute(
                new ListTestIdsCommand(
                    $this->filteredTests(
                        $configuration,
                        $testSuite,
                    ),
                ),
                true,
            );
        }

        if ($cliConfiguration->listTests()) {
            $this->execute(
                new ListTestsAsTextCommand(
                    $this->filteredTests(
                        $configuration,
                        $testSuite,
                    ),
                ),
                true,
            );
        }

        if ($cliConfiguration->hasListTestsXml()) {
            $this->execute(
                new ListTestsAsXmlCommand(
                    $this->filteredTests(
                        $configuration,
                        $testSuite,
                    ),
                    $cliConfiguration->listTestsXml(),
                ),
                true,
            );
        }

        if ($cliConfiguration->listTestFiles()) {
            $this->execute(
                new ListTestFilesCommand(
                    $this->filteredTests(
                        $configuration,
                        $testSuite,
                    ),
                ),
                true,
            );
        }
    }

    private function writeRuntimeInformation(Printer $printer, Configuration $configuration): void
    {
        $printer->print(Version::getVersionString() . PHP_EOL . PHP_EOL);

        $runtime = 'PHP ' . PHP_VERSION;

        if (CodeCoverage::instance()->isActive()) {
            $runtime .= ' with ' . CodeCoverage::instance()->driverNameAndVersion();
        }

        $this->writeMessage($printer, 'Runtime', $runtime);

        if ($configuration->hasConfigurationFile()) {
            $this->writeMessage(
                $printer,
                'Configuration',
                $configuration->configurationFile(),
            );
        }
    }

    /**
     * @param ?list<string> $pharExtensions
     */
    private function writePharExtensionInformation(Printer $printer, ?array $pharExtensions): void
    {
        if ($pharExtensions === null) {
            return;
        }

        foreach ($pharExtensions as $extension) {
            $this->writeMessage(
                $printer,
                'Extension',
                $extension,
            );
        }
    }

    private function writeMessage(Printer $printer, string $type, string $message): void
    {
        $printer->print(
            sprintf(
                "%-15s%s\n",
                $type . ':',
                $message,
            ),
        );
    }

    private function writeRandomSeedInformation(Printer $printer, Configuration $configuration): void
    {
        if ($configuration->executionOrder() === TestSuiteSorter::ORDER_RANDOMIZED) {
            $this->writeMessage(
                $printer,
                'Random Seed',
                (string) $configuration->randomOrderSeed(),
            );
        }
    }

    private function registerLogfileWriters(Configuration $configuration): void
    {
        if ($configuration->hasLogEventsText()) {
            if (is_file($configuration->logEventsText())) {
                unlink($configuration->logEventsText());
            }

            EventFacade::instance()->registerTracer(
                new EventLogger(
                    $configuration->logEventsText(),
                    $configuration->withTelemetry(),
                ),
            );
        }

        if ($configuration->hasLogEventsVerboseText()) {
            EventFacade::emitter()->testRunnerTriggeredPhpunitDeprecation(
                'The "--log-events-verbose-text <file>" CLI option is deprecated and will be removed in PHPUnit 14. Use "--log-events-text <file> --with-telemetry" instead.',
            );

            if (is_file($configuration->logEventsVerboseText())) {
                unlink($configuration->logEventsVerboseText());
            }

            EventFacade::instance()->registerTracer(
                new EventLogger(
                    $configuration->logEventsVerboseText(),
                    true,
                ),
            );
        }

        if ($configuration->hasLogfileJunit()) {
            try {
                new JunitXmlLogger(
                    OutputFacade::printerFor($configuration->logfileJunit()),
                    EventFacade::instance(),
                );
            } catch (DirectoryDoesNotExistException|InvalidSocketException $e) {
                EventFacade::emitter()->testRunnerTriggeredPhpunitWarning(
                    sprintf(
                        'Cannot log test results in JUnit XML format to "%s": %s',
                        $configuration->logfileJunit(),
                        $e->getMessage(),
                    ),
                );
            }
        }

        if ($configuration->hasLogfileOtr()) {
            try {
                new OtrXmlLogger(
                    EventFacade::instance(),
                    $configuration->logfileOtr(),
                    $configuration->includeGitInformationInOtrLogfile(),
                    $configuration->executionOrder() === TestSuiteSorter::ORDER_RANDOMIZED ? $configuration->randomOrderSeed() : null,
                );
            } catch (CannotOpenUriForWritingException $e) {
                EventFacade::emitter()->testRunnerTriggeredPhpunitWarning(
                    sprintf(
                        'Cannot log test results in Open Test Reporting XML format to "%s": %s',
                        $configuration->logfileOtr(),
                        $e->getMessage(),
                    ),
                );
            }
        }

        if ($configuration->hasLogfileTeamcity()) {
            try {
                new TeamCityLogger(
                    DefaultPrinter::from(
                        $configuration->logfileTeamcity(),
                    ),
                    EventFacade::instance(),
                );
            } catch (DirectoryDoesNotExistException|InvalidSocketException $e) {
                EventFacade::emitter()->testRunnerTriggeredPhpunitWarning(
                    sprintf(
                        'Cannot log test results in TeamCity format to "%s": %s',
                        $configuration->logfileTeamcity(),
                        $e->getMessage(),
                    ),
                );
            }
        }
    }

    private function testDoxResultCollector(Configuration $configuration): ?TestDoxResultCollector
    {
        if ($configuration->hasLogfileTestdoxHtml() ||
            $configuration->hasLogfileTestdoxText() ||
            $configuration->outputIsTestDox()) {
            return new TestDoxResultCollector(
                EventFacade::instance(),
                new IssueFilter($configuration->source()),
            );
        }

        return null;
    }

    private function initializeTestRunHistory(Configuration $configuration): TestRunHistory
    {
        if ($configuration->recordTestRunHistory()) {
            $testRunHistory = new DefaultTestRunHistory($configuration->testRunHistoryFile());

            new TestRunHistoryHandler(
                $testRunHistory,
                EventFacade::instance(),
                $this->testRunHistoryMayBePruned($configuration),
            );

            return $testRunHistory;
        }

        if ($configuration->executionOrderDefects() === TestSuiteSorter::ORDER_DEFECTS_FIRST) {
            EventFacade::emitter()->testRunnerTriggeredPhpunitWarning(
                'Tests cannot be ordered by defects because recording of the test run history is disabled',
            );
        }

        if ($configuration->executionOrder() === TestSuiteSorter::ORDER_DURATION_ASCENDING ||
            $configuration->executionOrder() === TestSuiteSorter::ORDER_DURATION_DESCENDING) {
            EventFacade::emitter()->testRunnerTriggeredPhpunitWarning(
                'Tests cannot be ordered by duration because recording of the test run history is disabled',
            );
        }

        return new NullTestRunHistory;
    }

    /**
     * Pruning drops all test run history entries that the current test run
     * did not touch, so it is only safe when the current test run executes
     * every test that exists: no test selection or filtering of any kind may
     * be configured.
     */
    private function testRunHistoryMayBePruned(Configuration $configuration): bool
    {
        if ($configuration->hasCliArguments() || $configuration->hasTestFilesFile()) {
            return false;
        }

        if ($configuration->hasFilter() || $configuration->hasExcludeFilter()) {
            return false;
        }

        if ($configuration->hasTestIdFilter() || $configuration->hasTestIdFilterFile()) {
            return false;
        }

        if ($configuration->hasGroups() || $configuration->hasExcludeGroups()) {
            return false;
        }

        if ($configuration->hasTestsCovering() || $configuration->hasTestsUsing() || $configuration->hasTestsRequiringPhpExtension()) {
            return false;
        }

        if ($configuration->includeTestSuites() !== [] || $configuration->excludeTestSuites() !== []) {
            return false;
        }

        // @codeCoverageIgnoreStart
        if ($configuration->hasDefaultTestSuite() && count($configuration->testSuite()) > 1) {
            return false;
        }
        // @codeCoverageIgnoreEnd

        return true;
    }

    private function configureBaseline(Configuration $configuration): ?BaselineGenerator
    {
        if ($configuration->hasGenerateBaseline()) {
            return new BaselineGenerator(
                EventFacade::instance(),
                $configuration->source(),
            );
        }

        if ($configuration->source()->useBaseline()) {
            $baselineFile = $configuration->source()->baseline();
            $baseline     = null;

            try {
                $baseline = (new Reader)->read($baselineFile);
            } catch (CannotLoadBaselineException $e) {
                $message = $e->getMessage();

                // @codeCoverageIgnoreStart
                if ($message === '') {
                    $message = 'Cannot load baseline';
                }
                // @codeCoverageIgnoreEnd

                EventFacade::emitter()->testRunnerTriggeredPhpunitWarning($message);
            }

            if ($baseline !== null) {
                ErrorHandler::instance()->useBaseline($baseline);
            }
        }

        return null;
    }

    private function checkPhpConfiguration(Configuration $configuration): void
    {
        if (!$configuration->warnWhenPhpIsNotConfiguredForDevelopment()) {
            return;
        }

        foreach ((new PhpConfigurationChecker)->check() as $result) {
            if ($result->isOk()) {
                continue;
            }

            EventFacade::emitter()->testRunnerTriggeredPhpunitWarning(
                sprintf(
                    'PHP is not configured for development: %s should be %s, but is %s',
                    $result->name(),
                    $result->valueForConfiguration(),
                    $result->actualValue(),
                ),
            );
        }
    }

    /**
     * @codeCoverageIgnore
     */
    private function registerInterruptHandler(): void
    {
        if (!function_exists('pcntl_async_signals')) {
            return;
        }

        $pid = getmypid();

        pcntl_async_signals(true);

        pcntl_signal(SIGINT, static function () use ($pid): void
        {
            if (getmypid() !== $pid) {
                return;
            }

            if (TestResultFacade::wasInterrupted()) {
                exit(2);
            }

            TestResultFacade::interrupt();
        });
    }

    private function exitWithCrashMessage(Throwable $t): never
    {
        $message = $t->getMessage();

        if (trim($message) === '') {
            $message = '(no message)';
        }

        printf(
            '%s%sAn error occurred inside PHPUnit.%s%sMessage:  %s',
            PHP_EOL,
            PHP_EOL,
            PHP_EOL,
            PHP_EOL,
            $message,
        );

        $first = true;

        if ($t->getPrevious() !== null) {
            $t = $t->getPrevious();
        }

        do {
            printf(
                '%s%s: %s:%d%s%s%s%s',
                PHP_EOL,
                $first ? 'Location' : 'Caused by',
                $t->getFile(),
                $t->getLine(),
                PHP_EOL,
                PHP_EOL,
                $t->getTraceAsString(),
                PHP_EOL,
            );

            $first = false;
        } while (($t = $t->getPrevious()) !== null);

        exit(Result::CRASH);
    }

    private function exitWithErrorMessage(string $message): never
    {
        print Version::getVersionString() . PHP_EOL . PHP_EOL . $message . PHP_EOL;

        exit(Result::EXCEPTION);
    }

    /**
     * @return list<PhptTestCase|TestCase>
     */
    private function filteredTests(Configuration $configuration, TestSuite $suite): array
    {
        (new TestSuiteFilterProcessor)->process($configuration, $suite);

        return $suite->collect();
    }

    private function configureDeprecationTriggers(Configuration $configuration): void
    {
        $deprecationTriggers = [
            'functions' => [],
            'methods'   => [],
        ];

        $ignoreUndefinedTriggers = $configuration->source()->deprecationTriggers()['ignoreUndefinedTriggers'] ?? false;

        foreach ($configuration->source()->deprecationTriggers()['functions'] as $function) {
            if (!function_exists($function)) {
                if (!$ignoreUndefinedTriggers) {
                    EventFacade::emitter()->testRunnerTriggeredPhpunitWarning(
                        sprintf(
                            'Function %s cannot be configured as a deprecation trigger because it is not declared',
                            $function,
                        ),
                    );
                }

                continue;
            }

            $deprecationTriggers['functions'][] = $function;
        }

        foreach ($configuration->source()->deprecationTriggers()['methods'] as $method) {
            $parts = explode('::', $method, 2);

            if (count($parts) !== 2) {
                EventFacade::emitter()->testRunnerTriggeredPhpunitWarning(
                    sprintf(
                        '%s cannot be configured as a deprecation trigger because it is not in ClassName::methodName format',
                        $method,
                    ),
                );

                continue;
            }

            [$className, $methodName] = $parts;

            if ($methodName === '' || !class_exists($className) || !method_exists($className, $methodName)) {
                if (!$ignoreUndefinedTriggers) {
                    EventFacade::emitter()->testRunnerTriggeredPhpunitWarning(
                        sprintf(
                            'Method %s::%s cannot be configured as a deprecation trigger because it is not declared',
                            $className,
                            $methodName,
                        ),
                    );
                }

                continue;
            }

            $deprecationTriggers['methods'][] = [
                'className'  => $className,
                'methodName' => $methodName,
            ];
        }

        if ($deprecationTriggers !== ['functions' => [], 'methods' => []]) {
            ErrorHandler::instance()->useDeprecationTriggers($deprecationTriggers);
        }
    }

    private function configureIssueTriggerResolvers(Configuration $configuration): void
    {
        $classNames = $configuration->source()->issueTriggerResolvers();

        foreach (array_reverse($classNames) as $className) {
            if (!class_exists($className)) {
                EventFacade::emitter()->testRunnerTriggeredPhpunitWarning(
                    sprintf(
                        'Class %s cannot be used as an issue trigger resolver because it does not exist',
                        $className,
                    ),
                );

                continue;
            }

            $resolver = new $className;

            if (!$resolver instanceof Resolver) {
                EventFacade::emitter()->testRunnerTriggeredPhpunitWarning(
                    sprintf(
                        'Class %s cannot be used as an issue trigger resolver because it does not implement %s',
                        $className,
                        Resolver::class,
                    ),
                );

                continue;
            }

            ErrorHandler::instance()->addIssueTriggerResolver($resolver);
        }
    }

    private function configureDeprecationFilters(Configuration $configuration): void
    {
        foreach ($configuration->source()->deprecationFilters() as $className) {
            if (!class_exists($className)) {
                EventFacade::emitter()->testRunnerTriggeredPhpunitWarning(
                    sprintf(
                        'Class %s cannot be used as a deprecation filter because it does not exist',
                        $className,
                    ),
                );

                continue;
            }

            $filter = new $className;

            if (!$filter instanceof DeprecationFilter) {
                EventFacade::emitter()->testRunnerTriggeredPhpunitWarning(
                    sprintf(
                        'Class %s cannot be used as a deprecation filter because it does not implement %s',
                        $className,
                        DeprecationFilter::class,
                    ),
                );

                continue;
            }

            ErrorHandler::instance()->addDeprecationFilter($filter);
        }
    }

    private function preload(): void
    {
        if (!defined('PHPUNIT_COMPOSER_INSTALL')) {
            return;
        }

        $composerInstall = PHPUNIT_COMPOSER_INSTALL;

        // @codeCoverageIgnoreStart
        if (!is_string($composerInstall)) {
            return;
        }
        // @codeCoverageIgnoreEnd

        $classMapFile = dirname($composerInstall) . '/composer/autoload_classmap.php';

        // @codeCoverageIgnoreStart
        if (!is_file($classMapFile)) {
            return;
        }
        // @codeCoverageIgnoreEnd

        $classMap = require $classMapFile;

        // @codeCoverageIgnoreStart
        if (!is_array($classMap)) {
            return;
        }
        // @codeCoverageIgnoreEnd

        foreach ($classMap as $codeUnitName => $sourceCodeFile) {
            // @codeCoverageIgnoreStart
            if (!is_string($codeUnitName) || !is_string($sourceCodeFile)) {
                continue;
            }
            // @codeCoverageIgnoreEnd

            if (!str_starts_with($codeUnitName, 'PHPUnit\\') &&
                !str_starts_with($codeUnitName, 'SebastianBergmann\\')) {
                continue;
            }

            if (str_contains($sourceCodeFile, '/tests/')) {
                continue;
            }

            require_once $sourceCodeFile;
        }
    }
}
