<?php
// DependenciesTest.php
// Created: 2025-01-21
// Updated: 2025-03-21

declare(strict_types=1);

namespace {
    use PHPUnit\Framework\TestCase;
    use PHPUnit\Framework\Attributes\{CoversClass,UsesClass};
    use Index\Dependencies;

    #[CoversClass(Dependencies::class)]
    final class DependenciesTest extends TestCase {
        public function testDependencyCall(): void {
            $deps = new Dependencies;
            $deps->register(\Index\Db\DbResultIterator::class, construct: fn($result) => null);
            $deps->register(\Index\Db\NullDb\NullDbResult::class);

            $func = function(
                \Index\Db\DbResult $result,
                string $soup,
                ?\Index\Db\DbConnection $conn,
                string $named = '',
                ?\Index\Db\NullDb\NullDbTransaction $transaction = null
            ) {
                return $result instanceof \Index\Db\NullDb\NullDbResult
                    && $soup === 'beans'
                    && $conn === null
                    && $named === 'bowl'
                    && $transaction === null;
            };
            $this->assertTrue($deps->call($func, 'beans', named: 'bowl'));

            $outOfOrder = function(
                string $soup,
                \Index\Db\DbResult $result,
                string $named = '',
                ?\Index\Db\DbResultIterator $iterator = null
            ) {
                return $result instanceof \Index\Db\NullDb\NullDbResult
                    && $soup === 'beans'
                    && $named === 'bowl'
                    && $iterator instanceof \Index\Db\DbResultIterator;
            };
            $this->assertTrue($deps->call($outOfOrder, 'beans', named: 'bowl'));
        }

        public function testDependencyResolver(): void {
            $deps = new Dependencies;
            $deps->register(\Index\Db\DbResultIterator::class, construct: fn($result) => null);
            $deps->register(\Index\Db\NullDb\NullDbResult::class);

            $iterator = $deps->resolve(\Index\Db\DbResultIterator::class);
            $this->assertInstanceOf(\Index\Db\DbResultIterator::class, $iterator);
            $this->assertFalse($iterator->valid());

            $result = $deps->resolve(\Index\Db\NullDb\NullDbResult::class);
            $this->assertInstanceOf(\Index\Db\DbResult::class, $result);
        }

        public function testRequiredDependency(): void {
            $this->expectException(TypeError::class);

            $deps = new Dependencies;
            $deps->register(DependenciesTest\RequiredParamTestClass::class);
            $deps->resolve(DependenciesTest\RequiredParamTestClass::class);
            $instance = $deps->resolve(DependenciesTest\RequiredParamTestClass::class);
            $this->assertInstanceOf(DependenciesTest\RequiredParamTestClass::class, $instance);

            // this is the one that actually triggers the exception because of lazy instantiation
            $this->assertInstanceOf(DependenciesTest\RequiredParamTestClass::class, $instance->test);
        }

        public function testNullableDependency(): void {
            $deps = new Dependencies;
            $deps->register(DependenciesTest\NullableParamTestClass::class);
            $instance = $deps->resolve(DependenciesTest\NullableParamTestClass::class);
            $this->assertInstanceOf(DependenciesTest\NullableParamTestClass::class, $instance);
            $this->assertNull($instance->test);
        }

        public function testFetchAllDependencies(): void {
            $deps = new Dependencies;
            $deps->register(\Index\Db\NullDb\NullDbResult::class);
            $deps->register(DependenciesTest\NullableParamTestClass::class);
            $deps->register(DependenciesTest\Routes1::class);
            $deps->register(new DependenciesTest\Routes2);
            $deps->register(DependenciesTest\RequiredParamTestClass::class);
            $deps->register(DependenciesTest\Routes3::class);
            $deps->register(\Index\Db\DbResultIterator::class, construct: fn($result) => null);

            $routes3 = $deps->resolve(\Index\Http\Routing\RouteHandler::class, fn($object) => $object instanceof DependenciesTest\Routes3);
            $this->assertInstanceOf(DependenciesTest\Routes3::class, $routes3);

            $routes2 = $deps->resolve(DependenciesTest\Routes2::class);
            $this->assertInstanceOf(DependenciesTest\Routes2::class, $routes2);

            $all = $deps->all(\Index\Http\Routing\RouteHandler::class);
            $this->assertEquals(3, count($all));
            $this->assertInstanceOf(DependenciesTest\Routes1::class, $all[0]);
            $this->assertInstanceOf(DependenciesTest\Routes2::class, $all[1]);
            $this->assertInstanceOf(DependenciesTest\Routes3::class, $all[2]);

            $some = $deps->all(\Index\Http\Routing\RouteHandler::class, fn($object) => !($object instanceof DependenciesTest\Routes2));
            $this->assertEquals(2, count($some));
            $this->assertInstanceOf(DependenciesTest\Routes1::class, $some[0]);
            $this->assertInstanceOf(DependenciesTest\Routes3::class, $some[1]);
        }
    }
}

namespace DependenciesTest {
    interface DependencyTestInterface {}

    class RequiredParamTestClass {
        public function __construct(public DependencyTestInterface $test) {}
    }

    class NullableParamTestClass {
        public function __construct(public ?DependencyTestInterface $test) {}
    }

    class Routes1 implements \Index\Http\Routing\RouteHandler {
        use \Index\Http\Routing\RouteHandlerCommon;
    }

    class Routes2 implements \Index\Http\Routing\RouteHandler {
        use \Index\Http\Routing\RouteHandlerCommon;
    }

    class Routes3 implements \Index\Http\Routing\RouteHandler {
        use \Index\Http\Routing\RouteHandlerCommon;
    }
}