<?php

use App\Models\Customer;
use App\Models\DunningCase;
use App\Models\Subscription;
use App\Services\Billing\DunningSchedule;
use App\Services\Stripe\FakeStripeClient;
use App\Services\Stripe\HttpStripeClient;
use App\Services\Stripe\StripeClient;
use App\Support\Settings;
use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\Http;

/**
 * Der Tageslauf: fällige Fälle eine Stufe weiter, und ab der eingestellten
 * Stufe eine Gebührenrechnung.
 *
 * Die Gebühr wird eine EIGENE Rechnung neben der gescheiterten. Eine
 * Stripe-Abo-Rechnung ist beim Scheitern bereits finalisiert; nachträglich
 * lässt sich nichts mehr darauf buchen. Die Alternative wäre gewesen, die
 * ursprüngliche zu stornieren und neu auszustellen — bei drei Mahnstufen also
 * dreimal, und die Buchhaltung sähe drei Stornos für einen Vorgang.
 */
function dunningCaseAt(int $level, ?Carbon $due = null): DunningCase
{
    $customer = Customer::factory()->create(['stripe_customer_id' => 'cus_42']);
    $subscription = Subscription::factory()->create([
        'customer_id' => $customer->id,
        'stripe_subscription_id' => 'sub_42',
        'stripe_status' => 'past_due',
    ]);

    return DunningCase::query()->create([
        'subscription_id' => $subscription->id,
        'stripe_invoice_id' => 'in_'.$level,
        'level' => $level,
        'opened_at' => Carbon::now()->subDays(DunningSchedule::dayOfLevel($level)),
        'next_step_at' => $due ?? Carbon::now()->subMinute(),
        'fee_invoice_ids' => [],
    ]);
}

// ---- Der Client-Aufruf ---------------------------------------------------

it('creates a fee invoice that Stripe collects on its own', function () {
    config()->set('admin_access.secrets_key', 'base64:'.base64_encode(random_bytes(32)));
    withStripeSecret();
    Http::preventStrayRequests();

    Http::fake([
        'api.stripe.com/v1/invoiceitems' => Http::response(['id' => 'ii_1']),
        'api.stripe.com/v1/invoices' => Http::response(['id' => 'in_fee_1']),
    ]);

    $id = app(HttpStripeClient::class)->createFeeInvoice('cus_42', 500, 'EUR', '2. Mahnung');

    expect($id)->toBe('in_fee_1');

    // `auto_advance` ist der Punkt: ohne das bliebe die Rechnung ein Entwurf,
    // und niemand zöge sie je ein.
    Http::assertSent(fn ($request) => str_ends_with($request->url(), '/invoices')
        && $request['auto_advance'] === 'true'
        && $request['customer'] === 'cus_42');

    Http::assertSent(fn ($request) => str_ends_with($request->url(), '/invoiceitems')
        && (int) $request['amount'] === 500);
});

// ---- Der Tageslauf -------------------------------------------------------

it('moves a due case one level on', function () {
    $case = dunningCaseAt(0);
    app()->instance(StripeClient::class, new FakeStripeClient);

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

    $case->refresh();

    expect($case->level)->toBe(1)
        // Die nächste Frist rechnet vom BEGINN, nicht von heute: sonst
        // verschöbe sich der ganze Plan mit jedem Lauf, der einen Tag zu spät
        // kommt, und aus 24 Tagen bis zur Sperre würden unbemerkt dreissig.
        ->and($case->next_step_at->isSameDay(
            $case->opened_at->copy()->addDays(DunningSchedule::dayOfLevel(2))
        ))->toBeTrue();
});

it('does nothing to a case that is not due yet', function () {
    $case = dunningCaseAt(1, Carbon::now()->addDays(3));
    app()->instance(StripeClient::class, new FakeStripeClient);

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

    expect($case->fresh()->level)->toBe(1);
});

it('runs twice in a day without moving anything twice', function () {
    // Der Zeitplaner ruft das täglich, und ein Betreiber ruft es von Hand
    // dazwischen. Zwei Stufen an einem Tag wären zwei Mahnungen und zwei
    // Gebühren.
    $case = dunningCaseAt(0);
    app()->instance(StripeClient::class, new FakeStripeClient);

    $this->artisan('clupilot:advance-dunning');
    $this->artisan('clupilot:advance-dunning');

    expect($case->fresh()->level)->toBe(1);
});

it('mints a fee invoice from the configured level on', function () {
    $case = dunningCaseAt(1);
    $fake = new FakeStripeClient;
    app()->instance(StripeClient::class, $fake);

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

    $case->refresh();

    expect($case->level)->toBe(2)
        ->and($fake->feeInvoices)->toHaveCount(1)
        ->and($fake->feeInvoices[0]['amount'])->toBe(500)
        ->and($case->fee_invoice_ids)->toHaveCount(1);
});

it('charges nothing for the notice or the first reminder', function () {
    $case = dunningCaseAt(0);
    $fake = new FakeStripeClient;
    app()->instance(StripeClient::class, $fake);

    $this->artisan('clupilot:advance-dunning');

    expect($case->fresh()->level)->toBe(1)
        ->and($fake->feeInvoices)->toBe([]);
});

it('never mints the same level\'s fee twice', function () {
    // Ein zweiter Lauf am selben Tag, ein abgebrochener Lauf, ein Neustart des
    // Arbeiterprozesses — die Gebühr darf je Stufe genau einmal entstehen.
    $case = dunningCaseAt(1);
    $fake = new FakeStripeClient;
    app()->instance(StripeClient::class, $fake);

    $this->artisan('clupilot:advance-dunning');
    $case->update(['next_step_at' => Carbon::now()->subMinute(), 'level' => 2]);
    $this->artisan('clupilot:advance-dunning');

    expect(collect($fake->feeInvoices)->where('level', 2))->toHaveCount(1);
});

it('leaves a settled case alone', function () {
    $case = dunningCaseAt(1);
    $case->update(['settled_at' => Carbon::now()]);
    app()->instance(StripeClient::class, new FakeStripeClient);

    $this->artisan('clupilot:advance-dunning');

    expect($case->fresh()->level)->toBe(1);
});

it('follows the configured schedule rather than a fixed one', function () {
    Settings::set(DunningSchedule::DAYS, [0, 1, 2, 3, 4]);
    Settings::set(DunningSchedule::FEES, [2 => 250, 3 => 750]);

    $case = dunningCaseAt(1);
    $fake = new FakeStripeClient;
    app()->instance(StripeClient::class, $fake);

    $this->artisan('clupilot:advance-dunning');

    expect($fake->feeInvoices[0]['amount'])->toBe(250);
});
