<?php

use App\Models\Order;
use App\Models\ProvisioningRun;
use App\Provisioning\PipelineRegistry;
use App\Provisioning\Steps\Customer\ConfigureDnsAndTls;
use App\Provisioning\Steps\Customer\ConfigureNextcloud;
use App\Provisioning\WorkInFlight;
use Illuminate\Support\Arr;
use Tests\Support\Steps\FakeAdvanceStep;
use Tests\Support\Steps\FakeRetryStep;

it('resolves step instances by index', function () {
    $registry = new PipelineRegistry(['host' => [FakeAdvanceStep::class, FakeRetryStep::class]]);

    expect($registry->count('host'))->toBe(2)
        ->and($registry->resolve('host', 0))->toBeInstanceOf(FakeAdvanceStep::class)
        ->and($registry->resolve('host', 1))->toBeInstanceOf(FakeRetryStep::class);
});

it('throws for an unknown pipeline', function () {
    (new PipelineRegistry([]))->stepsFor('nope');
})->throws(InvalidArgumentException::class);

it('throws for an out-of-range step index', function () {
    (new PipelineRegistry(['host' => [FakeAdvanceStep::class]]))->resolve('host', 5);
})->throws(InvalidArgumentException::class);

it('answers whether a run still to come carries out another pipeline whole', function () {
    // The question an action about to start a run has to ask about the run already
    // in flight. Asking merely "is something running?" is what made a domain
    // proven during a restart never get routed, and a storage pack booked during
    // an address run get charged monthly and never delivered.
    $registry = new PipelineRegistry([
        'both' => [FakeAdvanceStep::class, FakeRetryStep::class],
        'first' => [FakeAdvanceStep::class],
        'second' => [FakeRetryStep::class],
    ]);

    // Everything still ahead of it.
    expect($registry->stillCovers('both', 0, 'first'))->toBeTrue()
        ->and($registry->stillCovers('both', 0, 'second'))->toBeTrue()
        // EVERY step, not any: half of an ordered piece of work is worse than
        // none of it — a quota enforced over a filesystem that never grew.
        ->and($registry->stillCovers('first', 0, 'both'))->toBeFalse()
        // Already past it: a run standing at its second step will not go back and
        // do its first, which is the case a name comparison cannot see at all.
        ->and($registry->stillCovers('both', 1, 'first'))->toBeFalse()
        ->and($registry->stillCovers('both', 1, 'second'))->toBeTrue()
        // The step it is standing on counts as still to come — it is about to run,
        // or running, and every step reads the state when it gets there.
        ->and($registry->stillCovers('second', 0, 'second'))->toBeTrue();
});

it('asks that question of the runs actually in flight against an order', function () {
    $order = Order::factory()->create();
    $work = app(WorkInFlight::class);

    $running = fn (string $pipeline) => ProvisioningRun::factory()->create([
        'subject_type' => Order::class, 'subject_id' => $order->id,
        'pipeline' => $pipeline, 'status' => ProvisioningRun::STATUS_RUNNING,
    ]);

    expect($work->covers($order, 'address'))->toBeFalse()
        ->and($work->covers($order, 'storage'))->toBeFalse();

    // A quota run writes the figure Nextcloud enforces and nothing else — it does
    // not grow the disk that figure is a promise about, so it does not stand for a
    // storage change. This is the pair that had a booked pack charged every month
    // and never delivered.
    $running('quota');
    expect($work->covers($order, 'storage'))->toBeFalse()
        ->and($work->covers($order, 'address'))->toBeFalse();

    // A plan change carries both pipelines whole; there is genuinely nothing to add.
    ProvisioningRun::query()->delete();
    $running('plan-change');
    expect($work->covers($order, 'storage'))->toBeTrue()
        ->and($work->covers($order, 'address'))->toBeTrue();

    // A run that has finished covers nothing, however much it contained.
    ProvisioningRun::query()->update(['status' => ProvisioningRun::STATUS_COMPLETED]);
    expect($work->covers($order, 'storage'))->toBeFalse()
        ->and($work->covers($order, 'address'))->toBeFalse();

    // A pipeline that no longer exists — renamed by a deploy while a run was in
    // flight — covers nothing either. Refusing to start the work over it would
    // lose the work as well as the run.
    ProvisioningRun::query()->delete();
    $running('gone-in-a-deploy');
    expect($work->covers($order, 'address'))->toBeFalse();
});

it('lays the maintenance pipelines out so a half-finished address is a safe one', function () {
    // Nextcloud BEFORE the router, in the address pipeline and in the plan change
    // that reuses it. Router first meant a run could get the certificate and then
    // fail at Nextcloud — leaving the customer's own domain serving Nextcloud's
    // untrusted-domain error under a valid certificate, with domain_cert_ok true
    // so the portal called it live. The reasoning for both directions is written
    // out above the `address` pipeline in config/provisioning.php.
    $nextcloud = ConfigureNextcloud::class;
    $dns = ConfigureDnsAndTls::class;

    foreach (['address', 'plan-change', 'customer'] as $pipeline) {
        $steps = (array) config('provisioning.pipelines.'.$pipeline);

        expect(array_search($nextcloud, $steps, true))
            ->toBeLessThan(array_search($dns, $steps, true));
    }
});

it('gives every step of every pipeline a name a human can read, in both languages', function () {
    // `configure_instance_mail` stand in der Pflicht-Pipeline und hatte keinen
    // Spracheintrag: die Konsole zeigte bei JEDER bezahlten Bestellung den
    // rohen Schlüssel. Ein neuer Schritt ist eine Zeile Code und eine Zeile
    // Sprache — die zweite vergisst man, und niemand merkt es, weil kein Test
    // je danach gefragt hat.
    //
    // Gelesen wird die Sprachdatei selbst, NICHT trans($key, [], 'de'):
    // Laravel fällt bei einem fehlenden deutschen Eintrag stumm auf die
    // Ersatzsprache zurück, und ein Test darauf hätte einen fehlenden
    // deutschen Text durchgewunken, solange es den englischen gibt.
    // Nachgeprüft, nicht angenommen — die erste Fassung dieses Tests lief
    // genau deshalb gruen, obwohl der deutsche Eintrag gelöscht war.
    $dateien = [];
    $fehlend = [];

    foreach (array_keys((array) config('provisioning.pipelines')) as $pipeline) {
        foreach ((array) config('provisioning.pipelines.'.$pipeline) as $step) {
            // label() des Schrittes selbst, nicht ein hier nachgebauter
            // Schlüssel: Host- und Kundenschritte liegen in verschiedenen
            // Sprachdateien (`hosts.step.*` bzw. `provisioning.step.*`), und
            // ein Test, der die Zusammensetzung nachrechnet, prüft die
            // Zusammensetzung statt den Eintrag (R19).
            [$gruppe, $rest] = explode('.', app($step)->label(), 2);

            foreach (['de', 'en'] as $sprache) {
                $pfad = lang_path($sprache.'/'.$gruppe.'.php');
                $dateien[$sprache.'/'.$gruppe] ??= is_file($pfad) ? (array) require $pfad : [];

                if (! Arr::has($dateien[$sprache.'/'.$gruppe], $rest)) {
                    $fehlend[] = $sprache.': '.$gruppe.'.'.$rest;
                }
            }
        }
    }

    expect(array_values(array_unique($fehlend)))->toBe([]);
});
