<?php

use App\Actions\ApplyStripeBillingEvent;
use App\Livewire\Admin\Customers;
use App\Livewire\Billing;
use App\Livewire\ConfirmRemoveOrder;
use App\Models\Customer;
use App\Models\Instance;
use App\Models\Order;
use App\Models\Subscription;
use App\Models\User;
use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\Queue;
use Livewire\Livewire;

/**
 * A downgrade that was booked lands on the day it was booked for.
 *
 * It used to land whenever `current_period_end` said so, and Stripe moves that
 * column forward on every renewal — so a downgrade booked in March was deferred
 * to April by April's renewal and to May by May's, while the customer went on
 * being billed for the package they had asked to leave. The due date is now
 * decided once, at the moment the customer decides, and stamped on the contract.
 *
 * What is proved here is the customer's experience of that: the date does not
 * move, a second decision replaces the first rather than queueing behind it,
 * changing their mind the other way cancels it, and everybody who reads the
 * contract can see it coming.
 */

/** A customer on Business, with a machine and a live contract. */
function pendingChangeCustomer(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;
    // The contract Stripe would be billing: the renewal that used to move the
    // due date arrives against this id, so there has to be one.
    $subscription->update(['instance_id' => $instance->id, 'stripe_subscription_id' => 'sub_'.$customer->id]);

    // These tests call Billing::purchase() to book the downgrade/upgrade,
    // which since Task 5 refuses outright without a Stripe key for the
    // active mode.
    withStripeSecret();

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

it('applies a booked downgrade on its own date, even after a renewal has moved the period', function () {
    Queue::fake();
    [, $user, $subscription] = pendingChangeCustomer();

    $bookedFor = $subscription->current_period_end;

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

    expect($subscription->fresh()->pending_plan)->toBe('team')
        ->and($subscription->fresh()->pending_effective_at->eq($bookedFor))->toBeTrue();

    // The renewal the defect turned on: Stripe bills the term and pushes the
    // period out by a month. Nothing about the booking may move with it.
    app(ApplyStripeBillingEvent::class)->invoicePaid([
        'id' => 'in_renewal',
        'subscription' => $subscription->stripe_subscription_id,
        'billing_reason' => 'subscription_cycle',
        'period_start' => $bookedFor->getTimestamp(),
        'period_end' => $bookedFor->copy()->addMonth()->getTimestamp(),
        'amount_paid' => 39900,
    ]);

    $renewed = $subscription->fresh();

    expect($renewed->current_period_end->gt($bookedFor))->toBeTrue()
        ->and($renewed->pending_effective_at->eq($bookedFor))->toBeTrue();

    // The moment it was booked for, which is now in the middle of the term the
    // renewal opened. Before, the customer would have kept Business — and paid
    // for it — for another whole month.
    Carbon::setTestNow($bookedFor->copy()->addHour());
    $this->artisan('clupilot:apply-due-plan-changes')->assertSuccessful();

    $after = $subscription->fresh();

    expect($after->plan)->toBe('team')
        ->and($after->pending_plan)->toBeNull()
        ->and($after->pending_effective_at)->toBeNull()
        ->and(Order::query()->where('type', 'downgrade')->value('status'))->toBe('applied');

    Carbon::setTestNow();
});

it('replaces a booked downgrade when the customer picks a different package', function () {
    [, $user, $subscription] = pendingChangeCustomer();

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

    // One booking and one cart entry, not two of either: the second decision is
    // the customer's decision, and queueing it behind the first would move them
    // down twice.
    expect($subscription->fresh()->pending_plan)->toBe('start')
        ->and(Order::query()->where('type', 'downgrade')->count())->toBe(1)
        ->and(Order::query()->where('type', 'downgrade')->value('plan'))->toBe('start');
});

it('keeps a booked downgrade until the upgrade the customer chose instead is actually paid', function () {
    fakeServices();
    Queue::fake();
    [, $user, $subscription] = pendingChangeCustomer('team');

    $bookedFor = $subscription->current_period_end;

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

    expect($subscription->fresh()->pending_plan)->toBe('start');

    $component->call('purchase', 'upgrade', 'business');

    // The click used to unbook it there and then, and nothing in this
    // application marks a cart order paid — so the customer gave up a downgrade
    // they had genuinely booked in exchange for a cart line that was never
    // fulfilled. The booking stands while the upgrade is only wanted.
    expect($subscription->fresh()->pending_plan)->toBe('start')
        ->and($subscription->fresh()->pending_effective_at->eq($bookedFor))->toBeTrue();

    // Paid. Now the two genuinely contradict each other, and the booking goes:
    // left standing it would come due months later and quietly undo the bigger
    // package the customer has just paid for.
    Order::query()->where('type', 'upgrade')->sole()->update(['status' => 'paid']);

    expect($subscription->fresh()->plan)->toBe('business')
        ->and($subscription->fresh()->pending_plan)->toBeNull()
        ->and($subscription->fresh()->pending_effective_at)->toBeNull();

    Carbon::setTestNow($bookedFor->copy()->addHour());
    $this->artisan('clupilot:apply-due-plan-changes')->assertSuccessful();

    expect($subscription->fresh()->plan)->toBe('business');

    Carbon::setTestNow();
});

it('carries out the downgrade the customer kept when the upgrade is never paid', function () {
    Queue::fake();
    [, $user, $subscription] = pendingChangeCustomer('team');

    $bookedFor = $subscription->current_period_end;

    $component = Livewire::actingAs($user)->test(Billing::class);
    $component->call('purchase', 'downgrade', 'start');
    $component->call('purchase', 'upgrade', 'business');

    // Nothing pays the upgrade, which is the state every cart order is in today.
    // What the customer booked is what happens.
    Carbon::setTestNow($bookedFor->copy()->addHour());
    $this->artisan('clupilot:apply-due-plan-changes')->assertSuccessful();

    expect($subscription->fresh()->plan)->toBe('start')
        ->and($subscription->fresh()->pending_plan)->toBeNull();

    Carbon::setTestNow();
});

it('unbooks the downgrade when the customer takes it back out of the cart', function () {
    [, $user, $subscription] = pendingChangeCustomer();

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

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

    Livewire::actingAs($user)
        ->test(ConfirmRemoveOrder::class, ['uuid' => $order->uuid])
        ->call('remove');

    expect($subscription->fresh()->pending_plan)->toBeNull()
        ->and($subscription->fresh()->pending_effective_at)->toBeNull();

    Carbon::setTestNow($subscription->current_period_end->copy()->addHour());
    $this->artisan('clupilot:apply-due-plan-changes')->assertSuccessful();

    expect($subscription->fresh()->plan)->toBe('business');

    Carbon::setTestNow();
});

it('shows the booked downgrade to the customer and to the operator', function () {
    [, $user, $subscription] = pendingChangeCustomer();

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

    $when = $subscription->fresh()->pending_effective_at->local()->isoFormat('LL');

    // The customer, on the card that states what they have — the cart entry
    // disappears when it is paid for, and the booking does not.
    Livewire::actingAs($user)->test(Billing::class)
        ->assertSee(__('billing.pending_change_note', ['plan' => __('billing.plan.team'), 'date' => $when]));

    // And the operator, on the row that answers "what is this customer on",
    // because that is where the question gets asked.
    Livewire::actingAs(admin(), 'operator')->test(Customers::class)
        ->assertSee(__('admin.pending_change', ['plan' => __('billing.plan.team'), 'date' => $when]));
});

it('never applies a booked downgrade to a contract that has been cancelled', function () {
    Queue::fake();
    [, $user, $subscription] = pendingChangeCustomer();

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

    $bookedFor = $subscription->fresh()->pending_effective_at;

    // The customer leaves before the date arrives. Stripe ends the subscription
    // and the booking goes with the contract it was made on.
    app(ApplyStripeBillingEvent::class)->subscriptionDeleted([
        'id' => $subscription->stripe_subscription_id,
        'status' => 'canceled',
        'ended_at' => now()->getTimestamp(),
    ]);

    Carbon::setTestNow($bookedFor->copy()->addHour());
    $this->artisan('clupilot:apply-due-plan-changes')->assertSuccessful();

    $after = $subscription->fresh();

    expect($after->status)->toBe('cancelled')
        // Not moved onto a smaller package on the way out, and not left
        // announcing a change that can never happen.
        ->and($after->plan)->toBe('business')
        ->and($after->pending_plan)->toBeNull()
        ->and($after->pending_effective_at)->toBeNull();

    Carbon::setTestNow();
});

it('refuses to book a downgrade for someone who has no contract to book it on', function () {
    $customer = Customer::factory()->create();
    $user = User::factory()->create(['email' => $customer->email]);
    Instance::factory()->for($customer)->create(['plan' => 'business', 'status' => 'active']);

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

    // Accepted before, and it could never be carried out: there was nothing to
    // stamp the date on and nothing for the scheduler to find.
    expect(Order::query()->where('type', 'downgrade')->exists())->toBeFalse()
        ->and(Subscription::query()->count())->toBe(0);
});
