Spamworldpro Mini Shell
Spamworldpro


Server : Apache
System : Linux server2.corals.io 4.18.0-348.2.1.el8_5.x86_64 #1 SMP Mon Nov 15 09:17:08 EST 2021 x86_64
User : corals ( 1002)
PHP Version : 7.4.33
Disable Function : exec,passthru,shell_exec,system
Directory :  /home/corals/old/vendor/allure-framework/allure-php-commons/test/

Upload File :
current_dir [ Writeable ] document_root [ Writeable ]

 

Current File : /home/corals/old/vendor/allure-framework/allure-php-commons/test/AllureLifecycleTest.php
<?php

declare(strict_types=1);

namespace Qameta\Allure\Test;

use DateTimeImmutable;
use Exception;
use PHPUnit\Framework\TestCase;
use Psr\Log\LoggerInterface;
use Qameta\Allure\AllureLifecycle;
use Qameta\Allure\Exception\ActiveContainerNotFoundException;
use Qameta\Allure\Exception\ActiveExecutionContextNotFoundException;
use Qameta\Allure\Exception\ActiveStepNotFoundException;
use Qameta\Allure\Exception\ActiveTestNotFoundException;
use Qameta\Allure\Exception\InvalidExecutionContextException;
use Qameta\Allure\Internal\HooksNotifierInterface;
use Qameta\Allure\Internal\ResultStorageInterface;
use Qameta\Allure\Internal\ThreadContext;
use Qameta\Allure\Internal\ThreadContextInterface;
use Qameta\Allure\Io\ClockInterface;
use Qameta\Allure\Io\DataSourceInterface;
use Qameta\Allure\Io\ResultsWriterInterface;
use Qameta\Allure\Model\AttachmentResult;
use Qameta\Allure\Model\ContainerResult;
use Qameta\Allure\Model\ExecutionContextInterface;
use Qameta\Allure\Model\FixtureResult;
use Qameta\Allure\Model\Stage;
use Qameta\Allure\Model\StepResult;
use Qameta\Allure\Model\StorableResultInterface;
use Qameta\Allure\Model\TestResult;
use Throwable;

/**
 * @covers \Qameta\Allure\AllureLifecycle
 */
class AllureLifecycleTest extends TestCase
{
    /**
     * @dataProvider providerSwitchThread
     */
    public function testSwitchThread_GivenThread_SwitcherToSameThreadInContext(?string $thread): void
    {
        $threadContext = $this->createMock(ThreadContextInterface::class);
        $lifecycle = new AllureLifecycle(
            $this->createStub(LoggerInterface::class),
            $this->createClock(),
            $this->createStub(ResultsWriterInterface::class),
            $this->createStub(HooksNotifierInterface::class),
            $this->createStub(ResultStorageInterface::class),
            $threadContext,
        );
        $threadContext
            ->expects(self::once())
            ->method('switchThread')
            ->with(self::identicalTo($thread));
        $lifecycle->switchThread($thread);
    }

    /**
     * @dataProvider providerGetCurrentTest
     */
    public function testGetCurrentTest_Constructed_ReturnsCurrentTestFromContext(?string $currentTest): void
    {
        $threadContext = $this->createStub(ThreadContextInterface::class);
        $lifecycle = new AllureLifecycle(
            $this->createStub(LoggerInterface::class),
            $this->createClock(),
            $this->createStub(ResultsWriterInterface::class),
            $this->createStub(HooksNotifierInterface::class),
            $this->createStub(ResultStorageInterface::class),
            $threadContext,
        );
        $threadContext
            ->method('getCurrentTest')
            ->willReturn($currentTest);
        self::assertSame($currentTest, $lifecycle->getCurrentTest());
    }

    /**
     * @return iterable<string, array{string|null}>
     */
    public static function providerGetCurrentTest(): iterable
    {
        return [
            'No current test' => [null],
            'Current test exists' => ['a'],
        ];
    }

    /**
     * @dataProvider providerGetCurrentStep
     */
    public function testGetCurrentStep_Constructed_ReturnsCurrentStepFromContext(?string $currentTest): void
    {
        $threadContext = $this->createStub(ThreadContextInterface::class);
        $lifecycle = new AllureLifecycle(
            $this->createStub(LoggerInterface::class),
            $this->createClock(),
            $this->createStub(ResultsWriterInterface::class),
            $this->createStub(HooksNotifierInterface::class),
            $this->createStub(ResultStorageInterface::class),
            $threadContext,
        );
        $threadContext
            ->method('getCurrentStep')
            ->willReturn($currentTest);
        self::assertSame($currentTest, $lifecycle->getCurrentStep());
    }

    /**
     * @return iterable<string, array{string|null}>
     */
    public static function providerGetCurrentStep(): iterable
    {
        return [
            'No current step' => [null],
            'Current step exists' => ['a'],
        ];
    }

    /**
     * @dataProvider providerGetCurrentTestOrStep
     */
    public function testGetCurrentTestOrStep_Constructed_ReturnsCurrentTestOrStepFromContext(?string $currentTest): void
    {
        $threadContext = $this->createStub(ThreadContextInterface::class);
        $lifecycle = new AllureLifecycle(
            $this->createStub(LoggerInterface::class),
            $this->createClock(),
            $this->createStub(ResultsWriterInterface::class),
            $this->createStub(HooksNotifierInterface::class),
            $this->createStub(ResultStorageInterface::class),
            $threadContext,
        );
        $threadContext
            ->method('getCurrentTestOrStep')
            ->willReturn($currentTest);
        self::assertSame($currentTest, $lifecycle->getCurrentTestOrStep());
    }

    /**
     * @return iterable<string, array{string|null}>
     */
    public static function providerGetCurrentTestOrStep(): iterable
    {
        return [
            'No current test or step' => [null],
            'Current test or step exists' => ['a'],
        ];
    }

    /**
     * @return iterable<string, array{string|null}>
     */
    public static function providerSwitchThread(): iterable
    {
        return [
            'Default thread' => [null],
            'Custom thread' => ['a'],
        ];
    }

    public function testStartContainer_NoExceptionsThrownDuringStart_NotifiesHooksWithoutError(): void
    {
        $hooksNotifier = $this->createMock(HooksNotifierInterface::class);
        $lifecycle = new AllureLifecycle(
            $this->createStub(LoggerInterface::class),
            $this->createClock(),
            $this->createStub(ResultsWriterInterface::class),
            $hooksNotifier,
            $this->createStub(ResultStorageInterface::class),
            new ThreadContext(),
        );

        $container = new ContainerResult('a');
        $hooksNotifier
            ->expects(self::once())
            ->method('beforeContainerStart')
            ->id('before')
            ->with(self::identicalTo($container));
        $hooksNotifier
            ->expects(self::never())
            ->method('onLifecycleError');
        $hooksNotifier
            ->expects(self::once())
            ->after('before')
            ->method('afterContainerStart')
            ->with(self::identicalTo($container));
        $lifecycle->startContainer($container);
    }

    public function testStartContainer_ExceptionThrownDuringStart_NotifiesHooksWithError(): void
    {
        $hooksNotifier = $this->createMock(HooksNotifierInterface::class);
        $error = new Exception();
        $container = new ContainerResult('a');
        $lifecycle = new AllureLifecycle(
            $this->createStub(LoggerInterface::class),
            $this->createClock(),
            $this->createStub(ResultsWriterInterface::class),
            $hooksNotifier,
            $this->createNonSettableStorage($container, $error),
            new ThreadContext(),
        );

        $hooksNotifier
            ->expects(self::once())
            ->id('before')
            ->method('beforeContainerStart')
            ->with(self::identicalTo($container));
        $hooksNotifier
            ->expects(self::once())
            ->id('error')
            ->after('before')
            ->method('onLifecycleError')
            ->with(self::identicalTo($error));
        $hooksNotifier
            ->expects(self::once())
            ->after('error')
            ->method('afterContainerStart')
            ->with(self::identicalTo($container));
        $lifecycle->startContainer($container);
    }

    public function testStartContainer_NoExceptionsThrownDuringStart_NeverLogsError(): void
    {
        $logger = $this->createMock(LoggerInterface::class);
        $lifecycle = new AllureLifecycle(
            $logger,
            $this->createClock(),
            $this->createStub(ResultsWriterInterface::class),
            $this->createStub(HooksNotifierInterface::class),
            $this->createStub(ResultStorageInterface::class),
            new ThreadContext(),
        );

        $logger
            ->expects(self::never())
            ->method('error');
        $lifecycle->startContainer(new ContainerResult('a'));
    }

    public function testStartContainer_StorageFailsToSetContainer_LogsError(): void
    {
        $logger = $this->createMock(LoggerInterface::class);
        $error = new Exception();
        $container = new ContainerResult('a');
        $lifecycle = new AllureLifecycle(
            $logger,
            $this->createClock(),
            $this->createStub(ResultsWriterInterface::class),
            $this->createStub(HooksNotifierInterface::class),
            $this->createNonSettableStorage($container, $error),
            new ThreadContext(),
        );

        $logger
            ->expects(self::once())
            ->method('error')
            ->with(
                self::stringContains('Container (UUID: {uuid}) not started'),
                self::identicalTo(['uuid' => 'a', 'exception' => $error]),
            );
        $lifecycle->startContainer($container);
    }

    public function testStartContainer_ClockProvidesTime_ContainerStartIsSetToSameTime(): void
    {
        $time = new DateTimeImmutable('@0');
        $lifecycle = new AllureLifecycle(
            $this->createStub(LoggerInterface::class),
            $this->createClock($time),
            $this->createStub(ResultsWriterInterface::class),
            $this->createStub(HooksNotifierInterface::class),
            $this->createStub(ResultStorageInterface::class),
            new ThreadContext(),
        );

        $container = new ContainerResult('a');
        $lifecycle->startContainer($container);
        self::assertSame($time, $container->getStart());
    }

    public function testStartContainer_GivenContainer_SetsSameContainerInStorage(): void
    {
        $resultStorage = $this->createMock(ResultStorageInterface::class);
        $lifecycle = new AllureLifecycle(
            $this->createStub(LoggerInterface::class),
            $this->createClock(),
            $this->createStub(ResultsWriterInterface::class),
            $this->createStub(HooksNotifierInterface::class),
            $resultStorage,
            new ThreadContext(),
        );

        $container = new ContainerResult('a');
        $resultStorage
            ->expects(self::once())
            ->method('set')
            ->with(self::identicalTo($container));
        $lifecycle->startContainer($container);
    }

    public function testUpdateContainer_ContainerNeitherGivenNorStarted_LogsErrorAndReturnsNull(): void
    {
        $logger = $this->createMock(LoggerInterface::class);
        $lifecycle = new AllureLifecycle(
            $logger,
            $this->createClock(),
            $this->createStub(ResultsWriterInterface::class),
            $this->createStub(HooksNotifierInterface::class),
            $this->createStub(ResultStorageInterface::class),
            new ThreadContext(),
        );

        $logger
            ->expects(self::once())
            ->method('error')
            ->with(
                self::stringContains('Container (UUID: {uuid}) not updated'),
                self::equalTo(['uuid' => null, 'exception' => new ActiveContainerNotFoundException()]),
            );
        self::assertNull($lifecycle->updateContainer(fn () => null));
    }

    public function testUpdateContainer_StorageFailsToProvideGivenContainer_LogsErrorAndReturnsNull(): void
    {
        $logger = $this->createMock(LoggerInterface::class);
        $error = new Exception();
        $lifecycle = new AllureLifecycle(
            $logger,
            $this->createClock(),
            $this->createStub(ResultsWriterInterface::class),
            $this->createStub(HooksNotifierInterface::class),
            $this->createStorageWithoutContainer('a', $error),
            new ThreadContext(),
        );

        $logger
            ->expects(self::once())
            ->method('error')
            ->with(
                self::stringContains('Container (UUID: {uuid}) not updated'),
                self::equalTo(['uuid' => 'a', 'exception' => $error]),
            );
        self::assertNull($lifecycle->updateContainer(fn () => null, 'a'));
    }

    public function testUpdateContainer_StorageFailsToProvideStartedContainer_LogsErrorAndReturnsNull(): void
    {
        $logger = $this->createMock(LoggerInterface::class);
        $error = new Exception();
        $lifecycle = new AllureLifecycle(
            $logger,
            $this->createClock(),
            $this->createStub(ResultsWriterInterface::class),
            $this->createStub(HooksNotifierInterface::class),
            $this->createStorageWithoutContainer('a', $error),
            new ThreadContext(),
        );

        $lifecycle->startContainer(new ContainerResult('a'));

        $logger
            ->expects(self::once())
            ->method('error')
            ->with(
                self::stringContains('Container (UUID: {uuid}) not updated'),
                self::equalTo(['uuid' => 'a', 'exception' => $error]),
            );
        self::assertNull($lifecycle->updateContainer(fn () => null));
    }

    public function testUpdateContainer_ContainerNotGivenButStarted_NeverLogsErrorAndReturnsUuid(): void
    {
        $logger = $this->createMock(LoggerInterface::class);
        $container = new ContainerResult('a');
        $lifecycle = new AllureLifecycle(
            $logger,
            $this->createClock(),
            $this->createStub(ResultsWriterInterface::class),
            $this->createStub(HooksNotifierInterface::class),
            $this->createStorageWithContainer($container),
            new ThreadContext(),
        );

        $lifecycle->startContainer($container);

        $logger
            ->expects(self::never())
            ->method('error');
        self::assertSame('a', $lifecycle->updateContainer(fn () => null));
    }

    public function testUpdateContainer_StorageProvidesGivenContainer_NeverLogsErrorAndReturnsUuid(): void
    {
        $logger = $this->createMock(LoggerInterface::class);
        $lifecycle = new AllureLifecycle(
            $logger,
            $this->createClock(),
            $this->createStub(ResultsWriterInterface::class),
            $this->createStub(HooksNotifierInterface::class),
            $this->createStorageWithContainer(new ContainerResult('a')),
            new ThreadContext(),
        );

        $logger
            ->expects(self::never())
            ->method('error');
        self::assertSame('a', $lifecycle->updateContainer(fn () => null, 'a'));
    }

    public function testUpdateContainer_NoExceptionThrownDuringUpdate_NotifiesHooksWithoutError(): void
    {
        $hooksNotifier = $this->createMock(HooksNotifierInterface::class);
        $container = new ContainerResult('a');
        $lifecycle = new AllureLifecycle(
            $this->createStub(LoggerInterface::class),
            $this->createClock(),
            $this->createStub(ResultsWriterInterface::class),
            $hooksNotifier,
            $this->createStorageWithContainer($container),
            new ThreadContext(),
        );

        $hooksNotifier
            ->expects(self::once())
            ->id('before')
            ->method('beforeContainerUpdate')
            ->with(self::identicalTo($container));
        $hooksNotifier
            ->expects(self::never())
            ->method('onLifecycleError');
        $hooksNotifier
            ->expects(self::once())
            ->after('before')
            ->method('afterContainerUpdate')
            ->with(self::identicalTo($container));
        $lifecycle->updateContainer(fn () => null, 'a');
    }

    public function testUpdateContainer_ExceptionThrownDuringUpdate_NotifiesHooksWithError(): void
    {
        $hooksNotifier = $this->createMock(HooksNotifierInterface::class);
        $container = new ContainerResult('a');
        $lifecycle = new AllureLifecycle(
            $this->createStub(LoggerInterface::class),
            $this->createClock(),
            $this->createStub(ResultsWriterInterface::class),
            $hooksNotifier,
            $this->createStorageWithContainer($container),
            new ThreadContext(),
        );

        $error = new Exception();
        $hooksNotifier
            ->expects(self::once())
            ->id('before')
            ->method('beforeContainerUpdate')
            ->with(self::identicalTo($container));
        $hooksNotifier
            ->expects(self::once())
            ->id('error')
            ->after('before')
            ->method('onLifecycleError')
            ->with(self::identicalTo($error));
        $hooksNotifier
            ->expects(self::once())
            ->after('error')
            ->method('afterContainerUpdate')
            ->with(self::identicalTo($container));
        $lifecycle->updateContainer(fn () => throw $error, 'a');
    }

    public function testUpdateContainer_CallbackThrowsException_LogsErrorAndReturnsUuid(): void
    {
        $logger = $this->createMock(LoggerInterface::class);
        $container = new ContainerResult('a');
        $lifecycle = new AllureLifecycle(
            $logger,
            $this->createClock(),
            $this->createStub(ResultsWriterInterface::class),
            $this->createStub(HooksNotifierInterface::class),
            $this->createStorageWithContainer($container),
            new ThreadContext(),
        );
        $error = new Exception();
        $logger
            ->expects(self::once())
            ->method('error')
            ->with(
                self::stringContains('Container (UUID: {uuid}) not updated'),
                self::identicalTo(['uuid' => 'a', 'exception' => $error]),
            );
        self::assertSame('a', $lifecycle->updateContainer(fn () => throw $error, 'a'));
    }

    public function testUpdateContainer_StorageProvidesContainer_SameContainerPassedToCallback(): void
    {
        $container = new ContainerResult('a');
        $lifecycle = new AllureLifecycle(
            $this->createStub(LoggerInterface::class),
            $this->createClock(),
            $this->createStub(ResultsWriterInterface::class),
            $this->createStub(HooksNotifierInterface::class),
            $this->createStorageWithContainer($container),
            new ThreadContext(),
        );

        $lifecycle->updateContainer(fn (ContainerResult $c) => $c->setName('b'), 'a');
        self::assertSame('b', $container->getName());
    }

    public function testStopContainer_ContainerNeitherGivenNorStarted_LogsErrorAndReturnsNull(): void
    {
        $logger = $this->createMock(LoggerInterface::class);
        $lifecycle = new AllureLifecycle(
            $logger,
            $this->createClock(),
            $this->createStub(ResultsWriterInterface::class),
            $this->createStub(HooksNotifierInterface::class),
            $this->createStub(ResultStorageInterface::class),
            new ThreadContext(),
        );

        $logger
            ->expects(self::once())
            ->method('error')
            ->with(
                self::stringContains('Container (UUID: {uuid}) not stopped'),
                self::equalTo(['uuid' => null, 'exception' => new ActiveContainerNotFoundException()]),
            );
        self::assertNull($lifecycle->stopContainer());
    }

    public function testStopContainer_StorageFailsToProvideGivenContainer_LogsErrorAndReturnsNull(): void
    {
        $logger = $this->createMock(LoggerInterface::class);
        $error = new Exception();
        $lifecycle = new AllureLifecycle(
            $logger,
            $this->createClock(),
            $this->createStub(ResultsWriterInterface::class),
            $this->createStub(HooksNotifierInterface::class),
            $this->createStorageWithoutContainer('a', $error),
            new ThreadContext(),
        );

        $logger
            ->expects(self::once())
            ->method('error')
            ->with(
                self::stringContains('Container (UUID: {uuid}) not stopped'),
                self::equalTo(['uuid' => 'a', 'exception' => $error]),
            );
        self::assertNull($lifecycle->stopContainer('a'));
    }

    public function testStopContainer_StorageFailsToProvideStartedContainer_LogsErrorAndReturnsNull(): void
    {
        $logger = $this->createMock(LoggerInterface::class);
        $error = new Exception();
        $lifecycle = new AllureLifecycle(
            $logger,
            $this->createClock(),
            $this->createStub(ResultsWriterInterface::class),
            $this->createStub(HooksNotifierInterface::class),
            $this->createStorageWithoutContainer('a', $error),
            new ThreadContext(),
        );

        $lifecycle->startContainer(new ContainerResult('a'));

        $logger
            ->expects(self::once())
            ->method('error')
            ->with(
                self::stringContains('Container (UUID: {uuid}) not stopped'),
                self::equalTo(['uuid' => 'a', 'exception' => $error]),
            );
        self::assertNull($lifecycle->stopContainer());
    }

    public function testStopContainer_ContainerNotGivenButStarted_NeverLogsErrorAndReturnsUuid(): void
    {
        $logger = $this->createMock(LoggerInterface::class);
        $container = new ContainerResult('a');
        $lifecycle = new AllureLifecycle(
            $logger,
            $this->createClock(),
            $this->createStub(ResultsWriterInterface::class),
            $this->createStub(HooksNotifierInterface::class),
            $this->createStorageWithContainer($container),
            new ThreadContext(),
        );

        $lifecycle->startContainer($container);
        $logger
            ->expects(self::never())
            ->method('error');
        self::assertSame('a', $lifecycle->stopContainer());
    }

    public function testStopContainer_StorageProvidesGivenContainer_NeverLogsErrorAndReturnsUuid(): void
    {
        $logger = $this->createMock(LoggerInterface::class);
        $lifecycle = new AllureLifecycle(
            $logger,
            $this->createClock(),
            $this->createStub(ResultsWriterInterface::class),
            $this->createStub(HooksNotifierInterface::class),
            $this->createStorageWithContainer(new ContainerResult('a')),
            new ThreadContext(),
        );
        $logger
            ->expects(self::never())
            ->method('error');
        self::assertSame('a', $lifecycle->stopContainer('a'));
    }

    public function testStopContainer_ClockProvidesTime_ContainerHasSameStop(): void
    {
        $time = new DateTimeImmutable('@0');
        $container = new ContainerResult('a');
        $lifecycle = new AllureLifecycle(
            $this->createStub(LoggerInterface::class),
            $this->createClock($time),
            $this->createStub(ResultsWriterInterface::class),
            $this->createStub(HooksNotifierInterface::class),
            $this->createStorageWithContainer($container),
            new ThreadContext(),
        );

        $lifecycle->stopContainer('a');
        self::assertSame($time, $container->getStop());
    }

    public function testStopContainer_NoExceptionThrownDuringStop_NotifiesHooksWithoutError(): void
    {
        $hooksNotifier = $this->createMock(HooksNotifierInterface::class);
        $container = new ContainerResult('a');
        $lifecycle = new AllureLifecycle(
            $this->createStub(LoggerInterface::class),
            $this->createClock(),
            $this->createStub(ResultsWriterInterface::class),
            $hooksNotifier,
            $this->createStorageWithContainer($container),
            new ThreadContext(),
        );

        $hooksNotifier
            ->expects(self::once())
            ->id('before')
            ->method('beforeContainerStop')
            ->with(self::identicalTo($container));
        $hooksNotifier
            ->expects(self::never())
            ->method('onLifecycleError');
        $hooksNotifier
            ->expects(self::once())
            ->after('before')
            ->method('afterContainerStop')
            ->with(self::identicalTo($container));
        $lifecycle->stopContainer('a');
    }

    public function testStopContainer_ExceptionThrownDuringStop_NotifiesHooksWithError(): void
    {
        $hooksNotifier = $this->createMock(HooksNotifierInterface::class);
        $error = new Exception();
        $container = new ContainerResult('a');
        $lifecycle = new AllureLifecycle(
            $this->createStub(LoggerInterface::class),
            $this->createFailingClock($error),
            $this->createStub(ResultsWriterInterface::class),
            $hooksNotifier,
            $this->createStorageWithContainer($container),
            new ThreadContext(),
        );
        $hooksNotifier
            ->expects(self::once())
            ->id('before')
            ->method('beforeContainerStop')
            ->with(self::identicalTo($container));
        $hooksNotifier
            ->expects(self::once())
            ->id('error')
            ->after('before')
            ->method('onLifecycleError')
            ->with(self::identicalTo($error));
        $hooksNotifier
            ->expects(self::once())
            ->after('error')
            ->method('afterContainerStop')
            ->with(self::identicalTo($container));
        $lifecycle->stopContainer('a');
    }

    public function testWriteContainer_StorageFailsToProvideContainer_LogsError(): void
    {
        $logger = $this->createMock(LoggerInterface::class);
        $error = new Exception();
        $lifecycle = new AllureLifecycle(
            $logger,
            $this->createClock(),
            $this->createStub(ResultsWriterInterface::class),
            $this->createStub(HooksNotifierInterface::class),
            $this->createStorageWithoutContainer('a', $error),
            new ThreadContext(),
        );
        $logger
            ->expects(self::once())
            ->method('error')
            ->with(
                self::stringContains('Container (UUID: {uuid}) not written'),
                self::identicalTo(['uuid' => 'a', 'exception' => $error]),
            );
        $lifecycle->writeContainer('a');
    }

    public function testWriteContainer_NoExceptionThrownDuringWrite_NotifiesHooksWithoutError(): void
    {
        $hooksNotifier = $this->createMock(HooksNotifierInterface::class);
        $container = new ContainerResult('a');
        $lifecycle = new AllureLifecycle(
            $this->createStub(LoggerInterface::class),
            $this->createClock(),
            $this->createStub(ResultsWriterInterface::class),
            $hooksNotifier,
            $this->createStorageWithContainer($container),
            new ThreadContext(),
        );
        $hooksNotifier
            ->expects(self::once())
            ->id('before')
            ->method('beforeContainerWrite')
            ->with(self::identicalTo($container));
        $hooksNotifier
            ->expects(self::never())
            ->method('onLifecycleError');
        $hooksNotifier
            ->expects(self::once())
            ->after('before')
            ->method('afterContainerWrite')
            ->with(self::identicalTo($container));
        $lifecycle->writeContainer('a');
    }

    public function testWriteContainer_ExceptionThrownDuringWrite_NotifiesHooksWithError(): void
    {
        $hooksNotifier = $this->createMock(HooksNotifierInterface::class);
        $container = new ContainerResult('a');
        $error = new Exception();
        $lifecycle = new AllureLifecycle(
            $this->createStub(LoggerInterface::class),
            $this->createClock(),
            $this->createStub(ResultsWriterInterface::class),
            $hooksNotifier,
            $this->createStorageWithContainer($container, unsetError: $error),
            new ThreadContext(),
        );
        $hooksNotifier
            ->expects(self::once())
            ->id('before')
            ->method('beforeContainerWrite')
            ->with(self::identicalTo($container));
        $hooksNotifier
            ->expects(self::once())
            ->id('error')
            ->after('before')
            ->method('onLifecycleError')
            ->with(self::identicalTo($error));
        $hooksNotifier
            ->expects(self::once())
            ->after('error')
            ->method('afterContainerWrite')
            ->with(self::identicalTo($container));
        $lifecycle->writeContainer('a');
    }

    public function testWriteContainer_ExceptionThrownDuringWrite_LogsError(): void
    {
        $logger = $this->createMock(LoggerInterface::class);
        $container = new ContainerResult('a');
        $error = new Exception();
        $lifecycle = new AllureLifecycle(
            $logger,
            $this->createClock(),
            $this->createStub(ResultsWriterInterface::class),
            $this->createStub(HooksNotifierInterface::class),
            $this->createStorageWithContainer($container, unsetError: $error),
            new ThreadContext(),
        );
        $logger
            ->expects(self::once())
            ->method('error')
            ->with(
                self::stringContains('Container (UUID: {uuid}) not written'),
                self::identicalTo(['uuid' => 'a', 'exception' => $error]),
            );
        $lifecycle->writeContainer('a');
    }

    public function testWriteContainer_ContainerWithGivenUuid_StorageUnsetsResultWithSameUuid(): void
    {
        $container = new ContainerResult('a');
        $resultStorage = $this->createMock(ResultStorageInterface::class);
        $resultStorage
            ->method('getContainer')
            ->willReturn($container);
        $lifecycle = new AllureLifecycle(
            $this->createStub(LoggerInterface::class),
            $this->createClock(),
            $this->createStub(ResultsWriterInterface::class),
            $this->createStub(HooksNotifierInterface::class),
            $resultStorage,
            new ThreadContext(),
        );

        $resultStorage
            ->expects(self::once())
            ->method('unset')
            ->with(self::identicalTo('a'));
        $lifecycle->writeContainer('a');
    }

    public function testWriteContainer_ExcludedContainerWithNestedResults_RemovesNestedResults(): void
    {
        $resultsWriter = $this->createMock(ResultsWriterInterface::class);

        $container = new ContainerResult('a');

        $setUp = new FixtureResult('b');
        $setUpAttachment = new AttachmentResult('c');
        $setUp->addAttachments($setUpAttachment);
        $setUpStep = new StepResult('d');
        $setUpStepAttachment = new AttachmentResult('e');
        $setUpStep->addAttachments($setUpStepAttachment);
        $setUp->addSteps($setUpStep);
        $container->addBefores($setUp);

        $test = new TestResult('f');
        $testAttachment = new AttachmentResult('g');
        $test->addAttachments($testAttachment);
        $testStep = new StepResult('h');
        $testStepAttachment = new AttachmentResult('i');
        $testStep->addAttachments($testStepAttachment);
        $test->addSteps($testStep);
        $container->addChildren($test);

        $tearDown = new FixtureResult('j');
        $tearDownAttachment = new AttachmentResult('k');
        $tearDown->addAttachments($tearDownAttachment);
        $tearDownStep = new StepResult('l');
        $tearDownStepAttachment = new AttachmentResult('m');
        $tearDownStep->addAttachments($tearDownStepAttachment);
        $tearDown->addSteps($tearDownStep);
        $container->addAfters($tearDown);

        $lifecycle = new AllureLifecycle(
            $this->createStub(LoggerInterface::class),
            $this->createClock(),
            $resultsWriter,
            $this->createStub(HooksNotifierInterface::class),
            $this->createStorageWithContainer($container),
            new ThreadContext(),
        );

        $container->setExcluded(true);
        $removeAttachmentResults = [];
        $resultsWriter
            ->expects(self::exactly(6))
            ->method('removeAttachment')
            ->with(
                self::callback(
                    function (AttachmentResult $attachmentResult) use (&$removeAttachmentResults): bool {
                        /** @psalm-var list<AttachmentResult> $removeAttachmentResults */
                        $removeAttachmentResults[] = $attachmentResult;

                        return true;
                    },
                ),
            );
        $removeTestResults = [];
        $resultsWriter
            ->expects(self::exactly(1))
            ->method('removeTest')
            ->with(
                self::callback(
                    function (TestResult $testResult) use (&$removeTestResults): bool {
                        /** @psalm-var list<TestResult> $removeTestResults */
                        $removeTestResults[] = $testResult;

                        return true;
                    },
                ),
            );
        $lifecycle->writeContainer('a');
        self::assertSame(
            [
                $setUpAttachment,
                $setUpStepAttachment,
                $testAttachment,
                $testStepAttachment,
                $tearDownAttachment,
                $tearDownStepAttachment,
            ],
            $removeAttachmentResults,
        );
        self::assertSame([$test], $removeTestResults);
    }

    public function testWriteContainer_WriterFailsToRemoveExcludedTest_LogsError(): void
    {
        $logger = $this->createMock(LoggerInterface::class);
        $error = new Exception();
        $resultsWriter = $this->createStub(ResultsWriterInterface::class);
        $resultsWriter
            ->method('removeTest')
            ->willThrowException($error);

        $container = new ContainerResult('a');

        $test = new TestResult('b');
        $container->addChildren($test->setExcluded(true));

        $lifecycle = new AllureLifecycle(
            $logger,
            $this->createClock(),
            $resultsWriter,
            $this->createStub(HooksNotifierInterface::class),
            $this->createStorageWithContainer($container),
            new ThreadContext(),
        );

        $logger
            ->expects(self::once())
            ->method('error')
            ->with(
                self::stringContains('Test (UUID: {uuid}) not removed'),
                self::identicalTo(['uuid' => 'b', 'exception' => $error]),
            );
        $lifecycle->writeContainer('a');
    }

    public function testWriteContainer_WriterFailsToRemoveExcludedAttachment_LogsError(): void
    {
        $logger = $this->createMock(LoggerInterface::class);
        $error = new Exception();
        $resultsWriter = $this->createStub(ResultsWriterInterface::class);
        $resultsWriter
            ->method('removeAttachment')
            ->willThrowException($error);

        $container = new ContainerResult('a');

        $test = new TestResult('b');
        $testAttachment = new AttachmentResult('c');
        $test->addAttachments($testAttachment->setExcluded(true));
        $container->addChildren($test);

        $lifecycle = new AllureLifecycle(
            $logger,
            $this->createClock(),
            $resultsWriter,
            $this->createStub(HooksNotifierInterface::class),
            $this->createStorageWithContainer($container),
            new ThreadContext(),
        );

        $logger
            ->expects(self::once())
            ->method('error')
            ->with(
                self::stringContains('Attachment (UUID: {uuid}) not removed'),
                self::identicalTo(['uuid' => 'c', 'exception' => $error]),
            );
        $lifecycle->writeContainer('a');
    }

    public function testWriteContainer_ContainerNotExcluded_WriterWritesContainer(): void
    {
        $resultsWriter = $this->createMock(ResultsWriterInterface::class);

        $container = new ContainerResult('a');
        $lifecycle = new AllureLifecycle(
            $this->createStub(LoggerInterface::class),
            $this->createClock(),
            $resultsWriter,
            $this->createStub(HooksNotifierInterface::class),
            $this->createStorageWithContainer($container),
            new ThreadContext(),
        );

        $resultsWriter
            ->expects(self::once())
            ->method('writeContainer')
            ->with(self::identicalTo($container));
        $lifecycle->writeContainer('a');
    }

    public function testWriteContainer_ContainerExcluded_WriterNeverWritesContainer(): void
    {
        $resultsWriter = $this->createMock(ResultsWriterInterface::class);

        $container = new ContainerResult('a');
        $lifecycle = new AllureLifecycle(
            $this->createStub(LoggerInterface::class),
            $this->createClock(),
            $resultsWriter,
            $this->createStub(HooksNotifierInterface::class),
            $this->createStorageWithContainer($container),
            new ThreadContext(),
        );

        $container->setExcluded(true);
        $resultsWriter
            ->expects(self::never())
            ->method('writeContainer');
        $lifecycle->writeContainer('a');
    }

    public function testStartBeforeFixture_ExceptionNotThrownDuringStart_NeverLogsError(): void
    {
        $logger = $this->createMock(LoggerInterface::class);
        $container = new ContainerResult('a');
        $lifecycle = new AllureLifecycle(
            $logger,
            $this->createClock(),
            $this->createStub(ResultsWriterInterface::class),
            $this->createStub(HooksNotifierInterface::class),
            $this->createStorageWithContainer($container),
            new ThreadContext(),
        );
        $logger
            ->expects(self::never())
            ->method('error');
        $lifecycle->startBeforeFixture(new FixtureResult('b'), 'a');
    }

    public function testStartBeforeFixture_ContainerNeitherGivenNorStarted_LogsError(): void
    {
        $logger = $this->createMock(LoggerInterface::class);
        $lifecycle = new AllureLifecycle(
            $logger,
            $this->createClock(),
            $this->createStub(ResultsWriterInterface::class),
            $this->createStub(HooksNotifierInterface::class),
            $this->createStub(ResultStorageInterface::class),
            new ThreadContext(),
        );
        $logger
            ->expects(self::once())
            ->method('error')
            ->with(
                self::stringContains(
                    'Set up fixture (UUID: {uuid}, container UUID: {containerUuid}) not started',
                ),
                self::equalTo(
                    ['uuid' => 'b', 'containerUuid' => null, 'exception' => new ActiveContainerNotFoundException()],
                ),
            );
        $lifecycle->startBeforeFixture(new FixtureResult('b'));
    }

    public function testStartBeforeFixture_ExceptionThrownAfterContainerIsProvided_LogsError(): void
    {
        $logger = $this->createMock(LoggerInterface::class);
        $error = new Exception();
        $lifecycle = new AllureLifecycle(
            $logger,
            $this->createFailingClock($error),
            $this->createStub(ResultsWriterInterface::class),
            $this->createStub(HooksNotifierInterface::class),
            $this->createStorageWithContainer(new ContainerResult('a')),
            new ThreadContext(),
        );
        $logger
            ->expects(self::once())
            ->method('error')
            ->with(
                self::stringContains(
                    'Set up fixture (UUID: {uuid}, container UUID: {containerUuid}) not started',
                ),
                self::equalTo(
                    ['uuid' => 'b', 'containerUuid' => 'a', 'exception' => $error],
                ),
            );
        $lifecycle->startBeforeFixture(new FixtureResult('b'), 'a');
    }

    public function testStartBeforeFixture_ContainerNotGivenButStarted_NeverLogsError(): void
    {
        $logger = $this->createMock(LoggerInterface::class);
        $container = new ContainerResult('a');
        $lifecycle = new AllureLifecycle(
            $logger,
            $this->createClock(),
            $this->createStub(ResultsWriterInterface::class),
            $this->createStub(HooksNotifierInterface::class),
            $this->createStorageWithContainer($container),
            new ThreadContext(),
        );
        $lifecycle->startContainer($container);
        $logger
            ->expects(self::never())
            ->method('error');
        $lifecycle->startBeforeFixture(new FixtureResult('b'));
    }

    public function testStartBeforeFixture_ContainerGiven_NeverLogsError(): void
    {
        $logger = $this->createMock(LoggerInterface::class);
        $lifecycle = new AllureLifecycle(
            $logger,
            $this->createClock(),
            $this->createStub(ResultsWriterInterface::class),
            $this->createStub(HooksNotifierInterface::class),
            $this->createStorageWithContainer(new ContainerResult('a')),
            new ThreadContext(),
        );

        $logger
            ->expects(self::never())
            ->method('error');
        $lifecycle->startBeforeFixture(new FixtureResult('b'), 'a');
    }

    public function testStartBeforeFixture_GivenFixture_ContainerContainsSameFixture(): void
    {
        $container = new ContainerResult('a');
        $lifecycle = new AllureLifecycle(
            $this->createStub(LoggerInterface::class),
            $this->createClock(),
            $this->createStub(ResultsWriterInterface::class),
            $this->createStub(HooksNotifierInterface::class),
            $this->createStorageWithContainer($container),
            new ThreadContext(),
        );

        $fixture = new FixtureResult('b');
        $lifecycle->startBeforeFixture($fixture, 'a');
        self::assertSame([$fixture], $container->getBefores());
    }

    public function testStartBeforeFixture_ClockProvidesGivenTime_FixtureStartIsSameTime(): void
    {
        $time = new DateTimeImmutable('@0');
        $lifecycle = new AllureLifecycle(
            $this->createStub(LoggerInterface::class),
            $this->createClock($time),
            $this->createStub(ResultsWriterInterface::class),
            $this->createStub(HooksNotifierInterface::class),
            $this->createStorageWithContainer(new ContainerResult('a')),
            new ThreadContext(),
        );

        $fixture = new FixtureResult('b');
        $lifecycle->startBeforeFixture($fixture, 'a');
        self::assertSame($time, $fixture->getStart());
    }

    public function testStartBeforeFixture_GivenFixture_FixtureOnRunningStage(): void
    {
        $lifecycle = new AllureLifecycle(
            $this->createStub(LoggerInterface::class),
            $this->createClock(),
            $this->createStub(ResultsWriterInterface::class),
            $this->createStub(HooksNotifierInterface::class),
            $this->createStorageWithContainer(new ContainerResult('a')),
            new ThreadContext(),
        );

        $fixture = new FixtureResult('b');
        $lifecycle->startBeforeFixture($fixture, 'a');
        self::assertSame(Stage::running(), $fixture->getStage());
    }

    public function testStartBeforeFixture_ThreadContextWithNonEmptyStack_OnlyFixtureInContextStack(): void
    {
        $threadContext = new ThreadContext();
        $lifecycle = new AllureLifecycle(
            $this->createStub(LoggerInterface::class),
            $this->createClock(),
            $this->createStub(ResultsWriterInterface::class),
            $this->createStub(HooksNotifierInterface::class),
            $this->createStorageWithContainer(new ContainerResult('a')),
            $threadContext,
        );

        $fixture = new FixtureResult('b');
        $threadContext->push('c');
        $threadContext->push('d');
        $lifecycle->startBeforeFixture($fixture, 'a');
        self::assertSame(['b'], $this->extractThreadStack($threadContext));
    }

    public function testStartAfterFixture_ExceptionNotThrownDuringStart_NeverLogsError(): void
    {
        $logger = $this->createMock(LoggerInterface::class);
        $container = new ContainerResult('a');
        $lifecycle = new AllureLifecycle(
            $logger,
            $this->createClock(),
            $this->createStub(ResultsWriterInterface::class),
            $this->createStub(HooksNotifierInterface::class),
            $this->createStorageWithContainer($container),
            new ThreadContext(),
        );
        $logger
            ->expects(self::never())
            ->method('error');
        $lifecycle->startAfterFixture(new FixtureResult('b'), 'a');
    }

    public function testStartAfterFixture_ContainerNeitherGivenNorStarted_LogsError(): void
    {
        $logger = $this->createMock(LoggerInterface::class);
        $lifecycle = new AllureLifecycle(
            $logger,
            $this->createClock(),
            $this->createStub(ResultsWriterInterface::class),
            $this->createStub(HooksNotifierInterface::class),
            $this->createStub(ResultStorageInterface::class),
            new ThreadContext(),
        );
        $logger
            ->expects(self::once())
            ->method('error')
            ->with(
                self::stringContains(
                    'Tear down fixture (UUID: {uuid}, container UUID: {containerUuid}) not started',
                ),
                self::equalTo(
                    ['uuid' => 'a', 'containerUuid' => null, 'exception' => new ActiveContainerNotFoundException()],
                ),
            );
        $lifecycle->startAfterFixture(new FixtureResult('a'));
    }

    public function testStartAfterFixture_ExceptionThrownAfterContainerIsProvided_LogsError(): void
    {
        $logger = $this->createMock(LoggerInterface::class);
        $error = new Exception();
        $lifecycle = new AllureLifecycle(
            $logger,
            $this->createFailingClock($error),
            $this->createStub(ResultsWriterInterface::class),
            $this->createStub(HooksNotifierInterface::class),
            $this->createStorageWithContainer(new ContainerResult('a')),
            new ThreadContext(),
        );
        $logger
            ->expects(self::once())
            ->method('error')
            ->with(
                self::stringContains(
                    'Tear down fixture (UUID: {uuid}, container UUID: {containerUuid}) not started',
                ),
                self::equalTo(
                    ['uuid' => 'b', 'containerUuid' => 'a', 'exception' => $error],
                ),
            );
        $lifecycle->startAfterFixture(new FixtureResult('b'), 'a');
    }

    public function testStartAfterFixture_ContainerNotGivenButStarted_NeverLogsError(): void
    {
        $logger = $this->createMock(LoggerInterface::class);
        $container = new ContainerResult('a');
        $lifecycle = new AllureLifecycle(
            $logger,
            $this->createClock(),
            $this->createStub(ResultsWriterInterface::class),
            $this->createStub(HooksNotifierInterface::class),
            $this->createStorageWithContainer($container),
            new ThreadContext(),
        );
        $lifecycle->startContainer($container);
        $logger
            ->expects(self::never())
            ->method('error');
        $lifecycle->startAfterFixture(new FixtureResult('b'));
    }

    public function testStartAfterFixture_ContainerGiven_NeverLogsError(): void
    {
        $logger = $this->createMock(LoggerInterface::class);
        $lifecycle = new AllureLifecycle(
            $logger,
            $this->createClock(),
            $this->createStub(ResultsWriterInterface::class),
            $this->createStub(HooksNotifierInterface::class),
            $this->createStorageWithContainer(new ContainerResult('a')),
            new ThreadContext(),
        );

        $logger
            ->expects(self::never())
            ->method('error');
        $lifecycle->startAfterFixture(new FixtureResult('b'), 'a');
    }

    public function testStartAfterFixture_GivenFixture_ContainerContainsSameFixture(): void
    {
        $container = new ContainerResult('a');
        $lifecycle = new AllureLifecycle(
            $this->createStub(LoggerInterface::class),
            $this->createClock(),
            $this->createStub(ResultsWriterInterface::class),
            $this->createStub(HooksNotifierInterface::class),
            $this->createStorageWithContainer($container),
            new ThreadContext(),
        );

        $fixture = new FixtureResult('b');
        $lifecycle->startAfterFixture($fixture, 'a');
        self::assertSame([$fixture], $container->getAfters());
    }

    public function testStartAfterFixture_ClockProvidesGivenTime_FixtureStartIsSameTime(): void
    {
        $time = new DateTimeImmutable('@0');
        $lifecycle = new AllureLifecycle(
            $this->createStub(LoggerInterface::class),
            $this->createClock($time),
            $this->createStub(ResultsWriterInterface::class),
            $this->createStub(HooksNotifierInterface::class),
            $this->createStorageWithContainer(new ContainerResult('a')),
            new ThreadContext(),
        );

        $fixture = new FixtureResult('b');
        $lifecycle->startAfterFixture($fixture, 'a');
        self::assertSame($time, $fixture->getStart());
    }

    public function testStartAfterFixture_GivenFixture_FixtureOnRunningStage(): void
    {
        $lifecycle = new AllureLifecycle(
            $this->createStub(LoggerInterface::class),
            $this->createClock(),
            $this->createStub(ResultsWriterInterface::class),
            $this->createStub(HooksNotifierInterface::class),
            $this->createStorageWithContainer(new ContainerResult('a')),
            new ThreadContext(),
        );

        $fixture = new FixtureResult('b');
        $lifecycle->startAfterFixture($fixture, 'a');
        self::assertSame(Stage::running(), $fixture->getStage());
    }

    public function testStartAfterFixture_ThreadContextWithNonEmptyStack_OnlyFixtureInContextStack(): void
    {
        $threadContext = new ThreadContext();
        $lifecycle = new AllureLifecycle(
            $this->createStub(LoggerInterface::class),
            $this->createClock(),
            $this->createStub(ResultsWriterInterface::class),
            $this->createStub(HooksNotifierInterface::class),
            $this->createStorageWithContainer(new ContainerResult('a')),
            $threadContext,
        );

        $fixture = new FixtureResult('b');
        $threadContext->push('c');
        $threadContext->push('d');
        $lifecycle->startAfterFixture($fixture, 'a');
        self::assertSame(['b'], $this->extractThreadStack($threadContext));
    }

    public function testUpdateFixture_FixtureNeitherGivenNorStarted_LogsErrorAndReturnsNull(): void
    {
        $logger = $this->createMock(LoggerInterface::class);
        $lifecycle = new AllureLifecycle(
            $logger,
            $this->createClock(),
            $this->createStub(ResultsWriterInterface::class),
            $this->createStub(HooksNotifierInterface::class),
            $this->createStub(ResultStorageInterface::class),
            new ThreadContext(),
        );

        $logger
            ->expects(self::once())
            ->method('error')
            ->with(
                self::stringContains('Fixture (UUID: {uuid}) not updated'),
                self::equalTo(['uuid' => null, 'exception' => new ActiveTestNotFoundException()]),
            );
        self::assertNull($lifecycle->updateFixture(fn () => null));
    }

    public function testUpdateFixture_StorageFailsToProvideGivenFixture_LogsErrorAndReturnsNull(): void
    {
        $logger = $this->createMock(LoggerInterface::class);
        $error = new Exception();
        $lifecycle = new AllureLifecycle(
            $logger,
            $this->createClock(),
            $this->createStub(ResultsWriterInterface::class),
            $this->createStub(HooksNotifierInterface::class),
            $this->createStorageWithoutFixture('a', $error),
            new ThreadContext(),
        );

        $logger
            ->expects(self::once())
            ->method('error')
            ->with(
                self::stringContains('Fixture (UUID: {uuid}) not updated'),
                self::identicalTo(['uuid' => 'a', 'exception' => $error]),
            );
        self::assertNull($lifecycle->updateFixture(fn () => null, 'a'));
    }

    public function testUpdateFixture_StorageFailsToProvideStartedFixture_LogsErrorAndReturnsNull(): void
    {
        $logger = $this->createMock(LoggerInterface::class);
        $error = new Exception();
        $threadContext = new ThreadContext();
        $lifecycle = new AllureLifecycle(
            $logger,
            $this->createClock(),
            $this->createStub(ResultsWriterInterface::class),
            $this->createStub(HooksNotifierInterface::class),
            $this->createStorageWithoutFixture('a', $error),
            $threadContext,
        );

        $threadContext->push('a');
        $logger
            ->expects(self::once())
            ->method('error')
            ->with(
                self::stringContains('Fixture (UUID: {uuid}) not updated'),
                self::identicalTo(['uuid' => 'a', 'exception' => $error]),
            );
        self::assertNull($lifecycle->updateFixture(fn () => null));
    }

    public function testUpdateFixture_StorageProvidesFixtureWithGivenUuid_NeverLogsErrorAndReturnsSameUuid(): void
    {
        $logger = $this->createMock(LoggerInterface::class);
        $lifecycle = new AllureLifecycle(
            $logger,
            $this->createClock(),
            $this->createStub(ResultsWriterInterface::class),
            $this->createStub(HooksNotifierInterface::class),
            $this->createStorageWithFixture(new FixtureResult('a')),
            new ThreadContext(),
        );
        $logger
            ->expects(self::never())
            ->method('error');
        self::assertSame('a', $lifecycle->updateFixture(fn () => null, 'a'));
    }

    public function testUpdateFixture_StorageProvidesStartedFixture_NeverLogsErrorAndReturnsMatchingUuid(): void
    {
        $logger = $this->createMock(LoggerInterface::class);
        $threadContext = new ThreadContext();
        $lifecycle = new AllureLifecycle(
            $logger,
            $this->createClock(),
            $this->createStub(ResultsWriterInterface::class),
            $this->createStub(HooksNotifierInterface::class),
            $this->createStorageWithFixture(new FixtureResult('a')),
            $threadContext,
        );
        $threadContext->push('a');
        $logger
            ->expects(self::never())
            ->method('error');
        self::assertSame('a', $lifecycle->updateFixture(fn () => null));
    }

    public function testUpdateFixture_NoExceptionThrownDuringUpdate_NotifiesHooksWithoutError(): void
    {
        $hooksNotifier = $this->createMock(HooksNotifierInterface::class);
        $fixture = new FixtureResult('a');
        $lifecycle = new AllureLifecycle(
            $this->createStub(LoggerInterface::class),
            $this->createClock(),
            $this->createStub(ResultsWriterInterface::class),
            $hooksNotifier,
            $this->createStorageWithFixture($fixture),
            new ThreadContext(),
        );

        $hooksNotifier
            ->expects(self::once())
            ->id('before')
            ->method('beforeFixtureUpdate')
            ->with(self::identicalTo($fixture));
        $hooksNotifier
            ->expects(self::never())
            ->method('onLifecycleError');
        $hooksNotifier
            ->expects(self::once())
            ->after('before')
            ->method('afterFixtureUpdate')
            ->with(self::identicalTo($fixture));
        $lifecycle->updateFixture(fn () => null, 'a');
    }

    public function testUpdateFixture_ExceptionThrownDuringUpdate_NotifiesHooksWithError(): void
    {
        $hooksNotifier = $this->createMock(HooksNotifierInterface::class);
        $fixture = new FixtureResult('a');
        $lifecycle = new AllureLifecycle(
            $this->createStub(LoggerInterface::class),
            $this->createClock(),
            $this->createStub(ResultsWriterInterface::class),
            $hooksNotifier,
            $this->createStorageWithFixture($fixture),
            new ThreadContext(),
        );

        $error = new Exception();
        $hooksNotifier
            ->expects(self::once())
            ->id('before')
            ->method('beforeFixtureUpdate')
            ->with(self::identicalTo($fixture));
        $hooksNotifier
            ->expects(self::once())
            ->id('error')
            ->after('before')
            ->method('onLifecycleError')
            ->with(self::identicalTo($error));
        $hooksNotifier
            ->expects(self::once())
            ->after('error')
            ->method('afterFixtureUpdate')
            ->with(self::identicalTo($fixture));
        $lifecycle->updateFixture(fn () => throw $error, 'a');
    }

    public function testUpdateFixture_StorageProvidesFixture_SameFixturePassedToCallback(): void
    {
        $fixture = new FixtureResult('a');
        $lifecycle = new AllureLifecycle(
            $this->createStub(LoggerInterface::class),
            $this->createClock(),
            $this->createStub(ResultsWriterInterface::class),
            $this->createStub(HooksNotifierInterface::class),
            $this->createStorageWithFixture($fixture),
            new ThreadContext(),
        );

        $lifecycle->updateFixture(fn (FixtureResult $f) => $f->setName('b'), 'a');
        self::assertSame('b', $fixture->getName());
    }

    public function testStopFixture_FixtureNeitherGivenNorStarted_LogsErrorAndReturnsNull(): void
    {
        $logger = $this->createMock(LoggerInterface::class);
        $lifecycle = new AllureLifecycle(
            $logger,
            $this->createClock(),
            $this->createStub(ResultsWriterInterface::class),
            $this->createStub(HooksNotifierInterface::class),
            $this->createStub(ResultStorageInterface::class),
            new ThreadContext(),
        );

        $logger
            ->expects(self::once())
            ->method('error')
            ->with(
                self::stringContains('Fixture (UUID: {uuid}) not stopped'),
                self::equalTo(['uuid' => null, 'exception' => new ActiveTestNotFoundException()]),
            );
        self::assertNull($lifecycle->stopFixture());
    }

    public function testStopFixture_StorageFailsToProvideGivenFixture_LogsErrorAndReturnsNull(): void
    {
        $logger = $this->createMock(LoggerInterface::class);
        $error = new Exception();
        $lifecycle = new AllureLifecycle(
            $logger,
            $this->createClock(),
            $this->createStub(ResultsWriterInterface::class),
            $this->createStub(HooksNotifierInterface::class),
            $this->createStorageWithoutFixture('a', $error),
            new ThreadContext(),
        );

        $logger
            ->expects(self::once())
            ->method('error')
            ->with(
                self::stringContains('Fixture (UUID: {uuid}) not stopped'),
                self::identicalTo(['uuid' => 'a', 'exception' => $error]),
            );
        self::assertNull($lifecycle->stopFixture('a'));
    }

    public function testStopFixture_StorageFailsToProvideStartedFixture_LogsErrorAndReturnsNull(): void
    {
        $logger = $this->createMock(LoggerInterface::class);
        $error = new Exception();
        $threadContext = new ThreadContext();
        $lifecycle = new AllureLifecycle(
            $logger,
            $this->createClock(),
            $this->createStub(ResultsWriterInterface::class),
            $this->createStub(HooksNotifierInterface::class),
            $this->createStorageWithoutFixture('a', $error),
            $threadContext,
        );

        $threadContext->push('a');
        $logger
            ->expects(self::once())
            ->method('error')
            ->with(
                self::stringContains('Fixture (UUID: {uuid}) not stopped'),
                self::identicalTo(['uuid' => 'a', 'exception' => $error]),
            );
        self::assertNull($lifecycle->stopFixture());
    }

    public function testStopFixture_StorageProvidesFixtureWithGivenUuid_NeverLogsErrorAndReturnsSameUuid(): void
    {
        $logger = $this->createMock(LoggerInterface::class);
        $lifecycle = new AllureLifecycle(
            $logger,
            $this->createClock(),
            $this->createStub(ResultsWriterInterface::class),
            $this->createStub(HooksNotifierInterface::class),
            $this->createStorageWithFixture(new FixtureResult('a')),
            new ThreadContext(),
        );
        $logger
            ->expects(self::never())
            ->method('error');
        self::assertSame('a', $lifecycle->stopFixture('a'));
    }

    public function testStopFixture_StorageProvidesStartedFixture_NeverLogsErrorAndReturnsMatchingUuid(): void
    {
        $logger = $this->createMock(LoggerInterface::class);
        $threadContext = new ThreadContext();
        $lifecycle = new AllureLifecycle(
            $logger,
            $this->createClock(),
            $this->createStub(ResultsWriterInterface::class),
            $this->createStub(HooksNotifierInterface::class),
            $this->createStorageWithFixture(new FixtureResult('a')),
            $threadContext,
        );
        $threadContext->push('a');
        $logger
            ->expects(self::never())
            ->method('error');
        self::assertSame('a', $lifecycle->stopFixture());
    }

    public function testStopFixture_NoExceptionThrownDuringStop_NotifiesHooksWithoutError(): void
    {
        $hooksNotifier = $this->createMock(HooksNotifierInterface::class);
        $fixture = new FixtureResult('a');
        $lifecycle = new AllureLifecycle(
            $this->createStub(LoggerInterface::class),
            $this->createClock(),
            $this->createStub(ResultsWriterInterface::class),
            $hooksNotifier,
            $this->createStorageWithFixture($fixture),
            new ThreadContext(),
        );

        $hooksNotifier
            ->expects(self::once())
            ->id('before')
            ->method('beforeFixtureStop')
            ->with(self::identicalTo($fixture));
        $hooksNotifier
            ->expects(self::never())
            ->method('onLifecycleError');
        $hooksNotifier
            ->expects(self::once())
            ->after('before')
            ->method('afterFixtureStop')
            ->with(self::identicalTo($fixture));
        $lifecycle->stopFixture('a');
    }

    public function testStopFixture_ExceptionThrownDuringStop_NotifiesHooksWithError(): void
    {
        $hooksNotifier = $this->createMock(HooksNotifierInterface::class);
        $error = new Exception();
        $fixture = new FixtureResult('a');
        $lifecycle = new AllureLifecycle(
            $this->createStub(LoggerInterface::class),
            $this->createFailingClock($error),
            $this->createStub(ResultsWriterInterface::class),
            $hooksNotifier,
            $this->createStorageWithFixture($fixture),
            new ThreadContext(),
        );

        $hooksNotifier
            ->expects(self::once())
            ->id('before')
            ->method('beforeFixtureStop')
            ->with(self::identicalTo($fixture));
        $hooksNotifier
            ->expects(self::once())
            ->id('error')
            ->after('before')
            ->method('onLifecycleError')
            ->with(self::identicalTo($error));
        $hooksNotifier
            ->expects(self::once())
            ->after('error')
            ->method('afterFixtureStop')
            ->with(self::identicalTo($fixture));
        $lifecycle->stopFixture('a');
    }

    public function testStopFixture_ClockProvidesTime_FixtureStopIsSameTime(): void
    {
        $time = new DateTimeImmutable('@0');
        $fixture = new FixtureResult('a');
        $lifecycle = new AllureLifecycle(
            $this->createStub(LoggerInterface::class),
            $this->createClock($time),
            $this->createStub(ResultsWriterInterface::class),
            $this->createStub(HooksNotifierInterface::class),
            $this->createStorageWithFixture($fixture),
            new ThreadContext(),
        );

        $lifecycle->stopFixture('a');
        self::assertSame($time, $fixture->getStop());
    }

    public function testStopFixture_StorageProvidesFixture_FixtureOnFinishedStage(): void
    {
        $time = new DateTimeImmutable('@0');
        $fixture = new FixtureResult('a');
        $lifecycle = new AllureLifecycle(
            $this->createStub(LoggerInterface::class),
            $this->createClock($time),
            $this->createStub(ResultsWriterInterface::class),
            $this->createStub(HooksNotifierInterface::class),
            $this->createStorageWithFixture($fixture),
            new ThreadContext(),
        );

        $lifecycle->stopFixture('a');
        self::assertSame(Stage::finished(), $fixture->getStage());
    }

    public function testStopFixture_ThreadContextWithNonEmptyStack_ContextStackIsEmpty(): void
    {
        $threadContext = new ThreadContext();
        $fixture = new FixtureResult('a');
        $lifecycle = new AllureLifecycle(
            $this->createStub(LoggerInterface::class),
            $this->createClock(),
            $this->createStub(ResultsWriterInterface::class),
            $this->createStub(HooksNotifierInterface::class),
            $this->createStorageWithFixture($fixture),
            $threadContext,
        );

        $threadContext->push('b');
        $threadContext->push('c');
        $lifecycle->stopFixture('a');
        self::assertSame([], $this->extractThreadStack($threadContext));
    }

    public function testStopFixture_StorageProvidesFixtureWithUuid_StorageUnsetsSameUuid(): void
    {
        $fixture = new FixtureResult('a');
        $resultStorage = $this->createMock(ResultStorageInterface::class);
        $resultStorage
            ->method('getFixture')
            ->with(self::identicalTo('a'))
            ->willReturn($fixture);
        $lifecycle = new AllureLifecycle(
            $this->createStub(LoggerInterface::class),
            $this->createClock(),
            $this->createStub(ResultsWriterInterface::class),
            $this->createStub(HooksNotifierInterface::class),
            $resultStorage,
            new ThreadContext(),
        );

        $resultStorage
            ->expects(self::once())
            ->method('unset')
            ->with(self::identicalTo('a'));
        $lifecycle->stopFixture('a');
    }

    public function testScheduleTest_NoExceptionThrownDuringSchedule_NotifiesHooksWithoutError(): void
    {
        $hooksNotifier = $this->createMock(HooksNotifierInterface::class);
        $lifecycle = new AllureLifecycle(
            $this->createStub(LoggerInterface::class),
            $this->createClock(),
            $this->createStub(ResultsWriterInterface::class),
            $hooksNotifier,
            $this->createStub(ResultStorageInterface::class),
            new ThreadContext(),
        );

        $test = new TestResult('a');
        $hooksNotifier
            ->expects(self::once())
            ->id('before')
            ->method('beforeTestSchedule')
            ->with(self::identicalTo($test));
        $hooksNotifier
            ->expects(self::never())
            ->method('onLifecycleError');
        $hooksNotifier
            ->expects(self::once())
            ->after('before')
            ->method('afterTestSchedule')
            ->with(self::identicalTo($test));
        $lifecycle->scheduleTest($test);
    }

    public function testScheduleTest_ExceptionThrownDuringSchedule_NotifiesHooksWithError(): void
    {
        $hooksNotifier = $this->createMock(HooksNotifierInterface::class);
        $test = new TestResult('a');
        $error = new Exception();
        $lifecycle = new AllureLifecycle(
            $this->createStub(LoggerInterface::class),
            $this->createClock(),
            $this->createStub(ResultsWriterInterface::class),
            $hooksNotifier,
            $this->createNonSettableStorage($test, $error),
            new ThreadContext(),
        );

        $hooksNotifier
            ->expects(self::once())
            ->id('before')
            ->method('beforeTestSchedule')
            ->with(self::identicalTo($test));
        $hooksNotifier
            ->expects(self::once())
            ->id('error')
            ->after('before')
            ->method('onLifecycleError')
            ->with(self::identicalTo($error));
        $hooksNotifier
            ->expects(self::once())
            ->after('error')
            ->method('afterTestSchedule')
            ->with(self::identicalTo($test));
        $lifecycle->scheduleTest($test);
    }

    public function testScheduleTest_ExceptionThrownDuringScheduleWithoutContainer_LogsError(): void
    {
        $logger = $this->createMock(LoggerInterface::class);
        $test = new TestResult('a');
        $error = new Exception();
        $lifecycle = new AllureLifecycle(
            $logger,
            $this->createClock(),
            $this->createStub(ResultsWriterInterface::class),
            $this->createStub(HooksNotifierInterface::class),
            $this->createNonSettableStorage($test, $error),
            new ThreadContext(),
        );

        $logger
            ->expects(self::once())
            ->method('error')
            ->with(
                self::stringContains('Test (UUID: {uuid}) not scheduled (container UUID: {containerUuid})'),
                self::identicalTo(['uuid' => 'a', 'containerUuid' => null, 'exception' => $error]),
            );
        $lifecycle->scheduleTest($test);
    }

    public function testScheduleTest_ExceptionThrownDuringScheduleWithContainer_LogsError(): void
    {
        $logger = $this->createMock(LoggerInterface::class);
        $test = new TestResult('a');
        $error = new Exception();
        $lifecycle = new AllureLifecycle(
            $logger,
            $this->createClock(),
            $this->createStub(ResultsWriterInterface::class),
            $this->createStub(HooksNotifierInterface::class),
            $this->createStorageWithContainer(new ContainerResult('b'), setError: $error),
            new ThreadContext(),
        );

        $logger
            ->expects(self::once())
            ->method('error')
            ->with(
                self::stringContains('Test (UUID: {uuid}) not scheduled (container UUID: {containerUuid})'),
                self::identicalTo(['uuid' => 'a', 'containerUuid' => 'b', 'exception' => $error]),
            );
        $lifecycle->scheduleTest($test, 'b');
    }

    public function testScheduleTest_GivenContainerUuid_TestAddedToContainerChildren(): void
    {
        $container = new ContainerResult('a');
        $lifecycle = new AllureLifecycle(
            $this->createStub(LoggerInterface::class),
            $this->createClock(),
            $this->createStub(ResultsWriterInterface::class),
            $this->createStub(HooksNotifierInterface::class),
            $this->createStorageWithContainer($container),
            new ThreadContext(),
        );
        $test = new TestResult('b');
        $lifecycle->scheduleTest($test, 'a');
        self::assertSame([$test], $container->getChildren());
    }

    public function testScheduleTest_CurrentContainerUuid_TestAddedToContainerChildren(): void
    {
        $container = new ContainerResult('a');
        $threadContext = new ThreadContext();
        $lifecycle = new AllureLifecycle(
            $this->createStub(LoggerInterface::class),
            $this->createClock(),
            $this->createStub(ResultsWriterInterface::class),
            $this->createStub(HooksNotifierInterface::class),
            $this->createStorageWithContainer($container),
            $threadContext,
        );
        $threadContext->setContainer('a');
        $test = new TestResult('b');
        $lifecycle->scheduleTest($test);
        self::assertSame([$test], $container->getChildren());
    }

    public function testScheduleTest_GivenTest_SetsSameTestInStorage(): void
    {
        $resultStorage = $this->createMock(ResultStorageInterface::class);
        $lifecycle = new AllureLifecycle(
            $this->createStub(LoggerInterface::class),
            $this->createClock(),
            $this->createStub(ResultsWriterInterface::class),
            $this->createStub(HooksNotifierInterface::class),
            $resultStorage,
            new ThreadContext(),
        );

        $test = new TestResult('a');
        $resultStorage
            ->expects(self::once())
            ->method('set')
            ->with(self::identicalTo($test));
        $lifecycle->scheduleTest($test);
    }

    public function testScheduleTest_GivenTest_TestIsOnScheduledStage(): void
    {
        $lifecycle = new AllureLifecycle(
            $this->createStub(LoggerInterface::class),
            $this->createClock(),
            $this->createStub(ResultsWriterInterface::class),
            $this->createStub(HooksNotifierInterface::class),
            $this->createStub(ResultStorageInterface::class),
            new ThreadContext(),
        );
        $test = new TestResult('a');
        $lifecycle->scheduleTest($test);
        self::assertSame(Stage::scheduled(), $test->getStage());
    }

    public function testStartTest_StorageFailsToProvideTest_LogsError(): void
    {
        $logger = $this->createMock(LoggerInterface::class);
        $error = new Exception();
        $lifecycle = new AllureLifecycle(
            $logger,
            $this->createClock(),
            $this->createStub(ResultsWriterInterface::class),
            $this->createStub(HooksNotifierInterface::class),
            $this->createStorageWithoutTest('a', $error),
            new ThreadContext(),
        );

        $logger
            ->expects(self::once())
            ->method('error')
            ->with(
                self::stringContains('Test (UUID: {uuid}) not started'),
                self::identicalTo(['uuid' => 'a', 'exception' => $error]),
            );
        $lifecycle->startTest('a');
    }

    public function testStartTest_NoExceptionThrownDuringStart_NotifiesHooksWithoutError(): void
    {
        $hooksNotifier = $this->createMock(HooksNotifierInterface::class);
        $test = new TestResult('a');
        $lifecycle = new AllureLifecycle(
            $this->createStub(LoggerInterface::class),
            $this->createClock(),
            $this->createStub(ResultsWriterInterface::class),
            $hooksNotifier,
            $this->createStorageWithTest($test),
            new ThreadContext(),
        );

        $hooksNotifier
            ->expects(self::once())
            ->id('before')
            ->method('beforeTestStart')
            ->with(self::identicalTo($test));
        $hooksNotifier
            ->expects(self::never())
            ->method('onLifecycleError');
        $hooksNotifier
            ->expects(self::once())
            ->after('before')
            ->method('afterTestStart')
            ->with(self::identicalTo($test));
        $lifecycle->startTest('a');
    }

    public function testStartTest_ExceptionThrownDuringStart_NotifiesHooksWithError(): void
    {
        $hooksNotifier = $this->createMock(HooksNotifierInterface::class);
        $error = new Exception();
        $test = new TestResult('a');
        $lifecycle = new AllureLifecycle(
            $this->createStub(LoggerInterface::class),
            $this->createFailingClock($error),
            $this->createStub(ResultsWriterInterface::class),
            $hooksNotifier,
            $this->createStorageWithTest($test),
            new ThreadContext(),
        );

        $hooksNotifier
            ->expects(self::once())
            ->id('before')
            ->method('beforeTestStart')
            ->with(self::identicalTo($test));
        $hooksNotifier
            ->expects(self::once())
            ->id('error')
            ->after('before')
            ->method('onLifecycleError')
            ->with(self::identicalTo($error));
        $hooksNotifier
            ->expects(self::once())
            ->after('error')
            ->method('afterTestStart')
            ->with(self::identicalTo($test));
        $lifecycle->startTest('a');
    }

    public function testStartTest_ExceptionThrownDuringStart_LogsError(): void
    {
        $logger = $this->createMock(LoggerInterface::class);
        $error = new Exception();
        $lifecycle = new AllureLifecycle(
            $logger,
            $this->createFailingClock($error),
            $this->createStub(ResultsWriterInterface::class),
            $this->createStub(HooksNotifierInterface::class),
            $this->createStorageWithTest(new TestResult('a')),
            new ThreadContext(),
        );

        $logger
            ->expects(self::once())
            ->method('error')
            ->with(
                self::stringContains('Test (UUID: {uuid}) not started'),
                self::identicalTo(['uuid' => 'a', 'exception' => $error]),
            );
        $lifecycle->startTest('a');
    }

    public function testStartTest_ClockProvidesTime_TestStartIsSameTime(): void
    {
        $time = new DateTimeImmutable('@0');
        $test = new TestResult('a');
        $lifecycle = new AllureLifecycle(
            $this->createStub(LoggerInterface::class),
            $this->createClock($time),
            $this->createStub(ResultsWriterInterface::class),
            $this->createStub(HooksNotifierInterface::class),
            $this->createStorageWithTest($test),
            new ThreadContext(),
        );

        $lifecycle->startTest('a');
        self::assertSame($time, $test->getStart());
    }

    public function testStartTest_StorageProvidesTest_TestIsOnRunningStage(): void
    {
        $test = new TestResult('a');
        $lifecycle = new AllureLifecycle(
            $this->createStub(LoggerInterface::class),
            $this->createClock(),
            $this->createStub(ResultsWriterInterface::class),
            $this->createStub(HooksNotifierInterface::class),
            $this->createStorageWithTest($test),
            new ThreadContext(),
        );

        $lifecycle->startTest('a');
        self::assertSame(Stage::running(), $test->getStage());
    }

    public function testStartTest_ThreadContextWithNonEmptyStack_OnlyTestUuidInStack(): void
    {
        $test = new TestResult('a');
        $threadContext = new ThreadContext();
        $lifecycle = new AllureLifecycle(
            $this->createStub(LoggerInterface::class),
            $this->createClock(),
            $this->createStub(ResultsWriterInterface::class),
            $this->createStub(HooksNotifierInterface::class),
            $this->createStorageWithTest($test),
            $threadContext,
        );

        $threadContext->push('b');
        $threadContext->push('c');
        $lifecycle->startTest('a');
        self::assertSame(['a'], $this->extractThreadStack($threadContext));
    }

    public function testUpdateTest_TestNeitherGivenNorStarted_LogsErrorAndReturnsNull(): void
    {
        $logger = $this->createMock(LoggerInterface::class);
        $lifecycle = new AllureLifecycle(
            $logger,
            $this->createClock(),
            $this->createStub(ResultsWriterInterface::class),
            $this->createStub(HooksNotifierInterface::class),
            $this->createStub(ResultStorageInterface::class),
            new ThreadContext(),
        );

        $logger
            ->expects(self::once())
            ->method('error')
            ->with(
                self::stringContains('Test (UUID: {uuid}) not updated'),
                self::equalTo(['uuid' => null, 'exception' => new ActiveTestNotFoundException()])
            );
        self::assertNull($lifecycle->updateTest(fn () => null));
    }

    public function testUpdateTest_StorageFailsToProvideTest_LogsErrorAndReturnsNull(): void
    {
        $logger = $this->createMock(LoggerInterface::class);
        $error = new Exception();
        $lifecycle = new AllureLifecycle(
            $logger,
            $this->createClock(),
            $this->createStub(ResultsWriterInterface::class),
            $this->createStub(HooksNotifierInterface::class),
            $this->createStorageWithoutTest('a', $error),
            new ThreadContext(),
        );

        $logger
            ->expects(self::once())
            ->method('error')
            ->with(
                self::stringContains('Test (UUID: {uuid}) not updated'),
                self::identicalTo(['uuid' => 'a', 'exception' => $error])
            );
        self::assertNull($lifecycle->updateTest(fn () => null, 'a'));
    }

    public function testUpdateTest_StorageProvidesGivenTest_LogsNoErrorAndReturnsTestUuid(): void
    {
        $logger = $this->createMock(LoggerInterface::class);
        $lifecycle = new AllureLifecycle(
            $logger,
            $this->createClock(),
            $this->createStub(ResultsWriterInterface::class),
            $this->createStub(HooksNotifierInterface::class),
            $this->createStorageWithTest(new TestResult('a')),
            new ThreadContext(),
        );

        $logger
            ->expects(self::never())
            ->method('error');
        self::assertSame('a', $lifecycle->updateTest(fn () => null, 'a'));
    }

    public function testUpdateTest_StorageProvidesStartedTest_LogsNoErrorAndReturnsTestUuid(): void
    {
        $logger = $this->createMock(LoggerInterface::class);
        $threadContext = new ThreadContext();
        $lifecycle = new AllureLifecycle(
            $logger,
            $this->createClock(),
            $this->createStub(ResultsWriterInterface::class),
            $this->createStub(HooksNotifierInterface::class),
            $this->createStorageWithTest(new TestResult('a')),
            $threadContext,
        );

        $threadContext->push('a');
        $logger
            ->expects(self::never())
            ->method('error');
        self::assertSame('a', $lifecycle->updateTest(fn () => null));
    }

    public function testUpdateTest_NoExceptionThrownDuringUpdate_NotifiesHooksWithoutError(): void
    {
        $hooksNotifier = $this->createMock(HooksNotifierInterface::class);
        $test = new TestResult('a');
        $lifecycle = new AllureLifecycle(
            $this->createStub(LoggerInterface::class),
            $this->createClock(),
            $this->createStub(ResultsWriterInterface::class),
            $hooksNotifier,
            $this->createStorageWithTest($test),
            new ThreadContext(),
        );

        $hooksNotifier
            ->expects(self::once())
            ->id('before')
            ->method('beforeTestUpdate')
            ->with(self::identicalTo($test));
        $hooksNotifier
            ->expects(self::never())
            ->method('onLifecycleError');
        $hooksNotifier
            ->expects(self::once())
            ->after('before')
            ->method('afterTestUpdate')
            ->with(self::identicalTo($test));
        $lifecycle->updateTest(fn () => null, 'a');
    }

    public function testUpdateTest_ExceptionThrownDuringUpdate_NotifiesHooksWithError(): void
    {
        $hooksNotifier = $this->createMock(HooksNotifierInterface::class);
        $test = new TestResult('a');
        $lifecycle = new AllureLifecycle(
            $this->createStub(LoggerInterface::class),
            $this->createClock(),
            $this->createStub(ResultsWriterInterface::class),
            $hooksNotifier,
            $this->createStorageWithTest($test),
            new ThreadContext(),
        );

        $error = new Exception();
        $hooksNotifier
            ->expects(self::once())
            ->id('before')
            ->method('beforeTestUpdate')
            ->with(self::identicalTo($test));
        $hooksNotifier
            ->expects(self::once())
            ->id('error')
            ->after('before')
            ->method('onLifecycleError')
            ->with(self::identicalTo($error));
        $hooksNotifier
            ->expects(self::once())
            ->after('error')
            ->method('afterTestUpdate')
            ->with(self::identicalTo($test));
        $lifecycle->updateTest(fn () => throw $error, 'a');
    }

    public function testUpdateTest_ExceptionThrownDuringUpdate_LogsErrorAndReturnsTestUuid(): void
    {
        $logger = $this->createMock(LoggerInterface::class);
        $test = new TestResult('a');
        $lifecycle = new AllureLifecycle(
            $logger,
            $this->createClock(),
            $this->createStub(ResultsWriterInterface::class),
            $this->createStub(HooksNotifierInterface::class),
            $this->createStorageWithTest($test),
            new ThreadContext(),
        );

        $error = new Exception();
        $logger
            ->expects(self::once())
            ->method('error')
            ->with(
                self::stringContains('Test (UUID: {uuid}) not updated'),
                self::identicalTo(['uuid' => 'a', 'exception' => $error]),
            );
        self::assertSame('a', $lifecycle->updateTest(fn () => throw $error, 'a'));
    }

    public function testUpdateTest_StorageProvidesTest_SameTestPassedToCallback(): void
    {
        $test = new TestResult('a');
        $lifecycle = new AllureLifecycle(
            $this->createStub(LoggerInterface::class),
            $this->createClock(),
            $this->createStub(ResultsWriterInterface::class),
            $this->createStub(HooksNotifierInterface::class),
            $this->createStorageWithTest($test),
            new ThreadContext(),
        );

        $lifecycle->updateTest(fn (TestResult $t) => $t->setName('b'), 'a');
        self::assertSame('b', $test->getName());
    }

    public function testStopTest_TestNeitherGivenNorStarted_LogsErrorAndReturnsNull(): void
    {
        $logger = $this->createMock(LoggerInterface::class);
        $lifecycle = new AllureLifecycle(
            $logger,
            $this->createClock(),
            $this->createStub(ResultsWriterInterface::class),
            $this->createStub(HooksNotifierInterface::class),
            $this->createStub(ResultStorageInterface::class),
            new ThreadContext(),
        );

        $logger
            ->expects(self::once())
            ->method('error')
            ->with(
                self::stringContains('Test (UUID: {uuid}) not stopped'),
                self::equalTo(['uuid' => null, 'exception' => new ActiveTestNotFoundException()]),
            );
        self::assertNull($lifecycle->stopTest());
    }

    public function testStopTest_StorageFailsToProvideGivenTest_LogsErrorAndReturnsNull(): void
    {
        $logger = $this->createMock(LoggerInterface::class);
        $error = new Exception();
        $lifecycle = new AllureLifecycle(
            $logger,
            $this->createClock(),
            $this->createStub(ResultsWriterInterface::class),
            $this->createStub(HooksNotifierInterface::class),
            $this->createStorageWithoutTest('a', $error),
            new ThreadContext(),
        );

        $logger
            ->expects(self::once())
            ->method('error')
            ->with(
                self::stringContains('Test (UUID: {uuid}) not stopped'),
                self::identicalTo(['uuid' => 'a', 'exception' => $error]),
            );
        self::assertNull($lifecycle->stopTest('a'));
    }

    public function testStopTest_StorageFailsToProvideStartedTest_LogsErrorAndReturnsNull(): void
    {
        $logger = $this->createMock(LoggerInterface::class);
        $error = new Exception();
        $threadContext = new ThreadContext();
        $lifecycle = new AllureLifecycle(
            $logger,
            $this->createClock(),
            $this->createStub(ResultsWriterInterface::class),
            $this->createStub(HooksNotifierInterface::class),
            $this->createStorageWithoutTest('a', $error),
            $threadContext,
        );

        $threadContext->push('a');
        $logger
            ->expects(self::once())
            ->method('error')
            ->with(
                self::stringContains('Test (UUID: {uuid}) not stopped'),
                self::identicalTo(['uuid' => 'a', 'exception' => $error]),
            );
        self::assertNull($lifecycle->stopTest());
    }

    public function testStopTest_StorageProvidesStartedTest_NeverLogsErrorAndReturnsTestUuid(): void
    {
        $logger = $this->createMock(LoggerInterface::class);
        $lifecycle = new AllureLifecycle(
            $logger,
            $this->createClock(),
            $this->createStub(ResultsWriterInterface::class),
            $this->createStub(HooksNotifierInterface::class),
            $this->createStorageWithTest(new TestResult('a')),
            new ThreadContext(),
        );

        $logger
            ->expects(self::never())
            ->method('error');
        self::assertSame('a', $lifecycle->stopTest('a'));
    }

    public function testStopTest_NoExceptionThrownDuringStop_NotifiesHooksWithoutError(): void
    {
        $hooksNotifier = $this->createMock(HooksNotifierInterface::class);
        $test = new TestResult('a');
        $lifecycle = new AllureLifecycle(
            $this->createStub(LoggerInterface::class),
            $this->createClock(),
            $this->createStub(ResultsWriterInterface::class),
            $hooksNotifier,
            $this->createStorageWithTest($test),
            new ThreadContext(),
        );

        $hooksNotifier
            ->expects(self::once())
            ->id('before')
            ->method('beforeTestStop')
            ->with(self::identicalTo($test));
        $hooksNotifier
            ->expects(self::never())
            ->method('onLifecycleError');
        $hooksNotifier
            ->expects(self::once())
            ->after('before')
            ->method('afterTestStop')
            ->with(self::identicalTo($test));
        $lifecycle->stopTest('a');
    }

    public function testStopTest_ExceptionThrownDuringStop_NotifiesHooksWithError(): void
    {
        $hooksNotifier = $this->createMock(HooksNotifierInterface::class);
        $error = new Exception();
        $test = new TestResult('a');
        $lifecycle = new AllureLifecycle(
            $this->createStub(LoggerInterface::class),
            $this->createFailingClock($error),
            $this->createStub(ResultsWriterInterface::class),
            $hooksNotifier,
            $this->createStorageWithTest($test),
            new ThreadContext(),
        );

        $hooksNotifier
            ->expects(self::once())
            ->id('before')
            ->method('beforeTestStop')
            ->with(self::identicalTo($test));
        $hooksNotifier
            ->expects(self::once())
            ->id('error')
            ->after('before')
            ->method('onLifecycleError')
            ->with(self::identicalTo($error));
        $hooksNotifier
            ->expects(self::once())
            ->after('error')
            ->method('afterTestStop')
            ->with(self::identicalTo($test));
        $lifecycle->stopTest('a');
    }

    public function testStopTest_ExceptionThrownDuringStop_LogsErrorAndReturnsTestUuid(): void
    {
        $logger = $this->createMock(LoggerInterface::class);
        $error = new Exception();
        $test = new TestResult('a');
        $lifecycle = new AllureLifecycle(
            $logger,
            $this->createFailingClock($error),
            $this->createStub(ResultsWriterInterface::class),
            $this->createStub(HooksNotifierInterface::class),
            $this->createStorageWithTest($test),
            new ThreadContext(),
        );

        $logger
            ->expects(self::once())
            ->method('error')
            ->with(
                self::stringContains('Test (UUID: {uuid}) not stopped'),
                self::identicalTo(['uuid' => 'a', 'exception' => $error]),
            );
        self::assertSame('a', $lifecycle->stopTest('a'));
    }

    public function testStopTest_ClockProvidesTime_TestStopIsSameTime(): void
    {
        $time = new DateTimeImmutable('@0');
        $test = new TestResult('a');
        $lifecycle = new AllureLifecycle(
            $this->createStub(LoggerInterface::class),
            $this->createClock($time),
            $this->createStub(ResultsWriterInterface::class),
            $this->createStub(HooksNotifierInterface::class),
            $this->createStorageWithTest($test),
            new ThreadContext(),
        );

        $lifecycle->stopTest('a');
        self::assertSame($time, $test->getStop());
    }

    public function testStopTest_StorageProvidesTest_TestOnFinishedStage(): void
    {
        $test = new TestResult('a');
        $lifecycle = new AllureLifecycle(
            $this->createStub(LoggerInterface::class),
            $this->createClock(),
            $this->createStub(ResultsWriterInterface::class),
            $this->createStub(HooksNotifierInterface::class),
            $this->createStorageWithTest($test),
            new ThreadContext(),
        );

        $lifecycle->stopTest('a');
        self::assertSame(Stage::finished(), $test->getStage());
    }

    public function testStopTest_ThreadContextWithNonEmptyStack_ContextHasEmptyStack(): void
    {
        $test = new TestResult('a');
        $threadContext = new ThreadContext();
        $lifecycle = new AllureLifecycle(
            $this->createStub(LoggerInterface::class),
            $this->createClock(),
            $this->createStub(ResultsWriterInterface::class),
            $this->createStub(HooksNotifierInterface::class),
            $this->createStorageWithTest($test),
            $threadContext,
        );

        $threadContext->push('b');
        $threadContext->push('c');
        $lifecycle->stopTest('a');
        self::assertSame([], $this->extractThreadStack($threadContext));
    }

    public function testWriteTest_StorageFailsToProvideTest_LogsError(): void
    {
        $logger = $this->createMock(LoggerInterface::class);
        $error = new Exception();
        $lifecycle = new AllureLifecycle(
            $logger,
            $this->createClock(),
            $this->createStub(ResultsWriterInterface::class),
            $this->createStub(HooksNotifierInterface::class),
            $this->createStorageWithoutTest('a', $error),
            new ThreadContext(),
        );

        $logger
            ->expects(self::once())
            ->method('error')
            ->with(
                self::stringContains('Test (UUID: {uuid}) not written'),
                self::identicalTo(['uuid' => 'a', 'exception' => $error]),
            );
        $lifecycle->writeTest('a');
    }

    public function testWriteTest_NoExceptionThrownDuringWrite_NotifiesHooksWithoutError(): void
    {
        $hooksNotifier = $this->createMock(HooksNotifierInterface::class);
        $test = new TestResult('a');
        $lifecycle = new AllureLifecycle(
            $this->createStub(LoggerInterface::class),
            $this->createClock(),
            $this->createStub(ResultsWriterInterface::class),
            $hooksNotifier,
            $this->createStorageWithTest($test),
            new ThreadContext(),
        );

        $hooksNotifier
            ->expects(self::once())
            ->id('before')
            ->method('beforeTestWrite')
            ->with(self::identicalTo($test));
        $hooksNotifier
            ->expects(self::never())
            ->method('onLifecycleError');
        $hooksNotifier
            ->expects(self::once())
            ->after('before')
            ->method('afterTestWrite')
            ->with(self::identicalTo($test));
        $lifecycle->writeTest('a');
    }

    public function testWriteTest_ExceptionThrownDuringWrite_NotifiesHooksWithError(): void
    {
        $hooksNotifier = $this->createMock(HooksNotifierInterface::class);
        $test = new TestResult('a');
        $error = new Exception();
        $lifecycle = new AllureLifecycle(
            $this->createStub(LoggerInterface::class),
            $this->createClock(),
            $this->createStub(ResultsWriterInterface::class),
            $hooksNotifier,
            $this->createStorageWithTest($test, unsetError: $error),
            new ThreadContext(),
        );

        $hooksNotifier
            ->expects(self::once())
            ->id('before')
            ->method('beforeTestWrite')
            ->with(self::identicalTo($test));
        $hooksNotifier
            ->expects(self::once())
            ->id('error')
            ->after('before')
            ->method('onLifecycleError')
            ->with(self::identicalTo($error));
        $hooksNotifier
            ->expects(self::once())
            ->after('error')
            ->method('afterTestWrite')
            ->with(self::identicalTo($test));
        $lifecycle->writeTest('a');
    }

    public function testWriteTest_ExceptionThrownDuringWrite_LogsError(): void
    {
        $logger = $this->createMock(LoggerInterface::class);
        $test = new TestResult('a');
        $error = new Exception();
        $lifecycle = new AllureLifecycle(
            $logger,
            $this->createClock(),
            $this->createStub(ResultsWriterInterface::class),
            $this->createStub(HooksNotifierInterface::class),
            $this->createStorageWithTest($test, unsetError: $error),
            new ThreadContext(),
        );

        $logger
            ->expects(self::once())
            ->method('error')
            ->with(
                self::stringContains('Test (UUID: {uuid}) not written'),
                self::identicalTo(['uuid' => 'a', 'exception' => $error]),
            );
        $lifecycle->writeTest('a');
    }

    public function testWriteTest_StorageProvidesTest_StorageUnsetsSameTest(): void
    {
        $test = new TestResult('a');
        $resultStorage = $this->createMock(ResultStorageInterface::class);
        $resultStorage
            ->method('getTest')
            ->with(self::identicalTo('a'))
            ->willReturn($test);
        $lifecycle = new AllureLifecycle(
            $this->createStub(LoggerInterface::class),
            $this->createClock(),
            $this->createStub(ResultsWriterInterface::class),
            $this->createStub(HooksNotifierInterface::class),
            $resultStorage,
            new ThreadContext(),
        );

        $resultStorage
            ->expects(self::once())
            ->method('unset')
            ->with(self::identicalTo('a'));
        $lifecycle->writeTest('a');
    }

    public function testWriteTest_StorageProvidesNonExcludedTest_WriterWritesSameTest(): void
    {
        $test = new TestResult('a');
        $resultsWriter = $this->createMock(ResultsWriterInterface::class);
        $lifecycle = new AllureLifecycle(
            $this->createStub(LoggerInterface::class),
            $this->createClock(),
            $resultsWriter,
            $this->createStub(HooksNotifierInterface::class),
            $this->createStorageWithTest($test),
            new ThreadContext(),
        );

        $resultsWriter
            ->expects(self::once())
            ->method('writeTest')
            ->with(self::identicalTo($test));
        $lifecycle->writeTest('a');
    }

    public function testWriteTest_StorageProvidesExcludedTest_WriterNeverWritesTest(): void
    {
        $resultWriter = $this->createMock(ResultsWriterInterface::class);
        $test = new TestResult('a');
        $lifecycle = new AllureLifecycle(
            $this->createStub(LoggerInterface::class),
            $this->createClock(),
            $resultWriter,
            $this->createStub(HooksNotifierInterface::class),
            $this->createStorageWithTest($test),
            new ThreadContext(),
        );

        $test->setExcluded(true);
        $resultWriter
            ->expects(self::never())
            ->method('writeTest');
        $lifecycle->writeTest('a');
    }

    public function testWriteTest_ExcludedTestWithNestedResults_RemovesNestedResults(): void
    {
        $resultsWriter = $this->createMock(ResultsWriterInterface::class);

        $test = new TestResult('a');
        $testAttachment = new AttachmentResult('b');
        $test->addAttachments($testAttachment);
        $testStep = new StepResult('c');
        $testStepAttachment = new AttachmentResult('d');
        $testStep->addAttachments($testStepAttachment);
        $test->addSteps($testStep);

        $lifecycle = new AllureLifecycle(
            $this->createStub(LoggerInterface::class),
            $this->createClock(),
            $resultsWriter,
            $this->createStub(HooksNotifierInterface::class),
            $this->createStorageWithTest($test),
            new ThreadContext(),
        );

        $test->setExcluded(true);
        $removeAttachmentResults = [];
        $resultsWriter
            ->expects(self::exactly(2))
            ->method('removeAttachment')
            ->with(
                self::callback(
                    function (AttachmentResult $attachmentResult) use (&$removeAttachmentResults): bool {
                        /** @psalm-var list<AttachmentResult> $removeAttachmentResults */
                        $removeAttachmentResults[] = $attachmentResult;

                        return true;
                    },
                ),
            );
        $lifecycle->writeTest('a');
        self::assertSame(
            [$testAttachment, $testStepAttachment],
            $removeAttachmentResults,
        );
    }

    public function testWriteTest_WriterFailsToRemoveAttachment_LogsError(): void
    {
        $logger = $this->createMock(LoggerInterface::class);
        $error = new Exception();
        $resultsWriter = $this->createStub(ResultsWriterInterface::class);
        $resultsWriter
            ->method('removeAttachment')
            ->willThrowException($error);

        $test = new TestResult('a');
        $testAttachment = new AttachmentResult('b');
        $test->addAttachments($testAttachment->setExcluded(true));

        $lifecycle = new AllureLifecycle(
            $logger,
            $this->createClock(),
            $resultsWriter,
            $this->createStub(HooksNotifierInterface::class),
            $this->createStorageWithTest($test),
            new ThreadContext(),
        );

        $logger
            ->expects(self::once())
            ->method('error')
            ->with(
                self::stringContains('Attachment (UUID: {uuid}) not removed'),
                self::identicalTo(['uuid' => 'b', 'exception' => $error]),
            );
        $lifecycle->writeTest('a');
    }

    public function testStartStep_ParentNeitherGivenNorStarted_LogsError(): void
    {
        $logger = $this->createMock(LoggerInterface::class);
        $lifecycle = new AllureLifecycle(
            $logger,
            $this->createClock(),
            $this->createStub(ResultsWriterInterface::class),
            $this->createStub(HooksNotifierInterface::class),
            $this->createStub(ResultStorageInterface::class),
            new ThreadContext(),
        );

        $logger
            ->expects(self::once())
            ->method('error')
            ->with(
                self::stringContains('Step (UUID: {uuid}) not started (parent UUID: {parentUuid})'),
                self::equalTo(
                    [
                        'uuid' => 'a',
                        'parentUuid' => null,
                        'exception' => new ActiveExecutionContextNotFoundException(),
                    ]
                ),
            );
        $lifecycle->startStep(new StepResult('a'));
    }

    public function testStartStep_StorageFailsToProvideGivenExecutionContext_LogsError(): void
    {
        $logger = $this->createMock(LoggerInterface::class);
        $error = new Exception();
        $lifecycle = new AllureLifecycle(
            $logger,
            $this->createClock(),
            $this->createStub(ResultsWriterInterface::class),
            $this->createStub(HooksNotifierInterface::class),
            $this->createStorageWithoutExecutionContext('a', $error),
            new ThreadContext(),
        );

        $logger
            ->expects(self::once())
            ->method('error')
            ->with(
                self::stringContains('Step (UUID: {uuid}) not started (parent UUID: {parentUuid})'),
                self::equalTo(['uuid' => 'b', 'parentUuid' => 'a', 'exception' => $error]),
            );
        $lifecycle->startStep(new StepResult('b'), 'a');
    }

    public function testStartStep_StorageFailsToProvideStartedTest_LogsError(): void
    {
        $logger = $this->createMock(LoggerInterface::class);
        $error = new Exception();
        $threadContext = new ThreadContext();
        $lifecycle = new AllureLifecycle(
            $logger,
            $this->createClock(),
            $this->createStub(ResultsWriterInterface::class),
            $this->createStub(HooksNotifierInterface::class),
            $this->createStorageWithoutExecutionContext('a', $error),
            $threadContext,
        );

        $threadContext->push('a');
        $logger
            ->expects(self::once())
            ->method('error')
            ->with(
                self::stringContains('Step (UUID: {uuid}) not started (parent UUID: {parentUuid})'),
                self::equalTo(['uuid' => 'b', 'parentUuid' => 'a', 'exception' => $error]),
            );
        $lifecycle->startStep(new StepResult('b'));
    }

    public function testStartStep_StorageFailsToProvideStartedStep_LogsError(): void
    {
        $logger = $this->createMock(LoggerInterface::class);
        $error = new Exception();
        $threadContext = new ThreadContext();
        $lifecycle = new AllureLifecycle(
            $logger,
            $this->createClock(),
            $this->createStub(ResultsWriterInterface::class),
            $this->createStub(HooksNotifierInterface::class),
            $this->createStorageWithoutExecutionContext('b', $error),
            $threadContext,
        );

        $threadContext->push('a');
        $threadContext->push('b');
        $logger
            ->expects(self::once())
            ->method('error')
            ->with(
                self::stringContains('Step (UUID: {uuid}) not started (parent UUID: {parentUuid})'),
                self::equalTo(['uuid' => 'c', 'parentUuid' => 'b', 'exception' => $error]),
            );
        $lifecycle->startStep(new StepResult('c'));
    }

    public function testStartStep_StorageProvidesGivenExecutionContext_NeverLogsError(): void
    {
        $logger = $this->createMock(LoggerInterface::class);
        $lifecycle = new AllureLifecycle(
            $logger,
            $this->createClock(),
            $this->createStub(ResultsWriterInterface::class),
            $this->createStub(HooksNotifierInterface::class),
            $this->createStorageWithTest(new TestResult('a')),
            new ThreadContext(),
        );

        $logger
            ->expects(self::never())
            ->method('error');
        $lifecycle->startStep(new StepResult('b'), 'a');
    }

    public function testStartStep_StorageProvidesStartedTest_NeverLogsError(): void
    {
        $logger = $this->createMock(LoggerInterface::class);
        $threadContext = new ThreadContext();
        $lifecycle = new AllureLifecycle(
            $logger,
            $this->createClock(),
            $this->createStub(ResultsWriterInterface::class),
            $this->createStub(HooksNotifierInterface::class),
            $this->createStorageWithTest(new TestResult('a')),
            $threadContext,
        );

        $threadContext->push('a');
        $logger
            ->expects(self::never())
            ->method('error');
        $lifecycle->startStep(new StepResult('b'));
    }

    public function testStartStep_StorageProvidesStartedStep_NeverLogsError(): void
    {
        $logger = $this->createMock(LoggerInterface::class);
        $threadContext = new ThreadContext();
        $lifecycle = new AllureLifecycle(
            $logger,
            $this->createClock(),
            $this->createStub(ResultsWriterInterface::class),
            $this->createStub(HooksNotifierInterface::class),
            $this->createStorageWithStep(new StepResult('b')),
            $threadContext,
        );

        $threadContext->push('a');
        $threadContext->push('b');
        $logger
            ->expects(self::never())
            ->method('error');
        $lifecycle->startStep(new StepResult('c'));
    }

    public function testStartStep_NoExceptionThrownDuringStart_NotifiesHooksWithoutError(): void
    {
        $hooksNotifier = $this->createMock(HooksNotifierInterface::class);
        $lifecycle = new AllureLifecycle(
            $this->createStub(LoggerInterface::class),
            $this->createClock(),
            $this->createStub(ResultsWriterInterface::class),
            $hooksNotifier,
            $this->createStorageWithTest(new TestResult('a')),
            new ThreadContext(),
        );

        $step = new StepResult('b');
        $hooksNotifier
            ->expects(self::once())
            ->id('before')
            ->method('beforeStepStart')
            ->with(self::identicalTo($step));
        $hooksNotifier
            ->expects(self::never())
            ->method('onLifecycleError');
        $hooksNotifier
            ->expects(self::once())
            ->after('before')
            ->method('afterStepStart')
            ->with(self::identicalTo($step));
        $lifecycle->startStep($step, 'a');
    }

    public function testStartStep_ExceptionThrownDuringStart_NotifiesHooksWithError(): void
    {
        $hooksNotifier = $this->createMock(HooksNotifierInterface::class);
        $error = new Exception();
        $lifecycle = new AllureLifecycle(
            $this->createStub(LoggerInterface::class),
            $this->createFailingClock($error),
            $this->createStub(ResultsWriterInterface::class),
            $hooksNotifier,
            $this->createStorageWithTest(new TestResult('a')),
            new ThreadContext(),
        );

        $step = new StepResult('b');
        $hooksNotifier
            ->expects(self::once())
            ->id('before')
            ->method('beforeStepStart')
            ->with(self::identicalTo($step));
        $hooksNotifier
            ->expects(self::once())
            ->id('error')
            ->after('before')
            ->method('onLifecycleError')
            ->with(self::identicalTo($error));
        $hooksNotifier
            ->expects(self::once())
            ->after('error')
            ->method('afterStepStart')
            ->with(self::identicalTo($step));
        $lifecycle->startStep($step, 'a');
    }

    public function testStartStep_StorageProvidesParent_StepIsAmongParentChildren(): void
    {
        $test = new TestResult('a');
        $lifecycle = new AllureLifecycle(
            $this->createStub(LoggerInterface::class),
            $this->createClock(),
            $this->createStub(ResultsWriterInterface::class),
            $this->createStub(HooksNotifierInterface::class),
            $this->createStorageWithTest($test),
            new ThreadContext(),
        );

        $step = new StepResult('b');
        $lifecycle->startStep($step, 'a');
        self::assertSame([$step], $test->getSteps());
    }

    public function testStartStep_StepWithGivenUuid_StorageSetsSameUuid(): void
    {
        $resultStorage = $this->createMock(ResultStorageInterface::class);
        $resultStorage
            ->method('getExecutionContext')
            ->willReturn(new TestResult('a'));
        $lifecycle = new AllureLifecycle(
            $this->createStub(LoggerInterface::class),
            $this->createClock(),
            $this->createStub(ResultsWriterInterface::class),
            $this->createStub(HooksNotifierInterface::class),
            $resultStorage,
            new ThreadContext(),
        );

        $step = new StepResult('b');
        $resultStorage
            ->expects(self::once())
            ->method('set')
            ->with(self::identicalTo($step));
        $lifecycle->startStep($step, 'a');
    }

    public function testStartStep_ClockProvidesTime_StepStartIsSameTime(): void
    {
        $time = new DateTimeImmutable('@0');
        $lifecycle = new AllureLifecycle(
            $this->createStub(LoggerInterface::class),
            $this->createClock($time),
            $this->createStub(ResultsWriterInterface::class),
            $this->createStub(HooksNotifierInterface::class),
            $this->createStorageWithTest(new TestResult('a')),
            new ThreadContext(),
        );

        $step = new StepResult('b');
        $lifecycle->startStep($step, 'a');
        self::assertSame($time, $step->getStart());
    }

    public function testStartStep_GivenStep_StepOnRunningStage(): void
    {
        $lifecycle = new AllureLifecycle(
            $this->createStub(LoggerInterface::class),
            $this->createClock(),
            $this->createStub(ResultsWriterInterface::class),
            $this->createStub(HooksNotifierInterface::class),
            $this->createStorageWithTest(new TestResult('a')),
            new ThreadContext(),
        );

        $step = new StepResult('b');
        $lifecycle->startStep($step, 'a');
        self::assertSame(Stage::running(), $step->getStage());
    }

    public function testStartStep_ThreadContextWithNonEmptyStack_StepUuidAddedToStack(): void
    {
        $threadContext = new ThreadContext();
        $lifecycle = new AllureLifecycle(
            $this->createStub(LoggerInterface::class),
            $this->createClock(),
            $this->createStub(ResultsWriterInterface::class),
            $this->createStub(HooksNotifierInterface::class),
            $this->createStorageWithTest(new TestResult('a')),
            $threadContext,
        );

        $threadContext->push('b');
        $threadContext->push('c');
        $step = new StepResult('d');
        $lifecycle->startStep($step, 'a');
        self::assertSame(['d', 'c', 'b'], $this->extractThreadStack($threadContext));
    }

    public function testUpdateStep_StepNeitherGivenNorStarted_LogsErrorAndReturnsNull(): void
    {
        $logger = $this->createMock(LoggerInterface::class);
        $lifecycle = new AllureLifecycle(
            $logger,
            $this->createClock(),
            $this->createStub(ResultsWriterInterface::class),
            $this->createStub(HooksNotifierInterface::class),
            $this->createStub(ResultStorageInterface::class),
            new ThreadContext(),
        );

        $logger
            ->expects(self::once())
            ->method('error')
            ->with(
                self::stringContains('Step (UUID: {uuid}) not updated'),
                self::equalTo(['uuid' => null, 'exception' => new ActiveStepNotFoundException()]),
            );
        self::assertNull($lifecycle->updateStep(fn () => null));
    }

    public function testUpdateStep_StorageFailsToProvideGivenStep_LogsErrorAndReturnsNull(): void
    {
        $logger = $this->createMock(LoggerInterface::class);
        $error = new Exception();
        $lifecycle = new AllureLifecycle(
            $logger,
            $this->createClock(),
            $this->createStub(ResultsWriterInterface::class),
            $this->createStub(HooksNotifierInterface::class),
            $this->createStorageWithoutStep('a', $error),
            new ThreadContext(),
        );

        $logger
            ->expects(self::once())
            ->method('error')
            ->with(
                self::stringContains('Step (UUID: {uuid}) not updated'),
                self::equalTo(['uuid' => 'a', 'exception' => $error]),
            );
        self::assertNull($lifecycle->updateStep(fn () => null, 'a'));
    }

    public function testUpdateStep_StorageFailsToProvideStartedStep_LogsErrorAndReturnsNull(): void
    {
        $logger = $this->createMock(LoggerInterface::class);
        $error = new Exception();
        $threadContext = new ThreadContext();
        $lifecycle = new AllureLifecycle(
            $logger,
            $this->createClock(),
            $this->createStub(ResultsWriterInterface::class),
            $this->createStub(HooksNotifierInterface::class),
            $this->createStorageWithoutStep('b', $error),
            $threadContext,
        );

        $threadContext->push('a');
        $threadContext->push('b');
        $logger
            ->expects(self::once())
            ->method('error')
            ->with(
                self::stringContains('Step (UUID: {uuid}) not updated'),
                self::equalTo(['uuid' => 'b', 'exception' => $error]),
            );
        self::assertNull($lifecycle->updateStep(fn () => null));
    }

    public function testUpdateStep_NoExceptionThrownDuringUpdate_NotifiesHooksWithoutError(): void
    {
        $hooksNotifier = $this->createMock(HooksNotifierInterface::class);
        $step = new StepResult('a');
        $lifecycle = new AllureLifecycle(
            $this->createStub(LoggerInterface::class),
            $this->createClock(),
            $this->createStub(ResultsWriterInterface::class),
            $hooksNotifier,
            $this->createStorageWithStep($step),
            new ThreadContext(),
        );

        $hooksNotifier
            ->expects(self::once())
            ->id('before')
            ->method('beforeStepUpdate')
            ->with(self::identicalTo($step));
        $hooksNotifier
            ->expects(self::never())
            ->method('onLifecycleError');
        $hooksNotifier
            ->expects(self::once())
            ->after('before')
            ->method('afterStepUpdate')
            ->with(self::identicalTo($step));
        $lifecycle->updateStep(fn () => null, 'a');
    }

    public function testUpdateStep_ExceptionThrownDuringUpdate_NotifiesHooksWithError(): void
    {
        $hooksNotifier = $this->createMock(HooksNotifierInterface::class);
        $step = new StepResult('a');
        $lifecycle = new AllureLifecycle(
            $this->createStub(LoggerInterface::class),
            $this->createClock(),
            $this->createStub(ResultsWriterInterface::class),
            $hooksNotifier,
            $this->createStorageWithStep($step),
            new ThreadContext(),
        );

        $error = new Exception();
        $hooksNotifier
            ->expects(self::once())
            ->id('before')
            ->method('beforeStepUpdate')
            ->with(self::identicalTo($step));
        $hooksNotifier
            ->expects(self::once())
            ->id('error')
            ->after('before')
            ->method('onLifecycleError')
            ->with(self::identicalTo($error));
        $hooksNotifier
            ->expects(self::once())
            ->after('error')
            ->method('afterStepUpdate')
            ->with(self::identicalTo($step));
        $lifecycle->updateStep(fn () => throw $error, 'a');
    }

    public function testUpdateStep_ExceptionNotThrownDuringUpdate_NeverLogsErrorAndReturnsUuid(): void
    {
        $logger = $this->createMock(LoggerInterface::class);
        $lifecycle = new AllureLifecycle(
            $logger,
            $this->createClock(),
            $this->createStub(ResultsWriterInterface::class),
            $this->createStub(HooksNotifierInterface::class),
            $this->createStorageWithStep(new StepResult('a')),
            new ThreadContext(),
        );

        $logger
            ->expects(self::never())
            ->method('error');
        self::assertSame('a', $lifecycle->updateStep(fn () => null, 'a'));
    }

    public function testUpdateStep_ExceptionThrownDuringUpdate_LogsErrorAndReturnsUuid(): void
    {
        $logger = $this->createMock(LoggerInterface::class);
        $lifecycle = new AllureLifecycle(
            $logger,
            $this->createClock(),
            $this->createStub(ResultsWriterInterface::class),
            $this->createStub(HooksNotifierInterface::class),
            $this->createStorageWithStep(new StepResult('a')),
            new ThreadContext(),
        );

        $error = new Exception();
        $logger
            ->expects(self::once())
            ->method('error')
            ->with(
                self::stringContains('Step (UUID: {uuid}) not updated'),
                self::equalTo(['uuid' => 'a', 'exception' => $error]),
            );
        self::assertSame('a', $lifecycle->updateStep(fn () => throw $error, 'a'));
    }

    public function testUpdateStep_StorageProvidesStep_SameStepPassedToCallback(): void
    {
        $step = new StepResult('a');
        $lifecycle = new AllureLifecycle(
            $this->createStub(LoggerInterface::class),
            $this->createClock(),
            $this->createStub(ResultsWriterInterface::class),
            $this->createStub(HooksNotifierInterface::class),
            $this->createStorageWithStep($step),
            new ThreadContext(),
        );
        $lifecycle->updateStep(fn (StepResult $s) => $s->setName('b'), 'a');
        self::assertSame('b', $step->getName());
    }

    public function testUpdateExecutionContext_ContextNeitherGivenNorStarted_LogsErrorAndReturnsNull(): void
    {
        $logger = $this->createMock(LoggerInterface::class);
        $lifecycle = new AllureLifecycle(
            $logger,
            $this->createClock(),
            $this->createStub(ResultsWriterInterface::class),
            $this->createStub(HooksNotifierInterface::class),
            $this->createStub(ResultStorageInterface::class),
            new ThreadContext(),
        );

        $logger
            ->expects(self::once())
            ->method('error')
            ->with(
                self::stringContains('Execution context (UUID: {uuid}) not updated'),
                self::equalTo(['uuid' => null, 'exception' => new ActiveExecutionContextNotFoundException()]),
            );
        $lifecycle->updateExecutionContext(fn () => null);
    }

    public function testUpdateExecutionContext_StorageFailsToProvideGivenContext_LogsErrorAndReturnsNull(): void
    {
        $logger = $this->createMock(LoggerInterface::class);
        $error = new Exception();
        $lifecycle = new AllureLifecycle(
            $logger,
            $this->createClock(),
            $this->createStub(ResultsWriterInterface::class),
            $this->createStub(HooksNotifierInterface::class),
            $this->createStorageWithoutExecutionContext('a', $error),
            new ThreadContext(),
        );

        $logger
            ->expects(self::once())
            ->method('error')
            ->with(
                self::stringContains('Execution context (UUID: {uuid}) not updated'),
                self::equalTo(['uuid' => 'a', 'exception' => $error]),
            );
        self::assertNull($lifecycle->updateExecutionContext(fn () => null, 'a'));
    }

    public function testUpdateExecutionContext_StorageFailsToProvideStartedContext_LogsErrorAndReturnsNull(): void
    {
        $logger = $this->createMock(LoggerInterface::class);
        $error = new Exception();
        $threadContext = new ThreadContext();
        $lifecycle = new AllureLifecycle(
            $logger,
            $this->createClock(),
            $this->createStub(ResultsWriterInterface::class),
            $this->createStub(HooksNotifierInterface::class),
            $this->createStorageWithoutExecutionContext('b', $error),
            $threadContext,
        );

        $threadContext->push('a');
        $threadContext->push('b');
        $logger
            ->expects(self::once())
            ->method('error')
            ->with(
                self::stringContains('Execution context (UUID: {uuid}) not updated'),
                self::equalTo(['uuid' => 'b', 'exception' => $error]),
            );
        self::assertNull($lifecycle->updateExecutionContext(fn () => null));
    }

    public function testUpdateExecutionContext_StorageProvidesInvalidContext_LogsErrorAndReturnsNull(): void
    {
        $logger = $this->createMock(LoggerInterface::class);
        $lifecycle = new AllureLifecycle(
            $logger,
            $this->createClock(),
            $this->createStub(ResultsWriterInterface::class),
            $this->createStub(HooksNotifierInterface::class),
            $this->createStorageWithInvalidExecutionContext('a'),
            new ThreadContext(),
        );

        $logger
            ->expects(self::once())
            ->method('error')
            ->with(
                self::stringContains('Execution context (UUID: {uuid}) not updated'),
                self::equalTo(['uuid' => 'a']),
            );
        self::assertNull($lifecycle->updateExecutionContext(fn () => null, 'a'));
    }

    public function testUpdateExecutionContext_StorageProvidesGivenTest_NeverLogsErrorAndReturnsUuid(): void
    {
        $logger = $this->createMock(LoggerInterface::class);
        $lifecycle = new AllureLifecycle(
            $logger,
            $this->createClock(),
            $this->createStub(ResultsWriterInterface::class),
            $this->createStub(HooksNotifierInterface::class),
            $this->createStorageWithTest(new TestResult('a')),
            new ThreadContext(),
        );

        $logger
            ->expects(self::never())
            ->method('error');
        self::assertSame('a', $lifecycle->updateExecutionContext(fn () => null, 'a'));
    }

    public function testUpdateExecutionContext_StorageProvidesGivenFixture_NeverLogsErrorAndReturnsUuid(): void
    {
        $logger = $this->createMock(LoggerInterface::class);
        $lifecycle = new AllureLifecycle(
            $logger,
            $this->createClock(),
            $this->createStub(ResultsWriterInterface::class),
            $this->createStub(HooksNotifierInterface::class),
            $this->createStorageWithFixture(new FixtureResult('a')),
            new ThreadContext(),
        );

        $logger
            ->expects(self::never())
            ->method('error');
        self::assertSame('a', $lifecycle->updateExecutionContext(fn () => null, 'a'));
    }

    public function testUpdateExecutionContext_StorageProvidesGivenStep_NeverLogsErrorAndReturnsUuid(): void
    {
        $logger = $this->createMock(LoggerInterface::class);
        $lifecycle = new AllureLifecycle(
            $logger,
            $this->createClock(),
            $this->createStub(ResultsWriterInterface::class),
            $this->createStub(HooksNotifierInterface::class),
            $this->createStorageWithStep(new StepResult('a')),
            new ThreadContext(),
        );

        $logger
            ->expects(self::never())
            ->method('error');
        self::assertSame('a', $lifecycle->updateExecutionContext(fn () => null, 'a'));
    }

    public function testUpdateExecutionContext_StorageProvidesStartedStep_NeverLogsErrorAndReturnsUuid(): void
    {
        $logger = $this->createMock(LoggerInterface::class);
        $threadContext = new ThreadContext();
        $lifecycle = new AllureLifecycle(
            $logger,
            $this->createClock(),
            $this->createStub(ResultsWriterInterface::class),
            $this->createStub(HooksNotifierInterface::class),
            $this->createStorageWithStep(new StepResult('b')),
            $threadContext,
        );

        $threadContext->push('a');
        $threadContext->push('b');
        $logger
            ->expects(self::never())
            ->method('error');
        self::assertSame('b', $lifecycle->updateExecutionContext(fn () => null));
    }

    public function testUpdateExecutionContext_ContextNeitherGivenNorStarted_NotifiesHooksWithError(): void
    {
        $hooksNotifier = $this->createMock(HooksNotifierInterface::class);
        $lifecycle = new AllureLifecycle(
            $this->createStub(LoggerInterface::class),
            $this->createClock(),
            $this->createStub(ResultsWriterInterface::class),
            $hooksNotifier,
            $this->createStub(ResultStorageInterface::class),
            new ThreadContext(),
        );

        $hooksNotifier
            ->expects(self::once())
            ->method('onLifecycleError')
            ->with(self::isInstanceOf(ActiveExecutionContextNotFoundException::class));
        $lifecycle->updateExecutionContext(fn () => null);
    }

    public function testUpdateExecutionContext_StorageFailsToProvideContext_NotifiesHooksWithError(): void
    {
        $hooksNotifier = $this->createMock(HooksNotifierInterface::class);
        $error = new Exception();
        $lifecycle = new AllureLifecycle(
            $this->createStub(LoggerInterface::class),
            $this->createClock(),
            $this->createStub(ResultsWriterInterface::class),
            $hooksNotifier,
            $this->createStorageWithoutExecutionContext('a', $error),
            new ThreadContext(),
        );

        $hooksNotifier
            ->expects(self::once())
            ->method('onLifecycleError')
            ->with(self::identicalTo($error));
        $lifecycle->updateExecutionContext(fn () => null, 'a');
    }

    public function testUpdateExecutionContext_StorageProvidesInvalidContext_NotifiesHooksWithError(): void
    {
        $hooksNotifier = $this->createMock(HooksNotifierInterface::class);
        $lifecycle = new AllureLifecycle(
            $this->createStub(LoggerInterface::class),
            $this->createClock(),
            $this->createStub(ResultsWriterInterface::class),
            $hooksNotifier,
            $this->createStorageWithInvalidExecutionContext('a'),
            new ThreadContext(),
        );

        $hooksNotifier
            ->expects(self::once())
            ->method('onLifecycleError')
            ->with(self::isInstanceOf(InvalidExecutionContextException::class));
        $lifecycle->updateExecutionContext(fn () => null, 'a');
    }

    public function testUpdateExecutionContext_StorageProvidesTest_UpdatesSameTest(): void
    {
        $test = new TestResult('a');
        $lifecycle = new AllureLifecycle(
            $this->createStub(LoggerInterface::class),
            $this->createClock(),
            $this->createStub(ResultsWriterInterface::class),
            $this->createStub(HooksNotifierInterface::class),
            $this->createStorageWithTest($test),
            new ThreadContext(),
        );

        $lifecycle->updateExecutionContext(fn (ExecutionContextInterface $c) => $c->setName('b'), 'a');
        self::assertSame('b', $test->getName());
    }

    public function testUpdateExecutionContext_StorageProvidesFixture_UpdatesSameFixture(): void
    {
        $fixture = new FixtureResult('a');
        $lifecycle = new AllureLifecycle(
            $this->createStub(LoggerInterface::class),
            $this->createClock(),
            $this->createStub(ResultsWriterInterface::class),
            $this->createStub(HooksNotifierInterface::class),
            $this->createStorageWithFixture($fixture),
            new ThreadContext(),
        );

        $lifecycle->updateExecutionContext(fn (ExecutionContextInterface $c) => $c->setName('b'), 'a');
        self::assertSame('b', $fixture->getName());
    }

    public function testUpdateExecutionContext_StorageProvidesStep_UpdatesSameStep(): void
    {
        $step = new StepResult('a');
        $lifecycle = new AllureLifecycle(
            $this->createStub(LoggerInterface::class),
            $this->createClock(),
            $this->createStub(ResultsWriterInterface::class),
            $this->createStub(HooksNotifierInterface::class),
            $this->createStorageWithStep($step),
            new ThreadContext(),
        );

        $lifecycle->updateExecutionContext(fn (ExecutionContextInterface $c) => $c->setName('b'), 'a');
        self::assertSame('b', $step->getName());
    }

    public function testUpdateExecutionContext_NoExceptionThrownDuringTestUpdate_NotifiesHooksWithoutError(): void
    {
        $hooksNotifier = $this->createMock(HooksNotifierInterface::class);
        $test = new TestResult('a');
        $lifecycle = new AllureLifecycle(
            $this->createStub(LoggerInterface::class),
            $this->createClock(),
            $this->createStub(ResultsWriterInterface::class),
            $hooksNotifier,
            $this->createStorageWithTest($test),
            new ThreadContext(),
        );

        $hooksNotifier
            ->expects(self::once())
            ->id('before')
            ->method('beforeTestUpdate')
            ->with(self::identicalTo($test));
        $hooksNotifier
            ->expects(self::never())
            ->method('onLifecycleError');
        $hooksNotifier
            ->expects(self::once())
            ->after('before')
            ->method('afterTestUpdate')
            ->with(self::identicalTo($test));
        $lifecycle->updateExecutionContext(fn () => null, 'a');
    }

    public function testUpdateExecutionContext_ExceptionThrownDuringTestUpdate_NotifiesHooksWithError(): void
    {
        $hooksNotifier = $this->createMock(HooksNotifierInterface::class);
        $test = new TestResult('a');
        $lifecycle = new AllureLifecycle(
            $this->createStub(LoggerInterface::class),
            $this->createClock(),
            $this->createStub(ResultsWriterInterface::class),
            $hooksNotifier,
            $this->createStorageWithTest($test),
            new ThreadContext(),
        );

        $error = new Exception();
        $hooksNotifier
            ->expects(self::once())
            ->id('before')
            ->method('beforeTestUpdate')
            ->with(self::identicalTo($test));
        $hooksNotifier
            ->expects(self::once())
            ->id('error')
            ->after('before')
            ->method('onLifecycleError')
            ->with(self::identicalTo($error));
        $hooksNotifier
            ->expects(self::once())
            ->after('error')
            ->method('afterTestUpdate')
            ->with(self::identicalTo($test));
        $lifecycle->updateExecutionContext(fn () => throw $error, 'a');
    }

    public function testUpdateExecutionContext_NoExceptionThrownDuringFixtureUpdate_NotifiesHooksWithoutError(): void
    {
        $hooksNotifier = $this->createMock(HooksNotifierInterface::class);
        $fixture = new FixtureResult('a');
        $lifecycle = new AllureLifecycle(
            $this->createStub(LoggerInterface::class),
            $this->createClock(),
            $this->createStub(ResultsWriterInterface::class),
            $hooksNotifier,
            $this->createStorageWithFixture($fixture),
            new ThreadContext(),
        );

        $hooksNotifier
            ->expects(self::once())
            ->id('before')
            ->method('beforeFixtureUpdate')
            ->with(self::identicalTo($fixture));
        $hooksNotifier
            ->expects(self::never())
            ->method('onLifecycleError');
        $hooksNotifier
            ->expects(self::once())
            ->after('before')
            ->method('afterFixtureUpdate')
            ->with(self::identicalTo($fixture));
        $lifecycle->updateExecutionContext(fn () => null, 'a');
    }

    public function testUpdateExecutionContext_ExceptionThrownDuringFixtureUpdate_NotifiesHooksWithError(): void
    {
        $hooksNotifier = $this->createMock(HooksNotifierInterface::class);
        $fixture = new FixtureResult('a');
        $lifecycle = new AllureLifecycle(
            $this->createStub(LoggerInterface::class),
            $this->createClock(),
            $this->createStub(ResultsWriterInterface::class),
            $hooksNotifier,
            $this->createStorageWithFixture($fixture),
            new ThreadContext(),
        );

        $error = new Exception();
        $hooksNotifier
            ->expects(self::once())
            ->id('before')
            ->method('beforeFixtureUpdate')
            ->with(self::identicalTo($fixture));
        $hooksNotifier
            ->expects(self::once())
            ->id('error')
            ->after('before')
            ->method('onLifecycleError')
            ->with(self::identicalTo($error));
        $hooksNotifier
            ->expects(self::once())
            ->after('error')
            ->method('afterFixtureUpdate')
            ->with(self::identicalTo($fixture));
        $lifecycle->updateExecutionContext(fn () => throw $error, 'a');
    }

    public function testUpdateExecutionContext_NoExceptionThrownDuringStepUpdate_NotifiesHooksWithoutError(): void
    {
        $hooksNotifier = $this->createMock(HooksNotifierInterface::class);
        $step = new StepResult('a');
        $lifecycle = new AllureLifecycle(
            $this->createStub(LoggerInterface::class),
            $this->createClock(),
            $this->createStub(ResultsWriterInterface::class),
            $hooksNotifier,
            $this->createStorageWithStep($step),
            new ThreadContext(),
        );

        $hooksNotifier
            ->expects(self::once())
            ->id('before')
            ->method('beforeStepUpdate')
            ->with(self::identicalTo($step));
        $hooksNotifier
            ->expects(self::never())
            ->method('onLifecycleError');
        $hooksNotifier
            ->expects(self::once())
            ->after('before')
            ->method('afterStepUpdate')
            ->with(self::identicalTo($step));
        $lifecycle->updateExecutionContext(fn () => null, 'a');
    }

    public function testUpdateExecutionContext_ExceptionThrownDuringStepUpdate_NotifiesHooksWithError(): void
    {
        $hooksNotifier = $this->createMock(HooksNotifierInterface::class);
        $step = new StepResult('a');
        $lifecycle = new AllureLifecycle(
            $this->createStub(LoggerInterface::class),
            $this->createClock(),
            $this->createStub(ResultsWriterInterface::class),
            $hooksNotifier,
            $this->createStorageWithStep($step),
            new ThreadContext(),
        );

        $error = new Exception();
        $hooksNotifier
            ->expects(self::once())
            ->id('before')
            ->method('beforeStepUpdate')
            ->with(self::identicalTo($step));
        $hooksNotifier
            ->expects(self::once())
            ->id('error')
            ->after('before')
            ->method('onLifecycleError')
            ->with(self::identicalTo($error));
        $hooksNotifier
            ->expects(self::once())
            ->after('error')
            ->method('afterStepUpdate')
            ->with(self::identicalTo($step));
        $lifecycle->updateExecutionContext(fn () => throw $error, 'a');
    }

    public function testUpdateExecutionContext_ExceptionThrownDuringTestUpdate_LogsErrorAndReturnsUuid(): void
    {
        $logger = $this->createMock(LoggerInterface::class);
        $test = new TestResult('a');
        $lifecycle = new AllureLifecycle(
            $logger,
            $this->createClock(),
            $this->createStub(ResultsWriterInterface::class),
            $this->createStub(HooksNotifierInterface::class),
            $this->createStorageWithTest($test),
            new ThreadContext(),
        );

        $error = new Exception();
        $logger
            ->expects(self::once())
            ->method('error')
            ->with(
                self::stringContains('Test (UUID: {uuid}) not updated'),
                self::identicalTo(['uuid' => 'a', 'exception' => $error]),
            );
        self::assertSame('a', $lifecycle->updateExecutionContext(fn () => throw $error, 'a'));
    }

    public function testUpdateExecutionContext_ExceptionThrownDuringFixtureUpdate_LogsErrorAndReturnsUuid(): void
    {
        $logger = $this->createMock(LoggerInterface::class);
        $fixture = new FixtureResult('a');
        $lifecycle = new AllureLifecycle(
            $logger,
            $this->createClock(),
            $this->createStub(ResultsWriterInterface::class),
            $this->createStub(HooksNotifierInterface::class),
            $this->createStorageWithFixture($fixture),
            new ThreadContext(),
        );

        $error = new Exception();
        $logger
            ->expects(self::once())
            ->method('error')
            ->with(
                self::stringContains('Fixture (UUID: {uuid}) not updated'),
                self::identicalTo(['uuid' => 'a', 'exception' => $error]),
            );
        self::assertSame('a', $lifecycle->updateExecutionContext(fn () => throw $error, 'a'));
    }

    public function testUpdateExecutionContext_ExceptionThrownDuringStepUpdate_LogsErrorAndReturnsUuid(): void
    {
        $logger = $this->createMock(LoggerInterface::class);
        $step = new StepResult('a');
        $lifecycle = new AllureLifecycle(
            $logger,
            $this->createClock(),
            $this->createStub(ResultsWriterInterface::class),
            $this->createStub(HooksNotifierInterface::class),
            $this->createStorageWithStep($step),
            new ThreadContext(),
        );

        $error = new Exception();
        $logger
            ->expects(self::once())
            ->method('error')
            ->with(
                self::stringContains('Step (UUID: {uuid}) not updated'),
                self::identicalTo(['uuid' => 'a', 'exception' => $error]),
            );
        self::assertSame('a', $lifecycle->updateExecutionContext(fn () => throw $error, 'a'));
    }

    public function testStopStep_StepNeitherGivenNorStarted_LogsErrorAndReturnsNull(): void
    {
        $logger = $this->createMock(LoggerInterface::class);
        $lifecycle = new AllureLifecycle(
            $logger,
            $this->createClock(),
            $this->createStub(ResultsWriterInterface::class),
            $this->createStub(HooksNotifierInterface::class),
            $this->createStub(ResultStorageInterface::class),
            new ThreadContext(),
        );

        $logger
            ->expects(self::once())
            ->method('error')
            ->with(
                self::stringContains('Step (UUID: {uuid}) not stopped'),
                self::equalTo(['uuid' => null, 'exception' => new ActiveStepNotFoundException()]),
            );
        self::assertNull($lifecycle->stopStep());
    }

    public function testStopStep_StorageFailsToProvideGivenStep_LogsErrorAndReturnsNull(): void
    {
        $logger = $this->createMock(LoggerInterface::class);
        $error = new Exception();
        $lifecycle = new AllureLifecycle(
            $logger,
            $this->createClock(),
            $this->createStub(ResultsWriterInterface::class),
            $this->createStub(HooksNotifierInterface::class),
            $this->createStorageWithoutStep('a', $error),
            new ThreadContext(),
        );

        $logger
            ->expects(self::once())
            ->method('error')
            ->with(
                self::stringContains('Step (UUID: {uuid}) not stopped'),
                self::identicalTo(['uuid' => 'a', 'exception' => $error]),
            );
        self::assertNull($lifecycle->stopStep('a'));
    }

    public function testStopStep_StorageFailsToProvideStartedStep_LogsErrorAndReturnsNull(): void
    {
        $logger = $this->createMock(LoggerInterface::class);
        $error = new Exception();
        $threadContext = new ThreadContext();
        $lifecycle = new AllureLifecycle(
            $logger,
            $this->createClock(),
            $this->createStub(ResultsWriterInterface::class),
            $this->createStub(HooksNotifierInterface::class),
            $this->createStorageWithoutStep('b', $error),
            $threadContext,
        );

        $threadContext->push('a');
        $threadContext->push('b');
        $logger
            ->expects(self::once())
            ->method('error')
            ->with(
                self::stringContains('Step (UUID: {uuid}) not stopped'),
                self::identicalTo(['uuid' => 'b', 'exception' => $error]),
            );
        self::assertNull($lifecycle->stopStep());
    }

    public function testStopStep_StepNeitherGivenNorStarted_NotifiesHooksWithError(): void
    {
        $hooksNotifier = $this->createMock(HooksNotifierInterface::class);
        $lifecycle = new AllureLifecycle(
            $this->createStub(LoggerInterface::class),
            $this->createClock(),
            $this->createStub(ResultsWriterInterface::class),
            $hooksNotifier,
            $this->createStub(ResultStorageInterface::class),
            new ThreadContext(),
        );

        $hooksNotifier
            ->expects(self::once())
            ->method('onLifecycleError')
            ->with(self::isInstanceOf(ActiveStepNotFoundException::class));
        $lifecycle->stopStep();
    }

    public function testStopStep_StorageFailsToProvideGivenStep_NotifiesHooksWithError(): void
    {
        $hooksNotifier = $this->createMock(HooksNotifierInterface::class);
        $error = new Exception();
        $lifecycle = new AllureLifecycle(
            $this->createStub(LoggerInterface::class),
            $this->createClock(),
            $this->createStub(ResultsWriterInterface::class),
            $hooksNotifier,
            $this->createStorageWithoutStep('a', $error),
            new ThreadContext(),
        );

        $hooksNotifier
            ->expects(self::once())
            ->method('onLifecycleError')
            ->with(self::identicalTo($error));
        $lifecycle->stopStep('a');
    }

    public function testStopStep_StorageFailsToProvideStartedStep_NotifiesHooksWithError(): void
    {
        $hooksNotifier = $this->createMock(HooksNotifierInterface::class);
        $error = new Exception();
        $threadContext = new ThreadContext();
        $lifecycle = new AllureLifecycle(
            $this->createStub(LoggerInterface::class),
            $this->createClock(),
            $this->createStub(ResultsWriterInterface::class),
            $hooksNotifier,
            $this->createStorageWithoutStep('b', $error),
            $threadContext,
        );

        $threadContext->push('a');
        $threadContext->push('b');
        $hooksNotifier
            ->expects(self::once())
            ->method('onLifecycleError')
            ->with(self::identicalTo($error));
        $lifecycle->stopStep();
    }

    public function testStopStep_StorageProvidesGivenStep_NeverLogsErrorAndReturnsUuid(): void
    {
        $logger = $this->createMock(LoggerInterface::class);
        $lifecycle = new AllureLifecycle(
            $logger,
            $this->createClock(),
            $this->createStub(ResultsWriterInterface::class),
            $this->createStub(HooksNotifierInterface::class),
            $this->createStorageWithStep(new StepResult('a')),
            new ThreadContext(),
        );

        $logger
            ->expects(self::never())
            ->method('error');
        self::assertSame('a', $lifecycle->stopStep('a'));
    }

    public function testStopStep_StorageProvidesStartedStep_NeverLogsErrorAndReturnsUuid(): void
    {
        $logger = $this->createMock(LoggerInterface::class);
        $threadContext = new ThreadContext();
        $lifecycle = new AllureLifecycle(
            $logger,
            $this->createClock(),
            $this->createStub(ResultsWriterInterface::class),
            $this->createStub(HooksNotifierInterface::class),
            $this->createStorageWithStep(new StepResult('b')),
            $threadContext,
        );

        $threadContext->push('a');
        $threadContext->push('b');
        $logger
            ->expects(self::never())
            ->method('error');
        self::assertSame('b', $lifecycle->stopStep());
    }

    public function testStopStep_NoExceptionThrownDuringStop_NotifiesHooksWithoutError(): void
    {
        $hooksNotifier = $this->createMock(HooksNotifierInterface::class);
        $step = new StepResult('a');
        $lifecycle = new AllureLifecycle(
            $this->createStub(LoggerInterface::class),
            $this->createClock(),
            $this->createStub(ResultsWriterInterface::class),
            $hooksNotifier,
            $this->createStorageWithStep($step),
            new ThreadContext(),
        );

        $hooksNotifier
            ->expects(self::once())
            ->id('before')
            ->method('beforeStepStop')
            ->with(self::identicalTo($step));
        $hooksNotifier
            ->expects(self::never())
            ->method('onLifecycleError');
        $hooksNotifier
            ->expects(self::once())
            ->after('before')
            ->method('afterStepStop')
            ->with(self::identicalTo($step));
        $lifecycle->stopStep('a');
    }

    public function testStopStep_ExceptionThrownDuringStop_NotifiesHooksWithError(): void
    {
        $hooksNotifier = $this->createMock(HooksNotifierInterface::class);
        $error = new Exception();
        $step = new StepResult('a');
        $lifecycle = new AllureLifecycle(
            $this->createStub(LoggerInterface::class),
            $this->createFailingClock($error),
            $this->createStub(ResultsWriterInterface::class),
            $hooksNotifier,
            $this->createStorageWithStep($step),
            new ThreadContext(),
        );

        $hooksNotifier
            ->expects(self::once())
            ->id('before')
            ->method('beforeStepStop')
            ->with(self::identicalTo($step));
        $hooksNotifier
            ->expects(self::once())
            ->id('error')
            ->after('before')
            ->method('onLifecycleError');
        $hooksNotifier
            ->expects(self::once())
            ->after('error')
            ->method('afterStepStop')
            ->with(self::identicalTo($step));
        $lifecycle->stopStep('a');
    }

    public function testStopStep_ExceptionThrownDuringStop_LogsErrorAndReturnsUuid(): void
    {
        $logger = $this->createMock(LoggerInterface::class);
        $error = new Exception();
        $step = new StepResult('a');
        $lifecycle = new AllureLifecycle(
            $logger,
            $this->createFailingClock($error),
            $this->createStub(ResultsWriterInterface::class),
            $this->createStub(HooksNotifierInterface::class),
            $this->createStorageWithStep($step),
            new ThreadContext(),
        );

        $logger
            ->expects(self::once())
            ->method('error')
            ->with(
                self::stringContains('Step (UUID: {uuid}) not stopped'),
                self::identicalTo(['uuid' => 'a', 'exception' => $error]),
            );
        self::assertSame('a', $lifecycle->stopStep('a'));
    }

    public function testStopStep_ClockProvidesTime_StepStopIsSameTime(): void
    {
        $time = new DateTimeImmutable('@0');
        $step = new StepResult('a');
        $lifecycle = new AllureLifecycle(
            $this->createStub(LoggerInterface::class),
            $this->createClock($time),
            $this->createStub(ResultsWriterInterface::class),
            $this->createStub(HooksNotifierInterface::class),
            $this->createStorageWithStep($step),
            new ThreadContext(),
        );

        $lifecycle->stopStep('a');
        self::assertSame($time, $step->getStop());
    }

    public function testStopStep_StorageProvidesStep_StepOnFinishedStage(): void
    {
        $time = new DateTimeImmutable('@0');
        $step = new StepResult('a');
        $lifecycle = new AllureLifecycle(
            $this->createStub(LoggerInterface::class),
            $this->createClock($time),
            $this->createStub(ResultsWriterInterface::class),
            $this->createStub(HooksNotifierInterface::class),
            $this->createStorageWithStep($step),
            new ThreadContext(),
        );

        $lifecycle->stopStep('a');
        self::assertSame($time, $step->getStop());
    }

    public function testStopStep_StorageProvidesStep_StorageUnsetsSameStep(): void
    {
        $resultStorage = $this->createMock(ResultStorageInterface::class);
        $resultStorage
            ->method('getStep')
            ->with(self::identicalTo('a'))
            ->willReturn(new StepResult('a'));
        $lifecycle = new AllureLifecycle(
            $this->createStub(LoggerInterface::class),
            $this->createClock(),
            $this->createStub(ResultsWriterInterface::class),
            $this->createStub(HooksNotifierInterface::class),
            $resultStorage,
            new ThreadContext(),
        );

        $resultStorage
            ->expects(self::once())
            ->method('unset')
            ->with(self::identicalTo('a'));
        $lifecycle->stopStep('a');
    }

    public function testStopStep_ThreadContextWithNonEmptyStack_TopValueIsRemovedFromStack(): void
    {
        $threadContext = new ThreadContext();
        $lifecycle = new AllureLifecycle(
            $this->createStub(LoggerInterface::class),
            $this->createClock(),
            $this->createStub(ResultsWriterInterface::class),
            $this->createStub(HooksNotifierInterface::class),
            $this->createStorageWithStep(new StepResult('a')),
            $threadContext,
        );

        $threadContext->push('b');
        $threadContext->push('c');
        $lifecycle->stopStep('a');
        self::assertSame(['b'], $this->extractThreadStack($threadContext));
    }

    public function testAddAttachment_ThreadContextWithEmptyStack_LogsError(): void
    {
        $logger = $this->createMock(LoggerInterface::class);
        $lifecycle = new AllureLifecycle(
            $logger,
            $this->createClock(),
            $this->createStub(ResultsWriterInterface::class),
            $this->createStub(HooksNotifierInterface::class),
            $this->createStub(ResultStorageInterface::class),
            new ThreadContext(),
        );

        $attachment = new AttachmentResult('a');
        $logger
            ->expects(self::once())
            ->method('error')
            ->with(
                self::stringContains('Attachment (UUID: {uuid}) not added (parent UUID: {parentUuid})'),
                self::equalTo(
                    [
                        'uuid' => 'a',
                        'parentUuid' => null,
                        'exception' => new ActiveExecutionContextNotFoundException(),
                    ],
                ),
            );
        $lifecycle->addAttachment(
            $attachment,
            $this->createStub(DataSourceInterface::class),
        );
    }

    public function testAddAttachment_StorageFailedToProvideStartedTest_LogsError(): void
    {
        $logger = $this->createMock(LoggerInterface::class);
        $error = new Exception();
        $threadContext = new ThreadContext();
        $lifecycle = new AllureLifecycle(
            $logger,
            $this->createClock(),
            $this->createStub(ResultsWriterInterface::class),
            $this->createStub(HooksNotifierInterface::class),
            $this->createStorageWithoutTest('a', $error),
            $threadContext,
        );

        $threadContext->push('a');
        $attachment = new AttachmentResult('b');
        $logger
            ->expects(self::once())
            ->method('error')
            ->with(
                self::stringContains('Attachment (UUID: {uuid}) not added (parent UUID: {parentUuid})'),
                self::identicalTo(
                    [
                        'uuid' => 'b',
                        'parentUuid' => 'a',
                        'exception' => $error,
                    ],
                ),
            );
        $lifecycle->addAttachment(
            $attachment,
            $this->createStub(DataSourceInterface::class),
        );
    }

    public function testAddAttachment_StorageFailedToProvideStartedStep_LogsError(): void
    {
        $logger = $this->createMock(LoggerInterface::class);
        $error = new Exception();
        $threadContext = new ThreadContext();
        $lifecycle = new AllureLifecycle(
            $logger,
            $this->createClock(),
            $this->createStub(ResultsWriterInterface::class),
            $this->createStub(HooksNotifierInterface::class),
            $this->createStorageWithoutStep('b', $error),
            $threadContext,
        );

        $threadContext->push('a');
        $threadContext->push('b');
        $attachment = new AttachmentResult('c');
        $logger
            ->expects(self::once())
            ->method('error')
            ->with(
                self::stringContains('Attachment (UUID: {uuid}) not added (parent UUID: {parentUuid})'),
                self::identicalTo(
                    [
                        'uuid' => 'c',
                        'parentUuid' => 'b',
                        'exception' => $error,
                    ],
                ),
            );
        $lifecycle->addAttachment(
            $attachment,
            $this->createStub(DataSourceInterface::class),
        );
    }

    public function testAddAttachment_ThreadContextWithEmptyStack_NotifiesHooksWithError(): void
    {
        $hooksNotifier = $this->createMock(HooksNotifierInterface::class);
        $lifecycle = new AllureLifecycle(
            $this->createStub(LoggerInterface::class),
            $this->createClock(),
            $this->createStub(ResultsWriterInterface::class),
            $hooksNotifier,
            $this->createStub(ResultStorageInterface::class),
            new ThreadContext(),
        );

        $attachment = new AttachmentResult('a');
        $hooksNotifier
            ->expects(self::once())
            ->method('onLifecycleError')
            ->with(self::isInstanceOf(ActiveExecutionContextNotFoundException::class));
        $lifecycle->addAttachment(
            $attachment,
            $this->createStub(DataSourceInterface::class),
        );
    }

    public function testAddAttachment_StorageFailsToProvideStartedTest_NotifiesHooksWithError(): void
    {
        $hooksNotifier = $this->createMock(HooksNotifierInterface::class);
        $error = new Exception();
        $threadContext = new ThreadContext();
        $lifecycle = new AllureLifecycle(
            $this->createStub(LoggerInterface::class),
            $this->createClock(),
            $this->createStub(ResultsWriterInterface::class),
            $hooksNotifier,
            $this->createStorageWithoutTest('a', $error),
            $threadContext,
        );

        $threadContext->push('a');
        $attachment = new AttachmentResult('b');
        $hooksNotifier
            ->expects(self::once())
            ->method('onLifecycleError')
            ->with(self::identicalTo($error));
        $lifecycle->addAttachment(
            $attachment,
            $this->createStub(DataSourceInterface::class),
        );
    }

    public function testAddAttachment_StorageFailsToProvideStartedStep_NotifiesHooksWithError(): void
    {
        $hooksNotifier = $this->createMock(HooksNotifierInterface::class);
        $error = new Exception();
        $threadContext = new ThreadContext();
        $lifecycle = new AllureLifecycle(
            $this->createStub(LoggerInterface::class),
            $this->createClock(),
            $this->createStub(ResultsWriterInterface::class),
            $hooksNotifier,
            $this->createStorageWithoutStep('b', $error),
            $threadContext,
        );

        $threadContext->push('a');
        $threadContext->push('b');
        $attachment = new AttachmentResult('c');
        $hooksNotifier
            ->expects(self::once())
            ->method('onLifecycleError')
            ->with(self::identicalTo($error));
        $lifecycle->addAttachment(
            $attachment,
            $this->createStub(DataSourceInterface::class),
        );
    }

    public function testAddAttachment_NoExceptionThrownDuringWrite_NotifiesHooksWithoutError(): void
    {
        $hooksNotifier = $this->createMock(HooksNotifierInterface::class);
        $threadContext = new ThreadContext();
        $lifecycle = new AllureLifecycle(
            $this->createStub(LoggerInterface::class),
            $this->createClock(),
            $this->createStub(ResultsWriterInterface::class),
            $hooksNotifier,
            $this->createStorageWithTest(new TestResult('a')),
            $threadContext,
        );

        $threadContext->push('a');
        $attachment = new AttachmentResult('b');
        $hooksNotifier
            ->expects(self::once())
            ->id('before')
            ->method('beforeAttachmentWrite')
            ->with(self::identicalTo($attachment));
        $hooksNotifier
            ->expects(self::never())
            ->method('onLifecycleError');
        $hooksNotifier
            ->expects(self::once())
            ->after('before')
            ->method('afterAttachmentWrite')
            ->with(self::identicalTo($attachment));
        $lifecycle->addAttachment(
            $attachment,
            $this->createStub(DataSourceInterface::class),
        );
    }

    public function testAddAttachment_ExceptionThrownDuringWrite_NotifiesHooksWithError(): void
    {
        $error = new Exception();
        $resultsWriter = $this->createStub(ResultsWriterInterface::class);
        $resultsWriter
            ->method('writeAttachment')
            ->willThrowException($error);
        $hooksNotifier = $this->createMock(HooksNotifierInterface::class);
        $threadContext = new ThreadContext();
        $lifecycle = new AllureLifecycle(
            $this->createStub(LoggerInterface::class),
            $this->createClock(),
            $resultsWriter,
            $hooksNotifier,
            $this->createStorageWithTest(new TestResult('a')),
            $threadContext,
        );

        $threadContext->push('a');
        $attachment = new AttachmentResult('b');
        $hooksNotifier
            ->expects(self::once())
            ->id('before')
            ->method('beforeAttachmentWrite')
            ->with(self::identicalTo($attachment));
        $hooksNotifier
            ->expects(self::once())
            ->id('error')
            ->after('before')
            ->method('onLifecycleError')
            ->with(self::identicalTo($error));
        $hooksNotifier
            ->expects(self::once())
            ->after('error')
            ->method('afterAttachmentWrite')
            ->with(self::identicalTo($attachment));
        $lifecycle->addAttachment(
            $attachment,
            $this->createStub(DataSourceInterface::class),
        );
    }

    public function testAddAttachment_AttachmentNotExcluded_WriterWritesSameAttachmentWithGivenData(): void
    {
        $resultsWriter = $this->createMock(ResultsWriterInterface::class);
        $threadContext = new ThreadContext();
        $lifecycle = new AllureLifecycle(
            $this->createStub(LoggerInterface::class),
            $this->createClock(),
            $resultsWriter,
            $this->createStub(HooksNotifierInterface::class),
            $this->createStorageWithTest(new TestResult('a')),
            $threadContext,
        );

        $threadContext->push('a');
        $attachment = new AttachmentResult('b');
        $data = $this->createStub(DataSourceInterface::class);
        $resultsWriter
            ->expects(self::once())
            ->method('writeAttachment')
            ->with(self::identicalTo($attachment), self::identicalTo($data));
        $lifecycle->addAttachment($attachment, $data);
    }

    public function testAddAttachment_AttachmentExcluded_WriterNeverWritesAttachment(): void
    {
        $resultsWriter = $this->createMock(ResultsWriterInterface::class);
        $threadContext = new ThreadContext();
        $lifecycle = new AllureLifecycle(
            $this->createStub(LoggerInterface::class),
            $this->createClock(),
            $resultsWriter,
            $this->createStub(HooksNotifierInterface::class),
            $this->createStorageWithTest(new TestResult('a')),
            $threadContext,
        );

        $threadContext->push('a');
        $attachment = new AttachmentResult('b');
        $resultsWriter
            ->expects(self::never())
            ->method('writeAttachment');
        $lifecycle->addAttachment(
            $attachment->setExcluded(true),
            $this->createStub(DataSourceInterface::class),
        );
    }

    private function extractThreadStack(ThreadContextInterface $threadContext): array
    {
        $items = [];
        while (null !== $item = $threadContext->getCurrentTestOrStep()) {
            $items[] = $item;
            $threadContext->pop();
        }

        return $items;
    }

    private function createStorageWithContainer(
        ContainerResult $container,
        ?Throwable $setError = null,
        ?Throwable $unsetError = null,
    ): ResultStorageInterface {
        $resultStorage = $this->createMock(ResultStorageInterface::class);
        $resultStorage
            ->method('getContainer')
            ->with(self::identicalTo($container->getUuid()))
            ->willReturn($container);
        if (isset($setError)) {
            $resultStorage
                ->method('set')
                ->willThrowException($setError);
        }
        if (isset($unsetError)) {
            $resultStorage
                ->method('unset')
                ->willThrowException($unsetError);
        }

        return $resultStorage;
    }

    private function createStorageWithoutContainer(string $uuid, Throwable $error): ResultStorageInterface
    {
        $resultStorage = $this->createMock(ResultStorageInterface::class);
        $resultStorage
            ->method('getContainer')
            ->with(self::identicalTo($uuid))
            ->willThrowException($error);

        return $resultStorage;
    }

    private function createStorageWithFixture(FixtureResult $fixture): ResultStorageInterface
    {
        $resultStorage = $this->createMock(ResultStorageInterface::class);
        $resultStorage
            ->method('getFixture')
            ->with(self::identicalTo($fixture->getUuid()))
            ->willReturn($fixture);
        $resultStorage
            ->method('getExecutionContext')
            ->with(self::identicalTo($fixture->getUuid()))
            ->willReturn($fixture);

        return $resultStorage;
    }

    private function createStorageWithoutFixture(string $uuid, Throwable $error): ResultStorageInterface
    {
        $resultStorage = $this->createMock(ResultStorageInterface::class);
        $resultStorage
            ->method('getFixture')
            ->with(self::identicalTo($uuid))
            ->willThrowException($error);
        $resultStorage
            ->method('getExecutionContext')
            ->with(self::identicalTo($uuid))
            ->willThrowException($error);

        return $resultStorage;
    }

    private function createStorageWithTest(TestResult $test, ?Throwable $unsetError = null): ResultStorageInterface
    {
        $resultStorage = $this->createMock(ResultStorageInterface::class);
        $resultStorage
            ->method('getTest')
            ->with(self::identicalTo($test->getUuid()))
            ->willReturn($test);
        $resultStorage
            ->method('getExecutionContext')
            ->with(self::identicalTo($test->getUuid()))
            ->willReturn($test);
        if (isset($unsetError)) {
            $resultStorage
                ->method('unset')
                ->willThrowException($unsetError);
        }

        return $resultStorage;
    }

    private function createStorageWithoutTest(string $uuid, Throwable $error): ResultStorageInterface
    {
        $resultStorage = $this->createMock(ResultStorageInterface::class);
        $resultStorage
            ->method('getTest')
            ->with(self::identicalTo($uuid))
            ->willThrowException($error);
        $resultStorage
            ->method('getExecutionContext')
            ->with(self::identicalTo($uuid))
            ->willThrowException($error);

        return $resultStorage;
    }

    private function createStorageWithStep(StepResult $step): ResultStorageInterface
    {
        $resultStorage = $this->createMock(ResultStorageInterface::class);
        $resultStorage
            ->method('getStep')
            ->with(self::identicalTo($step->getUuid()))
            ->willReturn($step);
        $resultStorage
            ->method('getExecutionContext')
            ->with(self::identicalTo($step->getUuid()))
            ->willReturn($step);

        return $resultStorage;
    }

    private function createStorageWithoutStep(string $uuid, Throwable $error): ResultStorageInterface
    {
        $resultStorage = $this->createMock(ResultStorageInterface::class);
        $resultStorage
            ->method('getStep')
            ->with(self::identicalTo($uuid))
            ->willThrowException($error);
        $resultStorage
            ->method('getExecutionContext')
            ->with(self::identicalTo($uuid))
            ->willThrowException($error);

        return $resultStorage;
    }

    private function createStorageWithInvalidExecutionContext(string $uuid): ResultStorageInterface
    {
        $context = $this->createStub(ExecutionContextInterface::class);
        $context
            ->method('getUuid')
            ->willReturn($uuid);
        $resultStorage = $this->createMock(ResultStorageInterface::class);
        $resultStorage
            ->method('getExecutionContext')
            ->with(self::identicalTo($uuid))
            ->willReturn($context);

        return $resultStorage;
    }

    private function createStorageWithoutExecutionContext(string $uuid, Throwable $error): ResultStorageInterface
    {
        $resultStorage = $this->createMock(ResultStorageInterface::class);
        $resultStorage
            ->method('getExecutionContext')
            ->with(self::identicalTo($uuid))
            ->willThrowException($error);

        return $resultStorage;
    }

    private function createNonSettableStorage(StorableResultInterface $result, Throwable $error): ResultStorageInterface
    {
        $resultStorage = $this->createMock(ResultStorageInterface::class);
        $resultStorage
            ->method('set')
            ->with(self::identicalTo($result))
            ->willThrowException($error);

        return $resultStorage;
    }

    private function createClock(?DateTimeImmutable $time = null): ClockInterface
    {
        $clock = $this->createStub(ClockInterface::class);
        $clock
            ->method('now')
            ->willReturn($time ?? new DateTimeImmutable('@0'));

        return $clock;
    }

    private function createFailingClock(Throwable $error): ClockInterface
    {
        $clock = $this->createStub(ClockInterface::class);
        $clock
            ->method('now')
            ->willThrowException($error);

        return $clock;
    }
}

Spamworldpro Mini