<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\Intl\Tests\DateFormatter;

use PHPUnit\Framework\TestCase;
use Symfony\Component\Intl\DateFormatter\IntlDateFormatter;
use Symfony\Component\Intl\Globals\IntlGlobals;
use Symfony\Component\Intl\Intl;
use Symfony\Component\Intl\Util\IcuVersion;

/**
 * Test case for IntlDateFormatter implementations.
 *
 * @author Bernhard Schussek <bschussek@gmail.com>
 */
abstract class AbstractIntlDateFormatterTest extends TestCase
{
    protected function setUp()
    {
        \Locale::setDefault('en');
    }

    /**
     * When a time zone is not specified, it uses the system default however it returns null in the getter method.
     *
     * @see StubIntlDateFormatterTest::testDefaultTimeZoneIntl()
     */
    public function testConstructorDefaultTimeZone()
    {
        $formatter = $this->getDateFormatter('en', IntlDateFormatter::MEDIUM, IntlDateFormatter::SHORT);

        $this->assertEquals(date_default_timezone_get(), $formatter->getTimeZoneId());

        $this->assertEquals(
            $this->getDateTime(0, $formatter->getTimeZoneId())->format('M j, Y, g:i A'),
            $formatter->format(0)
        );
    }

    public function testConstructorWithoutDateType()
    {
        $formatter = new IntlDateFormatter('en', null, IntlDateFormatter::SHORT, 'UTC', IntlDateFormatter::GREGORIAN);

        $this->assertSame('EEEE, LLLL d, y, h:mm a', $formatter->getPattern());
    }

    public function testConstructorWithoutTimeType()
    {
        $formatter = new IntlDateFormatter('en', IntlDateFormatter::SHORT, null, 'UTC', IntlDateFormatter::GREGORIAN);

        $this->assertSame('M/d/yy, h:mm:ss a zzzz', $formatter->getPattern());
    }

    /**
     * @dataProvider formatProvider
     */
    public function testFormat($pattern, $timestamp, $expected)
    {
        $errorCode = IntlGlobals::U_ZERO_ERROR;
        $errorMessage = 'U_ZERO_ERROR';

        $formatter = $this->getDefaultDateFormatter($pattern);
        $this->assertSame($expected, $formatter->format($timestamp));
        $this->assertIsIntlSuccess($formatter, $errorMessage, $errorCode);
    }

    public function formatProvider()
    {
        $dateTime = new \DateTime('@0');

        $formatData = [
            /* general */
            ['y-M-d', 0, '1970-1-1'],
            ["EEE, MMM d, ''yy", 0, "Thu, Jan 1, '70"],
            ['h:mm a', 0, '12:00 AM'],
            ['yyyyy.MMMM.dd hh:mm aaa', 0, '01970.January.01 12:00 AM'],

            /* escaping */
            ["'M'", 0, 'M'],
            ["'yy'", 0, 'yy'],
            ["'''yy'", 0, "'yy"],
            ["''y", 0, "'1970"],
            ["''yy", 0, "'70"],
            ["H 'o'' clock'", 0, "0 o' clock"],

            /* month */
            ['M', 0, '1'],
            ['MM', 0, '01'],
            ['MMM', 0, 'Jan'],
            ['MMMM', 0, 'January'],
            ['MMMMM', 0, 'J'],
            ['MMMMMM', 0, '000001'],

            ['L', 0, '1'],
            ['LL', 0, '01'],
            ['LLL', 0, 'Jan'],
            ['LLLL', 0, 'January'],
            ['LLLLL', 0, 'J'],
            ['LLLLLL', 0, '000001'],

            /* year */
            ['y', 0, '1970'],
            ['yy', 0, '70'],
            ['yyy', 0, '1970'],
            ['yyyy', 0, '1970'],
            ['yyyyy', 0, '01970'],
            ['yyyyyy', 0, '001970'],

            /* day */
            ['d', 0, '1'],
            ['dd', 0, '01'],
            ['ddd', 0, '001'],

            /* quarter */
            ['Q', 0, '1'],
            ['QQ', 0, '01'],
            ['QQQ', 0, 'Q1'],
            ['QQQQ', 0, '1st quarter'],
            ['QQQQQ', 0, '1st quarter'],

            ['q', 0, '1'],
            ['qq', 0, '01'],
            ['qqq', 0, 'Q1'],
            ['qqqq', 0, '1st quarter'],
            ['qqqqq', 0, '1st quarter'],

            // 4 months
            ['Q', 7776000, '2'],
            ['QQ', 7776000, '02'],
            ['QQQ', 7776000, 'Q2'],
            ['QQQQ', 7776000, '2nd quarter'],

            // 7 months
            ['QQQQ', 15638400, '3rd quarter'],

            // 10 months
            ['QQQQ', 23587200, '4th quarter'],

            /* 12-hour (1-12) */
            ['h', 0, '12'],
            ['hh', 0, '12'],
            ['hhh', 0, '012'],

            ['h', 1, '12'],
            ['h', 3600, '1'],
            ['h', 43200, '12'], // 12 hours

            /* day of year */
            ['D', 0, '1'],
            ['D', 86400, '2'], // 1 day
            ['D', 31536000, '1'], // 1 year
            ['D', 31622400, '2'], // 1 year + 1 day

            /* day of week */
            ['E', 0, 'Thu'],
            ['EE', 0, 'Thu'],
            ['EEE', 0, 'Thu'],
            ['EEEE', 0, 'Thursday'],
            ['EEEEE', 0, 'T'],
            ['EEEEEE', 0, 'Th'],

            ['E', 1296540000, 'Tue'], // 2011-02-01
            ['E', 1296950400, 'Sun'], // 2011-02-06

            /* am/pm marker */
            ['a', 0, 'AM'],
            ['aa', 0, 'AM'],
            ['aaa', 0, 'AM'],
            ['aaaa', 0, 'AM'],

            // 12 hours
            ['a', 43200, 'PM'],
            ['aa', 43200, 'PM'],
            ['aaa', 43200, 'PM'],
            ['aaaa', 43200, 'PM'],

            /* 24-hour (0-23) */
            ['H', 0, '0'],
            ['HH', 0, '00'],
            ['HHH', 0, '000'],

            ['H', 1, '0'],
            ['H', 3600, '1'],
            ['H', 43200, '12'],
            ['H', 46800, '13'],

            /* 24-hour (1-24) */
            ['k', 0, '24'],
            ['kk', 0, '24'],
            ['kkk', 0, '024'],

            ['k', 1, '24'],
            ['k', 3600, '1'],
            ['k', 43200, '12'],
            ['k', 46800, '13'],

            /* 12-hour (0-11) */
            ['K', 0, '0'],
            ['KK', 0, '00'],
            ['KKK', 0, '000'],

            ['K', 1, '0'],
            ['K', 3600, '1'],
            ['K', 43200, '0'], // 12 hours

            /* minute */
            ['m', 0, '0'],
            ['mm', 0, '00'],
            ['mmm', 0, '000'],

            ['m', 1, '0'],
            ['m', 60, '1'],
            ['m', 120, '2'],
            ['m', 180, '3'],
            ['m', 3600, '0'],
            ['m', 3660, '1'],
            ['m', 43200, '0'], // 12 hours

            /* second */
            ['s', 0, '0'],
            ['ss', 0, '00'],
            ['sss', 0, '000'],

            ['s', 1, '1'],
            ['s', 2, '2'],
            ['s', 5, '5'],
            ['s', 30, '30'],
            ['s', 59, '59'],
            ['s', 60, '0'],
            ['s', 120, '0'],
            ['s', 180, '0'],
            ['s', 3600, '0'],
            ['s', 3601, '1'],
            ['s', 3630, '30'],
            ['s', 43200, '0'], // 12 hours
        ];

        /* general, DateTime */
        $formatData[] = ['y-M-d', $dateTime, '1970-1-1'];
        $formatData[] = ["EEE, MMM d, ''yy", $dateTime, "Thu, Jan 1, '70"];
        $formatData[] = ['h:mm a', $dateTime, '12:00 AM'];
        $formatData[] = ['yyyyy.MMMM.dd hh:mm aaa', $dateTime, '01970.January.01 12:00 AM'];

        if (IcuVersion::compare(Intl::getIcuVersion(), '59.1', '>=', 1)) {
            // Before ICU 59.1 GMT was used instead of UTC
            $formatData[] = ["yyyy.MM.dd 'at' HH:mm:ss zzz", 0, '1970.01.01 at 00:00:00 UTC'];
            $formatData[] = ['K:mm a, z', 0, '0:00 AM, UTC'];
            $formatData[] = ["yyyy.MM.dd 'at' HH:mm:ss zzz", $dateTime, '1970.01.01 at 00:00:00 UTC'];
            $formatData[] = ['K:mm a, z', $dateTime, '0:00 AM, UTC'];
        }

        return $formatData;
    }

    public function testFormatUtcAndGmtAreSplit()
    {
        $pattern = "yyyy.MM.dd 'at' HH:mm:ss zzz";
        $gmtFormatter = $this->getDateFormatter('en', IntlDateFormatter::MEDIUM, IntlDateFormatter::SHORT, 'GMT', IntlDateFormatter::GREGORIAN, $pattern);
        $utcFormatter = $this->getDateFormatter('en', IntlDateFormatter::MEDIUM, IntlDateFormatter::SHORT, 'UTC', IntlDateFormatter::GREGORIAN, $pattern);

        $this->assertSame('1970.01.01 at 00:00:00 GMT', $gmtFormatter->format(new \DateTime('@0')));
        $this->assertSame('1970.01.01 at 00:00:00 UTC', $utcFormatter->format(new \DateTime('@0')));
    }

    /**
     * @dataProvider formatErrorProvider
     */
    public function testFormatIllegalArgumentError($pattern, $timestamp, $errorMessage)
    {
        $errorCode = IntlGlobals::U_ILLEGAL_ARGUMENT_ERROR;

        $formatter = $this->getDefaultDateFormatter($pattern);
        $this->assertFalse($formatter->format($timestamp));
        $this->assertIsIntlFailure($formatter, $errorMessage, $errorCode);
    }

    public function formatErrorProvider()
    {
        return [
            ['y-M-d', 'foobar', 'datefmt_format: string \'foobar\' is not numeric, which would be required for it to be a valid date: U_ILLEGAL_ARGUMENT_ERROR'],
        ];
    }

    /**
     * @dataProvider formatWithTimezoneProvider
     */
    public function testFormatWithTimezone($timestamp, $timezone, $expected)
    {
        $pattern = 'yyyy-MM-dd HH:mm:ss';
        $formatter = $this->getDateFormatter('en', IntlDateFormatter::MEDIUM, IntlDateFormatter::SHORT, $timezone, IntlDateFormatter::GREGORIAN, $pattern);
        $this->assertSame($expected, $formatter->format($timestamp));
    }

    public function formatWithTimezoneProvider()
    {
        $data = [
            [0, 'UTC', '1970-01-01 00:00:00'],
            [0, 'GMT', '1970-01-01 00:00:00'],
            [0, 'GMT-03:00', '1969-12-31 21:00:00'],
            [0, 'GMT+03:00', '1970-01-01 03:00:00'],
            [0, 'Europe/Zurich', '1970-01-01 01:00:00'],
            [0, 'Europe/Paris', '1970-01-01 01:00:00'],
            [0, 'Africa/Cairo', '1970-01-01 02:00:00'],
            [0, 'Africa/Casablanca', '1970-01-01 00:00:00'],
            [0, 'Africa/Djibouti', '1970-01-01 03:00:00'],
            [0, 'Africa/Johannesburg', '1970-01-01 02:00:00'],
            [0, 'America/Antigua', '1969-12-31 20:00:00'],
            [0, 'America/Toronto', '1969-12-31 19:00:00'],
            [0, 'America/Vancouver', '1969-12-31 16:00:00'],
            [0, 'Asia/Aqtau', '1970-01-01 05:00:00'],
            [0, 'Asia/Bangkok', '1970-01-01 07:00:00'],
            [0, 'Asia/Dubai', '1970-01-01 04:00:00'],
            [0, 'Australia/Brisbane', '1970-01-01 10:00:00'],
            [0, 'Australia/Eucla', '1970-01-01 08:45:00'],
            [0, 'Australia/Melbourne', '1970-01-01 10:00:00'],
            [0, 'Europe/Berlin', '1970-01-01 01:00:00'],
            [0, 'Europe/Dublin', '1970-01-01 01:00:00'],
            [0, 'Europe/Warsaw', '1970-01-01 01:00:00'],
            [0, 'Pacific/Fiji', '1970-01-01 12:00:00'],
        ];

        return $data;
    }

    /**
     * @dataProvider formatTimezoneProvider
     */
    public function testFormatTimezone($pattern, $timezone, $expected)
    {
        $formatter = $this->getDefaultDateFormatter($pattern);
        $formatter->setTimeZone(new \DateTimeZone($timezone));

        $this->assertEquals($expected, $formatter->format(0));
    }

    public function formatTimezoneProvider()
    {
        return [
            ['z', 'GMT', 'GMT'],
            ['zz', 'GMT', 'GMT'],
            ['zzz', 'GMT', 'GMT'],
            ['zzzz', 'GMT', 'Greenwich Mean Time'],
            ['zzzzz', 'GMT', 'Greenwich Mean Time'],

            ['z', 'Etc/GMT', 'GMT'],
            ['zz', 'Etc/GMT', 'GMT'],
            ['zzz', 'Etc/GMT', 'GMT'],
            ['zzzz', 'Etc/GMT', 'Greenwich Mean Time'],
            ['zzzzz', 'Etc/GMT', 'Greenwich Mean Time'],

            ['z', 'Etc/GMT+3', 'GMT-3'],
            ['zz', 'Etc/GMT+3', 'GMT-3'],
            ['zzz', 'Etc/GMT+3', 'GMT-3'],
            ['zzzz', 'Etc/GMT+3', 'GMT-03:00'],
            ['zzzzz', 'Etc/GMT+3', 'GMT-03:00'],

            ['z', 'UTC', 'UTC'],
            ['zz', 'UTC', 'UTC'],
            ['zzz', 'UTC', 'UTC'],
            ['zzzz', 'UTC', 'Coordinated Universal Time'],
            ['zzzzz', 'UTC', 'Coordinated Universal Time'],

            ['z', 'Etc/UTC', 'UTC'],
            ['zz', 'Etc/UTC', 'UTC'],
            ['zzz', 'Etc/UTC', 'UTC'],
            ['zzzz', 'Etc/UTC', 'Coordinated Universal Time'],
            ['zzzzz', 'Etc/UTC', 'Coordinated Universal Time'],

            ['z', 'Etc/Universal', 'UTC'],
            ['z', 'Etc/Zulu', 'UTC'],
            ['z', 'Etc/UCT', 'UTC'],
            ['z', 'Etc/Greenwich', 'GMT'],
            ['zzzzz', 'Etc/Universal', 'Coordinated Universal Time'],
            ['zzzzz', 'Etc/Zulu', 'Coordinated Universal Time'],
            ['zzzzz', 'Etc/UCT', 'Coordinated Universal Time'],
            ['zzzzz', 'Etc/Greenwich', 'Greenwich Mean Time'],

            ['z', 'GMT+03:00', 'GMT+3'],
            ['zz', 'GMT+03:00', 'GMT+3'],
            ['zzz', 'GMT+03:00', 'GMT+3'],
            ['zzzz', 'GMT+03:00', 'GMT+03:00'],
            ['zzzzz', 'GMT+03:00', 'GMT+03:00'],
        ];
    }

    public function testFormatWithGmtTimezone()
    {
        $formatter = $this->getDefaultDateFormatter('zzzz');

        $formatter->setTimeZone('GMT+03:00');

        $this->assertEquals('GMT+03:00', $formatter->format(0));
    }

    public function testFormatWithGmtTimeZoneAndMinutesOffset()
    {
        $formatter = $this->getDefaultDateFormatter('zzzz');

        $formatter->setTimeZone('GMT+00:30');

        $this->assertEquals('GMT+00:30', $formatter->format(0));
    }

    public function testFormatWithNonStandardTimezone()
    {
        $formatter = $this->getDefaultDateFormatter('zzzz');

        $formatter->setTimeZone('Pacific/Fiji');

        $this->assertEquals('Fiji Standard Time', $formatter->format(0));
    }

    public function testFormatWithConstructorTimezone()
    {
        $formatter = $this->getDateFormatter('en', IntlDateFormatter::MEDIUM, IntlDateFormatter::SHORT, 'UTC');
        $formatter->setPattern('yyyy-MM-dd HH:mm:ss');

        $this->assertEquals(
            $this->getDateTime(0, 'UTC')->format('Y-m-d H:i:s'),
            $formatter->format(0)
        );
    }

    public function testFormatWithDateTimeZoneGmt()
    {
        $formatter = $this->getDateFormatter('en', IntlDateFormatter::MEDIUM, IntlDateFormatter::SHORT, new \DateTimeZone('GMT'), IntlDateFormatter::GREGORIAN, 'zzz');

        $this->assertEquals('GMT', $formatter->format(0));
    }

    public function testFormatWithDateTimeZoneGmtOffset()
    {
        $formatter = $this->getDateFormatter('en', IntlDateFormatter::MEDIUM, IntlDateFormatter::SHORT, new \DateTimeZone('GMT+03:00'), IntlDateFormatter::GREGORIAN, 'zzzz');

        $this->assertEquals('GMT+03:00', $formatter->format(0));
    }

    public function testFormatWithIntlTimeZone()
    {
        if (!\extension_loaded('intl')) {
            $this->markTestSkipped('Extension intl is required.');
        }

        $formatter = $this->getDateFormatter('en', IntlDateFormatter::MEDIUM, IntlDateFormatter::SHORT, \IntlTimeZone::createTimeZone('GMT+03:00'), IntlDateFormatter::GREGORIAN, 'zzzz');

        $this->assertEquals('GMT+03:00', $formatter->format(0));
    }

    public function testFormatWithTimezoneFromPhp()
    {
        $tz = date_default_timezone_get();
        date_default_timezone_set('Europe/London');

        $formatter = $this->getDateFormatter('en', IntlDateFormatter::MEDIUM, IntlDateFormatter::SHORT);
        $formatter->setPattern('yyyy-MM-dd HH:mm:ss');

        $this->assertEquals(
            $this->getDateTime(0, 'Europe/London')->format('Y-m-d H:i:s'),
            $formatter->format(0)
        );

        $this->assertEquals('Europe/London', date_default_timezone_get());

        // Restores TZ.
        date_default_timezone_set($tz);
    }

    /**
     * @dataProvider dateAndTimeTypeProvider
     */
    public function testDateAndTimeType($timestamp, $datetype, $timetype, $expected)
    {
        $formatter = $this->getDateFormatter('en', $datetype, $timetype, 'UTC');
        $this->assertSame($expected, $formatter->format($timestamp));
    }

    public function dateAndTimeTypeProvider()
    {
        return [
            [0, IntlDateFormatter::FULL, IntlDateFormatter::NONE, 'Thursday, January 1, 1970'],
            [0, IntlDateFormatter::LONG, IntlDateFormatter::NONE, 'January 1, 1970'],
            [0, IntlDateFormatter::MEDIUM, IntlDateFormatter::NONE, 'Jan 1, 1970'],
            [0, IntlDateFormatter::SHORT, IntlDateFormatter::NONE, '1/1/70'],
            [0, IntlDateFormatter::NONE, IntlDateFormatter::FULL, '12:00:00 AM Coordinated Universal Time'],
            [0, IntlDateFormatter::NONE, IntlDateFormatter::LONG, '12:00:00 AM UTC'],
            [0, IntlDateFormatter::NONE, IntlDateFormatter::MEDIUM, '12:00:00 AM'],
            [0, IntlDateFormatter::NONE, IntlDateFormatter::SHORT, '12:00 AM'],
        ];
    }

    public function testGetCalendar()
    {
        $formatter = $this->getDefaultDateFormatter();
        $this->assertEquals(IntlDateFormatter::GREGORIAN, $formatter->getCalendar());
    }

    public function testGetDateType()
    {
        $formatter = $this->getDateFormatter('en', IntlDateFormatter::FULL, IntlDateFormatter::NONE);
        $this->assertEquals(IntlDateFormatter::FULL, $formatter->getDateType());
    }

    public function testGetLocale()
    {
        $formatter = $this->getDefaultDateFormatter();
        $this->assertEquals('en', $formatter->getLocale());
    }

    public function testGetPattern()
    {
        $formatter = $this->getDateFormatter('en', IntlDateFormatter::FULL, IntlDateFormatter::NONE, 'UTC', IntlDateFormatter::GREGORIAN, 'yyyy-MM-dd');
        $this->assertEquals('yyyy-MM-dd', $formatter->getPattern());
    }

    public function testGetTimeType()
    {
        $formatter = $this->getDateFormatter('en', IntlDateFormatter::NONE, IntlDateFormatter::FULL);
        $this->assertEquals(IntlDateFormatter::FULL, $formatter->getTimeType());
    }

    /**
     * @dataProvider parseProvider
     */
    public function testParse($pattern, $value, $expected)
    {
        $errorCode = IntlGlobals::U_ZERO_ERROR;
        $errorMessage = 'U_ZERO_ERROR';

        $formatter = $this->getDefaultDateFormatter($pattern);
        $this->assertSame($expected, $formatter->parse($value));
        $this->assertIsIntlSuccess($formatter, $errorMessage, $errorCode);
    }

    public function parseProvider()
    {
        return array_merge(
            $this->parseYearProvider(),
            $this->parseQuarterProvider(),
            $this->parseMonthProvider(),
            $this->parseStandaloneMonthProvider(),
            $this->parseDayProvider(),
            $this->parseDayOfWeekProvider(),
            $this->parseDayOfYearProvider(),
            $this->parseHour12ClockOneBasedProvider(),
            $this->parseHour12ClockZeroBasedProvider(),
            $this->parseHour24ClockOneBasedProvider(),
            $this->parseHour24ClockZeroBasedProvider(),
            $this->parseMinuteProvider(),
            $this->parseSecondProvider(),
            $this->parseTimezoneProvider(),
            $this->parseAmPmProvider(),
            $this->parseStandaloneAmPmProvider(),
            $this->parseRegexMetaCharsProvider(),
            $this->parseQuoteCharsProvider(),
            $this->parseDashSlashProvider()
        );
    }

    public function parseYearProvider()
    {
        return [
            ['y-M-d', '1970-1-1', 0],
            ['yy-M-d', '70-1-1', 0],
        ];
    }

    public function parseQuarterProvider()
    {
        return [
            ['Q', '1', 0],
            ['QQ', '01', 0],
            ['QQQ', 'Q1', 0],
            ['QQQQ', '1st quarter', 0],
            ['QQQQQ', '1st quarter', 0],

            ['Q', '2', 7776000],
            ['QQ', '02', 7776000],
            ['QQQ', 'Q2', 7776000],
            ['QQQQ', '2nd quarter', 7776000],
            ['QQQQQ', '2nd quarter', 7776000],

            ['q', '1', 0],
            ['qq', '01', 0],
            ['qqq', 'Q1', 0],
            ['qqqq', '1st quarter', 0],
            ['qqqqq', '1st quarter', 0],
        ];
    }

    public function parseMonthProvider()
    {
        return [
            ['y-M-d', '1970-1-1', 0],
            ['y-MMM-d', '1970-Jan-1', 0],
            ['y-MMMM-d', '1970-January-1', 0],
        ];
    }

    public function parseStandaloneMonthProvider()
    {
        return [
            ['y-L-d', '1970-1-1', 0],
            ['y-LLL-d', '1970-Jan-1', 0],
            ['y-LLLL-d', '1970-January-1', 0],
        ];
    }

    public function parseDayProvider()
    {
        return [
            ['y-M-d', '1970-1-1', 0],
            ['y-M-dd', '1970-1-01', 0],
            ['y-M-ddd', '1970-1-001', 0],
        ];
    }

    public function parseDayOfWeekProvider()
    {
        return [
            ['E', 'Thu', 0],
            ['EE', 'Thu', 0],
            ['EEE', 'Thu', 0],
            ['EEEE', 'Thursday', 0],
            ['EEEEE', 'T', 432000],
            ['EEEEEE', 'Th', 0],
        ];
    }

    public function parseDayOfYearProvider()
    {
        return [
            ['D', '1', 0],
            ['D', '2', 86400],
        ];
    }

    public function parseHour12ClockOneBasedProvider()
    {
        return [
            // 12 hours (1-12)
            ['y-M-d h', '1970-1-1 1', 3600],
            ['y-M-d h', '1970-1-1 10', 36000],
            ['y-M-d hh', '1970-1-1 11', 39600],
            ['y-M-d hh', '1970-1-1 12', 0],
            ['y-M-d hh a', '1970-1-1 0 AM', 0],
            ['y-M-d hh a', '1970-1-1 1 AM', 3600],
            ['y-M-d hh a', '1970-1-1 10 AM', 36000],
            ['y-M-d hh a', '1970-1-1 11 AM', 39600],
            ['y-M-d hh a', '1970-1-1 12 AM', 0],
            ['y-M-d hh a', '1970-1-1 23 AM', 82800],
            ['y-M-d hh a', '1970-1-1 24 AM', 86400],
            ['y-M-d hh a', '1970-1-1 0 PM', 43200],
            ['y-M-d hh a', '1970-1-1 1 PM', 46800],
            ['y-M-d hh a', '1970-1-1 10 PM', 79200],
            ['y-M-d hh a', '1970-1-1 11 PM', 82800],
            ['y-M-d hh a', '1970-1-1 12 PM', 43200],
            ['y-M-d hh a', '1970-1-1 23 PM', 126000],
            ['y-M-d hh a', '1970-1-1 24 PM', 129600],
        ];
    }

    public function parseHour12ClockZeroBasedProvider()
    {
        return [
            // 12 hours (0-11)
            ['y-M-d K', '1970-1-1 1', 3600],
            ['y-M-d K', '1970-1-1 10', 36000],
            ['y-M-d KK', '1970-1-1 11', 39600],
            ['y-M-d KK', '1970-1-1 12', 43200],
            ['y-M-d KK a', '1970-1-1 0 AM', 0],
            ['y-M-d KK a', '1970-1-1 1 AM', 3600],
            ['y-M-d KK a', '1970-1-1 10 AM', 36000],
            ['y-M-d KK a', '1970-1-1 11 AM', 39600],
            ['y-M-d KK a', '1970-1-1 12 AM', 43200],
            ['y-M-d KK a', '1970-1-1 23 AM', 82800],
            ['y-M-d KK a', '1970-1-1 24 AM', 86400],
            ['y-M-d KK a', '1970-1-1 0 PM', 43200],
            ['y-M-d KK a', '1970-1-1 1 PM', 46800],
            ['y-M-d KK a', '1970-1-1 10 PM', 79200],
            ['y-M-d KK a', '1970-1-1 11 PM', 82800],
            ['y-M-d KK a', '1970-1-1 12 PM', 86400],
            ['y-M-d KK a', '1970-1-1 23 PM', 126000],
            ['y-M-d KK a', '1970-1-1 24 PM', 129600],
        ];
    }

    public function parseHour24ClockOneBasedProvider()
    {
        return [
            // 24 hours (1-24)
            ['y-M-d k', '1970-1-1 1', 3600],
            ['y-M-d k', '1970-1-1 10', 36000],
            ['y-M-d kk', '1970-1-1 11', 39600],
            ['y-M-d kk', '1970-1-1 12', 43200],
            ['y-M-d kk', '1970-1-1 23', 82800],
            ['y-M-d kk', '1970-1-1 24', 0],
            ['y-M-d kk a', '1970-1-1 0 AM', 0],
            ['y-M-d kk a', '1970-1-1 1 AM', 0],
            ['y-M-d kk a', '1970-1-1 10 AM', 0],
            ['y-M-d kk a', '1970-1-1 11 AM', 0],
            ['y-M-d kk a', '1970-1-1 12 AM', 0],
            ['y-M-d kk a', '1970-1-1 23 AM', 0],
            ['y-M-d kk a', '1970-1-1 24 AM', 0],
            ['y-M-d kk a', '1970-1-1 0 PM', 43200],
            ['y-M-d kk a', '1970-1-1 1 PM', 43200],
            ['y-M-d kk a', '1970-1-1 10 PM', 43200],
            ['y-M-d kk a', '1970-1-1 11 PM', 43200],
            ['y-M-d kk a', '1970-1-1 12 PM', 43200],
            ['y-M-d kk a', '1970-1-1 23 PM', 43200],
            ['y-M-d kk a', '1970-1-1 24 PM', 43200],
        ];
    }

    public function parseHour24ClockZeroBasedProvider()
    {
        return [
            // 24 hours (0-23)
            ['y-M-d H', '1970-1-1 0', 0],
            ['y-M-d H', '1970-1-1 1', 3600],
            ['y-M-d H', '1970-1-1 10', 36000],
            ['y-M-d HH', '1970-1-1 11', 39600],
            ['y-M-d HH', '1970-1-1 12', 43200],
            ['y-M-d HH', '1970-1-1 23', 82800],
            ['y-M-d HH a', '1970-1-1 0 AM', 0],
            ['y-M-d HH a', '1970-1-1 1 AM', 0],
            ['y-M-d HH a', '1970-1-1 10 AM', 0],
            ['y-M-d HH a', '1970-1-1 11 AM', 0],
            ['y-M-d HH a', '1970-1-1 12 AM', 0],
            ['y-M-d HH a', '1970-1-1 23 AM', 0],
            ['y-M-d HH a', '1970-1-1 24 AM', 0],
            ['y-M-d HH a', '1970-1-1 0 PM', 43200],
            ['y-M-d HH a', '1970-1-1 1 PM', 43200],
            ['y-M-d HH a', '1970-1-1 10 PM', 43200],
            ['y-M-d HH a', '1970-1-1 11 PM', 43200],
            ['y-M-d HH a', '1970-1-1 12 PM', 43200],
            ['y-M-d HH a', '1970-1-1 23 PM', 43200],
            ['y-M-d HH a', '1970-1-1 24 PM', 43200],
        ];
    }

    public function parseMinuteProvider()
    {
        return [
            ['y-M-d HH:m', '1970-1-1 0:1', 60],
            ['y-M-d HH:mm', '1970-1-1 0:10', 600],
        ];
    }

    public function parseSecondProvider()
    {
        return [
            ['y-M-d HH:mm:s', '1970-1-1 00:01:1', 61],
            ['y-M-d HH:mm:ss', '1970-1-1 00:01:10', 70],
        ];
    }

    public function parseTimezoneProvider()
    {
        return [
            ['y-M-d HH:mm:ss zzzz', '1970-1-1 00:00:00 GMT-03:00', 10800],
            ['y-M-d HH:mm:ss zzzz', '1970-1-1 00:00:00 GMT-04:00', 14400],
            ['y-M-d HH:mm:ss zzzz', '1970-1-1 00:00:00 GMT-00:00', 0],
            ['y-M-d HH:mm:ss zzzz', '1970-1-1 00:00:00 GMT+03:00', -10800],
            ['y-M-d HH:mm:ss zzzz', '1970-1-1 00:00:00 GMT+04:00', -14400],
            ['y-M-d HH:mm:ss zzzz', '1970-1-1 00:00:00 GMT-0300', 10800],
            ['y-M-d HH:mm:ss zzzz', '1970-1-1 00:00:00 GMT+0300', -10800],

            // a previous timezone parsing should not change the timezone for the next parsing
            ['y-M-d HH:mm:ss', '1970-1-1 00:00:00', 0],
        ];
    }

    public function parseAmPmProvider()
    {
        return [
            // AM/PM (already covered by hours tests)
            ['y-M-d HH:mm:ss a', '1970-1-1 00:00:00 AM', 0],
            ['y-M-d HH:mm:ss a', '1970-1-1 00:00:00 PM', 43200],
        ];
    }

    public function parseStandaloneAmPmProvider()
    {
        return [
            ['a', 'AM', 0],
            ['a', 'PM', 43200],
        ];
    }

    public function parseRegexMetaCharsProvider()
    {
        return [
            // regexp meta chars in the pattern string
            ['y[M-d', '1970[1-1', 0],
            ['y[M/d', '1970[1/1', 0],
        ];
    }

    public function parseQuoteCharsProvider()
    {
        return [
            ["'M'", 'M', 0],
            ["'yy'", 'yy', 0],
            ["'''yy'", "'yy", 0],
            ["''y", "'1970", 0],
            ["H 'o'' clock'", "0 o' clock", 0],
        ];
    }

    public function parseDashSlashProvider()
    {
        return [
            ['y-M-d', '1970/1/1', 0],
            ['yy-M-d', '70/1/1', 0],
            ['y/M/d', '1970-1-1', 0],
            ['yy/M/d', '70-1-1', 0],
        ];
    }

    /**
     * @dataProvider parseErrorProvider
     */
    public function testParseError($pattern, $value)
    {
        $errorCode = IntlGlobals::U_PARSE_ERROR;
        $errorMessage = 'Date parsing failed: U_PARSE_ERROR';

        $formatter = $this->getDefaultDateFormatter($pattern);
        $this->assertFalse($formatter->parse($value));
        $this->assertIsIntlFailure($formatter, $errorMessage, $errorCode);
    }

    public function parseErrorProvider()
    {
        return [
            // 1 char month
            ['y-MMMMM-d', '1970-J-1'],
            ['y-MMMMM-d', '1970-S-1'],

            // standalone 1 char month
            ['y-LLLLL-d', '1970-J-1'],
            ['y-LLLLL-d', '1970-S-1'],
        ];
    }

    /*
     * https://github.com/symfony/symfony/issues/4242
     */
    public function testParseAfterError()
    {
        $this->testParseError('y-MMMMM-d', '1970-J-1');
        $this->testParse('y-M-d', '1970-1-1', 0);
    }

    public function testParseWithNullPositionValue()
    {
        $position = null;
        $formatter = $this->getDefaultDateFormatter('y');
        $this->assertSame(0, $formatter->parse('1970', $position));
        // Since $position is not supported by the Symfony implementation, the following won't work.
        // The intl implementation works this way since 60.2.
        // $this->assertSame(4, $position);
    }

    public function testSetPattern()
    {
        $formatter = $this->getDefaultDateFormatter();
        $formatter->setPattern('yyyy-MM-dd');
        $this->assertEquals('yyyy-MM-dd', $formatter->getPattern());
    }

    /**
     * @dataProvider setTimeZoneIdProvider
     */
    public function testSetTimeZoneId($timeZoneId, $expectedTimeZoneId)
    {
        $formatter = $this->getDefaultDateFormatter();

        $formatter->setTimeZone($timeZoneId);

        $this->assertEquals($expectedTimeZoneId, $formatter->getTimeZoneId());
    }

    public function setTimeZoneIdProvider()
    {
        return [
            ['UTC', 'UTC'],
            ['GMT', 'GMT'],
            ['GMT-03:00', 'GMT-03:00'],
            ['Europe/Zurich', 'Europe/Zurich'],
            [null, date_default_timezone_get()],
            ['Foo/Bar', 'UTC'],
            ['GMT+00:AA', 'UTC'],
            ['GMT+00AA', 'UTC'],
        ];
    }

    protected function getDefaultDateFormatter($pattern = null)
    {
        return $this->getDateFormatter('en', IntlDateFormatter::MEDIUM, IntlDateFormatter::SHORT, 'UTC', IntlDateFormatter::GREGORIAN, $pattern);
    }

    protected function getDateTime($timestamp, $timeZone)
    {
        $dateTime = new \DateTime();
        $dateTime->setTimestamp(null === $timestamp ? time() : $timestamp);
        $dateTime->setTimezone(new \DateTimeZone($timeZone ?: getenv('TZ') ?: 'UTC'));

        return $dateTime;
    }

    protected function assertIsIntlFailure($formatter, $errorMessage, $errorCode)
    {
        $this->assertSame($errorMessage, $this->getIntlErrorMessage());
        $this->assertSame($errorCode, $this->getIntlErrorCode());
        $this->assertTrue($this->isIntlFailure($this->getIntlErrorCode()));
        $this->assertSame($errorMessage, $formatter->getErrorMessage());
        $this->assertSame($errorCode, $formatter->getErrorCode());
        $this->assertTrue($this->isIntlFailure($formatter->getErrorCode()));
    }

    protected function assertIsIntlSuccess($formatter, $errorMessage, $errorCode)
    {
        /* @var IntlDateFormatter $formatter */
        $this->assertSame($errorMessage, $this->getIntlErrorMessage());
        $this->assertSame($errorCode, $this->getIntlErrorCode());
        $this->assertFalse($this->isIntlFailure($this->getIntlErrorCode()));
        $this->assertSame($errorMessage, $formatter->getErrorMessage());
        $this->assertSame($errorCode, $formatter->getErrorCode());
        $this->assertFalse($this->isIntlFailure($formatter->getErrorCode()));
    }

    /**
     * @param $locale
     * @param $datetype
     * @param $timetype
     * @param null $timezone
     * @param int  $calendar
     * @param null $pattern
     *
     * @return mixed
     */
    abstract protected function getDateFormatter($locale, $datetype, $timetype, $timezone = null, $calendar = IntlDateFormatter::GREGORIAN, $pattern = null);

    /**
     * @return string
     */
    abstract protected function getIntlErrorMessage();

    /**
     * @return int
     */
    abstract protected function getIntlErrorCode();

    /**
     * @param int $errorCode
     *
     * @return bool
     */
    abstract protected function isIntlFailure($errorCode);
}
