<?php

use App\Actions\BookAddon;
use App\Actions\OpenSubscription;
use App\Livewire\Billing;
use App\Livewire\ConfirmBookStorage;
use App\Models\Order;
use App\Models\Subscription;
use App\Models\User;
use App\Services\Billing\AddonCatalogue;
use Illuminate\Support\Facades\DB;
use Livewire\Livewire;

/**
 * Höchstens drei Blöcke.
 *
 * Der Deckel liegt dort, wo Aufsteigen billiger wird: Start + 3 Blöcke sind
 * 90 GB für 84 €, Team ist 85 GB für 79 € mit mehr Nutzern und mehr RAM. Er
 * gehört in die Aktion und nicht ins Formular — dieselbe Lektion wie bei
 * ReissueTakeover (v1.3.82): die Ansicht versteckte den Knopf, die Methode
 * prüfte nichts, und Codex fand es zweimal.
 */
function limitContract(string $plan = 'start'): Subscription
{
    $order = Order::factory()->create(['plan' => $plan, 'datacenter' => 'fsn', 'status' => 'paid']);

    return app(OpenSubscription::class)($order);
}

/** The signed-in user behind a contract's customer, for the Livewire tests below. */
function limitUser(Subscription $subscription): User
{
    return User::factory()->create(['email' => $subscription->customer->email]);
}

/** Eine bezahlte Speicher-Bestellung dieses Kunden — eine Buchung hat eine. */
function limitOrder(Subscription $subscription): Order
{
    return Order::factory()->create([
        'customer_id' => $subscription->customer_id, 'type' => 'addon', 'addon_key' => 'storage', 'status' => 'paid',
    ]);
}

it('lehnt den vierten Block auf einmal ab', function () {
    $subscription = limitContract();

    expect(fn () => app(BookAddon::class)($subscription, AddonCatalogue::STORAGE, 4))
        ->toThrow(RuntimeException::class);

    expect($subscription->addons()->count())->toBe(0);
});

it('zählt drei einzelne Buchungen als drei', function () {
    $subscription = limitContract();

    foreach (range(1, 3) as $ignored) {
        app(BookAddon::class)($subscription, AddonCatalogue::STORAGE, 1, Order::factory()->create([
            'customer_id' => $subscription->customer_id, 'type' => 'addon', 'addon_key' => 'storage', 'status' => 'paid',
        ]));
    }

    expect(fn () => app(BookAddon::class)($subscription, AddonCatalogue::STORAGE, 1, Order::factory()->create([
        'customer_id' => $subscription->customer_id, 'type' => 'addon', 'addon_key' => 'storage', 'status' => 'paid',
    ])))->toThrow(RuntimeException::class);

    expect((int) $subscription->addons()->active()->sum('quantity'))->toBe(3);
});

it('zählt eine gekündigte Buchung nicht mit', function () {
    $subscription = limitContract();
    $addon = app(BookAddon::class)($subscription, AddonCatalogue::STORAGE, 3);

    app(BookAddon::class)->cancel($addon);

    expect(app(AddonCatalogue::class)->bookableQuantity($subscription->refresh(), AddonCatalogue::STORAGE))->toBe(3);
});

it('sagt, wie viele noch gehen', function () {
    $subscription = limitContract();
    app(BookAddon::class)($subscription, AddonCatalogue::STORAGE, 2);

    expect(app(AddonCatalogue::class)->bookableQuantity($subscription->refresh(), AddonCatalogue::STORAGE))->toBe(1);
});

/**
 * Nachtrag aus der Durchsicht: der Deckel hielt in BookAddon, aber der
 * Warenkorb kannte ihn nur halb.
 *
 * `purchase()` klemmte $lines mit `max(1, min(..., bookableQuantity, ...))` —
 * bei erreichtem Deckel (bookableQuantity = 0) floort das `max(1, 0)` auf
 * genau eine Bestellzeile statt auf keine. Der Knopf, der `purchase('storage')`
 * ohne Menge aufruft, ist in der Ansicht bedingungslos gerendert, also
 * entstand am Deckel weiterhin ein bezahlbarer Auftrag, den BookAddon nach
 * der Zahlung nur noch ablehnen konnte — während OrderObserver den
 * Fehlschlag abfängt und der Auftrag `paid` stehen bleibt. Genau das Szenario,
 * dessentwegen diese Aufgabe existiert, nur auf den Fall "genau am Deckel"
 * verengt statt beseitigt.
 */
it('legt am erreichten Deckel keine Bestellzeile mehr an', function () {
    $subscription = limitContract();
    app(BookAddon::class)($subscription, AddonCatalogue::STORAGE, 3);

    withStripeSecret();

    Livewire::actingAs(limitUser($subscription))
        ->test(Billing::class)
        ->call('purchase', 'storage');

    expect(Order::where('customer_id', $subscription->customer_id)->where('type', 'storage')->count())->toBe(0);
});

/**
 * Nachtrag aus der Durchsicht: das Bestätigungs-Modal klemmte an der
 * absoluten Grenze (3), nicht an der Restmenge. Die Karte, die das Modal
 * öffnet, kennt nur die Zahl, die einen Downgrade-Blocker aufheben würde —
 * nicht, dass der Vertrag schon einen Block hält. Ohne diese Klemmung hätte
 * der Dialog "3 Pakete" versprochen, während purchase() danach nur die
 * tatsächlich noch buchbaren 2 in den Warenkorb gelegt hätte: Dialogtext und
 * wirkliche Bestellmenge liefen auseinander.
 */
it('klemmt das Bestätigungs-Modal an der Restmenge, nicht an der absoluten Grenze', function () {
    $subscription = limitContract();
    app(BookAddon::class)($subscription, AddonCatalogue::STORAGE, 1);

    Livewire::actingAs(limitUser($subscription))
        ->test(ConfirmBookStorage::class, ['packs' => 3])
        ->assertSet('packs', 2)
        ->assertSee(__('billing.storage_confirm_title', ['count' => 2]))
        ->call('proceed')
        ->assertDispatched('storage-packs-confirmed', packs: 2);
});

/**
 * Nachtrag aus der Durchsicht (Befund 1): der Deckel stand außerhalb der
 * Transaktion, und für Speicher wurde der Vertrag gar nicht gesperrt.
 *
 * Zwei Speicher-Bestellungen desselben Vertrags, gleichzeitig verarbeitet,
 * lasen damit beide denselben alten Stand und fügten beide ein — der eindeutige
 * Index über (order_id, addon_key) greift nicht, weil es zwei verschiedene
 * Bestellungen sind. Ergebnis: ein Vertrag über dem Deckel.
 *
 * WAS DIESER TEST BEWEIST: dass der Vertrag auch für ein Mengen-Modul gesperrt
 * wird, dass die Zählung NACH dieser Sperre kommt und dass beide innerhalb der
 * Buchungstransaktion laufen — die Reihenfolge, ohne die eine Sperre nichts
 * bewirkt.
 *
 * WAS ER NICHT BEWEIST: dass zwei gleichzeitige Buchungen einander wirklich
 * ausschließen. Die Suite läuft auf SQLite, dort ist `lockForUpdate()` ein
 * No-op (SQLiteGrammar::compileLock() gibt '' zurück) und ein zweiter Prozess
 * ist im selben PHP-Aufruf nicht herstellbar. Echte Nebenläufigkeit hängt an
 * MariaDB und ist hier nicht nachgestellt; nachgewiesen wird die
 * Voraussetzung dafür, nicht die Wirkung.
 */
it('zählt den Deckel unter der Vertragssperre und in derselben Transaktion, die bucht', function () {
    $subscription = limitContract();
    $order = limitOrder($subscription);

    // RefreshDatabase hält den ganzen Test in einer Transaktion. Die Tiefe VOR
    // dem Aufruf ist deshalb der Nullpunkt, gegen den "innerhalb der
    // Buchungstransaktion" gemessen wird — absolut wäre 1 schon außerhalb.
    $baseline = DB::transactionLevel();
    $log = [];

    DB::listen(function ($query) use (&$log) {
        $log[] = ['sql' => $query->sql, 'level' => DB::transactionLevel()];
    });

    app(BookAddon::class)($subscription, AddonCatalogue::STORAGE, 1, $order);

    // Die Sperre ist die erste Anweisung der Transaktion und die einzige
    // Abfrage auf `subscriptions` in diesem Pfad; gezählt wird über
    // sum(quantity) auf den Buchungen.
    $lock = collect($log)->search(fn (array $statement) => str_contains($statement['sql'], 'from "subscriptions"'));
    $count = collect($log)->search(fn (array $statement) => str_contains($statement['sql'], 'sum("quantity")'));

    expect($lock)->not->toBeFalse()
        ->and($count)->not->toBeFalse()
        ->and($count)->toBeGreaterThan($lock)
        ->and($log[$lock]['level'])->toBeGreaterThan($baseline)
        ->and($log[$count]['level'])->toBeGreaterThan($baseline);
});

/**
 * Und die andere Hälfte derselben Verschiebung: der Kurzschluss bleibt vorn.
 *
 * Dieselbe Lektion wie bei der Kapazitätsprüfung eine Runde zuvor (siehe
 * StoragePackCapacityTest): Stripe stellt denselben Webhook nach einem Timeout
 * ein zweites Mal zu. Läge die Mengenprüfung vor dem Kurzschluss, würde die
 * Wiederholung fragen "passt NOCH ein Block drauf" — obwohl gar keiner
 * hinzukommt — und einen längst bezahlten, längst gebuchten Vorgang genau dann
 * mit einem Fehler quittieren, wenn die Buchung selbst den Deckel erreicht hat.
 */
it('gibt bei einem wiederholten Webhook die vorhandene Buchung zurück, auch am erreichten Deckel', function () {
    $subscription = limitContract();
    app(BookAddon::class)($subscription, AddonCatalogue::STORAGE, 2, limitOrder($subscription));

    $order = limitOrder($subscription);

    // Der dritte Block füllt den Deckel genau aus.
    $first = app(BookAddon::class)($subscription, AddonCatalogue::STORAGE, 1, $order);
    $second = app(BookAddon::class)($subscription, AddonCatalogue::STORAGE, 1, $order);

    expect($second->id)->toBe($first->id)
        ->and((int) $subscription->addons()->active()->sum('quantity'))->toBe(3);
});

/**
 * Nachtrag aus der Durchsicht (Befund 3): dieselbe `max(1, …)`-Falle, die in
 * purchase() schon behoben war, hatte im Fenster überlebt.
 *
 * Bei null buchbaren Blöcken machte `max(1, min($max, $packs))` daraus wieder
 * einen — das Fenster versprach einen Block und schickte eine Bestellung los,
 * die purchase() danach ablehnt. Ein Knopf, der etwas anbietet, das die Aktion
 * dahinter ablehnt, ist eine Falle.
 */
it('bietet am erreichten Deckel keinen Kauf aus dem Bestätigungsfenster an', function () {
    $subscription = limitContract();
    app(BookAddon::class)($subscription, AddonCatalogue::STORAGE, 3);

    Livewire::actingAs(limitUser($subscription))
        ->test(ConfirmBookStorage::class, ['packs' => 1])
        ->assertSet('packs', 0)
        // Keine Kaufzusage, sondern die Absage in dem Satz, den der Kunde
        // überall sonst zu dieser Grenze liest.
        ->assertDontSee(__('billing.storage_confirm_cta'))
        ->assertSee(__('billing.addon_limit_reached', [
            'module' => app(AddonCatalogue::class)->name(AddonCatalogue::STORAGE),
            'max' => 3,
        ]))
        ->call('proceed')
        ->assertNotDispatched('storage-packs-confirmed');
});
