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

use File::Spec;
use Test::More;

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

BEGIN { require 'check_datetime_version.pl' }

# The point of this group of tests is to try to check that DST changes
# are occuring at exactly the right time in various time zones.  It's
# important to check both pre-generated spans, as well as spans that
# have to be generated on the fly.

# Rule	AN	1996	max	-	Mar	lastSun	2:00s	0	-
# Rule	AN	2000	only	-	Aug	lastSun	2:00s	1:00	-
# Rule	AN	2001	max	-	Oct	lastSun	2:00s	1:00	-
# Zone	NAME		GMTOFF	RULES	FORMAT	[UNTIL]
# Zone Australia/Sydney	10:04:52 -	LMT	1895 Feb
# 			10:00	Aus	EST	1971
# 			10:00	AN	EST

{

    # one minute before change to standard time
    my $dt = DateTime->new(
        year      => 1997, month  => 3, day => 29,
        hour      => 15,   minute => 59,
        time_zone => 'UTC'
    );

    $dt->set_time_zone('Australia/Sydney');

    is( $dt->hour, 2, 'A/S 1997: hour should be 2' );

    $dt->set_time_zone('UTC')->add( minutes => 1 )
        ->set_time_zone('Australia/Sydney');

    is( $dt->hour, 2, 'A/S 1997: hour should still be 2' );
}

# same tests without using UTC as intermediate
{

    # Can't start at 1:59 or we get the _2nd_ 1:59 of that day (post-DST change)
    my $dt = DateTime->new(
        year      => 1997, month  => 3, day => 30,
        hour      => 1,    minute => 59,
        time_zone => 'Australia/Sydney'
    );

    $dt->add( hours => 1 );

    is( $dt->hour, 2, 'A/S 1997: hour should be 2' );

    $dt->add( minutes => 1 );

    is( $dt->hour, 2, 'A/S 1997: hour should still be 2' );
}

{

    # one minute before change to standard time
    my $dt = DateTime->new(
        year      => 2002, month  => 10, day => 26,
        hour      => 15,   minute => 59,
        time_zone => 'UTC'
    );

    $dt->set_time_zone('Australia/Sydney');

    is( $dt->hour, 1, 'A/S 2002: hour should be 1' );

    $dt->set_time_zone('UTC')->add( minutes => 1 )
        ->set_time_zone('Australia/Sydney');

    is( $dt->hour, 3, 'A/S 2002: hour should be 3' );

}

# same tests without using UTC as intermediate
{
    my $dt = DateTime->new(
        year      => 2002, month  => 10, day => 27,
        hour      => 1,    minute => 59,
        time_zone => 'Australia/Sydney'
    );

    is( $dt->hour, 1, 'A/S 2002: hour should be 1' );

    $dt->add( minutes => 1 );

    is( $dt->hour, 3, 'A/S 2002: hour should be 3' );
}

# do same tests with future dates so more data is generated
{

    # Can't start at 1:59 or we get the _2nd_ 1:59 of that day (post-DST change)
    my $dt = DateTime->new(
        year      => 2040, month  => 4, day => 1,
        hour      => 1,    minute => 59,
        time_zone => 'Australia/Sydney'
    );

    $dt->add( hours => 1 );

    is( $dt->hour, 2, 'A/S 2040: hour should be 2' );

    $dt->add( minutes => 1 );

    is( $dt->hour, 2, 'A/S 2040: hour should still be 2' );
}

{
    my $dt = DateTime->new(
        year      => 2040, month  => 10, day => 7,
        hour      => 1,    minute => 59,
        time_zone => 'Australia/Sydney'
    );

    is( $dt->hour, 1, 'A/S 2040: hour should be 1' );

    $dt->add( minutes => 1 );

    is( $dt->hour, 3, 'A/S 2040: hour should be 3' );
}

# Rule	EU	1981	max	-	Mar	lastSun	 1:00u	1:00	S
# Rule	EU	1996	max	-	Oct	lastSun	 1:00u	0	-
{

    # one minute before change to standard time
    my $dt = DateTime->new(
        year      => 1982, month  => 3, day => 28,
        hour      => 0,    minute => 59,
        time_zone => 'UTC'
    );

    $dt->set_time_zone('Europe/Vienna');

    is( $dt->hour, 1, 'E/V 1982: hour should be 1' );

    $dt->set_time_zone('UTC')->add( minutes => 1 )
        ->set_time_zone('Europe/Vienna');

    is( $dt->hour, 3, 'E/V 1982: hour should be 3' );
}

# same tests without using UTC as intermediate
{

    # wrapped in eval because if change data is buggy it can throw exception
    my $dt = DateTime->new(
        year      => 1982, month  => 3, day => 28,
        hour      => 1,    minute => 59,
        time_zone => 'Europe/Vienna'
    );

    is( $dt->hour, 1, 'E/V 1982: hour should be 1' );

    $dt->add( minutes => 1 );

    is( $dt->hour, 3, 'E/V 1982: hour should be 3' );
}

{

    # one minute before change to standard time
    my $dt = DateTime->new(
        year      => 1997, month  => 10, day => 26,
        hour      => 0,    minute => 59,
        time_zone => 'UTC'
    );

    $dt->set_time_zone('Europe/Vienna');

    is( $dt->hour, 2, 'E/V 1997: hour should be 2' );

    $dt->set_time_zone('UTC')->add( minutes => 1 )
        ->set_time_zone('Europe/Vienna');

    is( $dt->hour, 2, 'E/V 1997: hour should still be 2' );

}

# same tests without using UTC as intermediate
{

    # can't be created directly because of overlap between changes
    my $dt = DateTime->new(
        year      => 1997, month  => 10, day => 26,
        hour      => 1,    minute => 59,
        time_zone => 'Europe/Vienna'
    );

    $dt->add( hours => 1 );

    is( $dt->hour, 2, 'E/V 1997: hour should be 2' );

    $dt->add( minutes => 1 );

    is( $dt->hour, 2, 'E/V 1997: hour should still be 2' );
}

# future
{
    my $dt = DateTime->new(
        year      => 2040, month  => 3, day => 25,
        hour      => 1,    minute => 59,
        time_zone => 'Europe/Vienna'
    );

    is( $dt->hour, 1, 'E/V 2040: hour should be 1' );

    $dt->add( minutes => 1 );

    is( $dt->hour, 3, 'E/V 2040: hour should be 3' );
}

{
    my $dt = DateTime->new(
        year      => 2040, month  => 10, day => 28,
        hour      => 1,    minute => 59,
        time_zone => 'Europe/Vienna'
    );

    $dt->add( hours => 1 );

    is( $dt->hour, 2, 'E/V 2040: hour should be 2' );

    $dt->add( minutes => 1 );

    is( $dt->hour, 2, 'E/V 2040: hour should still be 2' );
}

# Africa/Algiers has an observance that ends at 1977-10-21T00:00:00
# local time and a rule that starts at exactly the same time

# Rule	Algeria	1977	only	-	May	 6	 0:00	1:00	S
# Rule	Algeria	1977	only	-	Oct	21	 0:00	0	-
#
# 			0:00	Algeria	WE%sT	1977 Oct 21
# 			1:00	Algeria	CE%sT	1979 Oct 26
{
    my $dt = DateTime->new(
        year      => 1977, month  => 10, day => 20,
        hour      => 23,   minute => 59,
        time_zone => 'Africa/Algiers'
    );

    is( $dt->time_zone_short_name, 'WEST', 'short name is WEST' );
    is( $dt->is_dst,               1,      'is dst' );

    # observance ends, new rule starts, net effect is same offset,
    # different short name, no longer is DST
    $dt->add( minutes => 1 );

    is( $dt->time_zone_short_name, 'CET', 'short name is CET' );
    is( $dt->is_dst,               0,     'is not dst' );
}

{
    my $dt = DateTime->new(
        year => 2000, month     => 10, day => 5,
        hour => 15,   time_zone => 'America/Chicago',
    );
    is( $dt->hour,   15,     'hour is 15' );
    is( $dt->offset, -18000, 'offset is -18000' );
    is( $dt->is_dst, 1,      'is dst' );

    $dt->set_time_zone('America/New_York');
    is( $dt->offset, -14400, 'offset is -14400' );
    is( $dt->is_dst, 1,      'is dst' );
    is(
        $dt->hour, 16,
        'America/New_York is exactly one hour later than America/Chicago - hour'
    );
    is(
        $dt->minute, 0,
        'America/New_York is exactly one hour later than America/Chicago - minute'
    );
    is(
        $dt->second, 0,
        'America/New_York is exactly one hour later than America/Chicago - second'
    );
}

{

    # this is the second of the two 01:59:59 times for that date
    my $dt = DateTime->new(
        year      => 2003, month  => 10, day    => 26,
        hour      => 1,    minute => 59, second => 59,
        time_zone => 'America/Chicago',
    );

    is( $dt->offset, -21600, 'offset should be -21600' );
    is( $dt->is_dst, 0,      'is not dst' );

    $dt->subtract( hours => 1 );

    is( $dt->offset, -18000, 'offset should be -18000' );
    is( $dt->is_dst, 1,      'is not dst' );
    is( $dt->hour, 1, "crossing DST bounday does not change local hour" );
}

{
    my $dt = DateTime->new(
        year => 2003, month     => 10, day => 26,
        hour => 2,    time_zone => 'America/Chicago',
    );

    is( $dt->offset, -21600, 'offset should be -21600' );
}

{
    my $dt = DateTime->new(
        year => 2003, month     => 10, day => 26,
        hour => 3,    time_zone => 'America/Chicago',
    );

    is( $dt->offset, -21600, 'offset should be -21600' );
}

{
    eval {
        DateTime->new(
            year => 2003, month     => 4, day => 6,
            hour => 2,    time_zone => 'America/Chicago',
        );
    };

    like( $@, qr/Invalid local time .+/, 'exception for invalid time' );

    eval {
        DateTime->new(
            year      => 2003, month  => 4,  day    => 6,
            hour      => 2,    minute => 59, second => 59,
            time_zone => 'America/Chicago',
        );
    };
    like( $@, qr/Invalid local time .+/, 'exception for invalid time' );
}

{
    eval {
        DateTime->new(
            year      => 2003, month  => 4,  day    => 6,
            hour      => 1,    minute => 59, second => 59,
            time_zone => 'America/Chicago',
        );
    };
    ok( !$@, 'no exception for valid time' );

SKIP:
    {
        skip
            "DateTime 0.29 has a date math bug that causes this test to fail",
            1
            if ( DateTime->VERSION >= 0.29 && DateTime->VERSION < 0.30 );

        my $dt = DateTime->new(
            year      => 2003, month => 4, day => 5,
            hour      => 2,
            time_zone => 'America/Chicago',
        );

        eval { $dt->add( days => 1 ) };
        like( $@, qr/Invalid local time .+/,
            'exception for invalid time produced via add' );
    }
}

{
    my $dt = DateTime->new(
        year      => 2003, month => 4, day => 5,
        hour      => 2,
        time_zone => 'America/Chicago',
    );
    eval { $dt->add( hours => 24 ) };
    ok( !$@, 'add 24 hours should work even if add 1 day does not' );

    is( $dt->hour, 3, "hour should no be 3" );
}

{
    my $dt = DateTime->new(
        year => 2003, month     => 4, day => 6,
        hour => 3,    time_zone => 'America/Chicago',
    );

    is( $dt->hour,   3,      'hour should be 3' );
    is( $dt->offset, -18000, 'offset should be -18000' );

    $dt->subtract( seconds => 1 );

    is( $dt->hour,   1,      'hour should be 1' );
    is( $dt->offset, -21600, 'offset should be -21600' );
}

{
    my $dt = DateTime->new(
        year => 2003, month     => 4, day => 6,
        hour => 3,    time_zone => 'floating',
    );
    $dt->set_time_zone('America/Chicago');

    is( $dt->hour, 3, 'hour should be 3 after switching from floating TZ' );
    is(
        $dt->offset, -18000,
        'tz offset should be -18000'
    );
}

{
    my $dt = DateTime->new(
        year => 2003, month     => 4, day => 6,
        hour => 3,    time_zone => 'America/Chicago',
    );
    $dt->set_time_zone('floating');

    is( $dt->hour, 3, 'hour should be 3 after switching to floating TZ' );
    is(
        $dt->local_rd_as_seconds - $dt->utc_rd_as_seconds, 0,
        'tz offset should be 0'
    );
}

{
    eval {
        DateTime->new(
            year      => 2040, month  => 3,  day    => 11,
            hour      => 2,    minute => 59, second => 59,
            time_zone => 'America/Chicago',
        );
    };
    like( $@, qr/Invalid local time .+/, 'exception for invalid time' );
}

{
    my $dt = DateTime->new(
        year      => 2001, month  => 10, day => 28,
        hour      => 0,    minute => 59,
        time_zone => 'UTC'
    );

    $dt->set_time_zone('Europe/Vienna');

    is( $dt->hour, 2, 'hour should be 2 in vienna at 00:59:00 UTC' );

    $dt->set_time_zone('UTC')->add( minutes => 1 )
        ->set_time_zone('Europe/Vienna');

    is( $dt->hour, 2, 'hour should be 2 in vienna at 01:00:00 UTC' );
}

{

    # Doing this triggered a recursion bug in earlier versions of
    # DateTime::TimeZone.
    local $ENV{TZ} = 'America/Chicago';

    my $local_tz = DateTime::TimeZone->new( name => 'America/Chicago' );
    my $utc_tz   = DateTime::TimeZone->new( name => 'UTC' );

    my $dt = DateTime->new( year => 2050, time_zone => $local_tz );

    my $sixm = DateTime::Duration->new( months => 6 );
    foreach (
        [ 2050, 7, 1, 1, 'CDT' ],
        [ 2051, 1, 1, 0, 'CST' ],
        [ 2051, 7, 1, 1, 'CDT' ],
        [ 2052, 1, 1, 0, 'CST' ],
        [ 2052, 7, 1, 1, 'CDT' ],
        [ 2053, 1, 1, 0, 'CST' ],
        [ 2053, 7, 1, 1, 'CDT' ],
        [ 2054, 1, 1, 0, 'CST' ],
        [ 2054, 7, 1, 1, 'CDT' ],
        [ 2055, 1, 1, 0, 'CST' ],
        [ 2055, 7, 1, 1, 'CDT' ],
        [ 2056, 1, 1, 0, 'CST' ],
        [ 2056, 7, 1, 1, 'CDT' ],
        [ 2057, 1, 1, 0, 'CST' ],
        [ 2057, 7, 1, 1, 'CDT' ],
        [ 2058, 1, 1, 0, 'CST' ],
        [ 2058, 7, 1, 1, 'CDT' ],
        [ 2059, 1, 1, 0, 'CST' ],
        [ 2059, 7, 1, 1, 'CDT' ],
        [ 2060, 1, 1, 0, 'CST' ],
        [ 2060, 7, 1, 1, 'CDT' ],
        ) {
        $dt->set_time_zone($utc_tz);

        $dt->add_duration($sixm);

        $dt->set_time_zone($local_tz);

        $_->[1] = sprintf( '%02d', $_->[1] );

        my $expect = join ' ', @$_;

        is(
            $dt->strftime('%Y %m%e%k %Z'), $expect,
            "datetime is $expect"
        );
    }
}

{
    my $local_tz = DateTime::TimeZone->new( name => 'America/New_York' );
    my $utc_tz   = DateTime::TimeZone->new( name => 'UTC' );

    my $dt = DateTime->new( year => 2060, time_zone => $local_tz );

    my $neg_sixm = DateTime::Duration->new( months => -6 );
    foreach (
        [ 2059, 7, 1, 1, 'EDT' ],
        [ 2059, 1, 1, 0, 'EST' ],
        [ 2058, 7, 1, 1, 'EDT' ],
        [ 2058, 1, 1, 0, 'EST' ],
        [ 2057, 7, 1, 1, 'EDT' ],
        [ 2057, 1, 1, 0, 'EST' ],
        [ 2056, 7, 1, 1, 'EDT' ],
        [ 2056, 1, 1, 0, 'EST' ],
        [ 2055, 7, 1, 1, 'EDT' ],
        [ 2055, 1, 1, 0, 'EST' ],
        [ 2054, 7, 1, 1, 'EDT' ],
        [ 2054, 1, 1, 0, 'EST' ],
        [ 2053, 7, 1, 1, 'EDT' ],
        [ 2053, 1, 1, 0, 'EST' ],
        [ 2052, 7, 1, 1, 'EDT' ],
        [ 2052, 1, 1, 0, 'EST' ],
        [ 2051, 7, 1, 1, 'EDT' ],
        [ 2051, 1, 1, 0, 'EST' ],
        [ 2050, 7, 1, 1, 'EDT' ],
        [ 2050, 1, 1, 0, 'EST' ],
        ) {
        $dt->set_time_zone($utc_tz);

        $dt->add_duration($neg_sixm);

        $dt->set_time_zone($local_tz);

        $_->[1] = sprintf( '%02d', $_->[1] );

        my $expect = join ' ', @$_;

        is(
            $dt->strftime('%Y %m%e%k %Z'), $expect,
            "datetime is $expect"
        );
    }
}

done_testing();