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

use Test::Most;
use Mojo::Base -signatures;

use FindBin;
use lib "$FindBin::Bin/lib", "$FindBin::Bin/../external/os-autoinst-common/lib";
use OpenQA::Jobs::Constants;
use OpenQA::Test::Client 'client';
require OpenQA::Test::Database;
use OpenQA::Test::TimeLimit '10';
use Test::Output qw(combined_like combined_unlike);
use Test::MockObject;
use Test::MockModule;
use Test::Mojo;
use Test::Warnings ':report_warnings';
use Mojo::File qw(tempdir path);
use Mojo::JSON qw(decode_json);
use Mojo::IOLoop;
use OpenQA::WebAPI::Plugin::AMQP;

my $plugin_mock = Test::MockModule->new('OpenQA::WebAPI::Plugin::AMQP');
my %published;
$plugin_mock->redefine(publish_amqp => sub ($self, $topic, $data) { $published{$topic} = $data });

OpenQA::Test::Database->new->create(fixtures_glob => '01-jobs.pl 03-users.pl 05-job_modules.pl');

# this test also serves to test plugin loading via config file
my $conf = "[global]\nplugins=AMQP\n[amqp]\npublish_attempts = 2\npublish_retry_delay = 0\n";
my $tempdir = tempdir;
path($ENV{OPENQA_CONFIG} = $tempdir)->make_path->child('openqa.ini')->spew($conf);

my $t = client(Test::Mojo->new('OpenQA::WebAPI'));
my $app = $t->app;
my $schema = $app->schema;
my $comments = $schema->resultset('Comments');
$app->config->{'scm git'}->{git_auto_update} = 'no';

# create a parent group
my $parent_groups = $schema->resultset('JobGroupParents');
$parent_groups->create({id => 2000, name => 'test'});

my $settings = {
    DISTRI => 'Unicorn',
    FLAVOR => 'pink',
    VERSION => '42',
    BUILD => '666',
    TEST => 'rainbow',
    ISO => 'whatever.iso',
    DESKTOP => 'DESKTOP',
    KVM => 'KVM',
    ISO_MAXSIZE => 1,
    MACHINE => 'RainbowPC',
    ARCH => 'x86_64'
};

# create a job via API
my $job;
subtest 'create job' => sub {
    $t->post_ok('/api/v1/jobs' => form => $settings)->status_is(200);
    ok $job = $t->tx->res->json->{id}, 'got ID of new job';
    is_deeply $published{'suse.openqa.job.create'},
      {
        ARCH => 'x86_64',
        BUILD => '666',
        DESKTOP => 'DESKTOP',
        DISTRI => 'Unicorn',
        FLAVOR => 'pink',
        ISO => 'whatever.iso',
        ISO_MAXSIZE => 1,
        KVM => 'KVM',
        MACHINE => 'RainbowPC',
        TEST => 'rainbow',
        VERSION => 42,
        group_id => undef,
        id => $job,
        remaining => 1
      },
      'job create triggers amqp';
};

subtest 'mark job as done' => sub {
    $t->post_ok("/api/v1/jobs/$job/set_done")->status_is(200);
    is_deeply $published{'suse.openqa.job.done'},
      {
        ARCH => 'x86_64',
        BUILD => '666',
        FLAVOR => 'pink',
        ISO => 'whatever.iso',
        MACHINE => 'RainbowPC',
        TEST => 'rainbow',
        bugref => undef,
        failedmodules => [],
        group_id => undef,
        id => $job,
        newbuild => undef,
        remaining => 0,
        result => INCOMPLETE,
        reason => undef,
      },
      'job done triggers amqp';
};

subtest 'mark job with taken over bugref as done' => sub {
    # prepare previous job of 99963 to test carry over
    my $jobs = $schema->resultset('Jobs');
    my $previous_job = $jobs->find(99962);
    $previous_job->comments->create(
        {
            text => 'bsc#123',
            user_id => $schema->resultset('Users')->first->id,
        });
    is $previous_job->bugref, 'bsc#123', 'added bugref recognized';

    # mark so far running job 99963 as failed which should trigger bug carry over
    $t->post_ok('/api/v1/jobs/99963/set_done', form => {result => FAILED})->status_is(200);
    my $job_event = $published{'suse.openqa.job.done'};
    my $comment_event = $published{'suse.openqa.comment.create'};
    is_deeply
      $job_event,
      {
        ARCH => 'x86_64',
        BUILD => '0091',
        FLAVOR => 'DVD',
        ISO => 'openSUSE-13.1-DVD-x86_64-Build0091-Media.iso',
        MACHINE => '64bit',
        TEST => 'kde',
        bugref => 'bsc#123',
        bugurl => 'https://bugzilla.suse.com/show_bug.cgi?id=123',
        failedmodules => ['aplay'],
        group_id => 1001,
        id => 99963,
        newbuild => undef,
        remaining => 3,
        result => 'failed',
        reason => undef,
      },
      'carried over bugref and resolved URL present in AMQP event'
      or always_explain $job_event;
    ok delete $comment_event->{created}, 'carryover comment creation date present';
    ok delete $comment_event->{updated}, 'carryover comment update date present';
    is_deeply
      $comment_event,
      {
        id => $comments->find({}, {order_by => {-desc => 'id'}, rows => 1})->id,
        job_id => 99963,
        group_id => undef,
        parent_group_id => undef,
        user => 'system',
        text => "bsc#123\n\n(Automatic carryover from t#99962)\n",
        bugref => 'bsc#123',
        taken_over_from_job_id => 99962,
      },
      'AMQP event for carry-over comment'
      or always_explain $comment_event;
};

subtest 'duplicate and cancel job' => sub {
    $t->post_ok("/api/v1/jobs/$job/duplicate")->status_is(200);
    my $newjob = $t->tx->res->json->{id};
    is_deeply $published{'suse.openqa.job.restart'},
      {
        id => $job,
        result => {$job => $newjob},
        auto => 0,
        ARCH => 'x86_64',
        BUILD => '666',
        FLAVOR => 'pink',
        ISO => 'whatever.iso',
        MACHINE => 'RainbowPC',
        TEST => 'rainbow',
        bugref => undef,
        failedmodules => [],
        group_id => undef,
        remaining => 1,
      },
      'job duplicate triggers amqp';

    $t->post_ok("/api/v1/jobs/$newjob/cancel")->status_is(200);
    is_deeply $published{'suse.openqa.job.cancel'},
      {
        ARCH => 'x86_64',
        BUILD => '666',
        FLAVOR => 'pink',
        ISO => 'whatever.iso',
        MACHINE => 'RainbowPC',
        TEST => 'rainbow',
        group_id => undef,
        id => $newjob,
        reason => undef,
        remaining => 0
      },
      'job cancel triggers amqp';
};

sub assert_common_comment_json {
    my ($json) = @_;
    ok $json->{id}, 'id';
    is $json->{job_id}, undef, 'job id';
    is $json->{text}, 'test', 'text';
    is $json->{user}, 'perci', 'user';
    ok $json->{created}, 't_created';
    ok $json->{updated}, 't_updated';
}

subtest 'create job group comment' => sub {
    $t->post_ok('/api/v1/groups/1001/comments' => form => {text => 'test'})->status_is(200);
    my $json = $published{'suse.openqa.comment.create'};
    assert_common_comment_json($json);
    is $json->{group_id}, 1001, 'job group id';
    is $json->{parent_group_id}, undef, 'parent group id';
};

subtest 'create parent group comment' => sub {
    $t->post_ok('/api/v1/parent_groups/2000/comments' => form => {text => 'test'})->status_is(200);
    my $json = $published{'suse.openqa.comment.create'};
    assert_common_comment_json($json);
    is $json->{group_id}, undef, 'job group id';
    is $json->{parent_group_id}, 2000, 'parent group id';
};

$app->config->{amqp}{topic_prefix} = '';

subtest 'publish without topic prefix' => sub {
    $t->post_ok('/api/v1/jobs' => form => $settings)->status_is(200);
    is $published{'openqa.job.create'}->{ARCH}, 'x86_64', 'got message with correct topic';
};

# Now let's unmock publish_amqp so we can test it...
$plugin_mock->unmock('publish_amqp');
%published = ();
# ...but we'll mock the thing it calls.
my $publisher_mock = Test::MockModule->new('Mojo::RabbitMQ::Client::Publisher');
my ($last_publisher, $last_client, $last_promise);
$publisher_mock->redefine(
    publish_p => sub ($publisher, $body, $headers, %args) {
        $last_publisher = $publisher;
        $publisher->client($last_client = Test::MockObject->new->set_true('close'));
        %published = (body => $body, headers => $headers, args => \%args);
        return $last_promise = Mojo::Promise->new;
    });

# we need an instance of the plugin now. I can't find a documented
# way to access the one that's already loaded...
my $amqp = OpenQA::WebAPI::Plugin::AMQP->new;
$amqp->register($app);

subtest 'amqp_publish call without headers' => sub {
    combined_like { $amqp->publish_amqp('some.topic', 'some message') } qr/AMQP URL: amqp:\/\/localhost:5672/,
      'URL is logged without credentials';
    is $last_publisher->url, 'amqp://guest:guest@localhost:5672/?exchange=pubsub', 'url specified';
    is $published{body}, 'some message', 'message body correctly passed';
    is_deeply $published{headers}, {}, 'headers is empty hashref';
    is_deeply $published{args}->{routing_key}, 'some.topic', 'topic appears as routing key';
};

subtest 'amqp_publish call with reference as body' => sub {
    %published = ();
    my $body = {field => 'value'};
    $amqp->publish_amqp('some.topic', $body);
    is $published{body}, $body, 'message body kept as ref not encoded by publish_amqp';
    is_deeply $published{args}->{routing_key}, 'some.topic', 'topic appears as routing key';
};

subtest 'amqp_publish call with headers' => sub {
    %published = ();
    $amqp->publish_amqp('some.topic', 'some message', {'someheader' => 'something'});
    is $published{body}, 'some message', 'message body correctly passed';
    is_deeply $published{headers}, {'someheader' => 'something'}, 'headers is expected hashref';
    is_deeply $published{args}->{routing_key}, 'some.topic', 'topic appears as routing key';

    $app->log(Mojo::Log->new(level => 'debug'));
    combined_like { $amqp->publish_amqp('some.topic', 'some message', 'some headers') } qr/headers are not a hashref/,
      'error logged if headers are no hashref';
};

subtest 'promise handlers' => sub {
    my %event_data = (id => 'foo', job_id => 'bar');
    combined_like { $amqp->publish_amqp('some.topic', \%event_data) } qr/Sending.*some\.topic/, 'publishing logged (1)';
    combined_like { $last_promise->resolve(1); $last_promise->wait } qr/some\.topic published/, 'success logged';
    combined_like { $amqp->publish_amqp('some.topic', \%event_data) } qr/Sending.*some\.topic/, 'publishing logged (2)';
    my $previous_promise = $last_promise;
    combined_like { $last_promise->reject('some error'); $last_promise->wait }
    qr/info.*Publishing some\.topic failed: some error \(event ID: foo, job ID: bar, 1 attempts left\)/,
      'failure logged, 1 attempt remaining';
    combined_like { Mojo::IOLoop->one_tick } qr/Sending.*some\.topic/, 'trying to publish the event again';
    isnt $last_promise, $previous_promise, 'another promise has been made (to re-try)';
    $previous_promise = $last_promise;
    combined_like { $last_promise->reject('some error'); $last_promise->wait }
    qr/error.*Publishing some\.topic failed: some error \(event ID: foo, job ID: bar, 0 attempts left\)/,
      'failure logged, no attempts remaining';
    combined_unlike { Mojo::IOLoop->one_tick } qr/Sending.*some\.topic/, 'no further retry logged';
    is $last_promise, $previous_promise, 'no further promise has been made (running out of retries)';
    $last_client->called_ok(close => 'connection was closed');
};

$app->config->{amqp}{cacertfile} = '/some/cacert.pem';
$app->config->{amqp}{certfile} = '/some/cert.pem';
$app->config->{amqp}{keyfile} = '/some/key.pem';

subtest 'extra TLS query params' => sub {
    combined_like { $amqp->publish_amqp('some.topic', 'some message') } qr/Sending.*some\.topic/, 'publishing logged';
    # because of exactly how this URL is constructed, only the slashes in the extra params get escaped
    my $expected
      = 'amqp://guest:guest@localhost:5672/?exchange=pubsub&cacertfile=%2Fsome%2Fcacert.pem&certfile=%2Fsome%2Fcert.pem&keyfile=%2Fsome%2Fkey.pem';
    is $last_publisher->url, $expected, 'url has all params';
};

done_testing();
