<?php

use App\Models\Host;
use App\Provisioning\Jobs\ApplyHostVpnPeer;
use App\Services\Wireguard\WireguardHub;
use App\Support\HostEnrolment;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Queue;

beforeEach(function () {
    fakeServices();
});

/**
 * Der Code ist ein Ausweis für den Rückweg, kein Schlüssel zu einer Auskunft:
 * er öffnet nichts nach außen (§5 der Spec), sondern verhindert nur, dass ein
 * Gerät im Tunnel für einen fremden Host spricht.
 */
it('hands out the code exactly once and stores only its hash', function () {
    $host = Host::factory()->create();

    $code = HostEnrolment::issue($host);

    expect($code)->toHaveLength(32);

    $stored = DB::table('hosts')->where('id', $host->id)->value('enrolment_code_hash');
    expect($stored)->not->toBe($code)
        ->and($stored)->not->toContain($code);
});

it('resolves a valid code to its host and consumes it', function () {
    $host = Host::factory()->create();
    $code = HostEnrolment::issue($host);

    expect(HostEnrolment::claim($code)?->id)->toBe($host->id);

    // Verbraucht. Ein zweiter Lauf braucht einen neuen Code aus der Konsole —
    // eine halb installierte Maschine wird neu aufgesetzt, nicht nachgebessert.
    expect(HostEnrolment::claim($code))->toBeNull();
});

/**
 * Der Fortschritt kommt VOR der Registrierung und darf den Code deshalb nicht
 * verbrauchen — sonst könnte der Host sich nach seiner ersten Meldung nie mehr
 * registrieren.
 */
it('resolves a code without consuming it, for the progress reports', function () {
    $host = Host::factory()->create();
    $code = HostEnrolment::issue($host);

    expect(HostEnrolment::resolve($code)?->id)->toBe($host->id)
        ->and(HostEnrolment::resolve($code)?->id)->toBe($host->id);

    // Und danach ist er immer noch einlösbar.
    expect(HostEnrolment::claim($code)?->id)->toBe($host->id);
});

it('refuses a code past its expiry', function () {
    $host = Host::factory()->create();
    $code = HostEnrolment::issue($host);

    $this->travel(25)->hours();

    expect(HostEnrolment::claim($code))->toBeNull();
});

it('refuses a code that was never issued', function () {
    expect(HostEnrolment::claim(str_repeat('a', 32)))->toBeNull();
});

/**
 * Der Hub muss den Peer kennen, BEVOR der Host im Tunnel ist — sonst käme er
 * nie hinein und müsste seinen Schlüssel über einen öffentlichen Weg melden.
 * Genau das vermeidet dieser Entwurf.
 */
it('allocates a tunnel address and admits the peer up front', function () {
    $host = Host::factory()->create(['wg_ip' => null, 'wg_pubkey' => null]);

    HostEnrolment::issue($host);

    $host->refresh();
    expect($host->wg_ip)->not->toBeNull()
        ->and($host->wg_pubkey)->not->toBeNull();

    // `peers()` ist nach dem öffentlichen Schlüssel indiziert, die Werte sind
    // Momentaufnahmen — geprüft wird deshalb gegen die Schlüssel.
    expect(array_keys(app(WireguardHub::class)->peers()))->toContain($host->wg_pubkey);
});

/**
 * Der Riegel gegen den Rückbau.
 *
 * `wg set` gelingt nur im Provisioning-Container — dem einzigen mit NET_ADMIN,
 * `/dev/net/tun` und `wg0`. Diese Methode lief aber im Web-Request, also im
 * `app`-Container: „Unable to modify interface: Operation not permitted", und
 * das Anlegen eines Hosts endete für den Betreiber in einem 500 — ausgerechnet
 * auf der Seite, die ihm den Einmal-Code zeigen sollte.
 *
 * Der Test davor blieb dabei grün, weil die Suite den Hub gegen
 * FakeWireguardHub tauscht. Dieser hier prüft deshalb den WEG, nicht das
 * Ergebnis: der Peer gehört auf die Queue, und in der Anfrage bleibt der Hub
 * unberührt.
 */
it('registers the tunnel peer on the provisioning queue, not in the request', function () {
    Queue::fake();

    $host = Host::factory()->create(['wg_ip' => null, 'wg_pubkey' => null]);

    HostEnrolment::issue($host);

    Queue::assertPushedOn('provisioning', ApplyHostVpnPeer::class,
        fn (ApplyHostVpnPeer $job) => $job->hostId === $host->id);

    expect(app(WireguardHub::class)->peers())->toBeEmpty();
});

it('carries the replaced key along so the old peer does not linger', function () {
    // Ein zweiter Übernahmeversuch tauscht den Schlüssel. Stünde der alte
    // nicht im Auftrag, läse der ihn aus der Zeile — wo längst der neue steht —
    // und der abgelöste Peer bliebe für immer auf dem Hub.
    $host = Host::factory()->create(['wg_ip' => null, 'wg_pubkey' => null]);

    HostEnrolment::issue($host);
    $erster = $host->refresh()->wg_pubkey;

    HostEnrolment::issue($host);
    $zweiter = $host->refresh()->wg_pubkey;

    expect($zweiter)->not->toBe($erster);

    $keys = array_keys(app(WireguardHub::class)->peers());
    expect($keys)->toContain($zweiter)
        ->and($keys)->not->toContain($erster);
});

/**
 * Der private Schlüssel steht in der kopierten Befehlszeile und sonst nirgends.
 * Ihn zu speichern hieße, ein Geheimnis aufzubewahren, das nach Task 9 des
 * Skripts ohnehin wertlos ist — und bis dahin eine Stelle mehr, an der es liegt.
 */
it('returns the private key with the code and never stores it', function () {
    $host = Host::factory()->create();

    $enrolment = HostEnrolment::issueWithKeys($host);

    expect($enrolment['private_key'])->not->toBeEmpty();

    $row = (array) DB::table('hosts')->where('id', $host->id)->first();
    foreach ($row as $value) {
        expect((string) $value)->not->toContain($enrolment['private_key']);
    }
});

/**
 * Ein zweiter Code macht den ersten wertlos. Sonst hätte eine Maschine, die im
 * Rettungssystem hängengeblieben ist, weiter einen gültigen Ausweis für einen
 * Host, den der Betreiber gerade neu übernimmt.
 */
it('invalidates the previous code when a new one is issued', function () {
    $host = Host::factory()->create();

    $first = HostEnrolment::issue($host);
    $second = HostEnrolment::issue($host);

    expect(HostEnrolment::claim($first))->toBeNull()
        ->and(HostEnrolment::claim($second)?->id)->toBe($host->id);
});
