#!/usr/bin/perl

# Load ALL of the PPI files, and look for a collection
# of known problems, implemented using PPI itself.

# Using PPI to analyse its own code at install-time? Fuck yeah! :)

use lib 't/lib';
use PPI::Test::pragmas;
use Test::More; # Plan comes later

use Test::Object;
use File::Spec::Functions ':ALL';
use Params::Util qw{_CLASS _ARRAY _INSTANCE _IDENTIFIER};
use Class::Inspector 1.22;
use PPI;
use PPI::Test 'find_files';
use PPI::Test::Object;

use constant CI => 'Class::Inspector';





#####################################################################
# Prepare

# Find all of the files to be checked
my %tests = map { $_ => $INC{$_} } grep { ! /\bXS\.pm/ } grep { /^PPI\b/ } keys %INC;
unless ( %tests ) {
	Test::More::plan( tests => 1 + ($ENV{AUTHOR_TESTING} ? 1 : 0) );
	ok( undef, "Failed to find any files to test" );
	exit();
}
my @files = sort values %tests;

# Find all the testable perl files in t/data
foreach my $dir ( '05_lexer', '08_regression', '11_util', '13_data', '15_transform' ) {
	my @perl = find_files( catdir('t', 'data', $dir) );
	push @files, @perl;
}

# Declare our plan
Test::More::plan( tests => scalar(@files) * 14 + 3 + ($ENV{AUTHOR_TESTING} ? 1 : 0) );





#####################################################################
# Self-test the search functions before we use them

# Check this actually finds something bad
my $sample = PPI::Document->new(\<<'END_PERL');
isa($foo, 'Bad::Class1');
isa($foo, 'PPI::Document');
$foo->isa('Bad::Class2');
$foo->isa("Bad::Class3");
isa($foo, 'ARRAY'); # Not bad
isa($foo->thing, qq <Bad::Class4> # ok?
);
END_PERL
isa_ok( $sample, 'PPI::Document' );

my $bad = $sample->find( \&bug_bad_isa_class_name );
ok( _ARRAY($bad), 'Found bad things' );
@$bad = map { $_->string } @$bad;
is_deeply( $bad, [ 'Bad::Class1', 'Bad::Class2', 'Bad::Class3', 'Bad::Class4' ],
	'Found all found known bad things' );





#####################################################################
# Run the Tests

foreach my $file ( @files ) {
	# MD5 the raw file
	my $md5a = PPI::Util::md5hex_file($file);
	like( $md5a, qr/^[[:xdigit:]]{32}\z/, 'md5hex_file ok' );

	# Load the file
	my $Document = PPI::Document->new($file);
	ok( _INSTANCE($Document, 'PPI::Document'), "$file: Parsed ok" );

	# Compare the preload signature to the post-load value
	my $md5b = $Document->hex_id;
	is( $md5b, $md5a, '->hex_id matches md5hex' );

	# By this point, everything should have parsed properly at least
	# once, so no need to skip.
	SCOPE: {
		my $rv = $Document->find( \&bug_bad_isa_class_name );
		if ( $rv ) {
			$Document->index_locations;
			foreach ( @$rv ) {
				print "# $file: Found bad class "
					. $_->content
					. "\n";
			}
		}
		is_deeply( $rv, '', "$file: All class names in ->isa calls exist" );
	}
	SCOPE: {
		my $rv = $Document->find( \&bad_static_method );
		if ( $rv ) {
			$Document->index_locations;
			foreach ( @$rv ) {
				my $c = $_->sprevious_sibling->content;
				my $m = $_->snext_sibling->content;
				my $l = $_->location;
				print "# $file: Found bad call ${c}->${m} at line $l->[0], col $l->[1]\n";
			}
		}
		is_deeply( $rv, '', "$file: All class names in static method calls" );
	}

	# Test with Test::Object stuff
	object_ok( $Document );
}





#####################################################################
# Test Functions

# Check for accidental use of illegal or non-existant classes in
# ->isa calls. This has happened at least once, presumably because
# PPI has a LOT of classes and it can get confusing.
sub bug_bad_isa_class_name {
	my ($Document, $Element) = @_;

	# Find a quote containing a class name
	$Element->isa('PPI::Token::Quote')             or return '';
	_CLASS($Element->string)                       or return '';
	if ( $Element->string =~ /^(?:ARRAY|HASH|CODE|SCALAR|REF|GLOB)$/ ) {
		return '';
	}

	# It should be the last thing in an expression in a list
	my $Expression = $Element->parent              or return '';
	$Expression->isa('PPI::Statement::Expression') or return '';
	$Element == $Expression->schild(-1)            or return '';

	my $List = $Expression->parent                 or return '';
	$List->isa('PPI::Structure::List')             or return '';
	$List->schildren == 1                          or return '';

	# The list should be the params list for an isa call
	my $Word = $List->sprevious_sibling            or return '';
	$Word->isa('PPI::Token::Word')                 or return '';
	$Word->content =~ /^(?:UNIVERSAL::)?isa\z/s    or return '';

	# Is the class real and loaded?
	CI->loaded($Element->string)                  and return '';

	# Looks like we found a class that doesn't exist in
	# an isa call.
	return 1;
}

# Check for the use of a method that doesn't exist
sub bad_static_method {
	my ($document, $element) = @_;

	# Find a quote containing a class name
	$element->isa('PPI::Token::Operator')   or return '';
	$element->content eq '->'               or return '';

	# Check the method
	my $method = $element->snext_sibling    or return '';
	$method->isa('PPI::Token::Word')        or return '';
	_IDENTIFIER($method->content)           or return '';

	# Check the class
	my $class = $element->sprevious_sibling or return '';
	$class->isa('PPI::Token::Word')         or return '';
	_CLASS($class->content)                 or return '';

	# It's usually a deep class
	$class  = $class->content;
	$method = $method->content;
	$class =~ /::/                          or return '';

	# Check the method exists
	$class->can($method)                   and return '';

	return 1;
}

1;
