<?php

use App\Models\PlanPrice;
use App\Services\Stripe\FakeStripeClient;
use App\Services\Stripe\StripeClient;
use App\Support\OperatingMode;
use App\Support\Settings;
use App\Support\StripeCatalogueMode;
use Illuminate\Contracts\Console\Kernel;

/**
 * Der Abgleich sagt, in wessen Konto er gearbeitet hat — und weigert sich, in
 * einen Katalog hineinzuarbeiten, der einem anderen gehört.
 *
 * Eine Stripe-Price-ID gehört dem Konto, das sie ausgestellt hat. Der Befehl
 * überspringt jede Zeile, die schon eine ID trägt (PlanPrices::inStep()), also
 * repariert ein zweiter Lauf nach dem Umschalten gar nichts — er meldete
 * „already in step" und ließe eine Zeile zurück, die behauptet, der Katalog
 * gehöre jetzt zum Livekonto. Genau diese Lüge macht die Weigerung unnötig.
 */
beforeEach(function () {
    $this->stripe = new FakeStripeClient;
    app()->instance(StripeClient::class, $this->stripe);
});

it('records the mode its objects were created in', function () {
    OperatingMode::set(OperatingMode::Test);

    $this->artisan('stripe:sync-catalogue')->assertSuccessful();

    expect(StripeCatalogueMode::recorded())->toBe(OperatingMode::Test)
        ->and(StripeCatalogueMode::hasStoredObjects())->toBeTrue();
});

it('records nothing on a dry run, which creates nothing', function () {
    OperatingMode::set(OperatingMode::Test);

    $this->artisan('stripe:sync-catalogue', ['--dry-run' => true])->assertSuccessful();

    expect(StripeCatalogueMode::recorded())->toBeNull();
});

it('refuses to work into a catalogue that belongs to the other account', function () {
    OperatingMode::set(OperatingMode::Test);
    $this->artisan('stripe:sync-catalogue')->assertSuccessful();

    OperatingMode::set(OperatingMode::Live);

    $this->artisan('stripe:sync-catalogue')->assertFailed();

    // Und die Zeile lügt danach nicht: der Katalog gehört weiterhin dem
    // Testkonto, weil dieser Lauf nichts angelegt hat.
    expect(StripeCatalogueMode::recorded())->toBe(OperatingMode::Test);
});

/**
 * „Herkunft nie festgehalten" ist NICHT „fremdes Konto".
 *
 * Der Zustand entsteht ohne jeden Fehler: `CheckoutController` → `PlanPrices::
 * ensure()` und `BookAddon` → `SyncStripeAddonItems` → `AddonPrices::ensure()`
 * legen fehlende Preise selbst an, und keiner der beiden ruft `record()`. Er
 * entsteht auch nach einem Lauf, der mittendrin abgebrochen ist.
 *
 * Die Objekte stammen dann aus genau dem geltenden Konto. Diesen Zustand in die
 * Konto-Weigerung zu schicken, hieße einem Betreiber das Leeren eines Katalogs
 * aufzutragen, an dem laufende Verträge abgerechnet werden — für ein
 * Wiederaufsetzen, für das die Idempotenzschlüssel dieses Befehls ausdrücklich
 * gebaut sind.
 */
it('takes up a catalogue whose origin was never written down', function () {
    OperatingMode::set(OperatingMode::Test);
    $this->artisan('stripe:sync-catalogue')->assertSuccessful();

    Settings::forget(StripeCatalogueMode::SETTING);

    $this->artisan('stripe:sync-catalogue')->assertSuccessful();

    expect(StripeCatalogueMode::recorded())->toBe(OperatingMode::Test);
});

it('leaves no half-built catalogue that contradicts itself when a run dies', function () {
    OperatingMode::set(OperatingMode::Test);

    // Stripe geht mitten im Lauf weg, nachdem das erste Produkt schon angelegt
    // und seine ID gespeichert ist. `createProduct()` und `ensure()` werfen
    // ungefangen — stünde record() am ENDE von handle(), bliebe genau der
    // Zustand zurück, den der Test darüber beschreibt: IDs da, Herkunft leer.
    app()->instance(StripeClient::class, new class extends FakeStripeClient
    {
        public function createPrice(
            string $productId,
            int $amountCents,
            string $currency,
            string $interval,
            array $metadata = [],
            ?string $idempotencyKey = null,
        ): string {
            throw new RuntimeException('Stripe went away mid-run');
        }
    });

    try {
        app(Kernel::class)->call('stripe:sync-catalogue');
    } catch (RuntimeException) {
        // Erwartet — der Abbruch IST der Testfall.
    }

    expect(StripeCatalogueMode::hasStoredObjects())->toBeTrue()
        ->and(StripeCatalogueMode::recorded())->toBe(OperatingMode::Test);
});

it('syncs into the new account once the stale ids are cleared', function () {
    OperatingMode::set(OperatingMode::Test);
    $this->artisan('stripe:sync-catalogue')->assertSuccessful();

    OperatingMode::set(OperatingMode::Live);

    // Was der `breaks`-Satz und die Fehlermeldung dem Betreiber auftragen —
    // Zeiger UND Register, weil PlanPrices::inStep() für den Netto-Preis eines
    // Reverse-Charge-Kunden allein das Register fragt.
    DB::table('plan_families')->update(['stripe_product_id' => null]);
    DB::table('plan_prices')->update(['stripe_price_id' => null]);
    DB::table('stripe_plan_prices')->delete();
    DB::table('stripe_addon_prices')->delete();

    $this->artisan('stripe:sync-catalogue')->assertSuccessful();

    expect(StripeCatalogueMode::recorded())->toBe(OperatingMode::Live)
        ->and(PlanPrice::query()->whereNotNull('stripe_price_id')->exists())->toBeTrue();
});
