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

use DateTime::TimeZone::Local;
use DateTime::TimeZone::Local::Unix;
use File::Basename qw( basename );
use File::Spec;
use Sys::Hostname qw( hostname );
use Test::More;

plan skip_all => 'HPUX is weird'
    if $^O eq 'hpux';

use lib File::Spec->catdir( File::Spec->curdir, 't' );

BEGIN { require 'check_datetime_version.pl' }

my $IsMaintainer = hostname() =~ /houseabsolute|quasar/ && -d '.hg';
my $CanWriteEtcLocaltime = -w '/etc/localtime' && -l '/etc/localtime';

# Ensures that we can load our OS-specific subclass. Otherwise this
# might happen later in an eval, and the error will get lost.
DateTime::TimeZone::Local->_load_subclass();

{
    my %links = DateTime::TimeZone->links();

    for my $alias ( sort keys %{ DateTime::TimeZone::links() } ) {
        local $ENV{TZ} = $alias;
        my $tz = eval { DateTime::TimeZone::Local->TimeZone() };
        is(
            $tz->name(), $links{$alias},
            "$alias in \$ENV{TZ} for Local->TimeZone()"
        );
    }
}

{
    for my $name ( sort DateTime::TimeZone::all_names() ) {
        local $ENV{TZ} = $name;
        my $tz = eval { DateTime::TimeZone::Local->TimeZone() };
        is(
            $tz->name(), $name,
            "$name in \$ENV{TZ} for Local->TimeZone()"
        );
    }
}

{
    local $ENV{TZ} = 'this will not work';

    my $tz = DateTime::TimeZone::Local::Unix->FromEnv();
    is(
        $tz, undef,
        'invalid time zone name in $ENV{TZ} fails'
    );

    local $ENV{TZ} = '123/456';

    $tz = DateTime::TimeZone::Local::Unix->FromEnv();
    is(
        $tz, undef,
        'invalid time zone name in $ENV{TZ} fails'
    );
}

{
    local $ENV{TZ} = 'Africa/Kinshasa';

    my $tz = DateTime::TimeZone::Local::Unix->FromEnv();
    is( $tz->name(), 'Africa/Kinshasa',
        'tz object name() is Africa::Kinshasa' );

    local $ENV{TZ} = 0;
    $tz = eval { DateTime::TimeZone::Local->TimeZone() };
    is(
        $tz->name(), 'UTC',
        "\$ENV{TZ} set to 0 returns UTC"
    );
}

{

    # This passes the _IsValidName() check but when passed to
    # DT::TZ->new() will throw an exception.
    {

        package Foo;
        use overload '""' => sub {"Foo"}, 'eq' => sub { "$_[0]" eq "$_[1]" };
    }
    local $ENV{TZ} = bless [], 'Foo';

    DateTime::TimeZone::Local::Unix->FromEnv();
    is( $@, '', 'FromEnv does not leave $@ set' );
}

{
    local $^O = 'DoesNotExist';
    my @err;
    local $SIG{__DIE__} = sub { push @err, shift };

    eval { DateTime::TimeZone::Local->_load_subclass() };

    is_deeply(
        \@err, [],
        'error loading local time zone module is not seen by __DIE__ handler'
    );
}

no warnings 'redefine';

SKIP:
{
    skip "/etc/localtime is not a symlink", 6
        unless -l '/etc/localtime';

    local *DateTime::TimeZone::Local::Unix::_Readlink
        = sub {'/usr/share/zoneinfo/US/Eastern'};

    my $tz;
    eval { $tz = DateTime::TimeZone::Local::Unix->FromEtcLocaltime() };
    is( $@, '',
        'valid time zone name in /etc/localtime symlink should not die' );
    is(
        $tz->name(), 'America/New_York',
        'FromEtchLocaltime() with _Readlink returning /usr/share/zoneinfo/US/Eastern'
    );

    local *DateTime::TimeZone::Local::Unix::_Readlink
        = sub {'/usr/share/zoneinfo/Foo/Bar'};

    $tz = DateTime::TimeZone::Local::Unix->FromEtcLocaltime();
    is( $@, '',
        'valid time zone name in /etc/localtime symlink should not leave $@ set'
    );
    ok( !$tz, 'no time zone was found' );

    local *DateTime::TimeZone::Local::Unix::_Readlink = sub {undef};
    local *DateTime::TimeZone::Local::Unix::_FindMatchingZoneinfoFile
        = sub {'America/Los_Angeles'};

    eval { $tz = DateTime::TimeZone::Local::Unix->FromEtcLocaltime() };
    is( $@, '',
        'fall back to _FindMatchZoneinfoFile if _Readlink finds nothing' );
    is(
        $tz->name(), 'America/Los_Angeles',
        'FromEtchLocaltime() with _FindMatchingZoneinfoFile returning America/Los_Angeles'
    );
}

SKIP:
{
    skip "cannot read /etc/sysconfig/clock", 2
        unless -r '/etc/sysconfig/clock' && -f _;

    local *DateTime::TimeZone::Local::Unix::_ReadEtcSysconfigClock
        = sub {'US/Eastern'};

    my $tz;
    eval { $tz = DateTime::TimeZone::Local::Unix->FromEtcSysconfigClock() };
    is( $@, '',
        'valid time zone name in /etc/sysconfig/clock should not die' );
    is(
        $tz->name(), 'America/New_York',
        'FromEtcSysConfigClock() with _ReadEtcSysconfigClock returning US/Eastern'
    );
}

SKIP:
{
    skip "cannot read /etc/default/init", 2
        unless -r '/etc/default/init' && -f _;

    local *DateTime::TimeZone::Local::Unix::_ReadEtcDefaultInit
        = sub {'Asia/Tokyo'};

    my $tz;
    eval { $tz = DateTime::TimeZone::Local::Unix->FromEtcDefaultInit() };
    is( $@, '', 'valid time zone name in /etc/default/init should not die' );
    is(
        $tz->name(), 'Asia/Tokyo',
        'FromEtcDefaultInit with _ReadEtcDefaultInit returning Asia/Tokyo'
    );
}

SKIP:
{
    skip
        "Cannot run these tests without explicitly knowing local time zone first (only runs on developers' machine)",
        4
        unless $IsMaintainer && -l '/etc/localtime';

    {
        local $ENV{TZ} = '';

        my $tz;
        eval { $tz = DateTime::TimeZone::Local->TimeZone() };
        is( $@, '', 'valid time zone name in /etc/localtime should not die' );
        is(
            $tz->name(), 'America/Chicago',
            '/etc/localtime should link to America/Chicago'
        );
    }

    {
        local *DateTime::TimeZone::Local::Unix::FromEtcLocaltime
            = sub {undef};

        my $tz;
        eval { $tz = DateTime::TimeZone::Local->TimeZone() };
        is( $@, '', 'valid time zone name in /etc/timezone should not die' );
        is(
            $tz->name(), 'America/Chicago',
            '/etc/timezone should contain America/Chicago'
        );
    }
}

SKIP:
{
    skip
        "Can only run this test on developers' machine", 2
            unless $IsMaintainer && -f '/etc/default/init';

    {

        # requires that /etc/default/init contain
        # TZ=Australia/Melbourne to work.
        local *DateTime::TimeZone::Local::Unix::FromEtcLocaltime
            = sub {undef};
        local *DateTime::TimeZone::Local::Unix::FromEtcTimezone = sub {undef};
        local *DateTime::TimeZone::Local::Unix::FromEtcTIMEZONE = sub {undef};

        my $tz;
        eval { $tz = DateTime::TimeZone::Local->TimeZone() };
        is( $@, '', '/etc/default/init contains TZ=Australia/Melbourne' );
        is(
            $tz->name(), 'Australia/Melbourne',
            '/etc/default/init should contain Australia/Melbourne'
        );
    }
}

SKIP:
{
    my $file = '/etc/timezone';

    skip "Cannot write this test unless we can write to /etc/timezone", 1
        unless $IsMaintainer && -w $file;

    open my $fh, '>', $file
        or die "Cannot write to $file: $!";
    print $fh 'Foo/Bar';
    close $fh;

    DateTime::TimeZone::Local::Unix->FromEtcTimezone();
    is( $@, '',
        'calling FromEtcTimezone when it contains a bad name should not leave $@ set'
    );

    open $fh, '>', $file
        or die "Cannot write to $file: $!";
    print $fh 'America/Chicago';
    close $fh;
}

SKIP:
{
    my $file = '/etc/TIMEZONE';

    skip "Cannot write this test unless we can write to /etc/TIMEZONE", 1
        unless $IsMaintainer && -w '/etc';

    open my $fh, '>', $file
        or die "Cannot write to $file: $!";
    print $fh "TZ = Foo/Bar\n";
    close $fh;

    DateTime::TimeZone::Local::Unix->FromEtcTIMEZONE();
    is( $@, '',
        'calling FromEtcTIMEZONE when it contains a bad name should not leave $@ set'
    );

    unlink $file
        or die "Cannot unlink $file: $!";
}

SKIP:
{
    skip "These tests are too dangerous to run on someone else's machine ;)",
        5
        unless $IsMaintainer;

    skip "These tests can only be run if we can overwrite /etc/localtime", 5
        unless $CanWriteEtcLocaltime;

    my $tz_file = readlink '/etc/localtime';

    unlink '/etc/localtime' or die "Cannot unlink /etc/localtime: $!";

    require File::Copy;
    File::Copy::copy( '/usr/share/zoneinfo/Asia/Calcutta', '/etc/localtime' )
        or die
        "Cannot copy /usr/share/zoneinfo/Asia/Calcutta to '/etc/localtime': $!";

    {
        local $ENV{TZ} = '';

        require Cwd;
        my $cwd = Cwd::cwd();

        my $tz;
        eval { $tz = DateTime::TimeZone::Local->TimeZone() };
        is( $@, '', 'copy of zoneinfo file at /etc/localtime' );
        is(
            $tz->name(), 'Asia/Calcutta',
            '/etc/localtime should be a copy of Asia/Calcutta'
        );

        is( Cwd::cwd(), $cwd,
            'cwd should not change after finding local time zone' );

        $tz = DateTime::TimeZone::Local->TimeZone();
        is( $@, '', 'calling _FindMatchZoneinfoFile does not leave $@ set' );
    }

    {
        local $ENV{TZ} = '';

        # Make sure that a die handler does not break our use of die
        # to escape from File::Find::find()
        local $SIG{__DIE__} = sub { die 'haha'; };

        my $tz;
        eval { $tz = DateTime::TimeZone::Local->TimeZone() };
        is(
            $tz->name(), 'Asia/Calcutta',
            'a __DIE__ handler did not interfere with our use of File::Find'
        );
    }

    unlink '/etc/localtime' or die "Cannot unlink /etc/localtime: $!";
    symlink $tz_file, '/etc/localtime'
        or die "Cannot symlink $tz_file to '/etc/localtime': $!";
}

{
    local $ENV{TZ} = 'Australia/Melbourne';
    my $tz = eval { DateTime::TimeZone->new( name => 'local' ) };
    is(
        $tz->name(), 'Australia/Melbourne',
        q|DT::TZ->new( name => 'local' )|
    );
}

SKIP:
{
    skip "These tests require File::Temp", 1
        unless require File::Temp;
    skip "These tests require a filesystem which support symlinks", 1
        unless eval { symlink '', ''; 1 };

    my $tempdir = File::Temp::tempdir( CLEANUP => 1 );

    my $first = File::Spec->catfile( $tempdir, 'first' );
    open my $fh, '>', $first
        or die "Cannot open $first: $!";
    close $fh;

    my $second = File::Spec->catfile( $tempdir, 'second' );
    symlink $first => $second
        or die "Cannot symlink $first => $second: $!";

    my $third = File::Spec->catfile( $tempdir, 'third' );
    symlink $second => $third
        or die "Cannot symlink $first => $second: $!";

    # It seems that on some systems (OSX, others?) the temp directory
    # returned by File::Temp may be a symlink (/tmp is a link to
    # /private/tmp), so when abs_path folows that link, we end up with
    # a different path to the "first" file.
    is(
        basename( DateTime::TimeZone::Local::Unix->_Readlink($third) ),
        basename($first),
        '_Readlink follows multiple levels of symlinking'
    );
}

done_testing();