<?php

use App\Models\InvoiceSeries;
use App\Models\Operator;
use App\Models\PlanFamily;
use App\Models\PlanPrice;
use App\Services\Secrets\SecretVault;
use App\Support\OperatingMode;
use App\Support\Readiness;
use App\Support\Readiness\Check;
use App\Support\Settings;
use App\Support\StripeCatalogueMode;

/**
 * Die Bereitschaftsprüfung berichtet, was fehlt — und sagt dazu, was
 * kaputtgeht. Ein Feldname allein ist keine Auskunft: „vat_id fehlt" sagt
 * nichts, „der Kunde bekommt keinen Beleg" sagt alles.
 */
function checkFor(string $key): ?Check
{
    return collect(Readiness::all())->firstWhere('key', $key);
}

it('reports the stripe key of the active mode as missing', function () {
    OperatingMode::set(OperatingMode::Test);

    expect(checkFor('billing.stripe_secret')->satisfied)->toBeFalse();
    expect(checkFor('billing.stripe_secret')->severity)->toBe(Check::SEVERITY_BLOCKING);
});

it('reports it as satisfied once the key of that mode is stored', function () {
    OperatingMode::set(OperatingMode::Test);
    app(SecretVault::class)->put('stripe.secret', 'sk_test_x', Operator::factory()->create(), OperatingMode::Test);

    expect(checkFor('billing.stripe_secret')->satisfied)->toBeTrue();
});

it('does not accept a live key as proof while the test mode is active', function () {
    // Sonst meldete die Seite Bereitschaft für einen Modus, in dem die Kasse
    // sperrt — die Seite widerspräche der Sperre aus Task 5.
    OperatingMode::set(OperatingMode::Test);
    app(SecretVault::class)->put('stripe.secret', 'sk_live_x', Operator::factory()->create(), OperatingMode::Live);

    expect(checkFor('billing.stripe_secret')->satisfied)->toBeFalse();
});

/**
 * Ein Schlüssel im falschen Platz ist ein blockierender Befund.
 *
 * Gemessen im Schluss-Review: `put('stripe.secret', 'sk_live_REALMONEY')` im
 * Testbetrieb wird angenommen, `get()` liefert ihn, die Bereitschaftsseite
 * meldete „erfüllt" — und oben auf derselben Seite steht die Plakette
 * „Testbetrieb". Das ist derselbe Ausgang, gegen den die strikte Ausnahme im
 * Tresor gebaut wurde („bucht echtes Geld ab, während die Konsole Testbetrieb
 * anzeigt"), nur über den eingetippten statt den automatischen Weg.
 *
 * Am Präfix ohne Netzverkehr entscheidbar — OperatingMode::ofStripeKey(),
 * dieselbe Stelle, die auch die Migration und StripeCheck fragen.
 */
it('refuses a live key sitting in the test slot', function () {
    OperatingMode::set(OperatingMode::Test);
    app(SecretVault::class)->put('stripe.secret', 'sk_live_REALMONEY', Operator::factory()->create(), OperatingMode::Test);

    expect(checkFor('billing.stripe_secret')->satisfied)->toBeFalse();
    expect(checkFor('billing.stripe_secret')->severity)->toBe(Check::SEVERITY_BLOCKING);
});

it('refuses a test key sitting in the live slot', function () {
    // Die andere Richtung kostet kein Geld, sondern nimmt keins ein: der Kunde
    // zahlt in Stripes Testkonto, und die Bestellung sieht bezahlt aus.
    OperatingMode::set(OperatingMode::Live);
    app(SecretVault::class)->put('stripe.secret', 'sk_test_notreal', Operator::factory()->create(), OperatingMode::Live);

    expect(checkFor('billing.stripe_secret')->satisfied)->toBeFalse();
});

it('accepts the restricted key of the active mode', function () {
    // rk_ ist ein echter Schlüssel, kein Sonderfall — die Erkennung darf ihn
    // nicht für einen Widerspruch halten, nur weil er nicht sk_ heißt.
    OperatingMode::set(OperatingMode::Test);
    app(SecretVault::class)->put('stripe.secret', 'rk_test_readonly', Operator::factory()->create(), OperatingMode::Test);

    expect(checkFor('billing.stripe_secret')->satisfied)->toBeTrue();
});

it('says what the wrong key does, not that a field is empty', function () {
    OperatingMode::set(OperatingMode::Test);
    app(SecretVault::class)->put('stripe.secret', 'sk_live_REALMONEY', Operator::factory()->create(), OperatingMode::Test);

    // Der Satz für „gar kein Schlüssel" wäre hier eine Lüge: einer liegt da,
    // er ist der falsche, und die Folge ist eine ganz andere.
    expect(checkFor('billing.stripe_secret')->breaks)
        ->not->toBe(__('readiness.billing.stripe_secret_breaks'));
});

it('reports incomplete company details without repeating the list', function () {
    Settings::forget('company.name');

    expect(checkFor('billing.company_details')->satisfied)->toBeFalse();
});

it('names what breaks, not just what is missing', function () {
    expect(checkFor('billing.company_details')->breaks)->not->toBe('');
});

it('says the installation is not ready while something blocking is open', function () {
    expect(Readiness::isReady())->toBeFalse();
    expect(Readiness::blocking())->not->toBeEmpty();
});

it('does not report the invoice series ready when the required series is switched off', function () {
    // IssueInvoice draws its series with `where('kind', ...)->where('active', true)`
    // (IssueInvoice.php:373 and :444) — a deactivated 'invoice' row is as
    // invisible to it as a missing one. Counting distinct kinds without the
    // same `active` filter would report readiness for an installation where
    // not a single invoice can draw a number.
    InvoiceSeries::query()->where('kind', 'invoice')->update(['active' => false]);

    expect(checkFor('billing.invoice_series')->satisfied)->toBeFalse();
});

it('does not raise a permanent alarm for a draft version stripe never touches', function () {
    // stripe:sync-catalogue only mirrors PUBLISHED versions (its own header
    // comment: "Only PUBLISHED versions are synced. A draft has promised
    // nothing..."). A draft is an entirely ordinary state — there is a
    // dedicated modal to create one — so a query that does not also filter on
    // `published_at` would keep this check permanently unsatisfied on any
    // installation that has ever drafted a next version: an alarm that is
    // always on and therefore never read.
    PlanPrice::query()->update(['stripe_price_id' => 'price_already_synced']);

    $family = PlanFamily::query()->create(['key' => 'draftonly', 'name' => 'Draft Only', 'tier' => 9]);
    $draft = $family->versions()->create([
        'version' => 1, 'quota_gb' => 10, 'traffic_gb' => 100, 'seats' => 1, 'ram_mb' => 1024,
        'cores' => 1, 'disk_gb' => 20, 'performance' => 'standard', 'features' => [],
        'available_from' => now(),
        // published_at left null: a draft, exactly what stripe:sync-catalogue skips.
    ]);
    $draft->prices()->create(['term' => 'monthly', 'amount_cents' => 1000, 'currency' => 'EUR']);

    // Der Abgleich hält seit dieser Runde fest, in welchem Konto er die IDs
    // angelegt hat; ohne diese Zeile wäre der Katalog oben von unbekannter
    // Herkunft und die Prüfung meldete zu Recht einen Widerspruch.
    StripeCatalogueMode::record();

    expect(checkFor('billing.catalogue_synced')->satisfied)->toBeTrue();
});

/**
 * Der Modus schaltet die Zugangsdaten um — nicht die Objekte, die aus ihnen
 * entstanden sind.
 *
 * Eine Stripe-Price-ID gehört dem Konto, das sie ausgestellt hat. Der geplante
 * Ablauf dieser Installation war: im Testbetrieb abgleichen, Live-Schlüssel
 * hinterlegen, umschalten — und diese Prüfung meldete weiter „erfüllt", weil
 * sie nur nachsah, ob die Spalte gefüllt ist. Die Seite sagte „Bereit für
 * Livebetrieb", und die erste echte Bestellung bekam von Stripe *No such
 * price*. Kein Auftrag, und die Kernzusage dieser Seite gebrochen in genau dem
 * Moment, für den sie existiert.
 */
it('does not call the catalogue synced when its ids belong to the other account', function () {
    PlanPrice::query()->update(['stripe_price_id' => 'price_from_the_test_account']);
    StripeCatalogueMode::record(OperatingMode::Test);

    OperatingMode::set(OperatingMode::Live);

    expect(checkFor('billing.catalogue_synced')->satisfied)->toBeFalse();
    expect(checkFor('billing.catalogue_synced')->severity)->toBe(Check::SEVERITY_BLOCKING);
});

it('says the sale is refused and a fresh sync is needed, not that a column is empty', function () {
    PlanPrice::query()->update(['stripe_price_id' => 'price_from_the_test_account']);
    StripeCatalogueMode::record(OperatingMode::Test);

    OperatingMode::set(OperatingMode::Live);

    // Der Satz für „nie abgeglichen" wäre hier falsch: abgeglichen wurde, nur
    // in einem anderen Konto — und ein blosser neuer Lauf repariert das NICHT,
    // weil stripe:sync-catalogue jede Zeile überspringt, die schon eine ID
    // trägt. Das muss dastehen, sonst schickt die Seite den Betreiber auf einen
    // Befehl, der ihm „already in step" antwortet.
    expect(checkFor('billing.catalogue_synced')->breaks)
        ->not->toBe(__('readiness.billing.catalogue_synced_breaks'))
        // Nur DIESER Fall trägt die Leeranweisung.
        ->and(checkFor('billing.catalogue_synced')->breaks)->toContain('stripe_plan_prices');
});

/**
 * Der dritte Zustand, und er darf nicht mit dem zweiten zusammenfallen.
 *
 * IDs liegen da, aber nichts sagt, aus welchem Konto — weil ein Abgleich
 * mittendrin abgebrochen ist, oder weil eine Bestellung sich ihren fehlenden
 * Preis selbst angelegt hat (CheckoutController → PlanPrices::ensure(),
 * BookAddon → AddonPrices::ensure(); keiner der beiden ruft record()). Sie
 * stammen aus genau dem geltenden Konto. Wer hier die Leeranweisung liest,
 * löscht einen Katalog, an dem laufende Verträge abgerechnet werden.
 */
it('tells an unrecorded origin apart from a foreign account', function () {
    PlanPrice::query()->update(['stripe_price_id' => 'price_minted_by_a_checkout']);
    Settings::forget(StripeCatalogueMode::SETTING);

    // Blockierend bleibt es: beweisen lässt sich die Herkunft nicht, und die
    // Seite behauptet nichts, was sie nicht weiß.
    expect(checkFor('billing.catalogue_synced')->satisfied)->toBeFalse();

    // Aber die Handlungsanweisung ist „lauf den Abgleich noch einmal", nicht
    // „lösche deinen Katalog" — kein Registername in diesem Satz.
    expect(checkFor('billing.catalogue_synced')->breaks)
        ->not->toContain('stripe_plan_prices')
        ->and(checkFor('billing.catalogue_synced')->breaks)->toContain('stripe:sync-catalogue');
});

it('is satisfied again once the catalogue was synced in the mode that is running', function () {
    PlanPrice::query()->update(['stripe_price_id' => 'price_from_the_live_account']);
    OperatingMode::set(OperatingMode::Live);
    StripeCatalogueMode::record();

    expect(checkFor('billing.catalogue_synced')->satisfied)->toBeTrue();
});

it('does not raise a permanent alarm for an internal family stripe never mirrors', function () {
    // stripe:sync-catalogue skips every family marked `internal` outright —
    // Product and Prices both, dry run and real (Task 12: a Stripe Price
    // cannot be deleted once minted, and a package nobody is ever billed for
    // is not mirrored). Both the internal test package and Enterprise are
    // published and on sale (`sales_enabled`) but `internal = true`, so their
    // price rows stay unsynced for ever, however often the sync runs. Unlike
    // the draft above, there is no command that would ever clear this one — a
    // query that does not also filter on `internal` would keep this check
    // permanently unsatisfied on every installation.
    PlanPrice::query()
        ->whereHas('version.family', fn ($query) => $query->where('internal', false))
        ->update(['stripe_price_id' => 'price_already_synced']);

    StripeCatalogueMode::record();

    expect(checkFor('billing.catalogue_synced')->satisfied)->toBeTrue();
});
