The Perl Toolchain Summit needs more sponsors. If your company depends on Perl, please support this very important event.
# -*- cperl -*-

use warnings FATAL => qw(all);

use ExtUtils::testlib;
use Test::More;
use Test::Exception;
use Test::Warn ;
use Test::Differences ;
use Test::Memory::Cycle;
use Config::Model;
use Config::Model::AnyId;
use Log::Log4perl qw(:easy :levels) ;

use strict;

my $arg = shift || '';

my $log = 0 ;

my $trace = $arg =~ /t/ ? 1 : 0 ;
$log                = 1 if $arg =~ /l/;

my $home = $ENV{HOME} || "";
my $log4perl_user_conf_file = "$home/.log4config-model";

if ($log and -e $log4perl_user_conf_file ) {
    Log::Log4perl::init($log4perl_user_conf_file);
}
else {
    Log::Log4perl->easy_init($log ? $WARN: $ERROR);
}

my $model = Config::Model -> new ( ) ;

Config::Model::Exception::Any->Trace(1) if $arg =~ /e/;

ok(1,"compiled");

my @element = (

    # Value constructor args are passed in their specific array ref
    cargo => {
        type       => 'leaf',
        value_type => 'string'
    },
);

# minimal set up to get things working
$model->create_config_class(
    name    => "Master",
    element => [
        bounded_list => {
            type       => 'list',
            list_class => 'Config::Model::ListId',    # default

            max   => 123,
            cargo => {
                type       => 'leaf',
                value_type => 'string'
            },
        },
        plain_list                => { type => 'list', @element },
        list_with_auto_created_id => {
            type            => 'list',
            auto_create_ids => 4,
            @element
        },
        olist => {
            type  => 'list',
            cargo => {
                type              => 'node',
                config_class_name => 'Slave'
            },
        },
        list_with_default_with_init_leaf => {
            type              => 'list',
            default_with_init => {
                0 => 'def_1 stuff',
                1 => 'def_2 stuff'
            },
            @element,
        },
        list_with_default_with_init_node => {
            type              => 'list',
            default_with_init => {
                0 => 'X=Bv Y=Cv',
                1 => 'X=Av'
            },
            cargo => {
                type              => 'node',
                config_class_name => 'Slave'
            },
        },
        map { 
              ("list_with_".$_."_duplicates" => { type => 'list', duplicates => $_ , @element, },);
            } qw/warn allow forbid suppress/ ,
    ]
);

$model->create_config_class(
    name    => "Bogus",
    element => [
        list_with_wrong_auto_create => {
            type            => 'list',
            auto_create_ids => ['foo'],
            @element
        },
        list_with_wrong_duplicates => {
            type              => 'list',
            duplicates => 'forbid',
            cargo => {
                type              => 'node',
                config_class_name => 'Slave'
            },
        },
        list_with_yada_duplicates => { 
            type => 'list', 
            duplicates => 'yada' ,
            @element, 
        },
    ]
);

$model->create_config_class(
    name    => "Slave",
    element => [
        [qw/X Y Z/] => {
            type       => 'leaf',
            value_type => 'enum',
            choice     => [qw/Av Bv Cv/]
        },
    ]
);

ok(1,"config classes created") ;

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;

eq_or_diff( [ $root->fetch_element('olist')->fetch_all_indexes ],
    [], "check index list of empty list" );

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

my $b = $root->fetch_element('bounded_list');
ok( $b, "bounded list created" );
is($inst->needs_save,0,"verify instance needs_save status after element creation") ;

is( $b->fetch_with_id(1)->store('foo'), 1, "stored in 1" );
is( $b->fetch_with_id(0)->store('baz'), 1, "stored in 0" );
is( $b->fetch_with_id(2)->store('bar'), 1, "stored in 2" );
is($inst->needs_save,3,"verify instance needs_save status after storing into element") ;
print  join("\n", $inst->list_changes("\n")),"\n" if $trace;

throws_ok { $b->fetch_with_id(124)->store('baz'); }
qr/Index 124 > max_index limit 123/, 'max error caught';

my $bogus_root = $model->instance( root_class_name => 'Bogus' )->config_root;
throws_ok { $bogus_root->fetch_element('list_with_wrong_auto_create'); }
qr/Wrong auto_create argument for list/, 'wrong auto_create caught';

eq_or_diff( [ $b->fetch_all_indexes ], [ 0, 1, 2 ], "check ids" );

$b->delete(1);
is( $b->fetch_with_id(1)->fetch, undef, "check deleted id" );

is( $b->index_type, 'integer', 'check list index_type' );
is( $b->max_index,  123,       'check list max boundary' );

$b->push( 'toto', 'titi' );
is( $b->fetch_with_id(2)->fetch, 'bar',  "check last item of table" );
is( $b->fetch_with_id(3)->fetch, 'toto', "check pushed toto item" );
is( $b->fetch_with_id(4)->fetch, 'titi', "check pushed titi item" );

$b->push_x(
    values     => [ 'toto', 'titi' ],
    check      => 'no',
    annotation => ['toto comment']
);
is( $b->fetch_with_id(5)->fetch, 'toto', "check pushed toto item with push_x" );
is( $b->fetch_with_id(5)->annotation,
    'toto comment', "check pushed toto annotation with push_x" );
is( $b->fetch_with_id(6)->fetch, 'titi', "check pushed titi item with push_x" );

$b->push_x(
    values     => 'toto2',
    check      => 'no',
    annotation => 'toto2 comment'
);

is( $b->fetch_with_id(7)->fetch, 'toto2', "check pushed toto2 item with push_x" );
is( $b->fetch_with_id(7)->annotation,
    'toto2 comment', "check pushed toto2 annotation with push_x" );

my @all = $b->fetch_all_values;
eq_or_diff( \@all, [qw/baz bar toto titi toto titi toto2/], "check fetch_all_values" );

my $lac = $root->fetch_element('list_with_auto_created_id');
eq_or_diff(
    [ $lac->fetch_all_indexes ],
    [ 0 .. 3 ],
    "check list_with_auto_created_id"
);

map { is( $b->fetch_with_id($_)->index_value, $_, "Check index value $_" ); }
  ( 0 .. 4 );

$b->move( 3, 4 );
is( $b->fetch_with_id(3)->fetch, undef,  "check after move idx 3 in 4" );
is( $b->fetch_with_id(4)->fetch, 'toto', "check after move idx 3 in 4" );
map {
    is( $b->fetch_with_id($_)->index_value, $_, "Check moved index value $_" );
} ( 0 .. 4 );

$b->fetch_with_id(3)->store('titi');
$b->swap( 3, 4 );

map {
    is( $b->fetch_with_id($_)->index_value, $_,
        "Check swapped index value $_" );
} ( 0 .. 4 );

is( $b->fetch_with_id(3)->fetch, 'toto', "check value after swap" );
is( $b->fetch_with_id(4)->fetch, 'titi', "check value after swap" );

$b->remove(3);
is( $b->fetch_with_id(3)->fetch, 'titi', "check after remove" );

# test move swap with node list
my $ol = $root->fetch_element('olist');

my @set = ( [qw/X Av/], [qw/X Bv/], [qw/Y Av/], [qw/Z Cv/], [qw/Z Av/], );

my $i = 0;
foreach my $item (@set) {
    my ( $e, $v ) = @$item;
    $ol->fetch_with_id( $i++ )->fetch_element($e)->store($v);
}

$inst->clear_changes ;

$ol->move( 3, 4 );
is($inst->needs_save,1,"verify instance needs_save status after move") ;
print scalar $inst->list_changes,"\n" if $trace ;
$inst->clear_changes ;

is( $ol->fetch_with_id(3)->fetch_element('Z')->fetch,
    undef, "check after move idx 3 in 4" );
is( $ol->fetch_with_id(4)->fetch_element('Z')->fetch,
    'Cv', "check after move idx 3 in 4" );
map {
    is( $ol->fetch_with_id($_)->index_value, $_, "Check moved index value $_" );
} ( 0 .. 4 );

$ol->swap( 0, 2 );
is($inst->needs_save,1,"verify instance needs_save status after move") ;
print scalar $inst->list_changes,"\n" if $trace ;
$inst->clear_changes ;

is( $ol->fetch_with_id(0)->fetch_element('X')->fetch,
    undef, "check after move idx 0 in 2" );
is( $ol->fetch_with_id(0)->fetch_element('Y')->fetch, 'Av',
    "check after move" );

is( $ol->fetch_with_id(2)->fetch_element('Y')->fetch,
    undef, "check after move" );
is( $ol->fetch_with_id(2)->fetch_element('X')->fetch, 'Av',
    "check after move" );

map {
    is( $ol->fetch_with_id($_)->index_value, $_, "Check moved index value $_" );
} ( 0 .. 4 );
print $root->dump_tree( experience => 'beginner' ) if $trace;

is( $ol->fetch_with_id(0)->fetch_element('X')->fetch,
    undef, "check before move" );
$ol->remove(0);
print $root->dump_tree( experience => 'beginner' ) if $trace;
is( $ol->fetch_with_id(0)->fetch_element('X')->fetch, 'Bv',
    "check after move" );

# test store
my @test = (
    [ a1         => ['a1'] ],
    [ '"a","b"'  => [qw/a b/] ],
    [ 'a,b'      => [qw/a b/] ],
    [ '"a\"a",b' => [qw/a"a b/] ],
    [ '"a,a",b'  => [ 'a,a', 'b' ] ],
    [ '",a1"'    => [',a1'] ],
);
foreach my $l (@test) {
    $b->load( $l->[0] );
    eq_or_diff( [ $b->fetch_all_values ], $l->[1], "test store $l->[0]" );
}

throws_ok { $b->load('a,,b'); } "Config::Model::Exception::Load",
  "fails load 'a,,b'";

# test preset mode
$inst->preset_start;
my $pl = $root->fetch_element('plain_list');

$pl->fetch_with_id(0)->store('prefoo');
$pl->fetch_with_id(1)->store('prebar');
$inst->preset_stop;
ok( 1, "filled preset values" );

eq_or_diff(
    [ $pl->fetch_all_values ],
    [ 'prefoo', 'prebar' ],
    "check that preset values are read"
);
$pl->fetch_with_id(2)->store('bar');

eq_or_diff(
    [ $pl->fetch_all_values ],
    [ 'prefoo', 'prebar', 'bar' ],
    "check that values are read"
);

eq_or_diff( [ $pl->fetch_all_values( mode => 'custom' ) ],
    ['bar'], "check that custom values are read" );

# test default_with_init on leaf
my $lwdwil = $root->fetch_element('list_with_default_with_init_leaf');
# note: calling fetch_all_indexes is required to trigger creation of default_with_init keys
eq_or_diff([$lwdwil->fetch_all_indexes],[0,1],"check default keys");
is($lwdwil->fetch_with_id(0)->fetch,'def_1 stuff',"test default_with_init leaf 0") ;
is($lwdwil->fetch_with_id(1)->fetch,'def_2 stuff',"test default_with_init leaf 1") ;

# test default_with_init on node
my $lwdwin = $root->fetch_element('list_with_default_with_init_node');
eq_or_diff([$lwdwin->fetch_all_indexes],[0,1],"check default keys");
is($lwdwin->fetch_with_id(0)->fetch_element('X')->fetch,'Bv',"test default_with_init node 0") ;
is($lwdwin->fetch_with_id(0)->fetch_element('Y')->fetch,'Cv',"test default_with_init node 0") ;
is($lwdwin->fetch_with_id(1)->fetch_element('X')->fetch,'Av',"test default_with_init node 0") ;

throws_ok { $bogus_root->fetch_element('list_with_wrong_duplicates'); } "Config::Model::Exception::Model",
  "fails duplicates with node cargo";

throws_ok { $bogus_root->fetch_element('list_with_yada_duplicates'); } "Config::Model::Exception::Model",
  "fails yada duplicates";

foreach my $what (qw/forbid warn suppress/) {
    my $lwd = $root->fetch_element('list_with_'.$what.'_duplicates');
    $lwd->push(qw/string1 string2/);
    $lwd->push('string1'); # does not trigger duplicate issues, yet
    $lwd->push('string1'); # does not trigger duplicate issues, yet
    
    # there we go
    if ($what eq 'forbid') {
        is($lwd->needs_check, 1, "verify needs_check is true") ;
        throws_ok { $lwd->fetch_all_values ; } "Config::Model::Exception::WrongValue", 
            "fails forbidden duplicates" ;
        is($lwd->needs_check, 0, "verify needs_check after fetch_all_values") ;
        throws_ok { $lwd->fetch_all_values ; } "Config::Model::Exception::WrongValue", 
            "fails forbidden duplicates even if needs_check is false" ;
        is($lwd->needs_check, 0, "verify again needs_check after fetch_all_values") ;
        $lwd->delete(2) ;
        is($lwd->needs_check, 1, "verify needs_check after list content modif") ;
    }
    elsif ($what eq 'warn') {
        warnings_like { $lwd->fetch_all_values ; } qr/Duplicated/ ,
            "warns with duplicated values" ;
        is($lwd->has_fixes, 2,"check nb of fixes") ;
        $inst->apply_fixes ;
        warnings_like { $lwd->fetch_all_values ; } [] , # no warning accepted
            "no longer warns with duplicated values" ;
    }
    else {
        $lwd->check_content ; 
    }
    is ($lwd->fetch_with_id(0)->fetch,'string1',
        "check that original values is untouched after $what duplicates");
}

# test preset clear stuff
# done after auto_create_ids tests, because preset_clear or layered_clear
# also clean up auto_create_ids (if there's no data in there)
$pl->clear ;
eq_or_diff( [ $pl->fetch_all_indexes ], [ ] ,"check that preset stuff was cleared");

$inst->preset_start;
$pl->fetch_with_id(0)->store('prefoo');
$pl->fetch_with_id(1)->store('prebar');
$inst->preset_stop;
eq_or_diff( [ $pl->fetch_all_indexes ], [0,1] ,"check preset indexes");
$pl->fetch_with_id(1)->store('bar');
$inst->preset_clear ;
eq_or_diff( [ $pl->fetch_all_indexes ], [0] ,"check that only preset stuff was cleared");
is($pl->fetch_with_id(0)->fetch,'bar',"check that bar was moved from 1 to 0");

# test layered stuff
$pl->clear ;
$inst->layered_start;
$pl->fetch_with_id(0)->store('prefoo');
$pl->fetch_with_id(1)->store('prebar');
$inst->layered_stop;
eq_or_diff( [ $pl->fetch_all_indexes ], [0,1] ,"check layered indexes");
$pl->fetch_with_id(1)->store('bar');
$inst->layered_clear ;
eq_or_diff( [ $pl->fetch_all_indexes ], [0] ,"check that only layered stuff was cleared");
is($pl->fetch_with_id(0)->fetch,'bar',"check that bar was moved from 1 to 0");
$pl->clear ;

# test sort
@set = qw/j h g f/ ;
$pl->store_set(@set);
$inst->clear_changes ;
$pl->sort;
eq_or_diff( [ $pl->fetch_all_values ], [sort @set] ,"check sort result");
is($inst->c_count, 1, "check that sort has triggered a change") ;
$pl->sort;
is($inst->c_count, 1, "check that redundant sort has not triggered a change") ;

# test unshift
@set = qw/j h g f/ ;
$pl->store_set(qw/a b/);
$pl->unshift(qw/1 2 3 4/);
eq_or_diff( [ $pl->fetch_all_values ], [qw/1 2 3 4 a b/] ,"check unshift result");
eq_or_diff( [ $pl->fetch_all_indexes ], [(0..5)] ,"check that indexes are reset correctly");

# test insert_at
$pl->store_set(qw/a b/);
$pl->insert_at(qw/1 d e/);
eq_or_diff( [ $pl->fetch_all_values ], [qw/a d e b/] ,"check insert_at result");
eq_or_diff( [ $pl->fetch_all_indexes ], [(0..3)] ,"check that indexes are reset correctly");

# test insert_before
$pl->store_set(qw/foo baz/);
$pl->insert_before( qw/baz bar1 bar2/);
eq_or_diff( [ $pl->fetch_all_values ], [qw/foo bar1 bar2 baz/] ,"check insert_before result");

$pl->insert_before( qr/z/ , qw/bar3 bar4/);
eq_or_diff( [ $pl->fetch_all_values ], [qw/foo bar1 bar2 bar3 bar4 baz/] ,"check insert_before with regexp result");

# test insort
my @set1 = qw/c1 e i1 j1 p1/ ;
my @set2 = qw/a2 z2 d2 e b2 k2/ ;
$pl->store_set(@set1);
$pl->sort;
$pl->insort( @set2 );
eq_or_diff( [ $pl->fetch_all_values ], [sort(@set1, @set2)] ,"check insort result");

memory_cycle_ok($model,"memory cycles");

done_testing;