<?php

use App\Models\Mailbox;
use App\Services\Secrets\SecretCipher;
use App\Support\Settings;
use Illuminate\Support\Facades\DB;

/**
 * Loads a fresh instance of the seed migration's anonymous class every call.
 * `require`, not `require_once`: PHP re-evaluates the `return new class ...`
 * expression each time, so every call gets its own instance — this is what
 * lets a test call ->up() with config the REAL migrate run (already applied
 * once by RefreshDatabase before any test's own beforeEach) could not have
 * used, to reach branches nothing else exercises.
 */
function loadMailboxSeedMigration(): object
{
    return require database_path('migrations/2026_07_28_100000_seed_mailboxes_from_environment.php');
}

/**
 * This container's real .env carries actual MAIL_HOST/MAIL_PORT/MAIL_URL
 * values (see the migration's UNCONFIGURED_HOST docblock) — env() reads
 * those directly, regardless of what an individual test's config()->set()
 * says, and Env::getRepository() is a process-wide singleton, not something
 * RefreshDatabase or a fresh Application instance resets between tests. Left
 * alone, every test in this file would be quietly deciding "explicitly
 * supplied" (Codex R15#8, P1) based on this machine's own mail setup rather
 * than on what it actually configured — true today only because this
 * container happens to have real mail credentials, and wrong the moment it
 * does not. Neutralised here for every test by default; withRealEnv() opts
 * individual tests back in to prove the explicitly-supplied paths themselves.
 */
$realMailEnvBefore = [];

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

    $realMailEnvBefore = captureRealEnv(['MAIL_HOST', 'MAIL_PORT', 'MAIL_URL']);
    setRealEnv(['MAIL_HOST' => null, 'MAIL_PORT' => null, 'MAIL_URL' => null]);
});

afterEach(function () use (&$realMailEnvBefore) {
    setRealEnv($realMailEnvBefore);
});

it('does not copy MAIL_SCHEME through — a Symfony transport only knows smtp and smtps', function () {
    clearMailboxSeed();
    config()->set('mail.mailers.smtp.scheme', 'not-a-symfony-scheme');
    config()->set('mail.mailers.smtp.port', 587);

    loadMailboxSeedMigration()->up();

    expect(Settings::get('mail.encryption'))->toBe('tls');
});

it('derives ssl from port 465, regardless of what scheme is configured', function () {
    clearMailboxSeed();
    config()->set('mail.mailers.smtp.scheme', 'irrelevant');
    config()->set('mail.mailers.smtp.port', 465);

    loadMailboxSeedMigration()->up();

    expect(Settings::get('mail.encryption'))->toBe('ssl')
        ->and(Settings::get('mail.port'))->toBe(465);
});

it('keeps implicit TLS when MAIL_SCHEME is smtps on a non-standard port — Codex R15#4, P1a', function () {
    // The finding, first half: an operator who already has MAIL_SCHEME=smtps
    // (Laravel's own dedicated, unambiguous encryption knob) must not have
    // that downgraded to STARTTLS just because their submission port is not
    // 465 — a relay speaking implicit TLS on, say, 587 stops accepting mail
    // the moment this migration replaces a working config with a guess.
    clearMailboxSeed();
    config()->set('mail.mailers.smtp.scheme', 'smtps');
    config()->set('mail.mailers.smtp.port', 587);

    loadMailboxSeedMigration()->up();

    expect(Settings::get('mail.encryption'))->toBe('ssl');
});

it('keeps implicit TLS when MAIL_URL uses the smtps scheme on a non-standard port', function () {
    // The finding, second half: the same relay, described through MAIL_URL
    // instead of MAIL_SCHEME. 'smtps' in the URL is resolved through the
    // same Illuminate\Support\ConfigurationUrlParser Laravel itself uses —
    // it surfaces on the merged config as the 'driver' key (parseConfiguration
    // maps a URL scheme into 'driver', not 'scheme' — verified directly
    // against the installed class), not re-derived by hand from the URL
    // string a second time.
    clearMailboxSeed();
    config()->set('mail.mailers.smtp.scheme', null);
    config()->set('mail.mailers.smtp.url', 'smtps://user:pass@smtp.clupilot.test:587');

    loadMailboxSeedMigration()->up();

    expect(Settings::get('mail.encryption'))->toBe('ssl');
});

it('treats the invalid MAIL_SCHEME=tls value this machine\'s own .env carries as no scheme at all', function () {
    // This container's real .env has exactly this value. Symfony's
    // EsmtpTransportFactory::getSupportedSchemes() only knows 'smtp' and
    // 'smtps' — 'tls' is not a real scheme under either spelling, so it must
    // fall through to the port heuristic (587 -> the ENCRYPTION CHOICE
    // named 'tls', a coincidence of spelling, not the scheme) exactly like
    // no MAIL_SCHEME at all — reproducing this installation's actual
    // behaviour unchanged. The point: an unrecognised scheme string must not
    // silently steer the result toward 'ssl' or 'none' either.
    clearMailboxSeed();
    config()->set('mail.mailers.smtp.scheme', 'tls');
    config()->set('mail.mailers.smtp.port', 587);

    loadMailboxSeedMigration()->up();

    expect(Settings::get('mail.encryption'))->toBe('tls');
});

it('derives tls (STARTTLS required) from an explicit smtp MAIL_SCHEME, even on port 465 — Codex R15#7, P1', function () {
    // The other explicit direction: MAIL_SCHEME=smtp is just as deliberate a
    // statement as smtps, and must win over the port heuristic the same way
    // — an operator who said "plain smtp" was never asking for implicit TLS
    // just because their port happens to be 465.
    //
    // But 'smtp' is NOT a "no encryption" statement — it is Symfony's
    // OPPORTUNISTIC scheme (autoTls, on by default: upgrade to STARTTLS if
    // the server offers it, never fail if it does not). Round 4 of this
    // review mapped this case to 'none' on the wrong theory that 'smtp' was
    // a plain, non-TLS scheme; that silently turned every existing
    // MAIL_SCHEME=smtp installation's encrypted-when-possible connection
    // into an always-plaintext one the moment this migration ran — the
    // credential- and message-exposing bug this test now guards against.
    // 'tls' (STARTTLS REQUIRED) is deliberately stricter than smtp's own
    // opportunistic default: a relay that does not offer STARTTLS now fails
    // loudly instead of sending a mailbox password in the clear.
    clearMailboxSeed();
    config()->set('mail.mailers.smtp.scheme', 'smtp');
    config()->set('mail.mailers.smtp.port', 465);

    loadMailboxSeedMigration()->up();

    expect(Settings::get('mail.encryption'))->toBe('tls');
});

it('falls back to the port heuristic when no scheme is stated anywhere — Codex R15#7, P1', function () {
    // The fifth scenario the R15#7 fix has to keep working, alongside smtp,
    // smtps, smtp:// and smtps://: nobody stated a scheme at all (no
    // MAIL_SCHEME, no MAIL_URL). resolveScheme() returns null, the match's
    // default arm applies, and — since this is not port 465 — that arm must
    // still read 'tls', exactly as before this fix. A genuinely unstated
    // scheme was never part of the 'smtp' => 'none' bug and must not become
    // collateral damage of fixing it.
    clearMailboxSeed();
    config()->set('mail.mailers.smtp.scheme', null);
    config()->set('mail.mailers.smtp.url', null);
    config()->set('mail.mailers.smtp.port', 587);

    loadMailboxSeedMigration()->up();

    expect(Settings::get('mail.encryption'))->toBe('tls');
});

it('does not treat a plain smtp:// MAIL_URL scheme as a stated encryption choice', function () {
    // Asymmetric with 'smtps://' on purpose: the URL's scheme slot is doing
    // double duty in Laravel's own MailManager (it is also how "which
    // transport" gets chosen) and 'smtp://' is the ordinary, only
    // real-world spelling for this mailer's URL — not a deliberate "no
    // encryption" statement. Only 'smtps://' is unambiguous enough to
    // promote; 'smtp://' is left to the port heuristic below, same as no
    // scheme at all — proven here by still reaching 'ssl' on port 465
    // despite the explicit smtp:// scheme in the URL.
    clearMailboxSeed();
    config()->set('mail.mailers.smtp.scheme', null);
    config()->set('mail.mailers.smtp.url', 'smtp://user:pass@smtp.clupilot.test:465');

    loadMailboxSeedMigration()->up();

    expect(Settings::get('mail.encryption'))->toBe('ssl');
});

it('prefers an explicit MAIL_SCHEME over a conflicting MAIL_URL scheme', function () {
    // MAIL_SCHEME is Laravel's single-purpose encryption knob; the URL's
    // scheme is doing double duty (see above). When both are present and
    // disagree, the unambiguous one wins — matching createSmtpTransport()'s
    // own precedence ($config['scheme'] ?? <port fallback>, checked before
    // anything born from the URL even enters the picture). 'tls', not
    // 'ssl': MAIL_SCHEME=smtp still wins the conflict, but it means
    // "STARTTLS required", not implicit TLS — see the R15#7 test above and
    // the match in seed() for why 'smtp' is no longer 'none' either.
    clearMailboxSeed();
    config()->set('mail.mailers.smtp.scheme', 'smtp');
    config()->set('mail.mailers.smtp.url', 'smtps://user:pass@smtp.clupilot.test:587');

    loadMailboxSeedMigration()->up();

    expect(Settings::get('mail.encryption'))->toBe('tls');
});

it('treats config/mail.php\'s own 127.0.0.1 / 2525 defaults as unset when neither env() nor MAIL_URL says otherwise', function () {
    // Genuinely unset, on BOTH signals isExplicitlySupplied() now checks —
    // not just the config()->set() value: this file's own beforeEach()
    // neutralises the real MAIL_HOST/MAIL_PORT/MAIL_URL this container
    // actually carries, so env() reads null here exactly like a fresh
    // install's would, and no MAIL_URL is set either. Codex R15#8, P1 is the
    // three tests below this one; this is the control that proves the
    // sentinel fallback itself still fires when it is supposed to.
    clearMailboxSeed();
    config()->set('mail.mailers.smtp.host', '127.0.0.1');
    config()->set('mail.mailers.smtp.port', 2525);

    loadMailboxSeedMigration()->up();

    // '' / 0 is exactly what MailboxTransport's guards treat as "not
    // configured" — 127.0.0.1 is not '', so it would have sailed straight
    // past the Task 4 guard and quietly dialled localhost instead.
    expect(Settings::get('mail.host'))->toBe('')
        ->and(Settings::get('mail.port'))->toBe(0);
});

it('stores an explicitly configured MAIL_PORT=2525 instead of zeroing it — Codex R15#8, P1', function () {
    // The finding itself: a relay genuinely listening on 2525 (config/mail.
    // php's own placeholder port) is indistinguishable from "nothing
    // configured" by the sentinel comparison alone. env('MAIL_PORT')
    // returning ANYTHING — including '2525' — is what now proves an operator
    // actually wrote this, not config/mail.php's substitution.
    clearMailboxSeed();
    config()->set('mail.mailers.smtp.host', 'relay.internal.example');
    config()->set('mail.mailers.smtp.port', 2525);

    withRealEnv(['MAIL_PORT' => '2525'], function () {
        loadMailboxSeedMigration()->up();
    });

    // Not 0 — MailboxTransport must still be able to dial :2525 for real.
    expect(Settings::get('mail.port'))->toBe(2525);
});

it('stores an explicitly configured MAIL_HOST=127.0.0.1 instead of blanking it — Codex R15#8, P1', function () {
    // The same finding, the host half: an install genuinely relaying through
    // localhost — a sidecar SMTP relay in the same container/pod, commonly
    // paired with MAIL_URL — is indistinguishable from "nothing configured"
    // by the sentinel comparison alone.
    clearMailboxSeed();
    config()->set('mail.mailers.smtp.host', '127.0.0.1');
    config()->set('mail.mailers.smtp.port', 2526); // a real, non-placeholder port alongside it

    withRealEnv(['MAIL_HOST' => '127.0.0.1'], function () {
        loadMailboxSeedMigration()->up();
    });

    // Not '' — MailboxTransport must still be able to dial 127.0.0.1 for real.
    expect(Settings::get('mail.host'))->toBe('127.0.0.1');
});

it('trusts a host and port supplied through MAIL_URL over the sentinel, even when they equal it exactly', function () {
    // The finding's other named case: "a port or host that came from the URL
    // must not be second-guessed by the sentinel at all." Distinct from the
    // MAIL_URL tests further down — those use a URL host/port that already
    // differs from the placeholder, so they would pass even without this
    // fix. This one deliberately points MAIL_URL AT 127.0.0.1:2525 — the
    // exact sentinel values — so only isExplicitlySupplied()'s suppliedByUrl
    // half, not the env() half (MAIL_HOST/MAIL_PORT individually stay
    // unset), can be what saves it.
    clearMailboxSeed();
    config()->set('mail.mailers.smtp.host', '127.0.0.1');
    config()->set('mail.mailers.smtp.port', 2525);
    config()->set('mail.mailers.smtp.url', 'smtp://user:pass@127.0.0.1:2525');

    loadMailboxSeedMigration()->up();

    expect(Settings::get('mail.host'))->toBe('127.0.0.1')
        ->and(Settings::get('mail.port'))->toBe(2525);
});

it('still stores a deliberately configured host and port untouched', function () {
    clearMailboxSeed();
    config()->set('mail.mailers.smtp.host', 'smtp.example.test');
    config()->set('mail.mailers.smtp.port', 587);

    loadMailboxSeedMigration()->up();

    expect(Settings::get('mail.host'))->toBe('smtp.example.test')
        ->and(Settings::get('mail.port'))->toBe(587);
});

it('resolves host, port, username and password from MAIL_URL when the individual SMTP keys are unset', function () {
    // Codex R15#3, P1: an install may configure SMTP through Laravel's OTHER
    // supported form, MAIL_URL (config/mail.php carries 'url' =>
    // env('MAIL_URL')), instead of MAIL_HOST/MAIL_USERNAME/MAIL_PASSWORD.
    // When it does, the individual keys sit at config/mail.php's own
    // placeholder defaults below (127.0.0.1, 2525, null, null) because
    // nothing ever set them — MailManager only substitutes the URL's
    // components in at transport-build time, inside its own protected
    // getConfig(). Read raw, as this migration used to, that is exactly
    // UNCONFIGURED_HOST / UNCONFIGURED_PORT: it stored a blank host, port 0,
    // and an unconfigured mailbox for an install that was sending mail
    // successfully.
    clearMailboxSeed();
    config()->set('mail.mailers.smtp.host', '127.0.0.1');
    config()->set('mail.mailers.smtp.port', 2525);
    config()->set('mail.mailers.smtp.username', null);
    config()->set('mail.mailers.smtp.password', null);
    config()->set('mail.mailers.smtp.url', 'smtp://no-reply%40clupilot.com:s3cr%40t@smtp.clupilot.test:587');

    loadMailboxSeedMigration()->up();

    expect(Settings::get('mail.host'))->toBe('smtp.clupilot.test')
        ->and(Settings::get('mail.port'))->toBe(587)
        ->and(Settings::get('mail.encryption'))->toBe('tls');

    $box = Mailbox::findByKey('no-reply');

    // isConfigured() is exactly what MailboxResolver checks before handing a
    // purpose to MailboxTransport — false here would mean every purpose
    // mailer falls through to "unconfigured" and throws on send, the outage
    // this finding describes.
    expect($box->isConfigured())->toBeTrue()
        ->and($box->smtpUsername())->toBe('no-reply@clupilot.com')
        // Decoded from the URL's percent-encoding the same way MailManager
        // decodes it (rawurldecode inside ConfigurationUrlParser), not left
        // as the literal 's3cr%40t'.
        ->and($box->password)->toBe('s3cr@t');
});

it("prefers MAIL_URL over the individual SMTP keys when both are set, matching MailManager's own precedence", function () {
    // Verified against the real (protected) Illuminate\Mail\MailManager::
    // getConfig() via reflection, not guessed: with both an 'url' and
    // individually-set host/port/username/password present, the URL's
    // components win for every field it supplies — Illuminate\Support\
    // ConfigurationUrlParser::getPrimaryOptions() array_merges them in AFTER
    // the individual keys, and array_filter there only drops what the URL
    // left null. Decoy values stand in for the individual keys here
    // specifically so a migration that read them instead of the URL would
    // fail this test with THEIR value, not a blank one — the sibling test
    // above already covers the blank/placeholder-default shape of the bug.
    clearMailboxSeed();
    config()->set('mail.mailers.smtp.host', 'decoy-host.invalid');
    config()->set('mail.mailers.smtp.port', 9999);
    config()->set('mail.mailers.smtp.username', 'decoy@invalid.test');
    config()->set('mail.mailers.smtp.password', 'decoy-password');
    config()->set('mail.mailers.smtp.url', 'smtp://no-reply%40clupilot.com:s3cr%40t@smtp.clupilot.test:587');

    loadMailboxSeedMigration()->up();

    expect(Settings::get('mail.host'))->toBe('smtp.clupilot.test')
        ->and(Settings::get('mail.port'))->toBe(587);

    $box = Mailbox::findByKey('no-reply');

    expect($box->smtpUsername())->toBe('no-reply@clupilot.com')
        ->and($box->password)->toBe('s3cr@t');
});

it('seeds address from mail.from.address and keeps the SMTP login as username when they differ', function () {
    // The installation this reproduces: MAIL_USERNAME is a relay login,
    // MAIL_FROM_ADDRESS is the address actually in force today. Before this
    // fix the login was seeded as the From address and username was left
    // null — invalid mail, or mail sent from the relay login instead of the
    // sender the system was actually configured to use.
    clearMailboxSeed();
    config()->set('mail.mailers.smtp.username', 'no-reply@clupilot.com');
    config()->set('mail.from.address', 'hello@clupilot.local');

    loadMailboxSeedMigration()->up();

    $box = Mailbox::findByKey('no-reply');

    expect($box->address)->toBe('hello@clupilot.local')
        ->and($box->username)->toBe('no-reply@clupilot.com');
});

it('leaves username null when the SMTP login and the From address are the same', function () {
    // Mailbox::smtpUsername() already falls back to address when username is
    // null — writing the identical string into both columns is a redundant
    // copy that only has to drift once (an operator changes one in the
    // console but not the other) to become a real, harder-to-spot bug.
    clearMailboxSeed();
    config()->set('mail.mailers.smtp.username', 'ops@clupilot.com');
    config()->set('mail.from.address', 'ops@clupilot.com');

    loadMailboxSeedMigration()->up();

    $box = Mailbox::findByKey('no-reply');

    expect($box->address)->toBe('ops@clupilot.com')
        ->and($box->username)->toBeNull();
});

it('falls back to the SMTP login for address when MAIL_FROM_ADDRESS was never set', function () {
    // config/mail.php's own env('MAIL_FROM_ADDRESS', 'hello@example.com') has
    // already substituted by the time this migration reads config() — an
    // install that never set MAIL_FROM_ADDRESS reads back that placeholder,
    // not null, so it has to be recognised the same way UNCONFIGURED_HOST is.
    clearMailboxSeed();
    config()->set('mail.mailers.smtp.username', 'no-reply@clupilot.com');
    config()->set('mail.from.address', 'hello@example.com');

    loadMailboxSeedMigration()->up();

    $box = Mailbox::findByKey('no-reply');

    expect($box->address)->toBe('no-reply@clupilot.com')
        ->and($box->username)->toBeNull();
});

it('seeds display_name from mail.from.name instead of a hardcoded CluPilot', function () {
    clearMailboxSeed();
    config()->set('mail.mailers.smtp.username', 'no-reply@clupilot.com');
    config()->set('mail.from.name', 'Example Org Support');

    loadMailboxSeedMigration()->up();

    expect(Mailbox::findByKey('no-reply')->display_name)->toBe('Example Org Support')
        ->and(Mailbox::findByKey('support')->display_name)->toBe('Example Org Support');
});

it('still derives the placeholder domain from the SMTP login, not from mail.from.address', function () {
    // The two can point at different domains. Only the login's domain is
    // proven to accept mail for this install — switching the placeholder
    // domain to mail.from.address's domain could seed support@/billing@/etc.
    // on a domain nobody could ever turn into a working mailbox.
    clearMailboxSeed();
    config()->set('mail.mailers.smtp.username', 'no-reply@clupilot.com');
    config()->set('mail.from.address', 'hello@clupilot.local');

    loadMailboxSeedMigration()->up();

    expect(Mailbox::findByKey('support')->address)->toBe('support@clupilot.com')
        ->and(Mailbox::findByKey('billing')->address)->toBe('billing@clupilot.com');
});

// --- Codex R15#4, P1b: an existing SMTP relay with a host/from address but
// no username or password (a trusted local or private-network relay) must
// still be taken over for real — not seeded as a placeholder — with
// authenticates recording that it needs no password, distinct from a
// placeholder that simply has not had one typed in yet.

it('takes over a relay with a real host but no username or password, marking it as not authenticating', function () {
    clearMailboxSeed();
    config()->set('mail.mailers.smtp.host', 'relay.internal.example');
    config()->set('mail.mailers.smtp.username', '');
    config()->set('mail.mailers.smtp.password', '');
    config()->set('mail.from.address', 'no-reply@clupilot.local');

    loadMailboxSeedMigration()->up();

    $box = Mailbox::findByKey('no-reply');

    // The real From address, not the 'no-reply@clupilot.com' placeholder —
    // before this fix, $isConfigured required a username and this relay has
    // none, so it would have been seeded as a placeholder and the new
    // transport would refuse to send for it, exactly the outage the finding
    // describes.
    expect($box->address)->toBe('no-reply@clupilot.local')
        ->and($box->authenticates)->toBeFalse()
        ->and($box->isConfigured())->toBeTrue()
        // Null, not '': an empty username column must read as "no separate
        // login" through Mailbox::smtpUsername()'s own fallback, not as a
        // literal empty-string login distinct from having none at all.
        ->and($box->username)->toBeNull();
});

it('marks authenticates true, and the mailbox as needing a password, when only a username was resolved', function () {
    // The other half of the distinction the brief draws: a username with no
    // password is an operator who started typing, not one who chose no
    // auth. authenticates must stay true so isConfigured() keeps asking for
    // a password rather than reading this as a deliberately open relay.
    clearMailboxSeed();
    config()->set('mail.mailers.smtp.host', 'relay.internal.example');
    config()->set('mail.mailers.smtp.username', 'no-reply@clupilot.com');
    config()->set('mail.mailers.smtp.password', '');

    loadMailboxSeedMigration()->up();

    $box = Mailbox::findByKey('no-reply');

    expect($box->authenticates)->toBeTrue()
        ->and($box->getRawOriginal('password'))->toBeNull()
        ->and($box->isConfigured())->toBeFalse();
});

it('leaves the four placeholder mailboxes authenticating by default, even when the real account does not', function () {
    // authenticates=false must land ONLY on the account the takeover
    // actually resolved — support@/billing@/office@/info@ are fabricated
    // placeholder addresses with no real credentials behind them at all, and
    // must keep asking for a password (authenticates=true, the column's own
    // default) so they read as "not yet set up", not as "deliberately open".
    // Getting this backwards would let a made-up placeholder address pass
    // isConfigured() and be handed to MailboxTransport for real.
    clearMailboxSeed();
    config()->set('mail.mailers.smtp.host', 'relay.internal.example');
    config()->set('mail.mailers.smtp.username', '');
    config()->set('mail.mailers.smtp.password', '');
    config()->set('mail.from.address', 'no-reply@clupilot.local');

    loadMailboxSeedMigration()->up();

    foreach (['support', 'billing', 'office', 'info'] as $key) {
        $box = Mailbox::findByKey($key);

        expect($box->authenticates)->toBeTrue("{$key} should still default to authenticates=true")
            ->and($box->isConfigured())->toBeFalse("{$key} should still read as unconfigured");
    }
});

it('never seeds a blank address for the real account when neither a username nor a from address resolved', function () {
    // A real host alone is not enough to know WHO mail should claim to be
    // from — without this guard, a relay with a host but no username, no
    // password, AND no MAIL_FROM_ADDRESS would resolve an empty address
    // string. Falling back to the placeholder shape here (as if the account
    // were not taken over at all) is safer than writing a blank address.
    clearMailboxSeed();
    config()->set('mail.mailers.smtp.host', 'relay.internal.example');
    config()->set('mail.mailers.smtp.username', '');
    config()->set('mail.mailers.smtp.password', '');
    config()->set('mail.from.address', 'hello@example.com'); // config/mail.php's own unconfigured placeholder

    loadMailboxSeedMigration()->up();

    $box = Mailbox::findByKey('no-reply');

    expect($box->address)->toBe('no-reply@clupilot.com')
        ->and($box->address)->not->toBe('');
});

it('carries the vault password across as ciphertext, never decrypting and re-encrypting it', function () {
    clearMailboxSeed();
    config()->set('mail.mailers.smtp.username', 'no-reply@clupilot.com');

    $ciphertext = app(SecretCipher::class)->encrypt('rotated-through-the-console');
    DB::table('app_secrets')->insert([
        'key' => 'mail.password', 'value' => $ciphertext, 'created_at' => now(), 'updated_at' => now(),
    ]);

    loadMailboxSeedMigration()->up();

    $stored = DB::table('mailboxes')->where('key', 'no-reply')->value('password');

    // Byte-for-byte, not just "decrypts to the same plaintext": AES-CBC's
    // random IV means re-encrypting the SAME plaintext produces a DIFFERENT
    // ciphertext string, so this is what actually discriminates "carried
    // as-is" from "decrypted and re-encrypted" — the migration's own claim,
    // never exercised by any test before this one.
    expect($stored)->toBe($ciphertext)
        ->and(app(SecretCipher::class)->decrypt($stored))->toBe('rotated-through-the-console');
});

it('leaves the vault row in the database even after a normal, fully successful carry-over', function () {
    clearMailboxSeed();
    config()->set('mail.mailers.smtp.username', 'no-reply@clupilot.com');

    $ciphertext = app(SecretCipher::class)->encrypt('rotated-through-the-console');
    DB::table('app_secrets')->insert([
        'key' => 'mail.password', 'value' => $ciphertext, 'created_at' => now(), 'updated_at' => now(),
    ]);

    loadMailboxSeedMigration()->up();

    expect(DB::table('app_secrets')->where('key', 'mail.password')->value('value'))->toBe($ciphertext);
});

it('does not destroy a stored vault password when nothing at all is configured, because it never lands anywhere', function () {
    // Reproduces the Critical finding: $isConfigured is false for every key
    // when nothing was resolved, so the carry-over never runs — the old code
    // deleted the vault row unconditionally regardless.
    //
    // Host pinned to the placeholder explicitly, not left at this
    // container's real .env value: Codex R15#4, P1b made $isConfigured
    // sensitive to the HOST too (a real host with no username is now a
    // legitimate unauthenticated relay, seeded for real — see the block of
    // tests above this one), so this scenario has to say "nothing
    // configured" on every signal itself rather than only blanking the
    // username and relying on the rest to already be blank by accident.
    clearMailboxSeed();
    config()->set('mail.mailers.smtp.host', '127.0.0.1');
    config()->set('mail.mailers.smtp.username', '');

    $ciphertext = app(SecretCipher::class)->encrypt('nowhere-to-go');
    DB::table('app_secrets')->insert([
        'key' => 'mail.password', 'value' => $ciphertext, 'created_at' => now(), 'updated_at' => now(),
    ]);

    loadMailboxSeedMigration()->up();

    expect(DB::table('app_secrets')->where('key', 'mail.password')->value('value'))->toBe($ciphertext)
        ->and(Mailbox::findByKey('no-reply')->getRawOriginal('password'))->toBeNull();
});

it('survives a migrate, rollback, migrate-again cycle without losing the credential', function () {
    // Reproduces the other Critical finding: down() deletes the mailbox
    // rows, which is where the ONLY copy of the password would end up if the
    // vault row had been deleted (the old behaviour) during the first up().
    clearMailboxSeed();
    config()->set('mail.mailers.smtp.username', 'no-reply@clupilot.com');

    $ciphertext = app(SecretCipher::class)->encrypt('rotated-through-the-console');
    DB::table('app_secrets')->insert([
        'key' => 'mail.password', 'value' => $ciphertext, 'created_at' => now(), 'updated_at' => now(),
    ]);

    $migration = loadMailboxSeedMigration();
    $migration->up();
    $migration->down();
    $migration->up();

    expect(DB::table('mailboxes')->where('key', 'no-reply')->value('password'))->toBe($ciphertext);
});

it('is safe to run twice — a migrations-table drift must not crash on the unique key', function () {
    // The real baseline from RefreshDatabase's own migrate is already there
    // (nothing cleared it) — calling up() again must update those rows in
    // place, not attempt a second INSERT under the same key.
    //
    // A plain try/catch, NOT expect(fn () => ...)->not->toThrow(Throwable::class):
    // verified directly that the latter is not reliable here — a closure that
    // unconditionally throws still passes ->not->toThrow(Throwable::class) in
    // this Pest version, which would have made this test pass whether or not
    // the migration actually crashed.
    $threw = null;
    try {
        loadMailboxSeedMigration()->up();
    } catch (Throwable $e) {
        $threw = $e;
    }

    expect($threw)->toBeNull();
    // 'instance-relay' comes from a migration of its own (the shared sending
    // account the CUSTOMER instances use — see InstanceRelaySeedMigrationTest),
    // and is listed here because nothing cleared the real baseline: this
    // migration must leave it exactly where it found it.
    expect(Mailbox::query()->pluck('key')->sort()->values()->all())
        ->toBe(['billing', 'info', 'instance-relay', 'no-reply', 'office', 'support']);
});

it('un-seeds on rollback, and does not leave the settings cache holding what it just deleted', function () {
    // Against the real baseline again — this is what a real rollback tears
    // down. Warms the cache first the same way a real request would, so a
    // migration that bypassed Settings::forget() (a raw DB delete) and left
    // the Cache::forever() entry behind would still read back the old value.
    expect(Settings::get('mail.purpose.system'))->toBe('no-reply');

    loadMailboxSeedMigration()->down();

    // Its own five rows, and only those: 'instance-relay' belongs to another
    // migration and must survive this one's rollback — tearing down a
    // neighbour's row would leave the customer instances' sending account
    // gone with no migration recording it.
    expect(Mailbox::query()->pluck('key')->all())->toBe(['instance-relay'])
        ->and(Settings::get('mail.purpose.system'))->toBeNull()
        ->and(Settings::get('mail.host'))->toBeNull();
});

it('prints an operator-visible warning when SECRETS_KEY is missing and a .env password had to be skipped', function () {
    // No vault row exists here — nothing at all ends up in mailboxes.password
    // for this key, so "was NOT copied" is the true statement. The sibling
    // test below is the other half: a vault row DOES exist, the password IS
    // carried across regardless of $canEncrypt, and the warning must say a
    // different, equally true thing rather than reuse this wording.
    clearMailboxSeed();
    config()->set('admin_access.secrets_key', ''); // canEncrypt false
    config()->set('mail.mailers.smtp.username', 'no-reply@clupilot.com');
    config()->set('mail.mailers.smtp.password', 'dummy-nicht-echt');

    ob_start();
    loadMailboxSeedMigration()->up();
    $output = ob_get_clean();

    expect($output)->toContain('SECRETS_KEY')
        ->toContain('no-reply@clupilot.com')
        ->toContain('was NOT copied')
        ->and(DB::table('mailboxes')->where('key', 'no-reply')->value('password'))->toBeNull();
});

it('says a stored password was carried over, not that nothing was copied, when a vault row already exists and SECRETS_KEY is missing', function () {
    // Deferred #4: the vault-carry block a few lines below this branch runs
    // UNCONDITIONALLY on $storedPassword !== null — it needs no new
    // encryption, since the value is already ciphertext — so this mailbox
    // ends up WITH a working password even though $canEncrypt is false. The
    // old message said "was NOT copied into the mailbox table" regardless,
    // which is true of the .env value and false of the outcome: an operator
    // reading it mid mail-outage would be sent to re-enter a password that
    // is already sitting in the column.
    clearMailboxSeed();
    config()->set('mail.mailers.smtp.username', 'no-reply@clupilot.com');
    config()->set('mail.mailers.smtp.password', 'dummy-nicht-echt');

    // Encrypted under the valid key beforeEach() set up — standing in for a
    // password stored earlier, through the console, while SECRETS_KEY still
    // existed. THEN it goes missing, which is the state this migration
    // actually runs under below.
    $ciphertext = app(SecretCipher::class)->encrypt('rotated-through-the-console');
    DB::table('app_secrets')->insert([
        'key' => 'mail.password', 'value' => $ciphertext, 'created_at' => now(), 'updated_at' => now(),
    ]);

    config()->set('admin_access.secrets_key', ''); // canEncrypt false, now

    ob_start();
    loadMailboxSeedMigration()->up();
    $output = ob_get_clean();

    expect($output)->toContain('SECRETS_KEY')
        ->toContain('no-reply@clupilot.com')
        ->toContain('carried over')
        ->not->toContain('was NOT copied')
        ->and(DB::table('mailboxes')->where('key', 'no-reply')->value('password'))->toBe($ciphertext);
});

it('stays silent when there is no .env password to skip in the first place', function () {
    clearMailboxSeed();
    config()->set('admin_access.secrets_key', ''); // canEncrypt false
    config()->set('mail.mailers.smtp.username', 'no-reply@clupilot.com');
    config()->set('mail.mailers.smtp.password', '');

    ob_start();
    loadMailboxSeedMigration()->up();
    $output = ob_get_clean();

    expect($output)->toBe('');
});

it('Settings::forget removes the row and does not leave a stale cache entry behind', function () {
    Settings::set('probe.key', 'value');
    expect(Settings::get('probe.key'))->toBe('value');

    Settings::forget('probe.key');

    expect(Settings::get('probe.key'))->toBeNull()
        ->and(DB::table('app_settings')->where('key', 'probe.key')->exists())->toBeFalse();
});
