<?php

use App\Models\InvoiceSeries;
use App\Services\Billing\InvoiceNumbers;
use Illuminate\Support\Facades\DB;

/**
 * Gapless and ascending, which is a legal requirement rather than a preference.
 *
 * All of this exists because the obvious implementation — "the highest number
 * that exists, plus one" — hands the same number to two sales that land
 * together, and hands a used number back out after a row is removed. A number
 * that has already been printed on a document somebody holds.
 */
it('hands out the seeded series in order', function () {
    $s = InvoiceSeries::query()->where('kind', 'invoice')->firstOrFail();

    $numbers = [];

    foreach (range(1, 3) as $ignored) {
        DB::transaction(function () use ($s, &$numbers) {
            [$number] = app(InvoiceNumbers::class)->next($s, 2026);
            $numbers[] = $number;
        });
    }

    expect($numbers)->toBe(['RE-2026-0001', 'RE-2026-0002', 'RE-2026-0003']);
});

it('restarts a yearly series in a new year, and only then', function () {
    // Checked against the stored year, not against "is it January": an
    // installation that issued nothing for fourteen months must still restart,
    // and one issuing at 23:59 on New Year's Eve must not restart the invoice
    // that follows a minute later under the old year.
    $s = InvoiceSeries::query()->where('kind', 'invoice')->firstOrFail();

    DB::transaction(fn () => app(InvoiceNumbers::class)->next($s, 2026));
    DB::transaction(fn () => app(InvoiceNumbers::class)->next($s, 2026));

    [$next] = DB::transaction(fn () => app(InvoiceNumbers::class)->next($s->refresh(), 2027));

    expect($next)->toBe('RE-2027-0001');

    [$again] = DB::transaction(fn () => app(InvoiceNumbers::class)->next($s->refresh(), 2027));

    expect($again)->toBe('RE-2027-0002');
});

it('never restarts a series that does not reset yearly', function () {
    $s = InvoiceSeries::query()->where('kind', 'invoice')->firstOrFail();
    $s->update(['yearly_reset' => false, 'next_number' => 41]);

    [$number] = DB::transaction(fn () => app(InvoiceNumbers::class)->next($s->refresh(), 2026));
    expect($number)->toBe('RE-0041');

    [$across] = DB::transaction(fn () => app(InvoiceNumbers::class)->next($s->refresh(), 2027));
    expect($across)->toBe('RE-0042');
});

it('keeps every Rechnungskreis on its own counter', function () {
    // A credit note is not an invoice with a minus sign in front of it, and
    // sharing a counter would put gaps in both.
    $invoice = InvoiceSeries::query()->where('kind', 'invoice')->firstOrFail();
    $credit = InvoiceSeries::query()->where('kind', 'credit_note')->firstOrFail();

    [$a] = DB::transaction(fn () => app(InvoiceNumbers::class)->next($invoice, 2026));
    [$b] = DB::transaction(fn () => app(InvoiceNumbers::class)->next($credit, 2026));
    [$c] = DB::transaction(fn () => app(InvoiceNumbers::class)->next($invoice->refresh(), 2026));

    expect($a)->toBe('RE-2026-0001')
        ->and($b)->toBe('GS-2026-0001')
        ->and($c)->toBe('RE-2026-0002');
});

/*
 * There is deliberately no test for the "must be inside a transaction" guard.
 *
 * RefreshDatabase wraps every test in one, so DB::transactionLevel() is already
 * above zero before the first line of any test here runs — the guard cannot
 * fire, and a test written to prove it does would be green for a reason that
 * has nothing to do with the guard. A test that passes for the wrong reason is
 * worse than no test, so this note stands in its place.
 */

it('starts every seeded Rechnungskreis at one', function () {
    // An installation with no series cannot issue an invoice at all, and
    // finding that out at the moment of the first sale is the worst possible
    // time.
    //
    // toEqual, not toBe: pluck returns them in whatever order the database
    // hands them back, and the order of four seeded rows is not the assertion.
    expect(InvoiceSeries::query()->pluck('prefix', 'kind')->all())->toEqual([
        'invoice' => 'RE',
        'credit_note' => 'GS',
        'cancellation' => 'ST',
        'deposit' => 'AZ',
    ]);
});
