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

use Test::Most;
use Test::Warnings ':report_warnings';
use Time::HiRes 'gettimeofday';
use Mojo::File qw(tempdir tempfile);
use OpenQA::App;
use OpenQA::Setup;
use OpenQA::Log
  qw(log_error log_warning log_fatal log_info log_debug log_trace add_log_channel remove_log_channel log_format_callback get_channel_handle setup_log format_settings);
use OpenQA::Worker::App;
use File::Path qw(make_path remove_tree);
use Test::MockModule qw(strict);
use Test::Output qw(stdout_like stderr_like stdout_from stderr_from);
use Sys::Hostname;
use File::Spec::Functions 'catfile';
use FindBin;
use lib "$FindBin::Bin/lib", "$FindBin::Bin/../external/os-autoinst-common/lib";
use OpenQA::Test::TimeLimit '4';

my $reFile = qr/\[.*?\] \[(.*?)\] (?:\[pid:\d+\]\s)?(.*?) message/;
my $reStdOut = qr/(?:.*?)\[(.*?)\] (?:\[pid:\d+\]\s)?(.*?) message/;
my $reChannel = qr/\[.*?\] \[(.*?)\] (?:\[pid:\d+\]\s)?(.*?) message/;

subtest 'load correct configs' => sub {
    local $ENV{OPENQA_CONFIG} = 't/data/logging/';
    my $app = OpenQA::Worker::App->new(
        mode => 'production',
        log_name => 'worker',
        instance => 1,
        log_dir => undef,
        level => 'debug'
    );

    OpenQA::Setup::read_config($app);
    is $app->level, 'debug';
    is $app->mode, 'production';
    is $app->config->{logging}{level}, 'warning';
    is $app->log->level, 'info';
    setup_log($app, undef, $app->log_dir, $app->level);
    is $app->level, 'debug';
    is $app->log->level, 'debug';

    $app = OpenQA::Worker::App->new();
    OpenQA::Setup::read_config($app);
    is $app->level, undef;
    is $app->mode, 'production';
    is $app->config->{logging}{level}, 'warning';
    is $app->log->level, 'info';
    setup_log($app);
    is $app->level, undef;
    is $app->log->level, 'warning';

};

subtest 'Logging to stdout' => sub {
    local $ENV{OPENQA_WORKER_LOGDIR};
    local $ENV{OPENQA_LOGFILE};
    my $app = OpenQA::Worker::App->new(
        mode => 'production',
        log_name => 'worker',
        instance => 1,
        log_dir => undef,
        level => 'debug'
    );

    setup_log($app, undef, $app->log_dir, $app->level);

    my $output = stdout_from {
        log_trace('trace message');
        log_debug('debug message');
        log_error('error message');
        log_info('info message');
    };
    note $output;
    my @matches = ($output =~ m/$reStdOut/gm);

    like $output, qr/debug message/;
    unlike $output, qr/trace message/;

    like $output, qr/$$/, 'Pid is printed in debug mode';
    is @matches / 2, 3, 'Worker log matches';
    for (my $i = 0; $i < @matches; $i += 2) {
        like $matches[$i], qr/$matches[$i + 1]/, "OK $matches[$i]";
    }
};

subtest 'Logging to file' => sub {
    delete $ENV{OPENQA_LOGFILE};
    my $tempdir = tempdir;
    local $ENV{OPENQA_WORKER_LOGDIR} = $tempdir;

    my $app = OpenQA::Worker::App->new(
        mode => 'production',
        log_name => 'worker',
        instance => 1,
        log_dir => $ENV{OPENQA_WORKER_LOGDIR},
        level => 'debug'
    );
    my $output_logfile = catfile($ENV{OPENQA_WORKER_LOGDIR}, hostname() . '-1.log');
    setup_log($app, undef, $app->log_dir, $app->level);
    log_debug('debug message');
    log_error('error message');
    log_info('info message');

    # Tests
    my @matches = (Mojo::File->new($output_logfile)->slurp =~ m/$reFile/gm);
    is @matches / 2, 3, 'Worker log matches';
    for (my $i = 0; $i < @matches; $i += 2) {
        is $matches[$i], $matches[$i + 1], "OK $matches[$i]";
    }
};

subtest 'log fatal to stderr' => sub {
    delete $ENV{OPENQA_LOGFILE};
    delete $ENV{OPENQA_WORKER_LOGDIR};
    my $app = OpenQA::Worker::App->new(
        mode => 'production',
        log_name => 'worker',
        instance => 1,
        log_dir => undef,
        level => 'debug'
    );

    setup_log($app);
    OpenQA::App->set_singleton(undef);    # To make sure we are not setting it in other tests
    my $output = stderr_from {
        throws_ok { log_fatal('fatal message') }
        qr/fatal message.*t\/28-logging\.t/, 'Fatal raised exception';
    };
    like $output, qr/\[FATAL\] fatal message/, 'OK fatal';
};

subtest 'Checking log level' => sub {
    my $tempdir = tempdir;
    local $ENV{OPENQA_WORKER_LOGDIR} = $tempdir;
    delete $ENV{MOJO_LOG_LEVEL};    # The Makefile is overriding this variable
    delete $ENV{OPENQA_LOGFILE};

    my $output_logfile = catfile($ENV{OPENQA_WORKER_LOGDIR}, hostname() . '-1.log');

    my @loglevels = qw(trace debug info warn error fatal);
    my @channels = qw(channel1 channel2 channel3);
    my $counterFile = @loglevels;
    my $counterChannel = @loglevels;
    for my $level (@loglevels) {
        my $app = OpenQA::Worker::App->new(
            mode => 'production',
            log_name => 'worker',
            instance => 1,
            log_dir => $ENV{OPENQA_WORKER_LOGDIR},
            level => $level
        );

        setup_log($app, undef, $app->log_dir, $app->level);

        log_trace('trace message');
        log_debug('debug message');
        log_info('info message');
        log_warning('warn message');
        log_error('error message');
        throws_ok { log_fatal('fatal message') } qr/fatal message/, 'exception raised';
        my %matches = map { $_ => 1 } (Mojo::File->new($output_logfile)->slurp =~ m/$reFile/gm);
        is keys(%matches), $counterFile, "Worker log level $level entry";

        for my $channel (@channels) {
            my $logging_test_file = tempfile;

            add_log_channel($channel, path => $logging_test_file, level => $level);
            log_trace('trace message', channels => $channel);
            log_debug('debug message', channels => $channel);
            log_info('info message', channels => $channel);
            log_warning('warn message', channels => $channel);
            log_error('error message', channels => $channel);
            throws_ok { log_fatal('fatal message', channels => $channel); } qr/fatal message/, 'exception raised';
            %matches = map { $_ => 1 } (Mojo::File->new($logging_test_file)->slurp =~ m/$reChannel/gm);
            is keys(%matches), $counterChannel, "Worker channel log level $level entry";
            remove_log_channel($channel);
            is get_channel_handle($channel), undef, "Channel $channel was removed";
        }
        $counterChannel--;

        log_trace('trace message', channels => 'no_channel');
        log_debug('debug message', channels => 'no_channel');
        log_info('info message', channels => 'no_channel');
        log_warning('warn message', channels => 'no_channel');
        log_error('error', channels => 'no_channel');
        throws_ok { log_fatal('fatal message', channels => 'no_channel') } qr/fatal message/, 'exception raised';
        # print Mojo::File->new($output_logfile)->slurp;

        %matches = map { $_ => ($matches{$_} // 0) + 1 } (Mojo::File->new($output_logfile)->slurp =~ m/$reFile/gm);
        my @vals = grep { $_ != 2 } values %matches;
        is keys(%matches), $counterFile--, "Worker no existent channel log level $level entry";
        is @vals, 0, 'Worker no existent channel log level $level entry ';

        truncate $output_logfile, 0;
    }
};

subtest 'Logging to right place' => sub {
    delete $ENV{OPENQA_LOGFILE};
    my $tempdir = tempdir;
    local $ENV{OPENQA_WORKER_LOGDIR} = $tempdir;

    my $app = OpenQA::Worker::App->new(
        mode => 'production',
        log_name => 'worker',
        instance => 1,
        log_dir => $ENV{OPENQA_WORKER_LOGDIR},
        level => 'debug'
    );
    my $output_logfile = catfile($ENV{OPENQA_WORKER_LOGDIR}, hostname() . '-1.log');
    setup_log($app, undef, $app->log_dir, $app->level);
    log_debug('debug message');
    log_error('error message');
    log_info('info message');
    ok -f $output_logfile, 'Log file defined in logdir';

    local $ENV{OPENQA_LOGFILE} = 'test_log_file.log';
    $app = OpenQA::Worker::App->new(
        mode => 'production',
        log_name => 'worker',
        instance => 1,
        log_dir => $ENV{OPENQA_WORKER_LOGDIR},
        level => 'debug'
    );
    setup_log($app, undef, $app->log_dir, $app->level);
    log_debug('debug message');
    log_error('error message');
    log_info('info message');
    ok
      -f catfile($ENV{OPENQA_WORKER_LOGDIR}, $ENV{OPENQA_LOGFILE}),
      'Log file created defined in logdir and environment';


    local $ENV{OPENQA_LOGFILE} = catfile($ENV{OPENQA_WORKER_LOGDIR}, 'another_test_log_file.log');
    $app = OpenQA::Worker::App->new(
        mode => 'production',
        log_name => 'worker',
        instance => 1,
        level => 'debug'
    );
    setup_log($app);
    log_debug('debug message');
    log_error('error message');
    log_info('info message');
    ok -f $ENV{OPENQA_LOGFILE}, 'Log file created defined in environment';
};

subtest 'Logs to multiple channels' => sub {
    delete $ENV{OPENQA_LOGFILE};
    my $tempdir = tempdir;
    local $ENV{OPENQA_WORKER_LOGDIR} = $tempdir;

    my $output_logfile = catfile($ENV{OPENQA_WORKER_LOGDIR}, hostname() . '-1.log');
    my @loglevels = qw(trace debug info warn error fatal);
    my @channel_tupples = ([qw/channel1 channel2/], [qw/channel3 channel4/]);
    my $counterChannel = @loglevels;


    for my $level (@loglevels) {
        my $app = OpenQA::Worker::App->new(
            mode => 'production',
            log_name => 'worker',
            instance => 1,
            log_dir => $ENV{OPENQA_WORKER_LOGDIR},
            level => $level
        );
        setup_log($app, undef, $app->log_dir, $app->level);

        for my $channel_tupple (@channel_tupples) {
            my $logging_test_file1 = tempfile;
            my $logging_test_file2 = tempfile;

            add_log_channel($channel_tupple->[0], path => $logging_test_file1, level => $level);
            add_log_channel($channel_tupple->[1], path => $logging_test_file2, level => $level);

            log_trace('trace message', channels => $channel_tupple, standard => 1);
            log_debug('debug message', channels => $channel_tupple, standard => 1);
            log_info('info message', channels => $channel_tupple, standard => 1);
            log_warning('warn message', channels => $channel_tupple, standard => 1);
            log_error('error message', channels => $channel_tupple, standard => 1);

            throws_ok { log_fatal('fatal message', channels => $channel_tupple, standard => 1) } qr/fatal message/,
              'exception raised';

            my %matches = map { $_ => 1 } (Mojo::File->new($logging_test_file1)->slurp =~ m/$reChannel/gm);
            is keys(%matches), $counterChannel, "Worker multiple channel $channel_tupple->[0] log level $level entry";

            %matches = map { $_ => 1 } (Mojo::File->new($logging_test_file2)->slurp =~ m/$reChannel/gm);
            is keys(%matches), $counterChannel, "Worker multiple channel $channel_tupple->[1] log level $level entry";
            %matches = map { $_ => 1 } (Mojo::File->new($output_logfile)->slurp =~ m/$reFile/gm);
            is keys(%matches), $counterChannel, "Worker multiple channel Default log level $level entry";
            truncate $output_logfile, 0;
        }
        $counterChannel--;
    }
};

subtest 'Logs to bogus channels' => sub {
    delete $ENV{OPENQA_LOGFILE};
    my $tempdir = tempdir;
    local $ENV{OPENQA_WORKER_LOGDIR} = $tempdir;

    my $output_logfile = catfile($ENV{OPENQA_WORKER_LOGDIR}, hostname() . '-1.log');
    my @loglevels = qw(trace debug info warn error fatal);
    my @channel_tupples = ([qw/channel1 channel2/], [qw/channel3 channel4/]);
    my $counterChannel = @loglevels;

    for my $level (@loglevels) {
        my $app = OpenQA::Worker::App->new(
            mode => 'production',
            log_name => 'worker',
            instance => 1,
            log_dir => $ENV{OPENQA_WORKER_LOGDIR},
            level => $level
        );
        setup_log($app, undef, $app->log_dir, $app->level);

        for my $channel_tupple (@channel_tupples) {
            my $logging_test_file1 = tempfile;
            my $logging_test_file2 = tempfile;

            add_log_channel($channel_tupple->[0], path => $logging_test_file1, level => $level);
            add_log_channel($channel_tupple->[1], path => $logging_test_file2, level => $level);

            log_trace('trace message', channels => ['test', 'test1']);
            log_debug('debug message', channels => ['test', 'test1']);
            log_info('info message', channels => ['test', 'test1']);
            log_warning('warn message', channels => ['test', 'test1']);
            log_error('error message', channels => ['test', 'test1']);

            throws_ok { log_fatal('fatal message', channels => ['test', 'test1']) } qr/fatal message/,
              'exception raised';

            my %matches = map { $_ => 1 } (Mojo::File->new($logging_test_file1)->slurp =~ m/$reChannel/gm);
            is keys(%matches), 0, "Worker multiple channel $channel_tupple->[0] log level $level entry";

            %matches = map { $_ => 1 } (Mojo::File->new($logging_test_file2)->slurp =~ m/$reChannel/gm);
            is keys(%matches), 0, "Worker multiple channel $channel_tupple->[1] log level $level entry";

            %matches = map { $_ => 1 } (Mojo::File->new($output_logfile)->slurp =~ m/$reFile/gm);
            is keys(%matches), $counterChannel, "Worker multiple channel Default log level $level entry";
            truncate $output_logfile, 0;

        }
        $counterChannel--;
    }
};


subtest 'Logs to default channels' => sub {
    delete $ENV{OPENQA_LOGFILE};
    my $tempdir = tempdir;
    local $ENV{OPENQA_WORKER_LOGDIR} = $tempdir;

    my $output_logfile = catfile($ENV{OPENQA_WORKER_LOGDIR}, hostname() . '-1.log');
    my @loglevels = qw(trace debug info warn error fatal);
    my $counterChannel = @loglevels;


    for my $level (@loglevels) {
        my $app = OpenQA::Worker::App->new(
            mode => 'production',
            log_name => 'worker',
            instance => 1,
            log_dir => $ENV{OPENQA_WORKER_LOGDIR},
            level => $level
        );
        setup_log($app, undef, $app->log_dir, $app->level);

        my $logging_test_file1 = tempfile;
        my $logging_test_file2 = tempfile;

        add_log_channel('channel 1', path => $logging_test_file1, level => $level, default => 'set');
        add_log_channel('channel 2', path => $logging_test_file2, level => $level);

        log_trace('trace message');
        log_debug('debug message');
        log_info('info message');
        log_warning('warn message');
        log_error('error message');

        throws_ok { log_fatal('fatal message') } qr/fatal message/, 'exception raised';

        my %matches = map { $_ => 1 } (Mojo::File->new($logging_test_file1)->slurp =~ m/$reChannel/gm);
        is keys(%matches), $counterChannel, "Worker default channel 1 log level $level entry";

        %matches = map { $_ => 1 } (Mojo::File->new($logging_test_file2)->slurp =~ m/$reChannel/gm);
        is keys(%matches), 0, "Worker not default channel 2 log level $level entry";

        %matches = map { $_ => 1 } (Mojo::File->new($output_logfile)->slurp =~ m/$reFile/gm);
        is keys(%matches), 0, "Worker not default channels log level $level entry";

        truncate $logging_test_file1, 0;
        truncate $logging_test_file2, 0;



        add_log_channel('channel 2', path => $logging_test_file2, level => $level, default => 'append');

        log_trace('trace message');
        log_debug('debug message');
        log_info('info message');
        log_warning('warn message');
        log_error('error message');

        throws_ok { log_fatal('fatal message') } qr/fatal message/, 'exception raised';

        %matches = map { $_ => 1 } (Mojo::File->new($logging_test_file1)->slurp =~ m/$reChannel/gm);
        is keys(%matches), $counterChannel, "Worker default channel 1 log level $level entry";

        %matches = map { $_ => 1 } (Mojo::File->new($logging_test_file2)->slurp =~ m/$reChannel/gm);
        is keys(%matches), $counterChannel, "Worker append to default channel 2 log level $level entry";

        %matches = map { $_ => 1 } (Mojo::File->new($output_logfile)->slurp =~ m/$reFile/gm);
        is keys(%matches), 0, "Worker not default channels log level $level entry";


        remove_log_channel('channel 1');
        truncate $logging_test_file1, 0;
        truncate $logging_test_file2, 0;

        log_trace('trace message');
        log_debug('debug message');
        log_info('info message');
        log_warning('warn message');
        log_error('error message');

        throws_ok { log_fatal('fatal message') } qr/fatal message/, 'exception raised';

        %matches = map { $_ => 1 } (Mojo::File->new($logging_test_file1)->slurp =~ m/$reChannel/gm);
        is keys(%matches), 0, "Worker default channel 1 log level $level entry";

        %matches = map { $_ => 1 } (Mojo::File->new($logging_test_file2)->slurp =~ m/$reChannel/gm);
        is keys(%matches), $counterChannel, "Worker append to default channel 2 log level $level entry";

        %matches = map { $_ => 1 } (Mojo::File->new($output_logfile)->slurp =~ m/$reFile/gm);
        is keys(%matches), 0, "Worker not default channels log level $level entry";


        remove_log_channel('channel 2');
        truncate $logging_test_file1, 0;
        truncate $logging_test_file2, 0;

        log_trace('trace message');
        log_debug('debug message');
        log_info('info message');
        log_warning('warn message');
        log_error('error message');

        throws_ok { log_fatal('fatal message') } qr/fatal message/, 'exception raised';

        %matches = map { $_ => 1 } (Mojo::File->new($logging_test_file1)->slurp =~ m/$reChannel/gm);
        is keys(%matches), 0, "Worker default channel 1 log level $level entry";

        %matches = map { $_ => 1 } (Mojo::File->new($logging_test_file2)->slurp =~ m/$reChannel/gm);
        is keys(%matches), 0, "Worker append to default channel 2 log level $level entry";

        %matches = map { $_ => 1 } (Mojo::File->new($output_logfile)->slurp =~ m/$reFile/gm);
        is keys(%matches), $counterChannel, "Worker not default channels log level $level entry";


        truncate $output_logfile, 0;


        $counterChannel--;
    }
};

subtest 'Fallback to stderr/stdout' => sub {
    delete $ENV{OPENQA_LOGFILE};
    my $tempdir = tempdir;
    local $ENV{OPENQA_WORKER_LOGDIR} = $tempdir;

    # let _log_to_channel_by_name and _log_via_mojo_app fail
    my $utils_mock = Test::MockModule->new('OpenQA::Log');
    my $log_via_channel_tried = 0;
    my $log_via_mojo_app_tried = 0;
    $utils_mock->redefine(
        _log_to_channel_by_name => sub {
            ++$log_via_channel_tried;
            return 0;
        });
    $utils_mock->redefine(
        _log_via_mojo_app => sub {
            ++$log_via_mojo_app_tried;
            return 0;
        });

    # add a channel (which shouldn't be used, though)
    my $logging_test_file1 = tempfile;
    add_log_channel('channel 1', path => $logging_test_file1, level => undef, default => 'set');

    # write some messages which should be printed to stdout/stderr
    stdout_like {
        log_debug('debug message');
        log_info('info message');
    }
    qr/.*debug message.*\n.*info message.*/, 'debug/info written to stdout';
    stderr_like {
        log_warning('warning message');
        log_error('error message');
    }
    qr/.*warning message.*\n.*error message.*/, 'warning/error written to stderr';

    # check whether _log_msg attempted to use all ways to log before falling back
    is $log_via_channel_tried, 4, 'tried to log all four messages via the default channel';
    is $log_via_mojo_app_tried, 4, 'tried to log all four messages via Mojolicious app';
    is +Mojo::File->new($logging_test_file1)->slurp, '', 'nothing written to logfile';

    # check fallback on attempt to log to invalid channel
    $utils_mock->redefine(
        _log_to_channel_by_name => sub {
            ++$log_via_channel_tried;
            return $utils_mock->original('_log_to_channel_by_name')->(@_);
        });
    stderr_like {
        log_error('goes to stderr after all', channels => [qw(foo bar)]);
    }
    qr/.*goes to stderr after all.*/, 'logging to invalid channel ends up on stderr';
    is $log_via_channel_tried, 6, 'tried to log the message via the 2 channels';
    is $log_via_mojo_app_tried, 5, 'tried to log via Mojolicious app';

    # check fallback when logging to channel throws an exception
    $utils_mock->unmock('_log_to_channel_by_name');
    my $log_mock = Test::MockModule->new('Mojo::Log');
    $log_mock->redefine(
        error => sub {
            ++$log_via_channel_tried;
            die 'not enough disk space or whatever';
        });
    stderr_like {
        log_error('goes to stderr after all');
    }
    qr/.*goes to stderr after all.*/, 'logging to invalid channel ends up on stderr';
    is $log_via_channel_tried, 7, 'tried to log via the default channel';
    is $log_via_mojo_app_tried, 6, 'tried to log via Mojolicious app';

    # clear the system
    $utils_mock->unmock_all();
    $log_mock->unmock_all();
    remove_log_channel('channel 1');
};

subtest 'Formatting' => sub {
    my $time = gettimeofday;
    my $hires_mock = Test::MockModule->new('Time::HiRes');
    $hires_mock->redefine(gettimeofday => sub() { $time });
    my @loglevels = qw(debug info warn error fatal);
    for my $level (@loglevels) {
        like log_format_callback(undef, $level, ("test $level")), qr/\[.+\] \[$level\] test $level\n$/,
          "Formatting for $level works";
    }
};

subtest 'Formatting settings' => sub {
    my %vars = (FOO => 'bar', _SECRET_TOKEN => 'token', THE_PASSWORD => '123');
    my $log = format_settings \%vars;
    like $log, qr/FOO=bar.*THE_PASSWORD.*_SECRET_TOKEN/s, 'all keys and normal values present';
    unlike $log, qr/token/, 'secret token not present';
    unlike $log, qr/123/, 'password not present';
};

ok get_channel_handle, 'get_channel_handle returns valid handle from app';

done_testing;
