#!/bin/perl

use Modern::Perl;
use autodie qw(:all);
no indirect ':fatal';

use version ; our $VERSION = qv('1.3.6');

use Try::Tiny ;

#DONE:  1.  Most of it.
#TODO:  1.  Improve reporting - rerunning etc
#DONE:  2.  Honour , but warn about disabled tests and test conditions.  This might exist but looks broken.
#TODO:  3.  Rowcount seems broken.  runGDRTests.pl -c local_loaddecisioning -i up_CollectPaymentStatus.cs
#TODO:  4.  BUM.  There was something else !
#TODO:  5.  Tighten exactness of debugging over sql exec errors.

use Carp;
use Text::Diff;
use File::Basename;

use DBI;

use VSGDR::UnitTest::TestSet::Test;
use VSGDR::UnitTest::TestSet::Test::TestCondition;
use VSGDR::UnitTest::TestSet::Representation;
use VSGDR::UnitTest::TestSet::Resx;

use List::MoreUtils qw(any) ;

#use Smart::Comments;

use Getopt::Euclid qw( :vars<opt_> );
use Data::Dumper;
#use Data::Printer;

my $dataBase                = $opt_connection;
my $priv_dataBase           = undef ;
$priv_dataBase              = $opt_pconnection if defined $opt_pconnection ;



my %Parsers = () ;
my %ValidParserMakeArgs = ( vb  => 'NET::VB'
                          , cs  => 'NET::CS'
                          , xls => 'XLS'
                          , xml => 'XML'
                          ) ;

#my @validSuffixes       = keys %ValidParserMakeArgs ;
my @validSuffixes       = map { '.'.$_ } keys %ValidParserMakeArgs ;

#warn Dumper @validSuffixes ;
#exit;

my $reInit           = 1 ;
$reInit              = (!$opt_noReInit)  if defined $opt_noReInit ;

my $Warn             = 1 ;
$Warn                = (!$opt_noWarn)    if defined $opt_noWarn ;

my $Debug            = 0 ;
$Debug               = (!$opt_noDebug)   if defined $opt_noDebug ;

### Connect to database
my $dbh ;
my $dbh_quote_conn ;
$dbh                = DBI->connect("dbi:ODBC:${dataBase}", q{}, q{}, { AutoCommit => 1, PrintWarn => 1, PrintError => 1, RaiseError => 1});
$dbh_quote_conn     = DBI->connect("dbi:ODBC:${dataBase}", q{}, q{}, { AutoCommit => 1, PrintWarn => 1, PrintError => 1, RaiseError => 1});

# Always create a $priv_dbh handle, re-use the normal database dsn if no privileged dsn specified.
my $priv_dbh    = undef ;
$priv_dbh       = get_Priv_dbh() ;

# if noreinit specified
# loop over input files, picking up init and cleardown acction
# if compatible run init and teardown once out side loop
# else fallback to previous implementation

my $commonInitSQL             = undef ;
my $commonCleardownSQL        = undef ;

my $compatibleactions   = 1  ;
my @cleanupScripts      = () ;
my @initScripts         = () ;

my @cleanupActions      = () ;
my @initActions         = () ;

my @testSets            = () ;

if ( ! $reInit && ( scalar @opt_infile > 1 )) {

#    for ( my $i=0; $i <= $#opt_infile; $i++ ) {  ## Process SQL scripts:::                 done
    foreach my $infile (@opt_infile) {  ## Process SQL scripts:::                 done

        my($infname, $directories, $insfx) = fileparse($infile, @validSuffixes);
        $insfx      = lc $insfx ;
        
        croak 'Invalid input file'  unless exists $ValidParserMakeArgs{$insfx} ;
        $Parsers{${insfx}}  = VSGDR::UnitTest::TestSet::Representation->make( { TYPE => $ValidParserMakeArgs{${insfx}} } )
            unless exists $Parsers{${insfx}} ;

        my $testSet         = $Parsers{$insfx}->deserialise($infile);
        push @testSets, $testSet ;

        my $resxname    = $infname . '.resx' ;
        my $i_resx      = VSGDR::UnitTest::TestSet::Resx->new() ;
        $i_resx->deserialise($resxname) ;

        my $rh_testScripts  = $i_resx->scripts() ;

        push @cleanupScripts,   $rh_testScripts->{testCleanupAction}    if exists $rh_testScripts->{testCleanupAction} ;
        push @initScripts,      $rh_testScripts->{testInitializeAction} if exists $rh_testScripts->{testInitializeAction} ;

        $commonInitSQL                = $rh_testScripts->{$testSet->initializeActionLiteral()} ;
        $commonCleardownSQL           = $rh_testScripts->{$testSet->cleanupActionLiteral()} ;

        $testSet = undef ;

#    map { $test_testScripts{$_} = $$rh_testScripts{$_} } keys %{$rh_testScripts} ;

    }

    @cleanupActions = grep { defined($_->cleanupAction())  }   @testSets ;
    @initActions    = grep { defined($_->initializeAction()) } @testSets ;

    if ( ( scalar @cleanupActions  and ( scalar @testSets != scalar @cleanupActions ) )
      or ( scalar @initActions     and ( scalar @testSets != scalar @initActions ) )
       ) {
        $compatibleactions = 0 ;
    }
    else {
        if (@testSets > 1) {

            my $firstInitSQL = $initScripts[0] ;
            ( my $fi = $firstInitSQL) =~ s{\s+}{\ }xmsi ;
            shift @initScripts ;

            my $firstCleanupSQL = $cleanupScripts[0] ;
            ( my $fc = $firstCleanupSQL) =~ s{\s+}{\ }xmsi ;
            shift @cleanupScripts ;

            local $_ = undef ;
            while ( $_ = shift @initScripts) {
                s{\s+}{\ }xmsi ;
                 $compatibleactions = 0
                    if diff(\$_,\$fi) ;
            }
            while ( $_ = shift @cleanupScripts) {
                s{\s+}{\ }xmsi ;
                $compatibleactions = 0
                    if diff(\$_,\$fc) ;
            }
        }
    }
}
else {
    $compatibleactions = 0 ;    ## fallback to normal behaviour
}

say {*STDERR} 'Incompatible init/cleanup scripts, falling back to per-test init/cleanup'
    if not $compatibleactions and not $reInit ;

# don't bother with conditions if short-cutting init/teardown. we're implicitly short-cutting things anyway
if ( ! $reInit ) {
    if ( $compatibleactions ) {
        if ( $commonInitSQL ) {
            say {*STDERR} 'Running init';
            my $p_sth = $priv_dbh->prepare($commonInitSQL,{odbc_exec_direct => 1});
            $p_sth->execute();
            $p_sth->finish();
        }
    }
}


#for ( my $i=0; $i <= $#opt_infile; $i++ ) {  ## Process SQL scripts:::                 done
foreach my $infile (@opt_infile) {  ## Process SQL scripts:::                 done

    my($infname, $directories, $insfx) = fileparse($infile, @validSuffixes);
    $insfx          = substr(lc $insfx,1) ;    
    croak 'Invalid input file'  unless exists $ValidParserMakeArgs{$insfx} ;

    my $parser      = VSGDR::UnitTest::TestSet::Representation->make( { TYPE => $ValidParserMakeArgs{${insfx}} } );
    my $testSet     = $parser->deserialise($infile) ; ## doesn't die on a bad language eg cs for vb - probably a PRD replacement issue
    my $ra_tests    = $testSet->tests() ;


    my $resxname    = $infname . '.resx' ;
#say {*STDERR} "Running tests in $resxname" ;
    my $i_resx      = VSGDR::UnitTest::TestSet::Resx->new() ;
    $i_resx->deserialise($resxname) ;

    my $rh_testScripts  = $i_resx->scripts() ;

    my $TestSQL             = undef ;
    my $PreTestSQL          = undef ;
    my $PostTestSQL         = undef ;

    my $initSQL             = undef ;
    my $cleardownSQL        = undef ;

    $initSQL                = $rh_testScripts->{$testSet->initializeActionLiteral()} ;
    $cleardownSQL           = $rh_testScripts->{$testSet->cleanupActionLiteral()} ;

    $initSQL                = $priv_dbh->quote($initSQL)                if $initSQL ;
    $cleardownSQL           = $priv_dbh->quote($cleardownSQL)           if $cleardownSQL ;
    $initSQL                = 'exec sp_executesql N' . $initSQL         if $initSQL ;
    $cleardownSQL           = 'exec sp_executesql N' . $cleardownSQL    if $cleardownSQL ;


    # don't bother with conditions if short-cutting init/teardown. we're implicitly short-cutting things anyway
    if ( ! $reInit ) {
        if ( ! $compatibleactions ) {
            if ( $initSQL ) {
                say {*STDERR} 'Running init';
                my $p_sth = $priv_dbh->prepare($initSQL,{odbc_exec_direct => 1});
                $p_sth->execute();
                $p_sth->finish();
            }
        }
    }

    for my $test ( @{$ra_tests} ) {
#warn Dumper $test;
        my %test_res ;
        my @test_res = () ;

        try {

            say {*STDERR} "Performing test :- @{[$test->testName()]}" ;


            if ( $initSQL  and $reInit ) {
                say {*STDERR} 'Running setup';
                my $p_sth = $priv_dbh->prepare($initSQL,{odbc_exec_direct => 1});
                $p_sth->execute();

                @test_res = () ;
                do {
                    $p_sth->{PrintError} = 0;
                    $p_sth->{RaiseError} = 0;
                    push @test_res, $p_sth->fetchall_arrayref() ;

                } while ($p_sth->{odbc_more_results}) ;
                $p_sth->finish();
                { my @res         = @test_res ;
                  $test_res{INIT} = [ @res ] ;
                }
            }
            $TestSQL            = q{} ;
            $PreTestSQL         = q{} ;
            $PostTestSQL        = q{} ;

#warn Dumper $test->testAction() ;
#warn Dumper $rh_testScripts->{$test->testAction()};
#exit;

            $TestSQL            = $rh_testScripts->{$test->testAction()}        if $test->testAction()     !~ m{\A \s* (?:null|nothing) \s* \z}ixsm;
            $PreTestSQL         = $rh_testScripts->{$test->preTestAction()}     if $test->preTestAction()  !~ m{\A \s* (?:null|nothing) \s* \z}ixsm;
            $PostTestSQL        = $rh_testScripts->{$test->postTestAction()}    if $test->postTestAction() !~ m{\A \s* (?:null|nothing) \s* \z}ixsm;
#warn Dumper $TestSQL ;
            $TestSQL            = $dbh_quote_conn->quote($TestSQL)              if $TestSQL ;
            $PreTestSQL         = $dbh_quote_conn->quote($PreTestSQL)           if $PreTestSQL ;
            $PostTestSQL        = $dbh_quote_conn->quote($PostTestSQL)          if $PostTestSQL;
#warn Dumper $TestSQL ;
            $TestSQL            = 'exec sp_executesql N' . $TestSQL             if $TestSQL ;
            $PreTestSQL         = 'exec sp_executesql N' . $PreTestSQL          if $PreTestSQL ;
            $PostTestSQL        = 'exec sp_executesql N' . $PostTestSQL         if $PostTestSQL;

#warn Dumper $TestSQL ;

            if ( $PreTestSQL ) {
                say {*STDERR} 'Running PreTest' ;
#warn Dumper $test->preTestAction()  ;
#warn Dumper $rh_testScripts->{$test->preTestAction()}  ;
#warn Dumper $PreTestSQL  ;

                my $p_sth = $priv_dbh->prepare($PreTestSQL,{odbc_exec_direct => 1});
                $p_sth->execute();

                @test_res = () ;
                do {
                    $p_sth->{PrintError} = 0;
                    $p_sth->{RaiseError} = 0;
                    push @test_res, $p_sth->fetchall_arrayref() ;

                } while ($p_sth->{odbc_more_results}) ;
                { my @res         = @test_res ;
                  $test_res{PRETEST} = [ @res ] ;
                }
            }

#warn $TestSQL;
            say {*STDERR} 'Running Test' ;
            my $sth  = $dbh->prepare($TestSQL,{odbc_exec_direct => 1});
            
            try {
                $sth->{PrintError} = 0;
                $sth->{RaiseError} = 1;
                $sth->execute;
            }
            catch {
                if ($Debug ) {
                    say {*STDERR} $TestSQL ;
                }
            }
            finally {
#                say {*STDERR} "ARSE!";
            }
            
#warn Dumper $TestSQL ;
            @test_res = () ;
            do {
                $sth->{PrintError} = 0;
                $sth->{RaiseError} = 0;
                push @test_res, $sth->fetchall_arrayref() ;

            } while ($sth->{odbc_more_results}) ;
#warn Dumper @test_res;            
            $sth->finish();
            { my @res         = @test_res ;
              $test_res{TEST} = [ @res ] ;
            }
            if ( $PostTestSQL ) {
                say {*STDERR} 'Running PostTest' ;
                my $p_sth = $priv_dbh->prepare($PostTestSQL,{odbc_exec_direct => 1});
                $p_sth->execute();

                @test_res = () ;
                do {
                    $p_sth->{PrintError} = 0;
                    $p_sth->{RaiseError} = 0;
                    push @test_res, $p_sth->fetchall_arrayref() ;

                } while ($sth->{odbc_more_results}) ;
                $p_sth->finish();
                { my @res         = @test_res ;
                  $test_res{POSTTEST} = [ @res ] ;
                }
            }


            if ( $cleardownSQL and $reInit ) {
                say {*STDERR} 'Running cleardown';
                my $p_sth = $priv_dbh->prepare($cleardownSQL,{odbc_exec_direct => 1});
                $p_sth->execute();

                @test_res = () ;
                do {
                    $p_sth->{PrintError} = 0;
                    $p_sth->{RaiseError} = 0;
                    push @test_res, $p_sth->fetchall_arrayref() ;

                } while ($sth->{odbc_more_results}) ;
                { my @res         = @test_res ;
                  $test_res{CLEARDOWN} = [ @res ] ;
                }
            }
#warn Dumper @{$test->test_conditions()} ;
        foreach my $tc (@{$test->test_conditions()}) {
#            say STDERR $tc->conditionName . " failed."
#                unless $tc->check( $test_res{TEST} ) ;
#            warn Dumper $tc
#               unless $tc->check( $test_res{TEST} ) ;
#say $tc->conditionEnabled;
            if ( $tc->conditionISEnabled() ) {
                my $res = $tc->check( $test_res{TEST} );
#say $res;                            
            }
            else {
                say {*STDERR} 'Condition ' . $tc->conditionName . ' disabled.'
                    if $Warn ;
            };

        }

        %test_res = () ;

        }
        catch {
            carp $_ ;
            if ($Debug ) {
                say {*STDERR} $rh_testScripts->{$test->testAction()} ;
            }
        }
        finally {
        }
    }

    # don't bother with conditions if short-cutting init/teardown. we're implicitly short-cutting things anmyway
    if ( ! $reInit ) {
        if ( ! $compatibleactions ) {
            if ( $cleardownSQL ) {
                say {*STDERR} 'Running cleardown';
                my $p_sth = $priv_dbh->prepare($cleardownSQL,{odbc_exec_direct => 1});
                $p_sth->execute();
                $p_sth->finish();
            }
        }
    }
}

# don't bother with conditions if short-cutting init/teardown. we're implicitly short-cutting things anmyway
if ( ! $reInit ) {
    if ( $compatibleactions ) {
        if ( $commonCleardownSQL ) {
            say {*STDERR} 'Running cleardown';
            my $p_sth = $priv_dbh->prepare($commonCleardownSQL,{odbc_exec_direct => 1});
            $p_sth->execute();
            $p_sth->finish();
        }
    }
}

exit ;

# #######################################################################################


END {
    $dbh->disconnect()              if $dbh ;
    $dbh_quote_conn->disconnect()   if $dbh_quote_conn ;
    $priv_dbh->disconnect()         if $priv_dbh ;
}

# #######################################################################################


sub get_Priv_dbh {
    local $_ = undef ;
    my $_dbh ;

    if ( defined $priv_dataBase ) {
        $_dbh       = DBI->connect("dbi:ODBC:${priv_dataBase}", q{}, q{}, { AutoCommit => 1, PrintWarn => 1, PrintError => 1, RaiseError => 1})
    }
    else {
        $_dbh       = DBI->connect("dbi:ODBC:${dataBase}", q{}, q{}, { AutoCommit => 1, PrintWarn => 1, PrintError => 1, RaiseError => 1})
    }
    return $_dbh ;
}

__DATA__


=head1 NAME


runGDRTests.pl - Runs GDR test files.
Runs multiple input files, mixes vb and cs sources, can run setup and teardown once per run, once per test file
or for each test.

=head1 VERSION

1.3.6

=head1 USAGE

runGDRTests.pl -i <infile> -c <odbc connection> [options]


=head1 REQUIRED ARGUMENTS

=over

=item  -c[onnection]  [=]<dsn>

Specify ODBC connection for Test script


=item  -i[n][file]    [=]<file>

Specify input file

=for Euclid:
    file.type:    readable
    repeatable


=back



=head1 OPTIONS

=over

=item  -pc[onnection] [=]<dsn>

Specify privileged ODBC connection for Setup/Teardown scripts


=item  --[no]ReInit

[Don't] run initialisation and cleanup code for each test.  Perform once only for each run. (Or per
test file if not compatible across files.)

=for Euclid:
    false: --noReInit


=item  --[no]Warn

[Don't] warn of disabled test conditions.

=for Euclid:
    false: --noWarn



=item  --[no]Debug

[Don't] print failing test SQL

=for Euclid:
    false: --noDebug



=back


=head1 AUTHOR

Ded MedVed.



=head1 BUGS

Hopefully none.



=head1 COPYRIGHT

Copyright (c) 2012, Ded MedVed. All Rights Reserved.
This module is free software. It may be used, redistributed
and/or modified under the terms of the Perl Artistic License
(see http://www.perl.com/perl/misc/Artistic.html)

