<?php

use App\Models\Host;
use App\Services\Secrets\SecretCipher;
use Illuminate\Contracts\Encryption\DecryptException;
use Illuminate\Support\Facades\Crypt;
use Illuminate\Support\Facades\DB;

/**
 * Loads a fresh instance of the re-encryption migration every call.
 *
 * `require`, not `require_once`: PHP re-evaluates the `return new class …`
 * expression each time, so every call gets its own instance. The real migrate
 * run RefreshDatabase already performed saw an empty hosts table — these tests
 * put rows there first and then run it, which is the only way to reach the
 * branches that matter.
 */
function loadHostTokenKeyMigration(): object
{
    return require database_path('migrations/2026_07_31_160000_move_host_api_tokens_onto_the_secrets_key.php');
}

/** Writes a raw ciphertext past the model's mutator, the way an old row looks. */
function storeRawToken(Host $host, string $cipher): void
{
    DB::table('hosts')->where('id', $host->id)->update(['api_token_ref' => $cipher]);
}

function rawToken(Host $host): ?string
{
    $value = DB::table('hosts')->where('id', $host->id)->value('api_token_ref');

    return $value === null ? null : (string) $value;
}

beforeEach(fn () => config()->set('admin_access.secrets_key', 'base64:'.base64_encode(random_bytes(32))));

it('moves a token written under APP_KEY onto SECRETS_KEY', function () {
    $host = Host::factory()->create();
    storeRawToken($host, Crypt::encryptString('automation@pve!clupilot=old-secret'));

    loadHostTokenKeyMigration()->up();

    $raw = (string) rawToken($host);

    expect(app(SecretCipher::class)->decrypt($raw))->toBe('automation@pve!clupilot=old-secret')
        // The point of the whole exercise: APP_KEY no longer opens it, so
        // rotating APP_KEY cannot take every host's Proxmox access with it.
        ->and(fn () => Crypt::decryptString($raw))->toThrow(DecryptException::class)
        ->and($host->fresh()->api_token_ref)->toBe('automation@pve!clupilot=old-secret');
});

it('leaves a row it has already migrated exactly as it is', function () {
    $host = Host::factory()->create(['api_token_ref' => 'automation@pve!clupilot=already-moved']);
    $before = rawToken($host);

    loadHostTokenKeyMigration()->up();

    expect(rawToken($host))->toBe($before)
        ->and($host->fresh()->api_token_ref)->toBe('automation@pve!clupilot=already-moved');
});

it('never destroys a value neither key can read', function () {
    // Mutation boundary: the plaintext is a credential that exists nowhere else.
    // A token an operator can still recover by hand from an old APP_KEY is worth
    // more than a tidy column, so the row is left alone and reported — never
    // overwritten, never nulled.
    $host = Host::factory()->create();
    storeRawToken($host, 'eyJpdiI6IndyaXR0ZW4tdW5kZXItYS1sb25nLWdvbmUta2V5In0=');

    loadHostTokenKeyMigration()->up();

    expect(rawToken($host))->toBe('eyJpdiI6IndyaXR0ZW4tdW5kZXItYS1sb25nLWdvbmUta2V5In0=');
});

it('touches nothing at all when there is no key to re-encrypt with', function () {
    $host = Host::factory()->create();
    $appKeyCipher = Crypt::encryptString('automation@pve!clupilot=still-under-app-key');
    storeRawToken($host, $appKeyCipher);

    config()->set('admin_access.secrets_key', '');

    // Does not throw: a fresh install has no SECRETS_KEY yet, and blocking the
    // whole migration run on that would stop an installation that has nothing
    // to migrate from being created at all.
    loadHostTokenKeyMigration()->up();

    expect(rawToken($host))->toBe($appKeyCipher);
});

it('puts the column back under APP_KEY on a rollback', function () {
    $host = Host::factory()->create(['api_token_ref' => 'automation@pve!clupilot=roundtrip']);

    loadHostTokenKeyMigration()->down();

    expect(Crypt::decryptString((string) rawToken($host)))->toBe('automation@pve!clupilot=roundtrip');
});
