<?php

use App\Actions\ApplyStripeBillingEvent;
use App\Mail\CloudResumedMail;
use App\Mail\CloudSuspendedMail;
use App\Mail\DunningNoticeMail;
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;
use Illuminate\Support\Facades\Mail;

/**
 * Ohne die Mails merkt der Kunde von alldem nichts. Der Mahnlauf wäre dann eine
 * Maschine, die still Fristen zählt und am 24. Tag die Cloud abschaltet — für
 * den Kunden aus heiterem Himmel.
 */
function mailableCase(int $level = 0): DunningCase
{
    $customer = Customer::factory()->create([
        'name' => 'Berger GmbH', 'email' => 'berger@example.test', 'stripe_customer_id' => 'cus_42',
    ]);
    $order = Order::factory()->create(['customer_id' => $customer->id, 'plan' => 'start']);
    Instance::factory()->create([
        'customer_id' => $customer->id, 'order_id' => $order->id,
        'plan' => 'start', 'status' => 'active', 'vmid' => 101,
    ]);
    $subscription = Subscription::factory()->create([
        'customer_id' => $customer->id,
        'order_id' => $order->id,
        'stripe_subscription_id' => 'sub_42',
        'stripe_status' => 'past_due',
        'price_cents' => 21480,
    ]);

    return DunningCase::query()->create([
        'subscription_id' => $subscription->id,
        'stripe_invoice_id' => 'in_rueckstand',
        'level' => $level,
        'opened_at' => Carbon::now()->subDays(DunningSchedule::dayOfLevel($level)),
        'next_step_at' => Carbon::now()->subMinute(),
        'fee_invoice_ids' => [],
        // Alles bis zur aktuellen Stufe wurde bereits mitgeteilt — so sieht ein
        // Fall im Betrieb aus. Ohne diese Zeile hielte der Nachhol-Lauf die
        // Nachricht zur aktuellen Stufe für verloren und schickte sie zu Recht
        // nach.
        'notified_levels' => range(0, $level),
    ]);
}

it('tells the customer at once when the charge fails', function () {
    // Sofort, nicht erst mit dem Tageslauf: der Kunde soll davon erfahren,
    // solange er noch weiss, wovon die Rede ist.
    Mail::fake();

    $customer = Customer::factory()->create(['email' => 'berger@example.test', 'stripe_customer_id' => 'cus_42']);
    Subscription::factory()->create([
        'customer_id' => $customer->id, 'stripe_subscription_id' => 'sub_42', 'price_cents' => 21480,
    ]);

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

    Mail::assertQueued(DunningNoticeMail::class, fn ($mail) => $mail->level === 0
        && $mail->feeCents === 0
        && $mail->hasTo('berger@example.test'));
});

it('does not write again for a second failed attempt on the same invoice', function () {
    Mail::fake();

    $customer = Customer::factory()->create(['email' => 'berger@example.test', 'stripe_customer_id' => 'cus_42']);
    Subscription::factory()->create([
        'customer_id' => $customer->id, 'stripe_subscription_id' => 'sub_42',
    ]);

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

    Mail::assertQueued(DunningNoticeMail::class, 1);
});

it('sends a reminder with the fee spelled out separately', function () {
    // Rückstand und Mahnspesen getrennt: wer eine Zahl liest, die höher ist als
    // seine Rechnung, hält sie für einen Fehler und schreibt dem Support,
    // statt zu zahlen.
    Mail::fake();
    $case = mailableCase(1);
    $case->update(['fee_invoice_ids' => []]);
    app()->instance(StripeClient::class, new FakeStripeClient);
    app()->instance(ProxmoxClient::class, new FakeProxmoxClient);

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

    Mail::assertQueued(DunningNoticeMail::class, fn ($mail) => $mail->level === 2
        && $mail->amountCents === 21480
        && $mail->feeTotalCents === 500);
});

it('names the shutdown date while it is still ahead', function () {
    Mail::fake();
    mailableCase(0);
    app()->instance(StripeClient::class, new FakeStripeClient);
    app()->instance(ProxmoxClient::class, new FakeProxmoxClient);

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

    Mail::assertQueued(DunningNoticeMail::class, fn ($mail) => $mail->level === 1
        && $mail->suspendOn !== null);
});

it('sends the shutdown notice, not a fourth reminder', function () {
    Mail::fake();
    mailableCase(DunningSchedule::SUSPENDED - 1);
    app()->instance(StripeClient::class, new FakeStripeClient);
    app()->instance(ProxmoxClient::class, new FakeProxmoxClient);

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

    Mail::assertQueued(CloudSuspendedMail::class, 1);
    Mail::assertNotQueued(DunningNoticeMail::class);
});

it('sends the all-clear only to somebody whose cloud actually stood', function () {
    Mail::fake();
    $case = mailableCase(DunningSchedule::SUSPENDED);
    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();
    Mail::assertQueued(CloudResumedMail::class, 1);
});

it('stays quiet about a cloud that never stood', function () {
    // Wer auf Stufe 1 bezahlt, hat nie eine Sperre gesehen. Ihm zu schreiben,
    // seine Cloud laufe wieder, wäre eine Nachricht über etwas, das nie war.
    Mail::fake();
    mailableCase(1);
    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',
    ]);

    Mail::assertNotQueued(CloudResumedMail::class);
});

it('advances the case even when the mail cannot be sent', function () {
    // Ein Mailserver, der gerade nicht antwortet, darf keinen Fall aufhalten —
    // sonst bleibt der Kunde auf der Stufe stehen, auf der die Mail scheiterte,
    // und die Sperre kommt nie.
    $case = mailableCase(0);
    app()->instance(StripeClient::class, new FakeStripeClient);
    app()->instance(ProxmoxClient::class, new FakeProxmoxClient);

    Mail::shouldReceive('mailer')->andThrow(new RuntimeException('kein Mailserver'));

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

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