<?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\Framework\TestRunner;

use function assert;
use function hash_equals;
use function is_int;
use function property_exists;
use function strlen;
use function substr;
use function trim;
use function unserialize;
use PHPUnit\Event\Code\TestMethodBuilder;
use PHPUnit\Event\Code\ThrowableBuilder;
use PHPUnit\Event\Emitter;
use PHPUnit\Event\EventCollection;
use PHPUnit\Event\Facade;
use PHPUnit\Framework\AssertionFailedError;
use PHPUnit\Framework\Exception;
use PHPUnit\Framework\Test;
use PHPUnit\Framework\TestCase;
use PHPUnit\Runner\CodeCoverage;
use PHPUnit\TestRunner\TestResult\Facade as TestResultFacade;
use PHPUnit\TestRunner\TestResult\PassedTests;
use stdClass;

/**
 * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit
 */
final readonly class ChildProcessResultProcessor
{
    private Facade $eventFacade;
    private Emitter $emitter;
    private PassedTests $passedTests;
    private CodeCoverage $codeCoverage;

    public function __construct(Facade $eventFacade, Emitter $emitter, PassedTests $passedTests, CodeCoverage $codeCoverage)
    {
        $this->eventFacade  = $eventFacade;
        $this->emitter      = $emitter;
        $this->passedTests  = $passedTests;
        $this->codeCoverage = $codeCoverage;
    }

    /**
     * @param ?non-empty-string $processResultNonce
     */
    public function process(Test $test, string $serializedProcessResult, string $stderr, ?string $processResultNonce = null): void
    {
        if (TestResultFacade::wasInterrupted()) {
            assert($test instanceof TestCase);

            $this->emitter->testFinished(
                TestMethodBuilder::fromTestCase($test),
                0,
            );

            return;
        }

        if ($stderr !== '') {
            $exception = new Exception(trim($stderr));

            assert($test instanceof TestCase);

            $this->emitter->testErrored(
                TestMethodBuilder::fromTestCase($test),
                ThrowableBuilder::from($exception),
            );

            return;
        }

        if ($processResultNonce !== null && $serializedProcessResult !== '') {
            $nonceLength = strlen($processResultNonce);

            if (strlen($serializedProcessResult) < $nonceLength ||
                !hash_equals($processResultNonce, substr($serializedProcessResult, 0, $nonceLength))) {
                $this->emitter->childProcessErrored();

                $exception = new AssertionFailedError(
                    'Test was run in child process and the result file was tampered with or written by an unexpected process',
                );

                assert($test instanceof TestCase);

                $this->emitter->testErrored(
                    TestMethodBuilder::fromTestCase($test),
                    ThrowableBuilder::from($exception),
                );

                $this->emitter->testFinished(
                    TestMethodBuilder::fromTestCase($test),
                    0,
                );

                return;
            }

            $serializedProcessResult = substr($serializedProcessResult, $nonceLength);
        }

        $childResult = @unserialize($serializedProcessResult);

        if (!$childResult instanceof stdClass ||
            !property_exists($childResult, 'events') ||
            !property_exists($childResult, 'passedTests') ||
            !property_exists($childResult, 'testResult') ||
            !property_exists($childResult, 'numAssertions') ||
            !$childResult->events instanceof EventCollection ||
            !$childResult->passedTests instanceof PassedTests ||
            !is_int($childResult->numAssertions) ||
            $childResult->numAssertions < 0) {
            $this->emitter->childProcessErrored();

            $exception = new AssertionFailedError('Test was run in child process and ended unexpectedly');

            assert($test instanceof TestCase);

            $this->emitter->testErrored(
                TestMethodBuilder::fromTestCase($test),
                ThrowableBuilder::from($exception),
            );

            $this->emitter->testFinished(
                TestMethodBuilder::fromTestCase($test),
                0,
            );

            return;
        }

        $this->eventFacade->forward($childResult->events);
        $this->passedTests->import($childResult->passedTests);

        assert($test instanceof TestCase);

        $test->setResult($childResult->testResult);
        $test->addToAssertionCount($childResult->numAssertions);

        if (!$this->codeCoverage->isActive()) {
            return;
        }

        // @codeCoverageIgnoreStart
        if (!isset($childResult->codeCoverage) || !$childResult->codeCoverage instanceof \SebastianBergmann\CodeCoverage\CodeCoverage) {
            return;
        }

        CodeCoverage::instance()->codeCoverage()->merge(
            $childResult->codeCoverage,
        );
        // @codeCoverageIgnoreEnd
    }
}
