#!/usr/bin/perl -w

use strict;
use warnings FATAL => 'all';
use Test::More tests => 125;

use Config;
use File::Temp ();

use File::Spec::Functions qw/catdir splitdir splitpath tmpdir/;
my $tmp = File::Temp::tempdir('EIP-XXXXXXXX', CLEANUP => 1, DIR => File::Spec->tmpdir);

use ExtUtils::Config;
use ExtUtils::InstallPaths;

#########################

# We need to create a well defined environment to test install paths.
# We do this by setting up appropriate Config entries.

my @installstyle = qw(lib perl5);
my $config = ExtUtils::Config->new({
	installstyle	=> catdir(@installstyle),

	installprivlib  => catdir($tmp, @installstyle),
	installarchlib  => catdir($tmp, @installstyle, @Config{qw(version archname)}),
	installbin      => catdir($tmp, 'bin'),
	installscript   => catdir($tmp, 'bin'),
	installman1dir  => catdir($tmp, 'man', 'man1'),
	installman3dir  => catdir($tmp, 'man', 'man3'),
	installhtml1dir => catdir($tmp, 'html'),
	installhtml3dir => catdir($tmp, 'html'),

	installsitelib      => catdir($tmp, 'site', @installstyle, 'site_perl'),
	installsitearch     => catdir($tmp, 'site', @installstyle, 'site_perl', @Config{qw(version archname)}),
	installsitebin      => catdir($tmp, 'site', 'bin'),
	installsitescript   => catdir($tmp, 'site', 'bin'),
	installsiteman1dir  => catdir($tmp, 'site', 'man', 'man1'),
	installsiteman3dir  => catdir($tmp, 'site', 'man', 'man3'),
	installsitehtml1dir => catdir($tmp, 'site', 'html'),
	installsitehtml3dir => catdir($tmp, 'site', 'html'),
});

my $mb = ExtUtils::InstallPaths->new(installdirs => 'site', config => $config, dist_name => 'ExtUtils-InstallPaths');
isa_ok($mb, 'ExtUtils::InstallPaths');

# Get us into a known state.
$mb->install_base(undef);
$mb->prefix(undef);

{
	# Check install_path() accessor
	my ($map, $path);

	$map = $mb->install_path();
	is_deeply($map, {}, 'install_path() accessor');

	$path = $mb->install_path('elem' => '/foo/bar');
	is($path, '/foo/bar', '  returns assigned path');

	$path = $mb->install_path('elem');
	is($path, '/foo/bar', '  can read stored path');

	$map = $mb->install_path();
	is_deeply($map, { 'elem' => '/foo/bar' }, '  can access map');

	$path = $mb->install_path('elem' => undef);
	is($path, undef, '  can delete a path element');

	$map = $mb->install_path();
	is_deeply($map, {}, '  deletes path from map');
}

# Check install_base_relpaths() accessor
{
	my ($map, $path);

	$map = $mb->install_base_relpaths();
	is(ref($map), 'HASH', 'install_base_relpaths() accessor');

	eval{ $path = $mb->install_base_relpaths('elem' => '/foo/bar') };
	like($@, qr/Value must be a relative path/, '  emits error if path not relative');

	$path = $mb->install_base_relpaths('elem' => 'foo/bar');
	is($path, catdir(qw(foo bar)), '  returns assigned path');

	$path = $mb->install_base_relpaths('elem');
	is($path, catdir(qw(foo/bar)), '  can read stored path');

	$map = $mb->install_base_relpaths();
	is_deeply($map->{elem}, [qw(foo bar)], '  can access map');

	$path = $mb->install_base_relpaths('elem' => undef);
	is($path, undef, '  can delete a path element');

	$map = $mb->install_base_relpaths();
	is($map->{elem}, undef, '  deletes path from map');
}


# Check prefix_relpaths() accessor
{
	my ($map, $path);

	$map = $mb->prefix_relpaths();
	is(ref($map), 'HASH', 'prefix_relpaths() accessor');

	is_deeply($mb->prefix_relpaths(), $mb->prefix_relpaths('site'), '  defaults to \'site\'');

	eval{ $path = $mb->prefix_relpaths('site', 'elem' => '/foo/bar') };
	like($@, qr/Value must be a relative path/, '  emits error if path not relative');

	$path = $mb->prefix_relpaths('site', 'elem' => 'foo/bar');
	is($path, catdir(qw(foo bar)), '  returns assigned path');

	$path = $mb->prefix_relpaths('site', 'elem');
	is($path, catdir(qw(foo bar)), '  can read stored path');

	$map = $mb->prefix_relpaths();
	is_deeply($map->{elem}, [qw(foo bar)], '  can access map');

	$path = $mb->prefix_relpaths('site', 'elem' => undef);
	is($path, undef, '  can delete a path element');

	$map = $mb->prefix_relpaths();
	is($map->{elem}, undef, '  deletes path from map');
}


# Check that we install into the proper default locations.
{
	is($mb->installdirs, 'site');
	is($mb->install_base, undef);
	is($mb->prefix,       undef);

	test_install_destinations($mb, {
		lib     => catdir($tmp, 'site', @installstyle, 'site_perl'),
		arch	=> catdir($tmp, 'site', @installstyle, 'site_perl', @Config{qw(version archname)}),
		bin     => catdir($tmp, 'site', 'bin'),
		script  => catdir($tmp, 'site', 'bin'),
		bindoc  => catdir($tmp, 'site', 'man', 'man1'),
		libdoc  => catdir($tmp, 'site', 'man', 'man3'),
		binhtml => catdir($tmp, 'site', 'html'),
		libhtml => catdir($tmp, 'site', 'html'),
	});
	test_install_map($mb, {
		read                      => '',
		write                     => File::Spec->catfile($mb->install_destination('arch'), qw/auto ExtUtils InstallPaths .packlist/),
		catdir('blib', 'lib')     => catdir($tmp, 'site', @installstyle, 'site_perl'),
		catdir('blib', 'arch')    => catdir($tmp, 'site', @installstyle, 'site_perl', @Config{qw(version archname)}),
		catdir('blib', 'bin')     => catdir($tmp, 'site', 'bin'),
		catdir('blib', 'script')  => catdir($tmp, 'site', 'bin'),
	}, 'installdirs=site');
}

# Is installdirs honored?
{
	$mb->installdirs('core');
	is($mb->installdirs, 'core');

	test_install_destinations($mb, {
		lib     => catdir($tmp, @installstyle),
		arch	=> catdir($tmp, @installstyle, @Config{qw(version archname)}),
		bin     => catdir($tmp, 'bin'),
		script  => catdir($tmp, 'bin'),
		bindoc  => catdir($tmp, 'man', 'man1'),
		libdoc  => catdir($tmp, 'man', 'man3'),
		binhtml => catdir($tmp, 'html'),
		libhtml => catdir($tmp, 'html'),
	});

	$mb->installdirs('site');
	is($mb->installdirs, 'site');
}


# Check install_base()
{
	my $install_base = catdir('foo', 'bar');
	$mb->install_base($install_base);

	is($mb->prefix, undef);
	is($mb->install_base, $install_base);


	test_install_destinations($mb, {
		lib     => catdir($install_base, 'lib', 'perl5'),
		arch	=> catdir($install_base, 'lib', 'perl5', $Config{archname}),
		bin     => catdir($install_base, 'bin'),
		script  => catdir($install_base, 'bin'),
		bindoc  => catdir($install_base, 'man', 'man1'),
		libdoc  => catdir($install_base, 'man', 'man3'),
		binhtml => catdir($install_base, 'html'),
		libhtml => catdir($install_base, 'html'),
	});

	test_install_map($mb, {
		read                      => '',
		write                     => File::Spec->catfile($mb->install_destination('arch'), qw/auto ExtUtils InstallPaths .packlist/),
		catdir('blib', 'lib')     => catdir($install_base, 'lib', 'perl5'),
		catdir('blib', 'arch')    => catdir($install_base, 'lib', 'perl5', $Config{archname}),
		catdir('blib', 'bin')     => catdir($install_base, 'bin'),
		catdir('blib', 'script')  => catdir($install_base, 'bin'),
	}, 'install_base');
}


# Basic prefix test.  Ensure everything is under the prefix.
{
	$mb->install_base(undef);
	ok(!defined $mb->install_base);

	my $prefix = catdir(qw/some prefix/);
	$mb->prefix($prefix);
	is($mb->{prefix}, $prefix);

	test_prefix($prefix, $mb->install_sets('site'));
}

# And now that prefix honors installdirs.
{
	$mb->installdirs('core');
	is($mb->installdirs, 'core');

	my $prefix = catdir(qw/some prefix/);
	test_prefix($prefix);

	$mb->installdirs('site');
	is($mb->installdirs, 'site');
}

# Try a config setting which would result in installation locations outside
# the prefix.  Ensure it doesn't.
{
	# Get the prefix defaults
	my $defaults = $mb->prefix_relpaths('site');

	# Create a configuration involving weird paths that are outside of
	# the configured prefix.
	my @prefixes = ([qw(foo bar)], [qw(biz)], []);

	my %test_config;
	foreach my $type (keys %$defaults) {
		my $prefix = shift @prefixes || [qw(foo bar)];
		$test_config{$type} = catdir(File::Spec->rootdir, @$prefix, @{$defaults->{$type}});
	}

	# Poke at the innards of E::IP to change the default install locations.
	my $old =  $mb->install_sets->{site};
	$mb->install_sets->{site} = \%test_config;
	$mb->config->set(siteprefixexp => catdir(File::Spec->rootdir, 'wierd', 'prefix'));

	my $prefix = catdir('another', 'prefix');
	$mb->prefix($prefix);
	test_prefix($prefix, \%test_config);
	$mb->install_sets->{site} = $old;
}


# Check that we can use install_base after setting prefix.
{
	my $install_base = catdir('foo', 'bar');
	$mb->install_base($install_base);

	test_install_destinations($mb, {
		lib     => catdir($install_base, 'lib', 'perl5'),
		arch	=> catdir($install_base, 'lib', 'perl5', $Config{archname}),
		bin     => catdir($install_base, 'bin'),
		script  => catdir($install_base, 'bin'),
		bindoc  => catdir($install_base, 'man', 'man1'),
		libdoc  => catdir($install_base, 'man', 'man3'),
		binhtml => catdir($install_base, 'html'),
		libhtml => catdir($install_base, 'html'),
	});
}

sub dir_contains {
	my ($first, $second) = @_;
	# File::Spec doesn't have an easy way to check whether one directory
	# is inside another, unfortunately.

	($first, $second) = map { File::Spec->canonpath($_) } ($first, $second);
	my @first_dirs = File::Spec->splitdir($first);
	my @second_dirs = File::Spec->splitdir($second);

	return 0 if @second_dirs < @first_dirs;

	my $is_same = ( File::Spec->case_tolerant ? sub { lc(shift()) eq lc(shift()) } : sub { shift() eq shift() });

	while (@first_dirs) {
		return 0 unless $is_same->(shift @first_dirs, shift @second_dirs);
	}

	return 1;
}


sub test_prefix {
	my ($prefix, $test_config) = @_;

	local $Test::Builder::Level = $Test::Builder::Level + 1;

	foreach my $type (qw/lib arch bin script bindoc libdoc binhtml libhtml/) {
		my $dest = $mb->install_destination($type);
		ok dir_contains($prefix, $dest), "$type prefixed";

		SKIP: {
			skip("'$type' not configured", 1) unless $test_config && $test_config->{$type};

			have_same_ending($dest, $test_config->{$type}, "  suffix correctish ($test_config->{$type} + $prefix = $dest)");
		}
	}
}

sub have_same_ending {
	my ($dir1, $dir2, $message) = @_;

	$dir1 =~ s{/$}{} if $^O eq 'cygwin'; # remove any trailing slash
	my (undef, $dirs1, undef) = splitpath $dir1;
	my @dir1 = splitdir $dirs1;

	$dir2 =~ s{/$}{} if $^O eq 'cygwin'; # remove any trailing slash
	my (undef, $dirs2, undef) = splitpath $dir2;
	my @dir2 = splitdir $dirs2;

	is $dir1[-1], $dir2[-1], $message;
}

sub test_install_destinations {
	my ($build, $expect) = @_;

	local $Test::Builder::Level = $Test::Builder::Level + 1;

	while(my ($type, $expect) = each %$expect) {
		is($build->install_destination($type), $expect, "$type destination");
	}
}

sub test_install_map {
	my ($paths, $expect, $case) = @_;

	local $Test::Builder::Level = $Test::Builder::Level + 1;

	my $map = $paths->install_map;
	while(my ($type, $expect) = each %$expect) {
		is($map->{$type}, $expect, "$type destination for $case");
	}
}
