#!/usr/bin/env perl
# Copyright 2016-2020 SUSE LLC
# SPDX-License-Identifier: GPL-2.0-or-later

use Test::Most;
use Test::Warnings ':report_warnings';

# define test helper to check for exit code
our $exit_handler = sub { CORE::exit $_[0] };

BEGIN {
    *CORE::GLOBAL::exit = sub (;$) { $exit_handler->(@_ ? 0 + $_[0] : 0) }
}

sub exit_code (&) {
    my $exit_code;
    local $exit_handler = sub { $exit_code = $_[0] };
    shift->();
    return $exit_code;
}

use FindBin;
use lib "$FindBin::Bin/lib", "$FindBin::Bin/../external/os-autoinst-common/lib";
use OpenQA::Utils (qw(:DEFAULT prjdir sharedir resultdir assetdir imagesdir base_host random_string random_hex),
    qw(download_rate download_speed usleep_backoff));
use OpenQA::SignalBlocker;
use OpenQA::Task::SignalGuard;
use OpenQA::Test::TimeLimit '10';
use Scalar::Util 'reftype';
use Test::MockObject;
use Test::MockModule;
use Mojo::File qw(path tempdir tempfile);
use Mojo::Util qw(scope_guard);

subtest 'service ports' => sub {
    local $ENV{OPENQA_BASE_PORT} = undef;
    is service_port('webui'), 9526, 'webui port';
    is service_port('websocket'), 9527, 'websocket port';
    is service_port('livehandler'), 9528, 'livehandler port';
    is service_port('scheduler'), 9529, 'scheduler port';
    is service_port('cache_service'), 9530, 'cache service port';
    local $ENV{OPENQA_BASE_PORT} = 9530;
    is service_port('webui'), 9530, 'webui port';
    is service_port('websocket'), 9531, 'websocket port';
    is service_port('livehandler'), 9532, 'livehandler port';
    is service_port('scheduler'), 9533, 'scheduler port';
    is service_port('cache_service'), 9534, 'cache service port';
    throws_ok { service_port('unknown') } qr/Unknown service: unknown/, 'unknown port';
};

subtest 'set listen address' => sub {
    local $ENV{MOJO_LISTEN} = undef;
    set_listen_address(9526);
    like $ENV{MOJO_LISTEN}, qr/localhost:9526.*127\.0\.0\.1:9526/, 'address set';
    set_listen_address(9527);
    unlike $ENV{MOJO_LISTEN}, qr/localhost:9527.*127\.0\.0\.1:9527/, 'not changed';
};

subtest 'random number generator' => sub {
    my $r = random_string;
    my $r2 = random_string;
    is length($r), 16, 'length 16';
    like $r, qr/^\w+$/a, 'random_string only consists of word characters';
    is length($r), length($r2), 'same length';
    isnt $r, $r2, 'random_string produces different results';

    $r = random_string 32;
    $r2 = random_string 32;
    is length($r), 32, 'length 32';
    like $r, qr/^\w+$/a, 'random_string only consists of word characters';
    is length($r), length($r2), 'same length';
    isnt $r, $r2, 'random_string produces different results';

    is length(random_hex), 16, 'default length 16';
    $r = random_hex 97;
    $r2 = random_hex 97;
    is length($r), 97, 'length 97';
    like $r, qr/^[0-9A-F]+$/a, 'random_hex only consists of hex characters';
    is length($r), length($r2), 'same length';
    isnt $r, $r2, 'random_hex produces different results';
};

subtest 'download speed' => sub {
    is download_rate([1638459407, 528237], [1638459408, 628237], 1024), '930.91';
    is download_rate([1638459407, 528237], [1638459408, 628237], 1024 * 1024 * 1024), '976128930.91';
    is download_rate([1638459407, 528237], [1638459407, 528237], 1024), undef;

    is download_speed([1638459407, 528237], [1638459408, 628237], 1024), '930.91 Byte/s';
    is download_speed([1638459407, 528237], [1638459408, 628237], 1024 * 1024), '931 KiB/s';
    is download_speed([1638459407, 528237], [1638459408, 628237], 1024 * 1024 * 1024), '931 MiB/s';
    is download_speed([1638459407, 528237], [1638459408, 628237], 1024 * 1024 * 1024 * 1024), '931 GiB/s';
    is download_speed([1638459407, 528237], [1638459408, 628237], 1024 * 1024 * 1024 * 1024 * 1024), '953251 GiB/s';

    is download_speed([1638459407, 528237], [1638459508, 628237], 1024 * 1024 * 512), '5.1 MiB/s';
    is download_speed([1638459407, 528237], [1638459407, 588237], 123456789), '1.9 GiB/s';

    is download_speed([1638459407, 528237], [1638459407, 528237], 1024), '??/s';
};

subtest 'labels' => sub {
    is_deeply find_labels(''), [], ' no labels found';
    is_deeply find_labels('label'), [], ' no labels found';
    is_deeply find_labels('label-123'), [], ' no labels found';
    is_deeply find_labels('test label:foo label:bar 123'), ['foo', 'bar'], 'labels found';
    is_deeply find_labels('test label:force_result:failed'), ['force_result:failed'], 'labels found';
    is_deeply find_labels('label:force_result:failed'), ['force_result:failed'], 'labels found';
    is_deeply find_labels('label:force_result:failed  test'), ['force_result:failed'], 'labels found';
    is_deeply find_labels('label label:foo:label label:bar 123'), ['foo:label', 'bar'], 'labels found';
    is_deeply find_labels('label label:foo:bnc#123 label:bar 123'), ['foo:bnc#123', 'bar'], 'labels found';
};

is bugurl('bsc#1234'), 'https://bugzilla.suse.com/show_bug.cgi?id=1234', 'bug url is properly expanded';
ok find_bugref('gh#os-autoinst/openQA#1234'), 'github bugref is recognized';
is find_bugref('bsc#1234 poo#4321'), 'bsc#1234', 'first bugres found';
is_deeply find_bugrefs('bsc#1234 poo#4321'), ['bsc#1234', 'poo#4321'], 'multiple bugrefs found';
is_deeply find_bugrefs('bc#1234 #4321'), [], 'no bugrefs found';
is bugurl('gh#os-autoinst/openQA#1234'), 'https://github.com/os-autoinst/openQA/issues/1234';
is bugurl('gh#another_repo/openQA#1234'), 'https://github.com/another_repo/openQA/issues/1234';
is bugurl('poo#1234'), 'https://progress.opensuse.org/issues/1234';
is href_to_bugref('https://progress.opensuse.org/issues/1234'), 'poo#1234';
is bugref_to_href('boo#9876'), '<a href="https://bugzilla.opensuse.org/show_bug.cgi?id=9876">boo#9876</a>';
is href_to_bugref('https://github.com/foo/bar/issues/1234'), 'gh#foo/bar#1234';
is href_to_bugref('https://github.com/foo/bar_bar/issues/1234'), 'gh#foo/bar_bar#1234';
is href_to_bugref('https://github.com/foo6/bar666/issues/1234'), 'gh#foo6/bar666#1234';
is href_to_bugref('https://github.com/foo/bar#bar/issues/1234'), 'gh#foo/bar#bar#1234';
is href_to_bugref('https://github.com/os-autoinst/os-autoinst/pull/960'), 'gh#os-autoinst/os-autoinst#960',
  'github pull are also transformed same as issues';
is bugref_to_href('gh#foo/bar#1234'), '<a href="https://github.com/foo/bar/issues/1234">gh#foo/bar#1234</a>';
like bugref_to_href('bsc#2345 poo#3456 and more'),
  qr{a href="https://bugzilla.suse.com/show_bug.cgi\?id=2345">bsc\#2345</a> <a href=.*3456.*> and more},
  'bugrefs in text get replaced';
like bugref_to_href('boo#2345,poo#3456'),
  qr{a href="https://bugzilla.opensuse.org/show_bug.cgi\?id=2345">boo\#2345</a>,<a href=.*3456.*},
  'interpunctation is not consumed by href';
is bugref_to_href('jsc#SLE-3275'), '<a href="https://jira.suse.com/browse/SLE-3275">jsc#SLE-3275</a>';
is href_to_bugref('https://jira.suse.com/browse/FOOBAR-1234'), 'jsc#FOOBAR-1234', 'jira tickets url to bugref';
is href_to_bugref('https://pagure.io/foo/issue/1234'), 'pio#foo#1234', 'pagure.io (no group) url to bugref';
is href_to_bugref('https://pagure.io/foo/bar/issue/1234'), 'pio#foo/bar#1234', 'pagure.io (with group) url to bugref';
is href_to_bugref('https://gitlab.gnome.org/GNOME/foo/-/issues/1234'), 'ggo#GNOME/foo#1234',
  'GNOME gitlab url to bugref';
is href_to_bugref('https://gitlab.com/fedora/sigs/flatpak/fedora-flatpaks/-/issues/26'),
  'gfs#flatpak/fedora-flatpaks#26',
  'Fedora SIGs gitlab url to bugref';
is find_bug_number('yast_roleconf-ntp-servers-empty-bsc1114818-20181115.png'), 'bsc1114818',
  'find the bug number from the needle name';

my $t3 = {
    bar => {
        foo => 1,
        baz => [{fish => {boring => 'too'}}, {fish2 => {boring => 'not_really'}}]}};
walker(
    $t3 => sub {
        my ($k, $v, $ks, $what) = @_;
        next if reftype $what eq 'HASH' && exists $what->{_data};
        like $_[0], qr/bar|baz|foo|0|1|fish$|fish2|boring/, 'Walked';

        $what->[$k] = {_type => ref $v, _data => $v} if reftype $what eq 'ARRAY';
        $what->{$k} = {_type => ref $v, _data => $v} if reftype $what eq 'HASH';

    });

is_deeply $t3,
  {
    'bar' => {
        '_data' => {
            'baz' => {
                '_data' => [
                    {
                        '_data' => {
                            'fish' => {
                                '_data' => {
                                    'boring' => {
                                        '_data' => 'too',
                                        '_type' => ''
                                    }
                                },
                                '_type' => 'HASH'
                            }
                        },
                        '_type' => 'HASH'
                    },
                    {
                        '_data' => {
                            'fish2' => {
                                '_data' => {
                                    'boring' => {
                                        '_data' => 'not_really',
                                        '_type' => ''
                                    }
                                },
                                '_type' => 'HASH'
                            }
                        },
                        '_type' => 'HASH'
                    }
                ],
                '_type' => 'ARRAY'
            },
            'foo' => {
                '_data' => 1,
                '_type' => ''
            }
        },
        '_type' => 'HASH'
    }};

subtest 'get current version' => sub {
    # Let's check that the version matches our versioning scheme.
    # If it's a git version it should be in the form: git-tag-sha1
    # otherwise is a group of 3 decimals followed by a partial sha1: a.b.c.sha1

    my $changelog_dir = tempdir;
    my $git_dir = tempdir;
    my $changelog_file = $changelog_dir->child('public')->make_path->child('Changelog');
    my $refs_file = $git_dir->child('.git')->make_path->child('packed-refs');
    my $head_file = $git_dir->child('.git', 'refs', 'heads')->make_path->child('master');
    my $sha_regex = qr/\b[0-9a-f]{5,40}\b/;

    my $changelog_content = <<'EOT';
-------------------------------------------------------------------
Mon May 08 11:45:15 UTC 2017 - rd-ops-cm@suse.de

- Update to version 5.1743167903.dfbc473b:
  * foo
  * bar

-------------------------------------------------------------------
Sat Mar 18 20:03:22 UTC 2017 - coolo@suse.com

- bump mojo requirement

-------------------------------------------------------------------
Sat Mar 18 19:31:50 UTC 2017 - rd-ops-cm@suse.de

- Update to version 4.4.1489864450.251306a:
  * Make sure assets in pool are handled correctly
  * Call rsync of tests in a child process and notify webui
  * Move OpenQA::Cache to Worker namespace
  * Trying to make workers.ini more descriptive
  * docs: Add explanation for job priority (#1262)
  * Schedule worker reregistration in case of api-failure
  * Add more logging to job notifications
  * Use host_port when parsing URL
  * Prevent various timer loops
  * Do job cleanup even in case of api failure
EOT

    my $refs_content = <<'EOT';
# pack-refs with: peeled fully-peeled
f8ce111933922cde0c5d11952fbb59b307a700e5 refs/tags/4.0
bb8144fdb128896d0132188c55d298c3905b48aa refs/tags/4.1
87e71451fea9d54927efe9ce3f9e7071fb11e874 refs/tags/4.2
^9953cb8cc89f4e9187f4209035ce2990dbf544cc
ac6dd8d4475f8b7e0d683e64ff49d6d96151fb76 refs/tags/4.3
^11f0541f05d7bbc663ae90d6dedefde8d6f03ff4
EOT

    # Create a valid Changelog and check if result is the expected one
    $changelog_file->spew($changelog_content);
    is detect_current_version($changelog_dir), '5.1743167903.dfbc473b', 'Detect current version from Changelog format';
    like detect_current_version($changelog_dir), qr/(\d+\.\d+\.$sha_regex)/, 'Version scheme matches';
    $changelog_file->spew("- Update to version 5.1743167903.dfbc473b:\n- Update to version 5.1743167902.cebc473b:");
    is detect_current_version($changelog_dir), '5.1743167903.dfbc473b', 'Pick latest version detected in Changelog';

    # Failure detection case for Changelog file
    $changelog_file->spew('* Do job cleanup even in case of api failure');
    is detect_current_version($changelog_dir), undef, 'Invalid Changelog return no version';
    $changelog_file->spew('Update to version 3a2.d2d.2ad.9869466:');
    is detect_current_version($changelog_dir), undef, 'Invalid versions in Changelog returns undef';

    # Create a valid Git repository where we can fetch the exact version.
    $head_file->spew('7223a2408120127ad2d82d71ef1893bbe02ad8aa');
    $refs_file->spew($refs_content);
    is detect_current_version($git_dir), 'git-4.3-7223a240', 'detect current version from Git repository';
    like detect_current_version($git_dir), qr/(git\-\d+\.\d+\-$sha_regex)/, 'Git version scheme matches';

    # If refs file can't be found or there is no tag present, version should be undef
    unlink $refs_file;
    is detect_current_version($git_dir), undef, 'Git ref file missing, version is undef';
    $refs_file->spew('ac6dd8d4475f8b7e0d683e64ff49d6d96151fb76');
    is detect_current_version($git_dir), undef, 'Git ref file shows no tag, version is undef';
};

subtest 'Plugins handling' => sub {

    is path_to_class('foo/bar.pm'), 'foo::bar';
    is path_to_class('foo/bar/baz.pm'), 'foo::bar::baz';

    ok grep('OpenQA::Utils', loaded_modules), 'Can detect loaded modules';
    ok grep('Test::Most', loaded_modules), 'Can detect loaded modules';

    is_deeply [loaded_plugins('OpenQA::Utils', 'Test::Most')], ['OpenQA::Utils', 'Test::Most', 'Test::Most::Exception'],
      'Can detect loaded plugins, filtering by namespace';
    ok grep('Test::Most', loaded_plugins),
      'loaded_plugins() behave like loaded_modules() when no arguments are supplied';

    my $test_hash = {
        auth => {
            method => 'Fake',
            foo => 'bar',
            b => {bar2 => 2},
        },
        baz => {
            bar => 'test'
        }};

    my %reconstructed_hash;
    hashwalker $test_hash => sub {
        my ($key, $value, $keys) = @_;

        my $r_hash = \%reconstructed_hash;
        for (my $i = 0; $i < scalar @$keys; $i++) {
            $r_hash->{$keys->[$i]} //= {};
            $r_hash = $r_hash->{$keys->[$i]} if $i < (scalar @$keys) - 1;
        }

        $r_hash->{$key} = $value if ref $r_hash eq 'HASH';

    };

    is_deeply \%reconstructed_hash, $test_hash, 'hashwalker() reconstructed original hash correctly';
};

subtest asset_type_from_setting => sub {
    use OpenQA::Utils 'asset_type_from_setting';
    is asset_type_from_setting('ISO'), 'iso', 'simple from ISO';
    is asset_type_from_setting('UEFI_PFLASH_VARS'), 'hdd', 'simple from UEFI_PFLASH_VARS';
    is asset_type_from_setting('UEFI_PFLASH_VARS', 'relative'), 'hdd', 'relative from UEFI_PFLASH_VARS';
    is asset_type_from_setting('UEFI_PFLASH_VARS', '/absolute'), '', 'absolute from UEFI_PFLASH_VARS';
    is asset_type_from_setting('KERNEL', '/vmlinuz'), 'other', 'absolute from KERNEL';
    is asset_type_from_setting('KERNEL', 'vmlinuz'), 'other', 'relative from KERNEL';
};

subtest parse_assets_from_settings => sub {
    use OpenQA::Utils 'parse_assets_from_settings';
    my $settings = {
        ISO => 'foo.iso',
        ISO_2 => 'foo_2.iso',
        # this is a trap: shouldn't be treated as an asset
        HDD => 'hdd.qcow2',
        HDD_1 => 'hdd_1.qcow2',
        HDD_2 => 'hdd_2.qcow2',
        # shouldn't be treated as asset *yet* as it's absolute
        UEFI_PFLASH_VARS => '/absolute/path/uefi_pflash_vars.qcow2',
        # trap
        REPO => 'repo',
        REPO_1 => 'repo_1',
        REPO_2 => 'repo_2',
        # trap
        ASSET => 'asset.pm',
        ASSET_1 => 'asset_1.pm',
        ASSET_2 => 'asset_2.pm',
        KERNEL => 'vmlinuz',
        INITRD => 'initrd.img',
    };
    my $assets = parse_assets_from_settings($settings);
    my $refassets = {
        ISO => {type => 'iso', name => 'foo.iso'},
        ISO_2 => {type => 'iso', name => 'foo_2.iso'},
        HDD_1 => {type => 'hdd', name => 'hdd_1.qcow2'},
        HDD_2 => {type => 'hdd', name => 'hdd_2.qcow2'},
        REPO_1 => {type => 'repo', name => 'repo_1'},
        REPO_2 => {type => 'repo', name => 'repo_2'},
        ASSET_1 => {type => 'other', name => 'asset_1.pm'},
        ASSET_2 => {type => 'other', name => 'asset_2.pm'},
        KERNEL => {type => 'other', name => 'vmlinuz'},
        INITRD => {type => 'other', name => 'initrd.img'},
    };
    is_deeply $assets, $refassets, 'correct with absolute UEFI_PFLASH_VARS';
    # now make this relative: it should now be seen as an asset type
    $settings->{UEFI_PFLASH_VARS} = 'uefi_pflash_vars.qcow2';
    $assets = parse_assets_from_settings($settings);
    $refassets->{UEFI_PFLASH_VARS} = {type => 'hdd', name => 'uefi_pflash_vars.qcow2'};
    is_deeply $assets, $refassets, 'correct with relative UEFI_PFLASH_VARS';
    # KERNEL/INITRD are treated similar to UEFI_PFLASH_VARS
    $settings->{KERNEL} = '/path/vmlinuz';
    $assets = parse_assets_from_settings($settings);
    $refassets->{KERNEL} = {type => 'other', name => '/path/vmlinuz'};
    is_deeply $assets, $refassets, 'correct with absolute KERNEL';

};

subtest 'base_host' => sub {
    is base_host('http://opensuse.org'), 'opensuse.org';
    is base_host('www.opensuse.org'), 'www.opensuse.org';
    is base_host('test'), 'test';
    is base_host('https://opensuse.org/test/1/2/3'), 'opensuse.org';
};

subtest 'project directory functions' => sub {
    my $distri = 'opensuse';
    subtest 'default OPENQA_BASEDIR/OPENQA_SHAREDIR' => sub {
        local $ENV{OPENQA_BASEDIR};
        local $ENV{OPENQA_SHAREDIR};
        is prjdir, '/var/lib/openqa', 'prjdir';
        is sharedir, '/var/lib/openqa/share', 'sharedir';
        is resultdir, '/var/lib/openqa/testresults', 'resultdir';
        is assetdir, '/var/lib/openqa/share/factory', 'assetdir';
        is imagesdir, '/var/lib/openqa/images', 'imagesdir';
    };
    subtest 'custom OPENQA_BASEDIR' => sub {
        local $ENV{OPENQA_BASEDIR} = '/tmp/test';
        is prjdir, '/tmp/test/openqa', 'prjdir';
        is sharedir, '/tmp/test/openqa/share', 'sharedir';
        is resultdir, '/tmp/test/openqa/testresults', 'resultdir';
        is assetdir, '/tmp/test/openqa/share/factory', 'assetdir';
        is imagesdir, '/tmp/test/openqa/images', 'imagesdir';
        my $repo = 'fakerepo/os-autoinst-distri-opensuse';
        is git_commit_url("git\@github.com:$repo.git"), "https://github.com/$repo/commit/", 'correct git url for ssh';
        is git_commit_url("https://github.com/$repo.git"), "https://github.com/$repo/commit/",
          'correct git url for http';
        is git_commit_url("git\@github.com:$repo"), "https://github.com/$repo/commit/",
          'correct ssh url without .git extension';
        is git_commit_url("https://github.com/$repo"), "https://github.com/$repo/commit/",
          'correct https url without .git extension';
    };
    subtest 'handling of invalid input by git_commit_url' => sub {
        is git_commit_url(undef), '', 'no git_commit_url when repo hash is not defined';
        is git_commit_url('foo/bar'), '', 'no git_commit_url without protocol';
        is git_commit_url('foo:bar'), '', 'no git_commit_url without Git host';
    };
    subtest 'custom OPENQA_BASEDIR and OPENQA_SHAREDIR' => sub {
        local $ENV{OPENQA_BASEDIR} = '/tmp/test';
        local $ENV{OPENQA_SHAREDIR} = '/tmp/share';
        is prjdir, '/tmp/test/openqa', 'prjdir';
        is sharedir, '/tmp/share', 'sharedir';
        is resultdir, '/tmp/test/openqa/testresults', 'resultdir';
        is assetdir, '/tmp/share/factory', 'assetdir';
        is imagesdir, '/tmp/test/openqa/images', 'imagesdir';
    };
};

subtest 'change_sec_to_word' => sub {
    is change_sec_to_word(), undef, 'do pass parameter';
    is change_sec_to_word(1.2), undef, 'treat float as invalid parameter';
    is change_sec_to_word('test'), undef, 'treat string as invalid parameter';
    is change_sec_to_word(10), '10s', 'correctly converted (seconds)';
    is change_sec_to_word(70), '1m 10s', 'correctly converted (minutes + seconds)';
    is change_sec_to_word(900), '15m', 'correctly converted (minutes)';
    is change_sec_to_word(3900), '1h 5m', 'correctly converted (hours + minutes)';
    is change_sec_to_word(7201), '2h 1s', 'correctly converted (hours + seconds)';
    is change_sec_to_word(64890), '18h 1m 30s', 'correctly converted (hours + minutes + seconds)';
    is change_sec_to_word(648906), '7d 12h 15m 6s', 'correctly converted (all units)';
};

subtest 'usleep_backoff' => sub {
    subtest 'with fixed padding to test exact behavior' => sub {
        is usleep_backoff(1, 5, 30, 1_000_001), 6_000_001, 'delay one';
        is usleep_backoff(2, 5, 30, 1_000_001), 7_000_001, 'delay two';
        is usleep_backoff(3, 5, 30, 1_000_001), 8_000_001, 'delay three';
        is usleep_backoff(4, 5, 30, 1_000_001), 9_000_001, 'delay four';
        is usleep_backoff(5, 5, 30, 1_000_001), 10_000_001, 'delay five';
        is usleep_backoff(6, 5, 30, 1_000_001), 11_000_001, 'delay six';
        is usleep_backoff(7, 5, 30, 1_000_001), 12_000_001, 'delay seven';
        is usleep_backoff(8, 5, 30, 1_000_001), 13_000_001, 'delay eight';
        is usleep_backoff(9, 5, 30, 1_000_001), 14_000_001, 'delay nine';
        is usleep_backoff(10, 5, 30, 1_000_001), 15_000_001, 'delay ten';
    };

    subtest 'with random 1 second default padding' => sub {
        my $real_usleep_value = usleep_backoff(3, 5, 30);
        ok $real_usleep_value <= 8_000_000, 'less than 8 seconds';
        ok $real_usleep_value > 7_000_000, 'more than 7 seconds';
    };

    subtest 'disabled with 0 second minimum value' => sub {
        is usleep_backoff(5, 0, 30), 0, 'default to 0 if allowed';
    };

    subtest 'special cases' => sub {
        is usleep_backoff(5, 40, 30), 30_000_000, 'maximum is not exceeded even if the minimum is higher';
    };
};

my ($current_term_handler, $current_int_handler) = ($SIG{TERM}, $SIG{INT});
subtest 'signal handling in Minion jobs' => sub {
    my $job = Test::MockObject->new;
    my $signal_guard = OpenQA::Task::SignalGuard->new($job);
    $job->set_true($_) for qw(retry finish note);

    subtest 'signal arrives after lock acquisition' => sub {
        is exit_code { kill INT => $$ }, 0, 'exited when signal arrives after lock acquisition';
        $job->called_ok('note');
        $job->called_ok('retry');
        $job->called_args_pos_is(0, 3, 'Received signal INT, scheduling retry and releasing locks');
        $job->clear;
    };
    subtest 'signal arrives after retry disabled' => sub {
        $signal_guard->retry(0);
        is exit_code { kill INT => $$ }, undef, 'not exited (the job is supposed to be concluded at this point)';
        $job->called_ok('note');
        $job->called_args_pos_is(0, 3, 'Received signal INT, concluding');
        ok !$job->called('retry'), 'job not retried';
        $job->clear;
    };
    subtest 'signal arrives after abort-flag set' => sub {
        $signal_guard->abort(1);
        is exit_code { kill INT => $$ }, 0, 'exited when signal arrives after abort-flag set';
        $job->called_ok('note');
        $job->called_args_pos_is(0, 3, 'Received signal INT, aborting');
        ok !$job->called('retry'), 'job not retried';
        ok $job->called('finish'), 'job considered finished';
    };
};
is $SIG{TERM}, $current_term_handler, 'SIGTERM handler restored after signal guard goes out of scope';
is $SIG{INT}, $current_int_handler, 'SIGINT handler restored after signal guard goes out of scope';

subtest 'signal blocker' => sub {
    my $restore_signals = scope_guard sub {
        $SIG{TERM} = $current_term_handler;
        $SIG{TERM} = $current_int_handler;
    };

    my $sigterm_handled = 0;
    my $sigint_handled = 0;
    my $sigterm_handler = sub { ++$sigterm_handled };
    my $sigint_handler = sub { ++$sigint_handled };
    $SIG{TERM} = $sigterm_handler;
    $SIG{INT} = $sigint_handler;

    my $blocker = OpenQA::SignalBlocker->new;
    kill $_ => $$ for qw(INT TERM);
    is $sigterm_handled, 0, 'SIGTERM handler has not been called while $blocker still present';
    is $sigint_handled, 0, 'SIGINT handler has not been called while $blocker still present';

    undef $blocker;
    is $sigterm_handled, 1, 'SIGTERM handler has been called after $blocker was destroyed';
    is $sigint_handled, 1, 'SIGINT handler has been called after $blocker was destroyed';

    is $SIG{TERM}, $sigterm_handler, 'SIGTERM handler was restored after $blocker was destroyed';
    is $SIG{INT}, $sigint_handler, 'SIGINT handler was restored after $blocker was destroyed';
};

subtest 'human readable size' => sub {
    is human_readable_size(0), '0 Byte', 'zero';
    is human_readable_size(1), '1 Byte', 'one';
    is human_readable_size(13443399680), '13 GiB', 'two digits GB';
    is human_readable_size(8007188480), '7.5 GiB', 'smaller GB';
    is human_readable_size(-8007188480), '-7.5 GiB', 'negative smaller GB';
    is human_readable_size(717946880), '685 MiB', 'large MB';
    is human_readable_size(245760), '240 KiB', 'less than a MB';
};

subtest 'create downloads list' => sub {
    is_deeply create_downloads_list({}), {}, 'empty for no settings provided';
    is_deeply create_downloads_list({TEST_URL => 'test', TEST => 'test'}), {}, 'empty for non-asset URL';
    my $utils_mocked = Test::MockModule->new('OpenQA::Utils')->redefine(locate_asset => 'foo/bar');
    is_deeply create_downloads_list({ISO_URL => 'test', ISO => 'test'}), {test => ['foo/bar', '0']},
      'valid download for asset URL';
};

done_testing;
