<?php

use App\Actions\ApplyStripeBillingEvent;
use App\Models\Customer;
use App\Models\DunningCase;
use App\Models\Subscription;
use App\Services\Billing\DunningSchedule;
use App\Support\Settings;
use Illuminate\Support\Carbon;

/**
 * Das Fundament des Mahnlaufs.
 *
 * Bis hierher setzte `invoicePaymentFailed()` den Vertrag auf `past_due` und
 * schwieg — keine Mail, keine Anzeige, keine Frist. Der Kunde erfuhr davon
 * erst, wenn seine Cloud stand.
 *
 * Der Zustand liegt in einer EIGENEN Tabelle, nicht als Spalte am Vertrag: ein
 * Kunde kann mehr als einmal in Rückstand geraten, und eine Spalte
 * überschriebe die vorige Episode — genau die will jemand sehen, wenn der
 * Kunde anruft und fragt, warum schon wieder Gebühren anfallen.
 */
function subscriptionInArrears(): Subscription
{
    $customer = Customer::factory()->create(['stripe_customer_id' => 'cus_42']);

    return Subscription::factory()->create([
        'customer_id' => $customer->id,
        'stripe_subscription_id' => 'sub_42',
        'stripe_status' => 'active',
    ]);
}

// ---- Der Zeitplan --------------------------------------------------------

it('starts with the schedule the owner agreed', function () {
    expect(DunningSchedule::dayOfLevel(0))->toBe(0)
        ->and(DunningSchedule::dayOfLevel(1))->toBe(3)
        ->and(DunningSchedule::dayOfLevel(2))->toBe(10)
        ->and(DunningSchedule::dayOfLevel(3))->toBe(17)
        ->and(DunningSchedule::dayOfLevel(DunningSchedule::SUSPENDED))->toBe(24);
});

it('charges nothing before the configured level, and the agreed amounts after', function () {
    // Tag 0 ist ein HINWEIS, keine Mahnung: eine abgelaufene Karte ist keine
    // Zahlungsverweigerung, und wer sie am selben Tag mit einer Gebühr
    // bedenkt, verliert Kunden, die zahlen wollten.
    expect(DunningSchedule::feeCents(0))->toBe(0)
        ->and(DunningSchedule::feeCents(1))->toBe(0)
        ->and(DunningSchedule::feeCents(2))->toBe(500)
        ->and(DunningSchedule::feeCents(3))->toBe(1000);
});

it('lets the console move the level a fee starts at', function () {
    Settings::set(DunningSchedule::FEE_FROM_LEVEL, 3);

    expect(DunningSchedule::feeCents(2))->toBe(0)
        ->and(DunningSchedule::feeCents(3))->toBe(1000);
});

it('lets the console move the deadlines, and keeps them in order', function () {
    Settings::set(DunningSchedule::DAYS, [0, 5, 12, 20, 30]);

    expect(DunningSchedule::dayOfLevel(1))->toBe(5)
        ->and(DunningSchedule::dayOfLevel(DunningSchedule::SUSPENDED))->toBe(30);

    // Eine Reihenfolge, die rückwärts läuft, ist kein Zeitplan. Geklemmt statt
    // gespeichert: sonst stünde die dritte Mahnung vor der ersten und der
    // Tageslauf spränge über Stufen.
    Settings::set(DunningSchedule::DAYS, [0, 20, 5, 12, 30]);

    $tage = DunningSchedule::days();
    expect($tage)->toBe(array_values(collect($tage)->sort()->all()));
});

// ---- Der Fall entsteht ---------------------------------------------------

it('opens a case the first time a payment fails', function () {
    $subscription = subscriptionInArrears();

    app(ApplyStripeBillingEvent::class)->invoicePaymentFailed([
        'id' => 'in_1', 'subscription' => 'sub_42', 'attempt_count' => 1,
    ]);

    $case = DunningCase::query()->where('subscription_id', $subscription->id)->sole();

    expect($case->level)->toBe(0)
        ->and($case->stripe_invoice_id)->toBe('in_1')
        ->and($case->settled_at)->toBeNull()
        // Tag 0 ist heute — der Hinweis geht sofort hinaus, die erste Mahnung
        // erst nach der ersten Frist.
        ->and($case->next_step_at->isSameDay(Carbon::now()->addDays(3)))->toBeTrue();
});

it('does not open a second case for the same invoice', function () {
    // Stripe versucht dieselbe Rechnung mehrfach, und jeder Versuch ist ein
    // eigenes Ereignis. Zwei Fälle für eine Schuld hiesse zwei Mahnläufe und
    // doppelte Gebühren.
    subscriptionInArrears();

    foreach ([1, 2, 3] as $attempt) {
        app(ApplyStripeBillingEvent::class)->invoicePaymentFailed([
            'id' => 'in_1', 'subscription' => 'sub_42', 'attempt_count' => $attempt,
        ]);
    }

    expect(DunningCase::query()->count())->toBe(1);
});

it('opens a fresh case for a later, different invoice', function () {
    // Der Grund für die eigene Tabelle: ein Kunde kann im März und im Juni in
    // Rückstand geraten, und beide Episoden gehören nachlesbar nebeneinander.
    $subscription = subscriptionInArrears();

    app(ApplyStripeBillingEvent::class)->invoicePaymentFailed(['id' => 'in_1', 'subscription' => 'sub_42']);
    DunningCase::query()->first()->update(['settled_at' => Carbon::now()]);
    app(ApplyStripeBillingEvent::class)->invoicePaymentFailed(['id' => 'in_2', 'subscription' => 'sub_42']);

    expect(DunningCase::query()->where('subscription_id', $subscription->id)->count())->toBe(2)
        ->and(DunningCase::query()->whereNull('settled_at')->sole()->stripe_invoice_id)->toBe('in_2');
});

it('leaves the contract alone when nothing can be resolved', function () {
    app(ApplyStripeBillingEvent::class)->invoicePaymentFailed(['id' => 'in_1', 'subscription' => 'sub_unbekannt']);

    expect(DunningCase::query()->count())->toBe(0);
});
