<?php

use App\Livewire\Admin\CustomerDetail;
use App\Livewire\Admin\EditMailTemplate;
use App\Livewire\Admin\MailTemplates;
use App\Models\Customer;
use App\Models\Instance;
use App\Models\MailTemplate;
use App\Models\SentMail;
use App\Models\Subscription;
use App\Services\Mail\MailTemplateRenderer;
use App\Support\CompanyProfile;
use Illuminate\Support\Facades\File;
use Livewire\Livewire;

/**
 * The answers an operator gives over and over, written once.
 *
 * A template is INSERTED and then edited, never sent as it stands. So the thing
 * worth testing is not "did a mail go out" but "did the right details end up in
 * the field the operator is about to correct".
 */
beforeEach(fn () => CompanyProfile::put(['name' => 'CluPilot GmbH']));

it('fills a template with the customer own details', function () {
    $customer = Customer::factory()->create([
        'name' => 'Beispiel GmbH',
        'contact_name' => 'Bea Berger',
        'email' => 'bea@beispiel.test',
    ]);
    Subscription::factory()->plan('team')->create([
        'customer_id' => $customer->id,
        'status' => 'active',
    ]);

    $template = MailTemplate::create([
        'name' => 'Datenübernahme',
        'subject' => 'Ihre Anfrage — {{customer}}',
        'body' => "Guten Tag {{contact}},\n\nzu Ihrem Paket {{plan}} für {{amount}}:\n\n{{operator}}\n{{company}}",
    ]);

    $filled = app(MailTemplateRenderer::class)->render($template, $customer, operator('Owner'));

    expect($filled['subject'])->toBe('Ihre Anfrage — Beispiel GmbH')
        ->and($filled['body'])->toContain('Guten Tag Bea Berger,')
        ->and($filled['body'])->toContain(__('billing.plan.team'))
        ->and($filled['body'])->toContain('CluPilot GmbH');
});

it('writes to the person where one is on record, and to the company otherwise', function () {
    // "Guten Tag Beispiel GmbH" is how a mail announces that it came out of a
    // database.
    $withContact = Customer::factory()->create(['name' => 'Beispiel GmbH', 'contact_name' => 'Bea Berger']);
    $without = Customer::factory()->create(['name' => 'Zweite GmbH', 'contact_name' => null]);

    $renderer = app(MailTemplateRenderer::class);

    expect($renderer->values($withContact)['contact'])->toBe('Bea Berger')
        ->and($renderer->values($without)['contact'])->toBe('Zweite GmbH');
});

it('quotes the amount the customer knows, which is the gross one', function () {
    // What the price sheet quoted them and what their bank statement says.
    $customer = Customer::factory()->create();
    Subscription::factory()->plan('start')->create([
        'customer_id' => $customer->id,
        'status' => 'active',
    ]);

    $amount = app(MailTemplateRenderer::class)->values($customer)['amount'];

    // 39 € net at 20 % → 46,80 €.
    expect($amount)->toContain('46,80');
});

it('leaves a placeholder empty rather than inventing a package nobody bought', function () {
    // A guess in a mail to a paying customer is worse than a gap the operator
    // can see and fill.
    $customer = Customer::factory()->create();

    $values = app(MailTemplateRenderer::class)->values($customer);

    expect($values['plan'])->toBe('')
        ->and($values['amount'])->toBe('')
        ->and($values['instance'])->toBe('');
});

it('leaves a typo standing so it is seen before anything is sent', function () {
    // Replacing it with nothing produces "Guten Tag ," — a mail that looks sent
    // by a machine, which is what templates exist to avoid.
    $customer = Customer::factory()->create(['name' => 'Beispiel GmbH']);
    $template = MailTemplate::create([
        'name' => 'Mit Tippfehler',
        'subject' => 'Test',
        'body' => 'Guten Tag {{kunde}}, für {{customer}}.',
    ]);

    $body = app(MailTemplateRenderer::class)->render($template, $customer)['body'];

    expect($body)->toContain('{{kunde}}')
        ->and($body)->toContain('Beispiel GmbH');
});

it('tolerates a space inside the braces, because somebody will type one', function () {
    $customer = Customer::factory()->create(['name' => 'Beispiel GmbH']);
    $template = MailTemplate::create(['name' => 'X', 'subject' => '{{ customer }}', 'body' => '{{  CUSTOMER  }}']);

    $filled = app(MailTemplateRenderer::class)->render($template, $customer);

    expect($filled['subject'])->toBe('Beispiel GmbH')
        ->and($filled['body'])->toBe('Beispiel GmbH');
});

it('names the address of the cloud, where there is one', function () {
    $customer = Customer::factory()->create();
    Instance::factory()->create([
        'customer_id' => $customer->id,
        'subdomain' => 'kanzlei-berger',
        'status' => 'active',
    ]);

    expect(app(MailTemplateRenderer::class)->values($customer)['instance'])->toBe('kanzlei-berger');
});

// ---- Managing them ----

it('creates a template and offers it when writing', function () {
    // Counted against what is already there: the installation ships a set, so an
    // absolute count would only be testing the size of that set.
    $before = MailTemplate::query()->count();

    Livewire::actingAs(operator('Owner'), 'operator')
        ->test(MailTemplates::class)
        ->set('name', 'Eigene Antwort')
        ->set('subject', 'Ihre Anfrage')
        ->set('body', 'Guten Tag {{contact}},')
        ->call('create')
        ->assertHasNoErrors();

    expect(MailTemplate::query()->count())->toBe($before + 1)
        ->and(MailTemplate::query()->usable()->pluck('name'))->toContain('Eigene Antwort');
});

it('retires a template instead of deleting it', function () {
    // An answer that is no longer given is still the answer somebody was given
    // last year, and the mail register refers to it.
    $template = MailTemplate::create(['name' => 'Alt', 'subject' => 'S', 'body' => 'B']);

    Livewire::actingAs(operator('Owner'), 'operator')
        ->test(MailTemplates::class)
        ->call('toggleActive', $template->uuid);

    expect($template->fresh()->active)->toBeFalse()
        // The row is still there — retired, not deleted.
        ->and(MailTemplate::query()->where('name', 'Alt')->exists())->toBeTrue()
        ->and(MailTemplate::query()->usable()->pluck('name'))->not->toContain('Alt');
});

it('edits a template in a modal, never in the row', function () {
    // R20: a five-line textarea growing inside a list row pushes every column
    // beside it and reads as a rendering fault.
    $template = MailTemplate::create(['name' => 'Alt', 'subject' => 'S', 'body' => 'B']);

    Livewire::actingAs(operator('Owner'), 'operator')
        ->test(EditMailTemplate::class, ['uuid' => $template->uuid])
        ->set('name', 'Neu')
        ->set('body', 'Guten Tag {{contact}},')
        ->call('save');

    expect($template->fresh()->name)->toBe('Neu')
        ->and($template->fresh()->body)->toContain('{{contact}}');
});

it('keeps the templates to operators who may write to customers', function () {
    Livewire::actingAs(operator('Read-only'), 'operator')
        ->test(MailTemplates::class)
        ->assertForbidden();
});

// ---- Using one on the customer page ----

it('puts a template into the compose fields and opens that tab', function () {
    $customer = Customer::factory()->create(['name' => 'Beispiel GmbH', 'contact_name' => 'Bea Berger']);
    $template = MailTemplate::create([
        'name' => 'Datenübernahme',
        'subject' => 'Ihre Anfrage — {{customer}}',
        'body' => 'Guten Tag {{contact}},',
    ]);

    $page = Livewire::actingAs(operator('Owner'), 'operator')
        ->test(CustomerDetail::class, ['uuid' => $customer->uuid])
        ->call('useTemplate', $template->uuid);

    expect($page->get('subject'))->toBe('Ihre Anfrage — Beispiel GmbH')
        ->and($page->get('body'))->toBe('Guten Tag Bea Berger,')
        // Where the operator now has to be to finish it.
        ->and($page->get('tab'))->toBe('compose');
});

it('does not overwrite a subject the operator already typed', function () {
    // A template that eats work is a template nobody uses twice.
    $customer = Customer::factory()->create();
    $template = MailTemplate::create(['name' => 'X', 'subject' => 'Aus der Vorlage', 'body' => 'Text']);

    $page = Livewire::actingAs(operator('Owner'), 'operator')
        ->test(CustomerDetail::class, ['uuid' => $customer->uuid])
        ->set('subject', 'Selbst geschrieben')
        ->call('useTemplate', $template->uuid);

    expect($page->get('subject'))->toBe('Selbst geschrieben')
        ->and($page->get('body'))->toBe('Text');
});

it('will not insert a retired template', function () {
    $customer = Customer::factory()->create();
    $template = MailTemplate::create(['name' => 'X', 'subject' => 'S', 'body' => 'B', 'active' => false]);

    $page = Livewire::actingAs(operator('Owner'), 'operator')
        ->test(CustomerDetail::class, ['uuid' => $customer->uuid])
        ->call('useTemplate', $template->uuid);

    expect($page->get('body'))->toBe('');
});

it('sends what the operator saw, not what the template said', function () {
    // The whole point of inserting rather than sending: the last two sentences
    // are always specific to the person asking.
    $customer = Customer::factory()->create(['name' => 'Beispiel GmbH']);
    $template = MailTemplate::create(['name' => 'X', 'subject' => 'S', 'body' => 'Aus der Vorlage.']);

    Livewire::actingAs(operator('Owner'), 'operator')
        ->test(CustomerDetail::class, ['uuid' => $customer->uuid])
        ->call('useTemplate', $template->uuid)
        ->set('body', 'Aus der Vorlage. Und mein eigener Satz.')
        ->call('send');

    expect(SentMail::query()->where('customer_id', $customer->id)->latest('id')->first()->body)
        ->toBe('Aus der Vorlage. Und mein eigener Satz.');
});

// ---- The set that ships with the installation ----

it('ships the answers this business actually needs', function () {
    // An empty list is a feature nobody starts using: writing the first template
    // costs the same as writing the mail, so the operator writes the mail.
    $names = MailTemplate::query()->usable()->pluck('name');

    expect($names->count())->toBeGreaterThanOrEqual(10)
        ->and($names)->toContain('Datenübernahme — Rückfrage')
        ->and($names)->toContain('Störung — behoben')
        ->and($names)->toContain('Kündigung bestätigen');
});

it('promises nothing in a shipped template that nobody agreed to', function () {
    // Every figure and every date is a gap, never a number: a seeded "innerhalb
    // von 24 Stunden" is a promise put in the operator's mouth, and a seeded
    // price is wrong for every customer but one.
    foreach (MailTemplate::query()->get() as $template) {
        expect($template->body)
            ->not->toMatch('/\b\d+\s*(Stunden|Werktagen|Tagen)\b/')
            ->not->toMatch('/\b\d+[,.]\d{2}\s*€/');
    }
});

it('renders every shipped template without leaving a placeholder behind', function () {
    // A token that the renderer does not know stays visible on purpose — so a
    // shipped template must not contain one, or every operator meets a typo we
    // wrote for them.
    $customer = Customer::factory()->create(['name' => 'Beispiel GmbH', 'contact_name' => 'Bea Berger']);
    Subscription::factory()->plan('start')->create(['customer_id' => $customer->id, 'status' => 'active']);
    Instance::factory()->create(['customer_id' => $customer->id, 'subdomain' => 'beispiel', 'status' => 'active']);

    $renderer = app(MailTemplateRenderer::class);

    foreach (MailTemplate::query()->get() as $template) {
        $filled = $renderer->render($template, $customer, operator('Owner'));

        expect($filled['subject'])->not->toContain('{{', $template->name)
            ->and($filled['body'])->not->toContain('{{', $template->name);
    }
});

it('stores a shipped body without the file own indentation in it', function () {
    // Heredoc keeps the indentation of the source file, and a mail that arrives
    // indented by twenty spaces looks like a mistake — because it is one.
    foreach (MailTemplate::query()->get() as $template) {
        expect($template->body)->not->toStartWith(' ')
            ->and($template->body)->not->toContain("\n    ");
    }
});

it('leaves an edited template alone when the seed runs again', function () {
    // firstOrCreate on the name. A migration that overwrites text somebody
    // worked on is the worst kind of surprise.
    $template = MailTemplate::query()->where('name', 'Anfrage bestätigen')->firstOrFail();
    $template->update(['body' => 'Meine eigene Fassung.']);

    MailTemplate::query()->firstOrCreate(
        ['name' => 'Anfrage bestätigen'],
        ['subject' => 'Anders', 'body' => 'Anders', 'sort' => 0, 'active' => true],
    );

    expect($template->fresh()->body)->toBe('Meine eigene Fassung.')
        ->and(MailTemplate::query()->where('name', 'Anfrage bestätigen')->count())->toBe(1);
});

// ---- Moving one, and seeing that it moved ----

it('swaps a template with its neighbour rather than nudging a number', function () {
    // It used to add or subtract a fixed amount, which is not the same thing:
    // with values ten apart that usually landed right, and sometimes landed on a
    // value another template already had — equal sort falls back to the name, so
    // a click could reorder something else, or visibly nothing at all, which is
    // indistinguishable from a click that never registered.
    MailTemplate::query()->delete();

    $first = MailTemplate::create(['name' => 'A', 'subject' => 'S', 'body' => 'B', 'sort' => 10]);
    $second = MailTemplate::create(['name' => 'B', 'subject' => 'S', 'body' => 'B', 'sort' => 20]);
    $third = MailTemplate::create(['name' => 'C', 'subject' => 'S', 'body' => 'B', 'sort' => 30]);

    $order = fn () => MailTemplate::query()->orderBy('sort')->orderBy('name')->pluck('name')->all();

    $page = Livewire::actingAs(operator('Owner'), 'operator')->test(MailTemplates::class);

    $page->call('move', $third->uuid, -1);
    expect($order())->toBe(['A', 'C', 'B']);

    $page->call('move', $third->uuid, -1);
    expect($order())->toBe(['C', 'A', 'B']);

    $page->call('move', $third->uuid, 1);
    expect($order())->toBe(['A', 'C', 'B']);
});

it('does nothing at the ends instead of pretending', function () {
    // Both are what an operator expects to see: the top one cannot go up. A
    // number that silently changed without the order changing was the bug.
    MailTemplate::query()->delete();

    $top = MailTemplate::create(['name' => 'A', 'subject' => 'S', 'body' => 'B', 'sort' => 10]);
    $bottom = MailTemplate::create(['name' => 'B', 'subject' => 'S', 'body' => 'B', 'sort' => 20]);

    $page = Livewire::actingAs(operator('Owner'), 'operator')->test(MailTemplates::class);
    $page->call('move', $top->uuid, -1);
    $page->call('move', $bottom->uuid, 1);

    expect(MailTemplate::query()->orderBy('sort')->pluck('name')->all())->toBe(['A', 'B'])
        ->and($top->fresh()->sort)->toBe(10)
        ->and($bottom->fresh()->sort)->toBe(20);
});

it('separates two templates that shared a sort value', function () {
    // Equal values put the order in the hands of the name, so a swap between
    // them has to leave them distinct or the next click does nothing.
    MailTemplate::query()->delete();

    $a = MailTemplate::create(['name' => 'A', 'subject' => 'S', 'body' => 'B', 'sort' => 5]);
    $b = MailTemplate::create(['name' => 'B', 'subject' => 'S', 'body' => 'B', 'sort' => 5]);

    Livewire::actingAs(operator('Owner'), 'operator')
        ->test(MailTemplates::class)
        ->call('move', $b->uuid, -1);

    expect($a->fresh()->sort)->not->toBe($b->fresh()->sort)
        ->and(MailTemplate::query()->orderBy('sort')->orderBy('name')->pluck('name')->all())->toBe(['B', 'A']);
});

it('says a click landed, on the button and at the top of the window', function () {
    // Reported as "man klickt und sieht nicht was passiert". Every action here is
    // a round trip; a round trip with no sign of itself reads as a click that
    // missed. The bar is the floor under every page, the per-button state says
    // WHICH thing is happening.
    $page = File::get(resource_path('views/livewire/admin/mail-templates.blade.php'));

    expect($page)->toContain('wire:loading.attr="disabled"')
        ->toContain('animate-spin')
        // Scoped to the one button, not to the whole page: every row disabling
        // itself because a neighbour is moving is its own kind of confusing.
        ->toContain('wire:target="move(');

    expect(File::get(resource_path('js/app.js')))
        ->toContain('BUSY_SHOW_AFTER_MS')
        // Released on failure too — the request somebody must not be left
        // waiting on is exactly the one that went wrong.
        ->toContain('respond(() => busy.stop())');
});
