<?php
declare(strict_types=1);

// operators/pdo.php calls a global logger() helper that consumers
// (apexl/io) provide; vault itself has no logging dependency. Stub it so
// the execute() path is testable standalone. Bracketed namespace syntax
// is required to declare a global function alongside the test class.
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\exceptions\ExecutionException;
    use apexl\Vault\exceptions\SqliteUnsupportedException;
    use apexl\Vault\operators\drivers\sqlite;
    use PHPUnit\Framework\Attributes\Test;
    use PHPUnit\Framework\TestCase;
    use stdClass;

    /**
     * Behavioural coverage for the PDO error-handling contract, run against
     * the in-memory sqlite driver (skips when ext-pdo_sqlite is absent).
     *
     * Verifies the two halves of the ERRMODE fix:
     *  1. A unique-key collision on a plain insert() THROWS
     *     ExecutionException (previously the mysql driver swallowed these:
     *     no ERRMODE_EXCEPTION + implicit ON DUPLICATE KEY UPDATE).
     *  2. The update()-by-key path — what entityDatabaseOperator::store()
     *     uses for existing entities — still works unchanged.
     *  3. upsert() on sqlite refuses loudly rather than degrading to a
     *     plain insert.
     */
    final class SqliteDriverTest extends TestCase
    {
        private sqlite $drv;

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

            $cfg = new stdClass();
            $cfg->driver = 'sqlite';

            $this->drv = new sqlite($cfg);
            $this->drv->executeSql('DROP TABLE IF EXISTS tenants');
            $this->drv->executeSql(
                'CREATE TABLE tenants (
                    id INTEGER PRIMARY KEY AUTOINCREMENT,
                    name TEXT NOT NULL,
                    slug TEXT NOT NULL UNIQUE
                )'
            );
        }

        private function insertRow(string $name, string $slug): void
        {
            $this->drv->newQuery()
                ->insert('tenants')
                ->fields(['name' => $name, 'slug' => $slug])
                ->execute();
        }

        #[Test]
        public function plain_insert_round_trips(): void
        {
            $this->insertRow('Acme', 'acme');

            $row = $this->drv->newQuery()
                ->select('tenants')
                ->fields(['id', 'name', 'slug'])
                ->where('slug', 'acme')
                ->execute()
                ->fetchAssoc();

            self::assertSame('Acme', $row['name']);
            self::assertSame(1, $this->drv->lastInsertId());
        }

        #[Test]
        public function duplicate_key_insert_throws_execution_exception(): void
        {
            $this->insertRow('Original', 'duplicate-slug');

            $this->expectException(ExecutionException::class);

            $this->insertRow('Should Not Be A New Row', 'duplicate-slug');
        }

        #[Test]
        public function duplicate_key_insert_does_not_overwrite_the_existing_row(): void
        {
            $this->insertRow('Original', 'duplicate-slug');

            try {
                $this->insertRow('Should Not Be A New Row', 'duplicate-slug');
            } catch (ExecutionException) {
                // expected
            }

            $rows = $this->drv->newQuery()
                ->select('tenants')
                ->fields(['name'])
                ->where('slug', 'duplicate-slug')
                ->execute()
                ->fetchAllAssoc();

            self::assertCount(1, $rows);
            self::assertSame('Original', $rows[0]['name']);
        }

        #[Test]
        public function update_by_primary_key_still_works(): void
        {
            $this->insertRow('Before', 'stable-slug');
            $id = $this->drv->lastInsertId();

            $this->drv->newQuery()
                ->update('tenants')
                ->fields(['name' => 'After'])
                ->where('id', $id)
                ->execute();

            $row = $this->drv->newQuery()
                ->select('tenants')
                ->fields(['name', 'slug'])
                ->where('id', $id)
                ->execute()
                ->fetchAssoc();

            self::assertSame('After', $row['name']);
            self::assertSame('stable-slug', $row['slug']);
        }

        #[Test]
        public function update_into_a_unique_collision_throws_execution_exception(): void
        {
            $this->insertRow('One', 'slug-one');
            $this->insertRow('Two', 'slug-two');
            $id = $this->drv->lastInsertId();

            $this->expectException(ExecutionException::class);

            $this->drv->newQuery()
                ->update('tenants')
                ->fields(['slug' => 'slug-one'])
                ->where('id', $id)
                ->execute();
        }

        #[Test]
        public function upsert_is_unsupported_on_sqlite_and_says_so(): void
        {
            $this->expectException(SqliteUnsupportedException::class);

            $this->drv->newQuery()->upsert('tenants');
        }
    }
}
