<?php

use Symfony\Component\Process\Process;

/**
 * The update agent's version arithmetic, run as the shell actually runs it.
 *
 * These exist because a PHP suite cannot reach a bash pipeline, and the bug
 * that made them necessary was invisible to every other kind of test: the tag
 * count was written as `release_version_gt … && echo`, so the LAST iteration —
 * almost always a tag that is not newer — left the loop with a non-zero status.
 * `pipefail` carried it out of the pipeline, the command substitution inherited
 * it, and `set -e` ended the agent before it could write a status file. On the
 * server the console showed a check that never finished and a last-checked time
 * frozen at the moment of the deployment.
 *
 * Every case therefore runs under the agent's own `set -Eeuo pipefail`, and
 * asserts the EXIT CODE as well as the answer. An answer nobody lives to read
 * is not an answer.
 */
function runRelease(string $snippet, array $tags = []): Process
{
    $lib = base_path('deploy/lib/release.sh');

    // A throwaway repository, because release_tags_ahead reads real tags. A
    // fixed identity and no signing, so this cannot pick up whatever the
    // machine running the suite happens to have configured.
    $repo = sys_get_temp_dir().'/clupilot-release-test-'.bin2hex(random_bytes(6));
    $tagCommands = '';

    foreach ($tags as $tag) {
        $tagCommands .= "git tag {$tag}\n";
    }

    $script = <<<BASH
    set -Eeuo pipefail
    mkdir -p {$repo} && cd {$repo}
    git init -q .
    git -c user.email=t@example.com -c user.name=t commit -q --allow-empty -m init
    {$tagCommands}
    . {$lib}
    {$snippet}
    BASH;

    $process = Process::fromShellCommandline('bash -c '.escapeshellarg($script));
    $process->run();

    (new Process(['rm', '-rf', $repo]))->run();

    return $process;
}

it('counts the releases ahead without taking the agent down with it', function () {
    // The exact shape that failed: the newest tag is ahead, and the tags after
    // it in the list are not — so the loop's final iteration is a false one.
    $process = runRelease(
        'release_tags_ahead 1.2.0',
        ['v1.1.0', 'v1.1.1', 'v1.2.0', 'v1.2.1'],
    );

    expect($process->getExitCode())->toBe(0)
        ->and(trim($process->getOutput()))->toBe('1');
});

it('survives a run where no tag at all is ahead', function () {
    // Every iteration false, including the last. This is the ordinary state of
    // an up-to-date server, which is to say: most ticks, on most machines.
    $process = runRelease(
        'release_tags_ahead 1.2.1',
        ['v1.1.0', 'v1.2.0', 'v1.2.1'],
    );

    expect($process->getExitCode())->toBe(0)
        ->and(trim($process->getOutput()))->toBe('0');
});

it('survives a repository with no tags whatsoever', function () {
    $process = runRelease('release_tags_ahead 1.0.0');

    expect($process->getExitCode())->toBe(0)
        ->and(trim($process->getOutput()))->toBe('0');
});

it('counts several releases ahead', function () {
    $process = runRelease(
        'release_tags_ahead 1.1.0',
        ['v1.1.0', 'v1.1.1', 'v1.2.0', 'v1.2.1'],
    );

    expect($process->getExitCode())->toBe(0)
        ->and(trim($process->getOutput()))->toBe('3');
});

it('picks the newest tag out of a long list without dying on a broken pipe', function () {
    // The second instance of the same trap. `git tag … | head -1` makes head
    // exit after one line, git takes SIGPIPE, and pipefail ends the caller.
    // Few tags hide it — git finishes writing before head leaves — so it needs
    // enough of them to actually fill a pipe buffer.
    $tags = [];

    for ($i = 1; $i <= 400; $i++) {
        $tags[] = 'v1.'.$i.'.0';
    }

    $process = runRelease('release_newest_tag', $tags);

    expect($process->getExitCode())->toBe(0)
        ->and(trim($process->getOutput()))->toBe('v1.400.0');
});

it('answers with nothing, and cleanly, when there are no releases yet', function () {
    $process = runRelease('release_newest_tag; echo "[$?]"');

    expect($process->getExitCode())->toBe(0)
        ->and(trim($process->getOutput()))->toBe('[0]');
});

it('orders by version, not alphabetically', function () {
    // v1.10.0 is newer than v1.9.0 and sorts earlier as a string. Getting this
    // wrong means a server sitting on 1.9.0 is told it is current.
    $process = runRelease(
        'release_tags_ahead 1.9.0',
        ['v1.9.0', 'v1.10.0'],
    );

    expect($process->getExitCode())->toBe(0)
        ->and(trim($process->getOutput()))->toBe('1');
});

it('answers the pairwise comparison the same way, and exits cleanly either way', function () {
    $cases = [
        ['1.2.1', '1.2.0', 'ja'],
        ['1.2.0', '1.2.1', 'nein'],
        ['1.2.0', '1.2.0', 'nein'],
        ['1.10.0', '1.9.0', 'ja'],
        ['2.0.0', '1.99.99', 'ja'],
    ];

    foreach ($cases as [$a, $b, $expected]) {
        $process = runRelease("if release_version_gt {$a} {$b}; then echo ja; else echo nein; fi");

        expect($process->getExitCode())->toBe(0)
            ->and(trim($process->getOutput()))->toBe($expected, "{$a} > {$b}");
    }
});
