<?php

use App\Livewire\ConfirmCancelPackage;
use App\Mail\InvoiceMail;
use App\Models\Customer;
use App\Models\Instance;
use App\Models\Invoice;
use App\Models\Order;
use App\Models\Subscription;
use App\Models\SubscriptionRecord;
use App\Models\User;
use App\Services\Stripe\FakeStripeClient;
use App\Services\Stripe\StripeClient;
use App\Support\CompanyProfile;
use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\Mail;
use Livewire\Livewire;

/**
 * A cancellation, and the two things that used to happen instead of it.
 *
 * Nothing in this application could cancel a Stripe subscription — there was no
 * such method on the client at all — so a customer who cancelled kept being
 * charged every month, for ever, for a package that had ended. And the date they
 * were promised was worked out by adding MONTHS to the order date whatever the
 * term said, so a yearly customer who cancelled in March lost the nine months
 * they had already paid for.
 *
 * The example these tests are built on, once, so the dates can be checked by hand
 * rather than recomputed from the implementation:
 *
 *   contract concluded   1 January 2026
 *   term                 yearly — 1 January 2026 → 1 January 2027
 *   cancellation         15 March 2026
 *
 *   service ends         1 January 2027   ← the term that was paid for
 *   the old answer       1 April 2026     ← months added to the order date
 */
beforeEach(function () {
    Carbon::setTestNow('2026-03-15 09:00:00');

    CompanyProfile::put([
        'name' => 'CluPilot Cloud e.U.',
        'address' => 'Dreherstraße 66/1/8',
        'postcode' => '1110',
        'city' => 'Wien',
        'vat_id' => 'ATU00000000',
    ]);
});

afterEach(function () {
    Carbon::setTestNow();
});

/** A Stripe that records what it was asked to stop. */
function cancellationStripe(): FakeStripeClient
{
    $fake = new FakeStripeClient;
    app()->instance(StripeClient::class, $fake);

    return $fake;
}

/**
 * A running package its owner can reach the cancel modal for.
 *
 * @return array{0: User, 1: Instance, 2: Subscription}
 */
function cancellablePackage(string $term = Subscription::TERM_MONTHLY, array $contract = []): array
{
    $user = User::factory()->create(['email' => 'berger@example.test', 'email_verified_at' => now()]);

    $customer = Customer::factory()->create([
        'email' => 'berger@example.test',
        'user_id' => $user->id,
        'status' => 'active',
    ]);

    // Two and a half months ago, which is what makes the old month-walk visible:
    // it would land on 1 April whatever the term is.
    $order = Order::factory()->create([
        'customer_id' => $customer->id,
        'plan' => 'team',
        'created_at' => '2026-01-01 09:00:00',
    ]);

    $instance = Instance::factory()->create([
        'customer_id' => $customer->id,
        'order_id' => $order->id,
        'plan' => 'team',
        'status' => 'active',
        'subdomain' => 'berger',
    ]);

    $subscription = Subscription::factory()->plan('team', $term)->create(array_merge([
        'customer_id' => $customer->id,
        'order_id' => $order->id,
        'instance_id' => $instance->id,
        'stripe_subscription_id' => 'sub_cancel',
        'started_at' => '2026-01-01 09:00:00',
        'current_period_start' => '2026-01-01 09:00:00',
        'current_period_end' => $term === Subscription::TERM_YEARLY
            ? '2027-01-01 09:00:00'
            : '2026-04-01 09:00:00',
    ], $contract));

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

it('tells Stripe to stop at the end of the term and keeps a yearly customer their year', function () {
    $stripe = cancellationStripe();
    [$user, $instance, $contract] = cancellablePackage(Subscription::TERM_YEARLY);

    Livewire::actingAs($user)->test(ConfirmCancelPackage::class)
        ->set('confirmName', 'berger')
        ->set('reason', 'no_longer_needed')
        ->set('exportWish', true)
        ->call('cancelPackage')
        ->assertHasNoErrors();

    // Stripe is told, and told WHICH cancellation this is. At the period end,
    // because the customer keeps everything they have paid for; an immediate stop
    // would be a withdrawal, which is a different promise and a refund.
    expect($stripe->cancellations)->toHaveCount(1)
        ->and($stripe->cancellations[0]['subscription'])->toBe('sub_cancel')
        ->and($stripe->cancellations[0]['when'])->toBe(StripeClient::CANCEL_AT_PERIOD_END);

    $instance->refresh();

    // The end of the YEAR that was bought, written out rather than recomputed —
    // and explicitly not the date months-from-the-order-date used to produce.
    expect($instance->status)->toBe('cancellation_scheduled')
        ->and($instance->service_ends_at->toDateTimeString())->toBe('2027-01-01 09:00:00')
        ->and($instance->service_ends_at->toDateString())->not->toBe('2026-04-01');

    $contract->refresh();

    // The contract carries the request — and is still ACTIVE, because until the
    // first of January this is a paying customer with a running cloud. Only
    // Stripe reporting the subscription gone ends it.
    expect($contract->cancel_requested_at?->toDateTimeString())->toBe('2026-03-15 09:00:00')
        ->and($contract->status)->toBe('active')
        ->and($contract->cancelled_at)->toBeNull();
});

it('keeps a monthly customer their month and no longer', function () {
    $stripe = cancellationStripe();
    [$user, $instance] = cancellablePackage(Subscription::TERM_MONTHLY);

    Livewire::actingAs($user)->test(ConfirmCancelPackage::class)
        ->set('confirmName', 'berger')
        ->set('reason', 'no_longer_needed')
        ->set('exportWish', true)
        ->call('cancelPackage')
        ->assertHasNoErrors();

    // The other half of the pair: a monthly contract really does end a month
    // after its period started, so the yearly assertion above is about the TERM
    // and not about this code being generous to everybody.
    expect($instance->fresh()->service_ends_at->toDateTimeString())->toBe('2026-04-01 09:00:00')
        ->and($stripe->cancellations[0]['when'])->toBe(StripeClient::CANCEL_AT_PERIOD_END);
});

it('cancels a granted package cleanly and asks Stripe nothing at all', function () {
    $stripe = cancellationStripe();

    // A grant was never sold through Stripe — GrantSubscription leaves the id
    // null on purpose — so there is no subscription to stop and nothing has gone
    // wrong.
    [$user, $instance, $contract] = cancellablePackage(Subscription::TERM_MONTHLY, [
        'stripe_subscription_id' => null,
        'granted_at' => '2026-01-01 09:00:00',
        'granted_by' => admin()->id,
    ]);

    Livewire::actingAs($user)->test(ConfirmCancelPackage::class)
        ->set('confirmName', 'berger')
        ->set('reason', 'no_longer_needed')
        ->set('exportWish', true)
        ->call('cancelPackage')
        ->assertHasNoErrors();

    expect($stripe->cancellations)->toBeEmpty()
        ->and($instance->fresh()->status)->toBe('cancellation_scheduled')
        ->and($instance->fresh()->service_ends_at->toDateTimeString())->toBe('2026-04-01 09:00:00')
        ->and($contract->fresh()->cancel_requested_at)->not->toBeNull();
});

it('records nothing at all when Stripe cannot be told to stop', function () {
    $stripe = cancellationStripe();
    $stripe->failWith = 'connection refused';

    [$user, $instance, $contract] = cancellablePackage();

    Livewire::actingAs($user)->test(ConfirmCancelPackage::class)
        ->set('confirmName', 'berger')
        ->set('reason', 'no_longer_needed')
        ->set('exportWish', true)
        ->call('cancelPackage')
        ->assertHasErrors(['confirmName']);

    // A cancellation we could not make effective is the defect this whole file
    // is about: the customer would read "gekündigt" on their settings page and
    // keep paying. Nothing is written, and they are asked to try again — which
    // costs nobody anything, because a cancellation has no deadline.
    expect($instance->fresh()->status)->toBe('active')
        ->and($instance->fresh()->service_ends_at)->toBeNull()
        ->and($contract->fresh()->cancel_requested_at)->toBeNull();
});

/**
 * A contract that has ended, and a Stripe that has not noticed.
 */
function endedContract(string $endedAt = '2026-04-01 09:00:00'): Subscription
{
    $customer = Customer::factory()->create(['name' => 'Berger GmbH', 'email' => 'kundin@example.test']);

    return Subscription::factory()->plan('team')->create([
        'customer_id' => $customer->id,
        'stripe_subscription_id' => 'sub_ended',
        'current_period_start' => '2026-03-01 09:00:00',
        'current_period_end' => $endedAt,
        'status' => 'cancelled',
        'cancelled_at' => $endedAt,
    ]);
}

/** The event Stripe raises when a term rolls over and is paid. */
function cyclePaid(string $invoiceId, string $periodStart, string $periodEnd): array
{
    return [
        'id' => 'evt_'.$invoiceId,
        'type' => 'invoice.paid',
        'data' => ['object' => [
            'id' => $invoiceId,
            'subscription' => 'sub_ended',
            'amount_paid' => 21480,
            'total' => 21480,
            'billing_reason' => 'subscription_cycle',
            'period_start' => Carbon::parse($periodStart)->timestamp,
            'period_end' => Carbon::parse($periodEnd)->timestamp,
        ]],
    ];
}

it('issues no invoice and uses no number for a term that begins after the contract ended', function () {
    Mail::fake();
    $contract = endedContract();

    // Stripe charged for April, and the contract ended on the first of April.
    // That is a month the customer does not have.
    $this->postJson(route('webhooks.stripe'), cyclePaid('in_after', '2026-04-01 09:00:00', '2026-05-01 09:00:00'))
        ->assertOk();

    // No document, and no mail carrying one.
    expect(Invoice::query()->count())->toBe(0);
    Mail::assertNothingQueued();

    // The money moved all the same, and the register is where an operator finds
    // the payments that have to go back — so it IS recorded. What must not
    // happen is a lawful invoice being written for a service that no longer
    // exists.
    expect(SubscriptionRecord::query()
        ->where('subscription_id', $contract->id)
        ->where('event', SubscriptionRecord::EVENT_RENEWAL)
        ->count())->toBe(1);

    // And the number was not merely unused, it was never drawn: the next
    // legitimate document is still the first of the year. A gapless series
    // cannot afford a number that went nowhere.
    $final = cyclePaid('in_final', '2026-03-01 09:00:00', '2026-04-01 09:00:00');
    $this->postJson(route('webhooks.stripe'), $final)->assertOk();

    expect(Invoice::query()->sole()->number)->toBe('RE-2026-0001');
});

it('still issues the final invoice for a term the customer really did use', function () {
    Mail::fake();
    $contract = endedContract();

    // The one case refusing everything would lose. The cycle invoice for March
    // went unpaid, Stripe's dunning ran, the contract ended on the first of
    // April in the middle of it — and then the customer paid. That money is for
    // a month they had a running cloud in, and they are owed the document for it.
    $this->travelTo('2026-04-05 11:00:00');

    $this->postJson(route('webhooks.stripe'), cyclePaid('in_march', '2026-03-01 09:00:00', '2026-04-01 09:00:00'))
        ->assertOk();

    $invoice = Invoice::query()->sole();

    expect($invoice->stripe_invoice_id)->toBe('in_march')
        ->and($invoice->subscription_id)->toBe($contract->id)
        ->and($invoice->number)->toBe('RE-2026-0001')
        // Issued AND sent: the customer gets the paper, not just a row.
        ->and($invoice->sent_at)->not->toBeNull();

    Mail::assertQueued(InvoiceMail::class);
});
