<?php

use App\Livewire\Admin\Overview;
use App\Models\Host;
use App\Models\Instance;
use App\Services\Provisioning\HostCapacity;
use Livewire\Livewire;

/**
 * Saying so before a paid order finds out.
 *
 * The platform books thick: a package reserves its whole `disk_gb` on ONE host
 * at placement. So the catalogue can go on offering something no machine has
 * room for, and the first anyone hears of it is a paid order failing at the
 * reservation step — after the money has moved.
 */
beforeEach(function () {
    // hosts.datacenter is a foreign key onto datacenters.code, so the code has
    // to exist before a host can carry it.
    App\Models\Datacenter::factory()->create(['code' => 'fsn1']);
});

it('measures the largest single reservation the estate could take', function () {
    // The largest free HOST, not the sum. A package goes on one machine, so two
    // hosts with 300 GB each cannot take a 540 GB instance between them.
    Host::factory()->active()->create(['datacenter' => 'fsn1', 'total_gb' => 300, 'reserve_pct' => 0]);
    Host::factory()->active()->create(['datacenter' => 'fsn1', 'total_gb' => 300, 'reserve_pct' => 0]);

    expect(app(HostCapacity::class)->largestPlaceableGb('fsn1'))->toBe(300)
        ->and(app(HostCapacity::class)->canPlace('fsn1', 540))->toBeFalse();
});

it('counts what is already committed, not what is on the disk', function () {
    // Thick booking: an instance holds its whole disk_gb whether the customer
    // has put a byte in it or not.
    $host = Host::factory()->active()->create(['datacenter' => 'fsn1', 'total_gb' => 1000, 'reserve_pct' => 0]);
    Instance::factory()->create(['host_id' => $host->id, 'status' => 'active', 'disk_gb' => 700]);

    expect(app(HostCapacity::class)->largestPlaceableGb('fsn1'))->toBe(300);
});

it('respects the reserve the operator set aside', function () {
    // reserve_pct is headroom for snapshots and for the host's own growth. A
    // capacity answer that spends it is not an answer.
    Host::factory()->active()->create(['datacenter' => 'fsn1', 'total_gb' => 1000, 'reserve_pct' => 20]);

    expect(app(HostCapacity::class)->largestPlaceableGb('fsn1'))->toBe(800);
});

it('warns on the console about a package that can no longer be placed', function () {
    // The whole point of the feature: this is the sentence that has to appear
    // BEFORE somebody pays for the package it names.
    // 150 GB: Business (200 GB Platte) findet darauf keinen Platz mehr, Team
    // (100) noch — auf der neuen Leiter passte jedes Paket auf 400 GB.
    Host::factory()->active()->create(['datacenter' => 'fsn1', 'total_gb' => 150, 'reserve_pct' => 0]);

    $atRisk = app(HostCapacity::class)->unplaceablePlans();

    expect($atRisk)->not->toBeEmpty();

    $page = Livewire::actingAs(operator('Owner'), 'operator')->test(Overview::class);

    $page->assertViewHas('notices', fn (array $notices) => collect($notices)
        ->contains(fn (array $n) => str_contains($n['text'], 'Host')));
});

it('says nothing while everything still fits', function () {
    // A warning that is always there is furniture. This one has to mean
    // something the moment it appears.
    Host::factory()->active()->create(['datacenter' => 'fsn1', 'total_gb' => 20000, 'reserve_pct' => 0]);

    expect(app(HostCapacity::class)->unplaceablePlans())->toBeEmpty();
});

it('does not take the overview down when the catalogue cannot be read', function () {
    // A capacity warning does not get to be the thing that breaks the page an
    // operator opens when something is already wrong.
    // PlanCatalogue is final — deliberately, it is the single reader of the
    // price tables — so it cannot be subclassed. The call site does not
    // type-hint what comes back from the container, which is enough to put a
    // throwing stand-in in its place.
    app()->bind(App\Services\Billing\PlanCatalogue::class, fn () => new class
    {
        public function sellable(): array
        {
            throw new RuntimeException('overlapping availability windows');
        }
    });

    expect(app(HostCapacity::class)->unplaceablePlans())->toBe([]);

    Livewire::actingAs(operator('Owner'), 'operator')->test(Overview::class)->assertOk();
});

it('does not shout at an installation that has no host yet', function () {
    // Zero hosts is not a capacity problem — it is a machine nobody has
    // onboarded, and that has its own notice. Without this guard a fresh
    // console greeted the operator with one warning per sellable package
    // before they had done anything at all.
    App\Models\Host::query()->delete();

    expect(app(HostCapacity::class)->unplaceablePlans())->toBe([]);
});
