<?php

use App\Models\Incident;
use App\Models\Instance;
use App\Models\StatusDay;
use Illuminate\Support\Carbon;

/**
 * The written record behind the status page.
 *
 * A status page is the one page in a product whose entire value is that people
 * believe it. Every test here is about a way it could quietly lie: a day nobody
 * measured drawn as a good day, an uptime figure that averages a four-sample
 * day against a three-hundred-sample day, a resolved incident quietly vanishing
 * the morning after, an unpublished draft appearing in public.
 */
it('never draws a day nobody measured as a good day', function () {
    // The single most tempting shortcut on a page like this: no row means
    // nothing was wrong. It means nothing was looked at.
    StatusDay::query()->create([
        'day' => Carbon::now()->local()->subDay()->toDateString(),
        'component' => 'portal',
        'samples' => 288, 'operational' => 288, 'degraded' => 0, 'down' => 0, 'unknown' => 0,
    ]);

    $history = $this->get('/status')->viewData('history')['portal']['days'];

    $yesterday = collect($history)->firstWhere(fn ($d) => $d['date']->isYesterday());
    $ninetyDaysAgo = collect($history)->first();

    expect($yesterday['state'])->toBe('operational')
        ->and($ninetyDaysAgo['state'])->toBeNull()
        ->and($ninetyDaysAgo['uptime'])->toBeNull();
});

it('weights the uptime figure by samples, not by days', function () {
    // A day the sampler only managed four runs on must not count as much as a
    // full day. Averaging the daily percentages is the version of this that
    // looks right and reports 50 % for one bad hour in three months.
    $day = fn (int $ago, int $samples, int $down) => StatusDay::query()->create([
        'day' => Carbon::now()->local()->subDays($ago)->toDateString(),
        'component' => 'portal',
        'samples' => $samples, 'operational' => $samples - $down,
        'degraded' => 0, 'down' => $down, 'unknown' => 0,
    ]);

    $day(1, 288, 0);
    $day(2, 4, 2);      // half of a tiny day was down

    $uptime = $this->get('/status')->viewData('history')['portal']['uptime'];

    // 290 up out of 292 measured, not the mean of 100 % and 50 % — which would
    // report 75 % for two minutes of downtime in a quarter.
    expect($uptime)->toBe(round(290 / 292 * 100, 2))
        ->and($uptime)->toBeGreaterThan(99.0);
});

it('counts a degraded sample as reachable but still colours the day', function () {
    // Folding "slower than usual" into downtime reports a bad morning as an
    // outage; ignoring it entirely loses the only trace of it on the page.
    $row = StatusDay::query()->create([
        'day' => Carbon::now()->local()->toDateString(),
        'component' => 'portal',
        'samples' => 100, 'operational' => 90, 'degraded' => 10, 'down' => 0, 'unknown' => 0,
    ]);

    expect($row->uptimePercent())->toBe(100.0)
        ->and($row->worst())->toBe('degraded');
});

it('leaves unknown samples out of the arithmetic entirely', function () {
    // "We could not tell" is not uptime and it is not downtime. Counting it as
    // either invents a measurement that was never taken.
    $row = StatusDay::query()->create([
        'day' => Carbon::now()->local()->toDateString(),
        'component' => 'instances',
        'samples' => 100, 'operational' => 50, 'degraded' => 0, 'down' => 0, 'unknown' => 50,
    ]);

    expect($row->uptimePercent())->toBe(100.0);
});

it('adds to today rather than replacing it, however often the sampler runs', function () {
    Instance::query()->delete();

    $this->artisan('clupilot:sample-status')->assertSuccessful();
    $this->artisan('clupilot:sample-status')->assertSuccessful();
    $this->artisan('clupilot:sample-status')->assertSuccessful();

    $row = StatusDay::query()->where('component', 'portal')->firstOrFail();

    // Three samples, not one overwritten three times — the counters are what
    // the ninety-day figure is computed from.
    expect($row->samples)->toBe(3)->and($row->operational)->toBe(3);
});

it('reports a duration forwards in time', function () {
    // It printed "Dauer -86 Minuten" on the public page: diffInMinutes is
    // signed and the operands were the wrong way round.
    $incident = Incident::query()->create([
        'title' => 'x', 'impact' => 'down', 'components' => ['portal'],
        'started_at' => Carbon::parse('2026-07-15 19:42'),
        'resolved_at' => Carbon::parse('2026-07-15 21:08'),
        'published_at' => now(),
    ]);

    expect($incident->durationMinutes())->toBe(86);
});

it('keeps a resolved incident on the page after it is over', function () {
    // The whole point. "Alle Dienste in Betrieb" the morning after is true and
    // useless to somebody who was locked out the evening before.
    $incident = Incident::query()->create([
        'title' => 'Instanzen zeitweise nicht erreichbar',
        'impact' => 'down', 'components' => ['instances'],
        'started_at' => now()->subDays(3),
        'resolved_at' => now()->subDays(3)->addHour(),
        'published_at' => now()->subDays(3),
    ]);
    $incident->updates()->create(['status' => 'resolved', 'body' => 'Seit 20:51 stabil.']);

    $this->get('/status')
        ->assertOk()
        ->assertSee('Instanzen zeitweise nicht erreichbar')
        ->assertSee('Seit 20:51 stabil.')
        ->assertSee(__('status.incident.stage.resolved'));
});

it('does not publish a draft', function () {
    // Drafts exist so an operator can write carefully during an outage instead
    // of publishing a sentence they then correct in front of everybody.
    Incident::query()->create([
        'title' => 'Interner Entwurf',
        'impact' => 'down', 'components' => ['portal'],
        'started_at' => now()->subHour(),
        'published_at' => null,
    ]);

    $this->get('/status')->assertOk()->assertDontSee('Interner Entwurf');
});

it('lets an open incident make a component look worse, never better', function () {
    // The direction matters. An incident that could improve a component's
    // reported state is a switch for hiding an outage.
    Instance::factory()->create(['status' => 'active']);   // → instances is 'unknown'

    Incident::query()->create([
        'title' => 'Ausfall', 'impact' => 'down', 'components' => ['instances', 'portal'],
        'started_at' => now()->subHour(), 'published_at' => now()->subHour(),
    ])->updates()->create(['status' => 'investigating', 'body' => 'Wir untersuchen das.']);

    $components = collect($this->get('/status')->viewData('components'))->keyBy('key');

    expect($components['instances']['state'])->toBe('down')
        // The portal answered this very request; an incident naming it cannot
        // turn a measurement into a claim, but it may still darken it.
        ->and($components['portal']['state'])->toBe('down');

    // And the banner follows the worst component.
    expect($this->get('/status')->viewData('overall'))->toBe('down');
});

it('shows an incident that is not over at the top, not filed under history', function () {
    Incident::query()->create([
        'title' => 'Läuft noch', 'impact' => 'degraded', 'components' => ['backups'],
        'started_at' => now()->subHour(), 'published_at' => now()->subHour(),
    ])->updates()->create(['status' => 'investigating', 'body' => 'Wird untersucht.']);

    $page = $this->get('/status');

    expect($page->viewData('ongoing'))->toHaveCount(1)
        ->and($page->viewData('past'))->toBeEmpty();

    $page->assertSee(__('status.incident.ongoing'));
});

it('measures whether the hosts answer at all', function () {
    // hosts.last_seen_at drove the console's health dot and was written exactly
    // once, at onboarding — so every host read "offline" half an hour later,
    // for good. Nothing was measuring host reachability, which is why it could
    // not appear here.
    App\Models\Host::factory()->create(['status' => 'active', 'last_seen_at' => now()]);
    App\Models\Host::factory()->create(['status' => 'active', 'last_seen_at' => now()->subHours(2)]);

    $components = collect(app(App\Services\Status\ServiceHealth::class)->components())->keyBy('key');

    expect($components['hosts']['state'])->toBe('degraded')
        ->and($components['hosts']['detail'])->toBe(['down' => 1, 'total' => 2]);
});

it('does not call a host that has stopped answering healthy', function () {
    // healthState() has three answers and they are not two: "stale" is between
    // five and thirty minutes without a word. Not yet an outage, and certainly
    // not health.
    App\Models\Host::factory()->create(['status' => 'active', 'last_seen_at' => now()->subMinutes(12)]);

    $components = collect(app(App\Services\Status\ServiceHealth::class)->components())->keyBy('key');

    expect($components['hosts']['state'])->toBe('degraded');
});

it('names no host on a page the whole internet can read', function () {
    // Counts only. The estate is not public information.
    App\Models\Host::factory()->create(['status' => 'active', 'name' => 'pve-wien-01', 'last_seen_at' => now()->subDay()]);

    $this->get('/status')->assertOk()->assertDontSee('pve-wien-01');
});
