<?php

use App\Models\DnsRecord;
use App\Models\Host;
use App\Models\Instance;
use App\Models\Operator;
use App\Services\Dns\HttpHetznerDnsClient;
use App\Services\Dns\LegacyRecordIds;
use App\Services\Secrets\SecretVault;
use App\Support\OperatingMode;
use App\Support\Settings;
use App\Support\StripeWebhookSecret;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Log;

/**
 * Fix-Runde zum Codex-Review vom 31.7.2026, zwei P1.
 */
beforeEach(function () {
    config()->set('admin_access.secrets_key', 'base64:'.base64_encode(random_bytes(32)));
});

// ---- P1 (1): Der Signaturschlüssel darf NICHT über die Modusgrenze fallen ----

it('never lets a stored live signing secret stand in for an empty test slot', function () {
    // Jeder andere Tresorwert bezeichnet dasselbe Konto in beiden Modi und darf
    // deshalb vom Live-Platz einspringen. Dieser nicht: Stripe stellt je Modus
    // ein EIGENES Signaturgeheimnis aus. Ohne diese Trennung prüft der
    // Testbetrieb seine Ereignisse mit dem Live-Schlüssel — sie scheitern alle,
    // und die Konsole meldet den Platz trotzdem als belegt.
    config()->set('services.stripe.webhook_secret', '');
    config()->set('services.stripe.webhook_secret_test', '');

    app(SecretVault::class)->put(
        'stripe.webhook_secret', 'whsec_live_only', Operator::factory()->create(), OperatingMode::Live
    );

    OperatingMode::set(OperatingMode::Test);

    expect(StripeWebhookSecret::current())->toBe('')
        ->and(app(SecretVault::class)->source('stripe.webhook_secret'))->toBe('none');
});

it('still uses the test line from the server file when only the live slot is stored', function () {
    // Die Sperre gilt dem gespeicherten Platz, nicht der Serverdatei: dort
    // steht je Modus eine eigene Zeile, und die ist für diesen Modus richtig.
    config()->set('services.stripe.webhook_secret_test', 'whsec_test_env');

    app(SecretVault::class)->put(
        'stripe.webhook_secret', 'whsec_live_only', Operator::factory()->create(), OperatingMode::Live
    );

    OperatingMode::set(OperatingMode::Test);

    expect(StripeWebhookSecret::current())->toBe('whsec_test_env')
        ->and(app(SecretVault::class)->source('stripe.webhook_secret'))->toBe('environment');
});

it('leaves the ordinary entries falling back as they did', function () {
    // Die Trennung gilt NUR dem Signaturschlüssel. Ein DNS-Token bezeichnet
    // dasselbe Hetzner-Konto in beiden Modi — dort ist der Rückfall gewollt und
    // ausdrücklich getestet (SecretVaultModeTest).
    app(SecretVault::class)->put(
        'dns.token', 'token_live', Operator::factory()->create(), OperatingMode::Live
    );

    OperatingMode::set(OperatingMode::Test);

    expect(app(SecretVault::class)->get('dns.token'))->toBe('token_live')
        ->and(app(SecretVault::class)->source('dns.token'))->toBe('stored_live');
});

// ---- P1 (2): Alte Record-IDs dürfen keinen Host unlöschbar machen ----

it('does not make a host unpurgeable because its record id predates the cloud API', function () {
    // Mein Fehler im Review: ich hatte auf DIESER Datenbank null Zeilen gezählt
    // und daraus geschlossen, es gebe nirgends welche. Der Live-Server ist eine
    // andere Installation. Eine Alt-ID muss den Abbau deshalb durchlassen —
    // sonst bleibt der Host für immer im Pool, weil PurgeHost bei jedem Versuch
    // an derselben Zeile stirbt.
    Settings::set('provisioning.dns_zone', 'probe.example');
    Http::preventStrayRequests();
    Http::fake();

    // Kein `toThrow(Throwable::class)` — Pest prüft bei einer Schnittstelle
    // die MELDUNG, nicht den Typ, und eine Verneinung davon ist immer grün.
    // Hier zählt schlicht, dass der Aufruf zurückkommt.
    (new HttpHetznerDnsClient)->deleteRecord('rec-123');

    // Und zwar OHNE einen Aufruf zu bauen, der irgendetwas anderes trifft.
    Http::assertNothingSent();
});

it('says the leftover record out loud instead of swallowing it', function () {
    // Durchlassen heißt nicht verschweigen: der Eintrag bleibt in der Zone
    // stehen, und die einzige Stelle, an der das noch auffallen kann, ist das
    // Log. Ein stiller Rücksprung wäre die Attrappe, vor der R19 warnt.
    Settings::set('provisioning.dns_zone', 'probe.example');
    Http::fake();

    Log::shouldReceive('warning')
        ->once()
        ->withArgs(fn (string $message, array $context = []) => str_contains($message, 'legacy')
            && ($context['record_id'] ?? null) === 'rec-123');

    (new HttpHetznerDnsClient)->deleteRecord('rec-123');
});

it('migrates the instance records it can address to the new format', function () {
    // `dns_records` trägt fqdn UND typ neben der ID — daraus lässt sich der
    // RRSet-Name exakt bilden. Für Hosts geht das nicht (dort steht nur der
    // Kurzname), die bleiben der toleranten Löschung oben überlassen.
    Settings::set('provisioning.dns_zone', 'probe.example');

    $instance = Instance::factory()->create();
    $legacy = DnsRecord::query()->create([
        'instance_id' => $instance->id,
        'provider' => 'hetzner',
        'record_id' => 'rec-legacy-42',
        'fqdn' => 'berger.probe.example',
        'type' => 'A',
        'value' => '203.0.113.9',
    ]);

    app(LegacyRecordIds::class)->migrate();

    expect($legacy->fresh()->record_id)->toBe('berger/A');
});

it('leaves a record it cannot address alone rather than guessing', function () {
    Settings::set('provisioning.dns_zone', 'probe.example');

    $instance = Instance::factory()->create();
    $foreign = DnsRecord::query()->create([
        'instance_id' => $instance->id,
        'provider' => 'hetzner',
        'record_id' => 'rec-foreign',
        'fqdn' => 'irgendwo.fremde-zone.test',
        'type' => 'A',
        'value' => '203.0.113.9',
    ]);

    app(LegacyRecordIds::class)->migrate();

    expect($foreign->fresh()->record_id)->toBe('rec-foreign');
});

it('counts as done for a host whose legacy record could not be removed', function () {
    Settings::set('provisioning.dns_zone', 'probe.example');
    $s = fakeServices();
    $host = Host::factory()->active()->create(['dns_record_id' => 'rec-legacy-99']);

    // Der Fake spricht dieselbe Sprache wie der echte Client — auch hier.
    $s['dns']->deleteRecord($host->dns_record_id);
})->throwsNoExceptions();
