<?php

use App\Actions\ApplyStripeBillingEvent;
use App\Actions\ResumeInstance;
use App\Actions\SuspendInstance;
use App\Models\Customer;
use App\Models\DunningCase;
use App\Models\Instance;
use App\Models\Order;
use App\Models\Subscription;
use App\Services\Billing\DunningSchedule;
use App\Services\Proxmox\FakeProxmoxClient;
use App\Services\Proxmox\ProxmoxClient;
use App\Services\Stripe\FakeStripeClient;
use App\Services\Stripe\StripeClient;
use Illuminate\Support\Carbon;

/**
 * Sperre und Freigabe, zusammen gebaut.
 *
 * Getrennt gebaut gäbe es einen Weg hinunter und keinen zurück — und der
 * einzige Zustand, aus dem ein Kunde nicht selbst herausfindet, ist der, in dem
 * seine Cloud steht.
 *
 * **Abschalten heisst abschalten.** Geordnet, mit Zeitlimit, und NICHTS wird
 * gelöscht: keine Platte angefasst, keine Instanz beendet, kein DNS-Eintrag
 * entfernt. Der teuerste Test in dieser Datei ist der, der das festhält.
 */
function suspendableInstance(): Instance
{
    $customer = Customer::factory()->create(['stripe_customer_id' => 'cus_42']);
    $order = Order::factory()->create(['customer_id' => $customer->id, 'plan' => 'start']);

    return Instance::factory()->create([
        'customer_id' => $customer->id, 'order_id' => $order->id,
        'plan' => 'start', 'status' => 'active', 'vmid' => 101,
    ]);
}

// ---- Die Sperre ----------------------------------------------------------

it('shuts the machine down without deleting anything', function () {
    $instance = suspendableInstance();
    $pve = new FakeProxmoxClient;
    app()->instance(ProxmoxClient::class, $pve);

    app(SuspendInstance::class)($instance);

    $instance->refresh();

    expect($instance->suspended_at)->not->toBeNull()
        // Geordnet heruntergefahren, nicht abgewürgt.
        ->and($pve->shutdownCalls)->toHaveCount(1)
        ->and($pve->shutdownCalls[0]['vmid'])->toBe(101)
        // Und alles, was den Kunden ausmacht, steht noch.
        ->and($instance->status)->toBe('active')
        ->and(Instance::query()->whereKey($instance->id)->exists())->toBeTrue()
        ->and($instance->vmid)->toBe(101);
});

it('does not shut a machine down twice', function () {
    // Ein zweiter Tageslauf, ein Neustart des Arbeiterprozesses: die Sperre
    // ist ein Zustand, kein Ereignis.
    $instance = suspendableInstance();
    $pve = new FakeProxmoxClient;
    app()->instance(ProxmoxClient::class, $pve);

    app(SuspendInstance::class)($instance);
    app(SuspendInstance::class)($instance->fresh());

    expect($pve->shutdownCalls)->toHaveCount(1);
});

// ---- Die Freigabe --------------------------------------------------------

it('starts the machine again and clears the mark', function () {
    $instance = suspendableInstance();
    $pve = new FakeProxmoxClient;
    app()->instance(ProxmoxClient::class, $pve);

    app(SuspendInstance::class)($instance);
    app(ResumeInstance::class)($instance->fresh());

    expect($instance->fresh()->suspended_at)->toBeNull()
        ->and($pve->runningVmids)->toContain(101);
});

// ---- Der Fall schliesst sich -------------------------------------------

function suspendedCase(): DunningCase
{
    $instance = suspendableInstance();
    // Über die BESTELLUNG verknüpft, wie im Betrieb: OpenSubscription füllt
    // `instance_id` nicht, `order_id` steht bei jedem Vertrag.
    $subscription = Subscription::factory()->create([
        'customer_id' => $instance->customer_id,
        'order_id' => $instance->order_id,
        'stripe_subscription_id' => 'sub_42',
        'stripe_status' => 'past_due',
    ]);

    return DunningCase::query()->create([
        'subscription_id' => $subscription->id,
        'stripe_invoice_id' => 'in_rueckstand',
        'level' => DunningSchedule::SUSPENDED,
        'opened_at' => Carbon::now()->subDays(24),
        'next_step_at' => null,
        'fee_invoice_ids' => ['2' => 'in_fee_2', '3' => 'in_fee_3'],
    ]);
}

it('closes the case and boots the machine when everything is paid', function () {
    $case = suspendedCase();
    $pve = new FakeProxmoxClient;
    app()->instance(ProxmoxClient::class, $pve);
    app(SuspendInstance::class)(Instance::query()->sole());

    // Stripe meldet nichts mehr als offen.
    $stripe = new FakeStripeClient;
    $stripe->openInvoiceRows = [];
    app()->instance(StripeClient::class, $stripe);

    app(ApplyStripeBillingEvent::class)->invoicePaid([
        'id' => 'in_rueckstand', 'subscription' => 'sub_42', 'billing_reason' => 'subscription_cycle',
    ]);

    expect($case->fresh()->settled_at)->not->toBeNull()
        ->and(Instance::query()->sole()->suspended_at)->toBeNull();
});

it('keeps the case open while the fee is still unpaid', function () {
    // Der Kern der Bedingung „bis Betrag PLUS Gebühren beglichen sind". Wer den
    // Rückstand zahlt und die Mahnspesen offen lässt, ist nicht fertig — und
    // seine Cloud bleibt aus.
    $case = suspendedCase();
    $pve = new FakeProxmoxClient;
    app()->instance(ProxmoxClient::class, $pve);
    app(SuspendInstance::class)(Instance::query()->sole());

    $stripe = new FakeStripeClient;
    $stripe->openInvoiceRows = [[
        'id' => 'in_fee_3', 'number' => 'R-3', 'amount_due_cents' => 1000,
        'currency' => 'EUR', 'created_at' => '1750000000',
    ]];
    app()->instance(StripeClient::class, $stripe);

    app(ApplyStripeBillingEvent::class)->invoicePaid([
        'id' => 'in_rueckstand', 'subscription' => 'sub_42', 'billing_reason' => 'subscription_cycle',
    ]);

    expect($case->fresh()->settled_at)->toBeNull()
        ->and(Instance::query()->sole()->suspended_at)->not->toBeNull();
});

it('settles a case that never got as far as a suspension', function () {
    $instance = suspendableInstance();
    $subscription = Subscription::factory()->create([
        'customer_id' => $instance->customer_id,
        'order_id' => $instance->order_id,
        'stripe_subscription_id' => 'sub_42',
    ]);
    $case = DunningCase::query()->create([
        'subscription_id' => $subscription->id,
        'stripe_invoice_id' => 'in_rueckstand',
        'level' => 1,
        'opened_at' => Carbon::now()->subDays(3),
        'next_step_at' => Carbon::now()->addDays(7),
        'fee_invoice_ids' => [],
    ]);

    app()->instance(ProxmoxClient::class, new FakeProxmoxClient);
    app()->instance(StripeClient::class, new FakeStripeClient);

    app(ApplyStripeBillingEvent::class)->invoicePaid([
        'id' => 'in_rueckstand', 'subscription' => 'sub_42', 'billing_reason' => 'subscription_cycle',
    ]);

    expect($case->fresh()->settled_at)->not->toBeNull();
});

it('shuts the cloud down when the last level is reached', function () {
    // Stufe 4 stand bis hierher nur im Zeitplan. Ohne diesen Anschluss wäre
    // die Sperre eine Zahl in einer Spalte, und die Cloud liefe weiter.
    $instance = suspendableInstance();
    // Über die BESTELLUNG verknüpft, wie im Betrieb: OpenSubscription füllt
    // `instance_id` nicht, `order_id` steht bei jedem Vertrag.
    $subscription = Subscription::factory()->create([
        'customer_id' => $instance->customer_id,
        'order_id' => $instance->order_id,
        'stripe_subscription_id' => 'sub_42',
        'stripe_status' => 'past_due',
    ]);
    DunningCase::query()->create([
        'subscription_id' => $subscription->id,
        'stripe_invoice_id' => 'in_rueckstand',
        'level' => DunningSchedule::SUSPENDED - 1,
        'opened_at' => Carbon::now()->subDays(17),
        'next_step_at' => Carbon::now()->subMinute(),
        'fee_invoice_ids' => [],
    ]);

    $pve = new FakeProxmoxClient;
    app()->instance(ProxmoxClient::class, $pve);
    app()->instance(StripeClient::class, new FakeStripeClient);

    test()->artisan('clupilot:advance-dunning')->assertSuccessful();

    expect($instance->fresh()->suspended_at)->not->toBeNull()
        ->and($pve->shutdownCalls)->toHaveCount(1)
        // Und wieder: nichts gelöscht.
        ->and(Instance::query()->whereKey($instance->id)->exists())->toBeTrue();
});
