<?php

use App\Services\Dns\FakeHetznerDnsClient;
use App\Services\Dns\HttpHetznerDnsClient;
use App\Support\Settings;
use Illuminate\Support\Facades\Http;

/**
 * Die alte DNS-API (dns.hetzner.com/api/v1) ist abgeschaltet und leitet auf die
 * Weboberfläche um; die Verwaltung liegt seit dem 7. Oktober 2025 in der
 * Cloud-Konsole. Hier steht das neue Modell fest: Bearer statt Auth-API-Token,
 * api.hetzner.cloud/v1 statt dns.hetzner.com/api/v1, und RRSets statt einzelner
 * Records — adressiert über {name}/{typ} statt über eine Record-ID.
 *
 * SICHERHEITSAUFLAGE (wie in ActiveChecksTest): jeder Test hier benutzt
 * Http::fake() und darf die echte Hetzner-API oder die echte Zone unter keinen
 * Umständen berühren. Deshalb eine erfundene Zone statt des .env-Werts dieser
 * Installation.
 */
beforeEach(function () {
    Settings::set('provisioning.dns_zone', 'probe.example');
    config()->set('provisioning.dns.token', 'cloud-token');
    Http::preventStrayRequests();
});

it('creates an A rrset through the cloud API and returns its {name}/{type} id', function () {
    Http::fake([
        'api.hetzner.cloud/v1/zones/probe.example/rrsets' => Http::response([
            'rrset' => ['id' => 'berger/A', 'name' => 'berger', 'type' => 'A'],
        ], 201),
    ]);

    $id = (new HttpHetznerDnsClient)->upsertRecord('berger.probe.example', 'A', '203.0.113.9');

    // Der zurückgegebene Wert ist genau das, was deleteRecord() später wieder
    // auseinandernehmen muss — und genau das, was die API als `rrset.id`
    // ausliefert. Eine Zusammensetzung aus Name und Typ, dokumentiert in
    // cloud.spec.json und am Live-Konto nachgemessen.
    expect($id)->toBe('berger/A');

    Http::assertSent(function ($request) {
        return $request->method() === 'POST'
            && $request->url() === 'https://api.hetzner.cloud/v1/zones/probe.example/rrsets'
            // Bearer, nicht mehr `Auth-API-Token` — der alte Kopf wird von der
            // Cloud-API schlicht ignoriert und der Aufruf als unangemeldet
            // abgewiesen.
            && $request->hasHeader('Authorization', 'Bearer cloud-token')
            && ! $request->hasHeader('Auth-API-Token')
            && $request['type'] === 'A'
            && $request['records'] === [['value' => '203.0.113.9']];
    });
});

it('sends the name without the zone suffix', function () {
    // Falle 1, und sie ist still: die API weist einen Namen, der auf den
    // Zonennamen endet, NICHT zurück. Am Live-Konto gemessen — `POST` mit
    // `name: probe.probe.example` kam mit 201 zurück und hätte
    // `probe.probe.example.probe.example` in die Zone gelegt. Kein Fehler,
    // keine Meldung, nur eine Adresse, die niemand auflöst. Diese Zusicherung
    // ist das Einzige, was den Fall überhaupt bemerkt.
    Http::fake(['api.hetzner.cloud/*' => Http::response(['rrset' => ['id' => 'berger/A']], 201)]);

    (new HttpHetznerDnsClient)->upsertRecord('berger.probe.example', 'A', '203.0.113.9');

    Http::assertSent(fn ($request) => $request['name'] === 'berger');
});

it('names the zone apex @ and lower-cases what it sends', function () {
    Http::fake(['api.hetzner.cloud/*' => Http::response(['rrset' => ['id' => '@/A']], 201)]);

    $client = new HttpHetznerDnsClient;

    expect($client->upsertRecord('probe.example', 'A', '203.0.113.9'))->toBe('@/A');
    Http::assertSent(fn ($request) => $request['name'] === '@');

    $client->upsertRecord('BERGER.Probe.Example', 'A', '203.0.113.9');
    Http::assertSent(fn ($request) => $request['name'] === 'berger');
});

it('replaces the records of an rrset that already exists', function () {
    // Am Live-Konto gemessen: ein zweites POST auf einen vorhandenen RRSet
    // antwortet mit 409 `uniqueness_error`. Das Ersetzen ist eine eigene
    // Aktion — und mit ihr wird das Schreiben idempotent, was der alte Client
    // mit Suchen-und-Aktualisieren über hundert Seiten nachbauen musste.
    Http::fake([
        'api.hetzner.cloud/v1/zones/probe.example/rrsets' => Http::response([
            'error' => ['code' => 'uniqueness_error', 'message' => 'RRSet(s) already exist(s)'],
        ], 409),
        'api.hetzner.cloud/v1/zones/probe.example/rrsets/berger/A/actions/set_records' => Http::response([
            'rrset' => ['id' => 'berger/A'],
        ], 201),
    ]);

    $id = (new HttpHetznerDnsClient)->upsertRecord('berger.probe.example', 'A', '203.0.113.10');

    expect($id)->toBe('berger/A');

    Http::assertSent(fn ($request) => $request->method() === 'POST'
        && $request->url() === 'https://api.hetzner.cloud/v1/zones/probe.example/rrsets/berger/A/actions/set_records'
        && $request['records'] === [['value' => '203.0.113.10']]);
});

it('refuses an fqdn that does not sit in the configured zone', function () {
    // Die Kehrseite derselben stillen Annahme: `fsn-01.clupilot.com` in der
    // Zone `probe.example` ist kein Tippfehler, den die API abfängt — sie legt
    // `fsn-01.clupilot.com.probe.example` an und meldet Erfolg. Was hier nicht
    // hineingehört, wird abgelehnt, statt veröffentlicht zu werden.
    Http::fake();

    expect(fn () => (new HttpHetznerDnsClient)->upsertRecord('fsn-01.clupilot.com', 'A', '203.0.113.9'))
        ->toThrow(RuntimeException::class);

    Http::assertNothingSent();
});

it('deletes an rrset by its name and type', function () {
    Http::fake(['api.hetzner.cloud/*' => Http::response([], 201)]);

    (new HttpHetznerDnsClient)->deleteRecord('berger/A');

    Http::assertSent(fn ($request) => $request->method() === 'DELETE'
        && $request->url() === 'https://api.hetzner.cloud/v1/zones/probe.example/rrsets/berger/A');
});

it('treats an already-deleted rrset as done', function () {
    // Ein verlorener Rücklauf oder ein Wiederholungslauf nach einem späteren
    // Fehlschlag darf einen Eintrag nicht unaufräumbar machen — und seinen
    // Host nicht unlöschbar. Am Live-Konto gemessen: 404 `not_found`.
    Http::fake([
        'api.hetzner.cloud/*' => Http::response([
            'error' => ['code' => 'not_found', 'message' => 'RRSet(s) not found'],
        ], 404),
    ]);

    (new HttpHetznerDnsClient)->deleteRecord('berger/A');
})->throwsNoExceptions();

it('hands back the same id from the fake as from the real client', function () {
    // Sonst beweist jeder Test, der über FakeHetznerDnsClient läuft, das
    // Gegenteil von dem, was er behauptet: `rec-1` wandert in
    // `hosts.dns_record_id`, PurgeHost und clupilot:prune-host-dns räumen damit
    // im Test sauber auf — und im Betrieb steht dort `{name}/{typ}` und der
    // echte Client wirft. Ein Fake, der eine andere Sprache spricht als das
    // Original, ist eine grüne Anzeige über einer roten Grundlage.
    Http::fake(['api.hetzner.cloud/*' => Http::response(['rrset' => ['id' => 'berger/A']], 201)]);

    $echt = (new HttpHetznerDnsClient)->upsertRecord('berger.probe.example', 'A', '203.0.113.9');
    $fake = (new FakeHetznerDnsClient)->upsertRecord('berger.probe.example', 'A', '203.0.113.9');

    expect($fake)->toBe($echt);
});

it('refuses in the fake the same out-of-zone fqdn the real client refuses', function () {
    expect(fn () => (new FakeHetznerDnsClient)->upsertRecord('fsn-01.clupilot.com', 'A', '203.0.113.9'))
        ->toThrow(RuntimeException::class);
});

it('lets an id from the old record API pass without building a broken path', function () {
    // Alte Record-IDs sind mit der Cloud-Konsole nicht kompatibel. Sie ergäben
    // `/rrsets/rec-123/` mit leerem Typ — einen Aufruf, der irgendetwas anderes
    // trifft und dabei erfolgreich aussieht. Also wird nichts geschickt.
    //
    // Geworfen wird aber auch nicht, und das war die Korrektur aus dem
    // Codex-Review (P1): PurgeHost ruft deleteRecord(), BEVOR es die Zeile
    // löscht, die die ID hält. Ein Wurf hier machte jeden Host mit einer
    // Alt-ID dauerhaft unlöschbar — jeder Versuch stirbt an derselben Zeile.
    // Der Eintrag bleibt in der Zone stehen; gemeldet wird er im Log
    // (siehe CodexFixRoundTest), nicht verschwiegen.
    Http::fake();

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

    Http::assertNothingSent();
});
