<?php
/*
 * Copyright 2026 Alessandro Chiapparini <sprint_planer_web@chiapparini.org>
 * SPDX-License-Identifier: Apache-2.0
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * See the LICENSE file in the project root for the full license text.
 */

declare(strict_types=1);

namespace App\Tests\Db;

use App\Db\Migrator;
use PDO;
use PHPUnit\Framework\TestCase;

/**
 * R01-N22: pin the contract that `Migrator::pendingFiles()` lets the request
 * path detect a deploy-skipped migration without applying SQL itself, and
 * that `migrate()` is still the apply path used by `bin/migrate.php`.
 */
final class MigratorTest extends TestCase
{
    private string $tmpDir;

    protected function setUp(): void
    {
        $this->tmpDir = sys_get_temp_dir() . '/spw-migrator-' . bin2hex(random_bytes(4));
        if (!mkdir($this->tmpDir) && !is_dir($this->tmpDir)) {
            $this->fail('cannot mkdir tmp');
        }
    }

    protected function tearDown(): void
    {
        foreach (glob($this->tmpDir . '/*') ?: [] as $f) {
            @unlink($f);
        }
        @rmdir($this->tmpDir);
    }

    private function makePdo(): PDO
    {
        $pdo = new PDO('sqlite::memory:', null, null, [
            PDO::ATTR_ERRMODE            => PDO::ERRMODE_EXCEPTION,
            PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
            PDO::ATTR_EMULATE_PREPARES   => false,
        ]);
        $pdo->exec('PRAGMA foreign_keys = ON');
        return $pdo;
    }

    private function writeMigration(string $name, string $sql): void
    {
        file_put_contents($this->tmpDir . DIRECTORY_SEPARATOR . $name, $sql);
    }

    public function testPendingFilesReturnsEverythingOnVirginDb(): void
    {
        $this->writeMigration('001_init.sql', 'CREATE TABLE a (x INTEGER);');
        $this->writeMigration('002_more.sql', 'CREATE TABLE b (x INTEGER);');

        $migrator = new Migrator($this->makePdo(), $this->tmpDir);
        $this->assertSame(['001_init.sql', '002_more.sql'], $migrator->pendingFiles());
    }

    public function testPendingFilesIsEmptyAfterMigrate(): void
    {
        $this->writeMigration('001_init.sql', 'CREATE TABLE a (x INTEGER);');
        $this->writeMigration('002_more.sql', 'CREATE TABLE b (x INTEGER);');

        $migrator = new Migrator($this->makePdo(), $this->tmpDir);
        $migrator->migrate();
        $this->assertSame([], $migrator->pendingFiles());
        $this->assertSame(2, $migrator->currentVersion());
    }

    public function testPendingFilesReportsOnlyNewlyAdded(): void
    {
        $this->writeMigration('001_init.sql', 'CREATE TABLE a (x INTEGER);');
        $pdo = $this->makePdo();
        (new Migrator($pdo, $this->tmpDir))->migrate();

        // Operator dropped a new file in but didn't re-run migrate.
        $this->writeMigration('002_more.sql', 'CREATE TABLE b (x INTEGER);');
        $migrator = new Migrator($pdo, $this->tmpDir);
        $this->assertSame(['002_more.sql'], $migrator->pendingFiles());
    }

    public function testPendingFilesIgnoresFilesWithoutVersionPrefix(): void
    {
        $this->writeMigration('001_init.sql',  'CREATE TABLE a (x INTEGER);');
        $this->writeMigration('README.md',     '# notes');
        $this->writeMigration('foo_bar.sql',   'CREATE TABLE c (x INTEGER);');

        $migrator = new Migrator($this->makePdo(), $this->tmpDir);
        $this->assertSame(['001_init.sql'], $migrator->pendingFiles());
    }

    public function testPendingFilesCreatesSchemaVersionTable(): void
    {
        // Empty dir: even without any migrations the call must not blow up
        // and the version table must exist (idempotent ensure).
        $pdo = $this->makePdo();
        $migrator = new Migrator($pdo, $this->tmpDir);
        $this->assertSame([], $migrator->pendingFiles());

        $row = $pdo->query("SELECT name FROM sqlite_master WHERE type='table' AND name='schema_version'")->fetch();
        $this->assertIsArray($row);
        $this->assertSame('schema_version', $row['name']);
    }

    public function testRealMigrationsDirectoryHasNoPendingAfterFullApply(): void
    {
        // Smoke test against the real migrations/ folder so a future reviewer
        // can't drop a malformed file in without the suite noticing.
        $pdo = $this->makePdo();
        $migrator = new Migrator($pdo);
        $migrator->migrate();
        $this->assertSame([], $migrator->pendingFiles(), 'real migrations apply cleanly');
    }
}
