<?php

namespace Tests\Unit\Framework;

use Openguru\OpenCartFramework\Container\Container;
use Openguru\OpenCartFramework\Exceptions\ContainerDependencyResolutionException;
use Symfony\Component\HttpFoundation\JsonResponse;
use stdClass;
use Tests\Helpers\ExampleClasses\TestClassWithMethod;
use Tests\TestCase;
use Tests\Helpers\ExampleClasses\ExampleDatabaseConnection;
use Tests\Helpers\ExampleClasses\ExampleSmsGateway;
use Tests\Helpers\ExampleClasses\ExamplePersonRepository;
use Tests\Helpers\ExampleClasses\ExamplePersonService;
use Tests\Helpers\ExampleClasses\ExampleEmailWithConfig;

class ContainerTest extends TestCase
{
    private $config = [
        'key' => 'value',
    ];

    private $container;

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

        $this->container = new Container($this->config);
    }

    public function testGetConfigValue()
    {
        $this->assertEquals('value', $this->container->getConfigValue('key'));

        $this->assertIsArray($this->container->getConfigValue());
    }

    public function testBind(): void
    {
        $this->container->bind('abstract', function () {});

        $this->assertTrue($this->container->has('abstract'));
    }

    public function testResolve(): void
    {
        $this->container->bind(ExampleDatabaseConnection::class, function () {
            return new ExampleDatabaseConnection();
        });

        $concrete1 = $this->container->get(ExampleDatabaseConnection::class);
        $concrete2 = $this->container->get(ExampleDatabaseConnection::class);

        $this->assertInstanceOf(ExampleDatabaseConnection::class, $concrete1);
        $this->assertInstanceOf(ExampleDatabaseConnection::class, $concrete2);
        $this->assertNotSame($concrete1, $concrete2);
    }

    public function testSingleton(): void
    {
        $this->container->singleton(ExampleDatabaseConnection::class, function () {
            return new ExampleDatabaseConnection();
        });

        $concrete1 = $this->container->get(ExampleDatabaseConnection::class);
        $concrete2 = $this->container->get(ExampleDatabaseConnection::class);

        $this->assertInstanceOf(ExampleDatabaseConnection::class, $concrete1);
        $this->assertInstanceOf(ExampleDatabaseConnection::class, $concrete2);
        $this->assertSame($concrete1, $concrete2);
    }

    public function testDeepResolve(): void
    {
        $container = new Container([]);

        $container->bind(ExampleSmsGateway::class, function () { return new ExampleSmsGateway(); });
        $container->bind(ExampleDatabaseConnection::class, function () { return new ExampleDatabaseConnection(); });
        $container->bind(ExamplePersonRepository::class, function (Container $container) {
            return new ExamplePersonRepository(
                $container->get(ExampleDatabaseConnection::class),
            );
        });
        $container->bind(ExamplePersonService::class, function (Container $container) {
            return new ExamplePersonService(
                $container->get(ExamplePersonRepository::class),
                $container->get(ExampleSmsGateway::class),
            );
        });

        $personService = $container->get(ExamplePersonService::class);

        $this->assertInstanceOf(ExamplePersonService::class, $personService);
        $this->assertInstanceOf(ExamplePersonRepository::class, $personService->personRepository);
        $this->assertInstanceOf(ExampleSmsGateway::class, $personService->smsGateway);
    }

    public function testAutoResolve(): void
    {
        $personService = $this->container->get(ExamplePersonService::class);

        $this->assertInstanceOf(ExamplePersonService::class, $personService);
        $this->assertInstanceOf(ExamplePersonRepository::class, $personService->personRepository);
        $this->assertInstanceOf(ExampleSmsGateway::class, $personService->smsGateway);
    }

    public function testAutoResolveFailed(): void
    {
        $this->expectException(ContainerDependencyResolutionException::class);

        $response = $this->container->get(ExampleEmailWithConfig::class);

        $this->assertInstanceOf(ExampleEmailWithConfig::class, $response);
    }

    public function testAutoResolveWithCustomParams(): void
    {
        $container = new Container([]);

        $container->bind(
            ExampleEmailWithConfig::class,
            function () {
                return new ExampleEmailWithConfig(['foo' => 'bar'], 10);
            }
        );

        /** @var ExampleEmailWithConfig $emailWithConfig */
        $emailWithConfig = $container->get(ExampleEmailWithConfig::class);

        $this->assertInstanceOf(ExampleEmailWithConfig::class, $emailWithConfig);
        $this->assertEquals(['foo' => 'bar'], $emailWithConfig->config);
        $this->assertEquals(10, $emailWithConfig->rateLimit);
    }

    public function testCallMethodWithDependencies(): void
    {
        $this->container->bind(stdClass::class, function () {
            return new stdClass();
        });

        $response = $this->container->call(TestClassWithMethod::class, 'testMethod');
        $this->assertInstanceOf(JsonResponse::class, $response);
    }
}