/*
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.google.j2objc.util;

import junit.framework.TestCase;

import java.util.Arrays;
import java.util.Calendar;
import java.util.GregorianCalendar;
import java.util.List;
import java.util.Locale;
import java.util.TimeZone;

/**
 * Test the NSTimeZone-backed TimeZone implementation class.
 *
 * The testing strategy here is by no means comprehensive, but suffices for catching regressions:
 * if the underlying concrete TimeZone subclass is not based on a time zone database, it will not be
 * able to handle US time zone changes in 2005 as well as the historical transitions in Australia.
 *
 * @author Lukhnos Liu
 */
public class NativeTimeZoneTest extends TestCase {

  /**
   * Test that the TimeZone instance returns accurate offsets for instants in America/Los_Angeles
   * (commonly known as PT, which includes PST and PDT). This also accounts for the daylight
   * saving time (DST) change due to Energy Policy Act of 2005.
   */
  public void testUSPacificTimeZoneTransitionsSince1970() {
    TimeZone la = TimeZone.getTimeZone("America/Los_Angeles");
    assertEquals(-25200000, la.getOffset(1457863200000L)); // 2016-03-13T03:00:00.000-07:00
    assertEquals(-28800000, la.getOffset(1446368400000L)); // 2015-11-01T01:00:00.000-08:00
    assertEquals(-25200000, la.getOffset(1425808800000L)); // 2015-03-08T03:00:00.000-07:00
    assertEquals(-28800000, la.getOffset(1414918800000L)); // 2014-11-02T01:00:00.000-08:00
    assertEquals(-25200000, la.getOffset(1394359200000L)); // 2014-03-09T03:00:00.000-07:00
    assertEquals(-28800000, la.getOffset(1383469200000L)); // 2013-11-03T01:00:00.000-08:00
    assertEquals(-25200000, la.getOffset(1362909600000L)); // 2013-03-10T03:00:00.000-07:00
    assertEquals(-28800000, la.getOffset(1352019600000L)); // 2012-11-04T01:00:00.000-08:00
    assertEquals(-25200000, la.getOffset(1331460000000L)); // 2012-03-11T03:00:00.000-07:00
    assertEquals(-28800000, la.getOffset(1320570000000L)); // 2011-11-06T01:00:00.000-08:00
    assertEquals(-25200000, la.getOffset(1300010400000L)); // 2011-03-13T03:00:00.000-07:00
    assertEquals(-28800000, la.getOffset(1289120400000L)); // 2010-11-07T01:00:00.000-08:00
    assertEquals(-25200000, la.getOffset(1268560800000L)); // 2010-03-14T03:00:00.000-07:00
    assertEquals(-28800000, la.getOffset(1257066000000L)); // 2009-11-01T01:00:00.000-08:00
    assertEquals(-25200000, la.getOffset(1236506400000L)); // 2009-03-08T03:00:00.000-07:00
    assertEquals(-28800000, la.getOffset(1225616400000L)); // 2008-11-02T01:00:00.000-08:00
    assertEquals(-25200000, la.getOffset(1205056800000L)); // 2008-03-09T03:00:00.000-07:00
    assertEquals(-28800000, la.getOffset(1194166800000L)); // 2007-11-04T01:00:00.000-08:00
    assertEquals(-25200000, la.getOffset(1173607200000L)); // 2007-03-11T03:00:00.000-07:00
    assertEquals(-28800000, la.getOffset(1162112400000L)); // 2006-10-29T01:00:00.000-08:00
    assertEquals(-25200000, la.getOffset(1143972000000L)); // 2006-04-02T03:00:00.000-07:00
    assertEquals(-28800000, la.getOffset(1130662800000L)); // 2005-10-30T01:00:00.000-08:00
    assertEquals(-25200000, la.getOffset(1112522400000L)); // 2005-04-03T03:00:00.000-07:00
    assertEquals(-28800000, la.getOffset(1099213200000L)); // 2004-10-31T01:00:00.000-08:00
    assertEquals(-25200000, la.getOffset(1081072800000L)); // 2004-04-04T03:00:00.000-07:00
    assertEquals(-28800000, la.getOffset(1067158800000L)); // 2003-10-26T01:00:00.000-08:00
    assertEquals(-25200000, la.getOffset(1049623200000L)); // 2003-04-06T03:00:00.000-07:00
    assertEquals(-28800000, la.getOffset(1035709200000L)); // 2002-10-27T01:00:00.000-08:00
    assertEquals(-25200000, la.getOffset(1018173600000L)); // 2002-04-07T03:00:00.000-07:00
    assertEquals(-28800000, la.getOffset(1004259600000L)); // 2001-10-28T01:00:00.000-08:00
    assertEquals(-25200000, la.getOffset(986119200000L)); // 2001-04-01T03:00:00.000-07:00
    assertEquals(-28800000, la.getOffset(972810000000L)); // 2000-10-29T01:00:00.000-08:00
    assertEquals(-25200000, la.getOffset(954669600000L)); // 2000-04-02T03:00:00.000-07:00
    assertEquals(-28800000, la.getOffset(941360400000L)); // 1999-10-31T01:00:00.000-08:00
    assertEquals(-25200000, la.getOffset(923220000000L)); // 1999-04-04T03:00:00.000-07:00
    assertEquals(-28800000, la.getOffset(909306000000L)); // 1998-10-25T01:00:00.000-08:00
    assertEquals(-25200000, la.getOffset(891770400000L)); // 1998-04-05T03:00:00.000-07:00
    assertEquals(-28800000, la.getOffset(877856400000L)); // 1997-10-26T01:00:00.000-08:00
    assertEquals(-25200000, la.getOffset(860320800000L)); // 1997-04-06T03:00:00.000-07:00
    assertEquals(-28800000, la.getOffset(846406800000L)); // 1996-10-27T01:00:00.000-08:00
    assertEquals(-25200000, la.getOffset(828871200000L)); // 1996-04-07T03:00:00.000-07:00
    assertEquals(-28800000, la.getOffset(814957200000L)); // 1995-10-29T01:00:00.000-08:00
    assertEquals(-25200000, la.getOffset(796816800000L)); // 1995-04-02T03:00:00.000-07:00
    assertEquals(-28800000, la.getOffset(783507600000L)); // 1994-10-30T01:00:00.000-08:00
    assertEquals(-25200000, la.getOffset(765367200000L)); // 1994-04-03T03:00:00.000-07:00
    assertEquals(-28800000, la.getOffset(752058000000L)); // 1993-10-31T01:00:00.000-08:00
    assertEquals(-25200000, la.getOffset(733917600000L)); // 1993-04-04T03:00:00.000-07:00
    assertEquals(-28800000, la.getOffset(720003600000L)); // 1992-10-25T01:00:00.000-08:00
    assertEquals(-25200000, la.getOffset(702468000000L)); // 1992-04-05T03:00:00.000-07:00
    assertEquals(-28800000, la.getOffset(688554000000L)); // 1991-10-27T01:00:00.000-08:00
    assertEquals(-25200000, la.getOffset(671018400000L)); // 1991-04-07T03:00:00.000-07:00
    assertEquals(-28800000, la.getOffset(657104400000L)); // 1990-10-28T01:00:00.000-08:00
    assertEquals(-25200000, la.getOffset(638964000000L)); // 1990-04-01T03:00:00.000-07:00
    assertEquals(-28800000, la.getOffset(625654800000L)); // 1989-10-29T01:00:00.000-08:00
    assertEquals(-25200000, la.getOffset(607514400000L)); // 1989-04-02T03:00:00.000-07:00
    assertEquals(-28800000, la.getOffset(594205200000L)); // 1988-10-30T01:00:00.000-08:00
    assertEquals(-25200000, la.getOffset(576064800000L)); // 1988-04-03T03:00:00.000-07:00
    assertEquals(-28800000, la.getOffset(562150800000L)); // 1987-10-25T01:00:00.000-08:00
    assertEquals(-25200000, la.getOffset(544615200000L)); // 1987-04-05T03:00:00.000-07:00
    assertEquals(-28800000, la.getOffset(530701200000L)); // 1986-10-26T01:00:00.000-08:00
    assertEquals(-25200000, la.getOffset(514980000000L)); // 1986-04-27T03:00:00.000-07:00
    assertEquals(-28800000, la.getOffset(499251600000L)); // 1985-10-27T01:00:00.000-08:00
    assertEquals(-25200000, la.getOffset(483530400000L)); // 1985-04-28T03:00:00.000-07:00
    assertEquals(-28800000, la.getOffset(467802000000L)); // 1984-10-28T01:00:00.000-08:00
    assertEquals(-25200000, la.getOffset(452080800000L)); // 1984-04-29T03:00:00.000-07:00
    assertEquals(-28800000, la.getOffset(436352400000L)); // 1983-10-30T01:00:00.000-08:00
    assertEquals(-25200000, la.getOffset(420026400000L)); // 1983-04-24T03:00:00.000-07:00
    assertEquals(-28800000, la.getOffset(404902800000L)); // 1982-10-31T01:00:00.000-08:00
    assertEquals(-25200000, la.getOffset(388576800000L)); // 1982-04-25T03:00:00.000-07:00
    assertEquals(-28800000, la.getOffset(372848400000L)); // 1981-10-25T01:00:00.000-08:00
    assertEquals(-25200000, la.getOffset(357127200000L)); // 1981-04-26T03:00:00.000-07:00
    assertEquals(-28800000, la.getOffset(341398800000L)); // 1980-10-26T01:00:00.000-08:00
    assertEquals(-25200000, la.getOffset(325677600000L)); // 1980-04-27T03:00:00.000-07:00
    assertEquals(-28800000, la.getOffset(309949200000L)); // 1979-10-28T01:00:00.000-08:00
    assertEquals(-25200000, la.getOffset(294228000000L)); // 1979-04-29T03:00:00.000-07:00
    assertEquals(-28800000, la.getOffset(278499600000L)); // 1978-10-29T01:00:00.000-08:00
    assertEquals(-25200000, la.getOffset(262778400000L)); // 1978-04-30T03:00:00.000-07:00
    assertEquals(-28800000, la.getOffset(247050000000L)); // 1977-10-30T01:00:00.000-08:00
    assertEquals(-25200000, la.getOffset(230724000000L)); // 1977-04-24T03:00:00.000-07:00
    assertEquals(-28800000, la.getOffset(215600400000L)); // 1976-10-31T01:00:00.000-08:00
    assertEquals(-25200000, la.getOffset(199274400000L)); // 1976-04-25T03:00:00.000-07:00
    assertEquals(-28800000, la.getOffset(183546000000L)); // 1975-10-26T01:00:00.000-08:00
    assertEquals(-25200000, la.getOffset(162381600000L)); // 1975-02-23T03:00:00.000-07:00
    assertEquals(-28800000, la.getOffset(152096400000L)); // 1974-10-27T01:00:00.000-08:00
    assertEquals(-25200000, la.getOffset(126698400000L)); // 1974-01-06T03:00:00.000-07:00
    assertEquals(-28800000, la.getOffset(120646800000L)); // 1973-10-28T01:00:00.000-08:00
    assertEquals(-25200000, la.getOffset(104925600000L)); // 1973-04-29T03:00:00.000-07:00
    assertEquals(-28800000, la.getOffset(89197200000L)); // 1972-10-29T01:00:00.000-08:00
    assertEquals(-25200000, la.getOffset(73476000000L)); // 1972-04-30T03:00:00.000-07:00
    assertEquals(-28800000, la.getOffset(57747600000L)); // 1971-10-31T01:00:00.000-08:00
    assertEquals(-25200000, la.getOffset(41421600000L)); // 1971-04-25T03:00:00.000-07:00
    assertEquals(-28800000, la.getOffset(25693200000L)); // 1970-10-25T01:00:00.000-08:00
    assertEquals(-25200000, la.getOffset(9972000000L)); // 1970-04-26T03:00:00.000-07:00
  }

  /**
   * New South Wales observed DST during World War I and World War II, and has been observing DST
   * since 1971.
   */
  public void testAustraliaEasternTimeZoneTransitions() {
    TimeZone sydney = TimeZone.getTimeZone("Australia/Sydney");
    assertEquals(36000000, sydney.getOffset(1459612800000L)); // 2016-04-03T02:00:00.000+10:00
    assertEquals(39600000, sydney.getOffset(1443888000000L)); // 2015-10-04T03:00:00.000+11:00
    assertEquals(36000000, sydney.getOffset(1428163200000L)); // 2015-04-05T02:00:00.000+10:00
    assertEquals(39600000, sydney.getOffset(1412438400000L)); // 2014-10-05T03:00:00.000+11:00
    assertEquals(36000000, sydney.getOffset(1396713600000L)); // 2014-04-06T02:00:00.000+10:00
    assertEquals(39600000, sydney.getOffset(1380988800000L)); // 2013-10-06T03:00:00.000+11:00
    assertEquals(36000000, sydney.getOffset(1365264000000L)); // 2013-04-07T02:00:00.000+10:00
    assertEquals(39600000, sydney.getOffset(1349539200000L)); // 2012-10-07T03:00:00.000+11:00
    assertEquals(36000000, sydney.getOffset(1333209600000L)); // 2012-04-01T02:00:00.000+10:00
    assertEquals(39600000, sydney.getOffset(1317484800000L)); // 2011-10-02T03:00:00.000+11:00
    assertEquals(36000000, sydney.getOffset(1301760000000L)); // 2011-04-03T02:00:00.000+10:00
    assertEquals(39600000, sydney.getOffset(1286035200000L)); // 2010-10-03T03:00:00.000+11:00
    assertEquals(36000000, sydney.getOffset(1270310400000L)); // 2010-04-04T02:00:00.000+10:00
    assertEquals(39600000, sydney.getOffset(1254585600000L)); // 2009-10-04T03:00:00.000+11:00
    assertEquals(36000000, sydney.getOffset(1238860800000L)); // 2009-04-05T02:00:00.000+10:00
    assertEquals(39600000, sydney.getOffset(1223136000000L)); // 2008-10-05T03:00:00.000+11:00
    assertEquals(36000000, sydney.getOffset(1207411200000L)); // 2008-04-06T02:00:00.000+10:00
    assertEquals(39600000, sydney.getOffset(1193500800000L)); // 2007-10-28T03:00:00.000+11:00
    assertEquals(36000000, sydney.getOffset(1174752000000L)); // 2007-03-25T02:00:00.000+10:00
    assertEquals(39600000, sydney.getOffset(1162051200000L)); // 2006-10-29T03:00:00.000+11:00
    assertEquals(36000000, sydney.getOffset(1143907200000L)); // 2006-04-02T02:00:00.000+10:00
    assertEquals(39600000, sydney.getOffset(1130601600000L)); // 2005-10-30T03:00:00.000+11:00
    assertEquals(36000000, sydney.getOffset(1111852800000L)); // 2005-03-27T02:00:00.000+10:00
    assertEquals(39600000, sydney.getOffset(1099152000000L)); // 2004-10-31T03:00:00.000+11:00
    assertEquals(36000000, sydney.getOffset(1080403200000L)); // 2004-03-28T02:00:00.000+10:00
    assertEquals(39600000, sydney.getOffset(1067097600000L)); // 2003-10-26T03:00:00.000+11:00
    assertEquals(36000000, sydney.getOffset(1048953600000L)); // 2003-03-30T02:00:00.000+10:00
    assertEquals(39600000, sydney.getOffset(1035648000000L)); // 2002-10-27T03:00:00.000+11:00
    assertEquals(36000000, sydney.getOffset(1017504000000L)); // 2002-03-31T02:00:00.000+10:00
    assertEquals(39600000, sydney.getOffset(1004198400000L)); // 2001-10-28T03:00:00.000+11:00
    assertEquals(36000000, sydney.getOffset(985449600000L)); // 2001-03-25T02:00:00.000+10:00
    assertEquals(39600000, sydney.getOffset(967305600000L)); // 2000-08-27T03:00:00.000+11:00
    assertEquals(36000000, sydney.getOffset(954000000000L)); // 2000-03-26T02:00:00.000+10:00
    assertEquals(39600000, sydney.getOffset(941299200000L)); // 1999-10-31T03:00:00.000+11:00
    assertEquals(36000000, sydney.getOffset(922550400000L)); // 1999-03-28T02:00:00.000+10:00
    assertEquals(39600000, sydney.getOffset(909244800000L)); // 1998-10-25T03:00:00.000+11:00
    assertEquals(36000000, sydney.getOffset(891100800000L)); // 1998-03-29T02:00:00.000+10:00
    assertEquals(39600000, sydney.getOffset(877795200000L)); // 1997-10-26T03:00:00.000+11:00
    assertEquals(36000000, sydney.getOffset(859651200000L)); // 1997-03-30T02:00:00.000+10:00
    assertEquals(39600000, sydney.getOffset(846345600000L)); // 1996-10-27T03:00:00.000+11:00
    assertEquals(36000000, sydney.getOffset(828201600000L)); // 1996-03-31T02:00:00.000+10:00
    assertEquals(39600000, sydney.getOffset(814896000000L)); // 1995-10-29T03:00:00.000+11:00
    assertEquals(36000000, sydney.getOffset(794332800000L)); // 1995-03-05T02:00:00.000+10:00
    assertEquals(39600000, sydney.getOffset(783446400000L)); // 1994-10-30T03:00:00.000+11:00
    assertEquals(36000000, sydney.getOffset(762883200000L)); // 1994-03-06T02:00:00.000+10:00
    assertEquals(39600000, sydney.getOffset(751996800000L)); // 1993-10-31T03:00:00.000+11:00
    assertEquals(36000000, sydney.getOffset(731433600000L)); // 1993-03-07T02:00:00.000+10:00
    assertEquals(39600000, sydney.getOffset(719942400000L)); // 1992-10-25T03:00:00.000+11:00
    assertEquals(36000000, sydney.getOffset(699379200000L)); // 1992-03-01T02:00:00.000+10:00
    assertEquals(39600000, sydney.getOffset(688492800000L)); // 1991-10-27T03:00:00.000+11:00
    assertEquals(36000000, sydney.getOffset(667929600000L)); // 1991-03-03T02:00:00.000+10:00
    assertEquals(39600000, sydney.getOffset(657043200000L)); // 1990-10-28T03:00:00.000+11:00
    assertEquals(36000000, sydney.getOffset(636480000000L)); // 1990-03-04T02:00:00.000+10:00
    assertEquals(39600000, sydney.getOffset(625593600000L)); // 1989-10-29T03:00:00.000+11:00
    assertEquals(36000000, sydney.getOffset(606240000000L)); // 1989-03-19T02:00:00.000+10:00
    assertEquals(39600000, sydney.getOffset(594144000000L)); // 1988-10-30T03:00:00.000+11:00
    assertEquals(36000000, sydney.getOffset(574790400000L)); // 1988-03-20T02:00:00.000+10:00
    assertEquals(39600000, sydney.getOffset(562089600000L)); // 1987-10-25T03:00:00.000+11:00
    assertEquals(36000000, sydney.getOffset(542736000000L)); // 1987-03-15T02:00:00.000+10:00
    assertEquals(39600000, sydney.getOffset(530035200000L)); // 1986-10-19T03:00:00.000+11:00
    assertEquals(36000000, sydney.getOffset(511286400000L)); // 1986-03-16T02:00:00.000+10:00
    assertEquals(39600000, sydney.getOffset(499190400000L)); // 1985-10-27T03:00:00.000+11:00
    assertEquals(36000000, sydney.getOffset(478627200000L)); // 1985-03-03T02:00:00.000+10:00
    assertEquals(39600000, sydney.getOffset(467740800000L)); // 1984-10-28T03:00:00.000+11:00
    assertEquals(36000000, sydney.getOffset(447177600000L)); // 1984-03-04T02:00:00.000+10:00
    assertEquals(39600000, sydney.getOffset(436291200000L)); // 1983-10-30T03:00:00.000+11:00
    assertEquals(36000000, sydney.getOffset(415728000000L)); // 1983-03-06T02:00:00.000+10:00
    assertEquals(39600000, sydney.getOffset(404841600000L)); // 1982-10-31T03:00:00.000+11:00
    assertEquals(36000000, sydney.getOffset(386697600000L)); // 1982-04-04T02:00:00.000+10:00
    assertEquals(39600000, sydney.getOffset(372787200000L)); // 1981-10-25T03:00:00.000+11:00
    assertEquals(36000000, sydney.getOffset(352224000000L)); // 1981-03-01T02:00:00.000+10:00
    assertEquals(39600000, sydney.getOffset(341337600000L)); // 1980-10-26T03:00:00.000+11:00
    assertEquals(36000000, sydney.getOffset(320774400000L)); // 1980-03-02T02:00:00.000+10:00
    assertEquals(39600000, sydney.getOffset(309888000000L)); // 1979-10-28T03:00:00.000+11:00
    assertEquals(36000000, sydney.getOffset(289324800000L)); // 1979-03-04T02:00:00.000+10:00
    assertEquals(39600000, sydney.getOffset(278438400000L)); // 1978-10-29T03:00:00.000+11:00
    assertEquals(36000000, sydney.getOffset(257875200000L)); // 1978-03-05T02:00:00.000+10:00
    assertEquals(39600000, sydney.getOffset(246988800000L)); // 1977-10-30T03:00:00.000+11:00
    assertEquals(36000000, sydney.getOffset(226425600000L)); // 1977-03-06T02:00:00.000+10:00
    assertEquals(39600000, sydney.getOffset(215539200000L)); // 1976-10-31T03:00:00.000+11:00
    assertEquals(36000000, sydney.getOffset(194976000000L)); // 1976-03-07T02:00:00.000+10:00
    assertEquals(39600000, sydney.getOffset(183484800000L)); // 1975-10-26T03:00:00.000+11:00
    assertEquals(36000000, sydney.getOffset(162921600000L)); // 1975-03-02T02:00:00.000+10:00
    assertEquals(39600000, sydney.getOffset(152035200000L)); // 1974-10-27T03:00:00.000+11:00
    assertEquals(36000000, sydney.getOffset(131472000000L)); // 1974-03-03T02:00:00.000+10:00
    assertEquals(39600000, sydney.getOffset(120585600000L)); // 1973-10-28T03:00:00.000+11:00
    assertEquals(36000000, sydney.getOffset(100022400000L)); // 1973-03-04T02:00:00.000+10:00
    assertEquals(39600000, sydney.getOffset(89136000000L)); // 1972-10-29T03:00:00.000+11:00
    assertEquals(36000000, sydney.getOffset(67968000000L)); // 1972-02-27T02:00:00.000+10:00
    assertEquals(39600000, sydney.getOffset(57686400000L)); // 1971-10-31T03:00:00.000+11:00
    // The IANA timezone database, release 2020e (12/22/2020) fixed: "Australia: several 1917 through 1971
    // transitions". Since this test may still be run on older OS releases, we need to test for both.
    assertEitherSidneyOffset(sydney.getOffset(-813229200000L)); // 1944-03-26T01:00:00.000+10:00
    assertEitherSidneyOffset(sydney.getOffset(-828345600000L)); // 1943-10-03T03:00:00.000+11:00
    assertEitherSidneyOffset(sydney.getOffset(-844678800000L)); // 1943-03-28T01:00:00.000+10:00
    assertEitherSidneyOffset(sydney.getOffset(-860400000000L)); // 1942-09-27T03:00:00.000+11:00
    assertEitherSidneyOffset(sydney.getOffset(-876128400000L)); // 1942-03-29T01:00:00.000+10:00
    assertEitherSidneyOffset(sydney.getOffset(-883641600000L)); // 1942-01-01T03:00:00.000+11:00
    assertEitherSidneyOffset(sydney.getOffset(-1665392400000L)); // 1917-03-25T01:00:00.000+10:00
    assertEitherSidneyOffset(sydney.getOffset(-1672567140000L)); // 1917-01-01T01:01:00.000+11:00
    assertEitherSidneyOffset(sydney.getOffset(-1672567140001L)); // 1917-01-01T01:00:59.999+10:00
  }

  private void assertEitherSidneyOffset(int offset) {
    assertTrue(offset == 36000000 || offset == 39600000);
  }

  /**
   * Test the local date-based offset getter.
   */
  public void testGetOffsetWithLocalDates() {
    TimeZone la = TimeZone.getTimeZone("America/Los_Angeles");

    // Recall Java calendar month is 0-based, and the dayOfWeek, parameter is actually ignored by
    // some implementations.

    // 2016-03-12T03:00:00.000-08:00
    assertEquals(
        -28800000, la.getOffset(GregorianCalendar.AD, 2016, 2, 12, Calendar.SATURDAY, 10800000));

    // 2016-03-13T01:59:59.999-08:00
    assertEquals(
        -28800000, la.getOffset(GregorianCalendar.AD, 2016, 2, 13, Calendar.SUNDAY, 7200000 - 1));

    // 2016-03-13T02:00:00.000-08:00 (but 2 AM that day local time in America/Los_Angeles does not
    // exist, even though TimeZone returns something that is after the DST transition)
    assertEquals(
        -25200000, la.getOffset(GregorianCalendar.AD, 2016, 2, 13, Calendar.SUNDAY, 7200000));

    // 2016-03-13T02:30:00.000-08:00 (but 2:30 AM is a non-existent local time)
    assertEquals(
        -25200000, la.getOffset(GregorianCalendar.AD, 2016, 2, 13, Calendar.SUNDAY, 9000000));

    // 2016-03-13T02:59:59.999-08:00 (but 2:59:59.999 AM is a non-existent local time)
    assertEquals(
        -25200000, la.getOffset(GregorianCalendar.AD, 2016, 2, 13, Calendar.SUNDAY, 10800000 - 1));

    // 2016-03-13T03:00:00.000-07:00
    assertEquals(
        -25200000, la.getOffset(GregorianCalendar.AD, 2016, 2, 13, Calendar.SUNDAY, 10800000));

    // 2016-03-13T03:00:00.001-07:00
    assertEquals(
        -25200000, la.getOffset(GregorianCalendar.AD, 2016, 2, 13, Calendar.SUNDAY, 10800000 + 1));

    // 2015-10-31T01:00:00.000-07:00
    assertEquals(
        -25200000, la.getOffset(GregorianCalendar.AD, 2015, 9, 31, Calendar.SATURDAY, 3600000));

    // 2015-11-01T00:00:00.000-07:00
    assertEquals(-25200000, la.getOffset(GregorianCalendar.AD, 2015, 10, 1, Calendar.SUNDAY, 0));

    // 2015-11-01T00:59:59.000-07:00
    assertEquals(
        -25200000, la.getOffset(GregorianCalendar.AD, 2015, 10, 1, Calendar.SUNDAY, 3600000 - 1));

    // 2015-11-01T01:00:00.000-08:00 (this is the evidence that TimeZone implementations always
    // prefer the local datetime in terms of the raw offset).
    assertEquals(
        -28800000, la.getOffset(GregorianCalendar.AD, 2015, 10, 1, Calendar.SUNDAY, 3600000));

    // 2015-11-01T01:59:59.999-08:00
    assertEquals(
        -28800000, la.getOffset(GregorianCalendar.AD, 2015, 10, 1, Calendar.SUNDAY, 7200000 - 1));

    // 2015-11-01T02:00:00.000-08:00
    assertEquals(
        -28800000, la.getOffset(GregorianCalendar.AD, 2015, 10, 1, Calendar.SUNDAY, 7200000));

    // Don't test any BC dates as it's meaningless.
  }

  /**
   * Test the locale-dependent display name of a time zone.
   *
   * JVM and iOS/OS X don't agree on the localized names, nor do they have the same localization
   * coverage, and so we are just testing a few shared traits here.
   */
  public void testGetDisplayName() {
    TimeZone tz = TimeZone.getTimeZone("Europe/Paris");

    // JVM says Central European (Summer) Time, OS X/iOS uses Central European Standard/Summer Time.
    // Short names also differ and are not tested here.
    assertTrue(
        tz.getDisplayName(true, TimeZone.LONG, Locale.ENGLISH).startsWith("Central European"));
    assertTrue(tz.getDisplayName(true, TimeZone.LONG, Locale.ENGLISH).contains("Summer"));
    assertTrue(
        tz.getDisplayName(false, TimeZone.LONG, Locale.ENGLISH).startsWith("Central European"));
    assertFalse(tz.getDisplayName(false, TimeZone.LONG, Locale.ENGLISH).startsWith("Summer"));

    // French time zone names change across environment and OS versions, and so we only test common
    // substrings here.
    final String ete = "été"; // French for "summer"
    final String avancee = "avancée"; // French for "forward"
    String frStdName = tz.getDisplayName(false, TimeZone.LONG, Locale.FRANCE);
    String frDstName = tz.getDisplayName(true, TimeZone.LONG, Locale.FRANCE);
    assertTrue(frStdName.contains("Europe central"));
    assertFalse(frStdName.contains(ete) || frStdName.contains(avancee));
    assertTrue(frDstName.contains("Europe central"));
    assertTrue(frDstName.contains(ete) || frDstName.contains(avancee));

    // Similarly for German, though they finally agree on the DST name.
    assertTrue(
        tz.getDisplayName(true, TimeZone.LONG, Locale.GERMAN)
            .equals("Mitteleuropäische Sommerzeit"));
    assertTrue(
        tz.getDisplayName(false, TimeZone.LONG, Locale.GERMAN).startsWith("Mitteleuropäische"));
    assertFalse(tz.getDisplayName(false, TimeZone.LONG, Locale.GERMAN).contains("Sommer"));
  }

  public void testIsTimeZoneUsingDaylightSavingTime() {
    assertTrue(TimeZone.getTimeZone("America/Los_Angeles").useDaylightTime());
    assertFalse(TimeZone.getTimeZone("America/Phonenix").useDaylightTime()); // No DST in Arizona.
    assertTrue(TimeZone.getTimeZone("America/Denver").useDaylightTime());
    assertTrue(TimeZone.getTimeZone("Europe/Paris").useDaylightTime());
    assertTrue(TimeZone.getTimeZone("Australia/Sydney").useDaylightTime());
    assertFalse(TimeZone.getTimeZone("Asia/Tokyo").useDaylightTime());
    assertFalse(TimeZone.getTimeZone("Asia/Kolkata").useDaylightTime());
  }

  public void testCommonRawOffsets() {
    assertEquals(-8 * 3600000, TimeZone.getTimeZone("America/Los_Angeles").getRawOffset());
    assertEquals(-7 * 3600000, TimeZone.getTimeZone("America/Phoenix").getRawOffset());
    assertEquals(-7 * 3600000, TimeZone.getTimeZone("America/Denver").getRawOffset());
    assertEquals(-5 * 3600000, TimeZone.getTimeZone("America/New_York").getRawOffset());
    assertEquals(19800000, TimeZone.getTimeZone("Asia/Kolkata").getRawOffset()); // UTC+5.54
    assertEquals(9 * 3600000, TimeZone.getTimeZone("Asia/Tokyo").getRawOffset());
    assertEquals(10 * 3600000, TimeZone.getTimeZone("Australia/Sydney").getRawOffset());
  }

  public void testGetAvailableIds() {
    List<String> availableIds = Arrays.asList(TimeZone.getAvailableIDs(-7 * 3600000));
    assertTrue(availableIds.contains("America/Phoenix"));
    assertTrue(availableIds.contains("America/Denver"));
    assertFalse(availableIds.contains("America/Los_Angeles"));
  }

  public void testIds() {
    assertEquals("GMT", TimeZone.getTimeZone("GMT").getID());
    assertEquals("UTC", TimeZone.getTimeZone("UTC").getID());

    String vmName = System.getProperty("java.vendor");
    if (vmName != null && vmName.startsWith("J2ObjC")) {
      // NSTimeZone handles this custom time zone format and normalizes it.
      assertEquals("GMT+0530", TimeZone.getTimeZone("GMT+05:30").getID());
    } else {
      assertEquals("GMT+05:30", TimeZone.getTimeZone("GMT+05:30").getID());
    }
    assertEquals("America/New_York", TimeZone.getTimeZone("America/New_York").getID());
    assertEquals(TimeZone.getTimeZone("GMT").getID(), TimeZone.getTimeZone("").getID()); // WIP
    assertEquals(TimeZone.getTimeZone("GMT"), TimeZone.getTimeZone("america/new_york"));
  }

  /**
   * getTimeZone(String) always give you a new instance.
   */
  public void testEqualityAndSameness() {
    TimeZone a;
    TimeZone b;

    a = TimeZone.getTimeZone("GMT");
    b = TimeZone.getTimeZone("GMT");
    assertEquals(a, b);
    assertNotSame(a, b);

    a = TimeZone.getTimeZone("UTC");
    b = TimeZone.getTimeZone("UTC");
    assertEquals(a, b);
    assertNotSame(a, b);

    a = TimeZone.getTimeZone("UTC");
    b = TimeZone.getTimeZone("GMT");
    assertFalse(a.equals(b));
    assertNotSame(a, b);

    a = TimeZone.getTimeZone("America/New_York");
    b = TimeZone.getTimeZone("America/New_York");
    assertEquals(a, b);
    assertNotSame(a, b);
  }

  public void testHasSameRules() {
    TimeZone a;
    TimeZone b;

    a = TimeZone.getTimeZone("America/New_York");
    b = TimeZone.getTimeZone("US/Eastern");
    assertTrue(a.hasSameRules(b));
    assertFalse(a.hasSameRules(null));

    // Arizona does not observe DST, so even Phoenix and Denver have the same rawOffest, they have
    // different rules.
    a = TimeZone.getTimeZone("America/Phoenix");
    b = TimeZone.getTimeZone("America/Denver");
    assertEquals(a.getRawOffset(), b.getRawOffset());
    assertFalse(a.hasSameRules(b));
  }

  /**
   * Although Android SDK documentation says three-letter time zone IDs other than UTC and GMT are
   * not supported, actual implementations may still support them. The time zone "EST" is in
   * practice UTC-5 with the following properties.
   */
  public void testEST() {
    TimeZone tz = TimeZone.getTimeZone("EST");

    assertEquals("EST", tz.getID());
    assertFalse(tz.useDaylightTime());
    assertEquals(-5 * 3600000, tz.getRawOffset());
    assertEquals(0, tz.getDSTSavings());
  }

  /**
   * PDT is an abbreviation that all three platforms (JVM, Android, and iOS) agree that it's not
   * supported. On iOS it's not available via +[NSTimeZone timeZoneWithName:] though one can still
   * get it from +[NSTimeZone timeZoneWithAbbreviation:].
   *
   * PST is a whole different matter. It's usable both as a name and as an abbreviation on iOS.
   * It's available on JVM, but not on Android.
   */
  public void testNonAvailabilityOfPDT() {
    TimeZone tz = TimeZone.getTimeZone("PDT");
    assertFalse(tz.getID().equals("PDT"));
    assertFalse(tz.getID().equals("America/Los_Angeles"));
    assertFalse(tz.getID().equals("US/Pacific"));
    assertFalse(tz.getRawOffset() == -5 * 3600000);
    assertFalse(tz.getRawOffset() == -4 * 3600000);
  }

  public void testUTCAndGMT() {
    TimeZone utc = TimeZone.getTimeZone("UTC");
    TimeZone gmt = TimeZone.getTimeZone("GMT");

    assertEquals("UTC", utc.getID());
    assertEquals("GMT", gmt.getID());

    // Same rules, but not equal.
    assertTrue(utc.hasSameRules(gmt));
    assertFalse(utc.equals(gmt));

    assertEquals(utc.getDSTSavings(), gmt.getDSTSavings());
    assertEquals(utc.getRawOffset(), gmt.getRawOffset());
    assertEquals(utc.useDaylightTime(), gmt.useDaylightTime());
    assertEquals(0, utc.getRawOffset());
    assertEquals(0, utc.getDSTSavings());
    assertFalse(utc.useDaylightTime());

    List<String> ids = Arrays.asList(TimeZone.getAvailableIDs());
    assertTrue(ids.contains(gmt.getID()));
    assertTrue(ids.contains(utc.getID()));
  }

  /**
   * Different time zone libraries treat these two UTC alias differently. On OS X and iOS,
   * Etc/UTC and Etc/GMT are treated as two different time zones, even if their offset and other
   * fields are exactly the same as those in UTC. Therefore we don't even attempt to compare their
   * rules.
   */
  public void testEtcUTCAndEtcGMT() {
    TimeZone utc = TimeZone.getTimeZone("UTC");
    TimeZone etcUtc = TimeZone.getTimeZone("Etc/UTC");
    TimeZone etcGmt = TimeZone.getTimeZone("Etc/GMT");
    assertEquals("Etc/UTC", etcUtc.getID());
    assertEquals("Etc/GMT", etcGmt.getID());

    assertEquals(utc.getDSTSavings(), etcGmt.getDSTSavings());
    assertEquals(utc.getRawOffset(), etcGmt.getRawOffset());
    assertEquals(utc.useDaylightTime(), etcGmt.useDaylightTime());
    assertEquals(utc.getDSTSavings(), etcUtc.getDSTSavings());
    assertEquals(utc.getRawOffset(), etcUtc.getRawOffset());
    assertEquals(utc.useDaylightTime(), etcUtc.useDaylightTime());
    assertEquals(0, utc.getRawOffset());
    assertEquals(0, utc.getDSTSavings());
    assertFalse(utc.useDaylightTime());

    List<String> ids = Arrays.asList(TimeZone.getAvailableIDs());
    String vmName = System.getProperty("java.vendor");
    if (vmName != null && vmName.startsWith("J2ObjC")) {
      // It's ok for Etc/UTC and Etc/GMT not to be part of TimeZone.getAvailableIDs().
    } else {
      assertTrue(ids.contains(etcGmt.getID()));
      assertTrue(ids.contains(etcUtc.getID()));
    }
  }

  /**
   * US/Pacific is an alias to America/Los_Angeles, but the former is not part of the "known names"
   * reported by NSTimeZone.
   */
  public void testAmericaLosAngelesAndUSPacific() {
    TimeZone la = TimeZone.getTimeZone("America/Los_Angeles");
    TimeZone pacific = TimeZone.getTimeZone("US/Pacific");
    assertEquals("America/Los_Angeles", la.getID());
    assertEquals("US/Pacific", pacific.getID());

    // Same rules, but not equal.
    assertTrue(pacific.hasSameRules(la));
    assertFalse(pacific.equals(la));

    assertEquals(la.getDSTSavings(), pacific.getDSTSavings());
    assertEquals(la.getRawOffset(), pacific.getRawOffset());
    assertEquals(la.useDaylightTime(), pacific.useDaylightTime());
    assertEquals(-8 * 3600000L, la.getRawOffset());
    assertEquals(3600000L, la.getDSTSavings());
    assertTrue(la.useDaylightTime());

    List<String> ids = Arrays.asList(TimeZone.getAvailableIDs());
    assertTrue(ids.contains(la.getID()));

    String vmName = System.getProperty("java.vendor");
    if (vmName != null && vmName.startsWith("J2ObjC")) {
      // It's ok for US/Pacific not to be part of TimeZone.getAvailableIDs().
    } else {
      assertTrue(ids.contains(pacific.getID()));
    }
  }

  private static native void postNSSystemTimeZoneDidChangeNotification() /*-[
    [[NSNotificationCenter defaultCenter] postNotificationName:NSSystemTimeZoneDidChangeNotification
                                                        object:nil];
  ]-*/;

  public void testResetDefaultTimeZone() {
    TimeZone defaultTz = TimeZone.getDefault();
    if (defaultTz.getID().equals("Etc/UTC")) {
      // We assume that the test is never run with the environment TZ=Etc/UTC, but just to be safe.
      return;
    }

    TimeZone.setDefault(TimeZone.getTimeZone("Etc/UTC"));
    assertEquals("Etc/UTC", TimeZone.getDefault().getID());

    // Manually post NSSystemTimeZoneDidChangeNotification, which is dispatched immediately to all
    // observers.
    postNSSystemTimeZoneDidChangeNotification();

    // The default time zone should be updated.
    assertNotNull(TimeZone.getDefault());
    assertFalse(TimeZone.getDefault().getID().equals("Etc/UTC"));
  }

  public static boolean isNativeTimeZone(String id) {
    return NativeTimeZone.get(id) != null;
  }
}
