<?php

use App\Models\Host;
use App\Models\ProvisioningRun;
use App\Provisioning\RunRunner;
use App\Services\Ssh\FakeRemoteShell;
use App\Services\Ssh\RemoteShell;
use Illuminate\Support\Facades\Queue;

// QUEUE_CONNECTION ist in dieser Suite `sync`: ohne das hier faehrt jedes
// `AdvanceRunJob::dispatch()` den Lauf sofort im Test, gegen eine echte,
// unerreichbare Adresse — zwei Minuten TCP-Zeitueberlauf je Host. Die Laeufe,
// die dieser Test wirklich fahren will, stoesst er unten selbst an.
beforeEach(fn () => Queue::fake());

/**
 * Der Nachzieh-Griff für Hosts, die vor dem Frühwarnsystem übernommen wurden.
 *
 * Ihr /etc/nftables.conf kennt `clupilot_blocked` nicht, dort scheitert jedes
 * `nft add element` — und die Sperre steht in Datenbank, Portal, Konsole und in
 * der Mail an den Kunden als aktiv, in der Firewall aber nie. Der
 * Übernahme-Lauf hilft nicht: `SecureHostFirewall` kürzt sich über den
 * `host_firewall`-Brotkrumen ab, und der hängt am LAUF, nicht am Host — genau
 * das behauptete das Runbook bis zu dieser Fix-Welle fälschlich.
 */
function uebernommenerHost(array $attributes = []): Host
{
    return Host::factory()->active()->create(array_merge([
        'node' => 'pve',
        'ssh_host_key' => 'SHA256:abc',
    ], $attributes));
}

it('startet je uebernommenem Host einen host-firewall-Lauf', function () {
    $host = uebernommenerHost();

    $this->artisan('clupilot:refresh-host-firewall')->assertSuccessful();

    $run = ProvisioningRun::query()->where('subject_id', $host->id)->firstOrFail();

    expect($run->pipeline)->toBe('host-firewall')
        ->and($run->subject_type)->toBe(Host::class)
        ->and($run->current_step)->toBe(0);
});

it('schreibt das Regelwerk MIT den Sperrmengen, obwohl der Host laengst uebernommen ist', function () {
    // Der eigentliche Punkt: der Brotkrumen des ALTEN Laufs darf den neuen
    // nicht abkuerzen. Ohne den eigenen Lauf taete der Schritt nichts und
    // meldete trotzdem „erledigt".
    $shell = new FakeRemoteShell;
    app()->instance(RemoteShell::class, $shell);

    $host = uebernommenerHost();

    // Der Brotkrumen der Uebernahme, an ihrem eigenen Lauf.
    $alt = ProvisioningRun::factory()->forHost($host)->create([
        'status' => ProvisioningRun::STATUS_COMPLETED,
    ]);
    $alt->resources()->create([
        'host_id' => $host->id,
        'kind' => 'host_firewall',
        'external_id' => 'nftables',
    ]);

    $this->artisan('clupilot:refresh-host-firewall')->assertSuccessful();

    $neu = ProvisioningRun::query()->where('pipeline', 'host-firewall')->firstOrFail();
    app(RunRunner::class)->advance($neu);

    $regelwerk = $shell->files()['/etc/nftables.conf'] ?? '';

    expect($regelwerk)->toContain('clupilot_blocked')
        ->and($neu->fresh()->status)->toBe(ProvisioningRun::STATUS_COMPLETED);
});

it('laesst einen Host in Ruhe, der keinen Tunnel hat', function () {
    // Der Schritt verbindet ueber die WireGuard-Adresse und prueft den Tunnel
    // von der Hostseite. Ohne beides gibt es nichts nachzuziehen.
    Host::factory()->create(['status' => 'pending']);

    $this->artisan('clupilot:refresh-host-firewall')->assertSuccessful();

    expect(ProvisioningRun::query()->where('pipeline', 'host-firewall')->count())->toBe(0);
});

it('faengt einen Host nicht zweimal an, solange ein Lauf offen ist', function () {
    $host = uebernommenerHost();
    ProvisioningRun::factory()->forHost($host)->create([
        'status' => ProvisioningRun::STATUS_RUNNING,
    ]);

    $this->artisan('clupilot:refresh-host-firewall')->assertSuccessful();

    expect(ProvisioningRun::query()->where('pipeline', 'host-firewall')->count())->toBe(0);
});

it('aendert unter --dry-run gar nichts', function () {
    uebernommenerHost();

    $this->artisan('clupilot:refresh-host-firewall', ['--dry-run' => true])->assertSuccessful();

    expect(ProvisioningRun::count())->toBe(0);
});

it('nimmt mit --host genau einen', function () {
    $gemeint = uebernommenerHost(['name' => 'pve-fsn-07']);
    uebernommenerHost(['name' => 'pve-fsn-08']);

    $this->artisan('clupilot:refresh-host-firewall', ['--host' => 'pve-fsn-07'])->assertSuccessful();

    expect(ProvisioningRun::query()->where('pipeline', 'host-firewall')->pluck('subject_id')->all())
        ->toBe([$gemeint->id]);
});

it('stellt einen laufenden Host NICHT auf error, wenn der Nachzieh-Lauf scheitert', function () {
    // Deshalb eine eigene Pipeline und nicht 'host': RunRunner::failRun()
    // loest den Subjekt-Haken nur aus, wenn der gescheiterte Lauf DER Lauf des
    // Subjekts ist. Ein Nachziehen darf einen bezahlten, laufenden Host nicht
    // als kaputt markieren.
    $shell = new FakeRemoteShell;
    $shell->failConnect = true;
    app()->instance(RemoteShell::class, $shell);

    $host = uebernommenerHost();

    $this->artisan('clupilot:refresh-host-firewall')->assertSuccessful();

    $run = ProvisioningRun::query()->where('pipeline', 'host-firewall')->firstOrFail();

    // Bis zum Ende der Versuche fahren, statt eine Zahl zu raten: was hier
    // zaehlt, ist der Zustand des HOSTS, nachdem der Lauf endgueltig
    // gescheitert ist.
    for ($i = 0; $i < 20 && ! in_array($run->fresh()->status, [ProvisioningRun::STATUS_FAILED, ProvisioningRun::STATUS_COMPLETED], true); $i++) {
        $run->forceFill(['next_attempt_at' => null])->save();
        app(RunRunner::class)->advance($run);
    }

    expect($run->fresh()->status)->toBe(ProvisioningRun::STATUS_FAILED)
        ->and($host->fresh()->status)->toBe('active');
});
