/*
 * Copyright (c) 2012, 2016, Oracle and/or its affiliates. All rights reserved.
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
 *
 * This code is free software; you can redistribute it and/or modify it
 * under the terms of the GNU General Public License version 2 only, as
 * published by the Free Software Foundation.
 *
 * This code is distributed in the hope that it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
 * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
 * version 2 for more details (a copy is included in the LICENSE file that
 * accompanied this code).
 *
 * You should have received a copy of the GNU General Public License version
 * 2 along with this work; if not, write to the Free Software Foundation,
 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
 *
 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
 * or visit www.oracle.com if you need additional information or have any
 * questions.
 */

/*
 * This file is available under and governed by the GNU General Public
 * License version 2 only, as published by the Free Software Foundation.
 * However, the following notice accompanied the original version of this
 * file:
 *
 * Copyright (c) 2008-2012 Stephen Colebourne & Michael Nascimento Santos
 *
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *
 *  * Redistributions of source code must retain the above copyright notice,
 *    this list of conditions and the following disclaimer.
 *
 *  * Redistributions in binary form must reproduce the above copyright notice,
 *    this list of conditions and the following disclaimer in the documentation
 *    and/or other materials provided with the distribution.
 *
 *  * Neither the name of JSR-310 nor the names of its contributors
 *    may be used to endorse or promote products derived from this software
 *    without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
 * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
 * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
 * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */
package test.java.time;

import static org.testng.Assert.assertEquals;
import static org.testng.Assert.assertSame;

import java.lang.reflect.Field;
import java.time.Clock;
import java.time.Instant;
import java.time.ZoneId;
import java.time.ZoneOffset;

import org.testng.annotations.Test;
import org.testng.annotations.DataProvider;

/**
 * Test system clock.
 */
@Test
public class TestClock_System {

    private static final ZoneId PARIS = ZoneId.of("Europe/Paris");
    private static final Clock systemUTC = Clock.systemUTC();

    public void test_withZone_same() {
        Clock test = Clock.system(PARIS);
        Clock changed = test.withZone(PARIS);
        assertSame(test, changed);
    }

    //-----------------------------------------------------------------------
    public void test_toString() {
        Clock test = Clock.system(PARIS);
        assertEquals(test.toString(), "SystemClock[Europe/Paris]");
    }

    //-----------------------------------------------------------------------
    @DataProvider(name="sampleSystemUTC")
    Object[][] provider_sampleSystemUTC() {
        return new Object[][] {
            {"Clock.systemUTC()#1",  Clock.systemUTC()},
            {"Clock.systemUTC()#2",  Clock.systemUTC()},
            {"Clock.system(ZoneOffset.UTC)#1",  Clock.system(ZoneOffset.UTC)},
            {"Clock.system(ZoneOffset.UTC)#2",  Clock.system(ZoneOffset.UTC)}
        };
    }

    // Test for 8073394
    @Test(dataProvider="sampleSystemUTC")
    public void test_systemUTC(String s, Clock clock) {
        if (clock != systemUTC) {
            throw new RuntimeException("Unexpected clock instance for " + s + ": "
                + "\n\texpected: " + toString(systemUTC)
                + "\n\tactual:   " + toString(clock));
        }
    }

    private static String toString(Clock c) {
        return c == null ? null :
               c + " " + c.getClass().getName() + "@" + System.identityHashCode(c);
    }

    //-----------------------------------------------------------------------

    private static String formatTime(String prefix, Instant time) {
        return prefix + ": " + time + " - seconds: "
                        + time.getEpochSecond() + ", nanos: "
                        + time.getNano();
    }

    public void test_ClockResolution() {
        Clock highestUTC = Clock.systemUTC();

        Instant start = Instant.ofEpochMilli(System.currentTimeMillis());

        try {
            // smoke test
            Instant system1 = Instant.ofEpochMilli(System.currentTimeMillis());
            Instant system2 = Instant.ofEpochMilli(System.currentTimeMillis());
            Instant highest1 = highestUTC.instant();
            Instant highest2 = highestUTC.instant();
            System.out.println(formatTime("\nsystemUTC #1            ", system1));
            System.out.println(formatTime("systemUTC #2            ", system2));
            System.out.println(formatTime("highestResolutionUTC #1 ", highest1));
            System.out.println(formatTime("highestResolutionUTC #2 ", highest2));

            if (system2.isBefore(system1)) {
                System.err.println("system2 is before system1!");
                System.err.println(formatTime("\n\tsystem1", system1));
                System.err.println(formatTime("\n\tsystem2", system2));
                throw new RuntimeException("system2 is before system1!"
                        + formatTime("\n\tsystem1", system1)
                        + formatTime("\n\tsystem2", system2));
            }
            if (highest2.isBefore(highest1)) {
                System.err.println("highest2 is before highest1!");
                System.err.println(formatTime("\n\thighest1", system1));
                System.err.println(formatTime("\n\tsystem2", highest2));
                throw new RuntimeException("highest2 is before system1!"
                        + formatTime("\n\thighest1", system1)
                        + formatTime("\n\tsystem2", highest2));
            }

            // better test - but depends on implementation details.
            // we're not rounding - so highest1 should be greater or equal to
            // system1
            system1 = Instant.ofEpochMilli(System.currentTimeMillis());
            highest1 = highestUTC.instant();

            System.out.println(formatTime("\nsystemUTC            ", system1));
            System.out.println(formatTime("highestResolutionUTC ", highest1));

            if (highest1.isBefore(system1)) {
                System.err.println("highest1 is before system1!");
                System.err.println(formatTime("\n\tsystem1", system1));
                System.err.println(formatTime("\n\thighest1", highest1));
                throw new RuntimeException("highest1 is before system1!"
                        + formatTime("\n\tsystem1", system1)
                        + formatTime("\n\thighest1", highest1));
            }

            int count=0;
            // let's preheat the system a bit:
            int lastNanos = 0;
            for (int i = 0; i < 1000 ; i++) {
                system1 = Instant.ofEpochMilli(System.currentTimeMillis());
                final int sysnan = system1.getNano();
                int nanos;
                do {
                    highest1 = highestUTC.instant();
                    nanos = highest1.getNano();
                } while (nanos == lastNanos); // Repeat to get a different value
                lastNanos = nanos;

                if ((nanos % 1000000) > 0) {
                    count++; // we have micro seconds
                }
                if ((sysnan % 1000000) > 0) {
                    throw new RuntimeException("Expected only millisecconds "
                            + "precision for systemUTC, found "
                            + (sysnan % 1000000) + " remainder.");
                }
            }
            System.out.println("\nNumber of time stamps which had better than"
                    + " millisecond precision: "+count+"/"+1000);
            System.out.println(formatTime("\nsystemUTC            ", system1));
            System.out.println(formatTime("highestResolutionUTC ", highest1));
            if (count == 0) {
                System.err.println("Something is strange: no microsecond "
                        + "precision with highestResolutionUTC?");
                throw new RuntimeException("Micro second preccision not reached");
            }

            // check again
            if (highest1.isBefore(system1)) {
                System.err.println("highest1 is before system1!");
                System.err.println(formatTime("\n\tsystem1", system1));
                System.err.println(formatTime("\n\thighest1", highest1));
                throw new RuntimeException("highest1 is before system1!"
                        + formatTime("\n\tsystem1", system1)
                        + formatTime("\n\thighest1", highest1));
            }

            // leap of faith: ensure that highest1 is from within 10 secs of
            //   system1
            if (highest1.toEpochMilli() != system1.toEpochMilli()) {
                long delta = highest1.getEpochSecond() - system1.getEpochSecond();
                if (delta > 10) {
                    throw new RuntimeException("Unexpected long delay between two clocks ("
                            + delta + " seconds)"
                            + formatTime("\n\t system1", system1)
                            + formatTime("\n\t highest1", highest1));

                }
            } else {
                System.out.println("You won the lottery: the two dates are within 1 millisecond!\n");
            }

        } finally {
            Instant stop = Instant.ofEpochMilli(System.currentTimeMillis());
            if (start.isAfter(stop)) {
                // This should not happen - but can (un)probably be observed
                // when switching to summer time, or if another application
                // is switching the system date...
                System.err.println("Cannot test - date was setback: "
                        + formatTime("\n\tstarted at", start)
                        + formatTime("\n\tstopped at", stop) + "\n");
                return; // will prevent exceptions from being propagated.
            }
        }
    }

    static final long MAX_OFFSET = 0x0100000000L;
    static final long MIN_OFFSET = -MAX_OFFSET;

    // A helper class to test that SystemClock correctly recomputes
    // its offset.
    static class SystemClockOffset {

        static final int MILLIS_IN_SECOND = 1000;
        static final int NANOS_IN_MILLI = 1000_000;
        static final int NANOS_IN_MICRO = 1000;
        static final int NANOS_IN_SECOND = 1000_000_000;

        static final boolean verbose = true;
        static final Clock systemUTC = Clock.systemUTC();
        static final Field offsetField;

        static {
            try {
                offsetField = Class.forName("java.time.Clock$SystemClock").getDeclaredField("offset");
                offsetField.setAccessible(true);
            } catch (ClassNotFoundException | NoSuchFieldException ex) {
                throw new ExceptionInInitializerError(ex);
            }
        }

        static enum Answer {

            YES, // isOffLimit = YES:   we must get -1
            NO, // isOffLimit = NO:    we must not not get -1
            MAYBE  // isOffLimit = MAYBE: we might get -1 or a valid adjustment.
        };

        static long distance(long one, long two) {
            return one > two ? Math.subtractExact(one, two)
                    : Math.subtractExact(two, one);
        }

        static Answer isOffLimits(long before, long after, long offset) {
            long relativeDistanceBefore = distance(before, offset);
            long relativeDistanceAfter = distance(after, offset);
            if (relativeDistanceBefore >= MAX_OFFSET && relativeDistanceAfter >= MAX_OFFSET) {
                return Answer.YES;
            }
            if (relativeDistanceBefore < MAX_OFFSET && relativeDistanceAfter < MAX_OFFSET) {
                if (relativeDistanceBefore == 0 || relativeDistanceAfter == 0) {
                    return Answer.MAYBE; // unlucky case where
                }
                return Answer.NO;
            }
            return Answer.MAYBE;
        }

        static void testWithOffset(String name, long offset)
                throws IllegalAccessException {
            testWithOffset(name, offset, systemUTC);
        }

        static void testWithOffset(String name, long offset, Clock clock)
                throws IllegalAccessException {
            offsetField.set(clock, offset);
            long beforeMillis = System.currentTimeMillis();
            final Instant instant = clock.instant();
            long afterMillis = System.currentTimeMillis();
            long actualOffset = offsetField.getLong(clock);
            long instantMillis = instant.getEpochSecond() * MILLIS_IN_SECOND
                    + instant.getNano() / NANOS_IN_MILLI;
            if (instantMillis < beforeMillis || instantMillis > afterMillis) {
                throw new RuntimeException(name
                        + ": Invalid instant: " + instant
                        + " (~" + instantMillis + "ms)"
                        + " when time in millis is in ["
                        + beforeMillis + ", " + afterMillis
                        + "] and offset in seconds is " + offset);
            }
            Answer isOffLimits = isOffLimits(beforeMillis / MILLIS_IN_SECOND,
                    afterMillis / MILLIS_IN_SECOND, offset);
            switch (isOffLimits) {
                case YES:
                    if (actualOffset == offset) {
                        throw new RuntimeException(name
                                + ": offset was offlimit but was not recomputed "
                                + " when time in millis is in ["
                                + beforeMillis + ", " + afterMillis
                                + "] and offset in seconds was " + offset);
                    }
                    break;
                case NO:
                    if (actualOffset != offset) {
                        throw new RuntimeException(name
                                + ": offset was not offlimit but was recomputed.");
                    }
                    break;
                default:
                    break;
            }
            if (distance(actualOffset, instant.getEpochSecond()) >= MAX_OFFSET) {
                throw new RuntimeException(name + ": Actual offset is too far off:"
                        + " offset=" + actualOffset
                        + "instant.seconds=" + instant.getEpochSecond());
            }
            long adjustment = (instant.getEpochSecond() - actualOffset) * NANOS_IN_SECOND
                    + instant.getNano();
            validateAdjustment(name, actualOffset, beforeMillis, afterMillis, adjustment);
        }

        static void validateAdjustment(String name, long offset, long beforeMillis,
                long afterMillis, long adjustment) {
            System.out.println("Validating adjustment: " + adjustment);
            long expectedMax = distance(offset, beforeMillis / MILLIS_IN_SECOND)
                    * NANOS_IN_SECOND
                    + (beforeMillis % MILLIS_IN_SECOND) * NANOS_IN_MILLI
                    + (afterMillis - beforeMillis + 1) * NANOS_IN_MILLI;
            long absoluteAdjustment = distance(0, adjustment);
            if (absoluteAdjustment > expectedMax) {
                long adjSec = absoluteAdjustment / NANOS_IN_SECOND;
                long adjMil = (absoluteAdjustment % NANOS_IN_SECOND) / NANOS_IN_MILLI;
                long adjMic = (absoluteAdjustment % NANOS_IN_MILLI) / NANOS_IN_MICRO;
                long adjNan = (absoluteAdjustment % NANOS_IN_MICRO);
                long expSec = expectedMax / NANOS_IN_SECOND;
                long expMil = (expectedMax % NANOS_IN_SECOND) / NANOS_IN_MILLI;
                long expMic = (expectedMax % NANOS_IN_MILLI) / NANOS_IN_MICRO;
                long expNan = (expectedMax % NANOS_IN_MICRO);
                System.err.println("Excessive adjustment: " + adjSec + "s, "
                        + adjMil + "ms, " + adjMic + "mics, " + adjNan + "ns");
                System.err.println("Epected max: " + expSec + "s, "
                        + expMil + "ms, " + expMic + "mics, " + expNan + "ns");

                throw new RuntimeException(name
                        + ": Excessive adjustment: " + adjustment
                        + " when time in millis is in ["
                        + beforeMillis + ", " + afterMillis
                        + "] and offset in seconds is " + offset);
            }
        }
    }

    public void test_OffsetRegular() throws IllegalAccessException {
        System.out.println("*** Testing regular cases ***");
        SystemClockOffset.testWithOffset("System.currentTimeMillis()/1000",
                System.currentTimeMillis()/1000);
        SystemClockOffset.testWithOffset("System.currentTimeMillis()/1000 - 1024",
                System.currentTimeMillis()/1000 - 1024);
        SystemClockOffset.testWithOffset("System.currentTimeMillis()/1000 + 1024",
                System.currentTimeMillis()/1000 + 1024);
    }

    public void test_OffsetLimits() throws IllegalAccessException {
        System.out.println("*** Testing limits ***");
        SystemClockOffset.testWithOffset("System.currentTimeMillis()/1000 - MAX_OFFSET + 1",
                System.currentTimeMillis()/1000 - MAX_OFFSET + 1);
        SystemClockOffset.testWithOffset("System.currentTimeMillis()/1000 + MAX_OFFSET - 1",
                System.currentTimeMillis()/1000 + MAX_OFFSET - 1);
        SystemClockOffset.testWithOffset("System.currentTimeMillis()/1000 - MAX_OFFSET",
                System.currentTimeMillis()/1000 - MAX_OFFSET);
        SystemClockOffset.testWithOffset("System.currentTimeMillis()/1000 + MAX_OFFSET",
                System.currentTimeMillis()/1000 + MAX_OFFSET);
        SystemClockOffset.testWithOffset("System.currentTimeMillis()/1000 - MAX_OFFSET - 1024",
                System.currentTimeMillis()/1000 - MAX_OFFSET - 1024);
        SystemClockOffset.testWithOffset("System.currentTimeMillis()/1000 + MAX_OFFSET + 1024",
                System.currentTimeMillis()/1000 + MAX_OFFSET + 1024);
        SystemClockOffset.testWithOffset("0", 0);
        SystemClockOffset.testWithOffset("-1", -1);
        SystemClockOffset.testWithOffset("Integer.MAX_VALUE + System.currentTimeMillis()/1000",
                ((long)Integer.MAX_VALUE) + System.currentTimeMillis()/1000);
        SystemClockOffset.testWithOffset("System.currentTimeMillis()/1000 - Integer.MIN_VALUE",
                System.currentTimeMillis()/1000 - Integer.MIN_VALUE);
        SystemClockOffset.testWithOffset("Long.MAX_VALUE", Long.MAX_VALUE);
        SystemClockOffset.testWithOffset("System.currentTimeMillis()/1000 - Long.MIN_VALUE",
                (Long.MIN_VALUE + System.currentTimeMillis()/1000)*-1);
    }
}
