<?php
/**
 * For the full copyright and license information, please view the
 * docs/licenses/LICENSE.txt file that was distributed with this source code.
 */

declare(strict_types=1);

namespace Tests\Unit\Core\Foundation\IoC;

use Exception;
use PHPUnit\Framework\TestCase;
use PrestaShop\PrestaShop\Core\Foundation\IoC\Container;
use Tests\Unit\Core\Foundation\IoC\Fixtures\DepBuiltByClosure;
use Tests\Unit\Core\Foundation\IoC\Fixtures\Dummy;

class ContainerTest extends TestCase
{
    /**
     * @var Container
     */
    private $container;

    protected function setUp(): void
    {
        $this->container = new Container();
    }

    public function testBindByClosure()
    {
        $this->container->bind('foo', function () {
            return 'FOO';
        });

        $this->assertEquals('FOO', $this->container->make('foo'));
    }

    public function testCannotBindTheSameServiceTwice()
    {
        $this->expectException(\PrestaShop\PrestaShop\Core\Foundation\IoC\Exception::class);

        $this->container->bind('foo', function () {});
        $this->container->bind('foo', function () {});
    }

    public function testBindByClosureInstanceNotSharedByDefault()
    {
        $this->container->bind('different', function () {
            return new Dummy();
        });

        $first = $this->container->make('different');
        $second = $this->container->make('different');

        $this->assertNotSame($first, $second);
    }

    public function testBindByClosureInstanceSharedIfExplicitelyRequired()
    {
        $this->container->bind('same', function () {
            return new Dummy();
        }, true);

        $first = $this->container->make('same');
        $second = $this->container->make('same');

        $this->assertSame($first, $second);
    }

    public function testBindClassName()
    {
        $this->container->bind('dummy', 'Tests\Unit\Core\Foundation\IoC\Fixtures\Dummy');

        $this->assertEquals('Tests\Unit\Core\Foundation\IoC\Fixtures\Dummy', get_class(
            $this->container->make('dummy')
        ));
    }

    public function testMakeWithoutBind()
    {
        $this->assertEquals('Tests\Unit\Core\Foundation\IoC\Fixtures\Dummy', get_class(
            $this->container->make('Tests\Unit\Core\Foundation\IoC\Fixtures\Dummy')
        ));
    }

    public function testClassesCanBeLoadedWithCustomNamespacePrefix()
    {
        $this->container->aliasNamespace('Fixtures', 'Tests\Unit\Core\Foundation\IoC\Fixtures');

        $this->assertEquals('Tests\Unit\Core\Foundation\IoC\Fixtures\Dummy', get_class(
            $this->container->make('Fixtures:Dummy')
        ));
    }

    public function testAnAliasCannotBeChanged()
    {
        $this->expectException(\PrestaShop\PrestaShop\Core\Foundation\IoC\Exception::class);

        $this->container->aliasNamespace('Fixtures', 'Tests\Unit\Core\Foundation\IoC\Fixtures');
        $this->container->aliasNamespace('Fixtures', 'Tests\Unit\Core\Foundation\Other');
    }

    public function testDepsAreFetchedAutomagically()
    {
        $this->assertEquals('Tests\Unit\Core\Foundation\IoC\Fixtures\ClassWithDep', get_class(
            $this->container->make('Tests\Unit\Core\Foundation\IoC\Fixtures\ClassWithDep')
        ));
    }

    public function testDepsAreFetchedAutomagicallyWhenDependsOnThingWithADefaultValue()
    {
        $this->assertEquals('Tests\Unit\Core\Foundation\IoC\Fixtures\ClassWithDepAndDefault', get_class(
            $this->container->make('Tests\Unit\Core\Foundation\IoC\Fixtures\ClassWithDepAndDefault')
        ));
    }

    public function testUnbuildableNotBuilt()
    {
        $this->expectException(\PrestaShop\PrestaShop\Core\Foundation\IoC\Exception::class);

        $this->container->make('Tests\Unit\Core\Foundation\IoC\Fixtures\UnBuildable');
    }

    public function testNonExistingClassNotBuilt()
    {
        $this->expectException(\PrestaShop\PrestaShop\Core\Foundation\IoC\Exception::class);

        $this->container->make('Tests\Unit\Core\Foundation\IoC\Fixtures\AClassThatDoesntExistAtAll');
    }

    public function testDependencyLoopDoesntCrashContainer()
    {
        $this->expectException(\PrestaShop\PrestaShop\Core\Foundation\IoC\Exception::class);

        /*
         * CycleA depends on CycleB,
         * CycleB depends on CycleA
         */
        $this->container->make('Tests\Unit\Core\Foundation\IoC\Fixtures\CycleA');
    }

    public function testCanBuildClassWhoseDependencyIsBuitByClosure()
    {
        $this->container->bind(
            'Tests\Unit\Core\Foundation\IoC\Fixtures\DepBuiltByClosure',
            function () {
                return new DepBuiltByClosure(42);
            }
        );

        $instance = $this->container->make(
            'Tests\Unit\Core\Foundation\IoC\Fixtures\ClassDependingOnClosureBuiltDep'
        );
        $this->assertEquals(42, $instance->getDep()->getValue());
    }

    /**
     * data provider for test_container_can_bind_values_directly
     */
    public function valuesToBind()
    {
        return [
            [new Dummy()],
            [42],
            [[1, 2, 3]],
        ];
    }

    /**
     * @dataProvider valuesToBind
     */
    public function testContainerCanBindValuesDirectly($value)
    {
        $this->container->bind('value', $value);
        $this->assertSame($value, $this->container->make('value'));
    }

    public function testContainerDoesntBindStringsAsLiteralValues()
    {
        $this->expectException(Exception::class);

        $this->container->bind('value', 'a string which is not a class name');
        $this->container->make('value');
    }
}
