<?php

use App\Livewire\Admin\ProxyHosts;
use App\Models\Operator;
use App\Models\ProxyHost;
use App\Services\Proxy\CertificateInspector;
use App\Support\Settings;
use Livewire\Livewire;

function proxyPage(): \Livewire\Features\SupportTesting\Testable
{
    return Livewire::actingAs(Operator::factory()->role('Owner')->create(), 'operator')->test(ProxyHosts::class);
}

it('adds a hostname and lists it', function () {
    proxyPage()
        ->set('hostname', 'files.clupilot.com')
        ->set('purpose', 'public')
        ->call('add')
        ->assertSee('files.clupilot.com');

    expect(ProxyHost::query()->where('hostname', 'files.clupilot.com')->exists())->toBeTrue();
});

/**
 * Der Dienstbenutzer wählt NAMEN, nicht Konfiguration. Wäre hier alles erlaubt,
 * was durch die Prüfung schlüpft, könnte über den root-eigenen Helfer Caddy
 * beigebracht werden, was immer der Proxy kann — und `install-agent.sh` hält
 * fest, warum das die Freigabe wertlos machte.
 */
it('refuses anything that is not a hostname', function () {
    foreach ([
        'files.clupilot.com {\n reverse_proxy evil',
        'https://files.clupilot.com',
        'files.clupilot.com/pfad',
        'kein punkt drin',
        '-anfang.clupilot.com',
        '../../etc/passwd',
    ] as $attempt) {
        proxyPage()->set('hostname', $attempt)->call('add')->assertHasErrors('hostname');
    }

    // Nur die von Hand angelegten zählen: die Seite gleicht beim Öffnen die
    // Namen aus der Umgebung ein, also ist die Tabelle nie leer.
    expect(ProxyHost::query()->where('source', 'manual')->count())->toBe(0);
});

it('refuses the same hostname twice', function () {
    ProxyHost::create(['hostname' => 'files.clupilot.com', 'purpose' => 'public']);

    proxyPage()->set('hostname', 'files.clupilot.com')->call('add')->assertHasErrors('hostname');
});

/**
 * Entfernen nimmt den Namen aus der Liste, NICHT aus dem laufenden Proxy. Zwei
 * Entscheidungen in einem Klick, und die zweite nimmt eine Seite vom Netz.
 */
it('removes from the list without touching the running proxy', function () {
    $host = ProxyHost::create(['hostname' => 'alt.clupilot.com', 'purpose' => 'public']);

    proxyPage()->call('remove', $host->id)->assertSee(__('proxy.removed'));

    expect(ProxyHost::query()->where('source', 'manual')->count())->toBe(0);
});

/**
 * Der Zustand wird GEMESSEN. Das ist der ganze Grund für diese Seite: bei
 * `files.clupilot.com` waren DNS, Umgebungsvariable und Maschine richtig, und
 * es gab trotzdem kein Zertifikat — keine der drei Einstellungen hätte das
 * verraten.
 */
it('records what the certificate check actually found', function () {
    ProxyHost::create(['hostname' => 'gut.clupilot.com', 'purpose' => 'public']);
    ProxyHost::create(['hostname' => 'kaputt.clupilot.com', 'purpose' => 'public']);

    $this->swap(CertificateInspector::class, new class extends CertificateInspector
    {
        public function inspect(string $hostname, int $timeoutSeconds = 8): array
        {
            return $hostname === 'gut.clupilot.com'
                ? ['ok' => true, 'expires_at' => now()->addDays(75), 'issuer' => "Let's Encrypt", 'error' => null]
                : ['ok' => false, 'expires_at' => null, 'issuer' => null, 'error' => 'tlsv1 alert internal error'];
        }
    });

    proxyPage()->call('checkCertificates');

    $gut = ProxyHost::query()->where('hostname', 'gut.clupilot.com')->first();
    $kaputt = ProxyHost::query()->where('hostname', 'kaputt.clupilot.com')->first();

    expect($gut->daysLeft())->toBeGreaterThan(70)
        ->and($gut->isExpiringSoon())->toBeFalse()
        ->and($gut->certificate_error)->toBeNull()
        ->and($kaputt->certificate_expires_at)->toBeNull()
        ->and($kaputt->certificate_error)->toContain('internal error');
});

/**
 * Dreißig Tage: bei neunzig Tagen Laufzeit hätte die Erneuerung längst laufen
 * müssen. Was darunter steht, ist kein knappes Zertifikat, sondern eine
 * Erneuerung, die nicht stattfindet.
 */
it('calls a certificate with under thirty days left a problem', function () {
    $host = new ProxyHost(['hostname' => 'x.clupilot.com']);
    $host->certificate_expires_at = now()->addDays(12);

    expect($host->isExpiringSoon())->toBeTrue()
        ->and($host->daysLeft())->toBeLessThan(14);

    $host->certificate_expires_at = now()->addDays(60);
    expect($host->isExpiringSoon())->toBeFalse();
});

/**
 * Die Liste, die der root-eigene Helfer liest: NAMEN und Zweck, eine je Zeile.
 * Keine Caddy-Blöcke.
 */
it('lists hostnames for the host helper, and nothing else', function () {
    ProxyHost::create(['hostname' => 'files.clupilot.com', 'purpose' => 'public']);
    ProxyHost::create(['hostname' => 'admin.clupilot.com', 'purpose' => 'console']);

    $this->artisan('clupilot:proxy-hosts')
        ->expectsOutput('admin.clupilot.com console')
        ->expectsOutput('files.clupilot.com public')
        ->assertSuccessful();
});

it('keeps the acme contact address, and offers the owner as the fallback', function () {
    $owner = Operator::factory()->role('Owner')->create(['email' => 'inhaber@clupilot.com']);

    Livewire::actingAs($owner, 'operator')->test(ProxyHosts::class)
        ->assertSee('inhaber@clupilot.com')
        ->set('acmeEmail', 'ssl@clupilot.com')
        ->call('saveAcmeEmail');

    expect(Settings::get('proxy.acme_email'))->toBe('ssl@clupilot.com');
});

it('is closed to an operator without the permission', function () {
    Livewire::actingAs(Operator::factory()->create(), 'operator')
        ->test(ProxyHosts::class)
        ->assertForbidden();
});

/**
 * Der Fund, der diese Seite zweimal falsch gemacht hat.
 *
 * Die erste Fassung war ein LEERES Register: sie zeigte nichts, während die
 * Installation längst ein halbes Dutzend Namen bediente, deren Zertifikate der
 * Betreiber gerade sehen wollte. Eine Übersicht, die man erst befüllen muss,
 * beantwortet „was habe ich" nicht.
 */
it('shows the hostnames this installation already serves, without being told', function () {
    expect(ProxyHost::query()->count())->toBe(0);

    proxyPage()->assertSee('files.clupilot.test');

    $entdeckt = ProxyHost::query()->where('hostname', 'files.clupilot.test')->first();

    expect($entdeckt)->not->toBeNull()
        ->and($entdeckt->isFromConfig())->toBeTrue();
});

/**
 * Ein Name aus der Umgebung käme beim nächsten Abgleich wieder. Ein Knopf, der
 * nichts bewirkt, ist schlimmer als keiner — er wird einmal geglaubt.
 */
it('will not let go of a hostname that comes from the environment', function () {
    proxyPage();
    $ausDerKonfiguration = ProxyHost::query()->where('source', 'config')->firstOrFail();

    proxyPage()->call('remove', $ausDerKonfiguration->id)
        ->assertSee(__('proxy.remove_from_config'));

    expect(ProxyHost::query()->whereKey($ausDerKonfiguration->id)->exists())->toBeTrue();
});

/**
 * Die Übersicht in Zahlen — das, was ein Betreiber wissen will, bevor er eine
 * Liste liest.
 */
it('counts what is valid, what expires soon and what is broken', function () {
    proxyPage();
    ProxyHost::query()->delete();

    ProxyHost::create(['hostname' => 'gut.clupilot.com', 'purpose' => 'public'])
        ->forceFill(['certificate_expires_at' => now()->addDays(70), 'certificate_checked_at' => now()])->save();
    ProxyHost::create(['hostname' => 'knapp.clupilot.com', 'purpose' => 'public'])
        ->forceFill(['certificate_expires_at' => now()->addDays(9), 'certificate_checked_at' => now()])->save();
    ProxyHost::create(['hostname' => 'kaputt.clupilot.com', 'purpose' => 'public'])
        ->forceFill(['certificate_checked_at' => now(), 'certificate_error' => 'tlsv1 alert internal error'])->save();

    $summary = Livewire::actingAs(Operator::factory()->role('Owner')->create(), 'operator')
        ->test(ProxyHosts::class)
        ->viewData('summary');

    expect($summary['valid'])->toBe(1)
        ->and($summary['soon'])->toBe(1)
        ->and($summary['failing'])->toBe(1);
});

/**
 * Der Fund vom Live-Server: `127.0.0.1` stand in der Übersicht, als Konsole,
 * mit „Kein gültiges Zertifikat: Connection refused".
 *
 * Er kam aus `ADMIN_HOSTS`, wo eine nackte IP als Wiederzugang ausdrücklich
 * stehen darf. Der erste Filter verlangte nur einen Punkt — davon hat
 * `127.0.0.1` drei. Für eine IP stellt Let's Encrypt kein Zertifikat aus, also
 * war das ein dauerhaft roter Eintrag, den niemand beheben kann.
 */
it('keeps IP addresses out of a certificate overview', function () {
    config()->set('admin_access.hosts', ['admin.clupilot.com', '127.0.0.1', '2001:db8::1', '198.51.100.7']);

    $gefunden = array_keys(App\Support\KnownHostnames::all());

    expect($gefunden)->toContain('admin.clupilot.com')
        ->and($gefunden)->not->toContain('127.0.0.1')
        ->and($gefunden)->not->toContain('198.51.100.7')
        ->and($gefunden)->not->toContain('2001:db8::1');
});

/**
 * Und die Zeile, die schon in der Datenbank steht, geht beim nächsten Abgleich
 * weg — sonst bliebe mein Fehler auf jeder Installation stehen, die einmal
 * aktualisiert hat.
 */
it('clears out a config row that should never have been there', function () {
    $ip = ProxyHost::create(['hostname' => '127.0.0.1', 'purpose' => 'console', 'source' => 'config']);

    proxyPage();

    expect(ProxyHost::query()->whereKey($ip->id)->exists())->toBeFalse();
});

/**
 * Aber nichts mit laufendem Zertifikat verschwindet still. Es wird zu einem
 * Eintrag von Hand, den der Betreiber selbst entfernt — eine Frist, die weiter
 * läuft, gehört nicht wortlos aus einer Übersicht genommen.
 */
it('turns a vanished name with a live certificate into a manual entry', function () {
    $alt = ProxyHost::create(['hostname' => 'frueher.clupilot.com', 'purpose' => 'public', 'source' => 'config']);
    $alt->forceFill(['certificate_expires_at' => now()->addDays(40), 'certificate_checked_at' => now()])->save();

    proxyPage();

    expect($alt->fresh()->isFromConfig())->toBeFalse()
        ->and(ProxyHost::query()->whereKey($alt->id)->exists())->toBeTrue();
});
