<?php

app_require('modules/selfserve/traits/selfserve_lane_command_t.php');
app_require('modules/selfserve/helpers/selfserve_lane_command.php');
app_require('modules/selfserve/helpers/selfserve_lane_mode.php');
app_require('modules/selfserve/helpers/selfserve_lane_port.php');
app_require('modules/selfserve/helpers/selfserve_lane_relay.php');
app_require('modules/selfserve/helpers/selfserve_lane_state.php');
app_require('modules/selfserve/helpers/selfserve_lane_status.php');
app_require('modules/selfserve/classes/selfserve_lane_command_arguments.php');

use modules\selfserve\classes\selfserve_lane_command_arguments;
use modules\selfserve\helpers\selfserve_lane_command;
use modules\selfserve\helpers\selfserve_lane_mode;
use modules\selfserve\helpers\selfserve_lane_port;
use modules\selfserve\helpers\selfserve_lane_relay;
use modules\selfserve\helpers\selfserve_lane_state;
use modules\selfserve\helpers\selfserve_lane_status;
use modules\selfserve\traits\selfserve_lane_command_t;

class SelfserveLaneCommandValueFake
{
    public function __construct(private readonly string $value) {}

    public function value(): string
    {
        return $this->value;
    }
}

class SelfserveLaneCommandDepartmentLaneFake
{
    public SelfserveLaneCommandValueFake $relay_machine_id;
    public SelfserveLaneCommandValueFake $relay_machine_program_picker_id;
    public SelfserveLaneCommandValueFake $relay_machine_cleaner_id;

    public function __construct(string $machineRelayId, string $programRelayId, string $cleanerRelayId)
    {
        $this->relay_machine_id = new SelfserveLaneCommandValueFake($machineRelayId);
        $this->relay_machine_program_picker_id = new SelfserveLaneCommandValueFake($programRelayId);
        $this->relay_machine_cleaner_id = new SelfserveLaneCommandValueFake($cleanerRelayId);
    }
}

class SelfserveLaneStopFlowHarness
{
    use selfserve_lane_command_t;

    public const DEFAULT_WASH_START_TIME = 0;
    public const DEFAULT_CUSTOMER_NUMBER = 0;
    public const DEFAULT_LICENSE_PLATE = '';

    public int $id = 77;
    public object $department_lane;
    public int $invoiceCalls = 0;
    public int $ensureInvoiceOrderContextCalls = 0;
    public int $vehicleTypeProductAddCalls = 0;
    public int $programSelectorStatusReads = 0;
    public ?bool $lastVehicleTypeProductDecision = null;
    public ?\Throwable $openThrowable = null;
    public ?\Throwable $reportedStopTimeout = null;
    /** @var selfserve_lane_port[] */
    public array $openedPorts = [];
    /** @var selfserve_lane_relay[] */
    public array $turnedOffRelays = [];
    /** @var array<int,string> */
    public array $events = [];

    private selfserve_lane_status $laneStatus;
    private selfserve_lane_mode $laneMode;
    private selfserve_lane_state $laneState;
    private int $customerNumber = 1234;
    private string $licensePlate = 'AB12345';
    private int $washStartTime = 120;
    private ?int $reservationStartTime = null;
    private bool $bypassCustomerValidation = false;
    private bool $programSelectorOnline;
    private bool $programSelectorOn;
    private bool $machineStartTriggered;

    public function __construct(
        bool $machineStartTriggered,
        bool $programSelectorOnline = false,
        bool $programSelectorOn = true,
        string $machineRelayId = 'relay-machine',
        string $programRelayId = 'relay-program',
        string $cleanerRelayId = 'relay-cleaner'
    ) {
        $this->machineStartTriggered = $machineStartTriggered;
        $this->programSelectorOnline = $programSelectorOnline;
        $this->programSelectorOn = $programSelectorOn;
        $this->department_lane = new SelfserveLaneCommandDepartmentLaneFake(
            $machineRelayId,
            $programRelayId,
            $cleanerRelayId
        );
        $this->laneStatus = selfserve_lane_status::OCCUPIED;
        $this->laneMode = selfserve_lane_mode::MANUAL;
        $this->laneState = selfserve_lane_state::IN_WASH;
    }

    public function getLaneStatus(): selfserve_lane_status
    {
        return $this->laneStatus;
    }

    public function setLaneStatus(selfserve_lane_status $status): void
    {
        $this->laneStatus = $status;
    }

    public function setLaneMode(selfserve_lane_mode $mode): void
    {
        $this->laneMode = $mode;
    }

    public function setLaneState(selfserve_lane_state $state): void
    {
        $this->laneState = $state;
    }

    public function setWashStartTime(?int $time): void
    {
        $this->washStartTime = $time ?? self::DEFAULT_WASH_START_TIME;
    }

    public function setCustomerNumber(?int $customerNumber): void
    {
        $this->customerNumber = $customerNumber ?? self::DEFAULT_CUSTOMER_NUMBER;
    }

    public function getCustomerNumber(): int
    {
        return $this->customerNumber;
    }

    public function setLicensePlate(?string $licensePlate): void
    {
        $this->licensePlate = $licensePlate ?? self::DEFAULT_LICENSE_PLATE;
    }

    public function getLicensePlate(): string
    {
        return $this->licensePlate;
    }

    public function setReservationStartTime(?int $reservationStartTime): void
    {
        $this->reservationStartTime = $reservationStartTime;
    }

    public function isBypassCustomerNumberValidation(): bool
    {
        return $this->bypassCustomerValidation;
    }

    public function invoice(): bool
    {
        $this->invoiceCalls++;
        return true;
    }

    public function ensureInvoiceOrderContextForVehicleProduct(): bool
    {
        $this->ensureInvoiceOrderContextCalls++;
        return true;
    }

    public function addVehicleTypeProductToLastInvoiceOrder(): bool
    {
        $this->vehicleTypeProductAddCalls++;
        return true;
    }

    public function getMachineProgramPickerRelayStatus(): array
    {
        $this->programSelectorStatusReads++;
        return [
            'online' => $this->programSelectorOnline,
            'on' => $this->programSelectorOn,
        ];
    }

    public function open(selfserve_lane_port $port): bool
    {
        $this->openedPorts[] = $port;
        $this->events[] = 'open:' . $port->name;
        if ($this->openThrowable !== null) {
            throw $this->openThrowable;
        }

        return true;
    }

    public function turnOffRelay(selfserve_lane_relay $relay): bool
    {
        $this->turnedOffRelays[] = $relay;
        $this->events[] = 'relay:' . $relay->name . ':off';
        return true;
    }

    public function setRelayStatusHard(selfserve_lane_relay $relay, bool $on, ?int $toggle_after = null): bool
    {
        if ($on === false) {
            $this->turnedOffRelays[] = $relay;
            $this->events[] = 'relay:' . $relay->name . ':off';
        }
        return true;
    }

    public function logLaneAction(...$args): void {}

    protected function completeLatestSessionForStop(): void
    {
        // No-op in unit tests.
    }

    protected function hasMachineStartSignalForStop(): bool
    {
        return $this->machineStartTriggered;
    }

    protected function addVehicleTypeProductToInvoiceIfNeeded(bool $should_add): void
    {
        $this->lastVehicleTypeProductDecision = $should_add;
        if ($should_add) {
            $this->vehicleTypeProductAddCalls++;
        }
    }

    protected function reportWashStopExitTimeout(\Throwable $e): void
    {
        $this->reportedStopTimeout = $e;
    }
}

it('adds vehicle type product on STOP when the physical machine ON signal was recorded, after turning relays off before exit', function (): void {
    $lane = new SelfserveLaneStopFlowHarness(machineStartTriggered: true);
    $args = (new selfserve_lane_command_arguments())->setCustomerNumber(1234);

    $lane->execute(selfserve_lane_command::STOP, $args);

    expect($lane->invoiceCalls)->toBe(1);
    expect($lane->ensureInvoiceOrderContextCalls)->toBe(0);
    expect($lane->vehicleTypeProductAddCalls)->toBe(1);
    expect($lane->lastVehicleTypeProductDecision)->toBeTrue();
    expect($lane->programSelectorStatusReads)->toBe(0);
    expect($lane->openedPorts)->toBe([selfserve_lane_port::EXIT]);
    expect($lane->turnedOffRelays)->toBe([
        selfserve_lane_relay::MACHINE_CLEANER,
        selfserve_lane_relay::MACHINE_PROGRAM_PICKER,
        selfserve_lane_relay::MACHINE,
    ]);
    expect($lane->events)->toBe([
        'relay:MACHINE_CLEANER:off',
        'relay:MACHINE_PROGRAM_PICKER:off',
        'relay:MACHINE:off',
        'open:EXIT',
    ]);
    expect($lane->getLaneStatus())->toBe(selfserve_lane_status::AVAILABLE);
});

it('skips vehicle type product add when no physical machine ON signal was recorded and only disables configured relays', function (): void {
    $lane = new SelfserveLaneStopFlowHarness(
        machineStartTriggered: false,
        programSelectorOnline: false,
        machineRelayId: 'relay-machine',
        programRelayId: 'relay-program',
        cleanerRelayId: ''
    );
    $args = (new selfserve_lane_command_arguments())->setCustomerNumber(1234);

    $lane->execute(selfserve_lane_command::STOP, $args);

    expect($lane->invoiceCalls)->toBe(1);
    expect($lane->ensureInvoiceOrderContextCalls)->toBe(0);
    expect($lane->vehicleTypeProductAddCalls)->toBe(0);
    expect($lane->lastVehicleTypeProductDecision)->toBeFalse();
    expect($lane->programSelectorStatusReads)->toBe(0);
    expect($lane->turnedOffRelays)->toBe([
        selfserve_lane_relay::MACHINE_PROGRAM_PICKER,
        selfserve_lane_relay::MACHINE,
    ]);
    expect($lane->events)->toBe([
        'relay:MACHINE_PROGRAM_PICKER:off',
        'relay:MACHINE:off',
        'open:EXIT',
    ]);
});

it('does not use selector relay online status as machine-wash billing evidence', function (): void {
    $lane = new SelfserveLaneStopFlowHarness(
        machineStartTriggered: false,
        programSelectorOnline: true,
        programSelectorOn: true
    );
    $args = (new selfserve_lane_command_arguments())->setCustomerNumber(1234);

    $lane->execute(selfserve_lane_command::STOP, $args);

    expect($lane->invoiceCalls)->toBe(1);
    expect($lane->ensureInvoiceOrderContextCalls)->toBe(0);
    expect($lane->vehicleTypeProductAddCalls)->toBe(0);
    expect($lane->lastVehicleTypeProductDecision)->toBeFalse();
    expect($lane->programSelectorStatusReads)->toBe(0);
});

it('continues STOP when exit relay dispatch times out ambiguously', function (): void {
    $lane = new SelfserveLaneStopFlowHarness(machineStartTriggered: true);
    $timeout = new \RuntimeException('Edge gateway command timed out');
    $lane->openThrowable = $timeout;
    $args = (new selfserve_lane_command_arguments())->setCustomerNumber(1234);

    $lane->execute(selfserve_lane_command::STOP, $args);

    expect($lane->openedPorts)->toBe([selfserve_lane_port::EXIT]);
    expect($lane->reportedStopTimeout)->toBe($timeout);
    expect($lane->invoiceCalls)->toBe(1);
    expect($lane->vehicleTypeProductAddCalls)->toBe(1);
    expect($lane->turnedOffRelays)->toBe([
        selfserve_lane_relay::MACHINE_CLEANER,
        selfserve_lane_relay::MACHINE_PROGRAM_PICKER,
        selfserve_lane_relay::MACHINE,
    ]);
    expect($lane->events)->toBe([
        'relay:MACHINE_CLEANER:off',
        'relay:MACHINE_PROGRAM_PICKER:off',
        'relay:MACHINE:off',
        'open:EXIT',
    ]);
    expect($lane->getLaneStatus())->toBe(selfserve_lane_status::AVAILABLE);
});

it('still fails STOP for non-timeout exit relay errors', function (): void {
    $lane = new SelfserveLaneStopFlowHarness(machineStartTriggered: true);
    $lane->openThrowable = new \RuntimeException('Invalid relay ID for port EXIT');
    $args = (new selfserve_lane_command_arguments())->setCustomerNumber(1234);

    expect(fn() => $lane->execute(selfserve_lane_command::STOP, $args))
        ->toThrow(\RuntimeException::class, 'Invalid relay ID');
    expect($lane->reportedStopTimeout)->toBeNull();
    expect($lane->invoiceCalls)->toBe(0);
    expect($lane->getLaneStatus())->toBe(selfserve_lane_status::OCCUPIED);
});
