<?php
declare(strict_types=1);

namespace {
    if (!function_exists('logger')) {
        function logger(?string $channel = null): object
        {
            return new class () {
                public function __call(string $name, array $arguments): void
                {
                }
            };
        }
    }
}

namespace Apexl\Vault\Tests {
    use Apexl\Vault\Interfaces\ConnectionInterface;
    use Apexl\Vault\Operators\Drivers\SqLite;
    use Apexl\Vault\Operators\Pdo;
    use PHPUnit\Framework\Attributes\Test;
    use PHPUnit\Framework\TestCase;
    use ReflectionProperty;
    use RuntimeException;
    use stdClass;

    /**
     * Vault had no transaction methods at all, so every caller that wanted one reached
     * for the raw handle and kept its own bookkeeping. Wizaplace's Transaction is the
     * example that bit: it tracks whether a transaction is open per instance, while the
     * connection has exactly one, so a nested loader's commit ended the outer table's
     * transaction — which is why Newton's on-demand importer passes `transaction => false`.
     *
     * Depth is counted on the connection now, so nesting joins instead of ending.
     */
    final class TransactionOwnershipTest extends TestCase
    {
        private SqLite $drv;

        protected function setUp(): void
        {
            if (!extension_loaded('pdo_sqlite')) {
                $this->markTestSkipped('ext-pdo_sqlite not available');
            }

            $this->drv = new SqLite($this->config());
            $this->drv->executeSql('DROP TABLE IF EXISTS crates');
            $this->drv->executeSql('CREATE TABLE crates (id INTEGER PRIMARY KEY, name TEXT)');
        }

        private function config(): stdClass
        {
            $cfg = new stdClass();
            $cfg->driver = 'sqlite';

            return $cfg;
        }

        private function connection(): ConnectionInterface
        {
            return $this->drv->connection();
        }

        private function names(): array
        {
            return array_column(
                $this->drv->select('crates')->fields(['name'])->execute()->fetchAllAssoc() ?: [],
                'name',
            );
        }

        private function insert(string $name): void
        {
            $this->drv->executeSql(sprintf("INSERT INTO crates (name) VALUES ('%s')", $name));
        }

        #[Test]
        public function workCommitsWhenItReturns(): void
        {
            $this->connection()->transaction(fn() => $this->insert('kept'));

            $this->assertSame(['kept'], $this->names());
        }

        #[Test]
        public function workRollsBackWhenItThrows(): void
        {
            try {
                $this->connection()->transaction(function (): void {
                    $this->insert('doomed');
                    throw new RuntimeException('nope');
                });
                $this->fail('the exception should have surfaced');
            } catch (RuntimeException) {
            }

            $this->assertSame([], $this->names());
            $this->assertFalse($this->connection()->inTransaction());
        }

        #[Test]
        public function theReturnValuePassesThrough(): void
        {
            $this->assertSame(42, $this->connection()->transaction(fn(): int => 42));
        }

        /** The regression: an inner transaction must not commit the outer one's work. */
        #[Test]
        public function anInnerTransactionDoesNotCommitTheOuterWork(): void
        {
            try {
                $this->connection()->transaction(function (): void {
                    $this->insert('outer');

                    // A nested helper doing its own atomic bit — this used to end the
                    // transaction its caller was still in the middle of.
                    $this->connection()->transaction(fn() => $this->insert('inner'));

                    $this->assertTrue(
                        $this->connection()->inTransaction(),
                        'the inner commit ended the outer transaction',
                    );

                    throw new RuntimeException('outer fails after the inner one succeeded');
                });
                $this->fail('the exception should have surfaced');
            } catch (RuntimeException) {
            }

            // Both rows go, including the inner one that "committed".
            $this->assertSame([], $this->names());
        }

        #[Test]
        public function nestedWorkCommitsOnceTheOutermostFrameReturns(): void
        {
            $this->connection()->transaction(function (): void {
                $this->insert('outer');
                $this->connection()->transaction(fn() => $this->insert('inner'));
            });

            $this->assertSame(['outer', 'inner'], $this->names());
            $this->assertFalse($this->connection()->inTransaction());
        }

        /** Depth must be counted per connection, not per caller — that was the bug. */
        #[Test]
        public function theConnectionIsTheSameObjectEveryTimeYouAskForIt(): void
        {
            $this->assertSame($this->drv->connection(), $this->drv->connection());
        }

        /**
         * Read the builder's handle by reflection rather than through getConnection(),
         * which is deprecated — calling it here would put a notice in every suite run.
         */
        #[Test]
        public function theHandleIsTheOneTheBuilderRunsOn(): void
        {
            $this->drv->executeSql('SELECT 1');
            $property = new ReflectionProperty(Pdo::class, 'connection');

            $this->assertSame($property->getValue($this->drv), $this->connection()->handle());
        }
    }
}
