<?php

use App\Livewire\Admin\Setup;
use App\Models\Operator;
use App\Support\Navigation;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Route;
use Livewire\Livewire;

/**
 * Die Seitenleiste der Konsole.
 *
 * Sie trug 26 Einträge in drei Gruppen, von denen eine — „Betrieb" — allein
 * zwölf hielt: Maschinen, Geld und Post unter einem Wort, das auf alle drei
 * zutrifft und deshalb keinem hilft. Neu geordnet in sechs Gruppen.
 *
 * Was diese Tests festhalten, ist nicht die Ordnung selbst (die darf sich
 * ändern), sondern dass beim Umsortieren nichts verlorengeht und keine leere
 * Überschrift entsteht.
 *
 * Seit dem zweiten Umbau (2026-08-04) gilt das über ZWEI Orte: die Leiste und
 * den Einrichtungsbereich. Neun Seiten, die man einmal einrichtet, sind
 * dorthin gezogen — die Zusicherung „nichts geht verloren" muss deshalb beide
 * zusammen prüfen, sonst hiesse sie nur noch „nichts geht in der Leiste
 * verloren", und genau dort ist ja etwas verschwunden.
 */

/** Jeder Routenname, den die Konsole irgendwo anbietet — Leiste UND Kacheln. */
function consoleRouteNames(): array
{
    $namen = array_column(consoleItems(), 0);

    foreach (Navigation::setup() as $kacheln) {
        foreach ($kacheln as $kachel) {
            $namen[] = $kachel[0];
        }
    }

    return $namen;
}

/** @return list<array{0:string,1:string,2:string,3:mixed}> */
function consoleItems(): array
{
    return collect(Navigation::console())->flatMap(fn (array $g) => $g['items'])->all();
}

/**
 * Die Gruppen-Überschrift, wie x-shell.nav sie zeichnet.
 *
 * Auf das Element geprüft und nicht auf das blanke Wort: „Post" steckt auch in
 * „Posteingang". Ein Teilstring-Vergleich auf ein kurzes Wort besteht aus
 * Gründen, die mit der Prüfung nichts zu tun haben.
 */
function navLabel(string $label): string
{
    return 'lbl">'.e($label).'</p>';
}

it('verliert beim Umsortieren keinen Eintrag und legt keinen doppelt an', function () {
    $routes = array_column(consoleItems(), 0);

    // Doppelte wären beim Verschieben von Hand die naheliegende Panne: einmal
    // an der neuen Stelle eingefügt, an der alten vergessen.
    expect($routes)->toHaveCount(count(array_unique($routes)));

    // Auch über beide Orte zusammen darf nichts doppelt stehen: eine Seite in
    // der Leiste UND als Kachel wäre die Verwirrung, die dieser Umbau
    // abschaffen soll.
    $alle = consoleRouteNames();
    expect($alle)->toHaveCount(count(array_unique($alle)));
});

it('laesst keine Seite fallen, die vor dem Umbau in der Leiste stand', function () {
    // Die achtundzwanzig Einträge von vor dem 4.8.2026, ausgeschrieben statt
    // aus der Geschichte gelesen: ein Test, der sich seine Erwartung aus
    // demselben Repository holt, das er prüft, prüft nichts.
    $vorher = [
        'admin.overview', 'admin.customers', 'admin.instances', 'admin.plans',
        'admin.hosts', 'admin.datacenters', 'admin.capacity', 'admin.proxy-hosts', 'admin.vpn',
        'admin.provisioning', 'admin.maintenance', 'admin.incidents', 'admin.mail-pace',
        'admin.revenue', 'admin.invoices', 'admin.payment-problems', 'admin.finance',
        'admin.inbox', 'admin.mail-log', 'admin.templates', 'admin.mail.preview',
        'admin.mail', 'admin.integrations', 'admin.readiness', 'admin.dpa',
        'admin.settings', 'admin.roles', 'admin.open-work',
    ];

    expect(array_values(array_diff($vorher, consoleRouteNames())))->toBe([]);
});

it('fuehrt keine Kachel, deren Route es nicht gibt', function () {
    foreach (Navigation::setup() as $gruppe => $kacheln) {
        foreach ($kacheln as [$route, , $key]) {
            expect(Route::has($route))->toBeTrue("Route {$route} (Kachel {$key} in {$gruppe}) existiert nicht.");
            expect(__('setup.tile.'.$key))->not->toBe('setup.tile.'.$key, "Kachel {$key} hat keinen Text.");
        }
    }
});

it('führt keinen Eintrag, dessen Route es nicht gibt', function () {
    foreach (consoleItems() as [$route, , $key]) {
        expect(Route::has($route))->toBeTrue("Route {$route} (Eintrag {$key}) existiert nicht.");
    }
});

it('gibt jedem Eintrag eine übersetzte Beschriftung in beiden Sprachen', function () {
    foreach (['de', 'en'] as $locale) {
        app()->setLocale($locale);

        foreach (consoleItems() as [, , $key]) {
            expect(__('admin.nav.'.$key))->not->toBe('admin.nav.'.$key, "{$key} fehlt in lang/{$locale}/admin.php");
        }

        foreach (Navigation::console() as $group) {
            if ($group['label'] !== null) {
                expect($group['label'])->not->toStartWith('admin.nav_group.');
            }
        }
    }
});

it('stellt jede Gruppe unter eine Überschrift — außer der Übersicht', function () {
    $groups = Navigation::console();

    // Die erste Gruppe trägt allein die Übersicht und braucht keine
    // Überschrift: sie ist die Seite, auf der man landet.
    expect($groups[0]['label'])->toBeNull()
        ->and($groups[0]['items'])->toHaveCount(1)
        ->and($groups[0]['items'][0][0])->toBe('admin.overview');

    // Die LETZTE trägt allein die Tür zum Einrichtungsbereich und braucht
    // ebenfalls keine: eine Überschrift über einem einzigen Eintrag benennt
    // nur ihn selbst noch einmal.
    $letzte = $groups[count($groups) - 1];
    expect($letzte['label'])->toBeNull()
        ->and($letzte['items'])->toHaveCount(1)
        ->and($letzte['items'][0][0])->toBe('admin.setup');

    // Alle dazwischen haben eine. Eine namenlose Gruppe MITTEN in der Leiste
    // wäre wieder der Zustand, aus dem diese Arbeit kommt: schwebende Einträge
    // ohne Zugehörigkeit.
    foreach (array_slice($groups, 1, -1) as $group) {
        expect($group['label'])->not->toBeNull()->and($group['label'])->not->toBe('');
    }
});

it('lässt keine Gruppe so groß werden, dass sie wieder alles verschluckt', function () {
    // „Betrieb" hielt zwölf. Die Grenze ist kein Selbstzweck: ab etwa sieben
    // liest man eine Gruppe nicht mehr auf einen Blick, und dann fängt das
    // Sortieren von vorne an.
    foreach (Navigation::console() as $group) {
        expect(count($group['items']))->toBeLessThanOrEqual(7, "Gruppe „{$group['label']}\" ist auf ".count($group['items']).' Einträge gewachsen.');
    }
});

it('zeigt einem Betreiber ohne Berechtigungen keine leere Überschrift', function () {
    // „Post" besteht ausschließlich aus geschützten Einträgen. Vor dieser
    // Arbeit druckte x-shell.nav die Überschrift bedingungslos — die Gruppe
    // hätte also als Wort ohne Inhalt dagestanden.
    //
    // „Read-only" trägt weder customers.manage noch mail.manage, sieht also
    // keinen einzigen Eintrag dieser Gruppe.
    $this->actingAs(Operator::factory()->role('Read-only')->create(), 'operator');

    $html = $this->get(route('admin.overview'))->assertOk()->getContent();

    // Auf das Überschriften-Element geprüft, nicht auf das blanke Wort:
    // „Post" steckt auch in „Posteingang" und „Postausgang", und ein
    // Teilstring-Vergleich hätte hier etwas anderes geprüft als behauptet.
    expect($html)->not->toContain(navLabel(__('admin.nav_group.post')));
});

it('zeigt die Gruppe, sobald der Betreiber einen ihrer Einträge sehen darf', function () {
    // Die Kehrseite: die Überschrift darf nicht aus Vorsicht verschwinden.
    $this->actingAs(admin(), 'operator');

    $html = $this->get(route('admin.overview'))->assertOk()->getContent();

    foreach (Navigation::console() as $group) {
        if ($group['label'] !== null) {
            expect($html)->toContain(navLabel($group['label']));
        }
    }
});

it('zaehlt die Zahlen neben den Eintraegen nur einmal je Minute', function () {
    // Diese Leiste rendert auf JEDER Konsolenseite. Ohne Zwischenspeicher wären
    // das drei Abfragen je Seitenaufruf — eine Abgabe, die man erst sucht, wenn
    // die Konsole längst langsam ist.
    Cache::forget('nav.attention');

    $erste = Navigation::attentionCounts();

    expect(Cache::has('nav.attention'))->toBeTrue()
        ->and(Navigation::attentionCounts())->toBe($erste);
});

it('zeigt keine Plakette, laesst den Eintrag aber stehen, wenn nichts offen ist', function () {
    // Eine Seite, die verschwindet, sobald nichts offen ist, ist genau dann
    // nicht erreichbar, wenn man nachsehen will, ob wirklich nichts offen ist.
    Cache::forever('nav.attention', ['open_work' => 0, 'incidents' => 0, 'payment_problems' => 0]);

    $this->actingAs(admin(), 'operator');
    $html = $this->get(route('admin.overview'))->assertOk()->getContent();

    expect($html)->toContain(navLabel(__('admin.nav_group.attention')))
        ->and($html)->toContain(__('admin.nav.incidents'));
});

it('nennt die Zahl, sobald etwas offen ist', function () {
    Cache::forever('nav.attention', ['open_work' => 0, 'incidents' => 7, 'payment_problems' => 0]);

    $this->actingAs(admin(), 'operator');
    $html = $this->get(route('admin.overview'))->assertOk()->getContent();

    expect($html)->toContain('>7</span>');
});

it('zeigt im Einrichtungsbereich nur Kacheln, die dieser Betreiber oeffnen darf', function () {
    // Eine Kachel, die zu einer 403 führt, ist schlimmer als eine fehlende:
    // sie sieht aus wie ein Weg.
    Livewire::actingAs(admin(), 'operator')
        ->test(Setup::class)
        ->assertSee(__('setup.group.delivery'))
        ->assertSee(__('setup.tile.plans'));

    $html = Livewire::actingAs(Operator::factory()->role('Read-only')->create(), 'operator')
        ->test(Setup::class)
        ->html();

    expect($html)->not->toContain(__('setup.tile.plans'));
});

it('haelt die Tuer zur Einrichtung markiert, solange man dahinter steht', function () {
    // Codex R15, P2 am Umbau: die neun verschobenen Seiten stehen nicht mehr in
    // der Leiste. Ohne diese Regel ist auf keiner von ihnen ein Eintrag
    // markiert — die Konsole sagt dann nicht mehr, wo man ist.
    $this->actingAs(admin(), 'operator');

    foreach (Navigation::setup() as $kacheln) {
        foreach ($kacheln as [$route, , $key]) {
            $this->get(route($route));

            expect(Navigation::isCurrent('admin.setup', console: true))
                ->toBeTrue("Auf {$key} ({$route}) ist die Einrichtung nicht markiert.");
        }
    }
});

it('nennt eine verschobene Seite weiter in der Brotkrume', function () {
    // Derselbe Schaden aus derselben Ursache, nur an der anderen Stelle:
    // currentLabel() sah nur in der Leiste nach und lieferte fuer die neun
    // Seiten null — der Kopf haette dort nur noch „Konsole" gesagt.
    $this->actingAs(admin(), 'operator');
    $this->get(route('admin.plans'));

    expect(Navigation::currentLabel(console: true))->toBe(__('admin.nav.plans'));
});

it('markiert die Einrichtung nicht, wenn man ganz woanders steht', function () {
    // Die Kehrseite: eine Markierung, die immer leuchtet, ist keine.
    $this->actingAs(admin(), 'operator');
    $this->get(route('admin.customers'));

    expect(Navigation::isCurrent('admin.setup', console: true))->toBeFalse();
});

it('macht die Versionszeile im Fuss zum Weg zur Aktualisierung', function () {
    // Gemeldet vom Betreiber: „Einstellungen ist in der Einrichtung, wegen den
    // Versionsupdates — man sieht es nicht, ohne genau hinzuklicken." Die
    // Version stand im Fuss schon, aber als toter Text.
    Cache::forever('nav.attention', ['open_work' => 0, 'incidents' => 0, 'payment_problems' => 0, 'update' => 0]);

    $this->actingAs(admin(), 'operator');
    $html = $this->get(route('admin.overview'))->assertOk()->getContent();

    expect($html)->toContain(route('admin.settings').'?tab=installation')
        // Solange nichts ansteht, bleibt sie leise: eine Zeile, die immer
        // ruft, ruft nie.
        ->and($html)->not->toContain(__('admin.nav_update_waiting'));
});

it('sagt im Fuss, dass eine Aktualisierung wartet', function () {
    Cache::forever('nav.attention', ['open_work' => 0, 'incidents' => 0, 'payment_problems' => 0, 'update' => 1]);

    $this->actingAs(admin(), 'operator');
    $html = $this->get(route('admin.overview'))->assertOk()->getContent();

    expect($html)->toContain(__('admin.nav_update_waiting'));
});
