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

use Pod::Parser;
use warnings;
use strict;
use English qw( -no_match_vars );
use Fatal qw(close);
use Carp;
use Getopt::Long qw(GetOptions);
use Test::More;

my $warnings = 0;
my $options_result = GetOptions( 'warnings' => \$warnings );
Carp::croak("$PROGRAM_NAME options parsing failed")
    unless $options_result;

package Marpa::Test::Display;

@Marpa::Test::Display::ISA       = qw(Exporter);
@Marpa::Test::Display::EXPORT_OK = qw(test_file);

use Text::Diff;
use Carp;
use Fatal qw(close);
use English qw( -no_match_vars );

our $FILE_ERROR = 'No error';

our $PREAMBLE   = q{1};
our $IN_COMMAND = 0;
our @DISPLAY;
our $DEFAULT_CODE             = q{ no_code_defined($_) };
our $CURRENT_CODE             = $DEFAULT_CODE;
our $COLLECTING_FROM_LINE_NUM = -1;
our $COLLECTED_DISPLAY;
our $COMMAND_COUNTDOWN = 0;
our $CURRENT_FILE      = '!!! NO CURRENT FILE !!!';
our $DISPLAY_SKIP      = 0;

sub no_code_defined {
    my $display = shift;
    return 'No code defined to test display:';
}

my %raw                     = ();
my %normalized              = ();
my %raw_display             = ();
my %normalized_display      = ();
my %normalized_display_uses = ();

sub normalize_whitespace {
    my $raw_ref = shift;
    my $text    = ${$raw_ref};
    $text =~ s/\A\s*//xms;
    $text =~ s/\s*\z//xms;
    $text =~ s/\s+/ /gxms;
    return \$text;
}

sub slurp {
    my ($file_name) = @_;
    my $open_result = open my $fh, '<', $file_name;
    if ( not $open_result ) {
        $Marpa::Test::Display::FILE_ERROR = "Cannot open $file_name: $ERRNO";
        return;
    }
    local ($RS) = undef;
    my $result = \<$fh>;
    close $fh;
    return $result;
}

sub parse_displays {
    my $raw_ref = shift;

    my $result = {};
    my @matches = ${$raw_ref} =~ m{
               ^ [ \t]* [#] \h* [#] [\h#]* use [ \t]+ Marpa[:][:]Test[:][:]Display \h+ (\w+(?:\s+\w+)*) \s* \h* $
               (.*?)
               ^ [ \t]* [#] \h* [#] [\h#]* no [ \t]+ Marpa[:][:]Test[:][:]Display \h* $
           }xmsg;
    while (@matches) {
        my $display_name = shift @matches;
        my $display_text = shift @matches;
        $result->{$display_name} = \$display_text;
    }

    return $result;
}

sub read_file {
    my $file_name    = shift;
    my $display_name = shift;

    my $file_ref = $normalized{$file_name};
    if ( not defined $file_ref ) {
        my $raw_ref = $raw{$file_name} = slurp($file_name);
        return if not defined $raw_ref;
        $file_ref = $normalized{$file_name} = normalize_whitespace($raw_ref);
        my $raw_display = $raw_display{$file_name} = parse_displays($raw_ref);
        for my $raw_display_name ( keys %{$raw_display} ) {
            $normalized_display{$file_name}{$raw_display_name} =
                normalize_whitespace( $raw_display->{$raw_display_name} );
        }
    }
    return $file_ref
        if not defined $display_name;
    my $display_ref = $normalized_display{$file_name}{$display_name};
    if ( not defined $display_ref ) {
        Carp::croak("No display named '$display_name' in file: $file_name");
    }
    $normalized_display_uses{$file_name}{$display_name}++;
    return $display_ref;
}

sub in_file {
    my ( $pod_display, $file_name, $display_name ) = @_;

    my $pod_display_ref = normalize_whitespace( \$pod_display );
    my $file_display_ref = read_file( $file_name, $display_name );
    if ( not defined $file_display_ref ) {
        return ( "$Marpa::Test::Display::FILE_ERROR\n", 1 );
    }

    my $location = index ${$file_display_ref}, ${$pod_display_ref};

    return (
        (   $location >= 0
            ? q{}
            : "Display in $Marpa::Test::Display::CURRENT_FILE not in $file_name\n"
                . $pod_display
        ),
        1
    );

}

sub is_file {
    my ( $pod_display, $file_name, $display_name ) = @_;

    my $pod_display_ref = normalize_whitespace( \$pod_display );
    my $file_display_ref = read_file( $file_name, $display_name );
    if ( not defined $file_display_ref ) {
        return ( "$Marpa::Test::Display::FILE_ERROR\n", 1 );
    }

    return q{} if ${$file_display_ref} eq ${$pod_display_ref};

    my $raw_file_display =
        defined $display_name
        ? $raw_display{$file_name}{$display_name}
        : $raw{$file_name};

    $pod_display =~ s/^\h*//gxms;
    ${$raw_file_display} =~ s/^\h*//gxms;

    my $header =
        $display_name
        ? "Display '$display_name'"
        : 'Display';
    $header
        .= " in $Marpa::Test::Display::CURRENT_FILE differs from the one in $file_name";

    return (
        (   $header
                . (
                Text::Diff::diff \$pod_display,
                $raw_file_display,
                { STYLE => 'Table' }
                )
        ),
        1
    );

}

sub test_file {
    my $file = shift;

    $Marpa::Test::Display::CURRENT_FILE      = $file;
    @Marpa::Test::Display::DISPLAY           = ();
    $Marpa::Test::Display::DEFAULT_CODE      = q{ no_code_defined($_) };
    $Marpa::Test::Display::CURRENT_CODE      = $DEFAULT_CODE;
    $Marpa::Test::Display::COMMAND_COUNTDOWN = 0;
    $Marpa::Test::Display::DISPLAY_SKIP      = 0;
    my $mismatch_count = 0;
    my $mismatches     = q{};

    my $parser = MyParser->new();
    $parser->parse_from_file($file);
    ## no critic (BuiltinFunctions::ProhibitStringyEval)
    my $eval_result = eval $PREAMBLE;
    ## use critic
    Carp::croak($EVAL_ERROR) unless $eval_result;

    for my $display_test (@Marpa::Test::Display::DISPLAY) {
        my ( $display, $code, $display_file, $display_line ) =
            @{$display_test}{qw(display code file line)};
        local $_ = $display;
        ## no critic (BuiltinFunctions::ProhibitStringyEval)
        $eval_result = eval '[ do {' . $code . '} ] ';
        ## use critic

        if (my $message =
              $eval_result
            ? $eval_result->[0]
            : $EVAL_ERROR . "Code with problem was:\n$code\n"
            )
        {
            my $do_not_add_display = $eval_result->[1];
            unless ($do_not_add_display) {
                $message .= "\n$display";
            }
            $mismatches .= "=== $message";
            $mismatch_count++;
        }

    }    # $display_test

    return ( $mismatch_count, \$mismatches );

}    # sub test_file

package MyParser;
@MyParser::ISA = qw(Pod::Parser);
use Carp;

sub queue_display {
    my $display  = shift;
    my $line_num = shift;
    push @Marpa::Test::Display::DISPLAY,
        {
        'display' => $display,
        'code'    => $Marpa::Test::Display::CURRENT_CODE,
        'file'    => $Marpa::Test::Display::CURRENT_FILE,
        'line'    => $line_num,
        }
        if not $Marpa::Test::Display::DISPLAY_SKIP;
    $Marpa::Test::Display::COMMAND_COUNTDOWN--;
    if ( $Marpa::Test::Display::COMMAND_COUNTDOWN <= 0 ) {
        $Marpa::Test::Display::CURRENT_CODE =
            $Marpa::Test::Display::DEFAULT_CODE;
        $Marpa::Test::Display::DISPLAY_SKIP = 0;
    }
    return;
}

sub verbatim {
    my ( $parser, $paragraph, $line_num ) = @_;

    if ( defined $Marpa::Test::Display::COLLECTED_DISPLAY ) {
        $Marpa::Test::Display::COLLECTED_DISPLAY .= $paragraph;
        $Marpa::Test::Display::COLLECTING_FROM_LINE_NUM //= $line_num;
        return;
    }
    queue_display( $paragraph, $line_num );
    return;
}

sub process_instruction {
    my $instruction = shift;
    my $code        = shift;
    my $line_num    = shift;

    $instruction =~ s/\s\z//xms;    # eliminate trailing whitespace
    $instruction =~ s/\s/ /gxms;    # normalize whitespace

    if ( $instruction =~ /^ next \s+ display $ /xms ) {
        $Marpa::Test::Display::COMMAND_COUNTDOWN = 1;
        $Marpa::Test::Display::CURRENT_CODE = join "\n", @{$code};
        return;
    }

    if ( $instruction =~ / ^ next \s+ (\d+) \s+ display(s)? $ /xms ) {
        $Marpa::Test::Display::COMMAND_COUNTDOWN = $1;
        Carp::croak(
            "File: $Marpa::Test::Display::CURRENT_FILE  Line: $line_num\n",
            "  'next $Marpa::Test::Display::COMMAND_COUNTDOWN display' has countdown less than one\n"
        ) if $Marpa::Test::Display::COMMAND_COUNTDOWN < 1;
        $Marpa::Test::Display::CURRENT_CODE = join "\n", @{$code};
        return;
    }

    if ( $instruction =~ / ^ default $ /xms ) {
        $Marpa::Test::Display::DEFAULT_CODE = join "\n", @{$code};
        $Marpa::Test::Display::CURRENT_CODE =
            $Marpa::Test::Display::DEFAULT_CODE
            if $Marpa::Test::Display::COMMAND_COUNTDOWN <= 0;
        return;
    }

    if ( $instruction =~ / ^ preamble $ /xms ) {
        $Marpa::Test::Display::PREAMBLE .= join "\n", @{$code};
        return;
    }

    if ( $instruction =~ / ^ skip \s+ display $ /xms ) {
        $Marpa::Test::Display::COMMAND_COUNTDOWN = 1;
        $Marpa::Test::Display::DISPLAY_SKIP++;
        return;
    }

    if ( $instruction =~ / ^ skip \s+ (\d+) \s+ display(s)? $ /xms ) {
        $Marpa::Test::Display::COMMAND_COUNTDOWN = $1;
        Carp::croak(
            "File: $Marpa::Test::Display::CURRENT_FILE  Line: $line_num\n",
            "  'display $Marpa::Test::Display::COMMAND_COUNTDOWN skip' has countdown less than one\n"
        ) if $Marpa::Test::Display::COMMAND_COUNTDOWN < 1;
        $Marpa::Test::Display::DISPLAY_SKIP++;
        return;
    }

    if ( $instruction =~ /^ start \s+ display $/xms ) {
        $Marpa::Test::Display::COLLECTED_DISPLAY = q{};
        return;
    }

    if ( $instruction =~ / ^ end \s+ display $ /xms ) {

        # line num will be set when first part of display is found
        queue_display(
            $Marpa::Test::Display::COLLECTED_DISPLAY,
            $Marpa::Test::Display::COLLECTING_FROM_LINE_NUM
        );
        $Marpa::Test::Display::COLLECTED_DISPLAY        = undef;
        $Marpa::Test::Display::COLLECTING_FROM_LINE_NUM = -1;
        return;
    }

    Carp::croak(
        "Unrecognized instruction in file $Marpa::Test::Display::CURRENT_FILE at line $line_num: $instruction\n"
    );

}

sub textblock {
    my ( $parser, $paragraph, $line_num ) = @_;
    return unless $Marpa::Test::Display::IN_COMMAND;

    ## Translate/Format this block of text; sample actions might be:

    my @lines = split /\n/xms, $paragraph;
    my $found_instruction = 0;
    LINE: while ( my $line = shift @lines ) {
        next LINE if $line =~ /^\s*$/xms;    # skip whitespace
        if ( $line =~ /\A[#][#]/xms ) {
            $line =~ s/\A[#][#]\s*//xms;
            process_instruction( $line, \@lines, $line_num );
            $found_instruction = 1;
            next LINE;
        }
        Carp::croak(
            "File: $Marpa::Test::Display::CURRENT_FILE  Line: $line_num\n",
            "test block doesn't begin with ## instruction\n$paragraph"
        ) if not $found_instruction;
        last LINE;
    }

    return;

}

sub interior_sequence { }

sub command {

    my ( $parser, $command, $paragraph ) = @_;
    if ( $command eq 'begin' ) {
        $Marpa::Test::Display::IN_COMMAND++ if $paragraph =~ m{
                \A
                Marpa[:][:]Test[:][:]Display[:]
                \s* \Z
            }xms;
        $Marpa::Test::Display::IN_COMMAND++ if $paragraph =~ /\Amake:$/xms;
    }
    elsif ( $command eq 'end' ) {
        $Marpa::Test::Display::IN_COMMAND = 0;
    }

    return;

}

package main;

my %exclude = map { ( $_, 1 ) } qw(
    Makefile.PL
);

my @test_files = ();
open my $manifest, '<', 'MANIFEST'
    or Carp::croak("Cannot open MANIFEST: $ERRNO");
FILE: while ( my $file = <$manifest> ) {
    chomp $file;
    $file =~ s/\s*[#].*\z//xms;
    next FILE if $exclude{$file};
    next FILE if -d $file;
    my ($ext) = $file =~ / [.] ([^.]+) \z /xms;
    next FILE unless defined $ext;
    $ext = lc $ext;
    next FILE
        if $ext ne 'pod'
            and $ext ne 'pl'
            and $ext ne 'pm'
            and $ext ne 't';

    push @test_files, $file;
}    # FILE
close $manifest;

Test::More::plan tests => 1 + scalar @test_files;

open my $error_file, '>', 'author.t/display.errs'
    or Carp::croak("Cannot open display.errs: $ERRNO");
FILE: for my $file (@test_files) {
    if ( not -f $file ) {
        Test::More::fail("attempt to test displays in non-file: $file");
        next FILE;
    }

    my ( $mismatch_count, $mismatches ) =
        Marpa::Test::Display::test_file($file);
    my $clean = $mismatch_count == 0;

    my $message =
        $clean
        ? "displays match for $file"
        : "displays in $file has $mismatch_count mismatches";

    Test::More::ok( $clean, $message );
    next FILE if $clean;
    print {$error_file} "=== $file ===\n" . ${$mismatches}
        or Carp::croak("print failed: $ERRNO");
}

my $unused       = q{};
my $unused_count = 0;
while ( my ( $file_name, $displays ) = each %normalized_display_uses ) {
    DISPLAY: while ( my ( $display_name, $uses ) = each %{$displays} ) {
        next DISPLAY if $uses > 0;
        $unused .= "display '$display_name' in $file_name never used\n";
        $unused_count++;
    }
}
if ($unused_count) {
    Test::More::fail('$unused count displays not used');
    print {$error_file} "=== UNUSED DISPLAYS ===\n" . $unused
        or Carp::croak("print failed: $ERRNO");
}
else {
    Test::More::pass('all displays used');
}
close $error_file;