<?php

use App\Livewire\Admin\MailPreview;
use App\Models\Invoice;
use App\Models\Mailbox;
use App\Models\MaintenanceWindow;
use App\Models\Order;
use App\Services\Mail\MailPreviews;
use Illuminate\Support\Facades\Mail;
use Livewire\Livewire;

/**
 * Looking at a mail before a customer does.
 *
 * There was no way to: an invoice mail needs an invoice, a maintenance
 * announcement needs a window, and "register an account to see whether the
 * confirmation reads well on a phone" is not a workflow. Which is how every mail
 * went out with a hard 600px table until somebody opened one on a telephone.
 */
it('renders every mail this installation sends', function () {
    // The whole point of the list: a mail that cannot be rendered is one nobody
    // finds out about until the event that sends it happens for real.
    $previews = app(MailPreviews::class);

    expect($previews->all())->not->toBeEmpty();

    foreach (array_keys($previews->all()) as $key) {
        $html = $previews->make($key)?->render();

        expect($html)->toBeString()
            ->and(strlen((string) $html))->toBeGreaterThan(500);
    }
});

it('leaves nothing behind in the database', function () {
    // make(), never create(). A preview must not leave an invoice, an order or a
    // maintenance window lying about — least of all one carrying a number drawn
    // from a series.
    $before = [
        'invoices' => Invoice::query()->count(),
        'orders' => Order::query()->count(),
        'windows' => MaintenanceWindow::query()->count(),
    ];

    foreach (array_keys(app(MailPreviews::class)->all()) as $key) {
        app(MailPreviews::class)->make($key)?->render();
    }

    expect(Invoice::query()->count())->toBe($before['invoices'])
        ->and(Order::query()->count())->toBe($before['orders'])
        ->and(MaintenanceWindow::query()->count())->toBe($before['windows']);
});

it('answers nothing for a key nobody knows, rather than throwing', function () {
    // Reached from a URL, and a URL is a string somebody can retype.
    expect(app(MailPreviews::class)->make('gibt-es-nicht'))->toBeNull();
});

it('sends a preview to the operator own address and to nobody else', function () {
    Mail::fake();

    $operator = operator('Owner');

    Livewire::actingAs($operator, 'operator')
        ->test(MailPreview::class)
        ->call('sendToMe', 'verify-email')
        ->assertDispatched('notify');

    // SENT, not queued. Every mailable here implements ShouldQueue, so send()
    // would have deferred to the queue — putting a failure in a worker's log
    // while the button reported success to somebody who is waiting for the mail.
    Mail::assertSent(App\Mail\VerifyEmailMail::class, fn ($mail) => $mail->hasTo($operator->email));
    Mail::assertNotQueued(App\Mail\VerifyEmailMail::class);
});

it('sends each preview through the mailer the mail itself asks for', function () {
    // "553 5.7.1 Sender address rejected: not owned by user no-reply@…".
    //
    // Every purpose mailbox has its OWN SMTP account, and a mail server lets an
    // account send only from the address it owns. Mail::to() hands back a pending
    // mail bound to the DEFAULT mailer and sendNow() then ignores the mailer the
    // mailable asked for — so a support mail addressed support@ went out over the
    // no-reply@ login and was rejected. The mailer is not a detail here; it IS
    // which account authenticates.
    Mail::fake();

    Livewire::actingAs(operator('Owner'), 'operator')
        ->test(MailPreview::class)
        ->call('sendToMe', 'operator-message');

    // Der Mailer der MAILART, nicht der des Zwecks: seit die Wegwahl auch das
    // Konto verschiebt, hat jede Mailart im Katalog einen eigenen. Ueber
    // MailCatalogue gefragt, damit diese Zusicherung nicht von der einen Liste
    // abdriften kann, die sie benennt.
    Mail::assertSent(
        App\Mail\OperatorMessageMail::class,
        fn ($mail) => $mail->mailer === App\Services\Mail\MailCatalogue::mailer('operator-message'),
    );
});

it('gives every preview a sender its own mailbox owns', function () {
    // The invariant behind the 553: From has to be the address of the mailbox
    // whose account signs in. A preview that takes the framework default
    // (MAIL_FROM_ADDRESS, which no mailbox owns) is rejected by any properly
    // configured server — which is exactly how the cloud-ready preview failed
    // while all nine rendered perfectly in the browser.
    // updateOrCreate, because the installation already seeds mailboxes: this
    // test cares about the mapping and the addresses, not about who created the
    // rows.
    foreach (App\Services\Mail\MailPurpose::ALL as $purpose) {
        Mailbox::query()->updateOrCreate(['key' => $purpose], [
            'address' => $purpose.'@clupilot.test',
            'password' => 'test-passwort',
            'authenticates' => true,
            'active' => true,
        ]);
        App\Support\Settings::set(App\Services\Mail\MailPurpose::settingKey($purpose), $purpose);
    }

    foreach (array_keys(app(MailPreviews::class)->all()) as $key) {
        $mailable = app(MailPreviews::class)->make($key);

        // Which mailer it will go out over, and therefore which account signs
        // in. Read out of the mailer's own configuration rather than parsed out
        // of its NAME: since the routing fix each catalogued mail type has a
        // mailer of its own (cp_mail_<mailart>), and a test that peels 'cp_'
        // off the front would be asserting the naming scheme instead of the
        // invariant.
        expect($mailable->mailer)->toBeString();

        $mailer = (array) config('mail.mailers.'.$mailable->mailer);

        expect($mailer['transport'] ?? null)->toBe('mailbox', "[{$key}] geht nicht ueber ein Postfach");

        $mailable->assertFrom($mailer['purpose'].'@clupilot.test');
    }
});

it('has no recipient field, because that would be a form for mailing strangers', function () {
    $component = Illuminate\Support\Facades\File::get(app_path('Livewire/Admin/MailPreview.php'));

    // The address comes from the signed-in operator, never from the request.
    expect($component)->toContain("Auth::guard('operator')->user()")
        ->and($component)->not->toContain('public string $to');
});

it('serves the rendered mail as the document itself', function () {
    // Not escaped into a console page: a mail is a whole document with its own
    // background, and judging it inside a frame judges the frame.
    $this->actingAs(operator('Owner'), 'operator')
        ->get(route('admin.mail.preview.show', 'verify-email'))
        ->assertOk()
        ->assertSee('max-width:600px', false);
});

it('keeps the preview to operators who may manage mail', function () {
    Livewire::actingAs(operator('Read-only'), 'operator')
        ->test(MailPreview::class)
        ->assertForbidden();

    $this->actingAs(operator('Read-only'), 'operator')
        ->get(route('admin.mail.preview.show', 'verify-email'))
        ->assertForbidden();
});

it('answers 404 for a preview that does not exist', function () {
    $this->actingAs(operator('Owner'), 'operator')
        ->get(route('admin.mail.preview.show', 'gibt-es-nicht'))
        ->assertNotFound();
});

// ---- What made this necessary ----

it('builds the mail layout fluid, so a phone does not scroll sideways', function () {
    // width="600" plus width:600px forced every phone to scroll to read a
    // sentence. A mail cannot rely on a media query — Outlook renders with Word
    // and drops <style> blocks — so the layout has to shrink by itself.
    $html = (string) app(MailPreviews::class)->make('verify-email')?->render();

    expect($html)->toContain('width:100%;max-width:600px')
        ->and($html)->not->toContain('width="600"')
        // No fixed width anywhere, and max-width does not count as one.
        ->and(preg_match('/style="[^"]*(?<!max-)width:600px/', $html))->toBe(0);
});

it('does not spend a third of a phone screen on side padding', function () {
    // 40px each side leaves 278px of a 390px screen. 24px leaves 310px, and on
    // the desktop 552px of a 600px canvas still reads well.
    foreach (array_keys(app(MailPreviews::class)->all()) as $key) {
        $html = (string) app(MailPreviews::class)->make($key)?->render();

        expect(preg_match('/padding:[^";]*\b40px\b[^";]*40px/', $html))->toBe(0, $key);
    }
});
