<?php // tests/Feature/Nextcloud/NextcloudUsersTest.php

use App\Models\Host;
use App\Models\Instance;
use App\Models\Seat;
use App\Services\Nextcloud\NextcloudUsers;
use App\Services\Proxmox\FakeProxmoxClient;
use App\Services\Proxmox\ProxmoxClient;

function gastBereit(): array
{
    $pve = new FakeProxmoxClient;
    app()->instance(ProxmoxClient::class, $pve);

    // Ohne host_id bliebe die host-Beziehung null, und run() wiese jeden
    // Befehl kommentarlos ab, statt einen zu bauen — die Instanz braucht
    // einen echten Host, damit forHost() überhaupt greifen kann.
    return [$pve, Instance::factory()->create(['status' => 'active', 'vmid' => 201, 'host_id' => Host::factory()])];
}

it('legt einen Benutzer mit erzeugtem Passwort an, das niemand sieht', function () {
    [$pve, $instance] = gastBereit();
    // FakeProxmoxClient antwortet auf einen ungeskripteten Befehl mit Exitcode
    // 0 — ohne diese Zeile sähe user:info wie "Benutzer existiert bereits"
    // aus. Dieselbe Falle, dasselbe Skript wie in CustomerStepsTest.
    $pve->guestScript('user:info', 1);
    $sitz = Seat::factory()->create(['email' => 'anna@firma.tld', 'name' => 'Anna', 'nc_username' => 'anna@firma.tld']);

    app(NextcloudUsers::class)->invite($instance, $sitz);

    $befehle = implode("\n", $pve->guestCommands);

    // --generate-password: Nextcloud erzeugt es, NIEMAND bekommt es zu sehen.
    // --email: dorthin geht der Link, an dem der Mitarbeiter sein eigenes setzt.
    expect($befehle)->toContain('--generate-password')
        ->and($befehle)->toContain('--email')
        ->and($befehle)->toContain('anna@firma.tld');
});

it('verschickt bei einem bestehenden Benutzer nur die Willkommensmail neu', function () {
    [$pve, $instance] = gastBereit();
    $pve->guestScripts['user:info'] = ['exitcode' => 0, 'out-data' => 'user_id: anna@firma.tld'];
    $sitz = Seat::factory()->create(['email' => 'anna@firma.tld', 'nc_username' => 'anna@firma.tld']);

    app(NextcloudUsers::class)->invite($instance, $sitz);

    $befehle = implode("\n", $pve->guestCommands);

    // Wiederholbar: ein zweiter Lauf nach einem Absturz legt keinen zweiten
    // Benutzer an. Genau wie CreateCustomerAdmin es tut.
    expect($befehle)->toContain('user:welcome --reset-password')
        ->and($befehle)->not->toContain('user:add');
});

it('setzt bei readonly einen eigenen Speicherplatz von null', function () {
    [$pve, $instance] = gastBereit();
    $sitz = Seat::factory()->create(['role' => 'readonly', 'nc_username' => 'anna@firma.tld']);

    app(NextcloudUsers::class)->applyRole($instance, $sitz);

    $befehle = implode("\n", $pve->guestCommands);

    expect($befehle)->toContain('group:adduser')
        ->and($befehle)->toContain('nur-lesen')
        ->and($befehle)->toContain('files quota')
        ->and($befehle)->toContain('0 B');
});

it('LOESCHT den eigenen Speicherplatz, wenn readonly verlassen wird', function () {
    // Die Falle aus ApplyStorageQuota: "An account with an explicit quota stops
    // following the default". Ein Konto, das mit einem festen Wert aus der
    // Rolle herauskommt, wäre bei der nächsten Paketänderung stumm
    // ausgenommen — und niemand merkte es, bis der Kunde fragt, warum sein
    // Mitarbeiter weniger Platz hat als bezahlt.
    [$pve, $instance] = gastBereit();
    $sitz = Seat::factory()->create(['role' => 'member', 'nc_username' => 'anna@firma.tld']);

    app(NextcloudUsers::class)->applyRole($instance, $sitz);

    $befehle = implode("\n", $pve->guestCommands);

    expect($befehle)->toContain('files quota --delete')
        ->and($befehle)->not->toContain('0 B');
});

it('nimmt admin nicht kurz aus der eigenen Gruppe, wenn owner und admin auf denselben Namen zeigen', function () {
    // owner und admin teilen sich dieselbe Nextcloud-Gruppe (admin). Tragend
    // ist die Bedingung `if ($gruppe !== $ziel)`: ohne sie liefe hier
    // group:removeuser admin VOR group:adduser admin — der Benutzer fiele kurz
    // aus genau der Gruppe heraus, in die er gerade soll.
    //
    // NICHT array_unique: das dedupliziert nur die Liste der Gruppennamen und
    // spart damit einen wiederholten Befehl, der ohnehin harmlos wäre. Ein
    // Kommentar, der den falschen Schutz benennt, hält den Nächsten vom
    // Nachsehen ab (R19).
    [$pve, $instance] = gastBereit();
    $sitz = Seat::factory()->create(['role' => 'admin', 'nc_username' => 'anna@firma.tld']);

    app(NextcloudUsers::class)->applyRole($instance, $sitz);

    $befehle = implode("\n", $pve->guestCommands);

    expect($befehle)->not->toContain("group:removeuser 'admin'")
        ->and($befehle)->toContain("group:adduser 'admin'");
});

it('scheitert nicht an einer Gruppe, die es im Gast gar nicht gibt', function () {
    // `mitarbeiter` und `nur-lesen` legt niemand an — sie entstehen erst,
    // wenn `user:add --group=…` sie zum ersten Mal braucht. `occ
    // group:removeuser` legt nichts an und beendet mit Fehlercode, wenn die
    // Gruppe fehlt; run() verundet alle Exitcodes.
    //
    // Ohne das Auffangnetz war damit die erste Rollenänderung auf einer
    // frischen Instanz dauerhaft „fehlgeschlagen — Die Cloud hat die Änderung
    // nicht angenommen": Wiederholen wählt wieder `role`, und wieder fehlt
    // dieselbe Gruppe.
    //
    // Der Fake wird hier ausdrücklich auf einen Fehlercode gesetzt — genau
    // das fehlte bisher, und deshalb belegten die Prüfungen nur die erzeugte
    // Befehlsmenge, nie die Antwort des Gasts.
    [$pve, $instance] = gastBereit();
    $pve->guestScript('group:removeuser', 1);
    $sitz = Seat::factory()->create(['role' => 'member', 'nc_username' => 'anna@firma.tld']);

    expect(app(NextcloudUsers::class)->applyRole($instance, $sitz))->toBeTrue();

    // Und der Grund, warum es trotzdem gelingt, steht im Befehl selbst.
    $befehle = implode("\n", $pve->guestCommands);

    expect($befehle)->toContain("group:removeuser 'admin' 'anna@firma.tld' 2>/dev/null || true");
});

it('meldet ein gescheitertes Zuordnen weiterhin als Fehlschlag', function () {
    // Die andere Hälfte derselben Grenze: das Auffangnetz darf nicht zum
    // Wegsehen werden. Wer in KEINER Gruppe landet, sieht in seiner neuen
    // Cloud nichts — das muss an der Zeile stehen.
    [$pve, $instance] = gastBereit();
    $pve->guestScript('group:adduser', 1);
    $sitz = Seat::factory()->create(['role' => 'member', 'nc_username' => 'anna@firma.tld']);

    expect(app(NextcloudUsers::class)->applyRole($instance, $sitz))->toBeFalse();
});

it('lässt eine Instanz ohne Host unangetastet', function () {
    // run() steigt vor dem Verbindungsaufbau aus, wenn es keinen Host gibt —
    // es darf dabei kein einziger Befehl abgesetzt werden, denn es gibt
    // nichts, worüber forHost() eine Verbindung aufbauen könnte.
    $pve = new FakeProxmoxClient;
    app()->instance(ProxmoxClient::class, $pve);
    $instance = Instance::factory()->create(['status' => 'active', 'vmid' => 201, 'host_id' => null]);
    $sitz = Seat::factory()->create(['nc_username' => 'anna@firma.tld']);

    expect(app(NextcloudUsers::class)->applyRole($instance, $sitz))->toBeFalse()
        ->and($pve->guestCommands)->toBe([]);
});

it('wirft Sitzungen beim Sperren SOFORT hinaus', function () {
    // user:disable allein lässt laufende Sitzungen bis zu fünf Minuten
    // weiterleben. Bei einem Mitarbeiter, der gerade gegangen ist, sind fünf
    // Minuten fünf zu viel.
    [$pve, $instance] = gastBereit();
    $sitz = Seat::factory()->create(['nc_username' => 'anna@firma.tld']);

    app(NextcloudUsers::class)->disable($instance, $sitz);

    $befehle = implode("\n", $pve->guestCommands);

    expect($befehle)->toContain('user:disable')
        ->and($befehle)->toContain('user:auth-tokens:delete');
});

it('gibt false zurück statt zu werfen, wenn der Gast nicht antwortet', function () {
    [$pve, $instance] = gastBereit();
    $pve->guestThrows[201] = new RuntimeException('guest agent unreachable');
    $sitz = Seat::factory()->create(['nc_username' => 'anna@firma.tld']);

    expect(app(NextcloudUsers::class)->disable($instance, $sitz))->toBeFalse();
});

it('führt gar nichts aus, wenn der Benutzername keiner ist', function () {
    // Der Name wandert in eine Wurzel-Shell im Gast. Dieselbe Regel wie bei
    // HostFirewall: ein Dienst, der eine Shell füttert, darf sich nicht
    // darauf verlassen, dass sein Aufrufer sauber war.
    [$pve, $instance] = gastBereit();
    $sitz = Seat::factory()->create(['nc_username' => 'anna; rm -rf /']);

    expect(app(NextcloudUsers::class)->disable($instance, $sitz))->toBeFalse()
        ->and($pve->guestCommands)->toBe([]);
});
