<?php

namespace App\Services\Ssh;

/**
 * Test double: script command output by substring, record everything.
 *
 * Anders als `App\Services\Proxmox\FakeProxmoxClient::guestExec()` beachtet
 * `run()` hier ein angehaengtes `|| true` NICHT: dieser Fake fuehrt keine
 * echte Shell aus, sondern liefert nur das verskriptete oder das
 * Standardergebnis zurueck — `|| true` ist eine Aussage der SHELL, und ohne
 * Shell gibt es nichts, das sie treffen koennte (siehe auch der Kommentar in
 * HostFirewallTest). Wer das Verhalten des anderen Fakes von hier aus
 * vermutet, irrt: die beiden behandeln dieselbe Redewendung unterschiedlich.
 */
class FakeRemoteShell implements RemoteShell
{
    /** @var array<string, CommandResult|callable> */
    private array $scripts = [];

    /** @var array<int, string> */
    private array $recorded = [];

    /** @var array<string, string> */
    private array $files = [];

    /** @var array<int, array{0:string,1:string,2:string,3?:string}> 3 is the private key, only for 'key' connections. */
    public array $connections = [];

    public string $fingerprint = 'SHA256:fakehostkeyfingerprint';

    /** When set, connect attempts throw (simulates an unreachable/rebooting host). */
    public bool $failConnect = false;

    private CommandResult $default;

    public function __construct()
    {
        $this->default = CommandResult::success();
    }

    public function script(string $substring, CommandResult|callable $result): static
    {
        $this->scripts[$substring] = $result;

        return $this;
    }

    public function defaultResult(CommandResult $result): static
    {
        $this->default = $result;

        return $this;
    }

    public function connectWithPassword(string $host, string $user, string $password): void
    {
        if ($this->failConnect) {
            throw new \RuntimeException("connection refused: {$host}");
        }
        $this->connections[] = ['password', $host, $user];
    }

    public function connectWithKey(string $host, string $user, string $privateKey, ?string $expectedFingerprint = null): void
    {
        if ($this->failConnect) {
            throw new \RuntimeException("connection refused: {$host}");
        }
        $this->connections[] = ['key', $host, $user, $privateKey];
    }

    public function run(string $command): CommandResult
    {
        $this->recorded[] = $command;

        foreach ($this->scripts as $substring => $result) {
            if (str_contains($command, $substring)) {
                return is_callable($result) ? $result($command) : $result;
            }
        }

        return $this->default;
    }

    /**
     * Observable through files(), NOT through ran(): an uploaded file is not a
     * command and is deliberately not recorded as one. Worth stating, because a
     * test once asserted `ran('/usr/local/sbin/clupilot-…-firewall.sh')` believing
     * it proved the script had been deployed — the only thing that substring
     * matched was the `chmod 700` on the line above, so the same fact was
     * asserted twice and the script's contents were never checked at all.
     */
    public function putFile(string $remotePath, string $contents): void
    {
        $this->files[$remotePath] = $contents;
    }

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

    // --- introspection helpers for tests ---

    /** @return array<int, string> */
    public function recorded(): array
    {
        return $this->recorded;
    }

    public function ran(string $substring): bool
    {
        foreach ($this->recorded as $command) {
            if (str_contains($command, $substring)) {
                return true;
            }
        }

        return false;
    }

    /** @return array<string, string> */
    public function files(): array
    {
        return $this->files;
    }

    /** @return array<int, mixed> */
    public function connectionsWith(string $type): array
    {
        return array_values(array_filter($this->connections, fn ($c) => $c[0] === $type));
    }
}
