<?php

use App\Livewire\Admin\Mail as MailPage;
use App\Livewire\EditMailbox;
use App\Models\Mailbox;
use App\Models\Operator;
use App\Services\Mail\MailCatalogue;
use App\Services\Mail\MailPurpose;
use App\Services\Mail\MailRoute;
use App\Support\Settings;
use Livewire\Livewire;

beforeEach(function () {
    config()->set('admin_access.secrets_key', 'base64:'.base64_encode(random_bytes(32)));

    // Blank slate, not the migration's seeded five: every other file in this
    // directory does the same (see tests/Pest.php). Two of the tests below
    // pin a mailbox to the key 'support' and another to 'no-reply' — both
    // collide with mailboxes.key's unique constraint against the seeded
    // baseline RefreshDatabase leaves in place. Clearing keeps this file
    // deterministic regardless of what the seed migration happens to contain.
    clearMailboxSeed();
});

it('is closed to an operator without mail.manage', function () {
    $user = Operator::factory()->role('Read-only')->create(['password' => 'password']);

    Livewire::actingAs($user, 'operator')->test(MailPage::class)->assertForbidden();
});

it('opens for an operator who has it', function () {
    $user = Operator::factory()->role('Owner')->create(['password' => 'password']);

    Livewire::actingAs($user, 'operator')->test(MailPage::class)->assertOk()->assertSet('usable', true);
});

it('grants mail.manage to Admin and Support as well as Owner', function () {
    // The migration hands the capability to three roles, not one. Nothing
    // above proves the other two actually received it — an Owner-only test
    // would stay green even if the migration's role loop silently lost a name.
    expect(Operator::factory()->role('Admin')->create(['password' => 'password'])->can('mail.manage'))->toBeTrue()
        ->and(Operator::factory()->role('Support')->create(['password' => 'password'])->can('mail.manage'))->toBeTrue();
});

it('is not opened by secrets.manage alone — that is the point of splitting them', function () {
    // Billing holds secrets-adjacent capabilities but has no business changing
    // the support sender, and vice versa. If this ever passes, the two
    // capabilities have quietly been merged again.
    $user = Operator::factory()->role('Billing')->create(['password' => 'password']);

    expect($user->can('mail.manage'))->toBeFalse();

    Livewire::actingAs($user, 'operator')->test(MailPage::class)->assertForbidden();
});

it('edits a mailbox in a modal, never in the row (R20)', function () {
    $box = Mailbox::factory()->create(['key' => 'support']);

    $blade = file_get_contents(resource_path('views/livewire/admin/mail.blade.php'));

    expect($blade)->not->toMatch('/<td[^>]*>(?:(?!<\/td>).)*<input/s')
        ->and($blade)->toContain('openModal');

    Livewire::actingAs(Operator::factory()->role('Owner')->create(['password' => 'password']), 'operator')
        ->test(EditMailbox::class, ['uuid' => $box->uuid])
        ->set('address', 'neu@clupilot.com')
        ->call('save');

    expect($box->fresh()->address)->toBe('neu@clupilot.com');
});

it('never hands a stored password back to the browser', function () {
    $box = Mailbox::factory()->create(['password' => 'streng-geheim']);

    Livewire::actingAs(Operator::factory()->role('Owner')->create(['password' => 'password']), 'operator')
        ->test(EditMailbox::class, ['uuid' => $box->uuid])
        ->assertSet('password', '')
        ->assertDontSee('streng-geheim');
});

it('keeps the old password when the field is left empty', function () {
    $box = Mailbox::factory()->create(['password' => 'bleibt']);

    Livewire::actingAs(Operator::factory()->role('Owner')->create(['password' => 'password']), 'operator')
        ->test(EditMailbox::class, ['uuid' => $box->uuid])
        ->set('address', 'neu@clupilot.com')
        ->call('save');

    expect($box->fresh()->password)->toBe('bleibt');
});

it('shows the SECRETS_KEY warning on the page', function () {
    config()->set('admin_access.secrets_key', '');

    Livewire::actingAs(Operator::factory()->role('Owner')->create(['password' => 'password']), 'operator')
        ->test(MailPage::class)
        ->assertOk()
        ->assertSet('usable', false)
        ->assertSee(__('mail_settings.no_key'));
});

it('shows the same SECRETS_KEY warning for a malformed key, not only a missing one', function () {
    // Codex R15#3, P2: Mail::render() sets $this->usable from the very same
    // SecretCipher::isUsable() EditMailbox and MailboxTester gate on. Before
    // the fix, a malformed (nonempty, wrong-length) key answered true here,
    // so the page claimed mailbox passwords could be stored when they could
    // not — the two tests below are what happened next.
    config()->set('admin_access.secrets_key', 'zu-kurz');

    Livewire::actingAs(Operator::factory()->role('Owner')->create(['password' => 'password']), 'operator')
        ->test(MailPage::class)
        ->assertOk()
        ->assertSet('usable', false)
        ->assertSee(__('mail_settings.no_key'));
});

it('fails the password save with a form error, not an exception, when SECRETS_KEY is missing', function () {
    // The test above only proves the PAGE says so. Nothing proved that typing
    // a password into the MODAL and saving it fails gracefully — the gap a
    // reviewer found: left unfixed, SecretCipher::encrypt() throws all the way
    // up, and with APP_DEBUG on, Laravel's debug page can put the just-typed
    // plaintext password in its request-payload inspector. All three
    // password-writing tests above run under this file's valid-key beforeEach,
    // so none of them would have caught it.
    $box = Mailbox::factory()->create(['key' => 'support']);
    $owner = Operator::factory()->role('Owner')->create(['password' => 'password']);

    // Confirmed BEFORE the key is cleared: this test is about the SECRETS_KEY
    // guard specifically, not about the password-confirmation gate above it —
    // conflating the two would leave it unclear which one actually produced
    // the error.
    $page = Livewire::actingAs($owner, 'operator')
        ->test(EditMailbox::class, ['uuid' => $box->uuid])
        ->set('confirmablePassword', 'password')
        ->call('confirmPassword');

    config()->set('admin_access.secrets_key', '');

    $page->set('password', 'neu-und-sicher')
        ->call('save')
        ->assertHasErrors('password');
});

it('fails the password save with a form error, not an exception, for a malformed SECRETS_KEY too', function () {
    // Codex R15#3, P2: the guard this mirrors (EditMailbox::save(), a few
    // lines up) is a pre-check on SecretCipher::isUsable() specifically to
    // stop an uncaught RuntimeException from Mailbox::setPasswordAttribute()
    // reaching the operator as Laravel's debug page — whose request-payload
    // inspector can show the very password just typed. Before the fix, that
    // guard did not fire here: isUsable() answered true for a malformed key,
    // and $box->password = $this->password went ahead and threw uncaught.
    $box = Mailbox::factory()->create(['key' => 'support']);
    $owner = Operator::factory()->role('Owner')->create(['password' => 'password']);

    $page = Livewire::actingAs($owner, 'operator')
        ->test(EditMailbox::class, ['uuid' => $box->uuid])
        ->set('confirmablePassword', 'password')
        ->call('confirmPassword');

    config()->set('admin_access.secrets_key', 'zu-kurz');

    $page->set('password', 'neu-und-sicher')
        ->call('save')
        ->assertHasErrors('password');
});

it('refuses to leave the system purpose empty, because it is the fallback', function () {
    Mailbox::factory()->create(['key' => 'no-reply']);
    Settings::set(MailPurpose::settingKey(MailPurpose::SYSTEM), 'no-reply');

    Livewire::actingAs(Operator::factory()->role('Owner')->create(['password' => 'password']), 'operator')
        ->test(MailPage::class)
        ->set('purposes.system', '')
        ->call('savePurposes')
        ->assertHasErrors('purposes.system');

    expect(Settings::get(MailPurpose::settingKey(MailPurpose::SYSTEM)))->toBe('no-reply');
});

it('refuses to deactivate the mailbox that "system" currently points at', function () {
    // savePurposes() above refuses to leave system EMPTY. Nothing stopped an
    // operator reaching the identical broken state — every purpose without
    // its own mailbox unable to send — by unchecking Aktiv on the mailbox
    // system already points at instead: MailboxResolver::active() filters an
    // inactive mailbox at both the direct lookup and the system fallback, so
    // resolution() returns null everywhere at once.
    $box = Mailbox::factory()->create(['key' => 'no-reply', 'active' => true]);
    Settings::set(MailPurpose::settingKey(MailPurpose::SYSTEM), 'no-reply');

    Livewire::actingAs(Operator::factory()->role('Owner')->create(['password' => 'password']), 'operator')
        ->test(EditMailbox::class, ['uuid' => $box->uuid])
        ->set('active', false)
        ->call('save')
        ->assertHasErrors('active');

    expect($box->fresh()->active)->toBeTrue();
});

it('still allows deactivating a mailbox that is not behind "system"', function () {
    // The control case for the guard above: the fix must refuse THIS ONE
    // transition, not deactivation in general — "saves display name,
    // username, no-reply and active together" already proves the field
    // itself still works, but not against a system mapping that exists and
    // simply points elsewhere.
    $systemBox = Mailbox::factory()->create(['key' => 'no-reply']);
    $otherBox = Mailbox::factory()->create(['key' => 'support', 'active' => true]);
    Settings::set(MailPurpose::settingKey(MailPurpose::SYSTEM), 'no-reply');

    Livewire::actingAs(Operator::factory()->role('Owner')->create(['password' => 'password']), 'operator')
        ->test(EditMailbox::class, ['uuid' => $otherBox->uuid])
        ->set('active', false)
        ->call('save')
        ->assertHasNoErrors();

    expect($otherBox->fresh()->active)->toBeFalse()
        ->and($systemBox->fresh()->active)->toBeTrue();
});

// --- Coverage beyond the brief's list: behaviours the page and modal claim
// but the tests above never exercise. Each was verified to fail for the
// right reason before the implementation existed, and again by mutation
// afterwards (see the task report).

it('lists every mailbox on the page', function () {
    Mailbox::factory()->create(['key' => 'support', 'address' => 'support@clupilot.com']);

    Livewire::actingAs(Operator::factory()->role('Owner')->create(['password' => 'password']), 'operator')
        ->test(MailPage::class)
        // Die Postfächer haben seit dem Umbau einen eigenen Reiter — vier
        // Karten in einer Spalte waren der Grund dafür.
        ->set('tab', 'postfaecher')
        ->assertSee('support@clupilot.com')
        ->assertSee('support');
});

it('shows the currently configured mailbox for each purpose', function () {
    Mailbox::factory()->create(['key' => 'support']);
    Settings::set(MailPurpose::settingKey(MailPurpose::SUPPORT), 'support');

    Livewire::actingAs(Operator::factory()->role('Owner')->create(['password' => 'password']), 'operator')
        ->test(MailPage::class)
        ->assertSet('purposes.support', 'support');
});

it('saves the mail server settings', function () {
    Livewire::actingAs(Operator::factory()->role('Owner')->create(['password' => 'password']), 'operator')
        ->test(MailPage::class)
        ->set('confirmablePassword', 'password')
        ->call('confirmPassword')
        ->set('host', 'smtp.example.com')
        ->set('port', 2525)
        ->set('encryption', 'ssl')
        ->call('saveServer')
        ->assertHasNoErrors();

    expect(Settings::get('mail.host'))->toBe('smtp.example.com')
        ->and(Settings::get('mail.port'))->toBe(2525)
        ->and(Settings::get('mail.encryption'))->toBe('ssl');
});

it('rejects an empty mail server host', function () {
    Livewire::actingAs(Operator::factory()->role('Owner')->create(['password' => 'password']), 'operator')
        ->test(MailPage::class)
        ->set('confirmablePassword', 'password')
        ->call('confirmPassword')
        ->set('host', '')
        ->call('saveServer')
        ->assertHasErrors('host');
});

// --- Codex R15#6, P2: last_verified_at proves a mailbox was tested against a
// SPECIFIC server config. Changing host, port or encryption here silently
// invalidates that proof for every mailbox at once — the shared-server mirror
// of EditMailbox::save()'s guard on a single mailbox's own address/username/
// authenticates. Each test below pins host/port/encryption to a KNOWN old
// value via Settings::set() before mounting, rather than trusting whatever
// RefreshDatabase's baseline seed happens to contain — the point is to control
// exactly which one field moves.

it("clears every mailbox's last_verified_at when the server host changes", function () {
    Settings::set('mail.host', 'old-relay.example.com');
    Settings::set('mail.port', 587);
    Settings::set('mail.encryption', 'tls');

    $a = Mailbox::factory()->create(['last_verified_at' => now()]);
    $b = Mailbox::factory()->create(['last_verified_at' => now()]);

    Livewire::actingAs(Operator::factory()->role('Owner')->create(['password' => 'password']), 'operator')
        ->test(MailPage::class)
        ->set('confirmablePassword', 'password')
        ->call('confirmPassword')
        ->set('host', 'new-relay.example.com')
        ->call('saveServer')
        ->assertHasNoErrors();

    expect($a->fresh()->last_verified_at)->toBeNull()
        ->and($b->fresh()->last_verified_at)->toBeNull();
});

it("clears every mailbox's last_verified_at when the server port changes", function () {
    Settings::set('mail.host', 'relay.example.com');
    Settings::set('mail.port', 587);
    Settings::set('mail.encryption', 'tls');

    $box = Mailbox::factory()->create(['last_verified_at' => now()]);

    Livewire::actingAs(Operator::factory()->role('Owner')->create(['password' => 'password']), 'operator')
        ->test(MailPage::class)
        ->set('confirmablePassword', 'password')
        ->call('confirmPassword')
        ->set('port', 2525)
        ->call('saveServer')
        ->assertHasNoErrors();

    expect($box->fresh()->last_verified_at)->toBeNull();
});

it("clears every mailbox's last_verified_at when the server encryption changes", function () {
    Settings::set('mail.host', 'relay.example.com');
    Settings::set('mail.port', 587);
    Settings::set('mail.encryption', 'tls');

    $box = Mailbox::factory()->create(['last_verified_at' => now()]);

    Livewire::actingAs(Operator::factory()->role('Owner')->create(['password' => 'password']), 'operator')
        ->test(MailPage::class)
        ->set('confirmablePassword', 'password')
        ->call('confirmPassword')
        ->set('encryption', 'ssl')
        ->call('saveServer')
        ->assertHasNoErrors();

    expect($box->fresh()->last_verified_at)->toBeNull();
});

it('keeps last_verified_at when the server card is saved with no actual change', function () {
    // The control case for the three tests above: an operator who opens the
    // card and saves without editing anything has changed nothing, and must
    // not wipe a legitimate verification. Sets every field to the SAME value
    // mount() already read, rather than never calling set() at all — this is
    // what "re-saving identical values" actually looks like through the form.
    Settings::set('mail.host', 'relay.example.com');
    Settings::set('mail.port', 587);
    Settings::set('mail.encryption', 'tls');

    $box = Mailbox::factory()->create(['last_verified_at' => now()]);

    Livewire::actingAs(Operator::factory()->role('Owner')->create(['password' => 'password']), 'operator')
        ->test(MailPage::class)
        ->set('confirmablePassword', 'password')
        ->call('confirmPassword')
        ->set('host', 'relay.example.com')
        ->set('port', 587)
        ->set('encryption', 'tls')
        ->call('saveServer')
        ->assertHasNoErrors();

    expect($box->fresh()->last_verified_at)->not->toBeNull();
});

it('shows "not yet verified" instead of a blank cell once the server change clears it', function () {
    // The console-list-facing half of the finding: a cleared timestamp must
    // read as an honest, explicit "not yet verified", not a blank cell that
    // could be mistaken for a rendering error.
    Settings::set('mail.host', 'relay.example.com');
    Settings::set('mail.port', 587);
    Settings::set('mail.encryption', 'tls');

    Mailbox::factory()->create(['key' => 'support', 'last_verified_at' => now()]);

    Livewire::actingAs(Operator::factory()->role('Owner')->create(['password' => 'password']), 'operator')
        ->test(MailPage::class)
        ->set('confirmablePassword', 'password')
        ->call('confirmPassword')
        ->set('host', 'new-relay.example.com')
        ->call('saveServer')
        ->set('tab', 'postfaecher')
        ->assertSee(__('mail_settings.never_verified'));
});

it('refuses to change the mail server without a recently confirmed password, capability notwithstanding', function () {
    // saveServer() writes mail.host — the platform's outbound relay for every
    // purpose mailbox at once. An Owner has mail.manage for the whole test;
    // nothing here proves the SECOND gate exists unless a properly capable
    // session still gets blocked for want of a recent confirmation.
    $before = Settings::get('mail.host');

    Livewire::actingAs(Operator::factory()->role('Owner')->create(['password' => 'password']), 'operator')
        ->test(MailPage::class)
        ->set('host', 'evil-relay.example.com')
        ->set('port', 2525)
        ->set('encryption', 'ssl')
        ->call('saveServer')
        ->assertForbidden();

    // Not ->toBeNull(): RefreshDatabase's baseline migrate already seeds
    // mail.host from the real environment (see the seed migration), so a
    // blank slate is not what "the write was blocked" looks like here — an
    // unchanged value, still not the attacker's relay, is.
    expect(Settings::get('mail.host'))->toBe($before)
        ->and(Settings::get('mail.host'))->not->toBe('evil-relay.example.com');
});

it('does not require a confirmed password to save the purpose mapping', function () {
    // The mirror of the test above: saveServer() is gated, savePurposes() is
    // deliberately not — this is the "changing an address" split the brief
    // describes, proven from the ungated side rather than left implicit in
    // the tests that merely never happen to call confirmPassword().
    Mailbox::factory()->create(['key' => 'support']);

    Livewire::actingAs(Operator::factory()->role('Owner')->create(['password' => 'password']), 'operator')
        ->test(MailPage::class)
        ->set('purposes.system', 'support')
        ->set('purposes.support', 'support')
        ->call('savePurposes')
        ->assertHasNoErrors();

    expect(Settings::get(MailPurpose::settingKey(MailPurpose::SUPPORT)))->toBe('support');
});

it('saves the purpose-to-mailbox mapping', function () {
    Mailbox::factory()->create(['key' => 'support']);
    Mailbox::factory()->create(['key' => 'no-reply']);

    Livewire::actingAs(Operator::factory()->role('Owner')->create(['password' => 'password']), 'operator')
        ->test(MailPage::class)
        ->set('purposes.system', 'no-reply')
        ->set('purposes.support', 'support')
        ->call('savePurposes')
        ->assertHasNoErrors();

    expect(Settings::get(MailPurpose::settingKey(MailPurpose::SUPPORT)))->toBe('support')
        ->and(Settings::get(MailPurpose::settingKey(MailPurpose::SYSTEM)))->toBe('no-reply');
});

// --- P2: a Livewire request can post any string to purposes.*, including one
// posted straight to /livewire/update that never went through the <select>
// at all — the fallback is only useful if it actually names a mailbox that
// can send.

it('refuses to map system to a mailbox key that names no mailbox at all', function () {
    Mailbox::factory()->create(['key' => 'support']);

    Livewire::actingAs(Operator::factory()->role('Owner')->create(['password' => 'password']), 'operator')
        ->test(MailPage::class)
        ->set('purposes.system', 'a-deleted-or-tampered-key')
        ->call('savePurposes')
        ->assertHasErrors('purposes.system');

    expect(Settings::get(MailPurpose::settingKey(MailPurpose::SYSTEM)))->toBeNull();
});

it('refuses to map system to a mailbox that exists but is not active', function () {
    // Existing is not enough for system specifically: MailboxResolver::active()
    // filters an inactive mailbox out at BOTH the direct lookup and the system
    // fallback, so an inactive "system" leaves every unmapped purpose with
    // nothing behind it — not just system's own mail.
    Mailbox::factory()->create(['key' => 'no-reply', 'active' => false]);

    Livewire::actingAs(Operator::factory()->role('Owner')->create(['password' => 'password']), 'operator')
        ->test(MailPage::class)
        ->set('purposes.system', 'no-reply')
        ->call('savePurposes')
        ->assertHasErrors('purposes.system');

    expect(Settings::get(MailPurpose::settingKey(MailPurpose::SYSTEM)))->toBeNull();
});

it('refuses to map a non-system purpose to a mailbox key that names no mailbox at all', function () {
    Mailbox::factory()->create(['key' => 'no-reply']);
    Settings::set(MailPurpose::settingKey(MailPurpose::SYSTEM), 'no-reply');

    Livewire::actingAs(Operator::factory()->role('Owner')->create(['password' => 'password']), 'operator')
        ->test(MailPage::class)
        ->set('purposes.support', 'a-deleted-or-tampered-key')
        ->call('savePurposes')
        ->assertHasErrors('purposes.support');

    expect(Settings::get(MailPurpose::settingKey(MailPurpose::SUPPORT)))->toBeNull();
});

it('still allows mapping a non-system purpose to a mailbox that exists but is inactive', function () {
    // The control case for the two refusals above: MailboxResolver::active()
    // already falls an inactive NON-system mapping through to system on its
    // own (see its own docblock), so only system needs the stricter active
    // check — a non-system purpose merely needs the key to name a real row.
    Mailbox::factory()->create(['key' => 'no-reply']);
    Mailbox::factory()->create(['key' => 'support', 'active' => false]);
    Settings::set(MailPurpose::settingKey(MailPurpose::SYSTEM), 'no-reply');

    Livewire::actingAs(Operator::factory()->role('Owner')->create(['password' => 'password']), 'operator')
        ->test(MailPage::class)
        ->set('purposes.support', 'support')
        ->call('savePurposes')
        ->assertHasNoErrors();

    expect(Settings::get(MailPurpose::settingKey(MailPurpose::SUPPORT)))->toBe('support');
});

it('still allows leaving a non-system purpose blank, even after it once had a mailbox', function () {
    // Blank is the legitimate "fall back to system" choice, not a tampered
    // value — the fix must not turn every unmapped purpose into a validation
    // error the moment it is resaved.
    Mailbox::factory()->create(['key' => 'no-reply']);
    Mailbox::factory()->create(['key' => 'support']);
    Settings::set(MailPurpose::settingKey(MailPurpose::SYSTEM), 'no-reply');
    Settings::set(MailPurpose::settingKey(MailPurpose::SUPPORT), 'support');

    Livewire::actingAs(Operator::factory()->role('Owner')->create(['password' => 'password']), 'operator')
        ->test(MailPage::class)
        ->set('purposes.support', '')
        ->call('savePurposes')
        ->assertHasNoErrors();

    expect(Settings::get(MailPurpose::settingKey(MailPurpose::SUPPORT)))->toBe('');
});

it('re-checks mail.manage on every page action, not only at mount', function () {
    // mount() alone would pass every test above: an Owner has the capability
    // for the whole test, so nothing so far proves saveServer()/savePurposes()
    // carry their own authorize() rather than relying on mount() having run
    // once. Swap the authenticated user between mount and the action — same
    // shape as SecretsPageTest's "refuses every action while locked", applied
    // to the capability gate instead of the password gate.
    $owner = Operator::factory()->role('Owner')->create(['password' => 'password']);
    $billing = Operator::factory()->role('Billing')->create(['password' => 'password']);

    // A fresh mount per action, like SecretsPageTest's equivalent: a 403
    // response carries no usable Livewire snapshot, so a second ->call() on
    // the same already-forbidden instance fails on the snapshot itself
    // rather than on the authorization this test means to check.
    foreach (['saveServer', 'savePurposes'] as $action) {
        $page = Livewire::actingAs($owner, 'operator')->test(MailPage::class)->assertOk();

        Livewire::actingAs($billing, 'operator');

        $page->call($action)->assertForbidden();
    }
});

it('re-checks mail.manage on modal save, not only at mount', function () {
    // Same gap as above, for the modal: mount() re-reads the record instead of
    // trusting the browser (R20), but that is a different guarantee from
    // save() re-authorising instead of trusting a mount that already happened.
    $box = Mailbox::factory()->create(['key' => 'support']);
    $owner = Operator::factory()->role('Owner')->create(['password' => 'password']);
    $billing = Operator::factory()->role('Billing')->create(['password' => 'password']);

    $modal = Livewire::actingAs($owner, 'operator')->test(EditMailbox::class, ['uuid' => $box->uuid])->assertOk();

    Livewire::actingAs($billing, 'operator');

    $modal->set('address', 'neu@clupilot.com')->call('save')->assertForbidden();
});

it('refuses to edit a mailbox without mail.manage, even though the modal bypasses the page route', function () {
    $box = Mailbox::factory()->create(['key' => 'support']);

    Livewire::actingAs(Operator::factory()->role('Billing')->create(['password' => 'password']), 'operator')
        ->test(EditMailbox::class, ['uuid' => $box->uuid])
        ->assertForbidden();
});

it('updates the password when a new one is entered', function () {
    // The mirror of "keeps the old password when the field is left empty":
    // that test alone would stay green even if save() stopped writing the
    // password entirely, since "never changes" also satisfies "kept the old
    // one". This proves the other half — a new value actually lands, once the
    // second gate (below) is satisfied.
    $box = Mailbox::factory()->create(['password' => 'alt']);

    Livewire::actingAs(Operator::factory()->role('Owner')->create(['password' => 'password']), 'operator')
        ->test(EditMailbox::class, ['uuid' => $box->uuid])
        ->set('confirmablePassword', 'password')
        ->call('confirmPassword')
        ->set('password', 'neu-und-sicher')
        ->call('save');

    expect($box->fresh()->password)->toBe('neu-und-sicher');
});

it('clears last_verified_at when the password changes, since it no longer proves anything', function () {
    $box = Mailbox::factory()->create(['password' => 'alt', 'last_verified_at' => now()]);

    Livewire::actingAs(Operator::factory()->role('Owner')->create(['password' => 'password']), 'operator')
        ->test(EditMailbox::class, ['uuid' => $box->uuid])
        ->set('confirmablePassword', 'password')
        ->call('confirmPassword')
        ->set('password', 'neu-und-sicher')
        ->call('save');

    expect($box->fresh()->last_verified_at)->toBeNull();
});

// --- P2: last_verified_at proves credentials were tested, not just that a
// password exists — changing EITHER half of the identity smtpUsername()
// authenticates as (an explicit username, or address as its fallback) must
// invalidate that proof exactly like a new password does. None of these set
// a password at all: the point is that the guard fires without one.

it('clears last_verified_at when the address changes, even though the password stays blank', function () {
    $box = Mailbox::factory()->create([
        'address' => 'alt@clupilot.com', 'username' => null, 'last_verified_at' => now(),
    ]);

    Livewire::actingAs(Operator::factory()->role('Owner')->create(['password' => 'password']), 'operator')
        ->test(EditMailbox::class, ['uuid' => $box->uuid])
        ->set('address', 'neu@clupilot.com')
        ->call('save');

    expect($box->fresh()->last_verified_at)->toBeNull();
});

it('clears last_verified_at when an explicit username changes, even though the password stays blank', function () {
    $box = Mailbox::factory()->create([
        'username' => 'alt-smtp-user', 'last_verified_at' => now(),
    ]);

    Livewire::actingAs(Operator::factory()->role('Owner')->create(['password' => 'password']), 'operator')
        ->test(EditMailbox::class, ['uuid' => $box->uuid])
        ->set('username', 'neu-smtp-user')
        ->call('save');

    expect($box->fresh()->last_verified_at)->toBeNull();
});

it('clears last_verified_at when the username is cleared, since smtpUsername() then falls back to the address', function () {
    // The fallback the brief names explicitly: username going from an
    // explicit value to blank changes what smtpUsername() returns just as
    // much as typing a new one would, even though the raw address field
    // never moves.
    $box = Mailbox::factory()->create([
        'address' => 'bleibt@clupilot.com', 'username' => 'alt-smtp-user', 'last_verified_at' => now(),
    ]);

    Livewire::actingAs(Operator::factory()->role('Owner')->create(['password' => 'password']), 'operator')
        ->test(EditMailbox::class, ['uuid' => $box->uuid])
        ->set('username', '')
        ->call('save');

    expect($box->fresh()->username)->toBeNull()
        ->and($box->fresh()->last_verified_at)->toBeNull();
});

it('keeps last_verified_at when neither the address nor the username actually changes', function () {
    // The control case for the three tests above: the guard must react to an
    // actual CHANGE, not fire on every save regardless of what moved —
    // otherwise it would stay green even if it degenerated into always
    // clearing the column.
    $box = Mailbox::factory()->create([
        'address' => 'bleibt@clupilot.com', 'username' => 'bleibt-auch', 'last_verified_at' => now(),
    ]);

    Livewire::actingAs(Operator::factory()->role('Owner')->create(['password' => 'password']), 'operator')
        ->test(EditMailbox::class, ['uuid' => $box->uuid])
        ->set('displayName', 'Neuer Anzeigename')
        ->call('save');

    expect($box->fresh()->last_verified_at)->not->toBeNull();
});

it('refuses to set a new mailbox password without a recently confirmed password', function () {
    // The credential-interception primitive the brief names: a signed-in
    // mail.manage session alone must not be enough to repoint where a
    // mailbox's outgoing SMTP password comes from.
    $box = Mailbox::factory()->create(['password' => 'alt']);

    Livewire::actingAs(Operator::factory()->role('Owner')->create(['password' => 'password']), 'operator')
        ->test(EditMailbox::class, ['uuid' => $box->uuid])
        ->set('password', 'neu-und-sicher')
        ->call('save')
        ->assertForbidden();

    expect($box->fresh()->password)->toBe('alt');
});

it('does not require a confirmed password to save address, display name, username or active when the password field is left empty', function () {
    // The other half of the split the brief draws: setting a NEW password is
    // gated, but "changing an address" — everything else this modal does —
    // is not. "saves display name, username, no-reply and active together"
    // already exercises this path without ever calling confirmPassword();
    // this test says so explicitly and checks the gate was never even
    // reachable (no 403), rather than leaving the claim implicit.
    $box = Mailbox::factory()->create(['address' => 'alt@clupilot.com']);

    Livewire::actingAs(Operator::factory()->role('Owner')->create(['password' => 'password']), 'operator')
        ->test(EditMailbox::class, ['uuid' => $box->uuid])
        ->set('address', 'neu@clupilot.com')
        ->set('displayName', 'Neu-Team')
        ->call('save')
        ->assertHasNoErrors()
        ->assertOk();

    expect($box->fresh()->address)->toBe('neu@clupilot.com');
});

it('saves display name, username, no-reply and active together', function () {
    $box = Mailbox::factory()->create(['no_reply' => false, 'active' => true]);

    Livewire::actingAs(Operator::factory()->role('Owner')->create(['password' => 'password']), 'operator')
        ->test(EditMailbox::class, ['uuid' => $box->uuid])
        ->set('displayName', 'Support-Team')
        ->set('username', 'smtp-user')
        ->set('noReply', true)
        ->set('active', false)
        ->call('save');

    $box->refresh();

    expect($box->display_name)->toBe('Support-Team')
        ->and($box->username)->toBe('smtp-user')
        ->and($box->no_reply)->toBeTrue()
        ->and($box->active)->toBeFalse();
});

it('pre-fills the form from the existing record', function () {
    $box = Mailbox::factory()->create([
        'address' => 'support@clupilot.com',
        'display_name' => 'Support-Team',
        'username' => 'smtp-support',
        'no_reply' => true,
        'active' => false,
        'authenticates' => false,
    ]);

    Livewire::actingAs(Operator::factory()->role('Owner')->create(['password' => 'password']), 'operator')
        ->test(EditMailbox::class, ['uuid' => $box->uuid])
        ->assertSet('address', 'support@clupilot.com')
        ->assertSet('displayName', 'Support-Team')
        ->assertSet('username', 'smtp-support')
        ->assertSet('noReply', true)
        ->assertSet('active', false)
        ->assertSet('authenticates', false);
});

// --- Codex R15#4, P1b: the modal's own checkbox for authenticates (R20 — no
// new modal, the existing one gets a field).

it('renders an authenticates checkbox in the modal, wired to the property', function () {
    // A property the component accepts is not the same as a field the
    // operator can actually reach — R20's own point.
    $box = Mailbox::factory()->create(['authenticates' => true]);

    Livewire::actingAs(Operator::factory()->role('Owner')->create(['password' => 'password']), 'operator')
        ->test(EditMailbox::class, ['uuid' => $box->uuid])
        ->assertSeeHtml('wire:model.live="authenticates"')
        ->assertSee(__('mail_settings.authenticates'));
});

it('saves authenticates together with the other fields', function () {
    $box = Mailbox::factory()->create(['authenticates' => true]);

    Livewire::actingAs(Operator::factory()->role('Owner')->create(['password' => 'password']), 'operator')
        ->test(EditMailbox::class, ['uuid' => $box->uuid])
        ->set('authenticates', false)
        ->call('save');

    expect($box->fresh()->authenticates)->toBeFalse();
});

it('clears last_verified_at when authenticates changes, even though the password stays blank', function () {
    // The fourth thing that invalidates a verification, alongside address
    // and username (see the trio of tests above this block): a proof that
    // credentials work means something different once whether to send them
    // at all has flipped.
    $box = Mailbox::factory()->create(['authenticates' => true, 'last_verified_at' => now()]);

    Livewire::actingAs(Operator::factory()->role('Owner')->create(['password' => 'password']), 'operator')
        ->test(EditMailbox::class, ['uuid' => $box->uuid])
        ->set('authenticates', false)
        ->call('save');

    expect($box->fresh()->last_verified_at)->toBeNull();
});

it('does not clear last_verified_at when authenticates is saved unchanged', function () {
    // The control case: the guard must react to an actual flip, not fire on
    // every save regardless — the same shape as the existing address/
    // username control test just above this block.
    $box = Mailbox::factory()->create(['authenticates' => true, 'last_verified_at' => now()]);

    Livewire::actingAs(Operator::factory()->role('Owner')->create(['password' => 'password']), 'operator')
        ->test(EditMailbox::class, ['uuid' => $box->uuid])
        ->set('displayName', 'Neuer Anzeigename')
        ->call('save');

    expect($box->fresh()->last_verified_at)->not->toBeNull();
});

it('does not report "no password stored" when testing a mailbox that does not authenticate', function () {
    // The console-list-facing half of the finding: before this fix, the
    // test-send button was the one place an unauthenticated mailbox actually
    // read back as broken — MailboxTesterTest.php covers the service itself
    // in depth (including that AUTH is never attempted); this proves the
    // BUTTON reaches that fixed guard rather than a mock standing in for it.
    // 127.0.0.1:1 refused in well under a millisecond (see "runs the real
    // tester..." above) is what makes the failure real and fast rather than
    // proving nothing by never actually attempting a connection.
    Settings::set('mail.host', '127.0.0.1');
    Settings::set('mail.port', 1);
    $box = Mailbox::factory()->create([
        'key' => 'support', 'address' => 'no-reply@clupilot.com',
        'username' => null, 'password' => null, 'authenticates' => false,
    ]);

    Livewire::actingAs(Operator::factory()->role('Owner')->create(['password' => 'password']), 'operator')
        ->test(MailPage::class)
        ->set('testRecipient', 'ziel@example.com')
        ->call('test', $box->uuid)
        ->assertSet('testResult.error', fn ($error) => str_contains((string) $error, 'Connection refused'));
});

// --- Task 8: the test-send button. MailboxTesterTest.php covers
// MailboxTester itself in depth (bypassing the log/array mailer, verbatim
// error passthrough, the SECRETS_KEY guard, a real success and a real SMTP
// rejection). What is left to prove here is the WIRING: does the button on
// this page reach that service with the right arguments, under the right
// authorization and validation, and does the page show what came back.

it('refuses to run a mailbox test without mail.manage, re-checked independently of mount', function () {
    $box = Mailbox::factory()->create(['key' => 'support']);
    $owner = Operator::factory()->role('Owner')->create(['password' => 'password']);
    $billing = Operator::factory()->role('Billing')->create(['password' => 'password']);

    $page = Livewire::actingAs($owner, 'operator')->test(MailPage::class)->assertOk();

    Livewire::actingAs($billing, 'operator');

    $page->set('testRecipient', 'ziel@example.com')
        ->call('test', $box->uuid)
        ->assertForbidden();
});

it('requires a recipient before sending a test', function () {
    $box = Mailbox::factory()->create(['key' => 'support']);

    Livewire::actingAs(Operator::factory()->role('Owner')->create(['password' => 'password']), 'operator')
        ->test(MailPage::class)
        ->set('testRecipient', '')
        ->call('test', $box->uuid)
        ->assertHasErrors('testRecipient');
});

it('requires the recipient to actually be an email address', function () {
    $box = Mailbox::factory()->create(['key' => 'support']);

    Livewire::actingAs(Operator::factory()->role('Owner')->create(['password' => 'password']), 'operator')
        ->test(MailPage::class)
        ->set('testRecipient', 'not-an-email')
        ->call('test', $box->uuid)
        ->assertHasErrors('testRecipient');
});

it('runs the real tester against the chosen mailbox and records the result', function () {
    // 127.0.0.1:1 is refused in well under a millisecond (see
    // MailboxTesterTest) — fast and deterministic enough to run through the
    // REAL MailboxTester here rather than a mock, proving the button is
    // wired to the thing that actually sends, not a stand-in for it.
    Settings::set('mail.host', '127.0.0.1');
    Settings::set('mail.port', 1);
    $box = Mailbox::factory()->create(['key' => 'support']);

    Livewire::actingAs(Operator::factory()->role('Owner')->create(['password' => 'password']), 'operator')
        ->test(MailPage::class)
        ->set('testRecipient', 'ziel@example.com')
        ->call('test', $box->uuid)
        ->assertSet('testedKey', 'support')
        ->assertSet('testResult.ok', false)
        ->assertSet('testResult.error', fn ($error) => str_contains((string) $error, 'Connection refused'));
});

it('shows the result against the mailbox it actually tested', function () {
    // Not assertSee('support') alone: the mailboxes table already prints
    // every box's own key in its row regardless of which one was tested, so
    // that assertion would stay green even if $testedKey were wired to the
    // wrong box (caught by mutation — see the task report). 'support:' with
    // the colon is unique to the alert's "<key>: <message>" markup; the table
    // row never follows a key with one.
    Settings::set('mail.host', '127.0.0.1');
    Settings::set('mail.port', 1);
    $box = Mailbox::factory()->create(['key' => 'support']);
    Mailbox::factory()->create(['key' => 'billing']); // a second row: proves it's not just "any key"

    Livewire::actingAs(Operator::factory()->role('Owner')->create(['password' => 'password']), 'operator')
        ->test(MailPage::class)
        ->set('tab', 'postfaecher')
        ->set('testRecipient', 'ziel@example.com')
        ->call('test', $box->uuid)
        ->assertSee('support:')
        ->assertDontSee('billing:')
        ->assertSee(__('mail_settings.test_failed'));
});

it('does not crash the test button when SECRETS_KEY is missing, and says so instead', function () {
    // The gap the brief could not know about: $box->password decrypts on
    // read, and this installation has no SECRETS_KEY at all. Proven at the
    // MailboxTester unit level already — this proves the BUTTON reaches that
    // guard rather than a mock standing in for it, the same distinction
    // "re-checks mail.manage on every page action" draws for authorization.
    $box = Mailbox::factory()->create(['key' => 'support']);
    config()->set('admin_access.secrets_key', '');

    Livewire::actingAs(Operator::factory()->role('Owner')->create(['password' => 'password']), 'operator')
        ->test(MailPage::class)
        ->set('testRecipient', 'ziel@example.com')
        ->call('test', $box->uuid)
        ->assertSet('testResult.error', __('mail_settings.no_key'));
});

it('does not crash the test button for a malformed SECRETS_KEY either, and says so instead', function () {
    // Codex R15#3, P2: MailboxTester::run() guards on isUsable() BEFORE
    // touching $box->password (which decrypts on read) precisely to avoid an
    // uncaught RuntimeException out of a Livewire action. A malformed key
    // used to slip that guard exactly like the missing-key case above, just
    // one property access later.
    $box = Mailbox::factory()->create(['key' => 'support']);
    config()->set('admin_access.secrets_key', 'zu-kurz');

    Livewire::actingAs(Operator::factory()->role('Owner')->create(['password' => 'password']), 'operator')
        ->test(MailPage::class)
        ->set('testRecipient', 'ziel@example.com')
        ->call('test', $box->uuid)
        ->assertSet('testResult.error', __('mail_settings.no_key'));
});

it('does not crash the test button when the stored password was encrypted under a different key, and says so instead', function () {
    // Codex R15#9, P2: isUsable() only proves SECRETS_KEY is well-formed —
    // never that THIS row's ciphertext was actually encrypted under it. A
    // key that is present and valid but does not match the stored
    // ciphertext is a case isUsable() cannot detect by design, and it used
    // to throw a DecryptException straight out of this same Livewire
    // action, unguarded. Proven at the MailboxTester unit level already;
    // this proves the BUTTON reaches that guard rather than a mock standing
    // in for it — Livewire::test() surfaces an uncaught exception as a
    // thrown PHP error, not silently, so this genuinely fails loudly if the
    // guard regresses rather than passing by accident.
    $box = Mailbox::factory()->create(['key' => 'support', 'password' => 'geheim']);
    config()->set('admin_access.secrets_key', 'base64:'.base64_encode(random_bytes(32)));

    Livewire::actingAs(Operator::factory()->role('Owner')->create(['password' => 'password']), 'operator')
        ->test(MailPage::class)
        ->set('testRecipient', 'ziel@example.com')
        ->call('test', $box->uuid)
        ->assertSet('testResult.error', __('mail_settings.test_password_undecryptable'));
});

it('offers a per-row test-send button, not only edit', function () {
    $blade = file_get_contents(resource_path('views/livewire/admin/mail.blade.php'));

    expect($blade)->toContain('wire:click="test(');
});

it('tells the operator the test send really goes out, even though ordinary mail only logs here', function () {
    Livewire::actingAs(Operator::factory()->role('Owner')->create(['password' => 'password']), 'operator')
        ->test(MailPage::class)
        ->set('tab', 'postfaecher')
        ->assertSee(__('mail_settings.test_hint'));
});

/**
 * ── Der Umbau auf Reiter ──────────────────────────────────────────────────
 *
 * Vier Karten in einer schmalen Spalte, davon zwei mit zusammen
 * zweiundzwanzig gleich aussehenden Zeilen und zwei gleich aussehenden
 * Speichern-Knöpfen: der Betreiber hat die Seite als unlesbar gemeldet. Die
 * Tests hier halten fest, was der Umbau zusichert — nicht, wie er aussieht.
 */
it('falls back to the first tab when the address bar names one that does not exist', function () {
    // Ein Reitername aus der Adresszeile ist eine Zeichenkette, die ein
    // Fremder getippt hat.
    Livewire::actingAs(Operator::factory()->role('Owner')->create(['password' => 'password']), 'operator')
        ->test(MailPage::class)
        ->set('tab', 'gibt-es-nicht')
        ->assertSet('tab', 'versand');
});

it('shows every mail type under its purpose, naming the address it would inherit', function () {
    Mailbox::factory()->create(['key' => 'rechnung', 'address' => 'rechnung@clupilot.com', 'active' => true]);
    Settings::set(MailPurpose::settingKey(MailPurpose::BILLING), 'rechnung');

    Livewire::actingAs(Operator::factory()->role('Owner')->create(['password' => 'password']), 'operator')
        ->test(MailPage::class)
        ->set('tab', 'zuordnung')
        // Die Mailart steht unter ihrem Zweck …
        ->assertSee(MailCatalogue::all()['invoice']['label'])
        // … und die leere Auswahl nennt die Adresse, die sonst greift, statt
        // wie bisher nur den Namen des Zwecks.
        ->assertSee(__('mail_settings.route_inherit', ['address' => 'rechnung@clupilot.com']));
});

it('saves purposes and routing with the one button under the assignment tab', function () {
    Mailbox::factory()->create(['key' => 'system', 'active' => true]);
    Mailbox::factory()->create(['key' => 'rechnung', 'active' => true]);

    Livewire::actingAs(Operator::factory()->role('Owner')->create(['password' => 'password']), 'operator')
        ->test(MailPage::class)
        ->set('purposes.system', 'system')
        ->set('routes.invoice', 'rechnung')
        ->call('saveAssignments')
        ->assertHasNoErrors();

    expect(Settings::get(MailPurpose::settingKey(MailPurpose::SYSTEM)))->toBe('system')
        ->and(Settings::get(MailRoute::settingKey('invoice')))->toBe('rechnung');
});

it('writes no routing at all when the purposes it depends on are refused', function () {
    // Die Reihenfolge im einen Knopf ist die Zusicherung: die Zwecke werden
    // zuerst geprüft und werfen, BEVOR eine einzige Wegwahl geschrieben ist.
    // Andernfalls hinterliesse eine abgelehnte Eingabe die halbe Zuordnung —
    // Wegwahlen, die auf einen Zweck zeigen, den es so nicht gibt.
    Mailbox::factory()->create(['key' => 'rechnung', 'active' => true]);

    Livewire::actingAs(Operator::factory()->role('Owner')->create(['password' => 'password']), 'operator')
        ->test(MailPage::class)
        ->set('purposes.system', '')   // „System" ist der Rückfall und darf nicht leer sein
        ->set('routes.invoice', 'rechnung')
        ->call('saveAssignments')
        ->assertHasErrors('purposes.system');

    expect(Settings::get(MailRoute::settingKey('invoice')))->toBeNull();
});

it('names the mailbox that would really carry the mail, not one that is switched off', function () {
    // Codex R15, P2 am Umbau: das Etikett „wie der Zweck (…)" nannte das
    // ZUGEORDNETE Postfach. MailboxResolver::for() lässt ein abgeschaltetes
    // aber auf „System" zurückfallen, und writePurposes() erlaubt genau das —
    // aktiv sein muss nur „System" selbst. Die Seite hätte dem Betreiber damit
    // eine Adresse genannt, die nichts verschickt, und zwar an der Stelle, an
    // der er nachsieht, um es richtig zu machen.
    Mailbox::factory()->create(['key' => 'system', 'address' => 'noreply@clupilot.com', 'active' => true]);
    Mailbox::factory()->create(['key' => 'rechnung', 'address' => 'rechnung@clupilot.com', 'active' => false]);
    Settings::set(MailPurpose::settingKey(MailPurpose::SYSTEM), 'system');
    Settings::set(MailPurpose::settingKey(MailPurpose::BILLING), 'rechnung');

    Livewire::actingAs(Operator::factory()->role('Owner')->create(['password' => 'password']), 'operator')
        ->test(MailPage::class)
        ->set('tab', 'zuordnung')
        ->assertSee(__('mail_settings.route_inherit', ['address' => 'noreply@clupilot.com']))
        ->assertDontSee(__('mail_settings.route_inherit', ['address' => 'rechnung@clupilot.com']));
});
