<?php

app_require('classes/edge_gateway_manager.php');

use classes\edge_gateway_manager;

it('keeps relay dispatch and discovery queueing on the edge gateway manager', function (): void {
    $managerSource = file_get_contents(app_path('classes/edge_gateway_manager.php'));
    $operationServiceSource = file_get_contents(app_path('classes/edge_gateway_operation_service.php'));

    expect($managerSource)->not->toBeFalse();
    expect($managerSource)->toContain('public function queueDiscovery');
    expect($managerSource)->toContain('public function dispatchRelayStatus');
    expect($managerSource)->toContain('public function dispatchRelayStatusLocalOnly');
    expect($managerSource)->toContain('public function dispatchRelaySwitch');
    expect($managerSource)->toContain('public function dispatchRelaySwitchLocalOnly');
    expect($managerSource)->toContain('public function dispatchRelaySwitchWithTimer');
    expect($managerSource)->toContain('public function dispatchRelaySwitchLocalOnlyWithTimer');
    expect($managerSource)->toContain('public function queueRelayStatusBatch');
    expect($managerSource)->toContain('public function queueRelaySwitchBatch');
    expect($managerSource)->toContain('public function relayBatchStatus');
    expect($managerSource)->toContain('private function createCommandJob');
    expect($managerSource)->toContain('private function queueRelayBatch');
    expect($managerSource)->toContain("'GET_RELAY_STATUS'");
    expect($managerSource)->toContain("'SET_RELAY_STATE'");
    expect($managerSource)->toContain("'relayId' => \$logicalRelayId");
    expect($managerSource)->toContain("'deviceId' => \$binding['device_id']");
    expect($managerSource)->toContain("'localIp' => \$binding['local_ip']");
    expect($managerSource)->toContain("'channel' => (int)\$binding['channel']");
    expect($managerSource)->toContain("'on' => \$on");
    expect($managerSource)->toContain("'require_fast_path' => \$requireFastLocalPath");
    expect($managerSource)->toContain('private function resolveRelayBindingDeviceGeneration');
    expect($managerSource)->toContain('private function normalizeDeviceCapabilities');
    expect($managerSource)->toContain("\$request['device_generation'] = \$deviceGeneration;");
    expect($managerSource)->toContain("\$request['toggle_after'] = \$toggleAfterSeconds;");
    expect($managerSource)->toContain('private function expireTimedOutRelayStatusCommandJobs');
    expect($managerSource)->toContain("AND command_type = 'GET_RELAY_STATUS'");
    expect($managerSource)->toContain("'Edge gateway command timed out', null, 'TIMED_OUT'");
    expect($managerSource)->toContain('private function normalizeCommandFailureStatus');
    expect($managerSource)->toContain('private function isCommandTimeoutError');
    expect($managerSource)->toContain("'batch_id' => \$batchId");
    expect($managerSource)->toContain("'batch_dedupe_key' => \$batchDedupeKey");
    expect($managerSource)->toContain("'idempotency_key' => \$batchId . ':' . (string)\$command['target']");
    expect($managerSource)->toContain("'no_auto_retry' => \$isGatePulse");
    expect($managerSource)->toContain('private function commandDisallowsAutomaticRetry');
    expect($managerSource)->toContain('private function requeueCommandForApiPolling');
    expect($managerSource)->toContain("'preferred_channel' => self::DELIVERY_CHANNEL_API");
    expect($managerSource)->toContain("'fallback_reason' => 'broker_dispatch_failed'");
    expect($managerSource)->toContain("\$this->requeueCommandForApiPolling(\$job, \$exception->getMessage());");
    expect($managerSource)->toContain('self::COMMAND_POLL_TIMEOUT_SECONDS + 5');
    expect($managerSource)->toContain('return $this->waitForCommandResult((int)$job->id, $waitTimeoutSeconds);');
    expect($managerSource)->toContain('private function buildRelayBatchDedupeKey');
    expect($managerSource)->toContain('private function findRecentRelayBatchByDedupeKey');
    expect($managerSource)->toContain('private function enforceRelayCommandRateLimit');
    expect($managerSource)->toContain('RELAY_GATE_RATE_LIMIT_PER_MINUTE');
    expect($managerSource)->toContain("JSON_EXTRACT(delivery_json, \\'$.no_auto_retry\\')");
    expect($managerSource)->toContain('not retrying non-idempotent gate pulse');
    expect($managerSource)->toContain('UNKNOWN_OUTCOME');
    expect($managerSource)->toContain('recent_unknown_gate_outcomes');
    expect($managerSource)->toContain('recent_relay_failure_rate');
    expect($managerSource)->toContain('local_transport_override');
    expect($managerSource)->toContain('private function resolveRelayBindingLocalIp');
    expect($managerSource)->toContain('private function findShellyCloudRelayLocalIp');
    expect($managerSource)->toContain('private function backfillRelayBindingLocalIpFromInventory');
    expect($managerSource)->toContain('$bindingObject->local_ip->set($localIp);');
    expect($managerSource)->toContain("'local_ip' => \$localIp");
    expect($managerSource)->toContain('(new shelly_relay_inventory())->listRelayOptions(true)');
    expect($managerSource)->toContain('public function buildUpdateOperationRequest');
    expect($managerSource)->toContain('public function rotateGatewayCredentials');
    expect($operationServiceSource)->toContain('public function queueOperation');
    expect($operationServiceSource)->toContain('public function cancelOperation');
    expect($operationServiceSource)->toContain('public function claimNextOperation');
    expect($operationServiceSource)->toContain('public function completeAgentOperation');
    expect($operationServiceSource)->toContain("public const STATUS_CANCEL_REQUESTED = 'CANCEL_REQUESTED';");
    expect($operationServiceSource)->toContain("public const STATUS_CANCELLED = 'CANCELLED';");
    expect($operationServiceSource)->toContain("public const ERROR_CANCELLED = 'EDGE_GATEWAY_CANCELLED';");
    expect($operationServiceSource)->toContain('public const OPERATION_LEASE_SECONDS = 45;');
    expect($operationServiceSource)->toContain('private function refreshOperationLease');
    expect($operationServiceSource)->toContain('agent_instance_id');
    expect($operationServiceSource)->toContain('lease_expires_at');
    expect($operationServiceSource)->toContain("'operation_type' => \$type");
    expect($managerSource)->toContain("command_type");
});

it('resolves relay bindings without requiring a primary department gateway first', function (): void {
    $managerSource = file_get_contents(app_path('classes/edge_gateway_manager.php'));

    expect($managerSource)->not->toBeFalse();
    preg_match(
        '/public function resolveRelayBinding\(int \$departmentId, string \$logicalRelayId\): array\s*\{(?P<body>.*?)\n\x20{4}\x7d\n\n\x20{4}\/\*\*/s',
        (string)$managerSource,
        $matches
    );

    expect($matches)->toHaveKey('body');
    $body = (string)$matches['body'];

    expect($body)->toContain("'department_id' => \$departmentId")
        ->and($body)->toContain("'relay_id' => \$logicalRelayId")
        ->and($body)->not->toContain('getPrimaryGatewayForDepartment')
        ->and($body)->toContain('(new edge_gateways_o())->select((int)$row[\'gateway_id\'])')
        ->and($body)->toContain('No active edge gateway found for relay')
        ->and($body)->toContain('statusPriority')
        ->and($body)->toContain('heartbeatTimestamp');
});

it('reactivates soft-deleted relay bindings before inserting replacements', function (): void {
    $managerSource = file_get_contents(app_path('classes/edge_gateway_manager.php'));

    expect($managerSource)->not->toBeFalse();
    preg_match(
        '/public function setRelayBindings\(int \$gatewayId, array \$bindings, \?int \$userId = null\): array\s*\{(?P<body>.*?)\n\x20{4}\x7d\n\n\x20{4}\/\*\*/s',
        (string)$managerSource,
        $matches
    );

    expect($matches)->toHaveKey('body');
    $body = (string)$matches['body'];

    expect($body)->toContain("'gateway_id' => \$gatewayId")
        ->and($body)->toContain("'relay_id' => \$relayId")
        ->and($body)->toContain('$bindingObject->deleted_at->set(null);')
        ->and($body)->not->toContain("'deleted_at' => null,\n            ], ['id']);");
});

it('loads relay command helpers on the manager and gateway operations on the dedicated service', function (): void {
    $reflection = new ReflectionClass(edge_gateway_manager::class);
    $operationServiceReflection = new ReflectionClass(\classes\edge_gateway_operation_service::class);

    expect($reflection->hasMethod('queueDiscovery'))->toBeTrue();
    expect($reflection->hasMethod('pollCommand'))->toBeTrue();
    expect($reflection->hasMethod('submitCommandResult'))->toBeTrue();
    expect($reflection->hasMethod('dispatchRelayStatus'))->toBeTrue();
    expect($reflection->hasMethod('dispatchRelayStatusLocalOnly'))->toBeTrue();
    expect($reflection->hasMethod('dispatchRelaySwitch'))->toBeTrue();
    expect($reflection->hasMethod('dispatchRelaySwitchLocalOnly'))->toBeTrue();
    expect($reflection->hasMethod('dispatchRelaySwitchWithTimer'))->toBeTrue();
    expect($reflection->hasMethod('dispatchRelaySwitchLocalOnlyWithTimer'))->toBeTrue();
    expect($reflection->hasMethod('queueRelayStatusBatch'))->toBeTrue();
    expect($reflection->hasMethod('queueRelaySwitchBatch'))->toBeTrue();
    expect($reflection->hasMethod('relayBatchStatus'))->toBeTrue();
    expect($reflection->hasMethod('claimNextCommandJob'))->toBeTrue();
    expect($reflection->getMethod('claimNextCommandJob')->isPrivate())->toBeTrue();
    expect($reflection->hasMethod('syncDeviceInventory'))->toBeTrue();
    expect($reflection->getMethod('syncDeviceInventory')->isPrivate())->toBeTrue();
    expect($reflection->hasMethod('resolveRelayBindingDeviceGeneration'))->toBeTrue();
    expect($reflection->getMethod('resolveRelayBindingDeviceGeneration')->isPrivate())->toBeTrue();
    expect($reflection->hasMethod('normalizeDeviceCapabilities'))->toBeTrue();
    expect($reflection->getMethod('normalizeDeviceCapabilities')->isPrivate())->toBeTrue();
    expect($reflection->hasMethod('resolveRelayBindingLocalIp'))->toBeTrue();
    expect($reflection->getMethod('resolveRelayBindingLocalIp')->isPrivate())->toBeTrue();
    expect($reflection->hasMethod('backfillRelayBindingLocalIpFromInventory'))->toBeTrue();
    expect($reflection->getMethod('backfillRelayBindingLocalIpFromInventory')->isPrivate())->toBeTrue();
    expect($reflection->hasMethod('syncGatewayInventory'))->toBeTrue();
    expect($operationServiceReflection->hasMethod('queueOperation'))->toBeTrue();
    expect($operationServiceReflection->hasMethod('cancelOperation'))->toBeTrue();
    expect($operationServiceReflection->hasMethod('listOperations'))->toBeTrue();
    expect($operationServiceReflection->hasMethod('claimNextOperation'))->toBeTrue();
    expect($operationServiceReflection->hasMethod('appendAgentOperationEvent'))->toBeTrue();
    expect($operationServiceReflection->hasMethod('completeAgentOperation'))->toBeTrue();
});
