<?php

use App\Livewire\Admin\PaymentProblems;
use App\Models\Customer;
use App\Models\DunningCase;
use App\Models\FailedCheckout;
use App\Models\Order;
use App\Models\Subscription;
use Illuminate\Support\Carbon;
use Livewire\Livewire;

/**
 * Zwei Arten, wie Geld ausbleibt, an einer Stelle.
 *
 * 1. Ein laufender Vertrag, dessen Abbuchung scheitert — daraus wird ein
 *    Mahnfall (DunningCase).
 * 2. Ein Kauf, der nie zustande kam: bei SEPA oder Sofortüberweisung schliesst
 *    der Kunde die Kasse ab, die Zahlung platzt Tage später, und
 *    `checkout.session.async_payment_failed` wurde bis hierher EMPFANGEN UND
 *    IGNORIERT. Es entsteht kein Auftrag — das ist richtig —, aber es entsteht
 *    auch keine Zeile. Für den Betreiber ein verlorener Verkauf, von dem er
 *    nie hört.
 */
function failedAsyncCheckout(string $sessionId = 'cs_geplatzt'): array
{
    return [
        'id' => 'evt_'.$sessionId,
        'type' => 'checkout.session.async_payment_failed',
        'data' => ['object' => [
            'id' => $sessionId,
            'customer_details' => ['email' => 'kunde@example.test', 'name' => 'Berger GmbH'],
            'amount_total' => 21480,
            'currency' => 'eur',
            'metadata' => ['plan' => 'team'],
        ]],
    ];
}

it('records a payment that bounced days later', function () {
    $this->postJson(route('webhooks.stripe'), failedAsyncCheckout())->assertOk();

    $problem = FailedCheckout::query()->sole();

    expect($problem->email)->toBe('kunde@example.test')
        ->and($problem->amount_cents)->toBe(21480)
        ->and($problem->plan)->toBe('team')
        ->and($problem->resolved_at)->toBeNull();
});

it('records it once, however often Stripe retries the webhook', function () {
    foreach (range(1, 3) as $ignored) {
        $this->postJson(route('webhooks.stripe'), failedAsyncCheckout())->assertOk();
    }

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

it('still creates no order for it', function () {
    // Das war schon richtig und bleibt es: eine geplatzte Zahlung ist kein
    // Auftrag. Neu ist nur, dass sie eine Spur hinterlässt.
    $this->postJson(route('webhooks.stripe'), failedAsyncCheckout())->assertOk();

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

it('shows both kinds of problem on one page', function () {
    $customer = Customer::factory()->create(['name' => 'Berger GmbH', 'stripe_customer_id' => 'cus_42']);
    $subscription = Subscription::factory()->create(['customer_id' => $customer->id]);
    DunningCase::query()->create([
        'subscription_id' => $subscription->id,
        'stripe_invoice_id' => 'in_1',
        'level' => 2,
        'opened_at' => Carbon::now()->subDays(10),
        'next_step_at' => Carbon::now()->addDays(7),
        'fee_invoice_ids' => [],
    ]);
    FailedCheckout::query()->create([
        'stripe_session_id' => 'cs_geplatzt', 'email' => 'neu@example.test',
        'name' => 'Neu GmbH', 'plan' => 'start', 'amount_cents' => 4900,
        'currency' => 'EUR', 'failed_at' => Carbon::now(),
    ]);

    Livewire::actingAs(operator('Owner'), 'operator')
        ->test(PaymentProblems::class)
        ->assertSee('Berger GmbH')
        ->assertSee('neu@example.test');
});

it('lets somebody mark a bounced payment as dealt with', function () {
    $problem = FailedCheckout::query()->create([
        'stripe_session_id' => 'cs_geplatzt', 'email' => 'neu@example.test',
        'plan' => 'start', 'amount_cents' => 4900, 'currency' => 'EUR',
        'failed_at' => Carbon::now(),
    ]);

    // Seit R20 geht die Begründung durch das Modal (siehe ResolveInModalTest):
    // die Seite fängt nur noch das Ereignis auf. Ein Feld in der Zeile wäre
    // genau der Inline-Editor, den die Regel verbietet.
    Livewire::actingAs(operator('Owner'), 'operator')
        ->test(PaymentProblems::class)
        ->call('onResolved', $problem->id, 'Kunde hat überwiesen')
        ->assertHasNoErrors();

    $problem->refresh();

    expect($problem->resolved_at)->not->toBeNull()
        // Wer und warum, nicht nur dass. Ein erledigter Vorgang ohne
        // Begründung ist beim nächsten Nachfragen wertlos.
        ->and($problem->note)->toContain('überwiesen');
});

it('is not reachable without billing.manage', function () {
    // Zahlungsprobleme nennen Kundennamen, Beträge und offene Schulden.
    // `Support` hat console.view, aber nicht billing.manage.
    Livewire::actingAs(operator('Support'), 'operator')
        ->test(PaymentProblems::class)
        ->assertForbidden();
});

it('is reachable for the billing role', function () {
    // Genau der Fall aus dem Gespräch: die Buchhaltung soll das sehen, ohne
    // Admin zu sein.
    Livewire::actingAs(operator('Billing'), 'operator')
        ->test(PaymentProblems::class)
        ->assertOk();
});
