<?php

function edge_gateway_relay_timer_free_port(): int
{
    $socket = stream_socket_server('tcp://127.0.0.1:0', $errno, $errstr);
    if ($socket === false) {
        throw new RuntimeException('Unable to allocate test port: ' . $errstr);
    }

    $address = (string)stream_socket_get_name($socket, false);
    fclose($socket);

    return (int)substr(strrchr($address, ':'), 1);
}

/**
 * @return array{process:resource,pipes:array<int,resource>}
 */
function edge_gateway_relay_timer_start_php_server(int $port, string $router, array $environment = []): array
{
    $command = escapeshellarg(PHP_BINARY) . ' -S 127.0.0.1:' . $port . ' ' . escapeshellarg($router);
    $descriptors = [
        0 => ['pipe', 'r'],
        1 => ['pipe', 'w'],
        2 => ['pipe', 'w'],
    ];
    $processEnvironment = [];
    foreach (array_merge($_ENV, $_SERVER, $environment) as $key => $value) {
        if (is_scalar($value) || $value === null) {
            $processEnvironment[(string)$key] = (string)$value;
        }
    }

    $process = proc_open(
        $command,
        $descriptors,
        $pipes,
        app_path(),
        $processEnvironment
    );

    if (!is_resource($process)) {
        throw new RuntimeException('Unable to start PHP test server');
    }

    foreach ($pipes as $pipe) {
        stream_set_blocking($pipe, false);
    }

    $deadline = microtime(true) + 5.0;
    do {
        set_error_handler(static fn(): bool => true);
        $connection = @fsockopen('127.0.0.1', $port, $errno, $errstr, 0.1);
        restore_error_handler();
        if (is_resource($connection)) {
            fclose($connection);
            return ['process' => $process, 'pipes' => $pipes];
        }

        usleep(50000);
    } while (microtime(true) < $deadline);

    edge_gateway_relay_timer_stop_php_server(['process' => $process, 'pipes' => $pipes]);
    throw new RuntimeException('Timed out waiting for PHP test server on port ' . $port);
}

/**
 * @param array{process:resource,pipes:array<int,resource>}|null $server
 */
function edge_gateway_relay_timer_stop_php_server(?array $server): void
{
    if ($server === null) {
        return;
    }

    foreach ($server['pipes'] as $pipe) {
        if (is_resource($pipe)) {
            fclose($pipe);
        }
    }

    if (is_resource($server['process'])) {
        proc_terminate($server['process']);
        proc_close($server['process']);
    }
}

/**
 * @return array{status:int,body:array<string,mixed>,raw:string}
 */
function edge_gateway_relay_timer_post_json(int $port, string $path, array $payload): array
{
    $context = stream_context_create([
        'http' => [
            'method' => 'POST',
            'header' => implode("\r\n", [
                'Content-Type: application/json',
                'X-TruckWash-Worker-Token: worker-token',
            ]) . "\r\n",
            'content' => json_encode($payload, JSON_UNESCAPED_SLASHES),
            'ignore_errors' => true,
            'timeout' => 5,
        ],
    ]);

    $raw = file_get_contents('http://127.0.0.1:' . $port . $path, false, $context);
    if ($raw === false) {
        throw new RuntimeException('Worker request failed');
    }

    $status = 0;
    foreach ($http_response_header ?? [] as $header) {
        if (preg_match('/^HTTP\/\S+\s+(\d+)/', $header, $matches) === 1) {
            $status = (int)$matches[1];
            break;
        }
    }

    $decoded = json_decode($raw, true);
    if (!is_array($decoded)) {
        throw new RuntimeException('Worker returned invalid JSON: ' . $raw);
    }

    return ['status' => $status, 'body' => $decoded, 'raw' => $raw];
}

function edge_gateway_relay_timer_fake_shelly_router(string $directory): string
{
    $router = $directory . DIRECTORY_SEPARATOR . 'fake-shelly.php';
    file_put_contents($router, <<<'PHP'
<?php

$logPath = (string)getenv('SHELLY_REQUEST_LOG');
if ($logPath !== '') {
    file_put_contents($logPath, (string)($_SERVER['REQUEST_URI'] ?? '/') . PHP_EOL, FILE_APPEND);
}

header('Content-Type: application/json; charset=utf-8');
$path = (string)(parse_url((string)($_SERVER['REQUEST_URI'] ?? '/'), PHP_URL_PATH) ?? '/');
if (str_contains($path, '/rpc/Switch.Set') || str_contains($path, '/rpc/Switch.GetStatus')) {
    echo json_encode(['output' => true], JSON_UNESCAPED_SLASHES);
    return;
}

if (preg_match('#/relay/\d+#', $path) === 1) {
    echo json_encode(['ison' => true], JSON_UNESCAPED_SLASHES);
    return;
}

echo json_encode(['ok' => true], JSON_UNESCAPED_SLASHES);
PHP);

    return $router;
}

it('passes direct relay switch timers through the LAN worker and prefers legacy timers for Gen1 relays', function (): void {
    $tempDir = sys_get_temp_dir() . DIRECTORY_SEPARATOR . 'edge-gateway-relay-timer-' . bin2hex(random_bytes(6));
    mkdir($tempDir, 0777, true);
    $logPath = $tempDir . DIRECTORY_SEPARATOR . 'shelly.log';
    $fakeServer = null;
    $workerServer = null;

    try {
        $fakePort = edge_gateway_relay_timer_free_port();
        $workerPort = edge_gateway_relay_timer_free_port();
        $fakeServer = edge_gateway_relay_timer_start_php_server(
            $fakePort,
            edge_gateway_relay_timer_fake_shelly_router($tempDir),
            ['SHELLY_REQUEST_LOG' => $logPath]
        );
        $workerServer = edge_gateway_relay_timer_start_php_server(
            $workerPort,
            app_path('resources/edge-gateway-agent/lan-worker.php'),
            ['TRUCKWASH_WORKER_TOKEN' => 'worker-token']
        );

        $response = edge_gateway_relay_timer_post_json($workerPort, '/relay/switch', [
            'local_ip' => '127.0.0.1:' . $fakePort,
            'channel' => 0,
            'on' => true,
            'timer' => 3,
            'device_generation' => 1,
        ]);

        $requests = file($logPath, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES) ?: [];
        expect($response['status'])->toBe(200)
            ->and($response['body']['on'])->toBeTrue()
            ->and($requests[0] ?? null)->toBe('/relay/0?turn=on&timer=3');
    } finally {
        edge_gateway_relay_timer_stop_php_server($workerServer);
        edge_gateway_relay_timer_stop_php_server($fakeServer);
        array_map('unlink', glob($tempDir . DIRECTORY_SEPARATOR . '*') ?: []);
        @rmdir($tempDir);
    }
});

it('caps batch relay switch timers and sends Gen3 relays through Shelly RPC first', function (): void {
    $tempDir = sys_get_temp_dir() . DIRECTORY_SEPARATOR . 'edge-gateway-relay-batch-' . bin2hex(random_bytes(6));
    mkdir($tempDir, 0777, true);
    $logPath = $tempDir . DIRECTORY_SEPARATOR . 'shelly.log';
    $fakeServer = null;
    $workerServer = null;

    try {
        $fakePort = edge_gateway_relay_timer_free_port();
        $workerPort = edge_gateway_relay_timer_free_port();
        $fakeServer = edge_gateway_relay_timer_start_php_server(
            $fakePort,
            edge_gateway_relay_timer_fake_shelly_router($tempDir),
            ['SHELLY_REQUEST_LOG' => $logPath]
        );
        $workerServer = edge_gateway_relay_timer_start_php_server(
            $workerPort,
            app_path('resources/edge-gateway-agent/lan-worker.php'),
            ['TRUCKWASH_WORKER_TOKEN' => 'worker-token']
        );

        $response = edge_gateway_relay_timer_post_json($workerPort, '/relay/batch-switch', [
            'batch_id' => 'batch-1',
            'commands' => [[
                'target' => 'EXIT',
                'relay_id' => 'relay-out',
                'local_ip' => '127.0.0.1:' . $fakePort,
                'channel' => 0,
                'on' => true,
                'toggle_after' => 999,
                'device_model' => 'S3SW-001X8EU',
            ]],
        ]);

        $requests = file($logPath, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES) ?: [];
        expect($response['status'])->toBe(200)
            ->and($response['body']['results'][0]['ok'])->toBeTrue()
            ->and($response['body']['results'][0]['relay_id'])->toBe('relay-out')
            ->and($requests[0] ?? null)->toBe('/rpc/Switch.Set?id=0&on=true&toggle_after=5');
    } finally {
        edge_gateway_relay_timer_stop_php_server($workerServer);
        edge_gateway_relay_timer_stop_php_server($fakeServer);
        array_map('unlink', glob($tempDir . DIRECTORY_SEPARATOR . '*') ?: []);
        @rmdir($tempDir);
    }
});

it('keeps PHP edge-agent relay fallback propagation aligned with LAN worker timer handling', function (): void {
    $agentSource = (string)file_get_contents(app_path('resources/edge-gateway-agent/agent.php'));

    expect($agentSource)->toContain('private const MAX_RELAY_TOGGLE_AFTER_SECONDS = 5;')
        ->and($agentSource)->toContain('$workerPayload[\'toggle_after\'] = $toggleAfter;')
        ->and($agentSource)->toContain('$workerPayload[\'device_generation\'] = $deviceGeneration;')
        ->and($agentSource)->toContain('private function resolveRelayToggleAfterSeconds(array $request): ?int')
        ->and($agentSource)->toContain('private function resolveShellyCommandGeneration(array $request): ?int')
        ->and($agentSource)->toContain('$deviceGeneration === 1')
        ->and($agentSource)->toContain('rawurlencode((string)$toggleAfter)')
        ->and($agentSource)->toContain("'&timer='");
});
