<?php

use App\Models\Host;
use App\Models\ProvisioningRun;
use App\Models\RunResource;
use App\Services\Secrets\SecretCipher;
use Illuminate\Contracts\Encryption\DecryptException;
use Illuminate\Support\Facades\Crypt;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;

it('auto-assigns a uuid and routes by it', function () {
    $host = Host::factory()->create();

    expect($host->uuid)->not->toBeNull()
        ->and($host->getRouteKeyName())->toBe('uuid');
});

it('encrypts api_token_ref under SECRETS_KEY, not APP_KEY', function () {
    // The column used Laravel's `encrypted` cast, i.e. APP_KEY — against
    // SecretVault's own written rule, which exists because rotating APP_KEY is
    // ordinary maintenance. One rotation would have made every host's Proxmox
    // token undecryptable in the same moment, discovered when provisioning
    // stopped rather than when the key changed.
    config()->set('admin_access.secrets_key', 'base64:'.base64_encode(random_bytes(32)));

    $host = Host::factory()->create(['api_token_ref' => 'automation@pve!clupilot=s3cr3t']);

    $raw = (string) DB::table('hosts')->where('id', $host->id)->value('api_token_ref');

    expect($raw)->not->toContain('s3cr3t')
        ->and($host->fresh()->api_token_ref)->toBe('automation@pve!clupilot=s3cr3t')
        // The mutation that matters: APP_KEY must NOT open it.
        ->and(fn () => Crypt::decryptString($raw))->toThrow(DecryptException::class)
        ->and(app(SecretCipher::class)->decrypt($raw))->toBe('automation@pve!clupilot=s3cr3t');
});

it('leaves a host without a token alone rather than needing a key for it', function () {
    // Most hosts have no token for most of onboarding, and a model that reached
    // for SECRETS_KEY to read a null column would make an unconfigured
    // installation unable to so much as list its hosts.
    config()->set('admin_access.secrets_key', '');

    $host = Host::factory()->create();

    expect($host->fresh()->api_token_ref)->toBeNull();
});

it('casts run context to an array and resolves the polymorphic subject', function () {
    $host = Host::factory()->create();
    $run = ProvisioningRun::factory()->forHost($host)->create(['context' => ['reboot_issued' => true]]);

    expect($run->fresh()->context)->toBe(['reboot_issued' => true])
        ->and($run->fresh()->subject->is($host))->toBeTrue();
});

it('merges and forgets context keys', function () {
    $run = ProvisioningRun::factory()->create(['context' => ['root_password' => 'x']]);

    $run->mergeContext(['wg_ip' => '10.66.0.5']);
    expect($run->fresh()->context)->toBe(['root_password' => 'x', 'wg_ip' => '10.66.0.5']);

    $run->forgetContext('root_password');
    expect($run->fresh()->context)->toBe(['wg_ip' => '10.66.0.5']);
});

it('logs append-only step events without an updated_at column', function () {
    $run = ProvisioningRun::factory()->create();
    $event = $run->events()->create(['step' => 'validate_host_input', 'attempt' => 0, 'outcome' => 'advanced']);

    expect($event->created_at)->not->toBeNull()
        ->and(Schema::hasColumn('provisioning_step_events', 'updated_at'))->toBeFalse();
});

it('keeps exactly one run_resource per kind on re-run (idempotency)', function () {
    $host = Host::factory()->create();
    $run = ProvisioningRun::factory()->forHost($host)->create();

    foreach (range(1, 2) as $_) {
        RunResource::firstOrCreate(
            ['run_id' => $run->id, 'kind' => 'wg_peer'],
            ['host_id' => $host->id, 'external_id' => 'pubkeyAAA'],
        );
    }

    expect(RunResource::where('run_id', $run->id)->where('kind', 'wg_peer')->count())->toBe(1);
});

it('computes free committable storage from total minus reserve', function () {
    $host = Host::factory()->create(['total_gb' => 1000, 'reserve_pct' => 15]);

    expect($host->freeGb())->toBe(850);
});
