<?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\Util\PHP;

use Generator;
use PHPUnit\Event\Emitter;
use PHPUnit\Event\Facade;
use PHPUnit\Event\TestRunner\ChildProcessReason;
use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\Attributes\Small;
use PHPUnit\Framework\Attributes\TestDox;
use PHPUnit\Framework\Attributes\UsesClass;
use PHPUnit\Framework\TestCase;
use PHPUnit\Framework\TestRunner\ChildProcessResultProcessor;
use PHPUnit\Runner\CodeCoverage;
use PHPUnit\TestRunner\TestResult\PassedTests;

#[CoversClass(JobRunner::class)]
#[UsesClass(Job::class)]
#[UsesClass(Result::class)]
#[UsesClass(RunningJob::class)]
#[Small]
final class JobRunnerTest extends TestCase
{
    public static function provider(): Generator
    {
        yield 'output to stdout' => [
            new Result('test', ''),
            new Job(
                <<<'EOT'
<?php declare(strict_types=1);
fwrite(STDOUT, 'test');

EOT,
                ChildProcessReason::TestRequiringProcessIsolation,
            ),
        ];

        yield 'output to stderr' => [
            new Result('', 'test'),
            new Job(
                <<<'EOT'
<?php declare(strict_types=1);
fwrite(STDERR, 'test');

EOT,
                ChildProcessReason::TestRequiringProcessIsolation,
            ),
        ];

        yield 'output to stdout and stderr' => [
            new Result('test-stdout', 'test-stderr'),
            new Job(
                <<<'EOT'
<?php declare(strict_types=1);
fwrite(STDOUT, 'test-stdout');
fwrite(STDERR, 'test-stderr');

EOT,
                ChildProcessReason::TestRequiringProcessIsolation,
            ),
        ];

        yield 'stderr redirected to stdout' => [
            new Result('test', ''),
            new Job(
                <<<'EOT'
<?php declare(strict_types=1);
fwrite(STDERR, 'test');

EOT,
                ChildProcessReason::TestRequiringProcessIsolation,
                redirectErrors: true,
            ),
        ];

        yield 'configured environment variables' => [
            new Result('test', ''),
            new Job(
                <<<'EOT'
<?php declare(strict_types=1);
print getenv('test');

EOT,
                ChildProcessReason::TestRequiringProcessIsolation,
                environmentVariables: ['test' => 'test'],
            ),
        ];

        yield 'arguments' => [
            new Result('test', ''),
            new Job(
                <<<'EOT'
<?php declare(strict_types=1);
print $argv[1];

EOT,
                ChildProcessReason::TestRequiringProcessIsolation,
                arguments: ['test'],
            ),
        ];

        yield 'input from stdin' => [
            new Result('test', ''),
            new Job(
                <<<'EOT'
<?php declare(strict_types=1);
print file_get_contents('php://stdin');

EOT,
                ChildProcessReason::TestRequiringProcessIsolation,
                input: 'test',
            ),
        ];

        $obfuscationRegex = '(?i)(?:(?:"|%22)?)(?:(?:old[-_]?|new[-_]?)?p(?:ass)?w(?:or)?d(?:1|2)?|pass(?:[-_]?phrase)?|secret|(?:api_?|private_?|public_?|access_?|secret_?)key(?:_?id)?|token|consumer_?(?:id|key|secret)|sign(?:ed|ature)?|auth(?:entication|orization)?)(?:(?:\s|%20)*(?:=|%3D)[^&]+|(?:"|%22)(?:\s|%20)*(?::|%3A)(?:\s|%20)*(?:"|%22)(?:%2[^2]|%[^2]|[^"%])+(?:"|%22))|bearer(?:\s|%20)+[a-z0-9\._\-]+|token(?::|%3A)[a-z0-9]{13}|gh[opsu]_[0-9a-zA-Z]{36}|ey[I-L](?:[\w=-]|%3D)+\.ey[I-L](?:[\w=-]|%3D)+(?:\.(?:[\w.+\/=-]|%3D|%2F|%2B)+)?|-{5}BEGIN(?:[a-z\s]|%20)+PRIVATE(?:\s|%20)KEY-{5}[^\-]+-{5}END(?:[a-z\s]|%20)+PRIVATE(?:\s|%20)KEY(?:-{5})?(?:\n|%0A)?';

        yield 'PHP setting value containing INI metacharacters' => [
            new Result($obfuscationRegex, ''),
            new Job(
                <<<'EOT'
<?php declare(strict_types=1);
print ini_get('highlight.string');

EOT,
                ChildProcessReason::TestRequiringProcessIsolation,
                phpSettings: ['highlight.string=' . $obfuscationRegex],
            ),
        ];

        yield 'PHP setting without value' => [
            new Result('1', ''),
            new Job(
                <<<'EOT'
<?php declare(strict_types=1);
print ini_get('highlight.string');

EOT,
                ChildProcessReason::TestRequiringProcessIsolation,
                phpSettings: ['highlight.string'],
            ),
        ];

        // https://github.com/sebastianbergmann/environment/issues/64
        $valueContainingEquals = 'phpstorm://open?file=%f&line=%l';

        yield 'PHP setting value containing equals sign' => [
            new Result($valueContainingEquals, ''),
            new Job(
                <<<'EOT'
<?php declare(strict_types=1);
print ini_get('highlight.string');

EOT,
                ChildProcessReason::TestRequiringProcessIsolation,
                phpSettings: ['highlight.string=' . $valueContainingEquals],
            ),
        ];

        $valueContainingTilde = 'C:\Users\RUNNER~1\AppData\Local\Temp';

        yield 'PHP setting value containing tilde' => [
            new Result($valueContainingTilde, ''),
            new Job(
                <<<'EOT'
<?php declare(strict_types=1);
print ini_get('highlight.string');

EOT,
                ChildProcessReason::TestRequiringProcessIsolation,
                phpSettings: ['highlight.string=' . $valueContainingTilde],
            ),
        ];

        $valueContainingParentheses = 'C:\Program Files (x86)\PHP';

        yield 'PHP setting value containing parentheses' => [
            new Result($valueContainingParentheses, ''),
            new Job(
                <<<'EOT'
<?php declare(strict_types=1);
print ini_get('highlight.string');

EOT,
                ChildProcessReason::TestRequiringProcessIsolation,
                phpSettings: ['highlight.string=' . $valueContainingParentheses],
            ),
        ];

        yield 'PHP setting that configures xdebug.mode' => [
            new Result('test', ''),
            new Job(
                <<<'EOT'
<?php declare(strict_types=1);
print 'test';

EOT,
                ChildProcessReason::TestRequiringProcessIsolation,
                phpSettings: ['xdebug.mode=off'],
            ),
        ];

        $valueContainingConsecutiveBackslashes = '\\\\server\\share (x86)';

        yield 'PHP setting value containing consecutive backslashes' => [
            new Result($valueContainingConsecutiveBackslashes, ''),
            new Job(
                <<<'EOT'
<?php declare(strict_types=1);
print ini_get('highlight.string');

EOT,
                ChildProcessReason::TestRequiringProcessIsolation,
                phpSettings: ['highlight.string=' . $valueContainingConsecutiveBackslashes],
            ),
        ];
    }

    #[DataProvider('provider')]
    public function testRunsJobInSeparateProcess(Result $expected, Job $job): void
    {
        $jobRunner = new JobRunner(
            new ChildProcessResultProcessor(
                new Facade,
                $this->createStub(Emitter::class),
                new PassedTests,
                new CodeCoverage,
            ),
        );

        $result = $jobRunner->run($job);

        $this->assertSame($expected->stdout(), $result->stdout());
        $this->assertSame($expected->stderr(), $result->stderr());
    }

    public function testStartsJobAsProcessWhoseStandardInputRemainsOpen(): void
    {
        $jobRunner = new JobRunner(
            new ChildProcessResultProcessor(
                new Facade,
                $this->createStub(Emitter::class),
                new PassedTests,
                new CodeCoverage,
            ),
        );

        $running = $jobRunner->start(
            new Job(
                <<<'EOT'
<?php declare(strict_types=1);
fwrite(STDOUT, fgets(STDIN));

EOT,
                ChildProcessReason::TestRequiringProcessIsolation,
            ),
        );

        $running->write("echoed\n");
        $running->closeStdin();

        $this->assertSame("echoed\n", $running->wait()->stdout());
    }

    #[TestDox('Server variables that cannot be represented as environment variables are not forwarded')]
    public function testDoesNotForwardServerVariablesThatAreNotEnvironmentVariables(): void
    {
        $server = $_SERVER;

        $_SERVER[0]                     = 'value for non-string key';
        $_SERVER['__test_array_value']  = ['value'];
        $_SERVER['__test_string_value'] = 'value';

        try {
            $jobRunner = new JobRunner(
                new ChildProcessResultProcessor(
                    new Facade,
                    $this->createStub(Emitter::class),
                    new PassedTests,
                    new CodeCoverage,
                ),
            );

            $result = $jobRunner->run(
                new Job(
                    <<<'EOT'
<?php declare(strict_types=1);
var_dump(getenv('__test_array_value'), getenv('__test_string_value'));

EOT,
                    ChildProcessReason::TestRequiringProcessIsolation,
                    environmentVariables: ['test' => 'test'],
                ),
            );

            $this->assertSame("bool(false)\nstring(5) \"value\"\n", $result->stdout());
        } finally {
            $_SERVER = $server;
        }
    }

    public function testRejectsPhpSettingValueContainingLineBreak(): void
    {
        $jobRunner = new JobRunner(
            new ChildProcessResultProcessor(
                new Facade,
                $this->createStub(Emitter::class),
                new PassedTests,
                new CodeCoverage,
            ),
        );

        $job = new Job(
            <<<'EOT'
<?php declare(strict_types=1);

EOT,
            ChildProcessReason::TestRequiringProcessIsolation,
            phpSettings: ["highlight.string=foo\nauto_prepend_file=/tmp/evil.php"],
        );

        $this->expectException(PhpProcessException::class);
        $this->expectExceptionMessage('PHP setting "highlight.string" contains a line-break character, which is not permitted');

        $jobRunner->run($job);
    }
}
