<?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\Attributes\DataProvider;
use PHPUnit\Framework\Attributes\Group;
use PHPUnit\Framework\Attributes\IgnoreDeprecations;
use PHPUnit\Framework\TestCase;
use Symfony\Bridge\PhpUnit\ExpectDeprecationTrait;
use Symfony\Component\DependencyInjection\ChildDefinition;
use Symfony\Component\DependencyInjection\Compiler\ResolveClassPass;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException;
use Symfony\Component\DependencyInjection\Tests\Fixtures\CaseSensitiveClass;

class ResolveClassPassTest extends TestCase
{
    use ExpectDeprecationTrait;

    #[DataProvider('provideValidClassId')]
    public function testResolveClassFromId($serviceId)
    {
        $container = new ContainerBuilder();
        $def = $container->register($serviceId);

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

        $this->assertSame($serviceId, $def->getClass());
    }

    public static function provideValidClassId()
    {
        yield [CaseSensitiveClass::class];
    }

    #[DataProvider('provideInvalidClassId')]
    public function testWontResolveClassFromId($serviceId)
    {
        $container = new ContainerBuilder();
        $def = $container->register($serviceId);

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

        $this->assertNull($def->getClass());
    }

    public static function provideInvalidClassId()
    {
        yield [\stdClass::class];
        yield ['bar'];
        yield [\DateTimeImmutable::class];
    }

    public function testNonFqcnChildDefinition()
    {
        $container = new ContainerBuilder();
        $parent = $container->register('App\Foo.parent', 'App\Foo');
        $child = $container->setDefinition('App\Foo.child', new ChildDefinition('App\Foo'));

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

        $this->assertSame('App\Foo', $parent->getClass());
        $this->assertNull($child->getClass());
    }

    public function testClassFoundChildDefinition()
    {
        $container = new ContainerBuilder();
        $parent = $container->register('foo.parent', 'App\Foo');
        $child = $container->setDefinition(self::class, new ChildDefinition('App\Foo'));

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

        $this->assertSame('App\Foo', $parent->getClass());
        $this->assertSame(self::class, $child->getClass());
    }

    public function testAmbiguousChildDefinition()
    {
        $this->expectException(InvalidArgumentException::class);
        $this->expectExceptionMessage('Service definition "App\Foo\Child" has a parent but no class, and its name looks like a FQCN. Either the class is missing or you want to inherit it from the parent service. To resolve this ambiguity, please rename this service to a non-FQCN (e.g. using dots), or create the missing class.');
        $container = new ContainerBuilder();
        $container->register('app.foo', 'App\Foo');
        $container->setDefinition('App\Foo\Child', new ChildDefinition('App\Foo'));

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

    public function testSkipsDefinitionsWithErrors()
    {
        $container = new ContainerBuilder();
        $def = $container->register('App\SomeClass');
        $def->addError('Some error message');

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

        // The class should not be set because the definition has errors
        $this->assertNull($def->getClass());
    }

    #[IgnoreDeprecations]
    #[Group('legacy')]
    public function testInvalidClassNameDefinition()
    {
        // $this->expectException(InvalidArgumentException::class);
        // $this->expectExceptionMessage('Service id "Acme\UnknownClass" looks like a FQCN but no corresponding class or interface exists. To resolve this ambiguity, please rename this service to a non-FQCN (e.g. using dots), or create the missing class or interface.');
        $this->expectUserDeprecationMessage('Since symfony/dependency-injection 7.4: Service id "Acme\UnknownClass" looks like a FQCN but no corresponding class or interface exists. To resolve this ambiguity, please rename this service to a non-FQCN (e.g. using dots), or create the missing class or interface.');
        $container = new ContainerBuilder();
        $container->register('Acme\UnknownClass');

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

    #[IgnoreDeprecations]
    #[Group('legacy')]
    public function testInvalidClassWhoseImplementedInterfaceIsMissingDefinition()
    {
        $this->expectUserDeprecationMessage('Since symfony/dependency-injection 7.4: Service id "Acme\ClassImplementsUnavailableInterface" looks like a FQCN but no corresponding class or interface exists. To resolve this ambiguity, please rename this service to a non-FQCN (e.g. using dots), or create the missing class or interface.');

        $autoloader = static function (string $class) {
            if ('Acme\ClassImplementsUnavailableInterface' === $class) {
                new class implements UnavailableInterface {};
            }
        };

        spl_autoload_register($autoloader);

        try {
            $container = new ContainerBuilder();
            $container->register('Acme\ClassImplementsUnavailableInterface');

            (new ResolveClassPass())->process($container);
        } finally {
            spl_autoload_unregister($autoloader);
        }
    }
}
