<?php

use App\Mail\InvoiceMail;
use App\Models\Customer;
use App\Models\Invoice;
use App\Models\InvoiceSeries;
use App\Models\Subscription;
use App\Models\SubscriptionRecord;
use App\Support\CompanyProfile;
use App\Support\Settings;
use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\Mail;
use Illuminate\Support\Facades\Queue;

/**
 * The invoice a renewal owes, which for a long time nobody sent.
 *
 * InvoiceMail went out from exactly one place — the first purchase — so a
 * customer paying every month for a year received one invoice, for month one.
 * Stripe already says when the money landed: `invoice.paid` with
 * `billing_reason: subscription_cycle`. What was missing was the document.
 */
beforeEach(function () {
    // Fixed, because the document states a service period and a number that
    // carries the year, and both are the point of these tests.
    Carbon::setTestNow('2026-08-01 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 contract Stripe bills, belonging to somebody with an address to write to. */
function renewingContract(string $stripeId = 'sub_ren'): Subscription
{
    $customer = Customer::factory()->create([
        'name' => 'Berger GmbH',
        'email' => 'kundin@example.com',
    ]);

    return Subscription::factory()->plan('team')->create([
        'customer_id' => $customer->id,
        'stripe_subscription_id' => $stripeId,
        'current_period_start' => now()->subMonth(),
        'current_period_end' => now(),
    ]);
}

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

it('issues exactly one invoice for a renewal and sends exactly one mail', function () {
    Mail::fake();
    $contract = renewingContract();

    $this->postJson(route('webhooks.stripe'), renewalPaid())->assertOk();

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

    expect($invoice->stripe_invoice_id)->toBe('in_ren')
        // The contract is what a renewal renews. There is no order behind it,
        // and inventing one would put a purchase nobody made in the shop's own
        // records.
        ->and($invoice->subscription_id)->toBe($contract->id)
        ->and($invoice->order_id)->toBeNull()
        // The contract's frozen NET price, not Stripe's gross total: what the
        // customer is owed is what they agreed to.
        ->and($invoice->net_cents)->toBe(17900)
        ->and($invoice->tax_cents)->toBe(3580)
        ->and($invoice->number)->toBe('RE-2026-0001')
        ->and($invoice->sent_at)->not->toBeNull();

    // One line, saying which term it covers. Stripe's period_end is the first
    // moment of the NEXT term, so printing it would put the same day on two
    // consecutive invoices.
    expect($invoice->snapshot['lines'])->toHaveCount(1)
        ->and($invoice->snapshot['lines'][0]['details'][0])
        ->toBe(__('invoice.line_period', ['from' => '01.08.2026', 'to' => '31.08.2026']));

    Mail::assertQueuedCount(1);
    Mail::assertQueued(InvoiceMail::class, fn (InvoiceMail $mail) => $mail->hasTo('kundin@example.com')
        && $mail->invoice->is($invoice));

    // And the payment is still in the register, where it always was.
    expect(SubscriptionRecord::query()->where('event', SubscriptionRecord::EVENT_RENEWAL)->count())->toBe(1);
});

it('issues one document and consumes one number however often Stripe delivers the invoice', function () {
    Mail::fake();
    renewingContract();

    $this->postJson(route('webhooks.stripe'), renewalPaid())->assertOk();
    $this->postJson(route('webhooks.stripe'), renewalPaid())->assertOk();

    // An invoice number comes out of a gapless series and can never be handed
    // out twice — so the second delivery must not take one, not even one it
    // then throws away.
    expect(Invoice::query()->count())->toBe(1)
        ->and(InvoiceSeries::query()->where('kind', 'invoice')->value('next_number'))->toBe(2);

    Mail::assertQueuedCount(1);
});

it('does not fail the webhook when the invoice cannot be issued, and keeps the payment', function () {
    Mail::fake();
    renewingContract();

    // An invoice without a VAT number is not a valid invoice here, so issuing
    // refuses — which is correct, and must not cost Stripe its 2xx.
    Settings::forget('company.vat_id');

    $this->postJson(route('webhooks.stripe'), renewalPaid())->assertOk();

    expect(Invoice::query()->count())->toBe(0)
        // A retry would re-enter this and fail again; the money is what must
        // not be lost.
        ->and(SubscriptionRecord::query()->where('event', SubscriptionRecord::EVENT_RENEWAL)->count())->toBe(1)
        ->and(Subscription::query()->sole()->current_period_end->timestamp)->toBe(now()->addMonth()->timestamp);

    Mail::assertNothingQueued();
});

it('does not fail the webhook when the mail cannot be sent, and leaves the invoice unsent', function () {
    renewingContract();

    // A mail server that is down, mid-send.
    Mail::shouldReceive('mailer')->andThrow(new RuntimeException('Connection refused'));

    $this->postJson(route('webhooks.stripe'), renewalPaid())->assertOk();

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

    // The document is issued and the payment recorded; only the sending failed,
    // and it says so rather than claiming to have gone out.
    expect($invoice->sent_at)->toBeNull()
        ->and(SubscriptionRecord::query()->where('event', SubscriptionRecord::EVENT_RENEWAL)->count())->toBe(1);
});

it('sends the invoice a second delivery finds unsent, without issuing a second document', function () {
    renewingContract();

    $mailer = Mail::getFacadeRoot();
    Mail::shouldReceive('mailer')->once()->andThrow(new RuntimeException('Connection refused'));
    $this->postJson(route('webhooks.stripe'), renewalPaid())->assertOk();

    // Stripe redelivers, and the mail server is back. The register entry is
    // already there, so tying the paperwork to it would mean this customer
    // never got their invoice.
    Mail::swap($mailer);
    Mail::fake();
    $this->postJson(route('webhooks.stripe'), renewalPaid())->assertOk();

    expect(Invoice::query()->count())->toBe(1)
        ->and(Invoice::query()->sole()->sent_at)->not->toBeNull();

    Mail::assertQueuedCount(1);
});

it('invoices nothing for a payment that arrives with nothing to describe it', function () {
    Mail::fake();
    renewingContract();

    $this->postJson(route('webhooks.stripe'), [
        'id' => 'evt_prorata', 'type' => 'invoice.paid',
        'data' => ['object' => [
            'id' => 'in_prorata', 'subscription' => 'sub_ren', 'amount_paid' => 4200,
            'billing_reason' => 'subscription_update',
            'period_start' => now()->timestamp, 'period_end' => now()->addMonth()->timestamp,
        ]],
    ])->assertOk();

    // A proration DOES get a document now — StripeAddonBillingTest walks one —
    // and it is built from Stripe's own lines, because Stripe worked the amount
    // out against ITS boundaries and its own view of the account. This invoice
    // carries no lines at all, so there is nothing honest to put on a document:
    // the contract's own price describes a whole term, which is exactly what
    // this charge is not. The money is in the register either way, which is the
    // trail for whoever has to raise the paper.
    expect(Invoice::query()->count())->toBe(0)
        ->and(SubscriptionRecord::query()->where('event', SubscriptionRecord::EVENT_INVOICE_PAID)->sole()->gross_cents)
        ->toBe(4200);

    Mail::assertNothingQueued();
});

it('still gives the first purchase exactly one invoice and one mail', function () {
    Mail::fake();
    Queue::fake();

    $this->postJson(route('webhooks.stripe'), [
        'id' => 'evt_checkout', 'type' => 'checkout.session.completed',
        'data' => ['object' => [
            'id' => 'cs_first', 'payment_status' => 'paid', 'subscription' => 'sub_first',
            'customer_details' => ['email' => 'neu@example.com', 'name' => 'Neu GmbH'],
            'amount_total' => 21480, 'currency' => 'eur',
            'metadata' => ['plan' => 'team', 'datacenter' => 'fsn'],
        ]],
    ])->assertOk();

    // Stripe then sends the checkout's OWN invoice. It is not a renewal, and
    // invoicing it here would send the same customer a second document for one
    // purchase in the same minute.
    $this->postJson(route('webhooks.stripe'), [
        'id' => 'evt_first_invoice', 'type' => 'invoice.paid',
        'data' => ['object' => [
            'id' => 'in_first', 'subscription' => 'sub_first', 'amount_paid' => 21480,
            'billing_reason' => 'subscription_create',
            'period_start' => now()->timestamp, 'period_end' => now()->addMonth()->timestamp,
        ]],
    ])->assertOk();

    expect(Invoice::query()->count())->toBe(1)
        ->and(Invoice::query()->sole()->order_id)->not->toBeNull()
        ->and(Invoice::query()->sole()->stripe_invoice_id)->toBeNull();

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