<?php

/*
 * This file is part of the NelmioApiDocBundle package.
 *
 * (c) Nelmio
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Nelmio\ApiDocBundle\Tests\SwaggerPhp;

use Nelmio\ApiDocBundle\OpenApiPhp\Util;
use OpenApi\Annotations as OA;
use OpenApi\Context;
use OpenApi\Generator;
use PHPUnit\Framework\TestCase;

/**
 * Class UtilTest.
 *
 * @covers \Nelmio\ApiDocBundle\OpenApiPhp\Util::getOperation
 * @covers \Nelmio\ApiDocBundle\OpenApiPhp\Util::getOperationParameter
 * @covers \Nelmio\ApiDocBundle\OpenApiPhp\Util::getChild
 * @covers \Nelmio\ApiDocBundle\OpenApiPhp\Util::getCollectionItem
 * @covers \Nelmio\ApiDocBundle\OpenApiPhp\Util::getIndexedCollectionItem
 * @covers \Nelmio\ApiDocBundle\OpenApiPhp\Util::searchCollectionItem
 * @covers \Nelmio\ApiDocBundle\OpenApiPhp\Util::searchIndexedCollectionItem
 * @covers \Nelmio\ApiDocBundle\OpenApiPhp\Util::createCollectionItem
 * @covers \Nelmio\ApiDocBundle\OpenApiPhp\Util::createChild
 * @covers \Nelmio\ApiDocBundle\OpenApiPhp\Util::createContext
 * @covers \Nelmio\ApiDocBundle\OpenApiPhp\Util::merge
 * @covers \Nelmio\ApiDocBundle\OpenApiPhp\Util::mergeFromArray
 * @covers \Nelmio\ApiDocBundle\OpenApiPhp\Util::mergeChild
 * @covers \Nelmio\ApiDocBundle\OpenApiPhp\Util::mergeCollection
 * @covers \Nelmio\ApiDocBundle\OpenApiPhp\Util::mergeTyped
 * @covers \Nelmio\ApiDocBundle\OpenApiPhp\Util::mergeProperty
 * @covers \Nelmio\ApiDocBundle\OpenApiPhp\Util::getNestingIndexes
 * @covers \Nelmio\ApiDocBundle\OpenApiPhp\Util::getNesting
 */
class UtilTest extends TestCase
{
    public $rootContext;

    /** @var OA\OpenApi */
    public $rootAnnotation;

    public function setUp(): void
    {
        parent::setUp();

        $this->rootContext = new Context(['isTestingRoot' => true]);
        $this->rootAnnotation = new OA\OpenApi(['_context' => $this->rootContext]);
    }

    public function testCreateContextSetsParentContext()
    {
        $context = Util::createContext([], $this->rootContext);

        $this->assertContextIsConnectedToRootContext($context);
    }

    public function testCreateContextWithProperties()
    {
        $context = Util::createContext(['testing' => 'trait']);

        $this->assertTrue($context->is('testing'));
        $this->assertSame('trait', $context->testing);
    }

    public function testCreateChild()
    {
        $info = Util::createChild($this->rootAnnotation, OA\Info::class);

        $this->assertInstanceOf(OA\Info::class, $info);
    }

    public function testCreateChildHasContext()
    {
        $info = Util::createChild($this->rootAnnotation, OA\Info::class);

        $this->assertInstanceOf(Context::class, $info->_context);
    }

    public function testCreateChildHasNestedContext()
    {
        $path = Util::createChild($this->rootAnnotation, OA\PathItem::class);
        $this->assertIsNested($this->rootAnnotation, $path);

        $parameter = Util::createChild($path, OA\Parameter::class);
        $this->assertIsNested($path, $parameter);

        $schema = Util::createChild($parameter, OA\Schema::class);
        $this->assertIsNested($parameter, $schema);

        $this->assertIsConnectedToRootContext($schema);
    }

    public function testCreateChildWithEmptyProperties()
    {
        $properties = [];
        /** @var OA\Info $info */
        $info = Util::createChild($this->rootAnnotation, OA\Info::class, $properties);

        $properties = array_filter(get_object_vars($info), function ($key) {
            return 0 !== strpos($key, '_');
        }, ARRAY_FILTER_USE_KEY);

        $this->assertEquals([Generator::UNDEFINED], array_unique(array_values($properties)));

        $this->assertIsNested($this->rootAnnotation, $info);
        $this->assertIsConnectedToRootContext($info);
    }

    public function testCreateChildWithProperties()
    {
        $properties = ['title' => 'testing', 'version' => '999', 'x' => new \stdClass()];
        /** @var OA\Info $info */
        $info = Util::createChild($this->rootAnnotation, OA\Info::class, $properties);

        $this->assertSame($info->title, $properties['title']);
        $this->assertSame($info->version, $properties['version']);
        $this->assertSame($info->x, $properties['x']);

        $this->assertIsNested($this->rootAnnotation, $info);
        $this->assertIsConnectedToRootContext($info);
    }

    public function testCreateCollectionItemAddsCreatedItemToCollection()
    {
        $collection = 'paths';
        $class = OA\PathItem::class;

        $p1 = Util::createCollectionItem($this->rootAnnotation, $collection, $class);
        $this->assertSame(0, $p1);
        $this->assertCount(1, $this->rootAnnotation->{$collection});
        $this->assertInstanceOf($class, $this->rootAnnotation->{$collection}[$p1]);
        $this->assertIsNested($this->rootAnnotation, $this->rootAnnotation->{$collection}[$p1]);
        $this->assertIsConnectedToRootContext($this->rootAnnotation->{$collection}[$p1]);

        $p2 = Util::createCollectionItem($this->rootAnnotation, $collection, $class);
        $this->assertSame(1, $p2);
        $this->assertCount(2, $this->rootAnnotation->{$collection});
        $this->assertInstanceOf($class, $this->rootAnnotation->{$collection}[$p2]);
        $this->assertIsNested($this->rootAnnotation, $this->rootAnnotation->{$collection}[$p2]);
        $this->assertIsConnectedToRootContext($this->rootAnnotation->{$collection}[$p2]);

        $this->rootAnnotation->components = Util::createChild($this->rootAnnotation, OA\Components::class);

        $collection = 'schemas';
        $class = OA\Schema::class;

        $d1 = Util::createCollectionItem($this->rootAnnotation->components, $collection, $class);
        $this->assertSame(0, $d1);
        $this->assertCount(1, $this->rootAnnotation->components->{$collection});
        $this->assertInstanceOf($class, $this->rootAnnotation->components->{$collection}[$d1]);
        $this->assertIsNested($this->rootAnnotation->components, $this->rootAnnotation->components->{$collection}[$d1]);
        $this->assertIsConnectedToRootContext($this->rootAnnotation->components->{$collection}[$d1]);
    }

    public function testCreateCollectionItemDoesNotAddToUnknownProperty()
    {
        $collection = 'foobars';
        $class = OA\Info::class;

        $expectedRegex = "/Property \"{$collection}\" doesn't exist .*/";
        set_error_handler(function ($_, $err) { echo $err; });
        $this->expectOutputRegex($expectedRegex);
        Util::createCollectionItem($this->rootAnnotation, $collection, $class);
        $this->expectOutputRegex($expectedRegex);
        $this->assertNull($this->rootAnnotation->{$collection});
        restore_error_handler();
    }

    public function testSearchCollectionItem()
    {
        $item1 = new \stdClass();
        $item1->prop1 = 'item 1 prop 1';
        $item1->prop2 = 'item 1 prop 2';
        $item1->prop3 = 'item 1 prop 3';

        $item2 = new \stdClass();
        $item2->prop1 = 'item 2 prop 1';
        $item2->prop2 = 'item 2 prop 2';
        $item2->prop3 = 'item 2 prop 3';

        $collection = [
            $item1,
            $item2,
        ];

        $this->assertSame(0, Util::searchCollectionItem($collection, get_object_vars($item1)));
        $this->assertSame(1, Util::searchCollectionItem($collection, get_object_vars($item2)));

        $this->assertNull(Util::searchCollectionItem(
            $collection,
            array_merge(get_object_vars($item2), ['prop3' => 'foobar'])
        ));

        $search = ['baz' => 'foobar'];
        $this->expectOutputString('Undefined property: stdClass::$baz');

        try {
            Util::searchCollectionItem($collection, array_merge(get_object_vars($item2), $search));
        } catch (\Exception $e) {
            echo $e->getMessage();
        }

        // no exception on empty collection
        $this->assertNull(Util::searchCollectionItem([], get_object_vars($item2)));
    }

    /**
     * @dataProvider provideIndexedCollectionData
     */
    public function testSearchIndexedCollectionItem($setup, $asserts)
    {
        foreach ($asserts as $collection => $items) {
            foreach ($items as $assert) {
                $setupCollection = empty($assert['components']) ?
                    ($setup[$collection] ?? []) :
                    (Generator::UNDEFINED !== $setup['components']->{$collection} ? $setup['components']->{$collection} : []);

                // get the indexing correct within haystack preparation
                $properties = array_fill(0, \count($setupCollection), null);

                // prepare the haystack array
                foreach ($items as $assertItem) {
                    // e.g. $properties[1] = new OA\PathItem(['path' => 'path 1'])
                    $properties[$assertItem['index']] = new $assertItem['class']([
                        $assertItem['key'] => $assertItem['value'],
                    ]);
                }

                $this->assertSame(
                    $assert['index'],
                    Util::searchIndexedCollectionItem($properties, $assert['key'], $assert['value']),
                    sprintf('Failed to get the correct index for %s', print_r($assert, true))
                );
            }
        }
    }

    /**
     * @dataProvider provideIndexedCollectionData
     */
    public function testGetIndexedCollectionItem($setup, $asserts)
    {
        $parent = new $setup['class'](array_merge(
            $this->getSetupPropertiesWithoutClass($setup),
            ['_context' => $this->rootContext]
        ));

        foreach ($asserts as $collection => $items) {
            foreach ($items as $assert) {
                $itemParent = empty($assert['components']) ? $parent : $parent->components;

                $child = Util::getIndexedCollectionItem(
                    $itemParent,
                    $assert['class'],
                    $assert['value']
                );

                $this->assertInstanceOf($assert['class'], $child);
                $this->assertSame($child->{$assert['key']}, $assert['value']);
                $this->assertSame(
                    $itemParent->{$collection}[$assert['index']],
                    $child
                );

                $setupHaystack = empty($assert['components']) ?
                    $setup[$collection] ?? [] :
                    $setup['components']->{$collection} ?? [];

                // the children created within provider are not connected
                if (!\in_array($child, $setupHaystack, true)) {
                    $this->assertIsNested($itemParent, $child);
                    $this->assertIsConnectedToRootContext($child);
                }
            }
        }
    }

    public function provideIndexedCollectionData(): array
    {
        return [[
            'setup' => [
                'class' => OA\OpenApi::class,
                'paths' => [
                    new OA\PathItem(['path' => 'path 0']),
                ],
                'components' => new OA\Components([
                    'parameters' => [
                        new OA\Parameter(['parameter' => 'parameter 0']),
                        new OA\Parameter(['parameter' => 'parameter 1']),
                    ],
                ]),
            ],
            'assert' => [
                // one fixed within setup and one dynamically created
                'paths' => [
                    [
                        'index' => 0,
                        'class' => OA\PathItem::class,
                        'key' => 'path',
                        'value' => 'path 0',
                    ],
                    [
                        'index' => 1,
                        'class' => OA\PathItem::class,
                        'key' => 'path',
                        'value' => 'path 1',
                    ],
                ],
                // search indexes out of order followed by dynamically created
                'parameters' => [
                    [
                        'index' => 1,
                        'class' => OA\Parameter::class,
                        'key' => 'parameter',
                        'value' => 'parameter 1',
                        'components' => true,
                    ],
                    [
                        'index' => 0,
                        'class' => OA\Parameter::class,
                        'key' => 'parameter',
                        'value' => 'parameter 0',
                        'components' => true,
                    ],
                    [
                        'index' => 2,
                        'class' => OA\Parameter::class,
                        'key' => 'parameter',
                        'value' => 'parameter 2',
                        'components' => true,
                    ],
                ],
                // two dynamically created
                'responses' => [
                    [
                        'index' => 0,
                        'class' => OA\Response::class,
                        'key' => 'response',
                        'value' => 'response 0',
                        'components' => true,
                    ],
                    [
                        'index' => 1,
                        'class' => OA\Response::class,
                        'key' => 'response',
                        'value' => 'response 1',
                        'components' => true,
                    ],
                ],
                // for sake of completeness
                'securitySchemes' => [
                    [
                        'index' => 0,
                        'class' => OA\SecurityScheme::class,
                        'key' => 'securityScheme',
                        'value' => 'securityScheme 0',
                        'components' => true,
                    ],
                ],
            ],
        ]];
    }

    /**
     * @dataProvider provideChildData
     */
    public function testGetChild($setup, $asserts)
    {
        $parent = new $setup['class'](array_merge(
            $this->getSetupPropertiesWithoutClass($setup),
            ['_context' => $this->rootContext]
        ));

        foreach ($asserts as $key => $assert) {
            if (array_key_exists('exceptionMessage', $assert)) {
                $this->expectExceptionMessage($assert['exceptionMessage']);
            }
            $child = Util::getChild($parent, $assert['class'], $assert['props']);

            $this->assertInstanceOf($assert['class'], $child);
            $this->assertSame($child, $parent->{$key});

            if (\array_key_exists($key, $setup)) {
                $this->assertSame($setup[$key], $parent->{$key});
            }

            $this->assertEquals($assert['props'], $this->getNonDefaultProperties($child));
        }
    }

    public function provideChildData(): array
    {
        return [[
            'setup' => [
                'class' => OA\PathItem::class,
                'get' => new OA\Get([]),
            ],
            'assert' => [
                // fixed within setup
                'get' => [
                    'class' => OA\Get::class,
                    'props' => [],
                ],
                // create new without props
                'put' => [
                    'class' => OA\Put::class,
                    'props' => [],
                ],
                // create new with multiple props
                'delete' => [
                    'class' => OA\Delete::class,
                    'props' => [
                        'summary' => 'testing delete',
                        'deprecated' => true,
                    ],
                ],
            ],
        ], [
            'setup' => [
                'class' => OA\Parameter::class,
            ],
            'assert' => [
                // create new with multiple props
                'schema' => [
                    'class' => OA\Schema::class,
                    'props' => [
                        'ref' => '#/testing/schema',
                        'minProperties' => 0,
                        'enum' => [null, 'check', 999, false],
                    ],
                ],
            ],
        ], [
            'setup' => [
                'class' => OA\Parameter::class,
            ],
            'assert' => [
                // externalDocs triggers invalid argument exception
                'schema' => [
                    'class' => OA\Schema::class,
                    'props' => [
                        'externalDocs' => [],
                    ],
                    'exceptionMessage' => 'Nesting Annotations is not supported.',
                ],
            ],
        ]];
    }

    public function testGetOperationParameterReturnsExisting()
    {
        $name = 'operation name';
        $in = 'operation in';

        $parameter = new OA\Parameter(['name' => $name, 'in' => $in]);
        $operation = new OA\Get(['parameters' => [
            new OA\Parameter([]),
            new OA\Parameter(['name' => 'foo']),
            new OA\Parameter(['in' => 'bar']),
            new OA\Parameter(['name' => $name, 'in' => 'bar']),
            new OA\Parameter(['name' => 'foo', 'in' => $in]),
            $parameter,
        ]]);

        $actual = Util::getOperationParameter($operation, $name, $in);
        $this->assertSame($parameter, $actual);
    }

    public function testGetOperationParameterCreatesWithNameAndIn()
    {
        $name = 'operation name';
        $in = 'operation in';

        $operation = new OA\Get(['parameters' => [
            new OA\Parameter([]),
            new OA\Parameter(['name' => 'foo']),
            new OA\Parameter(['in' => 'bar']),
            new OA\Parameter(['name' => $name, 'in' => 'bar']),
            new OA\Parameter(['name' => 'foo', 'in' => $in]),
        ]]);

        $actual = Util::getOperationParameter($operation, $name, $in);
        $this->assertInstanceOf(OA\Parameter::class, $actual);
        $this->assertSame($name, $actual->name);
        $this->assertSame($in, $actual->in);
    }

    public function testGetOperationReturnsExisting()
    {
        $get = new OA\Get([]);
        $path = new OA\PathItem(['get' => $get]);

        $this->assertSame($get, Util::getOperation($path, 'get'));
    }

    public function testGetOperationCreatesWithPath()
    {
        $pathStr = '/testing/get/path';
        $path = new OA\PathItem(['path' => $pathStr]);

        $get = Util::getOperation($path, 'get');
        $this->assertInstanceOf(OA\Get::class, $get);
        $this->assertSame($pathStr, $get->path);
    }

    public function testMergeWithEmptyArray()
    {
        $api = new OA\OpenApi([]);
        $expected = json_encode($api);

        Util::merge($api, [], false);
        $actual = json_encode($api);

        $this->assertSame($expected, $actual);

        Util::merge($api, [], true);
        $actual = json_encode($api);

        $this->assertSame($expected, $actual);
    }

    /**
     * @dataProvider provideMergeData
     */
    public function testMerge($setup, $merge, $assert)
    {
        $api = new OA\OpenApi($setup);

        Util::merge($api, $merge, false);
        $this->assertTrue($api->validate());
        $actual = json_decode(json_encode($api), true);

        $this->assertEquals($assert, $actual);
    }

    public function provideMergeData(): array
    {
        $no = 'do not overwrite';
        $yes = 'do overwrite';

        $requiredInfo = ['title' => '', 'version' => ''];

        $setupDefaults = [
            'info' => new OA\Info($requiredInfo),
            'paths' => [],
        ];
        $assertDefaults = [
            'info' => $requiredInfo,
            'openapi' => '3.0.0',
            'paths' => [],
        ];

        return [[
            // simple child merge
            'setup' => [
                'info' => new OA\Info(['version' => $no]),
                'paths' => [],
            ],
            'merge' => [
                'info' => ['title' => $yes, 'version' => $yes],
            ],
            'assert' => [
                    'info' => ['title' => $yes, 'version' => $no],
                ] + $assertDefaults,
        ], [
            // indexed collection merge
            'setup' => [
                    'components' => new OA\Components([
                        'schemas' => [
                            new OA\Schema(['schema' => $no, 'title' => $no]),
                        ],
                    ]),
                ] + $setupDefaults,
            'merge' => [
                'components' => [
                    'schemas' => [
                        $no => ['title' => $yes, 'description' => $yes],
                    ],
                ],
            ],
            'assert' => [
                    'components' => [
                        'schemas' => [
                            $no => ['title' => $no, 'description' => $yes],
                        ],
                    ],
                ] + $assertDefaults,
        ], [
            // collection merge
            'setup' => [
                    'tags' => [new OA\Tag(['name' => $no])],
                ] + $setupDefaults,
            'merge' => [
                'tags' => [
                    // this is actually appending right now, no clue if this is wanted,
                    // but the complete NelmioApiDocBundle test suite is not upset by this fact
                    ['name' => $yes],
                    // this should not append since a tag with exactly the same properties
                    // is already present
                    ['name' => $no],
                    // this does, but should not append since the name already exists, and the
                    // docs in Tag state that the tag names must be unique, but it is complicated
                    // and $api->validate() does not complain either
                    ['name' => $no, 'description' => $yes],
                ],
            ],
            'assert' => [
                    'tags' => [
                        ['name' => $no],
                        ['name' => $yes],
                        ['name' => $no, 'description' => $yes],
                    ],
                ] + $assertDefaults,
        ],
            [
                // heavy nested merge array
                'setup' => $setupDefaults,
                'merge' => $merge = [
                    'servers' => [
                        ['url' => 'http'],
                        ['url' => 'https'],
                    ],
                    'paths' => [
                        '/path/to/resource' => [
                            'get' => [
                                'responses' => [
                                    '200' => [
                                        '$ref' => '#/components/responses/default',
                                    ],
                                ],
                                'requestBody' => [
                                    'description' => 'request foo',
                                    'content' => [
                                        'foo-request' => [
                                            'schema' => [
                                                'type' => 'object',
                                                'required' => ['baz', 'bar'],
                                            ],
                                        ],
                                    ],
                                ],
                            ],
                        ],
                    ],
                    'tags' => [
                        ['name' => 'baz'],
                        ['name' => 'foo'],
                        ['name' => 'baz'],
                        ['name' => 'foo'],
                        ['name' => 'foo'],
                    ],
                    'components' => [
                        'responses' => [
                            'default' => [
                                'description' => 'default response',
                                'headers' => [
                                    'foo-header' => [
                                        'schema' => [
                                            'type' => 'array',
                                            'items' => [
                                                'type' => 'string',
                                                'enum' => ['foo', 'bar', 'baz'],
                                            ],
                                        ],
                                    ],
                                ],
                            ],
                        ],
                    ],
                ],
                'assert' => array_merge(
                    $assertDefaults,
                    $merge,
                    ['tags' => \array_slice($merge['tags'], 0, 2, true)]
                ),
            ], [
                // heavy nested merge array object
                'setup' => $setupDefaults,
                'merge' => new \ArrayObject([
                    'servers' => [
                        ['url' => 'http'],
                        ['url' => 'https'],
                    ],
                    'paths' => [
                        '/path/to/resource' => [
                            'get' => new \ArrayObject([
                                'responses' => [
                                    '200' => [
                                        '$ref' => '#/components/responses/default',
                                    ],
                                ],
                                'requestBody' => new \ArrayObject([
                                    'description' => 'request foo',
                                    'content' => [
                                        'foo-request' => [
                                            'schema' => [
                                                'required' => ['baz', 'bar'],
                                                'type' => 'object',
                                            ],
                                        ],
                                    ],
                                ]),
                            ]),
                        ],
                    ],
                    'tags' => new \ArrayObject([
                        ['name' => 'baz'],
                        ['name' => 'foo'],
                        new \ArrayObject(['name' => 'baz']),
                        ['name' => 'foo'],
                        ['name' => 'foo'],
                    ]),
                    'components' => new \ArrayObject([
                        'responses' => [
                            'default' => [
                                'description' => 'default response',
                                'headers' => new \ArrayObject([
                                    'foo-header' => new \ArrayObject([
                                        'schema' => new \ArrayObject([
                                            'type' => 'array',
                                            'items' => new \ArrayObject([
                                                'type' => 'string',
                                                'enum' => ['foo', 'bar', 'baz'],
                                            ]),
                                        ]),
                                    ]),
                                ]),
                            ],
                        ],
                    ]),
                ]),
                'assert' => array_merge(
                    $assertDefaults,
                    $merge,
                    ['tags' => \array_slice($merge['tags'], 0, 2, true)]
                ),
            ], [
                // heavy nested merge swagger instance
                'setup' => $setupDefaults,
                'merge' => new OA\OpenApi([
                    'servers' => [
                        new OA\Server(['url' => 'http']),
                        new OA\Server(['url' => 'https']),
                    ],
                    'paths' => [
                        new OA\PathItem([
                            'path' => '/path/to/resource',
                            'get' => new OA\Get([
                                'responses' => [
                                    new OA\Response([
                                        'response' => '200',
                                        'ref' => '#/components/responses/default',
                                    ]),
                                ],
                                'requestBody' => new OA\RequestBody([
                                    'description' => 'request foo',
                                    'content' => [
                                        new OA\MediaType([
                                            'mediaType' => 'foo-request',
                                            'schema' => new OA\Schema([
                                                'type' => 'object',
                                                'required' => ['baz', 'bar'],
                                            ]),
                                        ]),
                                    ],
                                ]),
                            ]),
                        ]),
                    ],
                    'tags' => [
                        new OA\Tag(['name' => 'baz']),
                        new OA\Tag(['name' => 'foo']),
                        new OA\Tag(['name' => 'baz']),
                        new OA\Tag(['name' => 'foo']),
                        new OA\Tag(['name' => 'foo']),
                    ],
                    'components' => new OA\Components([
                        'responses' => [
                            new OA\Response([
                                'response' => 'default',
                                'description' => 'default response',
                                'headers' => [
                                    new OA\Header([
                                        'header' => 'foo-header',
                                        'schema' => new OA\Schema([
                                            'type' => 'array',
                                            'items' => new OA\Items([
                                                'type' => 'string',
                                                'enum' => ['foo', 'bar', 'baz'],
                                            ]),
                                        ]),
                                    ]),
                                ],
                            ]),
                        ],
                    ]),
                ]),
                'assert' => array_merge(
                    $assertDefaults,
                    $merge,
                    ['tags' => \array_slice($merge['tags'], 0, 2, true)]
                ),
            ], ];
    }

    public function assertIsNested(OA\AbstractAnnotation $parent, OA\AbstractAnnotation $child)
    {
        self::assertTrue($child->_context->is('nested'));
        self::assertSame($parent, $child->_context->nested);
    }

    public function assertIsConnectedToRootContext(OA\AbstractAnnotation $annotation)
    {
        $this->assertContextIsConnectedToRootContext($annotation->_context);
    }

    public function assertContextIsConnectedToRootContext(Context $context)
    {
        // zircote/swagger-php < 4.2 support
        $getRootContext = \Closure::bind(function (Context $context) use (&$getRootContext) {
            if (null !== $context->_parent) {
                return $getRootContext($context->_parent);
            }

            return $context;
        }, null, Context::class);

        $this->assertSame($this->rootContext, $getRootContext($context));
    }

    private function getSetupPropertiesWithoutClass(array $setup)
    {
        return array_filter($setup, function ($k) {return 'class' !== $k; }, ARRAY_FILTER_USE_KEY);
    }

    private function getNonDefaultProperties($object)
    {
        $objectVars = \get_object_vars($object);
        $classVars = \get_class_vars(\get_class($object));
        $props = [];
        foreach ($objectVars as $key => $value) {
            if ($value !== $classVars[$key] && 0 !== \strpos($key, '_')) {
                $props[$key] = $value;
            }
        }

        return $props;
    }
}