<?php

declare(strict_types=1);

namespace Brick\DateTime\Tests;

use Brick\DateTime\Clock\FixedClock;
use Brick\DateTime\DateTimeException;
use Brick\DateTime\DayOfWeek;
use Brick\DateTime\Duration;
use Brick\DateTime\Instant;
use Brick\DateTime\LocalDate;
use Brick\DateTime\LocalDateTime;
use Brick\DateTime\LocalTime;
use Brick\DateTime\Month;
use Brick\DateTime\Parser\DateTimeParseException;
use Brick\DateTime\Period;
use Brick\DateTime\TimeZone;
use Brick\DateTime\TimeZoneOffset;
use Brick\DateTime\ZonedDateTime;
use DateTime;
use DateTimeImmutable;
use DateTimeZone;
use Generator;
use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\Attributes\RequiresPhp;

use function json_encode;

use const JSON_THROW_ON_ERROR;
use const PHP_VERSION_ID;

/**
 * Unit tests for class ZonedDateTime.
 */
class ZonedDateTimeTest extends AbstractTestCase
{
    /**
     * @param string $localDateTime The local date-time as a string.
     * @param string $timeZone      The time-zone as a string.
     * @param string $offset        The expected time-zone offset of the result zoned date-time.
     * @param int    $shift         The expected shift applied to the date-time (when in a gap), in seconds.
     * @param int    $epochSecond   The expected epoch-second the result zoned date-time resolves to.
     * @param int    $nanoOfSecond  The expected nano-of-second of the result zoned date-time.
     */
    #[DataProvider('providerOf')]
    public function testOf(string $localDateTime, string $timeZone, string $offset, int $shift, int $epochSecond, int $nanoOfSecond): void
    {
        $localDateTime = LocalDateTime::parse($localDateTime);
        $timeZone = TimeZone::parse($timeZone);
        $offset = TimeZoneOffset::parse($offset);

        $expectedDateTime = $localDateTime->plusSeconds($shift);

        $zonedDateTime = ZonedDateTime::of($localDateTime, $timeZone);

        self::assertInstanceOf(ZonedDateTime::class, $zonedDateTime);

        self::assertLocalDateTimeEquals($expectedDateTime, $zonedDateTime->getDateTime());
        self::assertTimeZoneEquals($timeZone, $zonedDateTime->getTimeZone());
        self::assertTimeZoneEquals($offset, $zonedDateTime->getTimeZoneOffset());

        self::assertSame($epochSecond, $zonedDateTime->getEpochSecond());
        self::assertSame($nanoOfSecond, $zonedDateTime->getNano());
    }

    public static function providerOf(): array
    {
        return [
            // WITH OFFSET FROM UTC
            // The date-time is resolved to an instant according to the offset without ambiguity.

            ['2000-01-01T12:34:56.123456789', '-18:00', '-18:00', 0, 946794896, 123456789],
            ['2001-02-02T12:34:56.123456789', '-17:00', '-17:00', 0, 981178496, 123456789],
            ['2002-03-03T12:34:56.123456789', '-16:00', '-16:00', 0, 1015216496, 123456789],
            ['2003-04-04T12:34:56.123456789', '-15:00', '-15:00', 0, 1049513696, 123456789],
            ['2004-05-05T12:34:56.123456789', '-14:00', '-14:00', 0, 1083810896, 123456789],
            ['2005-06-06T12:34:56.123456789', '-06:00', '-06:00', 0, 1118082896, 123456789],
            ['2006-07-07T12:34:56.123456789', '-05:00', '-05:00', 0, 1152293696, 123456789],
            ['2007-08-08T12:34:56.123456789', '-04:00', '-04:00', 0, 1186590896, 123456789],
            ['2008-09-09T12:34:56.123456789', '-03:00', '-03:00', 0, 1220974496, 123456789],
            ['2009-10-10T12:34:56.123456789', '-02:00', '-02:00', 0, 1255185296, 123456789],
            ['2010-11-11T12:34:56.123456789', '-01:30', '-01:30', 0, 1289484296, 123456789],
            ['2011-12-12T12:34:56.123456789', '-01:00', '-01:00', 0, 1323696896, 123456789],
            ['2012-01-13T12:34:56.123456789', '-00:45', '-00:45', 0, 1326460796, 123456789],
            ['2013-02-14T12:34:56.123456789', '-00:30', '-00:30', 0, 1360847096, 123456789],
            ['2014-03-15T12:34:56.123456789', '-00:15', '-00:15', 0, 1394887796, 123456789],
            ['2015-04-16T12:34:56.123456789', '+00:00', '+00:00', 0, 1429187696, 123456789],
            ['2016-05-17T12:34:56.123456789', '+00:15', '+00:15', 0, 1463487596, 123456789],
            ['2017-06-18T12:34:56.123456789', '+00:30', '+00:30', 0, 1497787496, 123456789],
            ['2018-07-19T12:34:56.123456789', '+00:45', '+00:45', 0, 1532000996, 123456789],
            ['2019-08-20T12:34:56.123456789', '+01:00', '+01:00', 0, 1566300896, 123456789],
            ['2011-09-21T12:34:56.123456789', '+01:30', '+01:30', 0, 1316603096, 123456789],
            ['2011-10-22T12:34:56.123456789', '+02:00', '+02:00', 0, 1319279696, 123456789],
            ['2011-11-23T12:34:56.123456789', '+03:00', '+03:00', 0, 1322040896, 123456789],
            ['2011-12-24T12:34:56.123456789', '+04:00', '+04:00', 0, 1324715696, 123456789],
            ['2011-01-25T12:34:56.123456789', '+05:00', '+05:00', 0, 1295940896, 123456789],
            ['2011-02-26T12:34:56.123456789', '+06:00', '+06:00', 0, 1298702096, 123456789],
            ['2011-03-27T12:34:56.123456789', '+14:00', '+14:00', 0, 1301178896, 123456789],
            ['2011-04-28T12:34:56.123456789', '+15:00', '+15:00', 0, 1303940096, 123456789],
            ['2011-05-29T12:34:56.123456789', '+16:00', '+16:00', 0, 1306614896, 123456789],
            ['2011-06-30T12:34:56.123456789', '+17:00', '+17:00', 0, 1309376096, 123456789],
            ['2011-07-31T12:34:56.123456789', '+18:00', '+18:00', 0, 1312050896, 123456789],

            // WITH REGION, NORMAL: NOT WITHIN A DST TRANSITION
            // The region is resolved to an offset without ambiguity.
            // The date-time is resolved to an instant without ambiguity.

            ['2001-06-01T12:34:56.123456789', 'Pacific/Niue',         '-11:00', 0,  991438496, 123456789],
            ['2002-06-02T12:34:56.123456789', 'Pacific/Honolulu',     '-10:00', 0, 1023057296, 123456789],
            ['2003-06-03T12:34:56.123456789', 'Pacific/Marquesas',    '-09:30', 0, 1054677896, 123456789],
            ['2004-12-04T12:34:56.123456789', 'America/Anchorage',    '-09:00', 0, 1102196096, 123456789],
            ['2005-06-05T12:34:56.123456789', 'America/Anchorage',    '-08:00', 0, 1118003696, 123456789],
            ['2006-06-06T12:34:56.123456789', 'America/Los_Angeles',  '-07:00', 0, 1149622496, 123456789],
            ['2007-12-07T12:34:56.123456789', 'America/Mexico_City',  '-06:00', 0, 1197052496, 123456789],
            ['2008-12-08T12:34:56.123456789', 'America/Nassau',       '-05:00', 0, 1228757696, 123456789],
            ['2009-01-09T12:34:56.123456789', 'America/Caracas',      '-04:30', 0, 1231520696, 123456789],
            ['2010-06-10T12:34:56.123456789', 'America/Nassau',       '-04:00', 0, 1276187696, 123456789],
            ['2011-12-11T12:34:56.123456789', 'America/St_Johns',     '-03:30', 0, 1323619496, 123456789],
            ['2012-06-12T12:34:56.123456789', 'America/Sao_Paulo',    '-03:00', 0, 1339515296, 123456789],
            ['2013-06-13T12:34:56.123456789', 'America/St_Johns',     '-02:30', 0, 1371135896, 123456789],
            ['2014-06-14T12:34:56.123456789', 'America/Noronha',      '-02:00', 0, 1402756496, 123456789],
            ['2015-06-15T12:34:56.123456789', 'Atlantic/Cape_Verde',  '-01:00', 0, 1434375296, 123456789],
            ['2016-12-16T12:34:56.123456789', 'Africa/Casablanca',    '+00:00', 0, 1481891696, 123456789],
            ['2017-06-17T12:34:56.123456789', 'Europe/London',        '+01:00', 0, 1497699296, 123456789],
            ['2019-06-19T12:34:56.123456789', 'Europe/Paris',         '+02:00', 0, 1560940496, 123456789],
            ['2020-06-20T12:34:56.123456789', 'Africa/Nairobi',       '+03:00', 0, 1592645696, 123456789],
            ['2001-12-21T12:34:56.123456789', 'Asia/Tehran',          '+03:30', 0, 1008925496, 123456789],
            ['2002-12-22T12:34:56.123456789', 'Asia/Yerevan',         '+04:00', 0, 1040546096, 123456789],
            ['2003-06-23T12:34:56.123456789', 'Asia/Tehran',          '+04:30', 0, 1056355496, 123456789],
            ['2004-06-24T12:34:56.123456789', 'Asia/Dushanbe',        '+05:00', 0, 1088062496, 123456789],
            ['2011-06-25T12:34:56.123456789', 'Asia/Colombo',         '+05:30', 0, 1308985496, 123456789],
            ['2006-06-26T12:34:56.123456789', 'Asia/Kathmandu',       '+05:45', 0, 1151304596, 123456789],
            ['2007-06-27T12:34:56.123456789', 'Asia/Dhaka',           '+06:00', 0, 1182926096, 123456789],
            ['2008-06-28T12:34:56.123456789', 'Asia/Rangoon',         '+06:30', 0, 1214633096, 123456789],
            ['2009-06-29T12:34:56.123456789', 'Asia/Bangkok',         '+07:00', 0, 1246253696, 123456789],
            ['2010-06-30T12:34:56.123456789', 'Asia/Hong_Kong',       '+08:00', 0, 1277872496, 123456789],
            ['2011-07-31T12:34:56.123456789', 'Australia/Eucla',      '+08:45', 0, 1312084196, 123456789],
            ['2012-07-01T12:34:56.123456789', 'Asia/Seoul',           '+09:00', 0, 1341113696, 123456789],
            ['2013-06-02T12:34:56.123456789', 'Australia/Adelaide',   '+09:30', 0, 1370142296, 123456789],
            ['2014-06-03T12:34:56.123456789', 'Pacific/Port_Moresby', '+10:00', 0, 1401762896, 123456789],
            ['2015-06-04T12:34:56.123456789', 'Australia/Lord_Howe',  '+10:30', 0, 1433383496, 123456789],
            ['2016-06-05T12:34:56.123456789', 'Pacific/Noumea',       '+11:00', 0, 1465090496, 123456789],
            ['2017-06-06T12:34:56.123456789', 'Pacific/Norfolk',      '+11:00', 0, 1496712896, 123456789],
            ['2018-06-07T12:34:56.123456789', 'Pacific/Auckland',     '+12:00', 0, 1528331696, 123456789],
            ['2019-06-08T12:34:56.123456789', 'Pacific/Chatham',      '+12:45', 0, 1559951396, 123456789],
            ['2020-06-09T12:34:56.123456789', 'Pacific/Apia',         '+13:00', 0, 1591659296, 123456789],
            ['2001-06-10T12:34:56.123456789', 'Pacific/Kiritimati',   '+14:00', 0,  992126096, 123456789],

            // WITH REGION, OVERLAP: SUMMER TO WINTER DST TRANSITION
            // If the date-time falls within the overlap (inclusive of the start time and exclusive of the end time),
            // the region can be resolved to two different offsets; the offset closest to UTC is chosen.
            // The date-time is resolved to an instant according to the resolved offset.

            // America/Juneau: at 2:00 AM (-08:00) clocks are set backward 1 hour, to 1:00 AM (-09:00)
            // Date-times falling between 1:00 AM and 2:00 AM on the transition day are resolved to -08:00

            ['2014-11-02T00:59:59.999999999', 'America/Juneau', '-08:00', 0, 1414918799, 999999999],
            ['2014-11-02T01:00:00.000000000', 'America/Juneau', '-08:00', 0, 1414918800,         0],
            ['2014-11-02T01:59:59.999999999', 'America/Juneau', '-08:00', 0, 1414922399, 999999999],
            ['2014-11-02T02:00:00.000000000', 'America/Juneau', '-09:00', 0, 1414926000,         0],
            ['2014-11-02T02:59:59.999999999', 'America/Juneau', '-09:00', 0, 1414929599, 999999999],

            // America/Chicago: at 2:00 AM (-05:00) clocks are set backward 1 hour, to 1:00 AM (-06:00)
            // Date-times falling between 1:00 AM and 2:00 AM on the transition day are resolved to -05:00

            ['2013-11-03T00:59:59.999999999', 'America/Chicago', '-05:00', 0, 1383458399, 999999999],
            ['2013-11-03T01:00:00.000000000', 'America/Chicago', '-05:00', 0, 1383458400,         0],
            ['2013-11-03T01:59:59.999999999', 'America/Chicago', '-05:00', 0, 1383461999, 999999999],
            ['2013-11-03T02:00:00.000000000', 'America/Chicago', '-06:00', 0, 1383465600,         0],
            ['2013-11-03T02:59:59.999999999', 'America/Chicago', '-06:00', 0, 1383469199, 999999999],

            ['2014-11-02T00:59:59.999999999', 'America/Chicago', '-05:00', 0, 1414907999, 999999999],
            ['2014-11-02T01:00:00.000000000', 'America/Chicago', '-05:00', 0, 1414908000,         0],
            ['2014-11-02T01:59:59.999999999', 'America/Chicago', '-05:00', 0, 1414911599, 999999999],
            ['2014-11-02T02:00:00.000000000', 'America/Chicago', '-06:00', 0, 1414915200,         0],
            ['2014-11-02T02:59:59.999999999', 'America/Chicago', '-06:00', 0, 1414918799, 999999999],

            // America/Miquelon: at 2:00 AM (-02:00) clocks are set backward 1 hour, to 1:00 AM  (-03:00)
            // Date-times falling between 1:00 AM and 2:00 AM on the transition day are resolved to -02:00

            ['2013-11-03T00:59:59.999999999', 'America/Miquelon', '-02:00', 0, 1383447599, 999999999],
            ['2013-11-03T01:00:00.000000000', 'America/Miquelon', '-02:00', 0, 1383447600,         0],
            ['2013-11-03T01:59:59.999999999', 'America/Miquelon', '-02:00', 0, 1383451199, 999999999],
            ['2013-11-03T02:00:00.000000000', 'America/Miquelon', '-03:00', 0, 1383454800,         0],
            ['2013-11-03T02:59:59.999999999', 'America/Miquelon', '-03:00', 0, 1383458399, 999999999],

            ['2014-11-02T00:59:59.999999999', 'America/Miquelon', '-02:00', 0, 1414897199, 999999999],
            ['2014-11-02T01:00:00.000000000', 'America/Miquelon', '-02:00', 0, 1414897200,         0],
            ['2014-11-02T01:59:59.999999999', 'America/Miquelon', '-02:00', 0, 1414900799, 999999999],
            ['2014-11-02T02:00:00.000000000', 'America/Miquelon', '-03:00', 0, 1414904400,         0],
            ['2014-11-02T02:59:59.999999999', 'America/Miquelon', '-03:00', 0, 1414907999, 999999999],

            // Europe/Paris: at 3:00 AM (+02:00) clocks are set backward 1 hour, to 2:00 AM (+01:00)
            // Date-times falling between 2:00 AM and 3:00 AM on the transition day are resolved to +01:00

            ['2013-10-27T01:00:00.000000000', 'Europe/Paris', '+02:00', 0, 1382828400,         0],
            ['2013-10-27T01:59:59.999999999', 'Europe/Paris', '+02:00', 0, 1382831999, 999999999],
            ['2013-10-27T02:00:00.000000000', 'Europe/Paris', '+01:00', 0, 1382835600,         0],
            ['2013-10-27T02:59:59.999999999', 'Europe/Paris', '+01:00', 0, 1382839199, 999999999],
            ['2013-10-27T03:00:00.000000000', 'Europe/Paris', '+01:00', 0, 1382839200,         0],

            ['2014-10-26T01:00:00.000000000', 'Europe/Paris', '+02:00', 0, 1414278000,         0],
            ['2014-10-26T01:59:59.999999999', 'Europe/Paris', '+02:00', 0, 1414281599, 999999999],
            ['2014-10-26T02:00:00.000000000', 'Europe/Paris', '+01:00', 0, 1414285200,         0],
            ['2014-10-26T02:59:59.999999999', 'Europe/Paris', '+01:00', 0, 1414288799, 999999999],
            ['2014-10-26T03:00:00.000000000', 'Europe/Paris', '+01:00', 0, 1414288800,         0],

            // Europe/Athens: at 4:00 AM (+03:00) clocks are set backward 1 hour, to 3:00 AM (+02:00)
            // Date-times falling between 3:00 AM nd 4:00 AM on the transition day are resolved to +02:00

            ['2013-10-27T02:00:00.000000000', 'Europe/Athens', '+03:00', 0, 1382828400,         0],
            ['2013-10-27T02:59:59.999999999', 'Europe/Athens', '+03:00', 0, 1382831999, 999999999],
            ['2013-10-27T03:00:00.000000000', 'Europe/Athens', '+02:00', 0, 1382835600,         0],
            ['2013-10-27T03:59:59.999999999', 'Europe/Athens', '+02:00', 0, 1382839199, 999999999],
            ['2013-10-27T04:00:00.000000000', 'Europe/Athens', '+02:00', 0, 1382839200,         0],

            ['2014-10-26T02:00:00.000000000', 'Europe/Athens', '+03:00', 0, 1414278000,         0],
            ['2014-10-26T02:59:59.999999999', 'Europe/Athens', '+03:00', 0, 1414281599, 999999999],
            ['2014-10-26T03:00:00.000000000', 'Europe/Athens', '+02:00', 0, 1414285200,         0],
            ['2014-10-26T03:59:59.999999999', 'Europe/Athens', '+02:00', 0, 1414288799, 999999999],
            ['2014-10-26T04:00:00.000000000', 'Europe/Athens', '+02:00', 0, 1414288800,         0],

            // Australia/Sydney: at 3:00 AM (+11:00) clocks are set backward 1 hour, to 2:00 AM (+10:00)
            // Date-times falling between 2:00 AM and 3:00 AM on the transition day are resolved to +10:00

            ['2013-04-07T01:00:00.000000000', 'Australia/Sydney', '+11:00', 0, 1365256800,         0],
            ['2013-04-07T01:59:59.999999999', 'Australia/Sydney', '+11:00', 0, 1365260399, 999999999],
            ['2013-04-07T02:00:00.000000000', 'Australia/Sydney', '+10:00', 0, 1365264000,         0],
            ['2013-04-07T02:59:59.999999999', 'Australia/Sydney', '+10:00', 0, 1365267599, 999999999],
            ['2013-04-07T03:00:00.000000000', 'Australia/Sydney', '+10:00', 0, 1365267600,         0],
            ['2013-04-07T03:59:59.999999999', 'Australia/Sydney', '+10:00', 0, 1365271199, 999999999],
            ['2013-04-07T04:00:00.000000000', 'Australia/Sydney', '+10:00', 0, 1365271200,         0],

            ['2014-04-06T01:00:00.000000000', 'Australia/Sydney', '+11:00', 0, 1396706400,         0],
            ['2014-04-06T01:59:59.999999999', 'Australia/Sydney', '+11:00', 0, 1396709999, 999999999],
            ['2014-04-06T02:00:00.000000000', 'Australia/Sydney', '+10:00', 0, 1396713600,         0],
            ['2014-04-06T02:59:59.999999999', 'Australia/Sydney', '+10:00', 0, 1396717199, 999999999],
            ['2014-04-06T03:00:00.000000000', 'Australia/Sydney', '+10:00', 0, 1396717200,         0],
            ['2014-04-06T03:59:59.999999999', 'Australia/Sydney', '+10:00', 0, 1396720799, 999999999],
            ['2014-04-06T04:00:00.000000000', 'Australia/Sydney', '+10:00', 0, 1396720800,         0],

            // WITH REGION, GAP: WINTER TO SUMMER DST TRANSITION
            // If the date-time falls within the gap (inclusive of the start time and exclusive of the end time),
            // the date-time is shifted forward by the length of the gap, and the region is resolved to
            // the later offset (summer time).

            // America/Juneau: at 2:00 AM (-09:00) clocks are set forward 1 hour, to 3:00 AM (-08:00)
            // Date-times falling between 2:00 AM and 3:00 AM on the transition day are shifted forward 1 hour

            ['2014-03-09T01:59:59.999999999', 'America/Juneau', '-09:00',    0, 1394362799, 999999999],
            ['2014-03-09T02:00:00.000000000', 'America/Juneau', '-08:00', 3600, 1394362800,         0],
            ['2014-03-09T02:59:59.999999999', 'America/Juneau', '-08:00', 3600, 1394366399, 999999999],
            ['2014-03-09T03:00:00.000000000', 'America/Juneau', '-08:00',    0, 1394362800,         0],
            ['2014-03-09T03:59:59.999999999', 'America/Juneau', '-08:00',    0, 1394366399, 999999999],

            // America/Chicago: at 2:00 AM (-06:00) clocks are set forward 1 hour, to 3:00 AM (-05:00)
            // Date-times falling between 2:00 AM and 3:00 AM on the transition day are shifted forward 1 hour

            ['2013-03-10T01:59:59.999999999', 'America/Chicago', '-06:00',    0, 1362902399, 999999999],
            ['2013-03-10T02:00:00.000000000', 'America/Chicago', '-05:00', 3600, 1362902400,         0],
            ['2013-03-10T02:59:59.999999999', 'America/Chicago', '-05:00', 3600, 1362905999, 999999999],
            ['2013-03-10T03:00:00.000000000', 'America/Chicago', '-05:00',    0, 1362902400,         0],
            ['2013-03-10T03:59:59.999999999', 'America/Chicago', '-05:00',    0, 1362905999, 999999999],

            ['2014-03-09T01:59:59.999999999', 'America/Chicago', '-06:00',    0, 1394351999, 999999999],
            ['2014-03-09T02:00:00.000000000', 'America/Chicago', '-05:00', 3600, 1394352000,         0],
            ['2014-03-09T02:59:59.999999999', 'America/Chicago', '-05:00', 3600, 1394355599, 999999999],
            ['2014-03-09T03:00:00.000000000', 'America/Chicago', '-05:00',    0, 1394352000,         0],
            ['2014-03-09T03:59:59.999999999', 'America/Chicago', '-05:00',    0, 1394355599, 999999999],

            // America/Miquelon: at 2:00 AM (-03:00) clocks are set forward 1 hour, to 3:00 AM (-02:00)
            // Date-times falling between 2:00 AM and 3:00 AM on the transition day are shifted forward 1 hour

            ['2013-03-10T01:59:59.999999999', 'America/Miquelon', '-03:00',    0, 1362891599, 999999999],
            ['2013-03-10T02:00:00.000000000', 'America/Miquelon', '-02:00', 3600, 1362891600,         0],
            ['2013-03-10T02:59:59.999999999', 'America/Miquelon', '-02:00', 3600, 1362895199, 999999999],
            ['2013-03-10T03:00:00.000000000', 'America/Miquelon', '-02:00',    0, 1362891600,         0],
            ['2013-03-10T03:59:59.999999999', 'America/Miquelon', '-02:00',    0, 1362895199, 999999999],

            ['2014-03-09T01:59:59.999999999', 'America/Miquelon', '-03:00',    0, 1394341199, 999999999],
            ['2014-03-09T02:00:00.000000000', 'America/Miquelon', '-02:00', 3600, 1394341200,         0],
            ['2014-03-09T02:59:59.999999999', 'America/Miquelon', '-02:00', 3600, 1394344799, 999999999],
            ['2014-03-09T03:00:00.000000000', 'America/Miquelon', '-02:00',    0, 1394341200,         0],
            ['2014-03-09T03:59:59.999999999', 'America/Miquelon', '-02:00',    0, 1394344799, 999999999],

            // Europe/Paris: at 2:00 AM (+01:00) clocks are set forward 1 hour, to 3:00 AM (+02:00)
            // Date-times falling between 2:00 AM and 3:00 AM on the transition day are shifted forward 1 hour

            ['2013-03-31T01:59:59.999999999', 'Europe/Paris', '+01:00',    0, 1364691599, 999999999],
            ['2013-03-31T02:00:00.000000000', 'Europe/Paris', '+02:00', 3600, 1364691600,         0],
            ['2013-03-31T02:59:59.999999999', 'Europe/Paris', '+02:00', 3600, 1364695199, 999999999],
            ['2013-03-31T03:00:00.000000000', 'Europe/Paris', '+02:00',    0, 1364691600,         0],
            ['2013-03-31T03:59:59.999999999', 'Europe/Paris', '+02:00',    0, 1364695199, 999999999],

            ['2014-03-30T01:59:59.999999999', 'Europe/Paris', '+01:00',    0, 1396141199, 999999999],
            ['2014-03-30T02:00:00.000000000', 'Europe/Paris', '+02:00', 3600, 1396141200,         0],
            ['2014-03-30T02:59:59.999999999', 'Europe/Paris', '+02:00', 3600, 1396144799, 999999999],
            ['2014-03-30T03:00:00.000000000', 'Europe/Paris', '+02:00',    0, 1396141200,         0],
            ['2014-03-30T03:59:59.999999999', 'Europe/Paris', '+02:00',    0, 1396144799, 999999999],

            // Europe/Athens: at 3:00 AM (+02:00) clocks are set forward 1 hour, to 4:00 AM (+03:00)
            // Date-times falling between 3:00 AM and 4:00 AM on the transition day are shifted forward 1 hour

            ['2013-03-31T02:59:59.999999999', 'Europe/Athens', '+02:00',    0, 1364691599, 999999999],
            ['2013-03-31T03:00:00.000000000', 'Europe/Athens', '+03:00', 3600, 1364691600,         0],
            ['2013-03-31T03:59:59.999999999', 'Europe/Athens', '+03:00', 3600, 1364695199, 999999999],
            ['2013-03-31T04:00:00.000000000', 'Europe/Athens', '+03:00',    0, 1364691600,         0],
            ['2013-03-31T04:59:59.999999999', 'Europe/Athens', '+03:00',    0, 1364695199, 999999999],

            ['2014-03-30T02:59:59.999999999', 'Europe/Athens', '+02:00',    0, 1396141199, 999999999],
            ['2014-03-30T03:00:00.000000000', 'Europe/Athens', '+03:00', 3600, 1396141200,         0],
            ['2014-03-30T03:59:59.999999999', 'Europe/Athens', '+03:00', 3600, 1396144799, 999999999],
            ['2014-03-30T04:00:00.000000000', 'Europe/Athens', '+03:00',    0, 1396141200,         0],
            ['2014-03-30T04:59:59.999999999', 'Europe/Athens', '+03:00',    0, 1396144799, 999999999],

            // Australia/Sydney: at 2:00 AM (+10:00) clocks are set forward 1 hour, to 3:00 AM (+11:00)
            // Date-times falling between 2:00 AM and 3:00 AM on the transition day are shifted forward 1 hour

            ['2013-10-06T01:59:59.999999999', 'Australia/Sydney', '+10:00',    0, 1380988799, 999999999],
            ['2013-10-06T02:00:00.000000000', 'Australia/Sydney', '+11:00', 3600, 1380988800,         0],
            ['2013-10-06T02:59:59.999999999', 'Australia/Sydney', '+11:00', 3600, 1380992399, 999999999],
            ['2013-10-06T03:00:00.000000000', 'Australia/Sydney', '+11:00',    0, 1380988800,         0],
            ['2013-10-06T03:59:59.999999999', 'Australia/Sydney', '+11:00',    0, 1380992399, 999999999],

            ['2014-10-05T01:59:59.999999999', 'Australia/Sydney', '+10:00',    0, 1412438399, 999999999],
            ['2014-10-05T02:00:00.000000000', 'Australia/Sydney', '+11:00', 3600, 1412438400,         0],
            ['2014-10-05T02:59:59.999999999', 'Australia/Sydney', '+11:00', 3600, 1412441999, 999999999],
            ['2014-10-05T03:00:00.000000000', 'Australia/Sydney', '+11:00',    0, 1412438400,         0],
            ['2014-10-05T03:59:59.999999999', 'Australia/Sydney', '+11:00',    0, 1412441999, 999999999],
        ];
    }

    #[DataProvider('providerOfInstant')]
    public function testOfInstant(string $formattedDatetime, string $timeZone): void
    {
        $instant = Instant::of(1000000000);
        $zonedDateTime = ZonedDateTime::ofInstant($instant, TimeZone::parse($timeZone));

        self::assertSame(1000000000, $zonedDateTime->getInstant()->getEpochSecond());
        self::assertSame($formattedDatetime, (string) $zonedDateTime->getDateTime());
    }

    public static function providerOfInstant(): array
    {
        return [
            ['2001-09-09T01:46:40', 'UTC'],
            ['2001-09-08T18:46:40', 'America/Los_Angeles'],
        ];
    }

    /**
     * @param string $text   The string to parse.
     * @param string $date   The expected date string.
     * @param string $time   The expected time string.
     * @param string $offset The expected time-zone offset.
     * @param string $zone   The expected time-zone, should be the same as offset when no region is specified.
     */
    #[DataProvider('providerParse')]
    public function testParse(string $text, string $date, string $time, string $offset, string $zone): void
    {
        $zonedDateTime = ZonedDateTime::parse($text);

        self::assertSame($date, (string) $zonedDateTime->getDate());
        self::assertSame($time, (string) $zonedDateTime->getTime());
        self::assertSame($offset, (string) $zonedDateTime->getTimeZoneOffset());
        self::assertSame($zone, (string) $zonedDateTime->getTimeZone());
    }

    public static function providerParse(): iterable
    {
        yield from [
            ['2001-02-03T01:02Z', '2001-02-03', '01:02', 'Z', 'Z'],
            ['2001-02-03T01:02:03Z', '2001-02-03', '01:02:03', 'Z', 'Z'],
            ['2001-02-03T01:02:03.456Z', '2001-02-03', '01:02:03.456', 'Z', 'Z'],
            ['2001-02-03T01:02-03:00', '2001-02-03', '01:02', '-03:00', '-03:00'],
            ['2001-02-03T01:02:03+04:00', '2001-02-03', '01:02:03', '+04:00', '+04:00'],
            ['2001-02-03T01:02:03.456+12:34', '2001-02-03', '01:02:03.456', '+12:34', '+12:34'],
            ['2001-02-03T01:02Z[Europe/London]', '2001-02-03', '01:02', 'Z', 'Europe/London'],
            ['2001-02-03T01:02+00:00[Europe/London]', '2001-02-03', '01:02', 'Z', 'Europe/London'],
            ['2001-02-03T01:02:03-00:00[Europe/London]', '2001-02-03', '01:02:03', 'Z', 'Europe/London'],
            ['2001-02-03T01:02:03.456+00:00[Europe/London]', '2001-02-03', '01:02:03.456', 'Z', 'Europe/London'],
        ];

        if (PHP_VERSION_ID >= 80107) {
            yield ['2001-02-03T01:02:03.456+12:34:56', '2001-02-03', '01:02:03.456', '+12:34:56', '+12:34:56'];
        }
    }

    #[DataProvider('providerParseInvalidStringThrowsException')]
    public function testParseInvalidStringThrowsException(string $text): void
    {
        $this->expectException(DateTimeParseException::class);
        ZonedDateTime::parse($text);
    }

    public static function providerParseInvalidStringThrowsException(): array
    {
        return [
            [''],
            ['2001'],
            ['2001-'],
            ['2001-02'],
            ['2001-02-'],
            ['2001-02-03'],
            ['2001-02-03T'],
            ['2001-02-03T04'],
            ['2001-02-03T04:'],
            ['2001-02-03T04:05'],
            ['2001-02-03T04:05:06'],
            ['2001-02-03T04:05:06.789'],
            ['2001-02-03T04:05Z[]'],
            ['2001-02-03T04:05[Europe/London]'],
            ['2001-02-03T04:05:06[Europe/London]'],
            ['2001-02-03T04:05.789Z[Europe/London]'],
            ['2001-02-03T04:05:06Z[Europe/London'],

            [' 2001-02-03T01:02:03Z'],
            ['2001-02-03T01:02:03Z '],
        ];
    }

    #[RequiresPhp('< 8.1.7')]
    #[DataProvider('providerParseSecondsOffsetThrowsException')]
    public function testParseSecondsOffsetThrowsException(string $text): void
    {
        $this->expectException(DateTimeException::class);
        ZonedDateTime::parse($text);
    }

    public static function providerParseSecondsOffsetThrowsException(): array
    {
        return [
            ['2001-02-03T01:02:03.456+12:34:56'],
        ];
    }

    #[DataProvider('providerFromNativeDateTime')]
    public function testFromNativeDateTime(string $dateTimeString, string $timeZone, string $expected): void
    {
        $dateTime = new DateTime($dateTimeString, new DateTimeZone($timeZone));
        self::assertIs(ZonedDateTime::class, $expected, ZonedDateTime::fromNativeDateTime($dateTime));
    }

    public static function providerFromNativeDateTime(): array
    {
        return [
            ['2018-07-21 14:09:10.23456', 'America/Los_Angeles', '2018-07-21T14:09:10.23456-07:00[America/Los_Angeles]'],
            ['2019-01-21 17:59', 'America/Los_Angeles', '2019-01-21T17:59-08:00[America/Los_Angeles]'],
            ['2019-01-23 09:10:11.123', '+05:30', '2019-01-23T09:10:11.123+05:30'],
        ];
    }

    public function testChangeTimeZone(): void
    {
        $timezone1 = TimeZone::parse('UTC');
        $timezone2 = TimeZone::parse('America/Los_Angeles');

        $datetime1 = ZonedDateTime::ofInstant(Instant::of(1000000000), $timezone1);
        $datetime2 = $datetime1->withTimeZoneSameInstant($timezone2);

        self::assertSame($timezone1, $datetime1->getTimeZone());
        self::assertSame($timezone2, $datetime2->getTimeZone());
        self::assertSame('2001-09-08T18:46:40', (string) $datetime2->getDateTime());

        $datetime2 = $datetime1->withTimeZoneSameLocal($timezone2);

        self::assertSame($timezone1, $datetime1->getTimeZone());
        self::assertSame($timezone2, $datetime2->getTimeZone());
        self::assertSame('2001-09-09T01:46:40', (string) $datetime2->getDateTime());
    }

    /**
     * @param string $z1  The first zoned date-time.
     * @param string $z2  The second zoned date-time.
     * @param int    $cmp The comparison value.
     */
    #[DataProvider('providerCompareTo')]
    public function testCompareTo(string $z1, string $z2, int $cmp): void
    {
        $z1 = ZonedDateTime::parse($z1);
        $z2 = ZonedDateTime::parse($z2);

        self::assertSame($cmp, $z1->compareTo($z2));
        self::assertSame($cmp === 0, $z1->isEqualTo($z2));
        self::assertSame($cmp === -1, $z1->isBefore($z2));
        self::assertSame($cmp === 1, $z1->isAfter($z2));
        self::assertSame($cmp <= 0, $z1->isBeforeOrEqualTo($z2));
        self::assertSame($cmp >= 0, $z1->isAfterOrEqualTo($z2));

        self::assertSame(-$cmp, $z2->compareTo($z1));
        self::assertSame($cmp === 0, $z2->isEqualTo($z1));
        self::assertSame($cmp === 1, $z2->isBefore($z1));
        self::assertSame($cmp === -1, $z2->isAfter($z1));
        self::assertSame($cmp >= 0, $z2->isBeforeOrEqualTo($z1));
        self::assertSame($cmp <= 0, $z2->isAfterOrEqualTo($z1));
    }

    public static function providerCompareTo(): array
    {
        return [
            ['2020-06-06T14:30:30Z', '2014-12-31T23:59:59.999Z', 1],
            ['2020-06-06T14:30:30Z', '2020-06-06T14:30:30+00:00', 0],
            ['2020-06-06T14:30:30Z', '2020-06-06T14:29:29.999999999+00:00', 1],
            ['2020-06-06T14:30:30Z', '2020-06-06T14:30:30.000000000+00:00', 0],
            ['2020-06-06T14:30:30Z', '2020-06-06T14:30:30.000000001+00:00', -1],
            ['2020-06-06T14:30:30Z', '2020-06-06T15:30:30+01:00', 0],
            ['2020-06-06T14:30:30Z', '2020-06-06T16:00:30+01:30', 0],
            ['2020-06-06T14:30:30Z', '2020-06-06T15:30:30+02:00', 1],
            ['2020-06-06T14:30:30Z', '2020-06-06T13:30:30-02:00', -1],
        ];
    }

    public function testGetYear(): void
    {
        self::assertSame(2000, $this->getTestZonedDateTime()->getYear());
    }

    #[DataProvider('providerGetMonth')]
    public function testGetMonth(int $monthValue, Month $month): void
    {
        $zonedDateTime = ZonedDateTime::of(
            LocalDateTime::of(2000, $monthValue, 1),
            TimeZone::utc(),
        );

        self::assertSame($month, $zonedDateTime->getMonth());
    }

    public static function providerGetMonth(): array
    {
        return [
            [1, Month::JANUARY],
            [2, Month::FEBRUARY],
            [3, Month::MARCH],
            [4, Month::APRIL],
            [5, Month::MAY],
            [6, Month::JUNE],
            [7, Month::JULY],
            [8, Month::AUGUST],
            [9, Month::SEPTEMBER],
            [10, Month::OCTOBER],
            [11, Month::NOVEMBER],
            [12, Month::DECEMBER],
        ];
    }

    public function testGetMonthValue(): void
    {
        self::assertSame(1, $this->getTestZonedDateTime()->getMonthValue());
    }

    public function testGetDayOfMonth(): void
    {
        self::assertSame(20, $this->getTestZonedDateTime()->getDayOfMonth());
    }

    public function testGetDayOfWeek(): void
    {
        self::assertSame(DayOfWeek::THURSDAY, $this->getTestZonedDateTime()->getDayOfWeek());
    }

    public function testGetDayOfYear(): void
    {
        self::assertSame(20, $this->getTestZonedDateTime()->getDayOfYear());
    }

    public function testGetHour(): void
    {
        self::assertSame(12, $this->getTestZonedDateTime()->getHour());
    }

    public function testGetMinute(): void
    {
        self::assertSame(34, $this->getTestZonedDateTime()->getMinute());
    }

    public function testGetSecond(): void
    {
        self::assertSame(56, $this->getTestZonedDateTime()->getSecond());
    }

    public function testWithDate(): void
    {
        $newDate = LocalDate::of(2000, 1, 22);

        self::assertIs(ZonedDateTime::class, '2000-01-22T12:34:56.123456789-08:00[America/Los_Angeles]', $this->getTestZonedDateTime()->withDate($newDate));
    }

    public function testWithTime(): void
    {
        $time = LocalTime::of(1, 2, 3, 987654321);

        self::assertIs(ZonedDateTime::class, '2000-01-20T01:02:03.987654321-08:00[America/Los_Angeles]', $this->getTestZonedDateTime()->withTime($time));
    }

    public function testWithYear(): void
    {
        self::assertIs(ZonedDateTime::class, '2020-01-20T12:34:56.123456789-08:00[America/Los_Angeles]', $this->getTestZonedDateTime()->withYear(2020));
    }

    public function testWithMonth(): void
    {
        self::assertIs(ZonedDateTime::class, '2000-07-20T12:34:56.123456789-07:00[America/Los_Angeles]', $this->getTestZonedDateTime()->withMonth(7));
        self::assertIs(ZonedDateTime::class, '2000-07-20T12:34:56.123456789-07:00[America/Los_Angeles]', $this->getTestZonedDateTime()->withMonth(Month::JULY));
    }

    public function testWithDay(): void
    {
        self::assertIs(ZonedDateTime::class, '2000-01-31T12:34:56.123456789-08:00[America/Los_Angeles]', $this->getTestZonedDateTime()->withDay(31));
    }

    public function testWithHour(): void
    {
        self::assertIs(ZonedDateTime::class, '2000-01-20T23:34:56.123456789-08:00[America/Los_Angeles]', $this->getTestZonedDateTime()->withHour(23));
    }

    public function testWithMinute(): void
    {
        self::assertIs(ZonedDateTime::class, '2000-01-20T12:00:56.123456789-08:00[America/Los_Angeles]', $this->getTestZonedDateTime()->withMinute(0));
    }

    public function testWithSecond(): void
    {
        self::assertIs(ZonedDateTime::class, '2000-01-20T12:34:06.123456789-08:00[America/Los_Angeles]', $this->getTestZonedDateTime()->withSecond(6));
    }

    public function testWithNano(): void
    {
        self::assertIs(ZonedDateTime::class, '2000-01-20T12:34:56.000000123-08:00[America/Los_Angeles]', $this->getTestZonedDateTime()->withNano(123));
    }

    public function testWithFixedOffsetTimeZone(): void
    {
        self::assertIs(ZonedDateTime::class, '2000-01-20T12:34:56.123456789-08:00', $this->getTestZonedDateTime()->withFixedOffsetTimeZone());
    }

    #[DataProvider('providerPlusPeriod')]
    public function testPlusPeriod(string $zonedDateTime, Period $plusPeriod, string $expected): void
    {
        $actual = ZonedDateTime::parse($zonedDateTime)->plusPeriod($plusPeriod);
        self::assertIs(ZonedDateTime::class, $expected, $actual);
    }

    public static function providerPlusPeriod(): Generator
    {
        yield from [
            ['2000-03-29T12:34:56.123456789-08:00[America/Los_Angeles]', Period::ofDays(3), '2000-04-01T12:34:56.123456789-08:00[America/Los_Angeles]'],
            ['2000-03-29T12:34:56.123456789-08:00[America/Los_Angeles]', Period::ofDays(4), '2000-04-02T12:34:56.123456789-07:00[America/Los_Angeles]'],
            ['2000-04-03T12:34:56.123456789-07:00[America/Los_Angeles]', Period::ofDays(-1), '2000-04-02T12:34:56.123456789-07:00[America/Los_Angeles]'],
            ['2000-04-03T12:34:56.123456789-07:00[America/Los_Angeles]', Period::ofDays(-2), '2000-04-01T12:34:56.123456789-08:00[America/Los_Angeles]'],
            ['2000-01-20T12:34:56.123456789-08:00[America/Los_Angeles]', Period::ofWeeks(10), '2000-03-30T12:34:56.123456789-08:00[America/Los_Angeles]'],
            ['2000-01-20T12:34:56.123456789-08:00[America/Los_Angeles]', Period::ofWeeks(11), '2000-04-06T12:34:56.123456789-07:00[America/Los_Angeles]'],
            ['2000-04-06T12:34:56.123456789-07:00[America/Los_Angeles]', Period::ofWeeks(-2), '2000-03-23T12:34:56.123456789-08:00[America/Los_Angeles]'],
            ['2000-02-16T12:34:56.123456789-08:00[America/Los_Angeles]', Period::ofMonths(1), '2000-03-16T12:34:56.123456789-08:00[America/Los_Angeles]'],
            ['2000-02-16T12:34:56.123456789-08:00[America/Los_Angeles]', Period::ofMonths(2), '2000-04-16T12:34:56.123456789-07:00[America/Los_Angeles]'],
            ['2000-05-16T12:34:56.123456789-07:00[America/Los_Angeles]', Period::ofMonths(-1), '2000-04-16T12:34:56.123456789-07:00[America/Los_Angeles]'],
            ['2000-05-16T12:34:56.123456789-07:00[America/Los_Angeles]', Period::ofMonths(-2), '2000-03-16T12:34:56.123456789-08:00[America/Los_Angeles]'],
            ['2000-02-16T12:34:56.123456789-07:00[America/Los_Angeles]', Period::of(1, -2, 3), '2000-12-19T12:34:56.123456789-08:00[America/Los_Angeles]'],
        ];

        foreach (self::providerPlusDays() as [$zonedDateTime, $plusDays, $expected]) {
            yield [$zonedDateTime, Period::ofDays($plusDays), $expected];
        }

        foreach (self::providerPlusWeeks() as [$zonedDateTime, $plusWeeks, $expected]) {
            yield [$zonedDateTime, Period::ofWeeks($plusWeeks), $expected];
        }

        foreach (self::providerPlusMonths() as [$zonedDateTime, $plusMonths, $expected]) {
            yield [$zonedDateTime, Period::ofMonths($plusMonths), $expected];
        }

        foreach (self::providerPlusYears() as [$zonedDateTime, $plusYears, $expected]) {
            yield [$zonedDateTime, Period::ofYears($plusYears), $expected];
        }
    }

    #[DataProvider('providerPlusDuration')]
    public function testPlusDuration(string $zonedDateTime, Duration $plusDuration, string $expected): void
    {
        $actual = ZonedDateTime::parse($zonedDateTime)->plusDuration($plusDuration);
        self::assertIs(ZonedDateTime::class, $expected, $actual);
    }

    public static function providerPlusDuration(): Generator
    {
        yield ['2000-01-20T12:34:56.123456789-08:00[America/Los_Angeles]', Duration::ofSeconds(-5), '2000-01-20T12:34:51.123456789-08:00[America/Los_Angeles]'];
        yield ['2000-01-20T12:34:56.123456789-08:00[America/Los_Angeles]', Duration::ofSeconds(5), '2000-01-20T12:35:01.123456789-08:00[America/Los_Angeles]'];
        yield ['2000-01-20T12:34:56.123456789-08:00[America/Los_Angeles]', Duration::ofMinutes(-10), '2000-01-20T12:24:56.123456789-08:00[America/Los_Angeles]'];
        yield ['2000-01-20T12:34:56.123456789-08:00[America/Los_Angeles]', Duration::ofMinutes(10), '2000-01-20T12:44:56.123456789-08:00[America/Los_Angeles]'];
        yield ['2000-01-20T12:34:56.123456789-08:00[America/Los_Angeles]', Duration::ofHours(-24), '2000-01-19T12:34:56.123456789-08:00[America/Los_Angeles]'];
        yield ['2000-01-20T12:34:56.123456789-08:00[America/Los_Angeles]', Duration::ofHours(24), '2000-01-21T12:34:56.123456789-08:00[America/Los_Angeles]'];
        yield ['2000-01-20T12:34:56.123456789-08:00[America/Los_Angeles]', Duration::ofSeconds(1, 5000), '2000-01-20T12:34:57.123461789-08:00[America/Los_Angeles]'];

        foreach (self::providerPlusSeconds() as [$zonedDateTime, $plusSeconds, $expected]) {
            yield [$zonedDateTime, Duration::ofSeconds($plusSeconds), $expected];
        }

        foreach (self::providerPlusMinutes() as [$zonedDateTime, $plusMinutes, $expected]) {
            yield [$zonedDateTime, Duration::ofMinutes($plusMinutes), $expected];
        }

        foreach (self::providerPlusHours() as [$zonedDateTime, $plusHours, $expected]) {
            yield [$zonedDateTime, Duration::ofHours($plusHours), $expected];
        }
    }

    #[DataProvider('providerPlusYears')]
    public function testPlusYears(string $zonedDateTime, int $plusYears, string $expected): void
    {
        $actual = ZonedDateTime::parse($zonedDateTime)->plusYears($plusYears);
        self::assertIs(ZonedDateTime::class, $expected, $actual);
    }

    public static function providerPlusYears(): array
    {
        return [
            ['2000-01-20T12:34:56.123456789-08:00[America/Los_Angeles]', -2, '1998-01-20T12:34:56.123456789-08:00[America/Los_Angeles]'],
            ['2000-01-20T12:34:56.123456789-08:00[America/Los_Angeles]', -1, '1999-01-20T12:34:56.123456789-08:00[America/Los_Angeles]'],
            ['2000-01-20T12:34:56.123456789-08:00[America/Los_Angeles]', 0, '2000-01-20T12:34:56.123456789-08:00[America/Los_Angeles]'],
            ['2000-01-20T12:34:56.123456789-08:00[America/Los_Angeles]', 1, '2001-01-20T12:34:56.123456789-08:00[America/Los_Angeles]'],
            ['2000-01-20T12:34:56.123456789-08:00[America/Los_Angeles]', 2, '2002-01-20T12:34:56.123456789-08:00[America/Los_Angeles]'],
        ];
    }

    #[DataProvider('providerPlusMonths')]
    public function testPlusMonths(string $zonedDateTime, int $plusMonths, string $expected): void
    {
        $actual = ZonedDateTime::parse($zonedDateTime)->plusMonths($plusMonths);
        self::assertIs(ZonedDateTime::class, $expected, $actual);
    }

    public static function providerPlusMonths(): array
    {
        return [
            ['2000-01-20T12:34:56.123456789-08:00[America/Los_Angeles]', -4, '1999-09-20T12:34:56.123456789-07:00[America/Los_Angeles]'],
            ['2000-01-20T12:34:56.123456789-08:00[America/Los_Angeles]', -3, '1999-10-20T12:34:56.123456789-07:00[America/Los_Angeles]'],
            ['2000-01-20T12:34:56.123456789-08:00[America/Los_Angeles]', -2, '1999-11-20T12:34:56.123456789-08:00[America/Los_Angeles]'],
            ['2000-01-20T12:34:56.123456789-08:00[America/Los_Angeles]', -1, '1999-12-20T12:34:56.123456789-08:00[America/Los_Angeles]'],
            ['2000-01-20T12:34:56.123456789-08:00[America/Los_Angeles]', 0, '2000-01-20T12:34:56.123456789-08:00[America/Los_Angeles]'],
            ['2000-01-20T12:34:56.123456789-08:00[America/Los_Angeles]', 1, '2000-02-20T12:34:56.123456789-08:00[America/Los_Angeles]'],
            ['2000-01-20T12:34:56.123456789-08:00[America/Los_Angeles]', 2, '2000-03-20T12:34:56.123456789-08:00[America/Los_Angeles]'],
            ['2000-01-20T12:34:56.123456789-08:00[America/Los_Angeles]', 3, '2000-04-20T12:34:56.123456789-07:00[America/Los_Angeles]'],
            ['2000-01-20T12:34:56.123456789-08:00[America/Los_Angeles]', 4, '2000-05-20T12:34:56.123456789-07:00[America/Los_Angeles]'],
        ];
    }

    #[DataProvider('providerPlusWeeks')]
    public function testPlusWeeks(string $zonedDateTime, int $plusWeeks, string $expected): void
    {
        $actual = ZonedDateTime::parse($zonedDateTime)->plusWeeks($plusWeeks);
        self::assertIs(ZonedDateTime::class, $expected, $actual);
    }

    public static function providerPlusWeeks(): array
    {
        return [
            ['2000-01-01T12:34:56.123456789-08:00[America/Los_Angeles]', -25, '1999-07-10T12:34:56.123456789-07:00[America/Los_Angeles]'],
            ['2000-01-01T12:34:56.123456789-08:00[America/Los_Angeles]', -1, '1999-12-25T12:34:56.123456789-08:00[America/Los_Angeles]'],
            ['2000-01-01T12:34:56.123456789-08:00[America/Los_Angeles]', 0, '2000-01-01T12:34:56.123456789-08:00[America/Los_Angeles]'],
            ['2000-01-01T12:34:56.123456789-08:00[America/Los_Angeles]', 1, '2000-01-08T12:34:56.123456789-08:00[America/Los_Angeles]'],
            ['2000-01-01T12:34:56.123456789-08:00[America/Los_Angeles]', 25, '2000-06-24T12:34:56.123456789-07:00[America/Los_Angeles]'],

            // https://github.com/brick/date-time/issues/115
            ['2025-03-23T01:00+01:00[Europe/Prague]', 1, '2025-03-30T01:00+01:00[Europe/Prague]'],
            ['2025-03-23T01:30+01:00[Europe/Prague]', 1, '2025-03-30T01:30+01:00[Europe/Prague]'],
            ['2025-03-23T02:00+01:00[Europe/Prague]', 1, '2025-03-30T03:00+02:00[Europe/Prague]'],
            ['2025-03-23T02:30+01:00[Europe/Prague]', 1, '2025-03-30T03:30+02:00[Europe/Prague]'],
            ['2025-03-23T03:00+01:00[Europe/Prague]', 1, '2025-03-30T03:00+02:00[Europe/Prague]'],
            ['2025-03-23T03:30+01:00[Europe/Prague]', 1, '2025-03-30T03:30+02:00[Europe/Prague]'],
        ];
    }

    #[DataProvider('providerPlusDays')]
    public function testPlusDays(string $zonedDateTime, int $plusDays, string $expected): void
    {
        $actual = ZonedDateTime::parse($zonedDateTime)->plusDays($plusDays);
        self::assertIs(ZonedDateTime::class, $expected, $actual);
    }

    public static function providerPlusDays(): array
    {
        return [
            ['2000-03-31T12:34:56.123456789-08:00[America/Los_Angeles]', -1, '2000-03-30T12:34:56.123456789-08:00[America/Los_Angeles]'],
            ['2000-03-31T12:34:56.123456789-08:00[America/Los_Angeles]', 0, '2000-03-31T12:34:56.123456789-08:00[America/Los_Angeles]'],
            ['2000-03-31T12:34:56.123456789-08:00[America/Los_Angeles]', 1, '2000-04-01T12:34:56.123456789-08:00[America/Los_Angeles]'],
            ['2000-03-31T12:34:56.123456789-08:00[America/Los_Angeles]', 2, '2000-04-02T12:34:56.123456789-07:00[America/Los_Angeles]'],
            ['2000-03-31T12:34:56.123456789-08:00[America/Los_Angeles]', 3, '2000-04-03T12:34:56.123456789-07:00[America/Los_Angeles]'],

            ['2000-04-03T12:34:56.123456789-07:00[America/Los_Angeles]', -1, '2000-04-02T12:34:56.123456789-07:00[America/Los_Angeles]'],
            ['2000-04-03T12:34:56.123456789-07:00[America/Los_Angeles]', -2, '2000-04-01T12:34:56.123456789-08:00[America/Los_Angeles]'],

            // https://github.com/brick/date-time/issues/115
            ['2025-03-29T01:00+01:00[Europe/Prague]', 1, '2025-03-30T01:00+01:00[Europe/Prague]'],
            ['2025-03-29T01:30+01:00[Europe/Prague]', 1, '2025-03-30T01:30+01:00[Europe/Prague]'],
            ['2025-03-29T02:00+01:00[Europe/Prague]', 1, '2025-03-30T03:00+02:00[Europe/Prague]'],
            ['2025-03-29T02:30+01:00[Europe/Prague]', 1, '2025-03-30T03:30+02:00[Europe/Prague]'],
            ['2025-03-29T03:00+01:00[Europe/Prague]', 1, '2025-03-30T03:00+02:00[Europe/Prague]'],
            ['2025-03-29T03:30+01:00[Europe/Prague]', 1, '2025-03-30T03:30+02:00[Europe/Prague]'],
        ];
    }

    #[DataProvider('providerPlusHours')]
    public function testPlusHours(string $zonedDateTime, int $plusHours, string $expected): void
    {
        $actual = ZonedDateTime::parse($zonedDateTime)->plusHours($plusHours);
        self::assertIs(ZonedDateTime::class, $expected, $actual);
    }

    public static function providerPlusHours(): array
    {
        return [
            ['2000-04-01T23:12:34.123456789-08:00[America/Los_Angeles]', -1, '2000-04-01T22:12:34.123456789-08:00[America/Los_Angeles]'],
            ['2000-04-01T23:12:34.123456789-08:00[America/Los_Angeles]', 0, '2000-04-01T23:12:34.123456789-08:00[America/Los_Angeles]'],
            ['2000-04-01T23:12:34.123456789-08:00[America/Los_Angeles]', 1, '2000-04-02T00:12:34.123456789-08:00[America/Los_Angeles]'],
            ['2000-04-01T23:12:34.123456789-08:00[America/Los_Angeles]', 2, '2000-04-02T01:12:34.123456789-08:00[America/Los_Angeles]'],
            ['2000-04-01T23:12:34.123456789-08:00[America/Los_Angeles]', 3, '2000-04-02T03:12:34.123456789-07:00[America/Los_Angeles]'],
            ['2000-04-01T23:12:34.123456789-08:00[America/Los_Angeles]', 4, '2000-04-02T04:12:34.123456789-07:00[America/Los_Angeles]'],

            ['2000-04-02T04:12:34.123456789-07:00[America/Los_Angeles]', -3, '2000-04-02T00:12:34.123456789-08:00[America/Los_Angeles]'],
            ['2000-04-02T04:12:34.123456789-07:00[America/Los_Angeles]', -2, '2000-04-02T01:12:34.123456789-08:00[America/Los_Angeles]'],
            ['2000-04-02T04:12:34.123456789-07:00[America/Los_Angeles]', -1, '2000-04-02T03:12:34.123456789-07:00[America/Los_Angeles]'],
            ['2000-04-02T04:12:34.123456789-07:00[America/Los_Angeles]', 0, '2000-04-02T04:12:34.123456789-07:00[America/Los_Angeles]'],
            ['2000-04-02T04:12:34.123456789-07:00[America/Los_Angeles]', 1, '2000-04-02T05:12:34.123456789-07:00[America/Los_Angeles]'],

            // https://github.com/brick/date-time/issues/115
            ['2025-03-29T01:00+01:00[Europe/Prague]', 24, '2025-03-30T01:00+01:00[Europe/Prague]'],
            ['2025-03-29T01:30+01:00[Europe/Prague]', 24, '2025-03-30T01:30+01:00[Europe/Prague]'],
            ['2025-03-29T02:00+01:00[Europe/Prague]', 24, '2025-03-30T03:00+02:00[Europe/Prague]'],
            ['2025-03-29T02:30+01:00[Europe/Prague]', 24, '2025-03-30T03:30+02:00[Europe/Prague]'],
            ['2025-03-29T03:00+01:00[Europe/Prague]', 24, '2025-03-30T04:00+02:00[Europe/Prague]'],
            ['2025-03-29T03:30+01:00[Europe/Prague]', 24, '2025-03-30T04:30+02:00[Europe/Prague]'],
        ];
    }

    #[DataProvider('providerPlusMinutes')]
    public static function testPlusMinutes(string $zonedDateTime, int $plusMinutes, string $expected): void
    {
        $actual = ZonedDateTime::parse($zonedDateTime)->plusMinutes($plusMinutes);
        self::assertIs(ZonedDateTime::class, $expected, $actual);
    }

    public static function providerPlusMinutes(): array
    {
        return [
            ['2000-04-02T01:59:59.654321-08:00[America/Los_Angeles]', -2, '2000-04-02T01:57:59.654321-08:00[America/Los_Angeles]'],
            ['2000-04-02T01:59:59.654321-08:00[America/Los_Angeles]', -1, '2000-04-02T01:58:59.654321-08:00[America/Los_Angeles]'],
            ['2000-04-02T01:59:59.654321-08:00[America/Los_Angeles]', 0, '2000-04-02T01:59:59.654321-08:00[America/Los_Angeles]'],
            ['2000-04-02T01:59:59.654321-08:00[America/Los_Angeles]', 1, '2000-04-02T03:00:59.654321-07:00[America/Los_Angeles]'],
            ['2000-04-02T01:59:59.654321-08:00[America/Los_Angeles]', 2, '2000-04-02T03:01:59.654321-07:00[America/Los_Angeles]'],
            ['2000-04-02T01:59:59.654321-08:00[America/Los_Angeles]', 3, '2000-04-02T03:02:59.654321-07:00[America/Los_Angeles]'],

            ['2000-04-02T03:01:01.654321-07:00[America/Los_Angeles]', -3, '2000-04-02T01:58:01.654321-08:00[America/Los_Angeles]'],
            ['2000-04-02T03:01:01.654321-07:00[America/Los_Angeles]', -2, '2000-04-02T01:59:01.654321-08:00[America/Los_Angeles]'],
            ['2000-04-02T03:01:01.654321-07:00[America/Los_Angeles]', -1, '2000-04-02T03:00:01.654321-07:00[America/Los_Angeles]'],
            ['2000-04-02T03:01:01.654321-07:00[America/Los_Angeles]', 0, '2000-04-02T03:01:01.654321-07:00[America/Los_Angeles]'],
            ['2000-04-02T03:01:01.654321-07:00[America/Los_Angeles]', 1, '2000-04-02T03:02:01.654321-07:00[America/Los_Angeles]'],

            // https://github.com/brick/date-time/issues/115
            ['2025-03-30T01:30+01:00[Europe/Prague]', 50, '2025-03-30T03:20+02:00[Europe/Prague]'],
            ['2025-03-30T01:30+01:00[Europe/Prague]', 100, '2025-03-30T04:10+02:00[Europe/Prague]'],
            ['2025-03-30T03:20+02:00[Europe/Prague]', -50, '2025-03-30T01:30+01:00[Europe/Prague]'],
            ['2025-03-30T04:10+02:00[Europe/Prague]', -100, '2025-03-30T01:30+01:00[Europe/Prague]'],
        ];
    }

    #[DataProvider('providerPlusSeconds')]
    public function testPlusSeconds(string $zonedDateTime, int $plusSeconds, string $expected): void
    {
        $actual = ZonedDateTime::parse($zonedDateTime)->plusSeconds($plusSeconds);
        self::assertIs(ZonedDateTime::class, $expected, $actual);
    }

    public static function providerPlusSeconds(): array
    {
        return [
            ['2000-04-02T01:59:59.654321-08:00[America/Los_Angeles]', -2, '2000-04-02T01:59:57.654321-08:00[America/Los_Angeles]'],
            ['2000-04-02T01:59:59.654321-08:00[America/Los_Angeles]', -1, '2000-04-02T01:59:58.654321-08:00[America/Los_Angeles]'],
            ['2000-04-02T01:59:59.654321-08:00[America/Los_Angeles]', 0, '2000-04-02T01:59:59.654321-08:00[America/Los_Angeles]'],
            ['2000-04-02T01:59:59.654321-08:00[America/Los_Angeles]', 1, '2000-04-02T03:00:00.654321-07:00[America/Los_Angeles]'],
            ['2000-04-02T01:59:59.654321-08:00[America/Los_Angeles]', 2, '2000-04-02T03:00:01.654321-07:00[America/Los_Angeles]'],

            ['2000-04-02T03:00:01.654321-07:00[America/Los_Angeles]', -3, '2000-04-02T01:59:58.654321-08:00[America/Los_Angeles]'],
            ['2000-04-02T03:00:01.654321-07:00[America/Los_Angeles]', -2, '2000-04-02T01:59:59.654321-08:00[America/Los_Angeles]'],
            ['2000-04-02T03:00:01.654321-07:00[America/Los_Angeles]', -1, '2000-04-02T03:00:00.654321-07:00[America/Los_Angeles]'],
            ['2000-04-02T03:00:01.654321-07:00[America/Los_Angeles]', 0, '2000-04-02T03:00:01.654321-07:00[America/Los_Angeles]'],
            ['2000-04-02T03:00:01.654321-07:00[America/Los_Angeles]', 1, '2000-04-02T03:00:02.654321-07:00[America/Los_Angeles]'],

            // https://github.com/brick/date-time/issues/115
            ['2025-03-30T01:30+01:00[Europe/Prague]', 3000, '2025-03-30T03:20+02:00[Europe/Prague]'],
            ['2025-03-30T01:30+01:00[Europe/Prague]', 6000, '2025-03-30T04:10+02:00[Europe/Prague]'],
            ['2025-03-30T03:20+02:00[Europe/Prague]', -3000, '2025-03-30T01:30+01:00[Europe/Prague]'],
            ['2025-03-30T04:10+02:00[Europe/Prague]', -6000, '2025-03-30T01:30+01:00[Europe/Prague]'],
        ];
    }

    #[DataProvider('providerPlusPeriod')]
    public function testMinusPeriod(string $dateTime, Period $plusPeriod, string $expected): void
    {
        $actual = ZonedDateTime::parse($dateTime)->minusPeriod($plusPeriod->negated());
        self::assertIs(ZonedDateTime::class, $expected, $actual);
    }

    #[DataProvider('providerPlusDuration')]
    public function testMinusDuration(string $dateTime, Duration $plusDuration, string $expected): void
    {
        $actual = ZonedDateTime::parse($dateTime)->minusDuration($plusDuration->negated());
        self::assertIs(ZonedDateTime::class, $expected, $actual);
    }

    #[DataProvider('providerPlusYears')]
    public function testMinusYears(string $dateTime, int $plusYears, string $expected): void
    {
        $actual = ZonedDateTime::parse($dateTime)->minusYears(-$plusYears);
        self::assertIs(ZonedDateTime::class, $expected, $actual);
    }

    #[DataProvider('providerPlusMonths')]
    public function testMinusMonths(string $dateTime, int $plusMonths, string $expected): void
    {
        $actual = ZonedDateTime::parse($dateTime)->minusMonths(-$plusMonths);
        self::assertIs(ZonedDateTime::class, $expected, $actual);
    }

    #[DataProvider('providerPlusWeeks')]
    public function testMinusWeeks(string $dateTime, int $plusWeeks, string $expected): void
    {
        $actual = ZonedDateTime::parse($dateTime)->minusWeeks(-$plusWeeks);
        self::assertIs(ZonedDateTime::class, $expected, $actual);
    }

    #[DataProvider('providerPlusDays')]
    public function testMinusDays(string $dateTime, int $plusDays, string $expected): void
    {
        $actual = ZonedDateTime::parse($dateTime)->minusDays(-$plusDays);
        self::assertIs(ZonedDateTime::class, $expected, $actual);
    }

    #[DataProvider('providerPlusHours')]
    public function testMinusHours(string $dateTime, int $plusHours, string $expected): void
    {
        $actual = ZonedDateTime::parse($dateTime)->minusHours(-$plusHours);
        self::assertIs(ZonedDateTime::class, $expected, $actual);
    }

    #[DataProvider('providerPlusMinutes')]
    public function testMinusMinutes(string $dateTime, int $plusMinutes, string $expected): void
    {
        $actual = ZonedDateTime::parse($dateTime)->minusMinutes(-$plusMinutes);
        self::assertIs(ZonedDateTime::class, $expected, $actual);
    }

    #[DataProvider('providerPlusSeconds')]
    public function testMinusSeconds(string $dateTime, int $plusSeconds, string $expected): void
    {
        $actual = ZonedDateTime::parse($dateTime)->minusSeconds(-$plusSeconds);
        self::assertIs(ZonedDateTime::class, $expected, $actual);
    }

    public function testIsBetweenInclusive(): void
    {
        $timeZone = TimeZone::parse('America/Los_Angeles');
        $localDateTime = '2000-01-20T12:34:56.123456789';
        $localDateTime = LocalDateTime::parse($localDateTime);
        $fromZonedDateTime = ZonedDateTime::of($localDateTime, $timeZone);

        $localDateTime = '2015-01-20T12:34:56.123456789';
        $localDateTime = LocalDateTime::parse($localDateTime);
        $toZonedDateTime = ZonedDateTime::of($localDateTime, $timeZone);

        $localDateTime = '2018-01-20T12:34:56.123456789';
        $localDateTime = LocalDateTime::parse($localDateTime);
        $notIncluZonedDateTime = ZonedDateTime::of($localDateTime, $timeZone);

        self::assertTrue($fromZonedDateTime->isBetweenInclusive($fromZonedDateTime, $toZonedDateTime));
        self::assertFalse($fromZonedDateTime->isBetweenInclusive($toZonedDateTime, $notIncluZonedDateTime));
    }

    public function testIsBetweenExclusive(): void
    {
        $timeZone = TimeZone::parse('America/Los_Angeles');
        $localDateTime = '2000-01-20T12:34:56.123456789';
        $localDateTime = LocalDateTime::parse($localDateTime);
        $fromZonedDateTime = ZonedDateTime::of($localDateTime, $timeZone);

        $localDateTime = '2015-01-20T12:34:56.123456789';
        $localDateTime = LocalDateTime::parse($localDateTime);
        $toZonedDateTime = ZonedDateTime::of($localDateTime, $timeZone);

        $localDateTime = '2014-01-20T12:34:56.123456789';
        $localDateTime = LocalDateTime::parse($localDateTime);
        $incluZonedDateTime = ZonedDateTime::of($localDateTime, $timeZone);

        self::assertTrue($incluZonedDateTime->isBetweenExclusive($fromZonedDateTime, $toZonedDateTime));
        self::assertFalse($fromZonedDateTime->isBetweenExclusive($fromZonedDateTime, $toZonedDateTime));
    }

    #[DataProvider('providerForPastFuture')]
    public function testIsFuture(int $clockTimestamp, string $zonedDateTime, bool $isFuture): void
    {
        $clock = new FixedClock(Instant::of($clockTimestamp));
        $zonedDateTime = ZonedDateTime::parse($zonedDateTime);
        self::assertSame($isFuture, $zonedDateTime->isFuture($clock));
    }

    #[DataProvider('providerForPastFuture')]
    public function testIsPast(int $clockTimestamp, string $zonedDateTime, bool $isFuture): void
    {
        $clock = new FixedClock(Instant::of($clockTimestamp));
        $zonedDateTime = ZonedDateTime::parse($zonedDateTime);
        self::assertSame(! $isFuture, $zonedDateTime->isPast($clock));
    }

    public static function providerForPastFuture(): array
    {
        return [
            [1234567890, '2009-02-14T00:31:29+01:00', false],
            [1234567890, '2009-02-14T00:31:31+01:00', true],
            [2345678901, '2044-04-30T17:28:20-08:00', false],
            [2345678901, '2044-04-30T17:28:22-08:00', true],
        ];
    }

    /**
     * @param string $dateTime The date-time string that will be parse()d by ZonedDateTime.
     * @param string $expected The expected output from the native DateTime object.
     */
    #[DataProvider('providerToNativeDateTime')]
    public function testToNativeDateTime(string $dateTime, string $expected): void
    {
        $zonedDateTime = ZonedDateTime::parse($dateTime);
        $dateTime = $zonedDateTime->toNativeDateTime();

        self::assertInstanceOf(DateTime::class, $dateTime);
        self::assertSame($expected, $dateTime->format('Y-m-d\TH:i:s.uO'));
    }

    /**
     * @param string $dateTime The date-time string that will be parse()d by ZonedDateTime.
     * @param string $expected The expected output from the native DateTime object.
     */
    #[DataProvider('providerToNativeDateTime')]
    public function testToNativeDateTimeImmutable(string $dateTime, string $expected): void
    {
        $zonedDateTime = ZonedDateTime::parse($dateTime);
        $dateTime = $zonedDateTime->toNativeDateTimeImmutable();

        self::assertInstanceOf(DateTimeImmutable::class, $dateTime);
        self::assertSame($expected, $dateTime->format('Y-m-d\TH:i:s.uO'));
    }

    public static function providerToNativeDateTime(): array
    {
        return [
            ['2018-10-18T12:34Z',                        '2018-10-18T12:34:00.000000+0000'],
            ['2018-10-18T12:34:56Z',                     '2018-10-18T12:34:56.000000+0000'],
            ['2018-10-18T12:34:00.001Z',                 '2018-10-18T12:34:00.001000+0000'],
            ['2018-10-18T12:34:56.123002Z',              '2018-10-18T12:34:56.123002+0000'],
            ['2011-07-31T23:59:59+01:00',                '2011-07-31T23:59:59.000000+0100'],
            ['2011-07-31T23:59:59.02-05:30',             '2011-07-31T23:59:59.020000-0530'],
            ['2011-07-31T23:59:59+01:00[Europe/London]', '2011-07-31T23:59:59.000000+0100'],
            ['2011-07-31T23:59:59.000123456-07:00',      '2011-07-31T23:59:59.000123-0700'],
        ];
    }

    #[DataProvider('providerToString')]
    public function testJsonSerialize(string $localDateTime, string $timeZone, string $expectedString): void
    {
        $localDateTime = LocalDateTime::parse($localDateTime);
        $zonedDateTime = ZonedDateTime::of($localDateTime, TimeZone::parse($timeZone));

        self::assertSame(json_encode($expectedString, JSON_THROW_ON_ERROR), json_encode($zonedDateTime, JSON_THROW_ON_ERROR));
    }

    #[DataProvider('providerToString')]
    public function testToISOString(string $localDateTime, string $timeZone, string $expectedString): void
    {
        $localDateTime = LocalDateTime::parse($localDateTime);
        $zonedDateTime = ZonedDateTime::of($localDateTime, TimeZone::parse($timeZone));

        self::assertSame($expectedString, $zonedDateTime->toISOString());
    }

    #[DataProvider('providerToString')]
    public function testToString(string $localDateTime, string $timeZone, string $expectedString): void
    {
        $localDateTime = LocalDateTime::parse($localDateTime);
        $zonedDateTime = ZonedDateTime::of($localDateTime, TimeZone::parse($timeZone));

        self::assertSame($expectedString, (string) $zonedDateTime);
    }

    public static function providerToString(): array
    {
        return [
            ['2000-01-20T12:34:56.123456789', 'America/Los_Angeles', '2000-01-20T12:34:56.123456789-08:00[America/Los_Angeles]'],
            ['2000-01-20T12:34:56.123456789', '-07:00', '2000-01-20T12:34:56.123456789-07:00'],
        ];
    }

    #[DataProvider('providerGetDurationTo')]
    public function testGetDurationTo(string $firstDate, string $secondDate, int $expectedSeconds, int $expectedNanos): void
    {
        $actualResult = ZonedDateTime::parse($firstDate)->getDurationTo(ZonedDateTime::parse($secondDate));

        self::assertDurationIs($expectedSeconds, $expectedNanos, $actualResult);
    }

    public static function providerGetDurationTo(): array
    {
        return [
            ['2023-01-01T10:00:00Z',           '2023-01-01T10:00:00Z',           0, 0],
            ['2023-01-01T10:00:00Z',           '2023-01-01T10:00:10Z',           10, 0],
            ['2023-01-01T10:00:00.001Z',       '2023-01-01T10:00:10.002Z',       10, 1000000],
            ['2023-01-01T10:00:00.001Z',       '2023-01-01T13:00:10.002+03:00',  10, 1000000],
            ['2023-01-01T10:00:00.000000001Z', '2023-01-01T10:00:00.000000009Z', 0, 8],
            ['2023-01-01T10:00:00Z',           '2023-01-02T10:00:00Z',           24 * 60 * 60, 0],
            ['2023-01-02T10:00:00Z',           '2023-01-01T10:00:00Z',           -24 * 60 * 60, 0],
        ];
    }

    #[DataProvider('providerGetIntervalTo')]
    public function testGetIntervalTo(string $firstDate, string $secondDate, string $expectedInterval): void
    {
        $actualResult = ZonedDateTime::parse($firstDate)->getIntervalTo(ZonedDateTime::parse($secondDate));

        self::assertSame($expectedInterval, (string) $actualResult);
    }

    public static function providerGetIntervalTo(): array
    {
        return [
            ['2023-01-01T10:00:00Z',           '2023-01-01T10:00:00Z',           '2023-01-01T10:00Z/2023-01-01T10:00Z'],
            ['2023-01-01T10:00:00Z',           '2023-01-01T10:00:10Z',           '2023-01-01T10:00Z/2023-01-01T10:00:10Z'],
            ['2023-01-01T10:00:00.001Z',       '2023-01-01T10:00:10.002Z',       '2023-01-01T10:00:00.001Z/2023-01-01T10:00:10.002Z'],
            ['2023-01-01T10:00:00.001Z',       '2023-01-01T13:00:10.002+03:00',  '2023-01-01T10:00:00.001Z/2023-01-01T10:00:10.002Z'],
            ['2023-01-01T10:00:00.001+03:00',  '2023-01-01T13:00:10.002+03:00',  '2023-01-01T07:00:00.001Z/2023-01-01T10:00:10.002Z'],
            ['2023-01-01T10:00:00.000000001Z', '2023-01-01T10:00:00.000000009Z', '2023-01-01T10:00:00.000000001Z/2023-01-01T10:00:00.000000009Z'],
            ['2023-01-01T10:00:00Z',           '2023-01-02T10:00:00Z',           '2023-01-01T10:00Z/2023-01-02T10:00Z'],
        ];
    }

    private function getTestZonedDateTime(): ZonedDateTime
    {
        $timeZone = TimeZone::parse('America/Los_Angeles');
        $localDateTime = LocalDateTime::parse('2000-01-20T12:34:56.123456789');

        return ZonedDateTime::of($localDateTime, $timeZone);
    }
}
