<?php

app_require('classes/edge_gateway_manager.php');
app_require('classes/gateway_shelly_transport.php');

use classes\edge_gateway_manager;
use classes\gateway_shelly_transport;

class GatewayShellyTransportManagerFake extends edge_gateway_manager
{
    /** @var array<int,array<string,mixed>> */
    public array $statusCalls = [];
    /** @var array<int,array<string,mixed>> */
    public array $switchCalls = [];
    /** @var array<int,array<string,mixed>> */
    public array $localOnlyStatusCalls = [];
    /** @var array<int,array<string,mixed>> */
    public array $localOnlySwitchCalls = [];
    /** @var array<int,array<string,mixed>> */
    public array $batchStatusCalls = [];
    /** @var array<int,array<string,mixed>> */
    public array $batchSwitchCalls = [];
    public ?Exception $statusException = null;
    public ?Exception $switchException = null;

    public function __construct()
    {
    }

    public function dispatchRelayStatus(int $departmentId, string $logicalRelayId, array $actionContext = []): array
    {
        if ($this->statusException instanceof Exception) {
            throw $this->statusException;
        }

        $this->statusCalls[] = [
            'department_id' => $departmentId,
            'relay_id' => $logicalRelayId,
        ];

        return [
            'online' => true,
            'on' => $logicalRelayId === 'relay-machine',
            'binding' => [
                'logical_relay_id' => $logicalRelayId,
                'device_id' => 'device-' . $logicalRelayId,
                'local_ip' => '192.168.1.50',
                'channel' => 0,
            ],
            'execution' => [
                'transport' => 'gateway',
                'fallback_mode' => 'PREFER_LOCAL',
            ],
            'raw' => ['source' => 'status'],
        ];
    }

    public function dispatchRelaySwitch(int $departmentId, string $logicalRelayId, bool $on, array $actionContext = []): array
    {
        return $this->dispatchRelaySwitchWithTimer($departmentId, $logicalRelayId, $on, null, $actionContext);
    }

    public function dispatchRelaySwitchWithTimer(
        int $departmentId,
        string $logicalRelayId,
        bool $on,
        ?int $toggleAfterSeconds,
        array $actionContext = []
    ): array
    {
        if ($this->switchException instanceof Exception) {
            throw $this->switchException;
        }

        $call = [
            'department_id' => $departmentId,
            'relay_id' => $logicalRelayId,
            'on' => $on,
        ];
        if ($toggleAfterSeconds !== null) {
            $call['toggle_after'] = $toggleAfterSeconds;
        }
        $this->switchCalls[] = $call;

        return [
            'online' => true,
            'on' => $on,
            'raw' => ['source' => 'switch'],
        ];
    }

    public function dispatchRelayStatusLocalOnly(int $departmentId, string $logicalRelayId, array $actionContext = []): array
    {
        $this->localOnlyStatusCalls[] = [
            'department_id' => $departmentId,
            'relay_id' => $logicalRelayId,
        ];

        return [
            'online' => true,
            'on' => true,
            'raw' => ['source' => 'local-status'],
        ];
    }

    public function dispatchRelaySwitchLocalOnly(int $departmentId, string $logicalRelayId, bool $on, array $actionContext = []): array
    {
        return $this->dispatchRelaySwitchLocalOnlyWithTimer($departmentId, $logicalRelayId, $on, null, $actionContext);
    }

    public function dispatchRelaySwitchLocalOnlyWithTimer(
        int $departmentId,
        string $logicalRelayId,
        bool $on,
        ?int $toggleAfterSeconds,
        array $actionContext = []
    ): array
    {
        $call = [
            'department_id' => $departmentId,
            'relay_id' => $logicalRelayId,
            'on' => $on,
        ];
        if ($toggleAfterSeconds !== null) {
            $call['toggle_after'] = $toggleAfterSeconds;
        }
        $this->localOnlySwitchCalls[] = $call;

        return [
            'online' => true,
            'on' => $on,
            'raw' => ['source' => 'local-switch'],
        ];
    }

    public function queueRelayStatusBatch(
        int $departmentId,
        array $requests,
        ?int $userId = null,
        bool $requireFastLocalPath = false
    ): array {
        $this->batchStatusCalls[] = [
            'department_id' => $departmentId,
            'requests' => $requests,
            'local_only' => $requireFastLocalPath,
        ];

        return [
            'batch_id' => 'batch-status',
            'status' => 'PENDING',
            'items' => $requests,
        ];
    }

    public function queueRelaySwitchBatch(
        int $departmentId,
        array $requests,
        ?int $userId = null,
        bool $requireFastLocalPath = false
    ): array {
        $this->batchSwitchCalls[] = [
            'department_id' => $departmentId,
            'requests' => $requests,
            'local_only' => $requireFastLocalPath,
        ];

        return [
            'batch_id' => 'batch-switch',
            'status' => 'PENDING',
            'items' => $requests,
        ];
    }
}

it('maps gateway relay status responses into the Shelly cloud payload shape', function (): void {
    $manager = new GatewayShellyTransportManagerFake();
    $transport = new gateway_shelly_transport($manager);

    $result = $transport->sendPostRequest('/v2/devices/api/get', [
        'ids' => ['relay-machine', 'relay-cleaner'],
    ], 17);

    expect($manager->statusCalls)->toBe([
        ['department_id' => 17, 'relay_id' => 'relay-machine'],
        ['department_id' => 17, 'relay_id' => 'relay-cleaner'],
    ]);
    expect($result)->toBeArray();
    expect($result[0]['id'])->toBe('relay-machine');
    expect($result[0]['relay_id'])->toBe('relay-machine');
    expect($result[0]['status']['switch:0']['output'])->toBeTrue();
    expect($result[0]['binding'])->toMatchArray([
        'logical_relay_id' => 'relay-machine',
        'device_id' => 'device-relay-machine',
        'local_ip' => '192.168.1.50',
        'channel' => 0,
    ]);
    expect($result[0]['execution'])->toMatchArray([
        'transport' => 'gateway',
        'fallback_mode' => 'PREFER_LOCAL',
    ]);
    expect($result[1]['status']['switch:0']['output'])->toBeFalse();
});

it('maps gateway relay switch responses into the Shelly cloud payload shape', function (): void {
    $manager = new GatewayShellyTransportManagerFake();
    $transport = new gateway_shelly_transport($manager);

    $result = $transport->sendPostRequest('/v2/devices/api/set/switch', [
        'id' => 'relay-machine',
        'on' => false,
    ], 17);

    expect($manager->switchCalls)->toBe([
        ['department_id' => 17, 'relay_id' => 'relay-machine', 'on' => false],
    ]);
    expect($result)->toBeArray();
    expect($result[0]['id'])->toBe('relay-machine');
    expect($result[0]['status']['switch:0']['output'])->toBeFalse();
});

it('uses local-only gateway dispatch for explicit local transport overrides', function (): void {
    $manager = new GatewayShellyTransportManagerFake();
    $transport = new gateway_shelly_transport($manager, true);

    $status = $transport->sendPostRequest('/v2/devices/api/get', [
        'ids' => ['relay-entry'],
    ], 17);
    $switch = $transport->sendPostRequest('/v2/devices/api/set/switch', [
        'id' => 'relay-entry',
        'on' => true,
    ], 17);

    expect($manager->localOnlyStatusCalls)->toBe([
        ['department_id' => 17, 'relay_id' => 'relay-entry'],
    ]);
    expect($manager->localOnlySwitchCalls)->toBe([
        ['department_id' => 17, 'relay_id' => 'relay-entry', 'on' => true],
    ]);
    expect($manager->statusCalls)->toBe([]);
    expect($manager->switchCalls)->toBe([]);
    expect($status[0]['raw']['source'])->toBe('local-status');
    expect($switch[0]['raw']['source'])->toBe('local-switch');
});

it('forwards Shelly toggle_after timers to gateway relay switches', function (): void {
    $manager = new GatewayShellyTransportManagerFake();
    $transport = new gateway_shelly_transport($manager, true);

    $transport->sendPostRequest('/v2/devices/api/set/switch', [
        'id' => 'relay-entry',
        'on' => true,
        'toggle_after' => 1,
    ], 17);

    expect($manager->localOnlySwitchCalls)->toBe([
        [
            'department_id' => 17,
            'relay_id' => 'relay-entry',
            'on' => true,
            'toggle_after' => 1,
        ],
    ]);
});

it('queues gateway relay batch status requests through the manager', function (): void {
    $manager = new GatewayShellyTransportManagerFake();
    $transport = new gateway_shelly_transport($manager);

    $result = $transport->sendPostRequest('/v2/devices/api/batch/get', [
        'commands' => [
            ['target' => 'MACHINE', 'relay_id' => 'relay-machine'],
            ['target' => 'ENTRANCE', 'relay_id' => 'relay-in'],
        ],
    ], 17);

    expect($manager->batchStatusCalls)->toHaveCount(1)
        ->and($manager->batchStatusCalls[0]['department_id'])->toBe(17)
        ->and($manager->batchStatusCalls[0]['requests'])->toBe([
            ['target' => 'MACHINE', 'relay_id' => 'relay-machine'],
            ['target' => 'ENTRANCE', 'relay_id' => 'relay-in'],
        ])
        ->and($result['batch_id'])->toBe('batch-status');
});

it('queues gateway relay batch switch requests through the manager', function (): void {
    $manager = new GatewayShellyTransportManagerFake();
    $transport = new gateway_shelly_transport($manager, true);

    $result = $transport->sendPostRequest('/v2/devices/api/batch/set/switch', [
        'commands' => [
            ['target' => 'MACHINE', 'relay_id' => 'relay-machine', 'on' => true],
            ['target' => 'EXIT', 'relay_id' => 'relay-out', 'on' => true, 'toggle_after' => 1],
        ],
    ], 17);

    expect($manager->batchSwitchCalls)->toHaveCount(1)
        ->and($manager->batchSwitchCalls[0]['local_only'])->toBeTrue()
        ->and($manager->batchSwitchCalls[0]['requests'])->toBe([
            ['target' => 'MACHINE', 'relay_id' => 'relay-machine', 'on' => true, 'toggle_after' => null],
            ['target' => 'EXIT', 'relay_id' => 'relay-out', 'on' => true, 'toggle_after' => 1],
        ])
        ->and($result['batch_id'])->toBe('batch-switch');
});

it('requires a valid department id for gateway transport requests', function (): void {
    $transport = new gateway_shelly_transport(new GatewayShellyTransportManagerFake());

    expect(fn() => $transport->sendPostRequest('/v2/devices/api/get', ['ids' => ['relay-machine']], null))
        ->toThrow(Exception::class, 'A department_id is required for gateway Shelly transport');
});

it('rejects unsupported gateway transport endpoints', function (): void {
    $transport = new gateway_shelly_transport(new GatewayShellyTransportManagerFake());

    expect(fn() => $transport->sendPostRequest('/v2/devices/api/unknown', [], 17))
        ->toThrow(Exception::class, 'Unsupported gateway Shelly transport endpoint');
});

it('surfaces binding lookup failures while resolving relay state through the gateway transport', function (): void {
    $manager = new GatewayShellyTransportManagerFake();
    $manager->statusException = new Exception('Relay binding missing');
    $transport = new gateway_shelly_transport($manager);

    expect(fn() => $transport->sendPostRequest('/v2/devices/api/get', [
        'ids' => ['relay-machine'],
    ], 17))->toThrow(Exception::class, 'Relay binding missing');
});

it('surfaces offline gateway failures while dispatching relay switch commands', function (): void {
    $manager = new GatewayShellyTransportManagerFake();
    $manager->switchException = new Exception('Gateway agent is offline');
    $transport = new gateway_shelly_transport($manager);

    expect(fn() => $transport->sendPostRequest('/v2/devices/api/set/switch', [
        'id' => 'relay-machine',
        'on' => true,
    ], 17))->toThrow(Exception::class, 'Gateway agent is offline');
});
