<?php

use App\Models\Host;
use App\Support\HostEnrolment;
use App\Support\HostTakeoverCommand;

beforeEach(function () {
    fakeServices();
    // Hostname kommt aus phpunit.xml — Routen werden beim Booten gebunden.

    $this->publicDir = storage_path('app/files/public');
    if (! is_dir($this->publicDir)) {
        mkdir($this->publicDir, 0755, true);
    }
});

function issuedCode(): string
{
    return HostEnrolment::issue(Host::factory()->create());
}

/**
 * Der private Teil: ohne Ausweis gibt es das Archiv nicht — und zwar als 404,
 * nicht als 403. Dieselbe Regel wie bei der Konsole: ein Fremder soll nicht
 * erfahren, dass es hier etwas gibt.
 */
it('refuses the installer without a code', function () {
    $this->get('http://files.clupilot.test/bootstrap.tar.gz')->assertNotFound();
    $this->get('http://files.clupilot.test/bootstrap.tar.gz?code=erfunden')->assertNotFound();
});

it('serves the installer to a valid code', function () {
    $response = $this->get('http://files.clupilot.test/bootstrap.tar.gz?code='.issuedCode());

    $response->assertOk();
    expect($response->headers->get('Content-Type'))->toBe('application/gzip')
        ->and($response->headers->get('X-Robots-Tag'))->toContain('noindex');
});

/**
 * Der Code wird beim Herunterladen NICHT verbraucht. Er wird danach für jede
 * Fortschrittsmeldung und zuletzt für die Registrierung gebraucht — ein
 * Download, der ihn aufbraucht, macht die Übernahme unmöglich, und zwar
 * nachdem der Server schon läuft.
 */
it('does not spend the code on the download', function () {
    $code = issuedCode();

    $this->get('http://files.clupilot.test/bootstrap.tar.gz?code='.$code)->assertOk();
    $this->get('http://files.clupilot.test/bootstrap.tar.gz?code='.$code)->assertOk();

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

/**
 * Der öffentliche Teil: AGB, AV, TOM. Anonym, ohne Ausweis, ohne Konto.
 */
it('serves public documents to anyone', function () {
    file_put_contents($this->publicDir.'/agb-2026-01.pdf', '%PDF-1.4 Testfassung');

    $response = $this->get('http://files.clupilot.test/agb-2026-01.pdf');

    $response->assertOk();

    // `response()->file()` streamt, der Rumpf steht also nicht im Ergebnis.
    // Geprüft wird deshalb die ausgelieferte Datei selbst — und dass sie im
    // Browser ANGEZEIGT wird: eine AGB, die heruntergeladen wird, wird
    // weggeklickt.
    expect($response->baseResponse->getFile()->getPathname())
        ->toBe($this->publicDir.'/agb-2026-01.pdf')
        ->and($response->headers->get('Content-Disposition'))->toStartWith('inline');

    unlink($this->publicDir.'/agb-2026-01.pdf');
});

it('does not invent documents that are not there', function () {
    $this->get('http://files.clupilot.test/gibt-es-nicht.pdf')->assertNotFound();
});

/**
 * Der teuerste denkbare Fehlgriff an dieser Stelle: ein Pfad, der aus dem
 * Verzeichnis herausführt. `.env` liegt zwei Ebenen darüber.
 */
it('cannot be walked out of its directory', function () {
    foreach (['..%2F..%2F..%2F.env', '....//....//.env', '%2e%2e%2f.env'] as $attempt) {
        $this->get('http://files.clupilot.test/'.$attempt)->assertNotFound();
    }

    // Und eine Punktdatei, die tatsächlich dort läge, ebenfalls nicht.
    file_put_contents($this->publicDir.'/.geheim', 'nicht ausliefern');
    $this->get('http://files.clupilot.test/.geheim')->assertNotFound();
    unlink($this->publicDir.'/.geheim');
});

/**
 * Die kopierte Zeile holt von diesem Hostnamen und legt den Code bei.
 */
it('points the command line at the files host, with the code', function () {
    $host = Host::factory()->create(['name' => 'fsn-01']);
    $enrolment = HostEnrolment::issueWithKeys($host);

    $command = HostTakeoverCommand::for($host, $enrolment);

    expect($command)
        ->toContain('https://files.clupilot.test/bootstrap.tar.gz?code='.$enrolment['code'])
        ->toContain('--code '.$enrolment['code']);
});

/**
 * Ohne FILES_HOST bleibt alles, wo es war. Sonst bräche eine schon ausgegebene
 * Befehlszeile in dem Moment, in dem jemand die Einstellung setzt, aber der
 * DNS-Eintrag noch nicht steht.
 */
it('falls back to the portal host until the files host exists', function () {
    config()->set('admin_access.files_host', '');
    config()->set('admin_access.app_host', 'app.clupilot.com');

    expect(HostTakeoverCommand::archiveUrl())
        ->toBe('https://app.clupilot.com/bootstrap.tar.gz');
});

/**
 * Der Fund, der diesen Test nötig gemacht hat.
 *
 * `PublicSiteGate` hängt an der ganzen `web`-Gruppe. Solange die Website
 * verborgen ist, bekäme ein Server im Rettungssystem die 503-Platzhalterseite
 * und schöbe sie in `tar xz` — der Betreiber sähe einen Entpackfehler auf einer
 * Maschine, an die er nur noch über die Anbieterkonsole kommt.
 */
it('serves downloads even while the site is hidden', function () {
    App\Support\Settings::set('site.public', false);

    // Von einer fremden Adresse, denn genau die hat ein Server im
    // Rettungssystem: 127.0.0.1 steht in TRUSTED_RANGES und käme ohnehin durch,
    // der Test bewiese dann nichts.
    $vonAussen = ['REMOTE_ADDR' => '203.0.113.77'];

    $this->withServerVariables($vonAussen)
        ->get('http://files.clupilot.test/bootstrap.tar.gz?code='.issuedCode())->assertOk();

    file_put_contents($this->publicDir.'/tom-2026-01.pdf', '%PDF-1.4');
    $this->withServerVariables($vonAussen)
        ->get('http://files.clupilot.test/tom-2026-01.pdf')->assertOk();
    unlink($this->publicDir.'/tom-2026-01.pdf');

    // Und die Website selbst bleibt für dieselbe Adresse verborgen — sonst
    // hätte die Ausnahme mehr aufgemacht als sie sollte.
    $this->withServerVariables($vonAussen)->get('http://localhost/')->assertStatus(503);
});

/**
 * Der Datei-Hostname bietet genau zwei Dinge an — und sonst nichts.
 *
 * Ohne diese Grenze fiel jede andere Anfrage auf die host-unabhängigen Routen
 * durch und landete beim Portal: wer `files.…` aufrief, wurde nach `app.…`
 * weitergeleitet. Das sagt jedem, der den Namen probiert, wo das Portal liegt
 * und dass beide auf derselben Maschine sitzen.
 */
it('offers nothing but the archive and the public files', function () {
    foreach (['/', '/dashboard', '/login', '/admin', '/tief/verschachtelt/pfad'] as $pfad) {
        $antwort = $this->get('http://files.clupilot.test'.$pfad);

        $antwort->assertNotFound();
        // Und ausdrücklich KEINE Weiterleitung: eine 302 auf app.… wäre genau
        // der Hinweis, den dieser Hostname nicht geben soll.
        expect($antwort->headers->get('Location'))->toBeNull();
    }
});

it('still serves its two things after that boundary', function () {
    file_put_contents($this->publicDir.'/av-2026-01.pdf', '%PDF-1.4');

    $this->get('http://files.clupilot.test/av-2026-01.pdf')->assertOk();
    $this->get('http://files.clupilot.test/bootstrap.tar.gz?code='.issuedCode())->assertOk();

    unlink($this->publicDir.'/av-2026-01.pdf');
});
