<?php

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

namespace Symfony\Component\DependencyInjection\Tests\Compiler;

use PHPUnit\Framework\TestCase;
use Symfony\Component\Config\Resource\ResourceInterface;
use Symfony\Component\Config\ResourceCheckerInterface;
use Symfony\Component\DependencyInjection\Argument\BoundArgument;
use Symfony\Component\DependencyInjection\ChildDefinition;
use Symfony\Component\DependencyInjection\Compiler\ResolveChildDefinitionsPass;
use Symfony\Component\DependencyInjection\Compiler\ResolveInstanceofConditionalsPass;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException;
use Symfony\Component\DependencyInjection\Exception\RuntimeException;
use Symfony\Component\DependencyInjection\Reference;
use Symfony\Contracts\Service\ResetInterface;
use Symfony\Contracts\Service\ServiceSubscriberInterface;

class ResolveInstanceofConditionalsPassTest extends TestCase
{
    public function testProcess()
    {
        $container = new ContainerBuilder();
        $def = $container->register('foo', self::class)->addTag('tag')->setAutowired(true)->setChanges([]);
        $def->setInstanceofConditionals([
            parent::class => (new ChildDefinition(''))->setProperty('foo', 'bar')->addTag('baz', ['attr' => 123]),
        ]);

        (new ResolveInstanceofConditionalsPass())->process($container);

        $parent = '.instanceof.'.parent::class.'.0.foo';
        $def = $container->getDefinition('foo');
        $this->assertEmpty($def->getInstanceofConditionals());
        $this->assertInstanceOf(ChildDefinition::class, $def);
        $this->assertTrue($def->isAutowired());
        $this->assertSame($parent, $def->getParent());
        $this->assertSame(['tag' => [[]], 'baz' => [['attr' => 123]]], $def->getTags());

        $parent = $container->getDefinition($parent);
        $this->assertSame(['foo' => 'bar'], $parent->getProperties());
        $this->assertSame([], $parent->getTags());
    }

    public function testProcessInheritance()
    {
        $container = new ContainerBuilder();

        $def = $container
            ->register('parent', parent::class)
            ->addMethodCall('foo', ['foo']);
        $def->setInstanceofConditionals([
            parent::class => (new ChildDefinition(''))->addMethodCall('foo', ['bar']),
        ]);

        $def = (new ChildDefinition('parent'))->setClass(self::class);
        $container->setDefinition('child', $def);

        (new ResolveInstanceofConditionalsPass())->process($container);
        (new ResolveChildDefinitionsPass())->process($container);

        $expected = [
            ['foo', ['bar']],
            ['foo', ['foo']],
        ];

        $this->assertSame($expected, $container->getDefinition('parent')->getMethodCalls());
        $this->assertSame($expected, $container->getDefinition('child')->getMethodCalls());
    }

    public function testProcessDoesReplaceShared()
    {
        $container = new ContainerBuilder();

        $def = $container->register('foo', 'stdClass');
        $def->setInstanceofConditionals([
            'stdClass' => (new ChildDefinition(''))->setShared(false),
        ]);

        (new ResolveInstanceofConditionalsPass())->process($container);

        $def = $container->getDefinition('foo');
        $this->assertFalse($def->isShared());
    }

    public function testProcessHandlesMultipleInheritance()
    {
        $container = new ContainerBuilder();

        $def = $container->register('foo', self::class)->setShared(true);

        $def->setInstanceofConditionals([
            parent::class => (new ChildDefinition(''))->setLazy(true)->setShared(false),
            self::class => (new ChildDefinition(''))->setAutowired(true),
        ]);

        (new ResolveInstanceofConditionalsPass())->process($container);
        (new ResolveChildDefinitionsPass())->process($container);

        $def = $container->getDefinition('foo');
        $this->assertTrue($def->isAutowired());
        $this->assertTrue($def->isLazy());
        $this->assertTrue($def->isShared());
    }

    public function testProcessUsesAutoconfiguredInstanceof()
    {
        $container = new ContainerBuilder();
        $def = $container->register('normal_service', self::class);
        $def->setInstanceofConditionals([
            parent::class => (new ChildDefinition(''))
                ->addTag('local_instanceof_tag')
                ->setFactory('locally_set_factory'),
        ]);
        $def->setAutoconfigured(true);
        $container->registerForAutoconfiguration(parent::class)
            ->addTag('autoconfigured_tag')
            ->setAutowired(true)
            ->setFactory('autoconfigured_factory');

        (new ResolveInstanceofConditionalsPass())->process($container);
        (new ResolveChildDefinitionsPass())->process($container);

        $def = $container->getDefinition('normal_service');
        // autowired thanks to the autoconfigured instanceof
        $this->assertTrue($def->isAutowired());
        // factory from the specific instanceof overrides global one
        $this->assertEquals('locally_set_factory', $def->getFactory());
        // tags are merged, the locally set one is first
        $this->assertSame(['local_instanceof_tag' => [[]], 'autoconfigured_tag' => [[]]], $def->getTags());
    }

    public function testAutoconfigureInstanceofDoesNotDuplicateTags()
    {
        $container = new ContainerBuilder();
        $def = $container->register('normal_service', self::class);
        $def
            ->addTag('duplicated_tag')
            ->addTag('duplicated_tag', ['and_attributes' => 1])
        ;
        $def->setInstanceofConditionals([
            parent::class => (new ChildDefinition(''))->addTag('duplicated_tag'),
        ]);
        $def->setAutoconfigured(true);
        $container->registerForAutoconfiguration(parent::class)
            ->addTag('duplicated_tag', ['and_attributes' => 1])
        ;

        (new ResolveInstanceofConditionalsPass())->process($container);
        (new ResolveChildDefinitionsPass())->process($container);

        $def = $container->getDefinition('normal_service');
        $this->assertSame(['duplicated_tag' => [[], ['and_attributes' => 1]]], $def->getTags());
    }

    public function testProcessDoesNotUseAutoconfiguredInstanceofIfNotEnabled()
    {
        $container = new ContainerBuilder();
        $def = $container->register('normal_service', self::class);
        $def->setInstanceofConditionals([
            parent::class => (new ChildDefinition(''))
                ->addTag('foo_tag'),
        ]);
        $container->registerForAutoconfiguration(parent::class)
            ->setAutowired(true);

        (new ResolveInstanceofConditionalsPass())->process($container);
        (new ResolveChildDefinitionsPass())->process($container);

        $def = $container->getDefinition('normal_service');
        $this->assertFalse($def->isAutowired());
    }

    public function testBadInterfaceThrowsException()
    {
        $this->expectException(RuntimeException::class);
        $this->expectExceptionMessage('"App\FakeInterface" is set as an "instanceof" conditional, but it does not exist.');
        $container = new ContainerBuilder();
        $def = $container->register('normal_service', self::class);
        $def->setInstanceofConditionals([
            'App\\FakeInterface' => (new ChildDefinition(''))
                ->addTag('foo_tag'),
        ]);

        (new ResolveInstanceofConditionalsPass())->process($container);
    }

    public function testBadInterfaceForAutomaticInstanceofIsOk()
    {
        $container = new ContainerBuilder();
        $container->register('normal_service', self::class)
            ->setAutoconfigured(true);
        $container->registerForAutoconfiguration('App\\FakeInterface')
            ->setAutowired(true);

        (new ResolveInstanceofConditionalsPass())->process($container);
        $this->assertTrue($container->hasDefinition('normal_service'));
    }

    /**
     * Test that autoconfigured calls are handled gracefully.
     */
    public function testProcessForAutoconfiguredCalls()
    {
        $container = new ContainerBuilder();

        $expected = [
            ['setFoo', [
                'plain_value',
                '%some_parameter%',
            ]],
            ['callBar', []],
            ['isBaz', []],
        ];

        $container->registerForAutoconfiguration(parent::class)->addMethodCall('setFoo', $expected[0][1]);
        $container->registerForAutoconfiguration(self::class)->addMethodCall('callBar');

        $def = $container->register('foo', self::class)->setAutoconfigured(true)->addMethodCall('isBaz');
        $this->assertEquals(
            [['isBaz', []]],
            $def->getMethodCalls(),
            'Definition shouldn\'t have only one method call.'
        );

        (new ResolveInstanceofConditionalsPass())->process($container);

        $this->assertEquals($expected, $container->findDefinition('foo')->getMethodCalls());
    }

    public function testProcessThrowsExceptionForArguments()
    {
        $this->expectException(InvalidArgumentException::class);
        $this->expectExceptionMessageMatches('/Autoconfigured instanceof for type "PHPUnit[\\\\_]Framework[\\\\_]TestCase" defines arguments but these are not supported and should be removed\./');
        $container = new ContainerBuilder();
        $container->registerForAutoconfiguration(parent::class)
            ->addArgument('bar');

        (new ResolveInstanceofConditionalsPass())->process($container);
    }

    public function testMergeReset()
    {
        $container = new ContainerBuilder();

        $container
            ->register('bar', self::class)
            ->addArgument('a')
            ->addMethodCall('setB')
            ->setDecoratedService('foo')
            ->addTag('t')
            ->setInstanceofConditionals([
                parent::class => (new ChildDefinition(''))->addTag('bar'),
            ])
        ;

        (new ResolveInstanceofConditionalsPass())->process($container);

        $abstract = $container->getDefinition('.abstract.instanceof.bar');

        $this->assertEmpty($abstract->getArguments());
        $this->assertEmpty($abstract->getMethodCalls());
        $this->assertNull($abstract->getDecoratedService());
        $this->assertEmpty($abstract->getTags());
        $this->assertTrue($abstract->isAbstract());
    }

    public function testProcessForAutoconfiguredBindings()
    {
        $container = new ContainerBuilder();

        $container->registerForAutoconfiguration(self::class)
            ->setBindings([
                '$foo' => new BoundArgument(234, false),
                parent::class => new BoundArgument(new Reference('foo'), false),
            ]);

        $container->register('foo', self::class)
            ->setAutoconfigured(true)
            ->setBindings(['$foo' => new BoundArgument(123, false)]);

        (new ResolveInstanceofConditionalsPass())->process($container);

        $expected = [
            '$foo' => new BoundArgument(123, false),
            parent::class => new BoundArgument(new Reference('foo'), false),
        ];
        $this->assertEquals($expected, $container->findDefinition('foo')->getBindings());
    }

    public function testBindingsOnInstanceofConditionals()
    {
        $container = new ContainerBuilder();
        $def = $container->register('foo', self::class)->setBindings(['$toto' => 123]);
        $def->setInstanceofConditionals([parent::class => new ChildDefinition('')]);

        (new ResolveInstanceofConditionalsPass())->process($container);

        $bindings = $container->getDefinition('foo')->getBindings();
        $this->assertSame(['$toto'], array_keys($bindings));
        $this->assertInstanceOf(BoundArgument::class, $bindings['$toto']);
        $this->assertSame(123, $bindings['$toto']->getValues()[0]);
    }

    public function testDecoratorsAreNotAutomaticallyTagged()
    {
        $container = new ContainerBuilder();

        $decorator = $container->register('decorator', self::class);
        $decorator->setDecoratedService('decorated');
        $decorator->setInstanceofConditionals([
            parent::class => (new ChildDefinition(''))->addTag('tag'),
            self::class => (new ChildDefinition(''))->addTag('other-tag'),
        ]);
        $decorator->setAutoconfigured(true);
        $decorator->addTag('manual');

        $container->registerForAutoconfiguration(parent::class)
            ->addTag('tag')
        ;
        $container->registerForAutoconfiguration(self::class)
            ->addTag('last-tag')
        ;

        (new ResolveInstanceofConditionalsPass())->process($container);
        (new ResolveChildDefinitionsPass())->process($container);

        $this->assertSame([
            'manual' => [[]],
            'other-tag' => [[]],
            'last-tag' => [[]],
        ], $container->getDefinition('decorator')->getTags());
    }

    public function testDecoratorsKeepBehaviorDescribingTags()
    {
        $container = new ContainerBuilder();

        $container->setParameter('container.behavior_describing_tags', [
            'container.service_subscriber',
            'kernel.reset',
        ]);

        $container->register('decorator', DecoratorWithBehavior::class)
            ->setAutoconfigured(true)
            ->setDecoratedService('decorated')
        ;

        $container->registerForAutoconfiguration(ResourceCheckerInterface::class)
            ->addTag('config_cache.resource_checker')
        ;
        $container->registerForAutoconfiguration(ServiceSubscriberInterface::class)
            ->addTag('container.service_subscriber')
        ;
        $container->registerForAutoconfiguration(ResetInterface::class)
            ->addTag('kernel.reset', ['method' => 'reset'])
        ;

        (new ResolveInstanceofConditionalsPass())->process($container);

        $this->assertEquals([
            'container.service_subscriber' => [0 => []],
            'kernel.reset' => [
                [
                    'method' => 'reset',
                ],
            ],
        ], $container->getDefinition('decorator')->getTags());
        $this->assertFalse($container->hasParameter('container.behavior_describing_tags'));
    }
}

class DecoratorWithBehavior implements ResetInterface, ResourceCheckerInterface, ServiceSubscriberInterface
{
    public function reset()
    {
    }

    public function supports(ResourceInterface $metadata): bool
    {
    }

    public function isFresh(ResourceInterface $resource, $timestamp): bool
    {
    }

    public static function getSubscribedServices(): array
    {
    }
}
