<?php

declare(strict_types=1);

use classes\db;
use classes\edge_gateway_manager;
use classes\edge_gateway_operation_service;
use Predis\Client as PredisClient;
use Tests\Support\Api\ApiCleanup;
use Tests\Support\Api\ApiFixtures;
use Tests\Support\Api\ApiSchemaBootstrap;

require_once dirname(__DIR__, 2) . '/Support/Api/ApiCleanup.php';
require_once dirname(__DIR__, 2) . '/Support/Api/ApiFixtures.php';
require_once dirname(__DIR__, 2) . '/Support/Api/ApiSchemaBootstrap.php';

app_require('classes/db.php');
app_require('classes/edge_gateway_manager.php');
app_require('classes/edge_gateway_operation_service.php');

it('persists install-session updates and derives gateway runtime status from heartbeats', function (): void {
    $context = edge_gateway_integration_context();

    try {
        $department = $context['fixtures']->createDepartment([
            'name' => 'Edge Integration Install Department',
        ]);

        $token = $context['manager']->createInstallToken((int)$department['id'], 'Integration Install Token');
        $claimTokenId = (int)($token['claim_token_id'] ?? 0);

        $context['manager']->reportInstallTokenStatus((string)$token['token'], [
            'status' => 'FAILED',
            'step' => 'DOWNLOAD_FAILED',
            'message' => 'The installer could not download the runtime bundle.',
            'diagnostics' => [
                'diag-1',
                'diag-2',
                'diag-3',
                'diag-4',
                'diag-5',
                'diag-6',
                'diag-7',
                'diag-8',
            ],
        ]);

        $failedStatus = $context['manager']->getInstallTokenStatus($claimTokenId);

        expect($failedStatus)
            ->toHaveKey('status', 'FAILED')
            ->toHaveKey('step', 'DOWNLOAD_FAILED')
            ->toHaveKey('last_error', 'The installer could not download the runtime bundle.')
            ->and($failedStatus['diagnostics'] ?? [])
            ->toBeArray()
            ->toHaveCount(6);

        $claimed = $context['manager']->claimGateway((string)$token['token'], 'edge-integration-host', 'php-agent-v1', [
            'source' => 'integration-test',
        ]);

        $gatewayId = (int)($claimed['gateway']['id'] ?? 0);
        $agentToken = (string)($claimed['agent_token'] ?? '');
        expect($gatewayId)->toBeGreaterThan(0)
            ->and($agentToken)->not->toBe('');

        $claimedStatus = $context['manager']->getInstallTokenStatus($claimTokenId);
        expect($claimedStatus)
            ->toHaveKey('status', 'CLAIMED')
            ->toHaveKey('gateway_id', $gatewayId)
            ->toHaveKey('last_error', null);

        edge_gateway_integration_set_heartbeat_age($context['mysqli'], $context['redis'], $gatewayId, edge_gateway_manager::HEARTBEAT_DEGRADED_AFTER_SECONDS + 1);
        $degraded = $context['manager']->getGateway($gatewayId);
        expect($degraded)->toHaveKey('status', 'DEGRADED');

        edge_gateway_integration_set_heartbeat_age($context['mysqli'], $context['redis'], $gatewayId, edge_gateway_manager::HEARTBEAT_OFFLINE_AFTER_SECONDS + 1);
        $offline = $context['manager']->getGateway($gatewayId);
        expect($offline)->toHaveKey('status', 'OFFLINE');

        $context['manager']->recordHeartbeat($gatewayId, $agentToken, [
            'status' => 'ONLINE',
            'hostname' => 'edge-integration-host',
            'metadata' => [
                'system_metrics' => [
                    'cpu_percent' => 44,
                ],
            ],
        ]);

        $online = $context['manager']->getGateway($gatewayId);
        expect($online)
            ->toHaveKey('status', 'ONLINE')
            ->and($online['metadata']['system_metrics']['cpu_percent'] ?? null)
            ->toBe(44);
    } finally {
        $context['cleanup']->run();
    }
});

it('assembles tasks, logs, statistics, operations, commands, and shell lifecycle state from persisted records', function (): void {
    $context = edge_gateway_integration_context();

    try {
        $department = $context['fixtures']->createDepartment([
            'name' => 'Edge Integration Runtime Department',
        ]);
        $user = $context['fixtures']->createUser([
            'display_name' => 'Edge Integration User',
        ]);

        $token = $context['manager']->createInstallToken((int)$department['id'], 'Integration Runtime Token', (int)$user['id']);
        $claimed = $context['manager']->claimGateway((string)$token['token'], 'edge-runtime-host', 'php-agent-v1');
        $gatewayId = (int)($claimed['gateway']['id'] ?? 0);
        $agentToken = (string)($claimed['agent_token'] ?? '');

        $operation = $context['operations']->queueOperation(
            $gatewayId,
            edge_gateway_operation_service::TYPE_DISCOVERY,
            ['inventory' => edge_gateway_integration_inventory('runtime')],
            (int)$user['id']
        );
        $operationId = (int)($operation['id'] ?? 0);
        expect($operationId)->toBeGreaterThan(0);

        $claimedOperation = $context['operations']->claimNextOperation($gatewayId, $agentToken, 0, 'integration-agent-1');
        expect($claimedOperation)
            ->toBeArray()
            ->toHaveKey('status', 'IN_PROGRESS');

        $context['operations']->appendAgentOperationEvent($gatewayId, $operationId, $agentToken, [
            'level' => 'INFO',
            'code' => 'DISCOVERY_RUNNING',
            'message' => 'Integration discovery is executing.',
            'context' => [
                'progress' => 70,
            ],
        ]);

        $context['operations']->completeAgentOperation($gatewayId, $operationId, $agentToken, [
            'ok' => true,
            'result' => [
                'inventory' => edge_gateway_integration_inventory('completed'),
            ],
        ]);

        $job = $context['fixtures']->createEdgeCommandJob([
            'gateway_id' => $gatewayId,
            'command_type' => 'GET_RELAY_STATUS',
            'request' => [
                'relayId' => 'relay-main',
            ],
            'requested_by' => (int)$user['id'],
        ]);
        $jobId = (int)($job['id'] ?? 0);

        $polledCommand = $context['manager']->pollCommand($gatewayId, $agentToken, 0);
        expect($polledCommand)
            ->toBeArray()
            ->toHaveKey('id', $jobId)
            ->toHaveKey('command_type', 'GET_RELAY_STATUS');

        $commandResult = $context['manager']->submitCommandResult($gatewayId, $jobId, $agentToken, true, [
            'relayId' => 'relay-main',
            'online' => true,
        ]);
        expect($commandResult)
            ->toHaveKey('acknowledged', true)
            ->and($commandResult['job']['status'] ?? null)
            ->toBe('COMPLETED');

        $context['manager']->recordBrokerPresence(
            $gatewayId,
            'connected',
            'broker-connection-1',
            null,
            ['transport' => 'ws']
        );

        $shellSession = $context['manager']->createShellSession(
            $gatewayId,
            (int)$user['id'],
            'Integration shell session',
            120,
            40,
            '/opt/truckwash-edge-agent'
        );
        $shellToken = (string)($shellSession['token'] ?? '');
        expect($shellToken)->not->toBe('');
        expect($shellSession['diagnostics']['broker_presence']['connection_id'] ?? null)
            ->toBe('broker-connection-1');

        $validatedShell = $context['manager']->validateShellSessionToken($shellToken);
        expect($validatedShell)->toHaveKey('status', 'PENDING');

        $openedShell = $context['manager']->markShellSessionOpened($shellToken, 'shell-connection-1');
        expect($openedShell)->toHaveKey('status', 'OPEN');

        $closedShell = $context['manager']->closeShellSessionByToken(
            $shellToken,
            "edge-shell-output\n",
            'agent_exit',
            [
                'message' => 'Integration shell completed.',
                'code' => 0,
                'stage' => 'shell_active',
            ]
        );
        expect($closedShell)
            ->toHaveKey('status', 'COMPLETED')
            ->and($closedShell['transcript'] ?? null)
            ->toBe("edge-shell-output\n")
            ->and($closedShell['metadata']['close_reason'] ?? null)
            ->toBe('agent_exit')
            ->and($closedShell['metadata']['close_stage'] ?? null)
            ->toBe('shell_active');

        $context['manager']->appendGatewayLogEntry(
            $gatewayId,
            'Integration log line',
            'INFO',
            'agent',
            'BROKER',
            ['source' => 'integration']
        );
        $context['manager']->appendRelayTransportLog(
            (int)$department['id'],
            '/v2/devices/api/set/switch',
            ['id' => 'M-7', 'on' => true, 'toggle_after' => 3],
            [['id' => 'M-7', 'online' => true, 'on' => true]],
            'cloud',
            null,
            [
                'module' => 'selfserve',
                'reason' => 'Integration relay start',
                'relay_name' => 'Roskilde Maskine',
                'relay_role' => 'MACHINE',
                'customer_number' => 700123,
                'actor' => [
                    'admin_user_id' => 42,
                ],
            ]
        );

        $context['manager']->recordTelemetryFromBroker($gatewayId, [
            'status' => 'ONLINE',
            'metadata' => [
                'system_metrics' => [
                    'cpu_percent' => 17,
                    'memory_mb' => 256,
                ],
            ],
            'inventory' => edge_gateway_integration_inventory('telemetry'),
        ]);

        $tasks = $context['manager']->buildGatewayTasksPage($gatewayId);
        $logs = $context['manager']->buildGatewayLogsPage($gatewayId);
        $statistics = $context['manager']->buildGatewayStatisticsPage($gatewayId);

        expect($tasks['operations'] ?? [])
            ->toBeArray()
            ->not->toBeEmpty()
            ->and(($tasks['operations'][0]['status'] ?? null))
            ->toBe('COMPLETED');
        expect($tasks['recent_commands'] ?? [])
            ->toBeArray()
            ->not->toBeEmpty()
            ->and(($tasks['recent_commands'][0]['status'] ?? null))
            ->toBe('COMPLETED');

        expect(edge_gateway_integration_messages($logs['log_entries'] ?? []))
            ->toContain('Integration log line');
        expect(edge_gateway_integration_messages($logs['relay_logs'] ?? []))
            ->toContain('MACHINE ON Roskilde Maskine handled by cloud via CLOUD');
        expect($logs['relay_logs'][0]['context']['module_responsible'] ?? null)
            ->toBe('selfserve')
            ->and($logs['relay_logs'][0]['context']['description'] ?? null)
            ->toBe('MACHINE ON Roskilde Maskine handled by cloud via CLOUD')
            ->and($logs['relay_logs'][0]['context']['relay_name'] ?? null)
            ->toBe('Roskilde Maskine')
            ->and($logs['relay_logs'][0]['context']['relay_role'] ?? null)
            ->toBe('MACHINE')
            ->and($logs['relay_logs'][0]['context']['associated']['customer_number'] ?? null)
            ->toBe(700123)
            ->and($logs['relay_logs'][0]['context']['associated']['admin_user_id'] ?? null)
            ->toBe(42)
            ->and($logs['relay_logs'][0]['context']['reason'] ?? null)
            ->toBe('Integration relay start')
            ->and($logs['relay_logs'][0]['context']['handler'] ?? null)
            ->toBe('cloud')
            ->and($logs['relay_logs'][0]['context']['signal']['request']['id'] ?? null)
            ->toBe('M-7')
            ->and($logs['relay_logs'][0]['context']['response']['on'] ?? null)
            ->toBeTrue();
        expect(edge_gateway_integration_messages($logs['timeline'] ?? []))
            ->toContain('Integration discovery is executing.');
        expect(array_values(array_filter($logs['timeline'] ?? [], static fn(array $entry): bool => ($entry['type'] ?? null) === 'relay')))
            ->not->toBeEmpty();
        expect($logs['shell_sessions'] ?? [])
            ->toBeArray()
            ->not->toBeEmpty()
            ->and(($logs['shell_sessions'][0]['transcript'] ?? null))
            ->toBe("edge-shell-output\n");

        expect($statistics['system_metrics']['cpu_percent'] ?? null)
            ->toBe(17);
        expect($statistics['gateway']['inventory'] ?? [])
            ->toBeArray()
            ->not->toBeEmpty();
    } finally {
        $context['cleanup']->run();
    }
});

it('persists gateway cutover relay bindings used by self-serve Shelly dispatch', function (): void {
    $context = edge_gateway_integration_context();

    try {
        $department = $context['fixtures']->createDepartment([
            'name' => 'Edge Integration Self Serve Relay Department',
        ]);
        $user = $context['fixtures']->createUser([
            'display_name' => 'Edge Integration Relay User',
        ]);

        $token = $context['manager']->createInstallToken((int)$department['id'], 'Integration Relay Token', (int)$user['id']);
        $claimed = $context['manager']->claimGateway((string)$token['token'], 'edge-relay-host', 'php-agent-v1');
        $gatewayId = (int)($claimed['gateway']['id'] ?? 0);

        $mode = $context['manager']->setDepartmentTransportMode(
            (int)$department['id'],
            edge_gateway_manager::TRANSPORT_MODE_GATEWAY,
            (int)$user['id']
        );
        $bindings = $context['manager']->setRelayBindings($gatewayId, [[
            'relay_id' => 'relay-machine',
            'device_id' => 'device-machine',
            'local_ip' => '10.50.60.70',
            'channel' => 0,
            'metadata' => [
                'fallback_mode' => edge_gateway_manager::RELAY_FALLBACK_PREFER_LOCAL,
            ],
        ]], (int)$user['id']);
        $resolved = $context['manager']->resolveRelayBinding((int)$department['id'], 'relay-machine');

        expect($mode)->toHaveKey('transport_mode', edge_gateway_manager::TRANSPORT_MODE_GATEWAY)
            ->and($bindings)->toHaveCount(1)
            ->and($resolved)->toMatchArray([
                'gateway_id' => $gatewayId,
                'department_id' => (int)$department['id'],
                'relay_id' => 'relay-machine',
                'device_id' => 'device-machine',
                'local_ip' => '10.50.60.70',
                'channel' => 0,
                'fallback_mode' => edge_gateway_manager::RELAY_FALLBACK_PREFER_LOCAL,
            ])
            ->and((array)($resolved['metadata'] ?? []))
            ->toHaveKey('fallback_mode', edge_gateway_manager::RELAY_FALLBACK_PREFER_LOCAL)
            ->and($context['manager']->getDepartmentTransportMode((int)$department['id']))
            ->toBe(edge_gateway_manager::TRANSPORT_MODE_GATEWAY);

        $context['manager']->setRelayBindings($gatewayId, [], (int)$user['id']);
        $recreatedBindings = $context['manager']->setRelayBindings($gatewayId, [[
            'relay_id' => 'relay-machine',
            'device_id' => 'device-machine-recreated',
            'local_ip' => '10.50.60.71',
            'channel' => 1,
        ]], (int)$user['id']);

        expect($recreatedBindings)->toHaveCount(1)
            ->and($recreatedBindings[0])->toMatchArray([
                'relay_id' => 'relay-machine',
                'device_id' => 'device-machine-recreated',
                'local_ip' => '10.50.60.71',
                'channel' => 1,
            ]);
    } finally {
        $context['cleanup']->run();
    }
});

/**
 * @return array{cleanup:ApiCleanup,fixtures:ApiFixtures,manager:edge_gateway_manager,operations:edge_gateway_operation_service,mysqli:mysqli,redis:?PredisClient}
 */
function edge_gateway_integration_context(): array
{
    if (!integration_enabled()) {
        test()->markTestSkipped('Set RUN_INTEGRATION_TESTS=1 to run edge gateway integration tests.');
    }

    static $bootstrapped = null;

    if ($bootstrapped === null) {
        $dbConfig = edge_gateway_integration_db_config();
        $GLOBALS['CONFIG_DB'] = $dbConfig;
        $GLOBALS['response'] = new class {
            public function internal_server_error(string $message): void
            {
                throw new RuntimeException($message);
            }
        };

        $db = edge_gateway_integration_wait_for_db($dbConfig);
        $GLOBALS['db'] = $db;

        $mysqli = $db->conn();
        mysqli_report(MYSQLI_REPORT_ERROR | MYSQLI_REPORT_STRICT);
        $mysqli->set_charset('utf8mb4');

        (new ApiSchemaBootstrap($mysqli))->ensureSchema();

        $bootstrapped = [
            'mysqli' => $mysqli,
            'redis' => edge_gateway_integration_redis_client(),
        ];
    }

    $cleanup = new ApiCleanup();

    return [
        'cleanup' => $cleanup,
        'fixtures' => new ApiFixtures($bootstrapped['mysqli'], $bootstrapped['redis'], $cleanup),
        'manager' => new edge_gateway_manager(),
        'operations' => new edge_gateway_operation_service(),
        'mysqli' => $bootstrapped['mysqli'],
        'redis' => $bootstrapped['redis'],
    ];
}

function edge_gateway_integration_wait_for_db(array $dbConfig): db
{
    $deadline = microtime(true) + 60;
    $lastError = null;

    do {
        try {
            $db = new db($dbConfig);
            $db->connect();
            return $db;
        } catch (RuntimeException $exception) {
            $lastError = $exception;
            usleep(500000);
        }
    } while (microtime(true) < $deadline);

    throw $lastError ?? new RuntimeException('Database connection failed before a connection attempt completed.');
}

/**
 * @return array{host:string,user:string,password:string,database:string,port:int}
 */
function edge_gateway_integration_db_config(): array
{
    $target = strtolower(trim((string)(getenv('CONFIG_DB_TARGET') ?: 'live')));
    if ($target !== 'debug') {
        $target = 'live';
    }

    $host = edge_gateway_integration_config_value('CONFIG_DB_HOST', 'CONFIG_DB_DEBUG_HOST', $target);
    $user = edge_gateway_integration_config_value('CONFIG_DB_USER', 'CONFIG_DB_DEBUG_USER', $target);
    $password = edge_gateway_integration_config_value('CONFIG_DB_PASSWORD', 'CONFIG_DB_DEBUG_PASSWORD', $target);
    $database = edge_gateway_integration_config_value('CONFIG_DB_DATABASE', 'CONFIG_DB_DEBUG_DATABASE', $target);
    $port = (int)(edge_gateway_integration_config_value('CONFIG_DB_PORT', 'CONFIG_DB_DEBUG_PORT', $target) ?: '3306');

    if ($host === '' || $user === '' || $database === '') {
        test()->markTestSkipped('Edge gateway integration tests require configured database environment variables.');
    }

    return [
        'host' => $host,
        'user' => $user,
        'password' => $password,
        'database' => $database,
        'port' => $port > 0 ? $port : 3306,
    ];
}

function edge_gateway_integration_redis_client(): ?PredisClient
{
    $target = strtolower(trim((string)(getenv('CONFIG_DB_TARGET') ?: 'live')));
    if ($target !== 'debug') {
        $target = 'live';
    }

    $host = edge_gateway_integration_config_value('REDIS_CONFIG_HOST', 'REDIS_CONFIG_DEBUG_HOST', $target);
    if ($host === '') {
        return null;
    }

    $parameters = [
        'scheme' => 'tcp',
        'host' => $host,
        'port' => (int)(edge_gateway_integration_config_value('REDIS_CONFIG_PORT', 'REDIS_CONFIG_DEBUG_PORT', $target) ?: '6379'),
        'database' => (int)(edge_gateway_integration_config_value('REDIS_CONFIG_DATABASE', 'REDIS_CONFIG_DEBUG_DATABASE', $target) ?: '0'),
        'password' => edge_gateway_integration_config_value('REDIS_CONFIG_PASSWORD', 'REDIS_CONFIG_DEBUG_PASSWORD', $target),
    ];

    $user = edge_gateway_integration_config_value('REDIS_CONFIG_USER', 'REDIS_CONFIG_DEBUG_USER', $target);
    if ($user !== '') {
        $parameters['username'] = $user;
    }

    return new PredisClient($parameters);
}

function edge_gateway_integration_config_value(string $liveKey, string $debugKey, string $target): string
{
    $liveValue = trim((string)(getenv($liveKey) ?: ''));
    $debugValue = trim((string)(getenv($debugKey) ?: ''));

    if ($target === 'debug' && $debugValue !== '') {
        return $debugValue;
    }

    return $liveValue;
}

function edge_gateway_integration_set_heartbeat_age(mysqli $mysqli, ?PredisClient $redis, int $gatewayId, int $secondsAgo): void
{
    $result = $mysqli->query("SELECT last_heartbeat_at, updated_at, created_at FROM edge_gateways WHERE id = " . (int)$gatewayId . " LIMIT 1");
    $row = $result instanceof mysqli_result ? $result->fetch_assoc() : null;
    if ($result instanceof mysqli_result) {
        $result->free();
    }

    $referenceTimestamp = strtotime((string)($row['last_heartbeat_at'] ?? $row['updated_at'] ?? $row['created_at'] ?? ''));
    if ($referenceTimestamp === false || $referenceTimestamp <= 0) {
        $referenceTimestamp = time();
    }

    $timestamp = date('Y-m-d H:i:s', $referenceTimestamp - max(0, $secondsAgo));
    $escaped = $mysqli->real_escape_string($timestamp);
    $mysqli->query("UPDATE edge_gateways SET last_heartbeat_at = '{$escaped}', status = 'ONLINE' WHERE id = " . (int)$gatewayId);

    if ($redis !== null) {
        foreach ([
            'obj_prop:*:' . $gatewayId . ':status',
            'obj_prop:*:' . $gatewayId . ':last_heartbeat_at',
            'obj_prop:*:' . $gatewayId . ':updated_at',
            'edge_gateway:view:v1:*',
        ] as $pattern) {
            $keys = $redis->keys($pattern);
            if (is_array($keys) && $keys !== []) {
                $redis->del($keys);
            }
        }
    }
}

function edge_gateway_integration_inventory(string $suffix): array
{
    return [[
        'device_id' => 'integration-' . $suffix,
        'local_ip' => '10.50.60.70',
        'model' => 'TruckWash Integration Gateway',
        'channel_count' => 1,
        'online' => true,
        'capabilities' => [
            'gateway_management_v2' => true,
        ],
        'metadata' => [
            'hostname' => 'integration-' . $suffix,
        ],
    ]];
}

/**
 * @param mixed $rows
 * @return array<int, string>
 */
function edge_gateway_integration_messages(mixed $rows): array
{
    if (!is_array($rows)) {
        return [];
    }

    $messages = [];
    foreach ($rows as $row) {
        if (is_array($row) && isset($row['message']) && is_string($row['message'])) {
            $messages[] = $row['message'];
        }
    }

    return $messages;
}
