<?php

use App\Livewire\Billing;
use App\Models\Customer;
use App\Models\Instance;
use App\Models\Order;
use App\Models\Subscription;
use App\Models\SubscriptionAddon;
use App\Models\SubscriptionRecord;
use App\Models\User;
use App\Services\Billing\AddonCatalogue;
use App\Services\Stripe\FakeStripeClient;
use App\Services\Stripe\StripeClient;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Queue;
use Livewire\Livewire;

/**
 * Whatever marks an order paid delivers what the order bought.
 *
 * The seam existed for exactly one type. OrderObserver fired only for `upgrade`,
 * so a module, a storage pack or a traffic pack marked paid — by an operator, by
 * a seeder, by the second checkout when it is built — reached nothing at all:
 * there was no path from a paid module order to a SubscriptionAddon anywhere in
 * the application, which left AddonPrices, SyncStripeAddonItems and
 * clupilot:end-cancelled-addons with no reachable caller behind a payment.
 *
 * **Nothing in this application marks a cart order paid yet.** That is why the
 * trigger hangs on the fact rather than on a call site: these tests do by hand
 * what the missing checkout will do, and the delivery is written once, where it
 * cannot be forgotten by whichever thing eventually pays.
 */
beforeEach(function () {
    $this->stripe = new FakeStripeClient;
    app()->instance(StripeClient::class, $this->stripe);

    // Every test here calls Billing::purchase() to place the order it then
    // marks paid by hand. Since Task 5, that call refuses outright without a
    // Stripe key for the active mode — binding the fake StripeClient above
    // does not help, because the guard reads the vault directly rather than
    // going through the StripeClient interface.
    withStripeSecret();
});

/** A customer on a live contract, with a machine. */
function paidOrderCustomer(string $plan = 'business'): array
{
    $customer = Customer::factory()->create();
    $user = User::factory()->create(['email' => $customer->email]);
    $order = Order::factory()->withSubscription()->for($customer)->create(['plan' => $plan]);
    $instance = Instance::factory()->for($customer)->create([
        'order_id' => $order->id, 'plan' => $plan, 'status' => 'active',
    ]);

    $subscription = $order->subscription;
    $subscription->update(['instance_id' => $instance->id, 'stripe_subscription_id' => 'sub_'.$customer->id]);

    return [$customer, $user, $subscription->fresh()];
}

it('books the module a paid order bought', function () {
    Queue::fake();
    [, $user, $subscription] = paidOrderCustomer();

    Livewire::actingAs($user)->test(Billing::class)->call('purchase', 'addon', 'collabora_pro');

    $order = Order::query()->where('type', 'addon')->sole();

    expect(SubscriptionAddon::query()->count())->toBe(0);

    $order->update(['status' => 'paid']);

    $addon = SubscriptionAddon::query()->sole();

    expect($addon->addon_key)->toBe('collabora_pro')
        ->and($addon->order_id)->toBe($order->id)
        // Frozen at the price the customer was shown, and entered in the register.
        ->and($addon->price_cents)->toBe(1900)
        ->and(SubscriptionRecord::query()->where('event', SubscriptionRecord::EVENT_ADDON_BOOKED)->count())->toBe(1)
        // And it is on the Stripe subscription, which is what makes it recur.
        ->and($this->stripe->subscriptionItems)->toHaveCount(1);

    // Delivered once, however often the row is touched again.
    $order->update(['status' => 'paid', 'updated_at' => now()->addMinute()]);

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

it('books the storage pack a paid order bought, one per order', function () {
    Queue::fake();
    [, $user, $subscription] = paidOrderCustomer();

    // Two packs is two orders, because that is how the shop writes them.
    Livewire::actingAs($user)->test(Billing::class)->call('purchase', 'storage', null, 2);

    Order::query()->where('type', 'storage')->get()->each->update(['status' => 'paid']);

    $packs = SubscriptionAddon::query()->where('addon_key', AddonCatalogue::STORAGE)->get();

    expect($packs)->toHaveCount(2)
        ->and($packs->pluck('order_id')->unique())->toHaveCount(2);
});

it('delivers nothing for a downgrade, which is never paid for', function () {
    Queue::fake();
    [, $user, $subscription] = paidOrderCustomer();

    Livewire::actingAs($user)->test(Billing::class)->call('purchase', 'downgrade', 'team');

    // A downgrade lands at the end of the term from the date stamped on the
    // contract. Marked paid by hand it must not move the package today.
    Order::query()->where('type', 'downgrade')->sole()->update(['status' => 'paid']);

    expect($subscription->fresh()->plan)->toBe('business')
        ->and($subscription->fresh()->pending_plan)->toBe('team');
});

it('says out loud that a paid traffic pack has nowhere to go', function () {
    Queue::fake();
    [, $user] = paidOrderCustomer();

    Log::spy();

    Livewire::actingAs($user)->test(Billing::class)->call('purchase', 'traffic');
    Order::query()->where('type', 'traffic')->sole()->update(['status' => 'paid']);

    // The metered allowance is a standing count of packs on the instance, while a
    // top-up is sold as a one-off for the month running out — so raising it would
    // hand over the extra thousand gigabytes every month for ever off one payment,
    // and doing nothing leaves the customer paying for nothing. Which of the two
    // is meant is a commercial decision, and until it is made the order has to be
    // impossible to miss.
    Log::shouldHaveReceived('error')
        ->withArgs(fn (string $message) => str_contains($message, 'no fulfilment path'))
        ->once();
});

it('never lets a failed delivery undo the record that money changed hands', function () {
    Queue::fake();
    [$customer, $user] = paidOrderCustomer();

    Livewire::actingAs($user)->test(Billing::class)->call('purchase', 'addon', 'collabora_pro');

    $order = Order::query()->where('type', 'addon')->sole();

    // The contract goes before the order is settled. Booking will refuse, and the
    // order must still be paid: that write is the evidence of the payment, and
    // rolling it back would erase the payment and leave nothing to retry from.
    Subscription::query()->where('customer_id', $customer->id)->delete();

    $order->update(['status' => 'paid']);

    expect($order->fresh()->status)->toBe('paid')
        ->and(SubscriptionAddon::query()->count())->toBe(0);
});
