<?php

use App\Livewire\Admin\NewInvoice;
use App\Models\Customer;
use App\Models\Invoice;
use App\Models\InvoiceSeries;
use App\Services\Billing\IssueInvoice;
use App\Support\CompanyProfile;
use Livewire\Livewire;

/**
 * An invoice for work that is on no price list.
 *
 * Somebody asks whether their data can be moved into Nextcloud; we look at it,
 * we say what it costs, we do it. That is a real invoice with no order and no
 * contract behind it — and writing it somewhere else would put it outside the
 * series. A numbered series with a document missing from it is worth nothing at
 * an audit, which is why this goes through the same door as every other
 * invoice.
 */
beforeEach(function () {
    // An invoice without a registered name, address and VAT number is not a
    // valid invoice, and IssueInvoice refuses to draw a number for one.
    CompanyProfile::put([
        'name' => 'CluPilot GmbH',
        'address' => 'Musterstraße 1',
        'postcode' => '1010',
        'city' => 'Wien',
        'vat_id' => 'ATU12345678',
    ]);

    InvoiceSeries::query()->firstOrCreate(
        ['kind' => 'invoice'],
        ['name' => 'Rechnungen', 'prefix' => 'RE', 'active' => true],
    );
});

it('issues a service invoice out of the same consecutive series as every other', function () {
    // The whole point. A second way of making invoices would be a second
    // numbering, and two numberings are two series with holes in them.
    $customer = Customer::factory()->create();

    $first = app(IssueInvoice::class)->forService($customer, [[
        'description' => 'Datenübernahme aus bestehendem System',
        'quantity_milli' => 4000,
        'unit' => 'Std.',
        'unit_net_cents' => 12000,
    ]]);

    $second = app(IssueInvoice::class)->forService($customer, [[
        'description' => 'Beratung',
        'quantity_milli' => 1000,
        'unit_net_cents' => 9000,
    ]]);

    expect($second->number_sequence)->toBe($first->number_sequence + 1)
        ->and($second->invoice_series_id)->toBe($first->invoice_series_id);
});

it('bills quantity times price, with the VAT the customer would be charged anyway', function () {
    $customer = Customer::factory()->create();

    $invoice = app(IssueInvoice::class)->forService($customer, [[
        'description' => 'Migration',
        'quantity_milli' => 4000,           // four hours
        'unit' => 'Std.',
        'unit_net_cents' => 12000,          // 120,00 € each
    ]]);

    expect($invoice->net_cents)->toBe(48000)
        ->and($invoice->tax_cents)->toBe(9600)      // 20 %
        ->and($invoice->gross_cents)->toBe(57600);
});

it('applies reverse charge to a verified EU business, like any other invoice', function () {
    // Not a special case for service work — the treatment is the customer's,
    // and this document goes through the same TaxTreatment as the rest.
    // Verified means the VALUE was checked, not that a flag was set: editing
    // the field must not inherit an old confirmation.
    $customer = Customer::factory()->create([
        'vat_id' => 'DE123456789',
        'vat_id_verified_at' => now(),
        'vat_id_verified_value' => 'DE123456789',
    ]);

    $invoice = app(IssueInvoice::class)->forService($customer, [[
        'description' => 'Migration',
        'quantity_milli' => 1000,
        'unit_net_cents' => 50000,
    ]]);

    expect($invoice->tax_cents)->toBe(0)
        ->and($invoice->gross_cents)->toBe(50000)
        // The note that makes a zero-rated invoice lawful — without it the
        // document says nothing about WHY no VAT was charged, which is the one
        // thing an auditor looks for.
        ->and($invoice->snapshot['meta']['closing'])->toBe(__('invoice.reverse_charge'));
});

it('freezes what the document says, so it still says it when the settings change', function () {
    $customer = Customer::factory()->create(['name' => 'Kanzlei Berger']);

    $invoice = app(IssueInvoice::class)->forService($customer, [[
        'description' => 'Datenübernahme',
        'quantity_milli' => 1000,
        'unit_net_cents' => 30000,
    ]]);

    CompanyProfile::put(['name' => 'Ein ganz anderer Name']);
    $customer->update(['name' => 'Auch anders']);

    $snapshot = $invoice->fresh()->snapshot;

    expect($snapshot['issuer']['name'])->toBe('CluPilot GmbH')
        ->and($snapshot['customer']['name'])->toBe('Kanzlei Berger')
        ->and($snapshot['lines'][0]['description'])->toBe('Datenübernahme');
});

it('refuses to draw a number for an invoice with nothing on it', function () {
    // A number taken and thrown away is a gap in a series that must not have
    // one.
    $customer = Customer::factory()->create();

    expect(fn () => app(IssueInvoice::class)->forService($customer, []))
        ->toThrow(RuntimeException::class);
});

// ---- The page an operator actually uses ----

it('writes the invoice the operator typed', function () {
    $customer = Customer::factory()->create();

    Livewire::actingAs(operator('Owner'), 'operator')
        ->test(NewInvoice::class)
        ->set('customerUuid', $customer->uuid)
        ->set('lines.0.description', 'Datenübernahme aus ownCloud')
        ->set('lines.0.detail', '42 GB, inkl. Prüfprotokoll')
        ->set('lines.0.quantity', '4')
        ->set('lines.0.unit', 'Std.')
        ->set('lines.0.price', '120')
        ->call('issue')
        ->assertRedirect(route('admin.invoices'));

    $invoice = Invoice::query()->latest('id')->first();

    expect($invoice)->not->toBeNull()
        ->and($invoice->customer_id)->toBe($customer->id)
        ->and($invoice->net_cents)->toBe(48000)
        ->and($invoice->snapshot['lines'][0]['description'])->toBe('Datenübernahme aus ownCloud')
        ->and($invoice->snapshot['lines'][0]['details'][0])->toBe('42 GB, inkl. Prüfprotokoll');
});

it('reads a price in euro the way a person types it', function () {
    // The form is in euro and the document is in cents. (int) (12.95 * 100) is
    // 1294 on a binary float, and an invoice a cent short of what was typed is
    // the kind of thing nobody finds until a customer does.
    $customer = Customer::factory()->create();

    Livewire::actingAs(operator('Owner'), 'operator')
        ->test(NewInvoice::class)
        ->set('customerUuid', $customer->uuid)
        ->set('lines.0.description', 'Beratung')
        ->set('lines.0.quantity', '1')
        ->set('lines.0.price', '12.95')
        ->call('issue');

    expect(Invoice::query()->latest('id')->first()->net_cents)->toBe(1295);
});

it('bills a quarter of an hour without losing it', function () {
    // Quantities are held to a thousandth because hours are billed in quarters.
    $customer = Customer::factory()->create();

    Livewire::actingAs(operator('Owner'), 'operator')
        ->test(NewInvoice::class)
        ->set('customerUuid', $customer->uuid)
        ->set('lines.0.description', 'Rückfrage')
        ->set('lines.0.quantity', '0.25')
        ->set('lines.0.price', '120')
        ->call('issue');

    expect(Invoice::query()->latest('id')->first()->net_cents)->toBe(3000);
});

it('adds up several lines the way the finished document will', function () {
    $customer = Customer::factory()->create();

    $page = Livewire::actingAs(operator('Owner'), 'operator')
        ->test(NewInvoice::class)
        ->set('customerUuid', $customer->uuid)
        ->set('lines.0.description', 'Migration')
        ->set('lines.0.quantity', '4')
        ->set('lines.0.price', '120')
        ->call('addLine')
        ->set('lines.1.description', 'Nacharbeit')
        ->set('lines.1.quantity', '2')
        ->set('lines.1.price', '90');

    // The preview is the same arithmetic the invoice uses, not a second one.
    $page->assertViewHas('totals', fn (array $t) => $t['net'] === 66000 && $t['gross'] === 79200);

    $page->call('issue');

    expect(Invoice::query()->latest('id')->first()->gross_cents)->toBe(79200);
});

it('refuses a line with no work on it and no price', function () {
    $customer = Customer::factory()->create();

    Livewire::actingAs(operator('Owner'), 'operator')
        ->test(NewInvoice::class)
        ->set('customerUuid', $customer->uuid)
        ->call('issue')
        ->assertHasErrors(['lines.0.description', 'lines.0.price']);

    expect(Invoice::query()->count())->toBe(0);
});

it('never removes the last line, so the form cannot end up with nothing to issue', function () {
    Livewire::actingAs(operator('Owner'), 'operator')
        ->test(NewInvoice::class)
        ->call('removeLine', 0)
        ->assertCount('lines', 1);
});

it('keeps the page to operators who may touch money', function () {
    Livewire::actingAs(operator('Support'), 'operator')
        ->test(NewInvoice::class)
        ->assertForbidden();
});

it('says what is missing before the form is filled in, not after', function () {
    // The company profile is another page. Finding out at the last click costs
    // the whole form.
    CompanyProfile::put(['vat_id' => '']);

    Livewire::actingAs(operator('Owner'), 'operator')
        ->test(NewInvoice::class)
        ->assertViewHas('missing', fn (array $missing) => in_array('vat_id', $missing, true))
        ->assertSee(__('new_invoice.to_finance'));
});

it('arrives with the recipient already chosen when a link says who', function () {
    // From a customer's own page, or from the support request that asked for
    // the work: the operator should not have to find the name again in a
    // dropdown they just came from.
    $customer = Customer::factory()->create();

    Livewire::withQueryParams(['customer' => $customer->uuid])
        ->actingAs(operator('Owner'), 'operator')
        ->test(NewInvoice::class)
        ->assertSet('customerUuid', $customer->uuid)
        ->assertViewHas('customer', fn ($c) => $c?->id === $customer->id);
});
