<?php

use App\Models\Host;
use App\Models\Instance;
use App\Models\Order;
use App\Models\ProvisioningRun;
use App\Provisioning\RunRunner;
use Illuminate\Support\Facades\Queue;

/**
 * The estate that was sold a quota and never given one.
 *
 * ApplyStorageQuota runs in the `customer` and `plan-change` pipelines now, so
 * anything built or changed from here on is enforced on the machine. Everything
 * from before is not: `quota_gb` reached the instance row, Nextcloud was never
 * told, and those customers still have the whole disk whatever they paid for.
 * Nothing in the product could reach them, because both pipelines only run on a
 * build or a change and these machines are having neither.
 */

/** A finished, running instance whose Nextcloud has never been told anything. */
function unquotedInstance(array $attributes = []): Instance
{
    $host = Host::factory()->active()->create(['datacenter' => 'fsn', 'node' => 'pve']);
    $order = Order::factory()->withSubscription()->create(['datacenter' => 'fsn', 'plan' => 'business']);

    return Instance::factory()->create(array_merge([
        'order_id' => $order->id,
        'customer_id' => $order->customer_id,
        'host_id' => $host->id,
        'vmid' => 101,
        'status' => 'active',
        'quota_gb' => 500,
        'quota_applied_gb' => null,
    ], $attributes));
}

/** Carry out every quota run the command started. */
function runQuotaRuns(): void
{
    $runner = app(RunRunner::class);

    ProvisioningRun::query()->where('pipeline', 'quota')->get()
        ->each(fn (ProvisioningRun $run) => $runner->advance($run));
}

it('applies the allowance to an instance that never had one, and records that it did', function () {
    $services = fakeServices();
    Queue::fake();

    $instance = unquotedInstance();

    $this->artisan('clupilot:apply-quotas')->assertSuccessful();
    runQuotaRuns();

    expect($services['pve']->guestRan("config:app:set files default_quota --value='500 GB'"))->toBeTrue()
        ->and($instance->fresh()->quota_applied_gb)->toBe(500)
        ->and($instance->fresh()->quotaIsEnforced())->toBeTrue();
});

it('is a no-op the second time round', function () {
    $services = fakeServices();
    Queue::fake();

    unquotedInstance();

    $this->artisan('clupilot:apply-quotas')->assertSuccessful();
    runQuotaRuns();

    $callsAfterFirstSweep = count($services['pve']->guestCommands);

    $this->artisan('clupilot:apply-quotas')
        ->expectsOutputToContain('bereits gesetzt')
        ->assertSuccessful();
    runQuotaRuns();

    expect(ProvisioningRun::query()->where('pipeline', 'quota')->count())->toBe(1)
        ->and($services['pve']->guestCommands)->toHaveCount($callsAfterFirstSweep);
});

it('changes nothing at all in dry-run mode', function () {
    $services = fakeServices();
    Queue::fake();

    $instance = unquotedInstance();

    $this->artisan('clupilot:apply-quotas', ['--dry-run' => true])
        ->expectsOutputToContain('Probelauf')
        ->assertSuccessful();

    expect(ProvisioningRun::query()->where('pipeline', 'quota')->count())->toBe(0)
        ->and($services['pve']->guestCommands)->toBe([])
        ->and($instance->fresh()->quota_applied_gb)->toBeNull();
});

it('skips what it has no business touching, and says which', function () {
    fakeServices();
    Queue::fake();

    // Still being built: its own run is writing this machine.
    unquotedInstance(['status' => 'provisioning']);
    // Nothing to reach at all.
    unquotedInstance(['status' => 'failed', 'vmid' => null]);
    // No allowance on the row. Writing "0 GB" would lock the customer out of
    // their own files, which is worse than leaving what is there.
    unquotedInstance(['quota_gb' => 0]);
    // A service that has run out.
    unquotedInstance(['status' => 'ended']);

    $busy = unquotedInstance();
    ProvisioningRun::factory()->create([
        'subject_type' => Order::class,
        'subject_id' => $busy->order_id,
        'pipeline' => 'plan-change',
        'status' => ProvisioningRun::STATUS_RUNNING,
    ]);

    $this->artisan('clupilot:apply-quotas')
        ->expectsOutputToContain('keine erreichbare Maschine')
        ->expectsOutputToContain('kein Kontingent hinterlegt')
        ->expectsOutputToContain('anderer Lauf aktiv')
        ->assertSuccessful();

    expect(ProvisioningRun::query()->where('pipeline', 'quota')->count())->toBe(0);
});

it('re-applies an allowance that has changed since it was last enforced', function () {
    $services = fakeServices();
    Queue::fake();

    // A downgrade landed on the row but the run that should have followed it
    // never finished — the customer is paying for less and still holds more.
    $instance = unquotedInstance(['quota_gb' => 200, 'quota_applied_gb' => 500]);

    $this->artisan('clupilot:apply-quotas')->assertSuccessful();
    runQuotaRuns();

    expect($services['pve']->guestRan("config:app:set files default_quota --value='200 GB'"))->toBeTrue()
        ->and($instance->fresh()->quota_applied_gb)->toBe(200);
});

it('repairs one named instance when asked to', function () {
    fakeServices();
    Queue::fake();

    $wanted = unquotedInstance();
    unquotedInstance();

    $this->artisan('clupilot:apply-quotas', ['--instance' => $wanted->uuid])->assertSuccessful();

    expect(ProvisioningRun::query()->where('pipeline', 'quota')->count())->toBe(1)
        ->and(ProvisioningRun::query()->where('pipeline', 'quota')->first()->context('instance_id'))
        ->toBe($wanted->id);
});
