# SPDX-License-Identifier: GPL-2.0-or-later

package AmavisTest;

use v5.20;
use List::Util;
use Test::MockModule;
use Test::Most;
use base 'Test::Class';

package Local {
    sub new {
        my $self = bless {}, shift;
        for my $var (@_) {
            push @{$self->{vars}}, [$var,  $$var ] if ref $var eq 'SCALAR';
            push @{$self->{vars}}, [$var, [@$var]] if ref $var eq 'ARRAY';
            push @{$self->{vars}}, [$var, {%$var}] if ref $var eq 'HASH';
        }
        return $self;
    }
    sub DESTROY {
        my $self = shift;
        for my $var (@{$self->{vars}}) {
            my ($ref, $value) = @$var;
            $$ref =  $value if ref $ref eq 'SCALAR';
            @$ref = @$value if ref $ref eq 'ARRAY';
            %$ref = %$value if ref $ref eq 'HASH';
        }
    }
}

sub new_policy_bank ($@) {
    my ($name, @args) = @_;
    # Supplemented in after_chroot_init
    $Amavis::Conf::policy_bank{$name} = {
        policy_bank_name => $name,
        policy_bank_path => $name,
        @args
    };
    return $name;
}

sub mock {
    my ($class, %redefine) = @_;
    my $mock = Test::MockModule->new($class);
    while (my ($name, $sub) = each %redefine) {
        if ($class->can($name)) {
            $mock->redefine($name => $sub);
        } else {
            $mock->define($name => $sub);
        }
    }
    $mock;
}

sub class { 'Amavis' }

sub startup : Tests(startup => 1) {
    my $test = shift;
    use_ok $test->class;
}

sub init_builtin_macros : Tests(3) {
    my $test = shift;

    use_ok $test->class;
    use_ok 'Amavis::In::Message';

    local $Amavis::MSGINFO = Amavis::In::Message->new;
    $Amavis::MSGINFO->body_digest('some digest');

    Amavis::init_builtin_macros();
    is $Amavis::builtins{b}->(), 'some digest';
}

sub load_policy_bank : Tests {
    my $test = shift;

    use_ok $test->class;

    # Use own scope guard for variables. The downside of using `local $var`
    # is that a "reference relationship" is not "preserved", i.e.
    ## our $log_templ   = 'default log template';
    ## our %policy_bank = (log_templ => \$log_templ);
    ## {
    ##   local $log_templ = 'custom log template';
    ##   say ${$policy_bank{log_templ}}; # 'default log template'
    ## }
    ## say ${$policy_bank{log_templ}}; # 'default log template'
    #
    my $local = Local->new(
        \$Amavis::Conf::log_templ,
        \%Amavis::Conf::current_policy_bank,
        \%Amavis::Conf::policy_bank,
        \@Amavis::Conf::av_scanners,
    );

    $Amavis::Conf::log_templ = 'log_templ';
    @Amavis::Conf::av_scanners = ('av_scanner');

    my @log;
    my $mock = Test::MockModule->new($test->class)->redefine(
        do_log => sub ($$;@) {
            push @log, sprintf $_[1], @_[2..$#_];
        }
    );

    subtest 'base policy bank' => sub {
        # from post_accept_hook
        Amavis::load_policy_bank('');
        ok $log[$#log], 'loaded base policy bank';
        is $Amavis::Conf::current_policy_bank{policy_bank_name}, '';
        is $Amavis::Conf::current_policy_bank{policy_bank_path}, '';
        is ${$Amavis::Conf::current_policy_bank{log_templ}}, 'log_templ';
        is_deeply $Amavis::Conf::current_policy_bank{av_scanners}, ['av_scanner'];
        push @Amavis::Conf::av_scanners, 'av_scanner2';
        is_deeply $Amavis::Conf::current_policy_bank{av_scanners}, ['av_scanner', 'av_scanner2'];
    };

    subtest 'first policy bank' => sub {
        Amavis::load_policy_bank(new_policy_bank('test', log_templ => 'template'));
        is $Amavis::Conf::current_policy_bank{policy_bank_name}, 'test';
        is $Amavis::Conf::current_policy_bank{policy_bank_path}, 'test';
        is $Amavis::Conf::current_policy_bank{log_templ}, 'template';
        is_deeply $Amavis::Conf::current_policy_bank{av_scanners}, ['av_scanner', 'av_scanner2'];
    };

    subtest 'second policy bank' => sub {
        Amavis::load_policy_bank(new_policy_bank('subtest', log_templ => 'other_templ', av_scanners => ['other av_scanner']));
        is $Amavis::Conf::current_policy_bank{policy_bank_name}, 'subtest';
        is $Amavis::Conf::current_policy_bank{policy_bank_path}, 'test/subtest';
        is $Amavis::Conf::current_policy_bank{log_templ}, 'other_templ';
        is_deeply $Amavis::Conf::current_policy_bank{av_scanners}, ['other av_scanner'];
    };

    subtest 'ignoring policy bank' => sub {
        Amavis::load_policy_bank('nonexistent');
        is $log[$#log], 'policy bank "nonexistent" does not exist, ignored';

        Amavis::load_policy_bank('subtest');
        is $log[$#log], 'policy bank "subtest" just loaded, ignored';
    };
}

sub load_policy_banks_for_headers : Tests {
    my $test = shift;

    use_ok $test->class;
    use_ok 'Amavis::In::Message';

    my $local = Local->new(
        \%Amavis::Conf::current_policy_bank,
        \%Amavis::Conf::policy_bank,
        \@Amavis::Conf::header_to_policy_bank_maps,
    );

    new_policy_bank($_) for qw(0 1 BANK_A BANK_B BANK_C BANK_D);

    my $mock = Test::MockModule->new($test->class)->redefine(
        do_log => sub ($$;@) {}
    );

    my @warn;
    my $policy_bank_path = sub {
        my (@orig_header) = @_;
        Amavis::load_policy_bank('');
        @warn = ();
        my $msginfo = Amavis::In::Message->new;
        $msginfo->orig_header(\@orig_header);
        {
            local $^W = 1;
            local $SIG{__WARN__} = sub {push @warn, $_[0]};
            Amavis::load_policy_banks_for_headers($msginfo);
        }
        return $Amavis::Conf::current_policy_bank{policy_bank_path};
    };

    subtest 'no lookup tables configured' => sub {
        @Amavis::Conf::header_to_policy_bank_maps = ();
        is $policy_bank_path->("X-Foo: bar\n"), '', 'nothing is loaded';
    };

    subtest 'a matching header field loads its policy bank' => sub {
        @Amavis::Conf::header_to_policy_bank_maps = Amavis::Conf::new_RE(
            [qr'^X-Foo: yes' => 'BANK_A'],
        );
        is $policy_bank_path->("X-Foo: yes\n"), 'BANK_A', 'X-Foo-Yes';
        is $policy_bank_path->("X-Foo: no\n"), '', 'X-Foo-No';
    };

    subtest 'a comma separated result loads several policy banks' => sub {
        @Amavis::Conf::header_to_policy_bank_maps = Amavis::Conf::new_RE(
            [qr'^X-Foo:' => ' BANK_A , BANK_B '],
        );
        is $policy_bank_path->("X-Foo: yes\n"), 'BANK_A/BANK_B', 'X-Foo-yes';
        @Amavis::Conf::header_to_policy_bank_maps = Amavis::Conf::new_RE(
            [qr'^X-Foo: .*one' => 'BANK_C'],
            [qr'^X-Foo: .*two' => 'BANK_D'],
        );
        is $policy_bank_path->("X-Foo: one two\n"), 'BANK_C/BANK_D';
    };

    subtest 'a boolean does not load a policy bank' => sub {
        @Amavis::Conf::header_to_policy_bank_maps = Amavis::Conf::new_RE(
            [qr'^X-Exclude:' => 0],
            qr'^X-Bare:',
        );
        is $policy_bank_path->("X-Exclude: yes\n"), '',
            'a false result does not load a policy bank named "0"';
        is $policy_bank_path->("X-Bare: yes\n"), '',
            'an implied result of 1 does not load a policy bank named "1"';
    };

    subtest 'folding' => sub {
        my $folded = "X-Foo: first line,\n continued SomeCode here\n";
        @Amavis::Conf::header_to_policy_bank_maps = Amavis::Conf::new_RE(
            [qr'^X-Foo: .* SomeCode 'is => 'BANK_A'],
        );
        is $policy_bank_path->($folded), 'BANK_A', 'folded';
    };
}

sub reload : Tests(4) {
    my $test = shift;
    use_ok $test->class;

    my (@sd_notify, $re_exec, $amavis);

    local $Amavis::max_servers = 1;
    local $Amavis::daemonize = 0;
    local $SIG{HUP};

    my $mock = mock($test->class,
        # Net::Server
        done                  => sub {0},
        run_client_connection => sub {sleep 1},
        accept                => sub {not shift->{server}{_HUP}},
        _initialize           => sub {},
        post_configure        => sub {},
        pre_bind              => sub {},
        bind                  => sub {},
        post_configure        => sub {},
        post_bind             => sub {$SIG{'HUP'}  = sub {$amavis->sig_hup}},
        log                   => sub {},
        hup_server            => sub {$re_exec = 1},
        shutdown_sockets      => sub {},
        server_exit           => sub {},
        # Amavis
        do_log                => sub ($$;@) {},
        sd_notify             => sub {push @sd_notify, $_[1]},
        post_bind_hook        => sub {},
        pre_loop_hook         => sub {},
        configs_readable      => sub {1},
    );
    $amavis = $test->class->new({commandline => []});
    my $pid = $$;
    unless (fork) {
        sleep 2;
        kill 'HUP', $pid;
        exit;
    }
    $amavis->run;
    waitpid 0, 0;
    ok $re_exec, 're-executed';
    ok not (List::Util::any {$_ eq 'STOPPING=1'} @sd_notify), 'not stopping';
    is $sd_notify[-1], 'RELOADING=1', 'reloading';
}

1;
