<?php

// tests/Feature/Seats/SyncSeatToNextcloudTest.php

use App\Livewire\Users;
use App\Models\Customer;
use App\Models\Host;
use App\Models\Instance;
use App\Models\Mailbox;
use App\Models\Seat;
use App\Provisioning\Jobs\SyncSeatToNextcloud;
use App\Services\Mail\GuestMailConfig;
use App\Services\Nextcloud\NextcloudUsers;
use App\Services\Proxmox\FakeProxmoxClient;
use App\Services\Proxmox\ProxmoxClient;
use App\Support\Settings;
use Illuminate\Support\Facades\Queue;
use Livewire\Livewire;
use Symfony\Component\Finder\Finder;

/**
 * Der Mailversand, eingerichtet — Server und ausgefülltes Versandkonto.
 *
 * Steht jetzt in jedem Test, der wirklich einladen will: die Einladung
 * verschickt die Nextcloud des Kunden selbst, und ohne Versand käme dort nie
 * eine Mail heraus. Angelegt ist das Postfach ab der eigenen Wanderung — aber
 * inaktiv und ohne Zugangsdaten, also genau der Zustand, den „Einladen" jetzt
 * abweist.
 */
function mailversandEingerichtet(): void
{
    Settings::set('mail.host', 'mail.clupilot.cloud');
    Settings::set('mail.port', 587);

    Mailbox::findByKey(GuestMailConfig::RELAY_KEY)->update([
        'address' => 'noreply@clupilot.cloud', 'password' => 'geheim', 'active' => true,
    ]);
}

it('schreibt den Erfolg an den Sitz', function () {
    app()->instance(ProxmoxClient::class, new FakeProxmoxClient);
    $customer = Customer::factory()->create();
    // host_id, weil die host-Beziehung sonst null bliebe: der Auftrag bricht
    // dann mit 'no_instance' ab und käme nie bis zum Gast — dieselbe Falle wie
    // in NextcloudUsersTest::gastBereit().
    Instance::factory()->for($customer)->create(['status' => 'active', 'vmid' => 201, 'host_id' => Host::factory()]);
    $sitz = Seat::factory()->for($customer)->create([
        'email' => 'anna@firma.tld', 'nc_username' => 'anna@firma.tld',
        'nc_state' => Seat::STATE_PENDING,
    ]);

    (new SyncSeatToNextcloud($sitz->uuid, 'invite'))->handle(app(NextcloudUsers::class));

    expect($sitz->fresh()->nc_state)->toBe(Seat::STATE_SYNCED)
        ->and($sitz->fresh()->nc_error)->toBeNull()
        ->and($sitz->fresh()->nc_synced_at)->not->toBeNull();
});

it('schreibt den Fehlschlag mit Grund an den Sitz', function () {
    $pve = new FakeProxmoxClient;
    $pve->guestThrows[201] = new RuntimeException('guest agent unreachable');
    app()->instance(ProxmoxClient::class, $pve);
    $customer = Customer::factory()->create();
    Instance::factory()->for($customer)->create(['status' => 'active', 'vmid' => 201, 'host_id' => Host::factory()]);
    $sitz = Seat::factory()->for($customer)->create(['nc_username' => 'anna@firma.tld']);

    (new SyncSeatToNextcloud($sitz->uuid, 'invite'))->handle(app(NextcloudUsers::class));

    // Nicht nur „irgendein Fehler": der Grund muss der des unerreichbaren
    // Gastes sein, nicht der der fehlenden Instanz — sonst liefe der Test auch
    // dann grün, wenn die Instanz gar nicht gefunden wurde.
    expect($sitz->fresh()->nc_state)->toBe(Seat::STATE_FAILED)
        ->and($sitz->fresh()->nc_error)->toBe('guest_failed');
});

it('sagt es am Sitz, wenn es gar keine erreichbare Instanz gibt', function () {
    // Der Kunde hat keine laufende Instanz — es gibt nichts zu spiegeln, und
    // der Inhaber muss den Unterschied zu „der Gast hat abgelehnt" lesen
    // können, weil nur das eine von beiden er selbst beheben kann.
    $customer = Customer::factory()->create();
    $sitz = Seat::factory()->for($customer)->create(['nc_username' => 'anna@firma.tld']);

    (new SyncSeatToNextcloud($sitz->uuid, 'invite'))->handle(app(NextcloudUsers::class));

    expect($sitz->fresh()->nc_state)->toBe(Seat::STATE_FAILED)
        ->and($sitz->fresh()->nc_error)->toBe('no_instance');
});

it('läuft auf der Bereitstellungs-Warteschlange, nirgends sonst', function () {
    // Nur dieser Arbeiter hat Tunnel und Proxmox-Zugangsdaten. Auf der
    // Standard-Warteschlange erreicht der Auftrag keinen einzigen Gast.
    $auftrag = new SyncSeatToNextcloud('egal', 'invite');

    expect($auftrag->connection)->toBe('provisioning')
        ->and($auftrag->queue)->toBe('provisioning');
});

it('legt beim Anlegen KEINEN Nextcloud-Benutzer an', function () {
    // Anlegen ist nicht Einladen. Der Inhaber soll sein Team vorbereiten
    // können, ohne dass jemand eine Mail bekommt.
    Queue::fake();
    $customer = Customer::factory()->create();
    $user = $customer->ensureUser();
    Instance::factory()->for($customer)->create(['status' => 'active']);

    Livewire::actingAs($user)->test(Users::class)
        ->set('inviteEmail', 'anna@firma.tld')
        ->set('inviteName', 'Anna')
        ->call('addSeat');

    Queue::assertNothingPushed();
    expect(Seat::where('email', 'anna@firma.tld')->first()->nc_state)->toBe(Seat::STATE_NONE);
});

it('schickt erst beim Einladen einen Auftrag los', function () {
    Queue::fake();
    mailversandEingerichtet();
    $customer = Customer::factory()->create();
    $user = $customer->ensureUser();
    Instance::factory()->for($customer)->create(['status' => 'active']);
    $sitz = Seat::factory()->for($customer)->create(['role' => 'member']);

    Livewire::actingAs($user)->test(Users::class)->call('sendInvite', $sitz->uuid);

    Queue::assertPushed(SyncSeatToNextcloud::class);
    expect($sitz->fresh()->nc_state)->toBe(Seat::STATE_PENDING)
        ->and($sitz->fresh()->nc_username)->toBe($sitz->email);
});

it('hält den Benutzernamen fest, auch wenn die Mailadresse sich ändert', function () {
    // Nextcloud kann Benutzer nicht umbenennen. Ein Sitz, dessen Adresse sich
    // später ändert, behält seinen Anmeldenamen.
    Queue::fake();
    mailversandEingerichtet();
    $customer = Customer::factory()->create();
    $user = $customer->ensureUser();
    Instance::factory()->for($customer)->create(['status' => 'active']);
    $sitz = Seat::factory()->for($customer)->create(['email' => 'alt@firma.tld', 'nc_username' => 'alt@firma.tld']);

    $sitz->update(['email' => 'neu@firma.tld']);
    Livewire::actingAs($user)->test(Users::class)->call('sendInvite', $sitz->uuid);

    expect($sitz->fresh()->nc_username)->toBe('alt@firma.tld');
});

it('weist die elfte Einladung derselben Stunde ab und sagt die Restzeit', function () {
    Queue::fake();
    mailversandEingerichtet();
    $customer = Customer::factory()->create();
    $user = $customer->ensureUser();
    Instance::factory()->for($customer)->create(['status' => 'active']);
    $sitze = Seat::factory()->for($customer)->count(11)->create(['role' => 'member']);

    $seite = Livewire::actingAs($user)->test(Users::class);

    foreach ($sitze->take(10) as $sitz) {
        $seite->call('sendInvite', $sitz->uuid);
    }

    $seite->call('sendInvite', $sitze->last()->uuid);

    // Keine stumme Verweigerung: die Meldung nennt die echte Restzeit. Auf die
    // Zahl geprüft statt auf den Satz — ein Test, der denselben __()-Aufruf
    // nachrechnet wie die Implementierung, würde auch eine fehlende Restzeit
    // durchwinken (siehe R19).
    $seite->assertDispatched(
        'notify',
        fn ($ereignis, $daten) => str_contains($daten['message'], '60'),
    );
    expect($sitze->last()->fresh()->nc_state)->toBe(Seat::STATE_NONE);
    Queue::assertPushed(SyncSeatToNextcloud::class, 10);
});

it('weist das vierte Draufdrücken auf denselben Sitz ab', function () {
    // Die zweite Grenze: gegen das wiederholte Klicken an EINER Zeile, das
    // demselben Menschen sonst vier Willkommensmails schickt.
    Queue::fake();
    mailversandEingerichtet();
    $customer = Customer::factory()->create();
    $user = $customer->ensureUser();
    Instance::factory()->for($customer)->create(['status' => 'active']);
    $sitz = Seat::factory()->for($customer)->create(['role' => 'member']);

    $seite = Livewire::actingAs($user)->test(Users::class);

    foreach (range(1, 4) as $ignoriert) {
        $seite->call('sendInvite', $sitz->uuid);
    }

    Queue::assertPushed(SyncSeatToNextcloud::class, 3);
});

it('entzieht, ohne die Zeile zu löschen', function () {
    Queue::fake();
    $customer = Customer::factory()->create();
    $user = $customer->ensureUser();
    Instance::factory()->for($customer)->create(['status' => 'active']);
    // Ein Sitz, der wirklich in der Nextcloud steht: nc_synced_at ist die
    // Angabe, an der die Seite entscheidet, ob es dort etwas zu sperren gibt.
    $sitz = Seat::factory()->for($customer)->create([
        'role' => 'member', 'nc_username' => 'anna@firma.tld',
        'nc_state' => Seat::STATE_SYNCED, 'nc_synced_at' => now(),
    ]);

    Livewire::actingAs($user)->test(Users::class)->call('revoke', $sitz->uuid);

    // Der Datensatz bleibt. Ein Fehlgriff im Userpanel darf die Arbeit eines
    // Menschen nicht vernichten — und in einem Produkt, das mit
    // Nachvollziehbarkeit verkauft wird, gäbe es danach nichts mehr zu zeigen.
    expect(Seat::find($sitz->id))->not->toBeNull()
        ->and($sitz->fresh()->status)->toBe('revoked');
    Queue::assertPushed(SyncSeatToNextcloud::class);
});

it('schickt für einen Sitz, der nie in der Nextcloud war, keinen Auftrag', function () {
    // Angelegt, nie eingeladen: es gäbe dort nichts zu sperren. Ein Auftrag
    // liefe ins Leere und stellte die Zeile danach auf „fehlgeschlagen" —
    // eine Fehlermeldung für etwas, das nie ein Fehler war.
    Queue::fake();
    $customer = Customer::factory()->create();
    $user = $customer->ensureUser();
    Instance::factory()->for($customer)->create(['status' => 'active']);
    $sitz = Seat::factory()->for($customer)->create(['role' => 'member']);

    Livewire::actingAs($user)->test(Users::class)->call('revoke', $sitz->uuid);

    Queue::assertNothingPushed();
    expect($sitz->fresh()->status)->toBe('revoked')
        ->and($sitz->fresh()->nc_state)->toBe(Seat::STATE_NONE);
});

it('sperrt nichts, was in der Nextcloud nie angekommen ist', function () {
    // Einladung losgeschickt, Gast war unerreichbar: der Sitz trägt schon
    // einen Anmeldenamen, in der Nextcloud existiert aber nichts. Ein
    // `user:disable` darauf müsste scheitern und liesse die Zeile dauerhaft
    // rot stehen — eine Fehlermeldung für etwas, das nie ein Fehler war.
    Queue::fake();
    $customer = Customer::factory()->create();
    $user = $customer->ensureUser();
    Instance::factory()->for($customer)->create(['status' => 'active']);
    $sitz = Seat::factory()->for($customer)->create([
        'role' => 'member', 'nc_username' => 'anna@firma.tld',
        'nc_state' => Seat::STATE_FAILED, 'nc_error' => 'no_instance', 'nc_synced_at' => null,
    ]);

    Livewire::actingAs($user)->test(Users::class)->call('revoke', $sitz->uuid);

    Queue::assertNothingPushed();
    expect($sitz->fresh()->status)->toBe('revoked');
});

it('vermerkt ein angelegtes Konto, auch wenn die Rolle danach scheitert', function () {
    // Der gefährlichste Halbschritt: `user:add` ist durch, `group:adduser`
    // nicht. Das Konto GIBT es ab jetzt — würde der Auftrag das verschweigen,
    // liesse ein späteres Entziehen die Sperre weg, weil die Seite den Sitz
    // für nie angekommen hielte. Der Zustand bleibt trotzdem 'failed'.
    $pve = new FakeProxmoxClient;
    // user:info auf 1, sonst hält invite() den Benutzer für vorhanden und
    // schickt nur `user:welcome` — dann liefe hier gar kein `user:add`, und der
    // Ablauf wäre ein anderer als der beschriebene.
    $pve->guestScript('user:info', 1);
    $pve->guestScript('group:adduser', 1);
    app()->instance(ProxmoxClient::class, $pve);
    $customer = Customer::factory()->create();
    Instance::factory()->for($customer)->create(['status' => 'active', 'vmid' => 201, 'host_id' => Host::factory()]);
    $sitz = Seat::factory()->for($customer)->create(['nc_username' => 'anna@firma.tld']);

    (new SyncSeatToNextcloud($sitz->uuid, 'invite'))->handle(app(NextcloudUsers::class));

    expect($pve->guestRan('user:add'))->toBeTrue()
        ->and($sitz->fresh()->nc_state)->toBe(Seat::STATE_FAILED)
        ->and($sitz->fresh()->nc_synced_at)->not->toBeNull();
});

it('macht die Sperre beim Wiederaufnehmen rückgängig', function () {
    // Ein wiederaufgenommener Sitz war schon einmal in der Nextcloud, und
    // revoke() hat ihm dort ein `user:disable` geschickt. `user:welcome` hebt
    // eine Sperre nicht auf — ohne `user:enable` bekäme er eine
    // Willkommensmail zu einer Tür, die zu bleibt.
    $pve = new FakeProxmoxClient;
    app()->instance(ProxmoxClient::class, $pve);
    $customer = Customer::factory()->create();
    Instance::factory()->for($customer)->create(['status' => 'active', 'vmid' => 201, 'host_id' => Host::factory()]);
    $sitz = Seat::factory()->for($customer)->create([
        'nc_username' => 'anna@firma.tld', 'nc_state' => Seat::STATE_PENDING, 'nc_synced_at' => now()->subYear(),
    ]);

    (new SyncSeatToNextcloud($sitz->uuid, 'invite'))->handle(app(NextcloudUsers::class));

    expect($pve->guestRan('user:enable'))->toBeTrue()
        ->and($sitz->fresh()->nc_state)->toBe(Seat::STATE_SYNCED);
});

it('schickt bei einer ersten Einladung kein Entsperren mit', function () {
    // Ein frisch angelegtes Konto ist nicht gesperrt. Ein `user:enable` wäre
    // hier folgenlos — und ein folgenloser Befehl im Gast ist einer, den
    // niemand mehr erklären kann, wenn er eines Tages doch etwas tut.
    $pve = new FakeProxmoxClient;
    $pve->guestScript('user:info', 1);
    app()->instance(ProxmoxClient::class, $pve);
    $customer = Customer::factory()->create();
    Instance::factory()->for($customer)->create(['status' => 'active', 'vmid' => 201, 'host_id' => Host::factory()]);
    $sitz = Seat::factory()->for($customer)->create(['nc_username' => 'anna@firma.tld']);

    (new SyncSeatToNextcloud($sitz->uuid, 'invite'))->handle(app(NextcloudUsers::class));

    expect($pve->guestRan('user:enable'))->toBeFalse()
        ->and($sitz->fresh()->nc_state)->toBe(Seat::STATE_SYNCED);
});

it('spiegelt eine Rollenänderung in die Nextcloud', function () {
    Queue::fake();
    $customer = Customer::factory()->create();
    $user = $customer->ensureUser();
    Instance::factory()->for($customer)->create(['status' => 'active']);
    $sitz = Seat::factory()->for($customer)->create([
        'role' => 'member', 'nc_username' => 'anna@firma.tld',
        'nc_state' => Seat::STATE_SYNCED, 'nc_synced_at' => now(),
    ]);

    Livewire::actingAs($user)->test(Users::class)->call('setRole', $sitz->uuid, 'readonly');

    Queue::assertPushed(SyncSeatToNextcloud::class, fn ($auftrag) => $auftrag->action === 'role');
    expect($sitz->fresh()->nc_state)->toBe(Seat::STATE_PENDING);
});

it('gibt dem Inhaber nach einem Fehlschlag einen zweiten Versuch', function () {
    Queue::fake();
    // Die Gegenrichtung zur Mailwache an retry(): steht der Versand, wird
    // wieder eingeladen. Ohne diese Zeile bestünde der Test aus dem falschen
    // Grund nicht mehr — er wäre an der Wache gescheitert statt am Auftrag.
    mailversandEingerichtet();
    $customer = Customer::factory()->create();
    $user = $customer->ensureUser();
    Instance::factory()->for($customer)->create(['status' => 'active']);
    $sitz = Seat::factory()->for($customer)->create([
        'role' => 'member', 'nc_username' => 'anna@firma.tld',
        'nc_state' => Seat::STATE_FAILED, 'nc_error' => 'guest_failed',
    ]);

    Livewire::actingAs($user)->test(Users::class)->call('retry', $sitz->uuid);

    // Der Grund verschwindet mit dem neuen Versuch — ein stehengebliebener
    // alter Fehlertext neben „wird eingerichtet …" wäre die Attrappe zurück.
    Queue::assertPushed(SyncSeatToNextcloud::class, fn ($auftrag) => $auftrag->action === 'invite');
    expect($sitz->fresh()->nc_state)->toBe(Seat::STATE_PENDING)
        ->and($sitz->fresh()->nc_error)->toBeNull();
});

it('macht einen entzogenen Sitz nicht über das Sperren wieder aktiv', function () {
    // Der Umschalter in suspend() kannte nur zwei Zustände: an einer
    // entzogenen Zeile machte er beim ersten Klick 'suspended' und beim
    // zweiten 'active', samt `user:enable`. Der Mensch, dessen Zugang der
    // Inhaber beendet hat, könnte sich wieder anmelden — und er zählte
    // wieder gegen die Platzgrenze, die nur in addSeat() geprüft wird.
    Queue::fake();
    $customer = Customer::factory()->create();
    $user = $customer->ensureUser();
    Instance::factory()->for($customer)->create(['status' => 'active']);
    $sitz = Seat::factory()->for($customer)->create([
        'role' => 'member', 'status' => 'revoked', 'nc_username' => 'anna@firma.tld',
        'nc_state' => Seat::STATE_SYNCED, 'nc_synced_at' => now(),
    ]);

    $seite = Livewire::actingAs($user)->test(Users::class);
    $seite->call('suspend', $sitz->uuid);
    $seite->call('suspend', $sitz->uuid);

    expect($sitz->fresh()->status)->toBe('revoked');
    Queue::assertNothingPushed();
});

it('lädt einen entzogenen Sitz nicht erneut ein', function () {
    // Derselbe Weg zurück, nur über die andere Tür — und ebenfalls an der
    // Platzgrenze vorbei. Der richtige Weg ist ein NEUER Sitz.
    Queue::fake();
    mailversandEingerichtet();
    $customer = Customer::factory()->create();
    $user = $customer->ensureUser();
    Instance::factory()->for($customer)->create(['status' => 'active']);
    $sitz = Seat::factory()->for($customer)->create(['role' => 'member', 'status' => 'revoked']);

    Livewire::actingAs($user)->test(Users::class)->call('sendInvite', $sitz->uuid);

    Queue::assertNothingPushed();
    expect($sitz->fresh()->nc_state)->toBe(Seat::STATE_NONE)
        ->and($sitz->fresh()->nc_username)->toBeNull()
        ->and($sitz->fresh()->status)->toBe('revoked');
});

it('zeigt an einer entzogenen Zeile keinen Handlungsknopf mehr', function () {
    // Ein Knopf, den der Server ohnehin abweist, ist eine Einladung zum
    // Draufdrücken. Die Zeile sagt stattdessen, dass sie endgültig ist.
    $customer = Customer::factory()->create();
    $user = $customer->ensureUser();
    Instance::factory()->for($customer)->create(['status' => 'active']);
    $sitz = Seat::factory()->for($customer)->create(['role' => 'member', 'status' => 'revoked']);

    Livewire::actingAs($user)->test(Users::class)
        ->assertSee(__('users.revoked_final'))
        ->assertDontSee("sendInvite('{$sitz->uuid}')", escape: false)
        ->assertDontSee("suspend('{$sitz->uuid}')", escape: false)
        ->assertDontSee("retry('{$sitz->uuid}')", escape: false)
        ->assertDontSee('confirm-revoke-seat', escape: false)
        ->assertDontSee('edit-seat', escape: false);
});

it('sagt an einer entzogenen Zeile „entzogen", nicht „noch nicht eingeladen"', function () {
    // Die Zustandsspalte muss „entzogen" VOR jeden nc_state stellen. Dieser
    // Sitz steht auf `none` — ohne den Vorrang schriebe die Spalte „angelegt —
    // noch nicht eingeladen" an einen Zugang, den der Inhaber beendet hat, und
    // das läse sich wie ein Weg zurück.
    $customer = Customer::factory()->create();
    $user = $customer->ensureUser();
    Instance::factory()->for($customer)->create(['status' => 'active']);
    Seat::factory()->for($customer)->create(['role' => 'member', 'status' => 'revoked']);

    Livewire::actingAs($user)->test(Users::class)
        ->assertSee(__('users.status_revoked'))
        ->assertSee(__('users.revoked_closed'))
        ->assertDontSee(__('users.state_none'));
});

it('lädt den Inhaber nicht zu seiner eigenen Cloud ein', function () {
    // Dieses Konto legt die Bereitstellung an (CreateCustomerAdmin). Ein
    // Auftrag von hier aus träfe entweder auf einen bestehenden Benutzer —
    // oder legte, solange die Bereitstellung noch nicht so weit ist, ein
    // ZWEITES Konto in der Gruppe `admin` an, neben dem echten.
    Queue::fake();
    mailversandEingerichtet();
    $customer = Customer::factory()->create();
    $user = $customer->ensureUser();
    Instance::factory()->for($customer)->create(['status' => 'active']);
    $sitz = Seat::factory()->for($customer)->owner()->create();

    Livewire::actingAs($user)->test(Users::class)->call('sendInvite', $sitz->uuid);

    Queue::assertNothingPushed();
    expect($sitz->fresh()->nc_username)->toBeNull()
        ->and($sitz->fresh()->nc_state)->toBe(Seat::STATE_NONE);
});

it('verknüpft den Inhaber-Sitz beim Besuch, auch wenn die Instanz später dazukommt', function () {
    // /users verlangt keine fertige Instanz. Wer die Seite öffnet, bevor die
    // Bereitstellung das Admin-Konto angelegt hat, bekommt einen owner-Sitz
    // ohne Verknüpfung — und ohne diese Zeilen in mount() bliebe er es für
    // immer, während das Panel dem Inhaber anböte, sich SELBST einzuladen.
    // Der Aufruf muss deshalb bei JEDEM Besuch stattfinden, nicht nur beim
    // ersten.
    $customer = Customer::factory()->create();
    $user = $customer->ensureUser();

    // Erster Besuch, noch ohne Instanz: mount() legt den owner-Sitz an, findet
    // aber kein Konto zum Verknüpfen.
    Livewire::actingAs($user)->test(Users::class)->assertOk();
    $sitz = $customer->seats()->where('role', 'owner')->firstOrFail();
    expect($sitz->nc_username)->toBeNull();

    Instance::factory()->for($customer)->create(['status' => 'active', 'nc_admin_ref' => 'admin']);

    Livewire::actingAs($user)->test(Users::class)->assertOk();

    expect($sitz->fresh()->nc_username)->toBe('admin')
        ->and($sitz->fresh()->nc_state)->toBe(Seat::STATE_SYNCED);
});

it('lässt den Wiederholen-Knopf an einer entzogenen Zeile stehen, wenn das Sperren scheiterte', function () {
    // Der gefährlichste Halbschritt auf der anderen Seite: der Inhaber
    // entzieht, der `disable`-Auftrag scheitert (Gast aus, Tunnel weg), und das
    // Konto im Gast ist WEITER OFFEN, während die Zeile „entzogen" sagt. Es
    // gibt keinen Wiederholungslauf (tries = 1), keinen Abgleich im Zeitplan,
    // und diese Seite ist die einzige Stelle im Repo, die nc_state anzeigt —
    // ohne diesen Knopf fällt es nie wieder jemandem auf.
    $customer = Customer::factory()->create();
    $user = $customer->ensureUser();
    Instance::factory()->for($customer)->create(['status' => 'active']);
    $sitz = Seat::factory()->for($customer)->create([
        'role' => 'member', 'status' => 'revoked', 'nc_username' => 'anna@firma.tld',
        'nc_state' => Seat::STATE_FAILED, 'nc_error' => 'guest_failed', 'nc_synced_at' => now(),
    ]);

    Livewire::actingAs($user)->test(Users::class)
        ->assertSee(__('users.state_failed'))
        ->assertSee(__('users.error_guest_failed'))
        ->assertSee("retry('{$sitz->uuid}')", escape: false)
        // Und die Zeile sagt trotzdem, dass der Zugang geschlossen ist.
        ->assertSee(__('users.status_revoked'));
});

it('wiederholt an einem entzogenen Sitz das Sperren, nie das Einladen', function () {
    Queue::fake();
    $customer = Customer::factory()->create();
    $user = $customer->ensureUser();
    Instance::factory()->for($customer)->create(['status' => 'active']);
    $sitz = Seat::factory()->for($customer)->create([
        'role' => 'member', 'status' => 'revoked', 'nc_username' => 'anna@firma.tld',
        'nc_state' => Seat::STATE_FAILED, 'nc_error' => 'guest_failed', 'nc_synced_at' => now(),
    ]);

    Livewire::actingAs($user)->test(Users::class)->call('retry', $sitz->uuid);

    Queue::assertPushed(SyncSeatToNextcloud::class, fn ($auftrag) => $auftrag->action === 'disable');
    expect($sitz->fresh()->status)->toBe('revoked');
});

it('wiederholt nichts an einer entzogenen Zeile, die nie in der Nextcloud war', function () {
    // Anlegen → einladen → Auftrag scheitert an `no_instance` → entziehen.
    // queueSync() bricht korrekt ab, `failed` bleibt stehen, und der
    // Fehlschlag-Block ist zu Recht da. Ohne Vorbehalt liefe ein Druck aber auf
    // `user:disable` gegen einen Benutzer, den es nie gab: scheitert, dieselbe
    // rote Zeile, beliebig oft. Genau der Dauerfehler, nur über die andere
    // Tür — und mit einem veralteten Grund aus einem anderen Vorgang.
    Queue::fake();
    $customer = Customer::factory()->create();
    $user = $customer->ensureUser();
    Instance::factory()->for($customer)->create(['status' => 'active']);
    $sitz = Seat::factory()->for($customer)->create([
        'role' => 'member', 'status' => 'revoked', 'nc_username' => 'anna@firma.tld',
        'nc_state' => Seat::STATE_FAILED, 'nc_error' => 'no_instance', 'nc_synced_at' => null,
    ]);

    Livewire::actingAs($user)->test(Users::class)
        ->call('retry', $sitz->uuid)
        // Und der Knopf steht dort gar nicht erst.
        ->assertDontSee("retry('{$sitz->uuid}')", escape: false);

    Queue::assertNothingPushed();
});

it('wiederholt nichts an einem Inhaber-Sitz', function () {
    // retry() war der ungehärtete Zwilling von sendInvite(): an einem
    // beschädigten Inhaber-Sitz (failed, nie gespiegelt) wählte es die Aktion
    // `invite` und legte damit genau das an, was die Wache in sendInvite()
    // verhindern soll — ein ZWEITES Konto in der Gruppe `admin`.
    Queue::fake();
    $customer = Customer::factory()->create();
    $user = $customer->ensureUser();
    Instance::factory()->for($customer)->create(['status' => 'active']);
    $sitz = Seat::factory()->for($customer)->owner()->create([
        'nc_state' => Seat::STATE_FAILED, 'nc_error' => 'guest_failed',
    ]);

    Livewire::actingAs($user)->test(Users::class)->call('retry', $sitz->uuid);

    Queue::assertNothingPushed();
    expect($sitz->fresh()->nc_state)->toBe(Seat::STATE_FAILED);
});

it('nimmt einen entzogenen Sitz über das Anlegen-Formular wieder auf', function () {
    // Die empfohlene Vordertür muss offen sein. Vor dem Fix prallte dieselbe
    // Adresse an der Eindeutigkeit von (customer_id, email) ab — die Meldung
    // empfahl damit einen Weg, den es nicht gab.
    Queue::fake();
    $customer = Customer::factory()->create();
    $user = $customer->ensureUser();
    Instance::factory()->for($customer)->create(['status' => 'active']);
    $sitz = Seat::factory()->for($customer)->create([
        'email' => 'anna@firma.tld', 'role' => 'member', 'status' => 'revoked',
        'nc_username' => 'anna@firma.tld', 'nc_state' => Seat::STATE_SYNCED, 'nc_synced_at' => now(),
    ]);

    Livewire::actingAs($user)->test(Users::class)
        ->set('inviteEmail', 'anna@firma.tld')
        ->set('inviteRole', 'readonly')
        ->call('addSeat')
        ->assertHasNoErrors();

    // Dieselbe Zeile, kein Duplikat — und der Anmeldename bleibt, wie er ist:
    // Nextcloud kann Benutzer nicht umbenennen, und es ist dieselbe Person.
    expect($customer->seats()->where('email', 'anna@firma.tld')->count())->toBe(1)
        ->and($sitz->fresh()->status)->toBe('invited')
        ->and($sitz->fresh()->role)->toBe('readonly')
        ->and($sitz->fresh()->nc_username)->toBe('anna@firma.tld');

    // Angelegt ist nicht eingeladen — auch beim Wiederaufnehmen nicht.
    Queue::assertNothingPushed();
});

it('lässt einen wieder Aufgenommenen auch wirklich wieder hinein', function () {
    // Der ganze Weg in einer Prüfung: entziehen, wieder aufnehmen, einladen.
    // Zwei Dinge müssen dabei zugleich stimmen — die Zeile darf nicht
    // „Eingeladen" behaupten, solange niemand eingeladen hat, und die Sperre,
    // die revoke() in den Gast geschickt hat, muss beim Einladen wieder
    // aufgehen. `user:welcome` allein tut das nicht.
    Queue::fake();
    mailversandEingerichtet();
    $pve = new FakeProxmoxClient;
    app()->instance(ProxmoxClient::class, $pve);
    $customer = Customer::factory()->create();
    $user = $customer->ensureUser();
    Instance::factory()->for($customer)->create(['status' => 'active', 'vmid' => 201, 'host_id' => Host::factory()]);
    $sitz = Seat::factory()->for($customer)->create([
        'email' => 'anna@firma.tld', 'role' => 'member', 'status' => 'active',
        'nc_username' => 'anna@firma.tld', 'nc_state' => Seat::STATE_SYNCED, 'nc_synced_at' => now()->subYear(),
    ]);

    $seite = Livewire::actingAs($user)->test(Users::class);
    $seite->call('revoke', $sitz->uuid);
    $seite->set('inviteEmail', 'anna@firma.tld')->set('inviteRole', 'member')
        ->call('addSeat')->assertHasNoErrors();

    expect($sitz->fresh()->nc_state)->toBe(Seat::STATE_NONE)
        ->and($sitz->fresh()->nc_error)->toBeNull()
        // Anmeldename und Ankunftszeitpunkt bleiben: der Name gehört dieser
        // Person, und dass es das Konto im Gast gibt, bleibt wahr.
        ->and($sitz->fresh()->nc_username)->toBe('anna@firma.tld')
        ->and($sitz->fresh()->nc_synced_at)->not->toBeNull();

    // Und die Zeile sagt es auch: „angelegt — noch nicht eingeladen", mit dem
    // Knopf „Einladen" statt „Erneut senden".
    $seite->assertSee(__('users.state_none'))
        ->assertSee(__('users.invite'))
        ->assertDontSee(__('users.status_invited'));

    $seite->call('sendInvite', $sitz->uuid);
    (new SyncSeatToNextcloud($sitz->uuid, 'invite'))->handle(app(NextcloudUsers::class));

    expect($pve->guestRan('user:enable'))->toBeTrue()
        ->and($sitz->fresh()->nc_state)->toBe(Seat::STATE_SYNCED);
});

it('weist die Wiederaufnahme ab, wenn kein Platz mehr frei ist', function () {
    // Der Grund, warum es die Vordertür sein muss und kein Knopf an der Zeile:
    // hier steht die Platzprüfung. Ein entzogener Sitz zählt nicht mit, ein
    // wiederaufgenommener schon.
    $customer = Customer::factory()->create();
    $user = $customer->ensureUser();
    Instance::factory()->for($customer)->create(['status' => 'active']);
    Seat::factory()->for($customer)->count(5)->create(['role' => 'member']);
    $sitz = Seat::factory()->for($customer)->create([
        'email' => 'anna@firma.tld', 'role' => 'member', 'status' => 'revoked',
    ]);

    Livewire::actingAs($user)->test(Users::class)
        ->set('inviteEmail', 'anna@firma.tld')
        ->call('addSeat')
        ->assertHasErrors(['inviteEmail']);

    expect($sitz->fresh()->status)->toBe('revoked');
});

it('weist eine Adresse ab, die noch einen lebenden Platz hat', function () {
    // Die Wiederaufnahme gilt NUR für entzogene Zeilen. Ein aktiver Sitz
    // derselben Adresse bleibt ein Duplikat — sonst wäre das Formular ein
    // stiller Rollenwechsel an einer bestehenden Zeile.
    $customer = Customer::factory()->create();
    $user = $customer->ensureUser();
    Instance::factory()->for($customer)->create(['status' => 'active']);
    $sitz = Seat::factory()->for($customer)->create([
        'email' => 'anna@firma.tld', 'role' => 'member', 'status' => 'active',
    ]);

    Livewire::actingAs($user)->test(Users::class)
        ->set('inviteEmail', 'anna@firma.tld')
        ->set('inviteRole', 'admin')
        ->call('addSeat')
        ->assertHasErrors(['inviteEmail']);

    expect($sitz->fresh()->role)->toBe('member');
});

// --- K1: entziehen, während die Einladung noch in der Warteschlange steht.
// Der einzige Arbeiter teilt sich die Warteschlange mit der bezahlten
// Bereitstellung — das Fenster ist MINUTEN lang, nicht Millisekunden.

it('schickt die Sperre auch dann, wenn die Einladung noch unterwegs ist', function () {
    // Der dritte Wächter-Fall in queueSync(): `none` und `failed` waren
    // abgedeckt, `pending` nicht — und das ist der einzige Zustand, in dem
    // noch ein Auftrag unterwegs ist. Ohne diesen Zweig kam an der Zeile
    // „Entfernt" ein aktives, freigeschaltetes Konto in der Cloud heraus, und
    // der Inhaber hatte im Portal keinen Weg mehr, die Sperre nachzuschicken:
    // kein Wiederholen-Knopf (nicht `failed`), kein Sperren-Knopf (entzogen).
    Queue::fake();
    $customer = Customer::factory()->create();
    $user = $customer->ensureUser();
    Instance::factory()->for($customer)->create(['status' => 'active']);
    $sitz = Seat::factory()->for($customer)->create([
        'role' => 'member', 'status' => 'invited', 'nc_username' => 'anna@firma.tld',
        'nc_state' => Seat::STATE_PENDING, 'nc_synced_at' => null,
    ]);

    Livewire::actingAs($user)->test(Users::class)->call('revoke', $sitz->uuid);

    Queue::assertPushed(SyncSeatToNextcloud::class, fn ($auftrag) => $auftrag->action === 'disable');
});

it('sperrt ein Konto, das erst nach dem Entziehen entsteht', function () {
    // Der ganze Weg, rein durch Klicken erreichbar: einladen, entziehen
    // solange der Auftrag wartet, Auftrag ausführen. Zwischen Absenden und
    // Ausführen hat sich die Absicht geändert — der Auftrag darf dann nicht
    // blind ausführen, was beim Absenden galt.
    Queue::fake();
    mailversandEingerichtet();
    $pve = new FakeProxmoxClient;
    // Der Benutzer existiert im Gast noch nicht: es läuft ein `user:add`,
    // nicht nur ein `user:welcome` — der beschriebene Ablauf.
    $pve->guestScript('user:info', 1);
    app()->instance(ProxmoxClient::class, $pve);
    $customer = Customer::factory()->create();
    $user = $customer->ensureUser();
    Instance::factory()->for($customer)->create(['status' => 'active', 'vmid' => 201, 'host_id' => Host::factory()]);
    $sitz = Seat::factory()->for($customer)->create(['role' => 'member', 'email' => 'anna@firma.tld']);

    $seite = Livewire::actingAs($user)->test(Users::class);
    $seite->call('sendInvite', $sitz->uuid);
    $seite->call('revoke', $sitz->uuid);

    (new SyncSeatToNextcloud($sitz->uuid, 'invite'))->handle(app(NextcloudUsers::class));

    expect($pve->guestRan('user:add'))->toBeTrue()
        ->and($pve->guestRan('user:disable'))->toBeTrue()
        // Und die Zeile darf danach stimmen: entzogen UND gespiegelt.
        ->and($sitz->fresh()->status)->toBe('revoked')
        ->and($sitz->fresh()->nc_state)->toBe(Seat::STATE_SYNCED);
});

it('schickt bei einem inzwischen entzogenen Sitz kein Entsperren mit', function () {
    // Eine Wiederaufnahme entsperrt das alte Konto. Wurde der Zugang
    // inzwischen wieder entzogen, hiesse das: eine Tür aufmachen, um sie im
    // selben Auftrag wieder zuzuziehen — im besten Fall folgenlos, im
    // schlechteren ein offenes Zeitfenster.
    $pve = new FakeProxmoxClient;
    app()->instance(ProxmoxClient::class, $pve);
    $customer = Customer::factory()->create();
    Instance::factory()->for($customer)->create(['status' => 'active', 'vmid' => 201, 'host_id' => Host::factory()]);
    $sitz = Seat::factory()->for($customer)->create([
        'role' => 'member', 'status' => 'revoked', 'nc_username' => 'anna@firma.tld',
        'nc_state' => Seat::STATE_PENDING, 'nc_synced_at' => now()->subYear(),
    ]);

    (new SyncSeatToNextcloud($sitz->uuid, 'invite'))->handle(app(NextcloudUsers::class));

    expect($pve->guestRan('user:enable'))->toBeFalse()
        ->and($pve->guestRan('user:disable'))->toBeTrue();
});

// --- K2: `retry()` kannte `enable` nicht.

it('wiederholt nach einem gescheiterten Entsperren nicht die Rolle', function () {
    // Gesperrter Sitz → Entsperren → suspend() setzt `status` auf 'active' und
    // schickt `enable`; der Gast antwortet nicht. Die Ableitung in retry()
    // kannte `enable` nicht und wählte `role`: der Auftrag fuhr Gruppen und
    // Quota, gelang, und die Zeile zeigte „Aktiv" — während `user:enable` nie
    // geschickt wurde und der Mitarbeiter weiterhin nicht hineinkam. Der
    // Wiederholen-Knopf war danach verschwunden.
    Queue::fake();
    $customer = Customer::factory()->create();
    $user = $customer->ensureUser();
    Instance::factory()->for($customer)->create(['status' => 'active']);
    $sitz = Seat::factory()->for($customer)->create([
        'role' => 'member', 'status' => 'active', 'nc_username' => 'anna@firma.tld',
        'nc_state' => Seat::STATE_FAILED, 'nc_error' => 'guest_failed', 'nc_synced_at' => now(),
    ]);

    Livewire::actingAs($user)->test(Users::class)->call('retry', $sitz->uuid);

    Queue::assertPushed(SyncSeatToNextcloud::class, fn ($auftrag) => $auftrag->action === 'restore');
});

it('wiederholt auch nach einer Wiederaufnahme, deren Entsperren scheiterte', function () {
    // Die zweite Tür, dieselbe Ursache: scheitert das `enable` INNERHALB des
    // invite-Auftrags, blieb der Status 'invited' — die Ableitung wählte
    // wieder `role`, das Konto blieb gesperrt, und die Zeile sagte
    // „Eingeladen".
    Queue::fake();
    $customer = Customer::factory()->create();
    $user = $customer->ensureUser();
    Instance::factory()->for($customer)->create(['status' => 'active']);
    $sitz = Seat::factory()->for($customer)->create([
        'role' => 'member', 'status' => 'invited', 'nc_username' => 'anna@firma.tld',
        'nc_state' => Seat::STATE_FAILED, 'nc_error' => 'guest_failed', 'nc_synced_at' => now()->subYear(),
    ]);

    Livewire::actingAs($user)->test(Users::class)->call('retry', $sitz->uuid);

    Queue::assertPushed(SyncSeatToNextcloud::class, fn ($auftrag) => $auftrag->action === 'restore');
});

it('macht beim Wiederholen einer offenen Zeile die Tür auf UND setzt die Rolle', function () {
    // Warum `restore` und nicht `enable`: welcher der beiden Befehle beim
    // letzten Mal scheiterte, weiss dieser Sitz nicht — und ein zweites Feld
    // dafür wäre eine weitere Behauptung über die Cloud des Kunden, die
    // irgendwann nicht mehr stimmt. Beide zu schicken ist billiger und
    // ehrlicher als es zu raten.
    $pve = new FakeProxmoxClient;
    app()->instance(ProxmoxClient::class, $pve);
    $customer = Customer::factory()->create();
    Instance::factory()->for($customer)->create(['status' => 'active', 'vmid' => 201, 'host_id' => Host::factory()]);
    $sitz = Seat::factory()->for($customer)->create([
        'role' => 'member', 'nc_username' => 'anna@firma.tld', 'nc_synced_at' => now(),
    ]);

    (new SyncSeatToNextcloud($sitz->uuid, 'restore'))->handle(app(NextcloudUsers::class));

    expect($pve->guestRan('user:enable'))->toBeTrue()
        ->and($pve->guestRan('group:adduser'))->toBeTrue()
        ->and($sitz->fresh()->nc_state)->toBe(Seat::STATE_SYNCED);
});

it('lässt das Entsperren aus dem Sperren-Knopf ein reines Entsperren bleiben', function () {
    // suspend() bleibt unangetastet: dort IST bekannt, was fehlt. Die Rolle
    // wurde auch während der Sperre mitgeführt (setRole schickt `role`), also
    // wäre sie hier fünf folgenlose Befehle im Gast — und ein folgenloser
    // Befehl ist einer, den niemand mehr erklären kann, wenn er eines Tages
    // doch etwas tut.
    Queue::fake();
    $customer = Customer::factory()->create();
    $user = $customer->ensureUser();
    Instance::factory()->for($customer)->create(['status' => 'active']);
    $sitz = Seat::factory()->for($customer)->create([
        'role' => 'member', 'status' => 'suspended', 'nc_username' => 'anna@firma.tld',
        'nc_state' => Seat::STATE_SYNCED, 'nc_synced_at' => now(),
    ]);

    Livewire::actingAs($user)->test(Users::class)->call('suspend', $sitz->uuid);

    Queue::assertPushed(SyncSeatToNextcloud::class, fn ($auftrag) => $auftrag->action === 'enable');
});

// --- W2: der Inhaber-Sitz.

it('entzieht den Inhaber-Sitz auch dann nicht, wenn es einen zweiten gibt', function () {
    // Die Wache in revoke() zählte nur owner-Sitze. Über setRole() liess sich
    // ein zweiter owner erzeugen — danach griff sie nicht mehr, und der
    // ECHTE Inhaber bekam `user:disable admin` samt
    // `user:auth-tokens:delete admin` in seine eigene Cloud. Vor diesem Umbau
    // war revoke() folgenlos; jetzt sperrt es wirklich aus.
    Queue::fake();
    $customer = Customer::factory()->create();
    $user = $customer->ensureUser();
    Instance::factory()->for($customer)->create(['status' => 'active']);
    $inhaber = Seat::factory()->for($customer)->owner()->create([
        'email' => $customer->email, 'nc_username' => 'admin',
        'nc_state' => Seat::STATE_SYNCED, 'nc_synced_at' => now(),
    ]);
    Seat::factory()->for($customer)->owner()->create(['email' => 'zweiter@firma.tld']);

    Livewire::actingAs($user)->test(Users::class)->call('revoke', $inhaber->uuid);

    expect($inhaber->fresh()->status)->toBe('active');
    Queue::assertNothingPushed();
});

it('macht aus einem Mitarbeiter keinen Inhaber', function () {
    // `setRole()` nahm jede Rolle aus Seat::ROLES an, also auch `owner`. Das
    // Auswahlfeld bietet sie nicht an — die Livewire-Methode ist trotzdem
    // öffentlich erreichbar, und genau das war der erste Schritt, mit dem
    // sich der Inhaber selbst aussperren liess.
    Queue::fake();
    $customer = Customer::factory()->create();
    $user = $customer->ensureUser();
    Instance::factory()->for($customer)->create(['status' => 'active']);
    $sitz = Seat::factory()->for($customer)->create(['role' => 'member']);

    Livewire::actingAs($user)->test(Users::class)->call('setRole', $sitz->uuid, 'owner');

    expect($sitz->fresh()->role)->toBe('member');
});

it('nimmt dem Inhaber-Sitz seine Rolle auch dann nicht, wenn es einen zweiten gibt', function () {
    // Dieselbe Klasse über die andere Tür: `setRole(<inhaberSitz>, 'member')`
    // — das Admin-Konto verlöre seine Adminrechte im eigenen Gast.
    Queue::fake();
    $customer = Customer::factory()->create();
    $user = $customer->ensureUser();
    Instance::factory()->for($customer)->create(['status' => 'active']);
    $inhaber = Seat::factory()->for($customer)->owner()->create([
        'email' => $customer->email, 'nc_username' => 'admin',
        'nc_state' => Seat::STATE_SYNCED, 'nc_synced_at' => now(),
    ]);
    Seat::factory()->for($customer)->owner()->create(['email' => 'zweiter@firma.tld']);

    Livewire::actingAs($user)->test(Users::class)->call('setRole', $inhaber->uuid, 'member');

    expect($inhaber->fresh()->role)->toBe('owner');
    Queue::assertNothingPushed();
});

// --- Der Knopf, der eine Mail verspricht, die niemand verschicken kann.

it('verschickt keine Einladung, solange der Mailversand nicht eingerichtet ist', function () {
    // Die Einladung verschickt NEXTCLOUD, nicht CluPilot — nur so entsteht das
    // Passwort dort, wo niemand sonst es zu sehen bekommt. Ohne eingerichteten
    // Versand kommt aber keine Mail heraus, und `occ user:add
    // --generate-password --email` GELINGT trotzdem: Nextcloud legt das Konto
    // an, versucht die Willkommensmail, protokolliert intern einen Fehler und
    // beendet mit 0.
    //
    // Die Zeile stünde also auf „Eingeladen", die Meldung verspräche einen
    // Link, an dem der Mitarbeiter sein Passwort selbst setzt — und niemand
    // bekäme etwas. Dieselbe Attrappe, gegen die dieses ganze Feature gebaut
    // wurde, eine Schicht tiefer.
    //
    // Kein ausgefülltes Postfach in dieser Fixture: das ist der Zustand, in
    // dem JEDE Instanz am ersten Tag steht, solange der Mailserver dieses
    // Produkts noch nicht existiert.
    Queue::fake();
    $customer = Customer::factory()->create();
    $user = $customer->ensureUser();
    Instance::factory()->for($customer)->create(['status' => 'active']);
    $sitz = Seat::factory()->for($customer)->create(['role' => 'member']);

    Livewire::actingAs($user)->test(Users::class)
        ->call('sendInvite', $sitz->uuid)
        // Kein Erfolg gemeldet. Auf den Satz selbst geprüft, nicht auf
        // irgendeine Meldung: „Einladung verschickt" ist genau das
        // Versprechen, das hier nicht zu halten ist.
        ->assertDispatched('notify', fn ($ereignis, $daten) => $daten['message'] !== __('users.invite_sent'));

    // Kein Auftrag, und der Sitz bleibt, wie er war — insbesondere ohne
    // Anmeldenamen, denn den vergibt erst das Einladen.
    Queue::assertNothingPushed();
    expect($sitz->fresh()->nc_state)->toBe(Seat::STATE_NONE)
        ->and($sitz->fresh()->nc_username)->toBeNull();
});

it('wiederholt keine Einladung, solange der Mailversand nicht eingerichtet ist', function () {
    // Dieselbe Attrappe über die andere Tür. Der Zweig
    // `nc_synced_at === null => 'invite'` in retry() schickt genau dasselbe
    // `occ user:add --generate-password --email` wie sendInvite() — und war
    // ungewacht.
    //
    // Der Weg dorthin ist am ersten Tag rein durch Klicken erreichbar:
    //
    //  1. Die Instanz ist noch in Bereitstellung. /users ist trotzdem offen —
    //     `customer.active` prüft den Kunden, nicht die Instanz.
    //  2. Ohne AKTIVE Instanz liefert mailversandBereit() `true`. Also kein
    //     Hinweis und keine Wache: „Einladen" meldet „Einladung verschickt …".
    //  3. Der Auftrag scheitert an `no_instance`, die Zeile wird rot, der
    //     Wiederholen-Knopf erscheint.
    //  4. Die Bereitstellung wird fertig. Der Mailversand ist weiter nicht
    //     eingerichtet — der Hinweis erscheint jetzt.
    //  5. Ein Klick legte das Konto an, occ beendete mit 0, die Plakette sagte
    //     „Eingeladen". Niemand bekam etwas.
    //
    // Derselbe Ausgang ohne Bereitstellung: ein einmal eingerichtetes
    // Versandkonto wird abgeschaltet (Anbieterwechsel, Passwortwechsel), und
    // jede rote Zeile ohne `nc_synced_at` führt beim Wiederholen dorthin.
    Queue::fake();
    $customer = Customer::factory()->create();
    $user = $customer->ensureUser();
    $instance = Instance::factory()->for($customer)->create(['status' => 'provisioning']);
    $sitz = Seat::factory()->for($customer)->create(['role' => 'member']);

    // Schritt 1 und 2: die Einladung geht hinaus, weil es noch nichts gibt,
    // worüber sich etwas sagen liesse.
    Livewire::actingAs($user)->test(Users::class)->call('sendInvite', $sitz->uuid);

    // Schritt 3: der Auftrag findet keine Instanz.
    (new SyncSeatToNextcloud($sitz->uuid, 'invite'))->handle(app(NextcloudUsers::class));

    expect($sitz->fresh()->nc_state)->toBe(Seat::STATE_FAILED)
        ->and($sitz->fresh()->nc_error)->toBe('no_instance')
        ->and($sitz->fresh()->nc_synced_at)->toBeNull();

    // Schritt 4: die Bereitstellung wird fertig, der Versand steht weiter nicht.
    $instance->update(['status' => 'active']);

    // Schritt 5: und hier muss Schluss sein.
    Livewire::actingAs($user)->test(Users::class)
        ->call('retry', $sitz->uuid)
        ->assertDispatched('notify', fn ($ereignis, $daten) => $daten['message'] !== __('users.retrying'));

    // Nur die erste Einladung aus Schritt 1, keine zweite — und die Zeile
    // bleibt rot, statt auf „wird eingerichtet …" zu springen.
    Queue::assertPushed(SyncSeatToNextcloud::class, 1);
    expect($sitz->fresh()->nc_state)->toBe(Seat::STATE_FAILED);
});

it('sagt es auf der Seite, statt den Knopf verschwinden zu lassen', function () {
    // Der Hinweis steht dort, wo der Inhaber ihn VOR dem Klicken liest. Der
    // Knopf bleibt: die Einladen-Spalte an jeder Zeile verschwinden zu lassen
    // liest sich nicht als „geht hier gerade nicht", sondern als „das kann
    // dieses Produkt nicht" — genau die Beschwerde, die schon einmal zur
    // immer sichtbaren Aktionsspalte geführt hat.
    $customer = Customer::factory()->create();
    $user = $customer->ensureUser();
    Instance::factory()->for($customer)->create(['status' => 'active']);
    $sitz = Seat::factory()->for($customer)->create(['role' => 'member']);

    Livewire::actingAs($user)->test(Users::class)
        ->assertSee(__('users.mail_not_ready'))
        ->assertSee("sendInvite('{$sitz->uuid}')", escape: false);
});

it('laedt wieder ein, sobald der Versand steht', function () {
    // Die Gegenrichtung: die Wache darf nicht mehr abweisen, als sie soll.
    Queue::fake();
    mailversandEingerichtet();
    $customer = Customer::factory()->create();
    $user = $customer->ensureUser();
    Instance::factory()->for($customer)->create(['status' => 'active']);
    $sitz = Seat::factory()->for($customer)->create(['role' => 'member']);

    Livewire::actingAs($user)->test(Users::class)
        ->call('sendInvite', $sitz->uuid)
        ->assertDontSee(__('users.mail_not_ready'));

    Queue::assertPushed(SyncSeatToNextcloud::class, fn ($auftrag) => $auftrag->action === 'invite');
});

it('verbraucht fuer eine abgewiesene Einladung kein Kontingent', function () {
    // Die Wache steht VOR dem Ratelimit. Sonst hätte ein Inhaber, dessen
    // Versand noch nicht steht, seine drei Versuche an dieser Zeile
    // verbraucht, bevor überhaupt einer hinausgehen konnte — und müsste
    // warten, sobald der Betrieb den Versand einrichtet.
    Queue::fake();
    $customer = Customer::factory()->create();
    $user = $customer->ensureUser();
    Instance::factory()->for($customer)->create(['status' => 'active']);
    $sitz = Seat::factory()->for($customer)->create(['role' => 'member']);

    $seite = Livewire::actingAs($user)->test(Users::class);

    foreach (range(1, 3) as $ignoriert) {
        $seite->call('sendInvite', $sitz->uuid);
    }

    mailversandEingerichtet();
    $seite->call('sendInvite', $sitz->uuid);

    Queue::assertPushed(SyncSeatToNextcloud::class, 1);
});

it('legt weiterhin Sitze an, auch ohne Versand', function () {
    // Anlegen verspricht ausdrücklich KEINE Mail — ein Inhaber soll sein Team
    // vorbereiten können, ohne dass jemand eine bekommt. Die Wache gehört
    // deshalb an „Einladen" und nirgendwo sonst.
    Queue::fake();
    $customer = Customer::factory()->create();
    $user = $customer->ensureUser();
    Instance::factory()->for($customer)->create(['status' => 'active']);

    Livewire::actingAs($user)->test(Users::class)
        ->set('inviteEmail', 'anna@firma.tld')
        ->call('addSeat')
        ->assertHasNoErrors();

    expect($customer->seats()->where('email', 'anna@firma.tld')->exists())->toBeTrue();
});

it('nennt in keiner Datei unter app/ das Löschen eines Benutzers', function () {
    // Testerzwungene Regel: kein Nextcloud-Benutzer wird je gelöscht, und
    // keine Datei. Wer das ändern will, muss diese Prüfung anfassen und
    // dabei über die Folgen stolpern.
    $treffer = [];

    foreach (Finder::create()->files()->in(app_path())->name('*.php') as $datei) {
        if (str_contains($datei->getContents(), 'user:delete')) {
            $treffer[] = $datei->getRelativePathname();
        }
    }

    expect($treffer)->toBe([]);
});
