<?php

// tests/Feature/Security/SecurityBlockMailTest.php

use App\Mail\SecurityBlockMail;
use App\Models\Host;
use App\Models\Instance;
use App\Models\Mailbox;
use App\Models\SecurityBlock;
use App\Services\Mail\MailPurpose;
use App\Services\Mail\MailRoute;
use App\Services\Security\BlockAddress;
use App\Support\Settings;
use Illuminate\Support\Facades\Mail;

// Blank slate statt der migrationseigenen fuenf Postfaecher (no-reply,
// support, billing, office, info): der letzte Test unten legt selbst eines
// mit dem Schluessel 'no-reply' an, und das kollidiert sonst mit
// mailboxes.key's Unique-Constraint gegen die von RefreshDatabase stehen
// gelassene Saat (siehe MailRoutingTest, derselbe Helfer aus tests/Pest.php).
beforeEach(fn () => clearMailboxSeed());

it('schickt dem Inhaber eine Mail, wenn seine Instanz gesperrt wird', function () {
    Mail::fake();
    $instance = Instance::factory()->create(['status' => 'active', 'vmid' => 101]);

    app(BlockAddress::class)->forInstance($instance, '203.0.113.7', 12);

    Mail::assertQueued(SecurityBlockMail::class, fn ($m) => $m->hasTo($instance->customer->email));
});

it('schickt hoechstens eine Mail je Instanz und Stunde', function () {
    // Ein Angreifer, der Adressen durchwechselt, erzeugt sonst zwanzig Mails —
    // und die zwanzigste liest niemand mehr.
    Mail::fake();
    $instance = Instance::factory()->create(['status' => 'active', 'vmid' => 101]);

    app(BlockAddress::class)->forInstance($instance, '203.0.113.7', 12);
    app(BlockAddress::class)->forInstance($instance, '203.0.113.8', 12);
    app(BlockAddress::class)->forInstance($instance, '203.0.113.9', 12);

    Mail::assertQueuedCount(1);
});

it('laesst die Sperre stehen, wenn die Mail scheitert', function () {
    // Zustellung ist nicht die Bedingung fuer Schutz. Eine Sperre, die von einem
    // kaputten Postfach abhinge, waere genau dann weg, wenn ohnehin schon etwas
    // im Argen liegt.
    Mail::shouldReceive('mailer')->andThrow(new RuntimeException('Postfach kaputt'));

    $instance = Instance::factory()->create(['status' => 'active', 'vmid' => 101]);
    $block = app(BlockAddress::class)->forInstance($instance, '203.0.113.7', 12);

    expect($block)->not->toBeNull()
        ->and(SecurityBlock::count())->toBe(1);
});

it('drosselt nicht, wenn gar keine Mail hinausging', function () {
    // Sonst schweigt eine Instanz eine volle Stunde, obwohl NULL Meldungen
    // ankamen — genau dann, wenn ohnehin schon etwas klemmt.
    Mail::fake();

    // Ein Postfach, das den ERSTEN Aufruf von Mail::to() wirft und danach an
    // die echte Fake-Implementierung durchreicht — so laesst sich "der erste
    // Versandversuch scheitert, der zweite geht durch" nachstellen, ohne die
    // Zusicherung am Ende (Mail::assertQueued) aufzugeben.
    $fake = Mail::getFacadeRoot();
    Mail::swap(new class($fake)
    {
        private int $calls = 0;

        public function __construct(private readonly object $inner) {}

        public function __call(string $name, array $arguments): mixed
        {
            if ($name === 'to' && ++$this->calls === 1) {
                throw new RuntimeException('Postfach kaputt');
            }

            return $this->inner->{$name}(...$arguments);
        }
    });

    $instance = Instance::factory()->create(['status' => 'active', 'vmid' => 101]);

    // erster Versuch: Versand scheitert
    app(BlockAddress::class)->forInstance($instance, '203.0.113.7', 12);
    // zweiter Versuch kurz darauf: Versand geht durch
    app(BlockAddress::class)->forInstance($instance, '203.0.113.8', 12);

    // Die zweite Sperre muss eine Mail erzeugen, obwohl die erste eine
    // Stunde 'verbraucht' zu haben schien.
    Mail::assertQueued(SecurityBlockMail::class);
});

it('geht standardmaessig aus dem System-Postfach und folgt der Wegwahl', function () {
    Mailbox::factory()->create(['key' => 'no-reply', 'address' => 'no-reply@clupilot.com', 'active' => true]);
    $info = Mailbox::factory()->create(['key' => 'info', 'active' => true]);
    Settings::set(MailPurpose::settingKey(MailPurpose::SYSTEM), 'no-reply');

    $block = SecurityBlock::factory()->create();

    expect((new SecurityBlockMail($block))->envelope()->from->address)->toBe('no-reply@clupilot.com');

    Settings::set(MailRoute::settingKey('security-block'), 'info');

    expect((new SecurityBlockMail($block))->envelope()->from->address)->toBe($info->address);
});

// ---- Host-Sperren: an jeden aktiven Betreiber mit hosts.manage (Koordinator-Entscheidung, Fix-Runde 1) ----

it('schickt bei einer Host-Sperre an jeden Betreiber, der Hosts verwalten darf', function () {
    Mail::fake();
    // Owner und Admin haben hosts.manage (seed_roles_and_permissions), Support
    // nicht — siehe database/migrations/2026_07_25_133900_seed_roles_and_permissions.php.
    $darf = operator('Admin');
    $darfNicht = operator('Support');
    $host = Host::factory()->create();

    app(BlockAddress::class)->forHost($host, '203.0.113.7', 12);

    // Beide Haelften: der Zustaendige bekommt sie, der Unzustaendige nicht.
    Mail::assertQueued(SecurityBlockMail::class, fn ($m) => $m->hasTo($darf->email));
    Mail::assertNotQueued(SecurityBlockMail::class, fn ($m) => $m->hasTo($darfNicht->email));
});

it('nennt in der Host-Mail den Host und fuehrt NICHT ins Portal', function () {
    // Der Betreiber hat im Portal kein Konto, und Host-Sperren erscheinen dort
    // nie — ein Knopf auf portal.security fuehrt ihn also zwangslaeufig ins
    // Leere. Und ohne den Hostnamen stuende in einer Meldung an jemanden mit
    // einem Dutzend Maschinen nicht, welche gemeint ist.
    $host = Host::factory()->active()->create(['name' => 'pve-fsn-07']);
    $block = SecurityBlock::factory()->forHost($host)->create(['ip' => '203.0.113.42']);

    $html = (new SecurityBlockMail($block))->render();

    expect($html)->toContain('pve-fsn-07')
        ->and($html)->toContain('203.0.113.42')
        ->and($html)->toContain(route('admin.hosts.show', $host))
        ->and($html)->not->toContain(route('portal.security'))
        // Und der Kundentext ist weg, nicht bloss ergaenzt.
        ->and($html)->not->toContain(__('security.mail_intro', ['ip' => $block->ip, 'attempts' => $block->attempts]));

    expect((new SecurityBlockMail($block))->envelope()->subject)
        ->toBe(__('security.mail_subject_host', ['host' => 'pve-fsn-07']));
});

it('laesst die Instanz-Mail unveraendert im Portal landen', function () {
    // Die Gegenprobe: der neue Host-Zweig darf den bestehenden Fall nicht
    // mitnehmen.
    $instance = Instance::factory()->create(['status' => 'active', 'vmid' => 101]);
    $block = SecurityBlock::factory()->create(['instance_id' => $instance->id, 'host_id' => null]);

    $html = (new SecurityBlockMail($block))->render();

    expect($html)->toContain(route('portal.security'))
        ->and($html)->toContain(__('security.mail_action'))
        ->and($html)->not->toContain(__('security.mail_action_host'));
});

it('sperrt auch dann, wenn es keinen zustaendigen Betreiber gibt', function () {
    Mail::fake();
    // Keine Betreiber mit hosts.manage — kein Empfaenger, kein Fehler, aber
    // sehr wohl eine Sperre. Schutz haengt nicht an Zustellung.
    $host = Host::factory()->create();

    expect(app(BlockAddress::class)->forHost($host, '203.0.113.7', 12))->not->toBeNull();
    Mail::assertNothingQueued();
});
