#!perl

use 5.006;
use strict;
use warnings;

our $VERSION = '0.007';

package App::ReportPrereqs;

use CPAN::Meta;
use ExtUtils::MakeMaker ();
use File::Basename qw(fileparse);
use Getopt::Long qw(GetOptions);
use HTTP::Tiny 0.014 ();
use List::Util qw(max);
use Module::CPANfile ();
use Module::Path qw(module_path);
use version 0.77 ();

if ( !caller ) {
    my $rc = _main();
    exit 0 if !defined $rc;
    exit 2 if $rc == 2;
    exit 1;
}

sub _main {
    my $cpanfile;
    my $meta;
    my $with_develop = 0;
    my @features;
    my $getoptions_ok = GetOptions(
        'cpanfile:s'      => \$cpanfile,
        'meta:s'          => \$meta,
        'with-develop'    => \$with_develop,
        'with-feature=s@' => \@features,
    );

    my $url = shift @ARGV;

    if (
        # Wrong options used
        !$getoptions_ok

        # --cpanfile and --meta cannot be used together
        || ( defined $cpanfile && defined $meta )

        # --cpanfile or --meta cannot be used together with a URL
        || ( ( defined $cpanfile || defined $meta ) && defined $url )

        # Only at most one URL can be specified
        || (@ARGV)
      )
    {
        _usage();
        return 2;
    }

    my $prereqs;
    my $source;
    if ( defined $meta ) {
        if ( $meta eq q{} ) {
            $meta = 'META.json';
        }
        $source = $meta;

        local $@;    ## no critic (Variables::RequireInitializationForLocalVars)
        if ( !eval { $prereqs = CPAN::Meta->load_file($meta)->effective_prereqs( \@features ); 1 } ) {
            my $error = $@;
            print {*STDERR} "Cannot read meta file '$meta': $error\n";
            return 1;
        }
    }
    else {
        if ( defined $url ) {
            $source = $url;

            my $res = HTTP::Tiny->new->get($source);
            if ( !$res->{success} ) {
                print {*STDERR} $res->{content};
                return 1;
            }

            $cpanfile = \$res->{content};
        }
        else {
            if ( !defined $cpanfile || $cpanfile eq q{} ) {
                $cpanfile = 'cpanfile';
            }
            $source = $cpanfile;
        }

        local $@;    ## no critic (Variables::RequireInitializationForLocalVars)
        if ( !eval { $prereqs = Module::CPANfile->load($cpanfile)->prereqs_with(@features); 1; } ) {
            my $error = $@;
            print {*STDERR} "Cannot read cpanfile file '$cpanfile': $error\n";
            return 1;
        }
    }

    # ---
    my @full_reports;
    my @dep_errors;

  PHASE:
    for my $phase (qw(configure build test runtime develop)) {
        next PHASE if ( $phase eq 'develop' ) and ( !$with_develop );

      TYPE:
        for my $type (qw(requires recommends suggests conflicts)) {
            my $req_ref = $prereqs->requirements_for( $phase, $type )->as_string_hash;
            my @modules = grep { $_ ne 'perl' } keys %{$req_ref};
            next TYPE if !@modules;

            my $title   = "\u$phase \u$type";
            my @reports = ( [qw(Module Want Have)] );

          MODULE:
            for my $module ( sort @modules ) {
                my $want = $req_ref->{$module};
                if ( !defined $want ) {
                    $want = 'undef';
                }
                elsif ( $want eq '0' ) {
                    $want = 'any';
                }

                my $req_string = $want eq 'any' ? 'any version required' : "version '$want' required";

                my $mod_path = module_path($module);

                if ( defined $mod_path ) {
                    my $have = MM->parse_version($mod_path);    ## no critic (Modules::RequireExplicitInclusion)

                    # This validation was added in EUMM 7.47_01 in ExtUtils::MM_Unix
                    # We use the same validation to make the file testable - otherwise the
                    # result depends on the version of EUMM used.
                    if (   ( !defined $have )
                        or ( $have !~ m{ ^ v? [0-9_\.\-]+ $ }xsm )
                        or ( !eval { version->parse($have) } ) )
                    {
                        $have = 'undef';
                    }

                    push @reports, [ $module, $want, $have ];

                    next MODULE if $type ne 'requires';

                    if ( $have eq 'undef' ) {
                        push @dep_errors, "$module version unknown ($req_string)";
                        next MODULE;
                    }

                    if ( !$prereqs->requirements_for( $phase, $type )->accepts_module( $module => $have ) ) {
                        push @dep_errors, "$module version '$have' is not in required range '$want'";
                        next MODULE;
                    }

                    next MODULE;
                }

                push @reports, [ $module, $want, 'missing' ];

                next MODULE if $type ne 'requires';

                push @dep_errors, "$module is not installed ($req_string)";
            }

            push @full_reports, "=== $title ===\n\n";

            my $ml = max( map { length $_->[0] } @reports );
            my $wl = max( map { length $_->[1] } @reports );
            my $hl = max( map { length $_->[2] } @reports );

            splice @reports, 1, 0, [ q{-} x $ml, q{-} x $wl, q{-} x $hl ];
            push @full_reports, map { sprintf "    %*s %*s %*s\n", -$ml, $_->[0], $wl, $_->[1], $hl, $_->[2] } @reports;

            push @full_reports, "\n";
        }
    }

    if (@full_reports) {
        print "Versions for all modules listed in $source:\n\n", @full_reports;
    }

    if (@dep_errors) {
        print "\n*** WARNING WARNING WARNING WARNING WARNING WARNING WARNING WARNING ***\n\n";
        print "The following REQUIRED prerequisites were not satisfied:\n\n";

        for my $error (@dep_errors) {
            print $error, "\n";
        }
    }

    return;
}

sub _usage {
    my $basename = fileparse($0);

    print {*STDERR} "usage: $basename [--with-develop] [--with-feature <feature>] [URL]\n";
    print {*STDERR} "       $basename [--with-develop] [--with-feature <feature>] [--cpanfile [<cpanfile>]]\n";
    print {*STDERR} "       $basename [--with-develop] [--with-feature <feature>] [--meta [<META.json>|<META.yml>]]\n";

    return;
}

1;

__END__

=pod

=encoding UTF-8

=head1 NAME

report-prereqs - report prerequisite versions

=head1 VERSION

Version 0.007

=head1 SYNOPSIS

=over

=item B<report-prereqs> [B<--with-{develop,feature=id}>] [URL]

=back

=head1 DESCRIPTION

The C<report-prereqs> utility will examine a F<cpanfile>, F<META.json>, or
F<META.yml> file for prerequisites with L<Module::CPANfile>
respectively L<CPAN::Meta>. It reports the version of all modules
listed as prerequisites (including 'recommends', 'suggests', etc.). However,
any 'develop' prerequisites are not reported, unless they show up in another
category or the C<--with-develop> option is used.

Option C<--with-feature> enables optional features provided by a CPAN
distribution. Option may be used more than once.

Versions are reported based on the result of C<parse_version> from
L<ExtUtils::MakeMaker>, which means prerequisite modules
are not actually loaded. Parse errors are reported as "undef". If a module is
not installed, "missing" is reported instead of a version string.

Additionally, unfulfilled required prerequisites are reported after the list
of all versions.

=head1 OPTIONS

=over

=item B<--cpanfile [ FILENAME ]>

Parse the C<filename> with L<Module::CPANfile> instead of the default behavior.
If the C<filename> is omitted it defaults to F<cpanfile> - which is also the
default.

Can not be used together with C<--meta> or a url.

=item B<--meta [ FILENAME ]>

Parse the C<filename> with L<CPAN::Meta> instead of the default behavior. If
the C<filename> is omitted it defaults to F<META.json>.

Can not be used together with C<--cpanfile> or a url.

=item B<--with-develop>

Also report develop prerequisites.

=item B<--with-feature> String: Name

Specify optional feature to enable. Option may be used more than once.

=back

=head1 EXIT STATUS

The report-prereqs utility exits 0 on success, 1 if an error occurs, and 2 if
invalid command line options were specified.

The state of the prerequisites does have no effect on the exist status.

=head1 EXAMPLES

=head2 Example 1 Using this on Travis CI with C<AUTHOR_TESTING>

Add the following lines to F<.travis.yml> right before your tests are run,
after all your dependencies are installed.

    - cpanm --verbose --notest --skip-satisfied App::ReportPrereqs
    - report-prereqs --with-develop

=head2 Example 2 Using this on Travis CI without C<AUTHOR_TESTING>

Add the following lines to F<.travis.yml> right before your tests are run,
after all your dependencies are installed.

    - cpanm --verbose --notest --skip-satisfied App::ReportPrereqs
    - report-prereqs

=head2 Example 3 Using this on AppVeyor with C<AUTHOR_TESTING>

Add the following lines to F<.appveyor.yml> right before your tests are run,
after all your dependencies are installed.

    - perl -S cpanm --verbose --notest --skip-satisfied App::ReportPrereqs
    - perl -S report-prereqs --with-develop

=head2 Example 4 Using this on AppVeyor without C<AUTHOR_TESTING>

Add the following lines to F<.appveyor.yml> right before your tests are run,
after all your dependencies are installed.

    - perl -S cpanm --verbose --notest --skip-satisfied App::ReportPrereqs
    - perl -S report-prereqs

=head2 Example 5 Show prerequistes from a cpanfile url

    report-prereqs https://raw.githubusercontent.com/skirmess/App-ReportPrereqs/master/cpanfile

=head2 Example 6 Show prerequisites from a MYMETA.json

    report-prereqs --meta MYMETA.json

=head1 RATIONALE

=head2 Why this instead of L<Dist::Zilla::Plugin::Test::ReportPrereqs|Dist::Zilla::Plugin::Test::ReportPrereqs>

The L<Test::ReportPrereqs|Dist::Zilla::Plugin::Test::ReportPrereqs>
L<Dist::Zilla|Dist::Zilla> plugin adds a test to your distribution that
prints all the prerequisites version during testing of your distribution.

The goal of this module is to produce the same output on Travis CI and
AppVeyor without including an additional file to the distribution.

=head1 SEE ALSO

L<Dist::Zilla::Plugin::Test::ReportPrereqs|Dist::Zilla::Plugin::Test::ReportPrereqs>

=head1 SUPPORT

=head2 Bugs / Feature Requests

Please report any bugs or feature requests through the issue tracker
at L<https://github.com/skirmess/App-ReportPrereqs/issues>.
You will be notified automatically of any progress on your issue.

=head2 Source Code

This is open source software. The code repository is available for
public review and contribution under the terms of the license.

L<https://github.com/skirmess/App-ReportPrereqs>

  git clone https://github.com/skirmess/App-ReportPrereqs.git

=head1 AUTHOR

Sven Kirmess <sven.kirmess@kzone.ch>

=head1 CONTRIBUTORS

=over

=item *

Stephan Sachse <ste.sachse@gmail.com>

=back

=head1 COPYRIGHT AND LICENSE

This software is Copyright (c) 2018-2022 by Sven Kirmess.

This is free software, licensed under:

  The (two-clause) FreeBSD License

=cut

# vim: ts=4 sts=4 sw=4 et: syntax=perl
