<?php

use App\Models\Instance;
use App\Models\MonitoringTarget;
use App\Provisioning\Jobs\SyncMonitoringStatus;
use App\Services\Monitoring\MonitoringClient;

/**
 * Monitoring verdicts have to be measured, not remembered.
 *
 * The status column was written once — 'up', at provisioning — and nothing ever
 * updated it. The console's notices and the public status page both read it, so
 * an outage would have been reported as healthy indefinitely, on a page whose
 * whole purpose is to be believed.
 */

function target(string $externalId, array $attributes = []): MonitoringTarget
{
    $instance = Instance::factory()->create(['status' => 'active']);

    return MonitoringTarget::query()->create(array_merge([
        'instance_id' => $instance->id,
        'external_id' => $externalId,
        'url' => 'https://example.test',
        'status' => 'unknown',
    ], $attributes));
}

it('records what monitoring actually reports', function () {
    $up = target('mon-up');
    $down = target('mon-down');

    $this->mock(MonitoringClient::class, function ($mock) {
        $mock->shouldReceive('health')->with('mon-up')->andReturnTrue();
        $mock->shouldReceive('health')->with('mon-down')->andReturnFalse();
    });

    (new SyncMonitoringStatus)->handle(app(MonitoringClient::class));

    expect($up->refresh()->status)->toBe('up')
        ->and($up->checked_at)->not->toBeNull()
        ->and($down->refresh()->status)->toBe('down');
});

it('does not blame the instance when the monitoring service is the one that is down', function () {
    // "The monitor did not answer" says nothing about the instance. Writing
    // 'down' would raise an alarm about the wrong system; writing 'up' would
    // hide a real one.
    $target = target('mon-x', ['status' => 'up', 'checked_at' => now()->subMinutes(3)]);
    $wasCheckedAt = $target->checked_at;

    $this->mock(MonitoringClient::class, function ($mock) {
        $mock->shouldReceive('health')->andThrow(new RuntimeException('connection refused'));
    });

    (new SyncMonitoringStatus)->handle(app(MonitoringClient::class));

    expect($target->refresh()->status)->toBe('up')
        // Not advanced — so the verdict is allowed to go stale and stop counting.
        ->and($target->checked_at->timestamp)->toBe($wasCheckedAt->timestamp);
});

it('does not treat a never-checked target as healthy on the public page', function () {
    target('mon-fresh');

    $this->get('/status')
        ->assertOk()
        ->assertSee(__('status.state.unknown'))
        ->assertDontSee(__('status.overall.operational'));
});

it('stops trusting a verdict nobody has refreshed', function () {
    target('mon-old', ['status' => 'up', 'checked_at' => now()->subHours(2)]);

    $this->get('/status')
        ->assertOk()
        ->assertSee(__('status.state.unknown'));
});

it('records nothing when the monitor cannot answer at all', function () {
    // The boolean contract folded "no monitoring configured" into true and
    // "monitor unreachable" into false. Recording either as a measurement
    // publishes a fleet-wide all-clear nobody took, or a fleet-wide outage that
    // is really one broken monitor.
    $target = target('mon-quiet', ['status' => 'up', 'checked_at' => now()->subMinutes(4)]);
    $wasCheckedAt = $target->checked_at;

    $this->mock(MonitoringClient::class, function ($mock) {
        $mock->shouldReceive('health')->andReturnNull();
    });

    (new SyncMonitoringStatus)->handle(app(MonitoringClient::class));

    expect($target->refresh()->status)->toBe('up')
        ->and($target->checked_at->timestamp)->toBe($wasCheckedAt->timestamp);
});

it('keeps the acceptance check passing when monitoring is not set up', function () {
    // isHealthy() still folds "cannot tell" into a pass — deliberately, and
    // only there: a delivery must not fail because monitoring is unconfigured.
    config()->set('services.monitoring.url', '');

    $client = new App\Services\Monitoring\HttpMonitoringClient;

    expect($client->health('anything'))->toBeNull()
        ->and($client->isHealthy('anything'))->toBeTrue();
});
