<?php

app_require('classes/superuser_system_status_service.php');

use classes\superuser_system_status_service;

function system_status_module_value(mixed $value, string $type = 'string'): array
{
    return [
        'raw' => $value,
        'parsed' => $value,
        'type' => $type,
    ];
}

class SuperuserSystemStatusServiceProbeDouble extends superuser_system_status_service
{
    public array $httpProbeCalls = [];
    public array $nextHttpProbeResult = [
        'status' => 'ok',
        'status_reason' => 'Probe connectivity confirmed.',
        'status_reason_key' => 'http_ok',
        'status_reason_params' => ['label' => 'Probe'],
        'checked_at' => '2026-04-08T17:00:00+00:00',
        'latency_ms' => 12.34,
        'http_status' => 200,
    ];
    public ?array $moduleConfigRowsOverride = null;
    public bool $backupStoreValidationShouldFail = false;
    public string $backupStoreFailureMessage = 'Backup bucket is missing.';
    public array $backupHealthSummary = [
        'latest_verified_backup' => ['backup_uuid' => 'verified-backup'],
        'latest_verified_age_seconds' => 60,
        'fresh' => true,
        'encryption' => ['available' => true, 'key_id' => 'test-key'],
        'verification_required' => true,
        'restore_enabled' => false,
    ];
    public bool $selfserveBootstrapShouldFail = false;
    public string $selfserveBootstrapFailureMessage = 'Schema bootstrap failed.';
    public bool $selfserveMinuteProductExistsValue = true;
    public ?int $selfserveMinuteProductCheckedId = null;
    public ?string $shellyProbeDeviceId = null;

    public function collectModulesPublic(bool $force, array &$warnings): array
    {
        return $this->collectModules($force, $warnings);
    }

    public function classifyHttpProbeResultPublic(array $httpResponse, string $label): array
    {
        return $this->classifyHttpProbeResult($httpResponse, $label);
    }

    public function evaluateRecaptchaProbeResponsePublic(array $httpResponse, string $label): array
    {
        return $this->evaluateRecaptchaProbeResponse($httpResponse, $label);
    }

    public function evaluateLicensePlateRecognizerProbeResponsePublic(array $httpResponse, string $label): array
    {
        return $this->evaluateLicensePlateRecognizerProbeResponse($httpResponse, $label);
    }

    public function evaluateProviderQuotaProbeResponsePublic(array $httpResponse, string $label, string $moduleKey): array
    {
        return $this->evaluateProviderQuotaProbeResponse($httpResponse, $label, $moduleKey);
    }

    public function normalizeWarningEntriesPublic(array $warnings): array
    {
        return $this->normalizeWarningEntries($warnings);
    }

    public function probeEconomicModulePublic(array $config): array
    {
        return $this->probeEconomicModule($config);
    }

    public function probeRecaptchaModulePublic(array $config): array
    {
        return $this->probeRecaptchaModule($config);
    }

    public function probeEmailModulePublic(array $config): array
    {
        return $this->probeEmailModule($config);
    }

    public function probeBackupsModulePublic(array $config): array
    {
        return $this->probeBackupsModule($config);
    }

    public function probeMotorApiModulePublic(array $config): array
    {
        return $this->probeMotorApiModule($config);
    }

    public function probeFxRatesApiModulePublic(array $config): array
    {
        return $this->probeFxRatesApiModule($config);
    }

    public function probeWeatherApiModulePublic(array $config): array
    {
        return $this->probeWeatherApiModule($config);
    }

    public function probeWorkfeedModulePublic(array $config): array
    {
        return $this->probeWorkfeedModule($config);
    }

    public function probeGatewayApiModulePublic(array $config): array
    {
        return $this->probeGatewayApiModule($config);
    }

    public function probeXlVaskModulePublic(array $config): array
    {
        return $this->probeXlVaskModule($config);
    }

    public function probeLimbleModulePublic(array $config): array
    {
        return $this->probeLimbleModule($config);
    }

    public function probeLicensePlateRecognizerModulePublic(array $config): array
    {
        return $this->probeLicensePlateRecognizerModule($config);
    }

    public function probeShellyModulePublic(array $config): array
    {
        return $this->probeShellyModule($config);
    }

    public function probeSelfserveModulePublic(array $config): array
    {
        return $this->probeSelfserveModule($config);
    }

    public function probeBirdModulePublic(array $config): array
    {
        return $this->probeBirdModule($config);
    }

    protected function performHttpProbe(
        string $url,
        array $headers,
        string $label,
        ?string $basicAuth = null,
        string $method = 'GET',
        ?string $body = null,
        ?callable $responseEvaluator = null
    ): array {
        $this->httpProbeCalls[] = [
            'url' => $url,
            'headers' => $headers,
            'label' => $label,
            'basic_auth' => $basicAuth,
            'method' => $method,
            'body' => $body,
            'has_evaluator' => $responseEvaluator !== null,
        ];

        return $this->nextHttpProbeResult;
    }

    protected function loadModuleConfigRows(array $moduleNames): array
    {
        if ($this->moduleConfigRowsOverride !== null) {
            return $this->moduleConfigRowsOverride;
        }

        return parent::loadModuleConfigRows($moduleNames);
    }

    protected function validateBackupsStore(): void
    {
        if ($this->backupStoreValidationShouldFail) {
            throw new RuntimeException($this->backupStoreFailureMessage);
        }
    }

    protected function backupHealthSummary(): array
    {
        return $this->backupHealthSummary;
    }

    protected function bootstrapSelfserveSchema(): void
    {
        if ($this->selfserveBootstrapShouldFail) {
            throw new RuntimeException($this->selfserveBootstrapFailureMessage);
        }
    }

    protected function selfserveMinuteProductExists(int $productId): bool
    {
        $this->selfserveMinuteProductCheckedId = $productId;
        return $this->selfserveMinuteProductExistsValue;
    }

    protected function findShellyProbeDeviceId(): ?string
    {
        return $this->shellyProbeDeviceId;
    }
}

beforeEach(function (): void {
    $this->previousEconomicApi = $GLOBALS['ECONOMIC_API'] ?? null;
});

afterEach(function (): void {
    if ($this->previousEconomicApi === null) {
        unset($GLOBALS['ECONOMIC_API']);
        return;
    }

    $GLOBALS['ECONOMIC_API'] = $this->previousEconomicApi;
});

it('reduces overall status using down and degraded precedence', function (): void {
    expect(superuser_system_status_service::reduceOverallStatus(['ok', 'configured']))->toBe('ok');
    expect(superuser_system_status_service::reduceOverallStatus(['ok', 'not_configured']))->toBe('degraded');
    expect(superuser_system_status_service::reduceOverallStatus(['configured', 'down']))->toBe('down');
});

it('classifies runtime usage percentages consistently', function (): void {
    expect(superuser_system_status_service::statusFromUsagePercent(42.5))->toBe('ok');
    expect(superuser_system_status_service::statusFromUsagePercent(91.0, 90, 99))->toBe('degraded');
    expect(superuser_system_status_service::statusFromUsagePercent(99.1, 90, 99))->toBe('down');
    expect(superuser_system_status_service::statusFromUsagePercent(null))->toBe('down');
});

it('reuses cached module probes only when the ttl is still valid and force is false', function (): void {
    $freshCachedProbe = ['checked_at' => date('c', time() - 15)];
    $expiredCachedProbe = ['checked_at' => date('c', time() - 120)];

    expect(superuser_system_status_service::shouldReuseCachedModuleProbe($freshCachedProbe, false, time(), 60))->toBeTrue();
    expect(superuser_system_status_service::shouldReuseCachedModuleProbe($freshCachedProbe, true, time(), 60))->toBeFalse();
    expect(superuser_system_status_service::shouldReuseCachedModuleProbe($expiredCachedProbe, false, time(), 60))->toBeFalse();
});

it('classifies authenticated http probe responses conservatively', function (int $httpStatus, string $expectedStatus, string $reasonFragment, string $expectedReasonKey): void {
    $service = new SuperuserSystemStatusServiceProbeDouble();

    $result = $service->classifyHttpProbeResultPublic([
        'checked_at' => '2026-04-08T17:10:00+00:00',
        'latency_ms' => 23.5,
        'http_status' => $httpStatus,
        'error' => '',
    ], 'Example API');

    expect($result['status'])->toBe($expectedStatus);
    expect($result['status_reason'])->toContain($reasonFragment);
    expect($result['status_reason_key'])->toBe($expectedReasonKey);
})->with([
    'success' => [200, 'ok', 'connectivity confirmed', 'http_ok'],
    'unauthorized' => [401, 'down', 'HTTP 401', 'http_status'],
    'forbidden' => [403, 'down', 'HTTP 403', 'http_status'],
    'rate limited' => [429, 'degraded', 'rate limited', 'http_rate_limited'],
    'server error' => [503, 'degraded', 'HTTP 503', 'http_status'],
    'no response' => [0, 'down', 'did not return an HTTP response', 'http_no_response'],
]);

it('classifies transport errors as down', function (): void {
    $service = new SuperuserSystemStatusServiceProbeDouble();

    $result = $service->classifyHttpProbeResultPublic([
        'checked_at' => '2026-04-08T17:10:00+00:00',
        'latency_ms' => 7.5,
        'http_status' => 0,
        'error' => 'Connection refused',
    ], 'Example API');

    expect($result['status'])->toBe('down');
    expect($result['status_reason'])->toContain('Connection refused');
    expect($result['status_reason_key'])->toBe('http_probe_failed');
});

it('normalizes and deduplicates warning entries for snapshots', function (): void {
    $service = new SuperuserSystemStatusServiceProbeDouble();

    $entries = $service->normalizeWarningEntriesPublic([
        'Redis is unavailable; module probe caching is bypassed.',
        [
            'key' => 'redis_cache_bypass',
            'params' => [],
            'message' => 'Redis is unavailable; module probe caching is bypassed.',
        ],
        [
            'key' => 'modules_without_probes',
            'params' => ['modules' => 'ocrspace, virkdata', 'module_keys' => ['ocrspace', 'virkdata']],
            'message' => 'Some modules expose configuration-only status because no safe read-only probe exists: ocrspace, virkdata.',
        ],
        [
            'key' => 'modules_without_probes',
            'params' => ['modules' => 'ocrspace, virkdata', 'module_keys' => ['ocrspace', 'virkdata']],
            'message' => 'Some modules expose configuration-only status because no safe read-only probe exists: ocrspace, virkdata.',
        ],
    ]);

    expect($entries)->toHaveCount(3);
    expect($entries[0])->toBe([
        'key' => null,
        'params' => [],
        'message' => 'Redis is unavailable; module probe caching is bypassed.',
    ]);
    expect($entries[1]['key'])->toBe('redis_cache_bypass');
    expect($entries[2]['params']['module_keys'])->toBe(['ocrspace', 'virkdata']);
});

it('interprets recaptcha probe payloads safely', function (array $payload, string $expectedStatus, string $reasonFragment, string $expectedReasonKey): void {
    $service = new SuperuserSystemStatusServiceProbeDouble();

    $result = $service->evaluateRecaptchaProbeResponsePublic([
        'checked_at' => '2026-04-08T17:10:00+00:00',
        'latency_ms' => 11.4,
        'http_status' => 200,
        'body' => json_encode($payload, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES),
        'error' => '',
    ], 'reCAPTCHA');

    expect($result['status'])->toBe($expectedStatus);
    expect($result['status_reason'])->toContain($reasonFragment);
    expect($result['status_reason_key'])->toBe($expectedReasonKey);
})->with([
    'dummy token rejected but credentials valid' => [
        ['success' => false, 'error-codes' => ['invalid-input-response']],
        'ok',
        'connectivity confirmed',
        'http_ok',
    ],
    'invalid secret is down' => [
        ['success' => false, 'error-codes' => ['invalid-input-secret']],
        'down',
        'credentials were rejected',
        'recaptcha_credentials_rejected',
    ],
    'unexpected validation errors degrade' => [
        ['success' => false, 'error-codes' => ['bad-request']],
        'degraded',
        'unexpected validation errors',
        'recaptcha_validation_errors',
    ],
]);

it('builds the expected http probe requests for newly supported modules', function (
    string $methodName,
    array $config,
    string $expectedUrl,
    array $expectedHeaders,
    ?string $expectedBasicAuth,
    string $expectedMethod,
    ?string $expectedBodyFragment,
    bool $expectsEvaluator
): void {
    $service = new SuperuserSystemStatusServiceProbeDouble();

    $service->{$methodName}($config);

    expect($service->httpProbeCalls)->toHaveCount(1);
    $call = $service->httpProbeCalls[0];

    expect($call['url'])->toBe($expectedUrl);
    expect($call['basic_auth'])->toBe($expectedBasicAuth);
    expect($call['method'])->toBe($expectedMethod);
    expect($call['has_evaluator'])->toBe($expectsEvaluator);

    foreach ($expectedHeaders as $expectedHeader) {
        expect($call['headers'])->toContain($expectedHeader);
    }

    if ($expectedBodyFragment !== null) {
        expect((string)$call['body'])->toContain($expectedBodyFragment);
    } else {
        expect($call['body'])->toBeNull();
    }
})->with([
    'recaptcha' => [
        'probeRecaptchaModulePublic',
        ['secret_key_v2' => system_status_module_value('recaptcha-secret')],
        'https://www.google.com/recaptcha/api/siteverify',
        ['Content-Type: application/x-www-form-urlencoded'],
        null,
        'POST',
        'secret=recaptcha-secret',
        true,
    ],
    'email' => [
        'probeEmailModulePublic',
        ['mailersend_api_key' => system_status_module_value('mailersend-key')],
        'https://api.mailersend.com/v1/api-quota',
        ['Authorization: Bearer mailersend-key', 'Accept: application/json'],
        null,
        'GET',
        null,
        true,
    ],
    'motorapi' => [
        'probeMotorApiModulePublic',
        ['secret_key' => system_status_module_value('motorapi-key')],
        'https://v1.motorapi.dk/usage',
        ['X-AUTH-TOKEN: motorapi-key'],
        null,
        'GET',
        null,
        true,
    ],
    'fxratesapi' => [
        'probeFxRatesApiModulePublic',
        ['secret_key' => system_status_module_value('fxrates-key')],
        'https://api.fxratesapi.com/latest?base=EUR&currencies=DKK',
        ['X-AUTH-TOKEN: fxrates-key'],
        null,
        'GET',
        null,
        false,
    ],
    'weatherapi' => [
        'probeWeatherApiModulePublic',
        ['secret_key' => system_status_module_value('weather-key')],
        'https://api.weatherapi.com/v1/current.json?key=weather-key&q=Copenhagen',
        ['Accept: application/json'],
        null,
        'GET',
        null,
        false,
    ],
    'workfeed' => [
        'probeWorkfeedModulePublic',
        [
            'api_url' => system_status_module_value('https://api.workfeed.test'),
            'CompanyID' => system_status_module_value('company-123'),
            'api_key' => system_status_module_value('workfeed-token'),
        ],
        'https://api.workfeed.test/companies/company-123/departments',
        ['Accept: application/json', 'Authorization: workfeed-token'],
        null,
        'GET',
        null,
        false,
    ],
    'gatewayapi' => [
        'probeGatewayApiModulePublic',
        ['api_token' => system_status_module_value('gateway-token')],
        'https://gatewayapi.eu/rest/me',
        ['Authorization: Token gateway-token', 'Accept: application/json'],
        null,
        'GET',
        null,
        false,
    ],
    'xlvask' => [
        'probeXlVaskModulePublic',
        [
            'username' => system_status_module_value('xl-user'),
            'password' => system_status_module_value('xl-pass'),
        ],
        'https://api.xlwash.com/customers',
        ['Accept: application/json'],
        'xl-user:xl-pass',
        'GET',
        null,
        false,
    ],
    'limble' => [
        'probeLimbleModulePublic',
        [
            'client_id' => system_status_module_value('limble-id'),
            'client_secret' => system_status_module_value('limble-secret'),
        ],
        'https://api.limblecmms.com:443/v2/tasks?limit=1&page=1',
        ['Accept: application/json', 'Content-Type: application/json'],
        'limble-id:limble-secret',
        'GET',
        null,
        false,
    ],
    'license plate recognizer' => [
        'probeLicensePlateRecognizerModulePublic',
        ['api_key' => system_status_module_value('lpr-key')],
        'https://vs4sws0kg4sog4ssw8kwowk4.coolify.truckwash.dk/info/',
        ['Authorization: Token lpr-key', 'Accept: application/json'],
        null,
        'GET',
        null,
        true,
    ],
    'bird' => [
        'probeBirdModulePublic',
        [
            'server_url' => system_status_module_value('https://api.bird.com'),
            'api_key' => system_status_module_value('bird-key'),
            'channelId' => system_status_module_value('channel-123'),
            'workplaceId' => system_status_module_value('workspace-456'),
        ],
        'https://api.bird.com/workspaces/workspace-456/channels/channel-123/calls?limit=1',
        ['Authorization: AccessKey bird-key', 'Accept: application/json'],
        null,
        'GET',
        null,
        false,
    ],
]);

it('evaluates license plate recognizer usage and quota health from the info payload', function (
    string $body,
    string $expectedStatus,
    string $expectedReasonKey,
    ?array $expectedUsage
): void {
    $service = new SuperuserSystemStatusServiceProbeDouble();

    $result = $service->evaluateLicensePlateRecognizerProbeResponsePublic([
        'checked_at' => '2026-04-08T17:00:00+00:00',
        'latency_ms' => 7.5,
        'http_status' => 200,
        'body' => $body,
        'error' => '',
    ], 'License Plate Recognizer');

    expect($result['status'])->toBe($expectedStatus);
    expect($result['status_reason_key'])->toBe($expectedReasonKey);
    expect(json_encode($result, JSON_THROW_ON_ERROR))->not->toContain('SECRET-LICENSE-KEY');

    if ($expectedUsage === null) {
        expect($result)->not->toHaveKey('usage');
        return;
    }

    expect($result['usage'])->toMatchArray($expectedUsage);
})->with([
    'normal usage' => [
        json_encode([
            'version' => '1.54.0',
            'usage' => ['calls' => 98],
            'license_key' => 'SECRET-LICENSE-KEY',
            'webhooks' => [],
            'total_calls' => 2500,
        ]),
        'ok',
        'licenseplaterecognizer_usage_available',
        [
            'provider' => 'licenseplaterecognizer',
            'calls_used' => 98,
            'quota_calls' => 2500,
            'calls_remaining' => 2402,
            'usage_percent' => 3.92,
            'version' => '1.54.0',
        ],
    ],
    'near quota' => [
        json_encode([
            'version' => '1.54.0',
            'usage' => ['calls' => 2250],
            'total_calls' => 2500,
        ]),
        'degraded',
        'licenseplaterecognizer_quota_near_limit',
        [
            'calls_used' => 2250,
            'quota_calls' => 2500,
            'calls_remaining' => 250,
            'usage_percent' => 90.0,
        ],
    ],
    'exhausted quota' => [
        json_encode([
            'version' => '1.54.0',
            'usage' => ['calls' => 2500],
            'total_calls' => 2500,
        ]),
        'down',
        'licenseplaterecognizer_quota_exhausted',
        [
            'calls_used' => 2500,
            'quota_calls' => 2500,
            'calls_remaining' => 0,
            'usage_percent' => 100.0,
        ],
    ],
    'unreadable payload' => [
        'not-json',
        'degraded',
        'provider_quota_unavailable',
        [
            'provider' => 'licenseplaterecognizer',
            'usage_available' => false,
            'status' => 'unknown',
            'calls_used' => null,
            'quota_calls' => null,
            'calls_remaining' => null,
            'usage_percent' => null,
            'unavailable_reason' => 'unreadable_payload',
        ],
    ],
    'missing quota' => [
        json_encode([
            'version' => '1.54.0',
            'usage' => ['calls' => 10],
        ]),
        'degraded',
        'provider_quota_unavailable',
        [
            'provider' => 'licenseplaterecognizer',
            'usage_available' => false,
            'status' => 'unknown',
            'calls_used' => null,
            'quota_calls' => null,
            'calls_remaining' => null,
            'usage_percent' => null,
            'unavailable_reason' => 'missing_limit',
        ],
    ],
]);

it('degrades provider quota probes when the provider response does not expose readable quota fields', function (
    string $body,
    string $expectedReason
): void {
    $service = new SuperuserSystemStatusServiceProbeDouble();

    $result = $service->evaluateProviderQuotaProbeResponsePublic([
        'checked_at' => '2026-04-08T17:00:00+00:00',
        'latency_ms' => 7.5,
        'http_status' => 200,
        'body' => $body,
        'error' => '',
    ], 'MailerSend', 'email');

    expect($result['status'])->toBe('degraded')
        ->and($result['status_reason_key'])->toBe('provider_quota_unavailable')
        ->and($result['usage'])->toMatchArray([
            'provider' => 'email',
            'usage_available' => false,
            'status' => 'unknown',
            'calls_used' => null,
            'quota_calls' => null,
            'calls_remaining' => null,
            'usage_percent' => null,
            'unavailable_reason' => $expectedReason,
        ]);
})->with([
    'invalid json' => ['not-json', 'unreadable_payload'],
    'missing limit' => [json_encode(['usage' => ['calls' => 12], 'plan' => 'pro']), 'missing_limit'],
    'missing usage' => [json_encode(['quota' => 100, 'plan' => 'pro']), 'missing_usage'],
    'invalid limit' => [json_encode(['used' => 12, 'quota' => 0]), 'invalid_limit'],
]);

it('carries license plate recognizer usage from probe results into module status rows', function (): void {
    $service = new SuperuserSystemStatusServiceProbeDouble();
    $service->nextHttpProbeResult = [
        'status' => 'ok',
        'status_reason' => 'License Plate Recognizer usage and quota are available.',
        'status_reason_key' => 'licenseplaterecognizer_usage_available',
        'status_reason_params' => ['used' => '98', 'quota' => '2500', 'remaining' => '2402', 'percent' => '3.9'],
        'checked_at' => '2026-04-08T17:00:00+00:00',
        'usage' => [
            'provider' => 'licenseplaterecognizer',
            'calls_used' => 98,
            'quota_calls' => 2500,
            'calls_remaining' => 2402,
            'usage_percent' => 3.92,
            'version' => '1.54.0',
        ],
    ];
    $service->moduleConfigRowsOverride = [
        'licenseplaterecognizer' => [
            'enabled' => system_status_module_value(true, 'bool'),
            'api_key' => system_status_module_value('lpr-key'),
        ],
    ];

    $warnings = [];
    $modules = $service->collectModulesPublic(false, $warnings);
    $moduleMap = [];
    foreach ($modules as $module) {
        $moduleMap[$module['key']] = $module;
    }

    expect($moduleMap['licenseplaterecognizer']['usage'])->toMatchArray([
        'provider' => 'licenseplaterecognizer',
        'calls_used' => 98,
        'quota_calls' => 2500,
        'calls_remaining' => 2402,
        'usage_percent' => 3.92,
    ]);
});

it('uses runtime economic credentials for the economic probe', function (): void {
    $service = new SuperuserSystemStatusServiceProbeDouble();
    $GLOBALS['ECONOMIC_API'] = [
        'app_secret_token' => 'economic-secret',
        'app_access_grant' => 'economic-grant',
    ];

    $service->probeEconomicModulePublic([]);

    expect($service->httpProbeCalls)->toHaveCount(1);
    $call = $service->httpProbeCalls[0];

    expect($call['url'])->toBe('https://restapi.e-conomic.com/layouts/');
    expect($call['headers'])->toContain('X-AppSecretToken: economic-secret');
    expect($call['headers'])->toContain('X-AgreementGrantToken: economic-grant');
});

it('returns down without attempting economic http calls when runtime credentials are missing', function (): void {
    $service = new SuperuserSystemStatusServiceProbeDouble();
    unset($GLOBALS['ECONOMIC_API']);

    $result = $service->probeEconomicModulePublic([]);

    expect($result['status'])->toBe('down');
    expect($result['status_reason'])->toContain('credentials are missing');
    expect($result['status_reason_key'])->toBe('economic_credentials_missing');
    expect($service->httpProbeCalls)->toBeEmpty();
});

it('reports backup probe failures from local validation', function (): void {
    $service = new SuperuserSystemStatusServiceProbeDouble();
    $service->backupStoreValidationShouldFail = true;
    $service->backupStoreFailureMessage = 'Backups bucket is unavailable.';

    $result = $service->probeBackupsModulePublic([]);

    expect($result['status'])->toBe('down');
    expect($result['status_reason'])->toContain('Backups bucket is unavailable');
    expect($result['status_reason_key'])->toBe('backup_probe_failed');
});

it('reports backups down when encryption key is missing', function (): void {
    $service = new SuperuserSystemStatusServiceProbeDouble();
    $service->backupHealthSummary['encryption'] = [
        'available' => false,
        'error' => 'BACKUP_ENCRYPTION_KEY_V1 is not configured.',
    ];

    $result = $service->probeBackupsModulePublic([]);

    expect($result['status'])->toBe('down');
    expect($result['status_reason_key'])->toBe('backup_encryption_key_missing');
});

it('reports backups degraded when no verified backup is available', function (): void {
    $service = new SuperuserSystemStatusServiceProbeDouble();
    $service->backupHealthSummary['latest_verified_backup'] = null;
    $service->backupHealthSummary['fresh'] = false;

    $result = $service->probeBackupsModulePublic([]);

    expect($result['status'])->toBe('degraded');
    expect($result['status_reason_key'])->toBe('backup_no_verified_backup');
});

it('reports backups degraded when the latest verified backup is stale', function (): void {
    $service = new SuperuserSystemStatusServiceProbeDouble();
    $service->backupHealthSummary['fresh'] = false;
    $service->backupHealthSummary['latest_verified_age_seconds'] = 7200;

    $result = $service->probeBackupsModulePublic([]);

    expect($result['status'])->toBe('degraded');
    expect($result['status_reason_key'])->toBe('backup_latest_verified_stale');
});

it('returns configured for shelly when no known device id is available for probing', function (): void {
    $service = new SuperuserSystemStatusServiceProbeDouble();

    $result = $service->probeShellyModulePublic([
        'server_url' => system_status_module_value('https://shelly.example'),
        'secret_key' => system_status_module_value('shelly-secret'),
    ]);

    expect($result['status'])->toBe('configured');
    expect($result['status_reason'])->toContain('no known device id');
    expect($result['status_reason_key'])->toBe('shelly_no_device_id');
    expect($service->httpProbeCalls)->toBeEmpty();
});

it('builds an authenticated shelly status probe when a known device id exists', function (): void {
    $service = new SuperuserSystemStatusServiceProbeDouble();
    $service->shellyProbeDeviceId = 'device-123';

    $service->probeShellyModulePublic([
        'server_url' => system_status_module_value('https://shelly.example'),
        'secret_key' => system_status_module_value('shelly-secret'),
    ]);

    expect($service->httpProbeCalls)->toHaveCount(1);
    $call = $service->httpProbeCalls[0];
    expect($call['url'])->toBe('https://shelly.example/device/status?id=device-123&auth_key=shelly-secret');
    expect($call['headers'])->toContain('Accept: application/json');
    expect($call['method'])->toBe('GET');
});

it('validates selfserve schema and minute product configuration', function (): void {
    $service = new SuperuserSystemStatusServiceProbeDouble();

    $result = $service->probeSelfserveModulePublic([
        'machine_wash_minutes_included' => system_status_module_value('15'),
        'minute_product' => system_status_module_value('77'),
    ]);

    expect($result['status'])->toBe('ok');
    expect($service->selfserveMinuteProductCheckedId)->toBe(77);
});

it('reports invalid selfserve minute configuration before touching the schema', function (): void {
    $service = new SuperuserSystemStatusServiceProbeDouble();

    $result = $service->probeSelfserveModulePublic([
        'machine_wash_minutes_included' => system_status_module_value('abc'),
        'minute_product' => system_status_module_value('77'),
    ]);

    expect($result['status'])->toBe('down');
    expect($result['status_reason'])->toContain('minutes configuration is invalid');
    expect($result['status_reason_key'])->toBe('selfserve_minutes_invalid');
    expect($service->selfserveMinuteProductCheckedId)->toBeNull();
});

it('reports missing selfserve minute products', function (): void {
    $service = new SuperuserSystemStatusServiceProbeDouble();
    $service->selfserveMinuteProductExistsValue = false;

    $result = $service->probeSelfserveModulePublic([
        'machine_wash_minutes_included' => system_status_module_value('15'),
        'minute_product' => system_status_module_value('88'),
    ]);

    expect($result['status'])->toBe('down');
    expect($result['status_reason'])->toContain('does not exist');
    expect($result['status_reason_key'])->toBe('selfserve_minute_product_missing');
    expect($result['status_reason_params'])->toBe(['productId' => 88]);
    expect($service->selfserveMinuteProductCheckedId)->toBe(88);
});

it('surfaces selfserve bootstrap failures', function (): void {
    $service = new SuperuserSystemStatusServiceProbeDouble();
    $service->selfserveBootstrapShouldFail = true;
    $service->selfserveBootstrapFailureMessage = 'Migration failed.';

    $result = $service->probeSelfserveModulePublic([
        'machine_wash_minutes_included' => system_status_module_value('15'),
        'minute_product' => system_status_module_value('88'),
    ]);

    expect($result['status'])->toBe('down');
    expect($result['status_reason'])->toContain('Migration failed');
    expect($result['status_reason_key'])->toBe('selfserve_probe_failed');
});

it('adds localization metadata for disabled and missing-config modules', function (): void {
    $service = new SuperuserSystemStatusServiceProbeDouble();
    $service->moduleConfigRowsOverride = [
        'reCAPTCHA' => [
            'enabled' => system_status_module_value(false, 'bool'),
        ],
        'Email' => [
            'enabled' => system_status_module_value(true, 'bool'),
            'mailersend_enabled' => system_status_module_value(false, 'bool'),
        ],
    ];

    $warnings = [];
    $modules = $service->collectModulesPublic(false, $warnings);
    $moduleMap = [];
    foreach ($modules as $module) {
        $moduleMap[$module['key']] = $module;
    }

    expect($moduleMap['reCAPTCHA']['status'])->toBe('disabled');
    expect($moduleMap['reCAPTCHA']['status_reason_key'])->toBe('module_disabled');
    expect($moduleMap['email']['status'])->toBe('not_configured');
    expect($moduleMap['email']['status_reason_key'])->toBe('email_delivery_not_implemented');
    expect($moduleMap['email']['status_reason_params'])->toBe([]);
    expect($warnings)->toBeEmpty();
});

it('marks newly supported modules as probe backed and leaves only truly unsupported modules as configuration only', function (): void {
    $service = new SuperuserSystemStatusServiceProbeDouble();
    $GLOBALS['ECONOMIC_API'] = [
        'app_secret_token' => 'economic-secret',
        'app_access_grant' => 'economic-grant',
    ];

    $service->moduleConfigRowsOverride = [
        'economic' => [
            'invoiceLayoutNumber' => system_status_module_value('1'),
            'invoiceDiscountLayoutNumber' => system_status_module_value('6'),
            'paymentTermsNumber' => system_status_module_value('2'),
            'adminFeeMonthly' => system_status_module_value('3'),
            'adminFeeOrder' => system_status_module_value('4'),
            'feeProductId' => system_status_module_value('5'),
        ],
        'Email' => [
            'enabled' => system_status_module_value(true, 'bool'),
            'mailersend_enabled' => system_status_module_value(true, 'bool'),
            'mailersend_api_key' => system_status_module_value('mailersend-key'),
            'smtp_from' => system_status_module_value('from@example.com'),
            'smtp_from_name' => system_status_module_value('Truck Wash'),
            'smtp_reply_to' => system_status_module_value('reply@example.com'),
            'smtp_reply_to_name' => system_status_module_value('Reply'),
        ],
        'Backups' => [
            'enabled' => system_status_module_value(true, 'bool'),
        ],
        'motorapi' => [
            'enabled' => system_status_module_value(true, 'bool'),
            'secret_key' => system_status_module_value('motorapi-key'),
        ],
        'fxratesapi' => [
            'enabled' => system_status_module_value(true, 'bool'),
            'secret_key' => system_status_module_value('fxrates-key'),
        ],
        'weatherapi' => [
            'enabled' => system_status_module_value(true, 'bool'),
            'secret_key' => system_status_module_value('weather-key'),
        ],
        'workfeed' => [
            'enabled' => system_status_module_value(true, 'bool'),
            'api_url' => system_status_module_value('https://api.workfeed.test'),
            'api_key' => system_status_module_value('workfeed-token'),
            'CompanyID' => system_status_module_value('company-123'),
        ],
        'GatewayAPI' => [
            'enabled' => system_status_module_value(true, 'bool'),
            'api_secret' => system_status_module_value('gateway-secret'),
            'api_token' => system_status_module_value('gateway-token'),
            'sender' => system_status_module_value('TruckWash'),
        ],
        'xlvask' => [
            'enabled' => system_status_module_value(true, 'bool'),
            'username' => system_status_module_value('xl-user'),
            'password' => system_status_module_value('xl-pass'),
        ],
        'limble' => [
            'enabled' => system_status_module_value(true, 'bool'),
            'client_id' => system_status_module_value('limble-id'),
            'client_secret' => system_status_module_value('limble-secret'),
        ],
        'licenseplaterecognizer' => [
            'enabled' => system_status_module_value(true, 'bool'),
            'api_key' => system_status_module_value('lpr-key'),
        ],
        'shelly' => [
            'enabled' => system_status_module_value(true, 'bool'),
            'server_url' => system_status_module_value('https://shelly.example'),
            'secret_key' => system_status_module_value('shelly-secret'),
        ],
        'selfserve' => [
            'enabled' => system_status_module_value(true, 'bool'),
            'machine_wash_minutes_included' => system_status_module_value('15'),
            'minute_product' => system_status_module_value('88'),
        ],
        'bird' => [
            'enabled' => system_status_module_value(true, 'bool'),
            'server_url' => system_status_module_value('https://api.bird.com'),
            'api_key' => system_status_module_value('bird-key'),
            'channelId' => system_status_module_value('channel-123'),
            'workplaceId' => system_status_module_value('workspace-456'),
        ],
        'ocrSpace' => [
            'enabled' => system_status_module_value(true, 'bool'),
            'api_key' => system_status_module_value('ocr-key'),
        ],
        'virkdata' => [
            'enabled' => system_status_module_value(true, 'bool'),
            'secret_key' => system_status_module_value('virk-key'),
        ],
    ];

    $warnings = [];
    $modules = $service->collectModulesPublic(false, $warnings);
    $moduleMap = [];
    foreach ($modules as $module) {
        $moduleMap[$module['key']] = $module;
    }

    expect($moduleMap['email']['probe_supported'])->toBeTrue();
    expect($moduleMap['email']['status'])->toBe('ok');
    expect($moduleMap['email']['status_reason_key'])->toBe('http_ok');
    expect($moduleMap['backups']['probe_supported'])->toBeTrue();
    expect($moduleMap['backups']['status'])->toBe('ok');
    expect($moduleMap['backups']['status_reason_key'])->toBe('backup_connectivity_confirmed');
    expect($moduleMap['motorapi']['probe_supported'])->toBeTrue();
    expect($moduleMap['motorapi']['status'])->toBe('ok');
    expect($moduleMap['bird']['probe_supported'])->toBeTrue();
    expect($moduleMap['bird']['status'])->toBe('ok');
    expect($moduleMap['shelly']['probe_supported'])->toBeTrue();
    expect($moduleMap['shelly']['status'])->toBe('configured');
    expect($moduleMap['shelly']['status_reason_key'])->toBe('shelly_no_device_id');
    expect($moduleMap['selfserve']['probe_supported'])->toBeTrue();
    expect($moduleMap['selfserve']['status'])->toBe('ok');
    expect($moduleMap['selfserve']['status_reason_key'])->toBe('selfserve_configuration_confirmed');
    expect($moduleMap['ocrspace']['probe_supported'])->toBeFalse();
    expect($moduleMap['ocrspace']['status'])->toBe('configured');
    expect($moduleMap['ocrspace']['status_reason_key'])->toBe('safe_probe_unavailable');
    expect($moduleMap['virkdata']['probe_supported'])->toBeFalse();
    expect($moduleMap['virkdata']['status'])->toBe('configured');
    expect($moduleMap['virkdata']['status_reason_key'])->toBe('safe_probe_unavailable');
    expect($warnings)->toHaveCount(1);
    expect($warnings[0]['key'])->toBe('modules_without_probes');
    expect($warnings[0]['params']['module_keys'])->toBe(['ocrspace', 'virkdata']);
    expect($warnings[0]['message'])->toContain('ocrspace, virkdata');
});
