package PS::MOPS::Test::UnitTester;

## @class PS::MOPS::Test::UnitTester
# Provide services for testing units in PS1-MOPS.
#
# @todo Separate testing environment definitions from tester.
#
# @par DESCRIPTION
# The UnitTester calls individual unit tests and collates their results, and
# then reports their results to various destinations.
#
# @par ARGUMENTS
# retrieve_storables
# write_to_database
# write_results_to_stdout
# write_results_to_file
#
# A PS-MOPS environment (i.e. an instance) is created for unit testing. Given
# a configuration file, the UnitTester performs the tests listed in the file.
# The tester manages unit testing for PS-MOPS Perl modules. Individual test
# results are recorded and used in the generation of a report. Output options
# are to STDOUT, file, and database. The number of tests performed is
# determined from the configuration file.
#
# All output outside of the test harness should go to STDERR.
#
# The testing protocol is implemented from Test::Harness::TAP.
#
# @par INPUT
# Tests are specified in tests-to-run.csv by default.
#
# Format of columns:
#
# PSMOPS function = 1 if a PSMOPS function.
#                   0 if not a PSMOPS function.
#                   -1 if test is not implemented.
#
# The -1 option allows for specifying tests that may be implemented in the
# future.
#
# @par Configuration file column descriptions
# Module = Module to which function belongs.
#
# Test = Name of test, by convention this will match the function/method name
#        if it is a PSMOPS function.
#
# 1st Modifier = Descriptive term about the test.
#
# 2nd Modifier = Additional descriptive term.
#
# Description = Not being used.
#
# @par OUTPUT
#
# Results are output to a file.
#
# @par Output file column descriptions
#
# Number = Number of test.
#
# Module = Module to which function belongs if a PSMOPS function.
#          Not PSMOPS, otherwise.
#
# Version = VCS version of the module.
#
# Function = Function/method name in module.
#
# 1st Modifier
#
# 2nd Modifier
#
# Result = PASS, if test was successful.
#          FAIL, if test failed.
#          NI, if test not implemented.
#
# Wall = Wallclock time [s].
#
# CPU = CPU time [s].
#
# @par METHODOLOGY
# The method by which the tester operates is that it creates a MOPS
# environment that is used to invoke the various functions that are being
# tested. A number of MOPS objects are fabricated and used for various tests.
# Due to increase dependencies that higher level modules have, it is more
# efficient to create a single testing environment, instead of creating a
# unique environment for each module being tested. In this sense, the unit
# tests are not completely independent of each other. However, it should be
# possible to run a specific test by including all the previous tests that
# precede the desired test. By doing this, tests can be run for individual
# modules.
#
# @note testing state - The current code being tested, notated by VCS revision
# numbers.

use strict;
no strict "refs";
our $VERSION = sprintf "%d", q$Revision: $ =~ /(\d+)/g;

my $DEBUG=0;

use Env;
use Storable; # Used for vcs versions and testing state.
use Benchmark ':hireswallclock';
use warnings; # Put warnings here to avoid warning from Benchmark.

require Exporter;

our @ISA=qw(Exporter);
our @EXPORT=qw(testUnits);

use PS::MOPS::Test::Config qw(UNIT_TEST_RESULTS_PATH
                              DEFAULT_UNIT_TEST_CONFIG_PATH
                              DEFAULT_UNIT_TEST_CONFIG_FILE
                              MODULE_TEST_MAPPING_FILE);

# Global variables.
our $g_WRITE_TO_DATABASE=0;
our $g_WRITE_RESULTS_TO_STDOUT=0;
our $g_WRITE_RESULTS_TO_FILE=0;
our $g_CONFIG_FILE_FULLPATH=undef; # Full path to configuration file.
our $g_STORABLE_PATH="/tmp";
our $g_testing_log_name="";

## @fn testUnits()
# Perform MOPS unit testing.
#
# @param test_instance_name Name of the Test Instance. This is the same as
# that given to the VCS extractor and installer.
#
# @param write_to_database If set to 1 will write results to the testing
# system database.
#
# @param retrieve_storables If set to 1 will retrieve testing state and VCS
# versions from Storables.
#
# @param config_file Fullpath to a custom configuration file.
#
# @return 1 for successful completion, 0 for unsuccessful completion.
sub testUnits
{
    my(%arg)=@_;

    print "Running testUnits()...\n";

    my $vcs_versions_ref=(); # The VCS versions.
    my $testing_state_ref=(); # The VCS versions of the Testing System.
    if($arg{retrieve_storables}){
        $vcs_versions_ref=retrieve("$g_STORABLE_PATH/vcs_versions-$arg{test_instance_name}.stor");
        $testing_state_ref=retrieve("$g_STORABLE_PATH/testing_state-$arg{test_instance_name}.stor");
    }

    my $g_VCS_VERSIONS=$vcs_versions_ref;
    my $g_TESTING_STATE=$testing_state_ref;

    use PS::MOPS::Test::Utility;

    # Process arguments passed to the subroutine.
    ## @todo More error checking for arguments.
    $g_testing_log_name=$arg{testing_log_name};
    
    if( defined($arg{write_to_database}) && $arg{write_to_database}==1) {
        $g_WRITE_TO_DATABASE=1;
    }
    if( defined($arg{write_results_to_stdout}) && $arg{write_results_to_stdout}==1) {
        $g_WRITE_RESULTS_TO_STDOUT=1;
    }
    if( defined($arg{write_results_to_file}) && $arg{write_results_to_file}==1) {
        $g_WRITE_RESULTS_TO_FILE=1;
    }
    if( defined($arg{config_file}) ){ # Custom config file is being used.
        $g_CONFIG_FILE_FULLPATH=$arg{config_file};
    }
    else {
        $g_CONFIG_FILE_FULLPATH=DEFAULT_UNIT_TEST_CONFIG_PATH . "/" . DEFAULT_UNIT_TEST_CONFIG_FILE;
    }

    if($DEBUG){
        print "g_WRITE_TO_DATABASE=$g_WRITE_TO_DATABASE\n";
        print "g_WRITE_RESULTS_TO_STDOUT=$g_WRITE_RESULTS_TO_STDOUT\n";
        print "PERL5LIB=$ENV{PERL5LIB}\n";
        print "INC=@INC\n";
    }

    my $unit_test_results_path=UNIT_TEST_RESULTS_PATH;
    my $system_unit_test_config_path=DEFAULT_UNIT_TEST_CONFIG_PATH;

    # Check for existence of config file.
    if(!(-e $g_CONFIG_FILE_FULLPATH)){
        die "ERROR: Config file $g_CONFIG_FILE_FULLPATH not found.";
    }

    # Flags.
    my $opt_itimes=0; # Show individual test times.
    my $opt_observe_pauses=0; # Observe pauses in config file.

    my $input;
    my @system_tests=();

    # Write results to a file.
    my $report_time=getFormattedTime();
    my $report_filename="$unit_test_results_path/unit-test-results_" . $report_time . ".csv";
    if (-e $report_filename && $g_WRITE_RESULTS_TO_FILE) {
        print STDERR "ERROR: Report file exists!\n";
        exit;
    }

    # @todo Separate database operations from the UnitTester by removing database
    # module dependency.

    # Import testing libraries.
    use PS::MOPS::Test::DataHandler;
    use PS::MOPS::DC::Test::Instance;
    use PS::MOPS::DC::Test::Fields;
    use PS::MOPS::DC::Test::Detections;
    use PS::MOPS::DC::Test::Tracklets;
    use PS::MOPS::DC::Test::Unit::Globals qw($g_TEST_INSTANCE
                                             $g_NUM_TEST_FIELDS
                                             $g_NUM_TEST_DETECTIONS
                                             $g_NUM_TEST_TRACKLETS
                                             $g_FIELDS
                                             $g_DETECTIONS
                                             $g_TRACKLETS);

    # Import unit tests.
    use PS::MOPS::DC::Test::Unit::Fields;
    use PS::MOPS::DC::Test::Unit::Detections;
    use PS::MOPS::DC::Test::Unit::Tracklets;
    use PS::MOPS::Test::OrbitsInterface;
    use PS::MOPS::Test::DerivedObjectsInterface;

    # Make test instance.
    my $inst=PS::MOPS::DC::Test::Instance->new(test_instance_name=>$arg{test_instance_name},
                                               delete_on_finish=>$arg{delete_on_finish});

    $g_TEST_INSTANCE=$inst;

    _defineTestingEnvironment();

    # Load tests to be run.

    # Test function name = test_{function}__{1st modifier}__{2nd modifier}
    # where modifiers are optional but allow for distinguishing multiple tests
    # of the same function.

    my $tests_ref=readCSVFile(filename=>$g_CONFIG_FILE_FULLPATH); # Read file containing list of tests.
    my $module_test_map=readCSVFile(filename=>"$system_unit_test_config_path/" . MODULE_TEST_MAPPING_FILE);
    my $num_tests=scalar(@{$tests_ref});
    for (my $i=0;$i<$num_tests;$i++) {
        my $prefix;
        $system_tests[$i]->{module}=@{$tests_ref}[$i]->[1];

        # Set prefix of test to be run by comparing each line of the module
        # test mapping file.

        my $match=0;
        for (my $j=0;$j<scalar(@{$module_test_map});$j++) { # Iterate over module mappings.
            my $psmops_module_name=@{$module_test_map}[$j]->[0];
            if ($psmops_module_name eq $system_tests[$i]->{module}) {
                $prefix=@{$module_test_map}[$j]->[1] . "::"; # Set prefix to mapped test module.
                $match=1;
                last;
            }
        }
        if (!$match) {
            print "ERROR: Unmatched module @{$tests_ref}[$i]->[1]!\n";
            exit;
        }

        $system_tests[$i]->{function}=@{$tests_ref}[$i]->[2];
        $system_tests[$i]->{psmops_function}=0; # It's not a PSMOPS function.
        my $f=$prefix;
        if (@{$tests_ref}[$i]->[0]==1) { # It's a PSMOPS function.
            $f.= "test_";
            $system_tests[$i]->{psmops_function}=1;
        }
        $f.=@{$tests_ref}[$i]->[2];
        if(defined(@{$tests_ref}[$i]->[3])){
            if ((@{$tests_ref}[$i]->[3] eq '' || !defined(@{$tests_ref}[$i]->[3])) && @{$tests_ref}[$i]->[4] ne '') { # 2nd modifier used without 1st.
                printf STDERR ("ERROR: 2nd modifier used without 1st modifier at test %d.\n",$i+1);
                exit;
            }
        }
        if (@{$tests_ref}[$i]->[3]) {
            $f.="__" . @{$tests_ref}[$i]->[3];
            $system_tests[$i]->{modifier1}=@{$tests_ref}[$i]->[3];
            if (@{$tests_ref}[$i]->[4]) {
                $f.="__" . @{$tests_ref}[$i]->[4];
                $system_tests[$i]->{modifier2}=@{$tests_ref}[$i]->[4];
            }
        }
        $system_tests[$i]->{subroutine_call}=$f;
        if (@{$tests_ref}[$i]->[0]==-1) { # It's not implemented.
            $system_tests[$i]->{ni}=1;
        }
    }

    print STDERR "\nUsing database ";
    print STDERR $inst->getDB()->getName();
    print STDERR ".\n";
    print STDERR "Testing with $g_NUM_TEST_FIELDS test fields.\n";
    print STDERR "Testing with $g_NUM_TEST_DETECTIONS test detections.\n";
    print STDERR "Testing with $g_NUM_TEST_TRACKLETS test tracklet.\n";
    print STDERR "\n";

    # Perform system tests.
    print STDERR "Running $num_tests tests...\n";
    print "1..$num_tests\n";

    my $system_start=new Benchmark;
    my $diff;
    for (my $i=0;$i<scalar(@system_tests);$i++) {
        my $test=$system_tests[$i]->{subroutine_call};
        printf STDERR ("%d: Testing $test...\n",$i+1);
        my $start=new Benchmark;
        if (defined($system_tests[$i]->{ni})) {
            print STDERR "\tWarning: not implemented\n";
            printf("ok %d # skip Not implemented.\n",$i+1);
        } else {
            # Run a single test.
            $system_tests[$i]->{result}=&$test(); # Record the results of testing.
            if($system_tests[$i]->{result}==1){printf("ok %d\n", $i+1);}
            else{printf("not ok %d\n", $i+1);}
        }
        my $end=new Benchmark;
        $diff=timediff( $end, $start );
        for(my $j=0;$j<scalar(@$diff);$j++){ # Rewrite zeros.
            if($diff->[$j]==0){
                $diff->[$j]="0.0";
            }
        }
        $system_tests[$i]->{benchmark_diff}=$diff;
        $system_tests[$i]->{version}=getVCSVersion(module=>$system_tests[$i]->{module},
                                                   vcs_versions=>$g_VCS_VERSIONS);
        $system_tests[$i]->{vcs_time}=getVCSTime(module=>$system_tests[$i]->{module},
                                                 vcs_versions=>$g_VCS_VERSIONS);

        print STDERR "Time: ", timestr( $diff, 'all' ), "\n\n" if $opt_itimes;
        print STDERR "\n";
    }

    my $system_end=new Benchmark;
    $diff=timediff( $system_end, $system_start );
    print STDERR "System Time: ", timestr( $diff, 'all' ), "\n\n";

    # Process the results.
    print STDERR "Writing results to file $report_filename...\n" if $g_WRITE_RESULTS_TO_FILE;

    # Iterate over all the test results and form an array of arrays containing
    # the result data. This is the data that gets written to the database.
    #
    # Format of results array:
    #
    # Index 0 = integer - Test number.
    #       1 = {1|0} - 1 indicates a PS-MOPS function.
    #       2 = string - Module name being tested.
    #       3 = string - Version value of the module being tested.
    #       4 = string - Last VCS time for the module.
    #       5 = string - Function in the module being tested.
    #       6 = string - First test name modifier.
    #       7 = string - Second test name modifier.
    #       8 = {NI|PASS|FAIL!} - Result value for the test.
    #       9 = float - Benchmark wall time.
    #      10 = float - Benchmark cpu time.
    my @results=();
    for (my $i=0;$i<scalar(@system_tests);$i++) { # Iterate over data lines.
        my @entry=(); # Results entries.
        $entry[0]=$i+1;
        $entry[1]=$system_tests[$i]->{psmops_function};
        $entry[2]=$system_tests[$i]->{module};
        $entry[3]=$system_tests[$i]->{version};
        $entry[4]=$system_tests[$i]->{vcs_time};
        $entry[5]=$system_tests[$i]->{function};
        $entry[6]=$system_tests[$i]->{modifier1};
        $entry[7]=$system_tests[$i]->{modifier2};

        if (defined($system_tests[$i]->{ni})) {
            $entry[8]="NI";
        } elsif (defined($system_tests[$i]->{result})) {
            if ($system_tests[$i]->{result}==1) {
                $entry[8]="PASS";
            } elsif ($system_tests[$i]->{result}==0) {
                $entry[8]="FAIL!";
            } else {
                print STDERR "ERROR: Undefined result for test $i.\n";
                exit;
            }
        } else {
            print STDERR "ERROR: Undefined result for test $i.\n";
            exit;
        }
        $entry[9]=$system_tests[$i]->{benchmark_diff}[0];

        $entry[10]=($system_tests[$i]->{benchmark_diff}[1]
        +$system_tests[$i]->{benchmark_diff}[2]
        +$system_tests[$i]->{benchmark_diff}[3]
        +$system_tests[$i]->{benchmark_diff}[4]);
        push(@results,\@entry);
    }

    if($g_WRITE_TO_DATABASE){
        _writeResultsToDatabase(results=>\@results,
                                testing_state=>$g_TESTING_STATE);
    }

    if($g_WRITE_RESULTS_TO_STDOUT){
        print "Report (";
        print $report_time;
        print "):\n";
        _writeResultsToSTDOUT(system_tests=>\@system_tests);
    }

    if($g_WRITE_RESULTS_TO_FILE){
        _writeResultsToFile(filename=>$report_filename,
                            results=>\@results);
    }

    print STDERR "\n";
    return 1;
} # End testUnits.


## @fn private _writeResultsToDatabase()
# Write unit testing results to the results database.
#
# @param results Array reference.
# @param testing_state Array reference.
#
# @todo Separate database reporting operations from the UnitTester.
# @return 1 for success, 0 for fail.
sub _writeResultsToDatabase
{
     my(%arg)=@_;
     print STDERR "Writing results to database...\n";
     my $dh=PS::MOPS::Test::DataHandler->new();
     my $result=0;
     $result=$dh->writeUnitTestResults(results=>$arg{results},
                                       testing_state=>$arg{testing_state},
                                       testing_log_name=>$g_testing_log_name);
     return 1 if $result;
     return 0;
}

## @fn private _writeResultsToSTDOUT()
# Output a report to the terminal. This is not up to date with the rest of the
# testing system.
#
# @todo Add support for modifier2.
# @todo Have this report generated from the results file.
# @todo Automate column sizing.
#
# @param system_tests Array reference.
sub _writeResultsToSTDOUT
{
    my(%arg)=@_;
    my @system_tests=@{$arg{system_tests}};
    printf("%-7.7s%-11.11s%-6.6s%-26.26s%-18.18s%-7.7s%-9.9s%-9.9s\n","Number","Module","Ver","Function","Modifier1","Result","Wall","CPU");
    my $num_tests=scalar(@system_tests);
    for (my $i=0;$i<$num_tests;$i++) {
        printf("%-7.7s",$i);
        my $module=$system_tests[$i]->{module};

        if ($module=~/.*\:\:(.*)$/) {
            printf("%-11.10s", $1);
        }

        my $ver=$system_tests[$i]->{version};
        printf("%-6.5s",$ver);

        printf("%-26.25s", $system_tests[$i]->{function});
        if ($system_tests[$i]->{modifier1}) {
            printf("%-18.17s",$system_tests[$i]->{modifier1});
        } else {
            printf("%-18.17s","");
        }

        if (!defined($system_tests[$i]->{ni})) {
            if ($system_tests[$i]->{result}==1) {
                printf("%-7.6s","PASS");
            } else {
                printf("%-7.6s","FAIL!");
            }
            printf("%-9.5f",$system_tests[$i]->{benchmark_diff}[0]);

            # TODO: Fix precision of CPU.
            my $cpu=$system_tests[$i]->{benchmark_diff}[1]*1.0
            +$system_tests[$i]->{benchmark_diff}[2]*1.0
            +$system_tests[$i]->{benchmark_diff}[3]*1.0
            +$system_tests[$i]->{benchmark_diff}[4]*1.0;
            printf("%-9.5f",$cpu);
        } else {
            if (defined($system_tests[$i]->{ni})) {
                if ($system_tests[$i]->{ni}==1) {
                    printf("%-7.6s","NI");
                }
            }
        }
        print "\n";
    }
}

## @fn private _writeResultsToFile()
# Write results to a file.
#
# @param filename Filename of file to write to.
# @param results Array reference.
sub _writeResultsToFile
{
    my(%arg)=@_;
    writeCSVFile(filename=>$arg{filename},
                 header=>[qw(Number
                             PSMOPS
                             Module
                             Ver
                             Time
                             Test
                             Modifier1
                             Modifier2
                             Result
                             Wall
                             CPU)], # Spaces are not supported in the header values.
                 data=>$arg{results});
}

# Define the parameters of the testing environment.
sub _defineTestingEnvironment {
    $g_NUM_TEST_FIELDS=10;
    $g_NUM_TEST_DETECTIONS=4;
    $g_NUM_TEST_TRACKLETS=1;
}

1;
