<?php

/**
 * Eine Lücke in einer Messreihe bleibt eine Lücke — auch in der Verlaufslinie.
 *
 * `HostLoadSeries` hält einen nicht aufgezeichneten Messwert bewusst als `null`
 * fest statt als 0, und die Kachel darüber weigert sich, ihn als Zahl zu
 * zeigen. Die Linie darunter tat das Gegenteil: sie warf die Lücke heraus und
 * verband die Nachbarn. Damit behauptete sie eine Messung, die es nicht gab,
 * und schob alles danach nach links — eine Stunde sah aus wie 58 Minuten.
 */
it('breaks the line where a sample is missing instead of bridging it', function () {
    $view = $this->blade('<x-ui.spark :points="$p" />', ['p' => [10, 20, null, 40, 50]]);

    // Zwei Züge: einer vor der Lücke, einer danach. Ein einziges `M` hieße,
    // die Linie läuft durch.
    expect(substr_count($view->__toString(), '<path'))->toBe(2);
});

it('draws one unbroken line when nothing is missing', function () {
    // Die Gegenprobe: an sauberen Reihen — und das sind alle bisherigen
    // Aufrufer — darf sich nichts ändern.
    $view = $this->blade('<x-ui.spark :points="$p" />', ['p' => [10, 20, 30, 40, 50]]);

    expect(substr_count($view->__toString(), '<path'))->toBe(1);
});

it('keeps later points in place when an earlier one is missing', function () {
    // Der eigentliche Schaden am Filtern: der letzte Punkt einer Stunde muss
    // ganz rechts liegen, egal wie viele Messwerte dazwischen fehlen.
    $withGap = $this->blade('<x-ui.spark :points="$p" />', ['p' => [10, null, 30, 40, 50]])->__toString();
    $without = $this->blade('<x-ui.spark :points="$p" />', ['p' => [10, 20, 30, 40, 50]])->__toString();

    expect($withGap)->toContain('96 ')   // rechter Rand erreicht
        ->and($without)->toContain('96 ');
});

it('closes the filled area with a path a renderer can actually follow', function () {
    // Beim Zerlegen in Züge ist der Flächenpfad kurz `… L` direkt vor einem `M`
    // geworden — gültig sieht das aus, gezeichnet wird es nicht, und die
    // Füllung verschwand still. Ein Blick auf die Seite hat es gezeigt, keine
    // Meldung.
    $view = $this->blade('<x-ui.spark :points="$p" area />', ['p' => [10, 20, null, 40, 50]])->__toString();

    preg_match_all('/class="area"[^>]*\sd="([^"]+)"/', $view, $m);

    expect($m[1])->toHaveCount(2);
    foreach ($m[1] as $d) {
        expect($d)->toStartWith('M')
            ->and($d)->not->toContain('LM')   // ein Befehl direkt hinter einem anderen
            ->and($d)->toEndWith('Z');
    }
});

it('draws nothing at all when every sample is missing', function () {
    $view = $this->blade('<x-ui.spark :points="$p" />', ['p' => [null, null]]);

    expect($view->__toString())->not->toContain('<path');
});

// --- Maßstab -----------------------------------------------------------------
//
// Eine Verlaufslinie, die auf ihr EIGENES Minimum und Maximum skaliert, macht
// aus Rauschen ein Gebirge: ein Host mit 0,2 % CPU, der zwischen 0,1 und 0,3
// schwankt, zeichnete einen Seismographen — und widersprach damit der Zahl
// direkt daneben. Genau die Regel, die im Chart.js-Entwurf schon stand
// („eine automatisch skalierte Achse lässt 3 % wie eine Wand aussehen") und
// beim Umstieg auf die Kacheln verlorenging.

/** Alle y-Werte aus den gezeichneten Pfaden. */
function sparkY(string $html): array
{
    preg_match_all('/class="line" d="M([^"]+)"/', $html, $m);
    $ys = [];
    foreach ($m[1] as $d) {
        foreach (preg_split('/\s+/', trim($d)) as $i => $n) {
            if ($i % 2 === 1) {
                $ys[] = (float) $n;
            }
        }
    }

    return $ys;
}

it('keeps a nearly idle host near the baseline instead of filling the box', function () {
    $html = $this->blade('<x-ui.spark :points="$p" :min="0" :max="100" />', ['p' => [0.1, 0.3, 0.2, 0.25]])->__toString();

    // 30 ist die Grundlinie, 4 die Oberkante. Bei 0,2 % darf nichts hochspringen.
    expect(max(sparkY($html)))->toBeGreaterThan(29.0)
        ->and(min(sparkY($html)))->toBeGreaterThan(29.0);
});

it('still uses the whole box when the load really does span it', function () {
    $html = $this->blade('<x-ui.spark :points="$p" :min="0" :max="100" />', ['p' => [0, 100]])->__toString();

    expect(min(sparkY($html)))->toBe(4.0)     // 100 % ganz oben
        ->and(max(sparkY($html)))->toBe(30.0); // 0 % auf der Grundlinie
});

it('draws a flat line at the bottom when nothing moved at all', function () {
    // Die Netz-Kacheln eines ruhigen Hosts: lauter Nullen. Ohne verankerte
    // Untergrenze wurde daraus eine Linie irgendwo in der Mitte.
    $html = $this->blade('<x-ui.spark :points="$p" :min="0" />', ['p' => [0, 0, 0, 0]])->__toString();

    expect(array_unique(sparkY($html)))->toBe([30.0]);
});

it('scales to the series itself when no bounds are given', function () {
    // Die Gegenprobe: alle bisherigen Aufrufer geben keine Grenzen mit und
    // dürfen sich nicht verändern.
    $html = $this->blade('<x-ui.spark :points="$p" />', ['p' => [10, 20]])->__toString();

    expect(min(sparkY($html)))->toBe(4.0)
        ->and(max(sparkY($html)))->toBe(30.0);
});
