package Test::Harness::YAML;
use strict;
use warnings;

use Benchmark;
use Best 0.05 [ [ qw/YAML::Syck 0.85 YAML/], qw/LoadFile DumpFile/ ];
use File::Spec;
use Getopt::Long;
use List::Util 'shuffle';
use Test::Harness;
use Test::TAP::Model;

# Package and global declarations
our @ISA = qw(Test::TAP::Model);
our $SMOKERFILE = ".smoker.yml";
our %Config;
$ENV{TEST_ALWAYS_CALLER} = 1;
$Test::Harness::Verbose  = 1;

$| = 1;

sub get_config {
    GetOptions \%Config, qw(
        --concurrent|j=i    --shuffle|s   --exclude|X=s@
        --output-file|o=s   --recurse|r   --anonymous|a
        --include=s@        --dry|n       --help|h
    );
    fix_config();
    my $Usage = qq{Usage: $0 [OPTIONS]
        --help, -h              This help message.
        --output-file=FILE, -o  Store results in FILE [default: $Config{"output-file"}]
        --dry, -n               Show which tests would be run but don't run them
        --concurrent=N, -j      Run N test jobs concurrently (MSWin requires Paralle::ForkManager)
        --shuffle, -s           Run tests in random order
        --recurse, -r           Recurse into directories on test include list
        --incude=I1,[I2,...]    Include files
        --exclude=E1,[E2,...]   Exclude files
        --anonymous, -a         Do not include ~/.smoker.yml data in report
    } . "\n";
    die $Usage if $Config{help};
}

sub fix_config {
    $Config{"concurrent"} ||= $ENV{PUGS_TESTS_CONCURRENT} || 1;
    local $@;
    eval { require Parallel::ForkManager; };
    if ($@) {
        if ($Config{"concurrent"} > 1 && $^O =~ /MSWin32|msys/) { # On cygwin we are okay.
            warn "Sorry, concurrency not supported on your platform\n";
            $Config{"concurrent"} = 1;
        }
        require POSIX;
    }
    else {
        no warnings 'redefine';
        *run_children = sub {
            my ($self, $child_count, $all_tests) = @_;
            my $pm = Parallel::ForkManager->new($child_count);

            for my $child (1 .. $child_count) {
                my @own_tests = @{$all_tests}[grep { ($_ % $child_count) == ($child-1) } (0..$#{$all_tests})];
                my $pid = $pm->start and next;
                # Inside child process now
                $self->{_child_num} = $child;
                $self->run_test($_) for @own_tests;
                $self->emit_chunk();
                $pm->finish;
                # Back in parent process now
                push @{ $self->{_children} }, $pid;
            }
            $self->gather_results();
        }
    }
    $Config{"output-file"} ||= "tests.yml";
    $Config{"recurse"}       = 1 if not defined $Config{"recurse"};
    # Needed for smokeserv
    $Config{"pugs-path"} = $ENV{HARNESS_PERL};
    push @{$Config{"exclude"}}, 'Disabled' if not $Config{"exclude"} or not @{$Config{"exclude"}};
    _build_include_re();
    _build_exclude_re();
}

get_config();

@ARGV = sort map glob, "t/*/*.t", "t/*/*/*.t", "ext/*/t/*.t" unless @ARGV;

my $s = __PACKAGE__->new;
$s->run;
$s->emit;
exit 0;

sub all_in {
    my $start = shift;

    my @hits = ();

    local *DH;
    if ( opendir( DH, $start ) ) {
        while ( my $file = readdir DH ) {
            next if $file eq File::Spec->updir || $file eq File::Spec->curdir;
            next if $file eq ".svn";
            next if $file eq "CVS";
            my $currfile = File::Spec->catfile( $start, $file );
            next if $Config{exclude_re} && $currfile =~ $Config{exclude_re};

            if ( -d $currfile ) {
                push( @hits, all_in( $currfile ) ) if $Config{recurse};
            } else {
                push( @hits, $currfile ) if $currfile =~ $Config{include_re};
            }
        }
    } else {
        warn "$start: $!\n";
    }

    return @hits;
}


# concurrency temp-file. FIXME: use a real temp file?
sub emit_chunk {
    my($self) = @_;
    DumpFile("tests.$$.yml", $self->structure);
}

sub emit {
    my($self) = @_;
    $self->{_timing}{end} = time;
    $self->{_timing}{duration} =
        $self->{_timing}{end} - $self->{_timing}{start};
    DumpFile($Config{"output-file"}, {
            meat => $self->structure,
            map { $_ => $self->{"_$_"} } qw{
                build_info smoker config revision timing
        }});
}

sub set_build_info {
    my($self) = @_;
    my $executable = $ENV{HARNESS_PERL} || "pugs";
    $ENV{PERL6LIB} = 'blib6/lib';
    $self->{_build_info} = join '', qx{$executable -V};
}

sub _build_exclude_re {
    my $excl = join "|", # map { quotemeta }
        map { split /,/ } @{ $Config{exclude} };
    $Config{exclude_re} = qr/($excl)/ if $excl;
}

sub _build_include_re {
    my @include = map { split /,/ } @{ $Config{include} };
    s/^\.// for @include;
    @include = ("t") unless @include;
    my $include_re = join( "|", map { quotemeta } @include );
    $Config{include_re} = qr/\.($include_re)$/;
}

sub _init {
    my($self) = shift;
    $self->set_build_info;
    $self->get_smoker;
    $self->get_revision;
    
    $Config{shuffle}+=0;
    $self->{_config} = \%Config;
    $self->{_timing}{start} = time;

    $self->SUPER::_init(@_);
}

sub get_smoker {
    my($self) = @_;
    if (!$Config{anonymous}) {
        $self->{_smoker} = eval { LoadFile($SMOKERFILE) } ||
            eval { LoadFile(($ENV{HOME}||'')."/$SMOKERFILE") };
        if (!$self->{_smoker}) {
            warn<<"AD";
Smoker info not found. Please create a file named $SMOKERFILE
either in this directory or under your home. You can use the
skeleton in util/smoker-example. Alternatively, say "--anonymous"
on the command line to withold your identity (and this message).
AD
        }
    }
    #$self->{_smoker} ||= { name => "anonymous" };
}

sub get_tests {
    my($self) = @_;
    my @tests;
    @ARGV = File::Spec->curdir unless @ARGV;
    push( @tests, -d $_ ? all_in( $_ ) : $_ ) for @ARGV;

    @tests = grep { $_ !~ $Config{exclude_re} } @tests if $Config{exclude_re};

    if ( @tests ) {
        if ($Config{shuffle}) {
            @tests = shuffle(@tests);
        } else {
            # default FS order isn't guaranteed sorted; and sorting
            # helps diffing raw YAML results.
            @tests = sort @tests;
        }
        if ( $Config{dry} ) {
            print join( "\n", @tests, "" );
            exit 0;
        } else {
            print "# ", scalar @tests, " tests to run\n" if $Test::Harness::debug;
        }
    }
    $self->{_config}{test_count} = scalar @tests;
    @tests;
}

sub get_revision {
    my($self) = @_;
    my $rev_get_cmd = $Config{"pugs-path"}.' -V:pugs_revision';
    do { $self->{_revision} = $1 if /pugs_revision: (\d+)\r?$/ } for `@{[$rev_get_cmd]}`;
    $self->{_revision} ||= "unknown";
    print "$rev_get_cmd returns revision '@{[$self->{_revision}]}'\n";
}

sub run {
    my $self = shift;
    return $self->SUPER::run(@_) if $Config{concurrent} == 1;
    my @tests = $self->get_tests;
    $self->run_children($Config{concurrent}, \@tests);
}

sub run_children {
    my ($self, $child_count, $all_tests) = @_;
    for my $child (1 .. $child_count) {
        my @own_tests = @{$all_tests}[grep { ($_ % $child_count) == ($child-1) } (0..$#{$all_tests})];
        defined(my $pid = fork) or die "Can't fork: $!";
        if ($pid) {
            push @{ $self->{_children} }, $pid;
        } else {
            $self->{_child_num} = $child;
            $self->run_test($_) for @own_tests;
            $self->emit_chunk();
            exit 0;
        }
    }
    $self->gather_results();
}

# the wait here is sequential rather than nonblocking / as-they-come, because
# we want to preserve ordering anyway and it's probably okay to keep a few
# zombies around for a relatively short while.
sub gather_results {
    my($self) = @_;
    my $kid;
    for my $pid (@{ $self->{_children} }) {
        my $file = "tests.$pid.yml";
        warn sprintf "waiting for chunk #%d...\n", ++$kid; 
        waitpid($pid, 0) or die "waitpid: $!";
        my $chunk = LoadFile($file) or die "can't parse chunk ($file)";
        push @{ $self->{meat}{test_files} }, @{$chunk->{test_files}};
        unlink $file or die "unlink: $!";
    }
    warn "all chunks completed.\n";
}

sub run_test {
    my $self = shift;
    my $test = shift;
    my @rest = @_;
    my $kid  = $self->{_child_num} ? "[$self->{_child_num}] " : "";
    warn "$kid$test\n";
    my $t = timeit( 1, sub {
        $self->SUPER::run_test($test, @rest);
    } );
    warn "    ".timestr($t)."\n";
}


__END__
# Simple YAML test harness written over Test::Harness::Straps.
# Hacked up from mini_harness.plx in the Test::Harness dist.
# (And some stuff stolen from prove, too.)

# Please improve me!
#
# TODO:
# 1. Modularize this.
# 2. Get to work concurrently with 'make test'
# 3. 'make smoke' make target that uploads the results of this
#    to a server somewhere.
