The Perl Toolchain Summit needs more sponsors. If your company depends on Perl, please support this very important event.
use strict;
use warnings;

use lib 't/lib';

use IPC::Run3 qw(run3);
use List::Util qw(max);
use Path::Class;
use Path::Class::Rule;
use TAP::Formatter::Session::TeamCity;

use Test::More 0.98;

my %todo = (
    'test-name-matches-file' => 1,
);

my @tests
    = @ARGV
    ? map { m{^t/} ? $_ : "t/test-data/basic/$_" } @ARGV
    : <t/test-data/basic/*>;
test_formatter($_) for @tests;

unless (@ARGV) {
    test_formatter('t/test-data/basic');
    test_formatter( 't/test-data/basic', q{ in parallel} );
}

done_testing;

sub test_formatter {
    my $test_dir    = shift;
    my $is_parallel = shift;

    local $TODO = 'This is a known bug'
        if $todo{ dir($test_dir)->basename };

    subtest(
        $test_dir . ( $is_parallel // q{} ),
        sub {

            my @t_files
                = Path::Class::Rule->new->file->name(qr/\.st/)
                ->all($test_dir);

            @t_files = grep { !$todo{ $_->dir->basename } } @t_files
                if @t_files > 1;

            my @prove
                = qw( prove --lib --merge --verbose --formatter TAP::Formatter::TeamCity );
            push @prove, qw( -j 2 ) if $is_parallel;

            my ( @stdout, $stderr );
            run3(
                [ @prove, @t_files ],
                \undef,
                \@stdout,
                \$stderr,
            );

            if ($stderr) {
                fail('got unexpected stderr');
                diag($stderr);
            }

            # we don't want to compare the test summary, but it has a different number
            # of lines depending on $is_ok so we just stop collecting lines once we
            # hit something that looks like that summary.
            my @output;
            for my $l (@stdout) {
                last
                    if $l =~ /Parse errors:/
                    || $l =~ /^Files=\d+/
                    || $l =~ /^Test Summary Report/
                    || $l =~ /^All tests successful\./;

                push @output, _remove_timestamp($l);
            }

            my $actual = join q{}, @output;

            if ($is_parallel) {
                _test_parallel_output( \@t_files, $actual );
            }
            else {
                _test_sequential_output( \@t_files, $actual );
            }
        }
    );
}

sub _remove_timestamp {
    my $line = shift;

    # We only touch TC message lines that include name/value pairs
    return $line unless $line =~ /^\Q##teamcity[\E[^ ]+ [^=]+='[^']*'/;

    my $ok = ok(
        $line =~ s/ timestamp='([^']+)'//,
        'teamcity directive line has a timestamp'
    ) or diag($line);

    if ($ok) {
        my $ts = $1;
        like(
            $ts,
            qr/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}$/,
            'timestamp matches expected format'
        );
    }

    return $line;
}

sub _test_parallel_output {
    my $t_files = shift;
    my $actual  = shift;

    # Lines of the form '# ...' are stderr output from the tests. With a
    # parallel run, the order these come in is not entirely predictable so we
    # filter them out. As long as we got the TeamCity build messages we
    # expected, we should be good.
    $actual =~ s/^ \#\ .+ $ \n//xmg;

    for my $t_file ( @{$t_files} ) {
        my $expected_lines_re = join "\n",
            map  { qr/ ^ \Q$_\E $ \n /xm }
            grep { !/ ^ #\ .+ /xm }
            ## no critic (BuiltinFunctions::ProhibitComplexMappings)
            map { chomp; $_ } $t_file->dir->file('expected.txt')->slurp;

        like(
            $actual,
            qr{
                  ^ \#\#\Qteamcity[progressMessage 'starting $t_file']\E $ \n
                  (?: ^ .+ $ \n)*
                  $expected_lines_re
            }xm,
            "output from $t_file"
        );
    }
}

sub _test_sequential_output {
    my $t_files = shift;
    my $actual  = shift;

    my $expected = join q{},
        map { scalar $_->dir->file('expected.txt')->slurp } @{$t_files};

    $_ =~ s{\n+$}{\n} for $actual, $expected;

    _clean_file_references( \$actual, \$expected );
    _clean_module_load_errors( \$expected );

    # This splits on lines without stripping out the newline.
    my @actual   = split /(?<=\n)/, $actual;
    my @expected = split /(?<=\n)/, $expected;

    is(
        scalar @actual, scalar @expected,
        'actual and expected output have the same number of lines'
    );

    for my $i ( 0 .. ( max( $#actual, $#expected ) ) ) {
        if ( defined $actual[$i] && defined $expected[$i] ) {
            is(
                $actual[$i], $expected[$i],
                "actual output vs expected line $i"
            );
        }
        elsif ( !defined $actual[$i] ) {
            ok( 0, "no line $i in actual output but one was expected" );
            diag( $expected[$i] );
        }
        else {
            ok(
                0,
                "got a line $i in actual output but none was expected"
            );
            diag( $actual[$i] );
        }
    }
}

sub _clean_file_references {

    # These hacks exist to replace user-specific paths with some sort of fixed
    # test. Long term, it'd be better to test the formatter by feeding it TAP
    # output directly rather than running various test files with the
    # formatter in place.
    for my $output (@_) {
        ${$output}
            =~ s{(#\s+at ).+/Moose([^\s]+) line \d+}{${1}CODE line XXX}g;
        ${$output} =~ s{\(\@INC contains: .+?\)}{(\@INC contains: XXX)}sg;
    }
}

sub _clean_module_load_errors {
    my $expected = shift;

    # The error message for attempting to load a module that doesn't exist was
    # changed in 5.18.0.
    ${$expected}
        =~ s{\Q(you may need to install the SomeNoneExistingModule module) }{}g
        if $] < 5.018;
}