<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

// Please update when phpunit needs to be reinstalled with fresh deps:
// Cache-Id: 2021-02-04 11:00 UTC

if ('cli' !== \PHP_SAPI && 'phpdbg' !== \PHP_SAPI) {
    throw new Exception('This script must be run from the command line.');
}

error_reporting(-1);

global $argv, $argc;
$argv = $_SERVER['argv'] ?? [];
$argc = $_SERVER['argc'] ?? 0;
$getEnvVar = static function ($name, $default = false) use ($argv) {
    if (false !== $value = getenv($name)) {
        return $value;
    }

    static $phpunitConfig = null;
    if (null === $phpunitConfig) {
        $phpunitConfigFilename = null;
        $getPhpUnitConfig = static function ($probableConfig) use (&$getPhpUnitConfig) {
            if (!$probableConfig) {
                return null;
            }
            if (is_dir($probableConfig)) {
                return $getPhpUnitConfig($probableConfig.\DIRECTORY_SEPARATOR.'phpunit.xml');
            }

            if (file_exists($probableConfig)) {
                return $probableConfig;
            }
            if (file_exists($probableConfig.'.dist')) {
                return $probableConfig.'.dist';
            }

            return null;
        };

        foreach ($argv as $cliArgumentIndex => $cliArgument) {
            if ('--' === $cliArgument) {
                break;
            }
            // long option
            if ('--configuration' === $cliArgument && array_key_exists($cliArgumentIndex + 1, $argv)) {
                $phpunitConfigFilename = $getPhpUnitConfig($argv[$cliArgumentIndex + 1]);
                break;
            }
            // short option
            if (str_starts_with($cliArgument, '-c')) {
                if ('-c' === $cliArgument && array_key_exists($cliArgumentIndex + 1, $argv)) {
                    $phpunitConfigFilename = $getPhpUnitConfig($argv[$cliArgumentIndex + 1]);
                } else {
                    $phpunitConfigFilename = $getPhpUnitConfig(substr($cliArgument, 2));
                }
                break;
            }
        }

        $phpunitConfigFilename = $phpunitConfigFilename ?: $getPhpUnitConfig('phpunit.xml');

        if ($phpunitConfigFilename) {
            $phpunitConfig = new DOMDocument();
            $phpunitConfig->load($phpunitConfigFilename);
        } else {
            $phpunitConfig = false;
        }
    }
    if (false !== $phpunitConfig) {
        $var = new DOMXPath($phpunitConfig);
        foreach ($var->query('//php/server[@name="'.$name.'"]') as $var) {
            return $var->getAttribute('value');
        }
        foreach ($var->query('//php/env[@name="'.$name.'"]') as $var) {
            return $var->getAttribute('value');
        }
    }

    return $default;
};

$passthruOrFail = static function ($command) {
    passthru($command, $status);

    if ($status) {
        exit($status);
    }
};

$PHPUNIT_VERSION = $getEnvVar('SYMFONY_PHPUNIT_VERSION', '9.6') ?: '9.6';

$MAX_PHPUNIT_VERSION = $getEnvVar('SYMFONY_MAX_PHPUNIT_VERSION', false);

if ($MAX_PHPUNIT_VERSION && version_compare($MAX_PHPUNIT_VERSION, $PHPUNIT_VERSION, '<')) {
    $PHPUNIT_VERSION = $MAX_PHPUNIT_VERSION;
}

if (version_compare($PHPUNIT_VERSION, '10.0', '>=') && version_compare($PHPUNIT_VERSION, '11.0', '<')) {
    fwrite(\STDERR, 'This script does not work with PHPUnit 10.'.\PHP_EOL);
    exit(1);
}

$PHPUNIT_REMOVE_RETURN_TYPEHINT = filter_var($getEnvVar('SYMFONY_PHPUNIT_REMOVE_RETURN_TYPEHINT', '0'), \FILTER_VALIDATE_BOOLEAN);

$COMPOSER_JSON = getenv('COMPOSER') ?: 'composer.json';

$root = __DIR__;
while (!file_exists($root.'/'.$COMPOSER_JSON) || file_exists($root.'/DeprecationErrorHandler.php')) {
    if ($root === dirname($root)) {
        break;
    }
    $root = dirname($root);
}

$oldPwd = getcwd();
$PHPUNIT_DIR = rtrim($getEnvVar('SYMFONY_PHPUNIT_DIR', $root.'/vendor/bin/.phpunit'), '/'.\DIRECTORY_SEPARATOR);
$PHP = defined('PHP_BINARY') ? \PHP_BINARY : 'php';
$PHP = escapeshellarg($PHP);
if ('phpdbg' === \PHP_SAPI) {
    $PHP .= ' -qrr';
}

$defaultEnvs = [
    'COMPOSER' => 'composer.json',
    'COMPOSER_VENDOR_DIR' => 'vendor',
    'COMPOSER_BIN_DIR' => 'bin',
    'COMPOSER_NO_INTERACTION' => '1',
    'SYMFONY_SIMPLE_PHPUNIT_BIN_DIR' => __DIR__,
];

foreach ($defaultEnvs as $envName => $envValue) {
    if ($envValue !== getenv($envName)) {
        putenv("$envName=$envValue");
        $_SERVER[$envName] = $_ENV[$envName] = $envValue;
    }
}

if ('disabled' === $getEnvVar('SYMFONY_DEPRECATIONS_HELPER') || version_compare($PHPUNIT_VERSION, '11.0', '>=')) {
    putenv('SYMFONY_DEPRECATIONS_HELPER=disabled');
}

if (!$getEnvVar('DOCTRINE_DEPRECATIONS')) {
    putenv('DOCTRINE_DEPRECATIONS=trigger');
    $_SERVER['DOCTRINE_DEPRECATIONS'] = $_ENV['DOCTRINE_DEPRECATIONS'] = 'trigger';
}

$COMPOSER = ($COMPOSER = getenv('COMPOSER_BINARY'))
    || file_exists($COMPOSER = $oldPwd.'/composer.phar')
    || ($COMPOSER = rtrim((string) ('\\' === \DIRECTORY_SEPARATOR ? preg_replace('/[\r\n].*/', '', shell_exec('where.exe composer.phar 2> NUL')) : shell_exec('which composer.phar 2> /dev/null'))))
    || ($COMPOSER = rtrim((string) ('\\' === \DIRECTORY_SEPARATOR ? preg_replace('/[\r\n].*/', '', shell_exec('where.exe composer 2> NUL')) : shell_exec('which composer 2> /dev/null'))))
    || file_exists($COMPOSER = rtrim((string) ('\\' === \DIRECTORY_SEPARATOR ? shell_exec('git rev-parse --show-toplevel 2> NUL') : shell_exec('git rev-parse --show-toplevel 2> /dev/null'))).\DIRECTORY_SEPARATOR.'composer.phar')
    ? ('#!/usr/bin/env php' === file_get_contents($COMPOSER, false, null, 0, 18) ? $PHP : '').' '.escapeshellarg($COMPOSER) // detect shell wrappers by looking at the shebang
    : 'composer';

$prevCacheDir = getenv('COMPOSER_CACHE_DIR');
if ($prevCacheDir) {
    if (false === $absoluteCacheDir = realpath($prevCacheDir)) {
        @mkdir($prevCacheDir, 0o777, true);
        $absoluteCacheDir = realpath($prevCacheDir);
    }
    if ($absoluteCacheDir) {
        putenv("COMPOSER_CACHE_DIR=$absoluteCacheDir");
    } else {
        $prevCacheDir = false;
    }
}
$SYMFONY_PHPUNIT_REMOVE = $getEnvVar('SYMFONY_PHPUNIT_REMOVE', 'phpspec/prophecy');
$SYMFONY_PHPUNIT_REQUIRE = $getEnvVar('SYMFONY_PHPUNIT_REQUIRE', '');
$configurationHash = md5(implode(\PHP_EOL, [md5_file(__FILE__), $SYMFONY_PHPUNIT_REMOVE, $SYMFONY_PHPUNIT_REQUIRE, (int) $PHPUNIT_REMOVE_RETURN_TYPEHINT]));
$PHPUNIT_VERSION_DIR = sprintf('phpunit-%s-%d', $PHPUNIT_VERSION, $PHPUNIT_REMOVE_RETURN_TYPEHINT);
if (!file_exists("$PHPUNIT_DIR/$PHPUNIT_VERSION_DIR/phpunit") || $configurationHash !== @file_get_contents("$PHPUNIT_DIR/.$PHPUNIT_VERSION_DIR.md5")) {
    // Build a standalone phpunit without symfony/yaml nor prophecy by default

    @mkdir($PHPUNIT_DIR, 0o777, true);
    chdir($PHPUNIT_DIR);
    if (file_exists("$PHPUNIT_VERSION_DIR")) {
        passthru(sprintf('\\' === \DIRECTORY_SEPARATOR ? 'rmdir /S /Q %s 2> NUL' : 'rm -rf %s', escapeshellarg("$PHPUNIT_VERSION_DIR.old")));
        rename("$PHPUNIT_VERSION_DIR", "$PHPUNIT_VERSION_DIR.old");
        passthru(sprintf('\\' === \DIRECTORY_SEPARATOR ? 'rmdir /S /Q %s' : 'rm -rf %s', escapeshellarg("$PHPUNIT_VERSION_DIR.old")));
    }

    $info = [];
    foreach (explode("\n", shell_exec("$COMPOSER info --no-ansi -a -n phpunit/phpunit \"$PHPUNIT_VERSION.*\"")) as $line) {
        $line = rtrim($line);

        if (!$info && preg_match('/^versions +: /', $line)) {
            $info['versions'] = explode(', ', ltrim(substr($line, 9), ': '));
        } elseif (isset($info['requires'])) {
            if ('' === $line) {
                break;
            }

            $line = explode(' ', $line, 2);
            $info['requires'][$line[0]] = $line[1];
        } elseif ($info && 'requires' === $line) {
            $info['requires'] = [];
        }
    }

    if (in_array('--colors=never', $argv, true) || (isset($argv[$i = array_search('never', $argv, true) - 1]) && '--colors' === $argv[$i])) {
        $COMPOSER .= ' --no-ansi';
    } else {
        $COMPOSER .= ' --ansi';
    }

    $info += [
        'versions' => [],
        'requires' => ['php' => '*'],
    ];

    $stableVersions = array_filter($info['versions'], static fn ($v) => !preg_match('/-dev$|^dev-/', $v));

    if (!$stableVersions) {
        $passthruOrFail("$COMPOSER create-project --ignore-platform-reqs --no-install --prefer-dist --no-scripts --no-plugins --no-progress -s dev phpunit/phpunit $PHPUNIT_VERSION_DIR \"$PHPUNIT_VERSION.*\"");
    } else {
        $passthruOrFail("$COMPOSER create-project --ignore-platform-reqs --no-install --prefer-dist --no-scripts --no-plugins --no-progress phpunit/phpunit $PHPUNIT_VERSION_DIR \"$PHPUNIT_VERSION.*\"");
    }

    @copy("$PHPUNIT_VERSION_DIR/phpunit.xsd", 'phpunit.xsd');
    chdir("$PHPUNIT_VERSION_DIR");
    if ($SYMFONY_PHPUNIT_REMOVE) {
        $passthruOrFail("$COMPOSER remove --no-update ".$SYMFONY_PHPUNIT_REMOVE);
    }
    if ($SYMFONY_PHPUNIT_REQUIRE) {
        $passthruOrFail("$COMPOSER require --no-update ".$SYMFONY_PHPUNIT_REQUIRE);
    }

    if (preg_match('{\^((\d++\.)\d++)[\d\.]*$}', $info['requires']['php'], $phpVersion) && version_compare($phpVersion[2].'99', \PHP_VERSION, '<')) {
        $passthruOrFail("$COMPOSER config platform.php \"$phpVersion[1].99\"");
    } else {
        $passthruOrFail("$COMPOSER config --unset platform.php");
    }
    if (file_exists($path = $root.'/vendor/symfony/phpunit-bridge')) {
        $haystack = "$PHPUNIT_DIR/$PHPUNIT_VERSION_DIR";
        $rootLen = strlen($root);

        $p = ($rootLen <= strlen($haystack) ? str_repeat('../', substr_count($haystack, '/', $rootLen)) : '').'vendor/symfony/phpunit-bridge';
        if (realpath($p) === realpath($path)) {
            $path = $p;
        }
        $passthruOrFail("$COMPOSER require --no-update symfony/phpunit-bridge \"*@dev\"");
        $passthruOrFail("$COMPOSER config repositories.phpunit-bridge path ".escapeshellarg(str_replace('/', \DIRECTORY_SEPARATOR, $path)));
        if ('\\' === \DIRECTORY_SEPARATOR) {
            file_put_contents('composer.json', preg_replace('/^( {8})"phpunit-bridge": \{$/m', "$0\n$1    ".'"options": {"symlink": false},', file_get_contents('composer.json')));
        }
    } else {
        $passthruOrFail("$COMPOSER require --no-update symfony/phpunit-bridge \"*\"");
    }
    $prevRoot = getenv('COMPOSER_ROOT_VERSION');
    putenv("COMPOSER_ROOT_VERSION=$PHPUNIT_VERSION.99");
    // --no-suggest is not in the list to keep compat with composer 1.0, which is shipped with Ubuntu 16.04LTS
    $exit = proc_close(proc_open("$COMPOSER update --no-dev --prefer-dist --no-progress", [], $p, getcwd()));
    putenv('COMPOSER_ROOT_VERSION'.(false !== $prevRoot ? '='.$prevRoot : ''));
    if ($prevCacheDir) {
        putenv("COMPOSER_CACHE_DIR=$prevCacheDir");
    }
    if ($exit) {
        exit($exit);
    }

    // Mutate TestCase code
    if (version_compare($PHPUNIT_VERSION, '11.0', '<')) {
        $alteredCode = file_get_contents($alteredFile = './src/Framework/TestCase.php');
        if ($PHPUNIT_REMOVE_RETURN_TYPEHINT) {
            $alteredCode = preg_replace('/^    ((?:protected|public)(?: static)? function \w+\(\)): void/m', '    $1', $alteredCode);
        }
        file_put_contents($alteredFile, $alteredCode);

        // Mutate Assert code
        $alteredCode = file_get_contents($alteredFile = './src/Framework/Assert.php');
        $alteredCode = preg_replace('/abstract class Assert[^\{]+\{/', '$0 '.\PHP_EOL."    use \Symfony\Bridge\PhpUnit\Legacy\PolyfillAssertTrait;", $alteredCode, 1);
        file_put_contents($alteredFile, $alteredCode);

        file_put_contents('phpunit', <<<'EOPHP'
            <?php

            define('PHPUNIT_COMPOSER_INSTALL', __DIR__.'/vendor/autoload.php');
            require PHPUNIT_COMPOSER_INSTALL;

            if (!class_exists(\SymfonyExcludeListPhpunit::class, false)) {
                class SymfonyExcludeListPhpunit {}
            }
            if (method_exists(\PHPUnit\Util\ExcludeList::class, 'addDirectory')) {
                (new PHPUnit\Util\Excludelist())->getExcludedDirectories();
                PHPUnit\Util\ExcludeList::addDirectory(\dirname((new \ReflectionClass(\SymfonyExcludeListPhpunit::class))->getFileName()));
                class_exists(\SymfonyExcludeListSimplePhpunit::class, false) && PHPUnit\Util\ExcludeList::addDirectory(\dirname((new \ReflectionClass(\SymfonyExcludeListSimplePhpunit::class))->getFileName()));
            } elseif (method_exists(\PHPUnit\Util\Blacklist::class, 'addDirectory')) {
                (new PHPUnit\Util\BlackList())->getBlacklistedDirectories();
                PHPUnit\Util\Blacklist::addDirectory(\dirname((new \ReflectionClass(\SymfonyExcludeListPhpunit::class))->getFileName()));
                class_exists(\SymfonyExcludeListSimplePhpunit::class, false) && PHPUnit\Util\Blacklist::addDirectory(\dirname((new \ReflectionClass(\SymfonyExcludeListSimplePhpunit::class))->getFileName()));
            } else {
                PHPUnit\Util\Blacklist::$blacklistedClassNames['SymfonyExcludeListPhpunit'] = 1;
                PHPUnit\Util\Blacklist::$blacklistedClassNames['SymfonyExcludeListSimplePhpunit'] = 1;
            }

            Symfony\Bridge\PhpUnit\TextUI\Command::main();

            EOPHP
        );
    }

    chdir('..');
    file_put_contents(".$PHPUNIT_VERSION_DIR.md5", $configurationHash);
    chdir($oldPwd);
}

// Create a symlink with a predictable path pointing to the currently used version.
// This is useful for static analytics tools such as PHPStan having to load PHPUnit's classes
// and for other testing libraries such as Behat using PHPUnit's assertions.
chdir($PHPUNIT_DIR);
if ('\\' === \DIRECTORY_SEPARATOR) {
    passthru('rmdir /S /Q phpunit 2> NUL');
    passthru(sprintf('mklink /j phpunit %s > NUL 2>&1', escapeshellarg($PHPUNIT_VERSION_DIR)));
} else {
    if (file_exists('phpunit')) {
        @unlink('phpunit');
    }
    @symlink($PHPUNIT_VERSION_DIR, 'phpunit');
}
chdir($oldPwd);

if (filter_var(getenv('SYMFONY_PHPUNIT_DISABLE_RESULT_CACHE'), \FILTER_VALIDATE_BOOLEAN)) {
    $argv[] = '--do-not-cache-result';
    ++$argc;
}

$components = [];
$cmd = array_map('escapeshellarg', $argv);
$exit = 0;

if (isset($argv[1]) && 'symfony' === $argv[1] && !file_exists('symfony') && file_exists('src/Symfony')) {
    $argv[1] = 'src/Symfony';
}
if (isset($argv[1]) && is_dir($argv[1]) && !file_exists($argv[1].'/phpunit.xml.dist')) {
    // Find Symfony components in plain php for Windows portability

    $finder = new RecursiveDirectoryIterator($argv[1], FilesystemIterator::KEY_AS_FILENAME | FilesystemIterator::UNIX_PATHS);
    $finder = new RecursiveIteratorIterator($finder);
    $finder->setMaxDepth(getenv('SYMFONY_PHPUNIT_MAX_DEPTH') ?: 3);

    foreach ($finder as $file => $fileInfo) {
        if ('phpunit.xml.dist' === $file) {
            $components[] = dirname($fileInfo->getPathname());
        }
    }
    if ($components) {
        array_shift($cmd);
    }
}

$cmd[0] = sprintf('%s %s --colors=%s', $PHP, escapeshellarg("$PHPUNIT_DIR/$PHPUNIT_VERSION_DIR/phpunit"), '' === $getEnvVar('NO_COLOR', '') ? 'always' : 'never');
$cmd = str_replace('%', '%%', implode(' ', $cmd)).' %1$s';

if ('\\' === \DIRECTORY_SEPARATOR) {
    $cmd = 'cmd /v:on /d /c "('.$cmd.')%2$s"';
} else {
    $cmd .= '%2$s';
}

if (version_compare($PHPUNIT_VERSION, '11.0', '>=')) {
    $GLOBALS['_composer_autoload_path'] = "$PHPUNIT_DIR/$PHPUNIT_VERSION_DIR/vendor/autoload.php";
}

if ($components) {
    $skippedTests = $_SERVER['SYMFONY_PHPUNIT_SKIPPED_TESTS'] ?? false;
    $runningProcs = [];

    foreach ($components as $component) {
        // Run phpunit tests in parallel

        if ($skippedTests) {
            putenv("SYMFONY_PHPUNIT_SKIPPED_TESTS=$component/$skippedTests");
        }

        $c = escapeshellarg($component);

        if ($proc = proc_open(sprintf($cmd, $c, " > $c/phpunit.stdout 2> $c/phpunit.stderr"), [], $pipes)) {
            $runningProcs[$component] = $proc;
        } else {
            $exit = 1;
            echo "\033[41mKO\033[0m $component\n\n";
        }
    }

    $lastOutput = null;
    $lastOutputTime = null;

    while ($runningProcs) {
        usleep(300000);
        $terminatedProcs = [];
        foreach ($runningProcs as $component => $proc) {
            $procStatus = proc_get_status($proc);
            if (!$procStatus['running']) {
                $terminatedProcs[$component] = $procStatus['exitcode'];
                unset($runningProcs[$component]);
                proc_close($proc);
            }
        }

        if (!$terminatedProcs && 1 === count($runningProcs)) {
            $component = key($runningProcs);

            $output = file_get_contents("$component/phpunit.stdout");
            $output .= file_get_contents("$component/phpunit.stderr");

            if ($lastOutput !== $output) {
                $lastOutput = $output;
                $lastOutputTime = microtime(true);
            } elseif (microtime(true) - $lastOutputTime > 60) {
                echo "\033[41mTimeout\033[0m $component\n\n";

                if ('\\' === \DIRECTORY_SEPARATOR) {
                    exec(sprintf('taskkill /F /T /PID %d 2>&1', $procStatus['pid']), $output, $exitCode);
                } else {
                    proc_terminate(current($runningProcs));
                }
            }
        }

        foreach ($terminatedProcs as $component => $procStatus) {
            foreach (['out', 'err'] as $file) {
                $file = "$component/phpunit.std$file";
                readfile($file);
                unlink($file);
            }

            // Fail on any individual component failures but ignore some error codes on Windows when APCu is enabled:
            // STATUS_STACK_BUFFER_OVERRUN (-1073740791/0xC0000409)
            // STATUS_ACCESS_VIOLATION (-1073741819/0xC0000005)
            // STATUS_HEAP_CORRUPTION (-1073740940/0xC0000374)
            if ($procStatus && ('\\' !== \DIRECTORY_SEPARATOR || !extension_loaded('apcu') || !filter_var(ini_get('apc.enable_cli'), \FILTER_VALIDATE_BOOLEAN) || !in_array($procStatus, [-1073740791, -1073741819, -1073740940]))) {
                $exit = $procStatus;
                echo "\033[41mKO\033[0m $component\n\n";
            } else {
                echo "\033[32mOK\033[0m $component\n\n";
            }
        }
    }
} elseif (!isset($argv[1]) || 'install' !== $argv[1] || file_exists('install')) {
    if (!class_exists(SymfonyExcludeListSimplePhpunit::class, false)) {
        class SymfonyExcludeListSimplePhpunit
        {
        }
    }
    array_splice($argv, 1, 0, ['--colors='.('' === $getEnvVar('NO_COLOR', '') ? 'always' : 'never')]);
    $_SERVER['argv'] = $argv;
    $_SERVER['argc'] = ++$argc;
    include "$PHPUNIT_DIR/$PHPUNIT_VERSION_DIR/phpunit";
}

exit($exit);
