# -*- cperl -*-

use warnings;
use strict;

use ExtUtils::testlib;
use Test::More;
use Test::Differences;
use Test::Memory::Cycle;
use Test::Log::Log4perl;
use Test::Exception;
use Config::Model;
use Config::Model::Tester::Setup qw/init_test/;

Test::Log::Log4perl->ignore_priority("info");

my ($model, $trace) = init_test();

# minimal set up to get things working
$model->create_config_class(
    name    => "Master",
    element => [
        [qw/my_hash my_hash2 my_hash3/] => {
            type       => 'hash',
            index_type => 'string',
            cargo      => { type => 'leaf', value_type => 'string' },
        },

        choice_list => {
            type   => 'check_list',
            choice => [ 'A' .. 'Z' ],
            help   => { A => 'A help', E => 'E help' },
        },

        ordered_checklist => {
            type    => 'check_list',
            choice  => [ 'A' .. 'Z' ],
            ordered => 1,
            help    => { A => 'A help', E => 'E help' },
        },

        ordered_checklist_refer_to => {
            type     => 'check_list',
            refer_to => '- ordered_checklist',
            ordered  => 1,
        },

        choice_list_with_default => {
            type         => 'check_list',
            choice       => [ 'A' .. 'Z' ],
            default_list => [ 'A', 'D' ],
            help         => { A => 'A help', E => 'E help' },
        },

        choice_list_with_upstream_default => {
            type                  => 'check_list',
            choice                => [ 'A' .. 'Z' ],
            upstream_default_list => [ 'A', 'D' ],
            help                  => { A => 'A help', E => 'E help' },
        },

        choice_list_with_default_and_upstream_default => {
            type                  => 'check_list',
            choice                => [ 'A' .. 'Z' ],
            default_list          => [ 'A', 'C' ],
            upstream_default_list => [ 'A', 'D' ],
            help                  => { A => 'A help', E => 'E help' },
        },

        macro => {
            type       => 'leaf',
            value_type => 'enum',
            choice     => [qw/AD AH AZ/],
        },

        'warped_choice_list' => {
            type  => 'check_list',
            level => 'hidden',
            warp  => {
                follow => '- macro',
                rules  => {
                    AD => {
                        choice       => [ 'A' .. 'D' ],
                        level        => 'normal',
                        default_list => [ 'A', 'B' ]
                    },
                    AH => {
                        choice => [ 'A' .. 'H' ],
                        level  => 'normal',
                    },
                } }
        },

        refer_to_list => {
            type     => 'check_list',
            refer_to => '- my_hash'
        },

        warped_refer_to_list => {
            type     => 'check_list',
            refer_to => '- warped_choice_list',
            level    => 'hidden',
            warp     => {
                follow => '- macro',
                rules  => {
                    AD => {
                        choice => [ 'A' .. 'D' ],
                        level  => 'normal',
                    },
                },
            },
        },

        refer_to_2_list => {
            type     => 'check_list',
            refer_to => '- my_hash + - my_hash2   + - my_hash3'
        },

        refer_to_check_list_and_choice => {
            type              => 'check_list',
            computed_refer_to => {
                formula   => '- refer_to_2_list + - $var',
                variables => { var => '- indirection ' },
            },
            choice => [qw/A1 A2 A3/],
        },

        indirection => { type => 'leaf', value_type => 'string' },

        dumb_list => {
            type  => 'list',
            cargo => { type => 'leaf', value_type => 'string' }
        },
        refer_to_dumb_list => {
            type     => 'check_list',
            refer_to => '- dumb_list + - my_hash',
        },
        'Ciphers',
        {
            'ordered'               => '1',
            'upstream_default_list' => [
                '3des-cbc',   'aes128-cbc', 'aes128-ctr',   'aes192-cbc',
                'aes192-ctr', 'aes256-cbc', 'aes256-ctr',   'arcfour',
                'arcfour128', 'arcfour256', 'blowfish-cbc', 'cast128-cbc'
            ],
            'type' => 'check_list',
            'description' =>
                'Specifies the ciphers allowed for protocol version 2 in order of preference. By default, all ciphers are allowed.',
            'choice' => [
                'aes128-cbc', '3des-cbc',   'blowfish-cbc', 'cast128-cbc',
                'arcfour128', 'arcfour256', 'arcfour',      'aes192-cbc',
                'aes256-cbc', 'aes128-ctr', 'aes192-ctr',   'aes256-ctr'
            ]
        },

    ] );

my $inst = $model->instance(
    root_class_name => 'Master',
    instance_name   => 'test1'
);
ok( $inst, "created dummy instance" );
$inst->initial_load_stop;

my $root = $inst->config_root;

my $cl = $root->fetch_element('choice_list');

# check get_choice
is_deeply( [ $cl->get_choice ], [ 'A' .. 'Z' ], "check_get_choice" );

is( $inst->needs_save, 0, "verify instance needs_save status after creation" );

ok( 1, "test get_checked_list for empty check_list" );
my @got = $cl->get_checked_list;
is( scalar @got, 0, "test nb of elt in check_list " );
is_deeply( \@got, [], "test get_checked_list after set_checked_list" );

my %expect;

my $hr = $cl->get_checked_list_as_hash;
is_deeply( $hr, \%expect, "test get_checked_list_as_hash for empty checklist" );

# check help
is( $cl->get_help('A'), 'A help', "test help" );

is( $inst->needs_save, 0, "verify instance needs_save status after reading meta data" );

subtest 'test _store method' => sub {
    # test with the polymorphic 'store' method
    my @test_args = (
        [ [ 'S', 1, 'yes' ], 1, ['S'] ],
        [ [ 'A', 1, 'yes' ], 2, ['A','S'] ],
        [ [ 'A', 0, 'yes' ], 1, ['S'] ],
        [ [ 'bug', 1, 'skip' ], 1, ['S'] ],
    );

    foreach my $test_arg_ref ( @test_args) {
        my ($args, $nb, $expect) = @$test_arg_ref;
        $cl->_store( @$args );
        ok( 1, "test _store method with @$args" );
        @got = $cl->get_checked_list;
        is( scalar @got, $nb, "test nb of elt in check_list after _store" );
        is_deeply( \@got, $expect, "test get_checked_list after _store" );
        $inst->clear_changes;
    }
};

subtest 'test _store warning' => sub {
    my $foo = Test::Log::Log4perl->expect(
        ignore_priority => 'info',
        [ 'User', warn => qr/Unknown check_list item/ ]
        );
    $cl->_store('bug-skipped', 1, 'skip');
};

throws_ok { $cl->_store('bug-error', 1, 'yes') } qr/wrong value/, 'test _store error';

subtest 'test store method' => sub {
    # test with the polymorphic 'store' method
    my @store_args = (
        [ 'S,T,O,R,E' ],
        [ value => 'S,T ,  O, R, E' ],
        [ 'S,O,T,R,E', check => 'yes' ],
        [ value => 'S,T ,  O, R, E', check => 'yes' ],
        [ 'S,T,O,R,E,bug', check => 'skip' ],
    );

    foreach my $test_arg ( @store_args) {
        $cl->store( @$test_arg );
        ok( 1, "test store method with @$test_arg" );
        @got = $cl->get_checked_list;
        is( scalar @got, 5, "test nb of elt in check_list after set" );
        is_deeply( \@got, [sort qw/S T O R E/], "test get_checked_list after set" );
        $inst->clear_changes;
    }
};

$cl->clear;

subtest "test set method and reported changes" => sub {
    my @set_args = (
        # set string,  changes , content after changes
        [ 'A,B'    =>  'A:1 B:1',qw/A B/],
        [ 'A,B,C'  =>  'C:1',    qw/A B C/],
        [ 'A,C,D'  =>  'B:0 D:1',    qw/A C D/],
    );

    while (@set_args) {
        my $test = shift @set_args;
        my ($set_string, $expected_changes, @expected_content) = @$test;
        $cl->set( '', $set_string );
        ok( 1, "test set method with $set_string" );
        @got = $cl->get_checked_list;
        is_deeply( \@got, \@expected_content, "test get_checked_list content after set" );

        is( $inst->needs_save, !!$expected_changes, "verify instance needs_save after set" );
        print join( "\n", $inst->list_changes("\n") ), "\n" if $trace;
        eq_or_diff([$inst->list_changes], ["choice_list: set_checked_list $expected_changes"],
                   "check change message after set check list to $set_string");
        $inst->clear_changes;
    }
};

$cl->clear;
$inst->clear_changes;

my @set = sort qw/A C Z V Y/;
subtest "test get_arguments" => sub {
    my @set_args = (
        \@set,
        [ \@set ],
        [ \@set , check => 'yes' ],
    );

    foreach my $test_arg ( @set_args) {
        my ($list, $check, $args) = $cl->get_arguments(@$test_arg);
        ok( 1, "test set_checked_list" );
        eq_or_diff($list, \@set, "test passed list");
    }
};

subtest 'test set_checked_list method' => sub {
    my @set_args = (
        \@set,
        [ \@set ],
        [ \@set , check => 'yes' ],
        [ [ sort qw/A C Z V Y bug/ ] , check => 'skip' ],
    );

    foreach my $test_arg ( @set_args) {
        $cl->set_checked_list(@$test_arg);
        ok( 1, "test set_checked_list" );
        @got = $cl->get_checked_list;
        is( scalar @got, 5, "test nb of elt in check_list after set_checked_list" );
        is_deeply( \@got, \@set, "test get_checked_list after set_checked_list" );

        is( $inst->needs_save, 1, "verify instance needs_save after set_checked_list" );
        print join( "\n", $inst->list_changes("\n") ), "\n" if $trace;
        $cl->clear;
        $inst->clear_changes;
    }

};

subtest 'test set_checked_list error handling' => sub {
    # bug is not an allowed value
    throws_ok { $cl->set_checked_list(qw/A bug/ ) } qr/wrong value/, 'got exception';
};

subtest 'test behavior when skipping bad value' => sub {
    my $foo = Test::Log::Log4perl->expect(
        ignore_priority => 'info',
        ['User', warn => qr/Unknown check_list item/ ]
        );
    $cl->set_checked_list([qw/A bug/], check => 'skip');
};

$cl->clear;
$inst->clear_changes;

# test global get and set as hash
$cl->set_checked_list(@set);
$hr = $cl->get_checked_list_as_hash;
for ( 'A' .. 'Z' ) { $expect{$_} = 0 }
for (@set) { $expect{$_} = 1 }
eq_or_diff( $hr, \%expect, "test get_checked_list_as_hash" );

$expect{V} = 0;
$expect{W} = 1;
$cl->set_checked_list_as_hash(%expect);
ok( 1, "test set_checked_list_as_hash" );
@got = sort $cl->get_checked_list;
is_deeply( \@got, [ sort qw/A C Z W Y/ ], "test get_checked_list after set_checked_list_as_hash" );

$cl->clear;

# test global get and set
@got = $cl->get_checked_list;
is( scalar @got, 0, "test nb of elt in check_list after clear" );

eval { $cl->check('a'); };
ok( $@, "check 'a': which is an error" );
print "normal error:\n", $@, "\n" if $trace;

# test layered choice_list
$inst->layered_start;
my @l_set = qw/B M W/;
$cl->set_checked_list(@l_set);
$inst->layered_stop;

eq_or_diff( [ $cl->get_checked_list( mode => 'layered' ) ],  \@l_set, "check layered content" );
eq_or_diff( [ $cl->get_checked_list( mode => 'standard' ) ], \@l_set, "check standard content" );
eq_or_diff( [ $cl->get_checked_list() ], [], "check user content" );

$cl->set_checked_list_as_hash( V => 1, W => 1 );
eq_or_diff( [ $cl->get_checked_list( mode => 'layered' ) ],  \@l_set, "check layered content" );
eq_or_diff( [ $cl->get_checked_list( mode => 'standard' ) ], \@l_set, "check standard content" );
eq_or_diff( [ $cl->get_checked_list( mode => 'user' ) ], [qw/B M V W/], "check user content" );
eq_or_diff( [ $cl->get_checked_list() ], [qw/V W/], "check content" );

$cl->clear_layered;
eq_or_diff( [ $cl->get_checked_list( mode => 'layered' ) ],
    [], "check layered content after clear" );

# now test with a refer_to parameter

$root->load("my_hash:X=x my_hash:Y=y");
ok( 1, "load my_hash:X=x my_hash:Y=y worked correctly" );

my $rflist = $root->fetch_element('refer_to_list');
ok( $rflist, "created refer_to_list" );

is_deeply( [ $rflist->get_choice ], [qw/X Y/], 'check simple refer choices' );

$root->load("my_hash:Z=z");
ok( 1, "load my_hash:Z=z worked correctly" );

is_deeply( [ $rflist->get_choice ], [qw/X Y Z/], 'check simple refer choices after 2nd load' );

# load hashes that are used by reference check list
$root->load("my_hash2:X2=x my_hash2:X=xy");

my $rf2list = $root->fetch_element('refer_to_2_list');
ok( $rf2list, "created refer_to_2_list" );
is_deeply( [ sort $rf2list->get_choice ], [qw/X X2 Y Z/], 'check refer_to_2_list choices' );

$root->load("my_hash3:Y2=y");
is_deeply( [ sort $rf2list->get_choice ], [qw/X X2 Y Y2 Z/], 'check refer_to_2_list choices' );

my $rtclac = $root->fetch_element('refer_to_check_list_and_choice');
ok( $rtclac, "created refer_to_check_list_and_choice" );

is_deeply( [ sort $rtclac->get_choice ],
    [qw/A1 A2 A3/], 'check refer_to_check_list_and_choice choices' );

eval { $rtclac->check('X'); };
ok( $@, "get_choice with undef 'indirection' parm: which is an error" );
print "normal error:\n", $@, "\n" if $trace;

$root->fetch_element('indirection')->store('my_hash');

is_deeply( [ sort $rtclac->get_choice ],
    [qw/A1 A2 A3 X Y Z/], 'check refer_to_check_list_and_choice choices with indirection set' );

$rf2list->check('X2');
is_deeply(
    [ sort $rtclac->get_choice ],
    [ sort qw/A1 A2 A3 X X2 Y Z/ ],
    'check X2 and test choices'
);

# load hashes that are used by reference check list
$root->load("my_hash2:X3=x");
$rf2list->check( 'X3', 'Y2' );
is_deeply( [ sort $rf2list->get_choice ],
    [qw/X X2 X3 Y Y2 Z/], 'check refer_to_2_list choices with X3' );
is_deeply(
    [ sort $rtclac->get_choice ],
    [qw/A1 A2 A3 X X2 X3 Y Y2 Z/],
    'check refer_to_check_list_and_choice choices'
);

my $dflist = $root->fetch_element('choice_list_with_default');
ok( $dflist, "created choice_list_with_default" );
@got = $dflist->get_checked_list;
is_deeply( \@got, [ 'A', 'D' ], "test default of choice_list_with_default" );
@got = $dflist->get_checked_list(mode =>'custom');
is_deeply( \@got, [  ], "test custom data  of choice_list_with_default" );
is($dflist->has_data, 0, "choice_list_with_default has no data");

$dflist->check('C');
$dflist->uncheck('D');
@got = $dflist->get_checked_list;
is_deeply( \@got, [ 'A', 'C' ], "test default of choice_list_with_default" );
is($dflist->has_data, 1, "choice_list_with_default has data");

@got = $dflist->get_checked_list('custom');
is_deeply( \@got, ['C'], "test custom of choice_list_with_default" );

@got = $dflist->get_checked_list('standard');
is_deeply( \@got, [ 'A', 'D' ], "test standard of choice_list_with_default" );

@got = $dflist->get_checked_list('backend');
is_deeply( \@got, [ 'A', 'C' ], "fetch with backend mode for choice_list_with_default" );

my $warp_list;
eval { $warp_list = $root->fetch_element('warped_choice_list'); };
ok( $@, "fetch_element without warp set (macro=undef): which is an error" );
print "normal error:\n", $@, "\n" if $trace;

# force read of hidden element
$warp_list = $root->fetch_element( name => 'warped_choice_list', accept_hidden => 1 );

ok( $warp_list, "created warped_choice_list" );

eval { $warp_list->get_choice; };
ok( $@, "get_choice without warp set (macro=undef): which is an error" );
print "normal error:\n", $@, "\n" if $trace;

$root->load("macro=AD");

is_deeply(
    [ $warp_list->get_choice ],
    [ 'A' .. 'D' ],
    'check warp_list choice after setting macro=AD'
);

@got = $warp_list->get_checked_list;
is_deeply( \@got, [ 'A', 'B' ], "test default of warped_choice_list" );

$root->load("macro=AH");

is_deeply(
    [ $warp_list->get_choice ],
    [ 'A' .. 'H' ],
    'check warp_list choice after setting macro=AH'
);

@got = $warp_list->get_checked_list;
is_deeply( \@got, [], "test default of warped_choice_list after setting macro=AH" );

# test reference to list values
$root->load("dumb_list=a,b,c,d,e");

my $rtl = $root->fetch_element("refer_to_dumb_list");
is_deeply( [ $rtl->get_choice ], [qw/X Y Z a b c d e/], "check choice of refer_to_dumb_list" );

# test check list with built_in default
my $wud = $root->fetch_element("choice_list_with_upstream_default");
@got = $wud->get_checked_list();
is_deeply( \@got, [], "test default of choice_list_with_upstream_default" );
is($wud->has_data, 0, "test checklist has data");

@got = $wud->get_checked_list('upstream_default');
is_deeply( \@got, [qw/A D/], "test upstream_default of choice_list_with_upstream_default" );

# test check list with upstream_default *and* default (should override)
$inst->clear_changes;
my $wudad = $root->fetch_element("choice_list_with_default_and_upstream_default");
is( $inst->needs_save, 0, "check needs_save after reading a default value" );
@got = $wudad->get_checked_list('default');
is_deeply( \@got, [qw/A C/], "test default of choice_list_with_default_and_upstream_default" );
is( $inst->needs_save, 0, "check needs_save after reading a default value" );

@got = $wudad->get_checked_list();
is_deeply( \@got, [qw/A C/], "test choice_list_with_default_and_upstream_default" );
is( $inst->needs_save, 1, "check needs_save after reading a default value" );

is_deeply( $wudad->fetch(), 'A,C', "test fetch choice_list_with_default_and_upstream_default" );
is( $inst->needs_save, 1, "check needs_save after reading a default value" );

### test preset feature

my $pinst = $model->instance(
    root_class_name => 'Master',
    instance_name   => 'preset_test'
);
ok( $pinst, "created dummy preset instance" );

my $p_root = $pinst->config_root;

$pinst->preset_start;
ok( $pinst->preset, "instance in preset mode" );

my $p_cl = $p_root->fetch_element('choice_list');
$p_cl->set_checked_list(qw/H C L/);    # acid burn test :-)

$pinst->preset_stop;
is( $pinst->preset, 0, "instance in normal mode" );

is( $p_cl->fetch, "C,H,L", "choice_list: read preset list" );

$p_cl->check(qw/A S H/);
is( $p_cl->fetch,             "A,C,H,L,S", "choice_list: read completed preset LIST" );
is( $p_cl->fetch('preset'),   "C,H,L",     "choice_list: read preset value as preset_value" );
is( $p_cl->fetch('standard'), "C,H,L",     "choice_list: read preset value as standard_value" );
is( $p_cl->fetch('custom'),   "A,C,H,L,S", "choice_list: read custom_value" );

$p_cl->set_checked_list(qw/A S H E/);
is( $p_cl->fetch,           "A,E,H,S", "choice_list: read overridden preset LIST" );
is( $p_cl->fetch('custom'), "A,E,H,S", "choice_list: read custom_value after override" );

my $wrtl = $p_root->fetch_element( name => 'warped_refer_to_list', accept_hidden => 1 );
ok( $wrtl, "created warped_refer_to_list (hidden)" );

my $ocl = $root->fetch_element('ordered_checklist');
@got = $ocl->get_checked_list();
is_deeply( \@got, [], "test default of ordered_checklist" );

@set = qw/A C Z V Y/;
$ocl->set_checked_list(@set);
@got = $ocl->get_checked_list;
is_deeply( \@got, \@set, "test ordered_checklist after set_checked_list" );

$ocl->swap(qw/A Y/);
@got = $ocl->get_checked_list;
is_deeply( \@got, [qw/Y C Z V A/], "test ordered_checklist after swap" );

$ocl->move_up(qw/Y/);
@got = $ocl->get_checked_list;
is_deeply( \@got, [qw/Y C Z V A/], "test ordered_checklist after move_up Y" );

$ocl->move_up(qw/V/);
@got = $ocl->get_checked_list;
is_deeply( \@got, [qw/Y C V Z A/], "test ordered_checklist after move_up V" );

$ocl->move_down(qw/A/);
@got = $ocl->get_checked_list;
is_deeply( \@got, [qw/Y C V Z A/], "test ordered_checklist after move_down A" );

$ocl->move_down(qw/C/);
@got = $ocl->get_checked_list;
is_deeply( \@got, [qw/Y V C Z A/], "test ordered_checklist after move_down C" );

$ocl->check('B');
@got = $ocl->get_checked_list;
is_deeply( \@got, [qw/Y V C Z A B/], "test ordered_checklist after check B" );

$ocl->move_up(qw/B/);
$ocl->uncheck('B');
@got = $ocl->get_checked_list;
is_deeply( \@got, [qw/Y V C Z A/], "test ordered_checklist after move_up B uncheck B" );

$ocl->check('B');
@got = $ocl->get_checked_list;
is_deeply( \@got, [qw/Y V C Z B A/], "test ordered_checklist after check B" );

is( $root->grab_value( $ocl->location ), "Y,V,C,Z,B,A", "test grab_value" );

my $oclrt = $root->fetch_element('ordered_checklist_refer_to');
@got = $oclrt->get_choice();
is_deeply( \@got, [qw/Y V C Z B A/], "test default of ordered_checklist_refer_to" );

my $ciphers     = $root->fetch_element('Ciphers');
my @cipher_list = qw/aes192-cbc aes128-cbc 3des-cbc blowfish-cbc aes256-cbc/;
$ciphers->set_checked_list(@cipher_list);
eq_or_diff( [ $ciphers->get_checked_list ], \@cipher_list, "check cipher list" );

# test warp in layered mode
my $layered_i = $model->instance(
    root_class_name => 'Master',
    instance_name   => 'test_layered'
);
ok( $layered_i, "created layered instance" );
my $l_root = $layered_i->config_root;
$layered_i->layered_start;

my $locl = $l_root->fetch_element('ordered_checklist');
$locl->set_checked_list(@set);

my $loclrt = $root->fetch_element('ordered_checklist_refer_to');
@got = $loclrt->get_choice();
is_deeply( \@got, [qw/Y V C Z B A/], "test default of ordered_checklist_refer_to in layered mode" );

$inst->apply_fixes;
ok( 1, "apply_fixes works" );

print join( "\n", $inst->list_changes("\n") ), "\n" if $trace;

memory_cycle_ok( $model, "memory cycle" );

done_testing;
