<?php

namespace Uid\Utils\Container;

use PHPUnit\Framework\TestCase;
use Psr\Container\ContainerInterface AS PsrContainerInterface;

class ContainerTest extends TestCase
{
    public function testConstruct()
    {
        $container = new Container(['bar' => ['value' => 'foo']]);
        $this->assertEquals('foo', $container->get('bar'));
        $this->assertSame($container, $container->get(Container::class));
        $this->assertSame($container, $container->get(ContainerInterface::class));
        $this->assertSame($container, $container->get(PsrContainerInterface::class));
    }
    
    public function testValue()
    {
        $container = (new Container())->value('value', 'VALUE');
        $this->assertEquals('VALUE', $container->get('value'));
    } 
    
    public function testAlias()
    {
        $container = (new Container())
                ->value('value', 'VALUE')
                ->alias('alias1', 'value')
                ->alias('alias2', 'value');
        $this->assertEquals('VALUE', $container->get('alias1'));
        $this->assertEquals('VALUE', $container->get('alias2'));
    }
    
    public function testCreate()
    {
        $container = (new Container())->create(ContainerTest::class);
        $this->assertInstanceOf(ContainerTest::class, $container->get(ContainerTest::class));
    }
    
    public function testSet()
    {
        $container = (new Container())->set('Service', ContainerTest::class);
        $this->assertInstanceOf(ContainerTest::class, $container->get('Service'));
    }
    
    public function testFactory()
    {
        $container = (new Container())
                ->factory(ContainerTest::class, fn() => new ContainerTest(), [], true)
                ->factory('Service1', fn(ContainerInterface $container) => $container->get(ContainerTest::class))
                ->factory('Service2', fn(ContainerTest $class) => $class)
                ->factory('Service3', fn(Container $class) => $class);

        $this->assertInstanceOf(ContainerTest::class, $container->get(ContainerTest::class));
        $this->assertInstanceOf(ContainerTest::class, $container->get('Service1'));
        $this->assertInstanceOf(ContainerTest::class, $container->get('Service2'));
        $this->assertInstanceOf(Container::class, $container->get('Service3'));
    }
    
    public function testSingleton()
    {
        $container = (new Container())
            ->factory(
                'foo',
                function() {
                    static $count = 0;
                    return ++$count;
                },
                [],
                true
            );

        $this->assertEquals(1, $container->get('foo'));
        $this->assertEquals(1, $container->get('foo'));
        $this->assertSame($container->get('foo'), $container->get('foo'));
    }
    
    public function testGetThrowCorrectException()
    {
        $this->expectException(\Psr\Container\ContainerExceptionInterface::class);
        $container = (new Container())->set('foo', function() {
            throw new \RuntimeException();
        });
        $container->get('foo');
    }
    
    public function testNotFound()
    {
        $this->expectException(NotFoundException::class);
        $container = new Container();
        $container->get('nonexistent');
    }

    public function testInvoke()
    {
        $container = (new Container())
            ->factory('foo', fn() => new class() {
                function getValue(ContainerInterface $c) { return 'Hello '. get_class($c); }
            });
        $result = $container->invoke('foo', 'getValue');
        $this->assertEquals('Hello '.Container::class, $result);
    }

    public function testInvokeMethodNotFound()
    {
        $this->expectException(NotFoundException::class);
        $container = new Container();
        $container->invoke(Container::class, 'nonexistentMethod');
    }
    
    public function testInvokeMethodWithDependency()
    {
        $container = (new Container())->factory('foo', fn() => new class() {
            function concat(ContainerTest $bar, $data, $end = '!')
            {
                return "Hello {$data}, ". get_class($bar). $end;
            }
        });
        $result = $container->invoke('foo', 'concat', ['data' => 'hire']);
        $this->assertEquals('Hello hire, '.self::class.'!', $result);
    }
    
    public function testInvokeMethodWithNonObjectEntry()
    {
        $this->expectException(ContainerException::class);

        $container = new Container();
        $container->invoke('nonexistent', 'someMethod');
    }
}
