/*
 * Copyright (C) 2015 The Android Open Source Project
 *
 * 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 android.net;

import android.net.SntpClient;
import android.util.Log;
import libcore.util.HexEncoding;

import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.net.SocketException;
import java.util.Arrays;
import junit.framework.TestCase;


public class SntpClientTest extends TestCase {
    private static final String TAG = "SntpClientTest";

    private static final int ORIGINATE_TIME_OFFSET = 24;
    private static final int TRANSMIT_TIME_OFFSET = 40;

    private static final int NTP_MODE_SERVER = 4;
    private static final int NTP_MODE_BROADCAST = 5;

    // From tcpdump (admittedly, an NTPv4 packet):
    //
    // Server, Leap indicator:  (0), Stratum 2 (secondary reference), poll 6 (64s), precision -20
    // Root Delay: 0.005447, Root dispersion: 0.002716, Reference-ID: 221.253.71.41
    //   Reference Timestamp:  3653932102.507969856 (2015/10/15 14:08:22)
    //   Originator Timestamp: 3653932113.576327741 (2015/10/15 14:08:33)
    //   Receive Timestamp:    3653932113.581012725 (2015/10/15 14:08:33)
    //   Transmit Timestamp:   3653932113.581012725 (2015/10/15 14:08:33)
    //     Originator - Receive Timestamp:  +0.004684958
    //     Originator - Transmit Timestamp: +0.004684958
    private static final String WORKING_VERSION4 =
            "240206ec" +
            "00000165" +
            "000000b2" +
            "ddfd4729" +
            "d9ca9446820a5000" +
            "d9ca9451938a3771" +
            "d9ca945194bd3fff" +
            "d9ca945194bd4001";

    private final SntpTestServer mServer = new SntpTestServer();
    private final SntpClient mClient = new SntpClient();

    public void testBasicWorkingSntpClientQuery() throws Exception {
        mServer.setServerReply(HexEncoding.decode(WORKING_VERSION4.toCharArray(), false));
        assertTrue(mClient.requestTime(mServer.getAddress(), mServer.getPort(), 500));
        assertEquals(1, mServer.numRequestsReceived());
        assertEquals(1, mServer.numRepliesSent());
    }

    public void testDnsResolutionFailure() throws Exception {
        assertFalse(mClient.requestTime("ntp.server.doesnotexist.example", 5000));
    }

    public void testTimeoutFailure() throws Exception {
        mServer.clearServerReply();
        assertFalse(mClient.requestTime(mServer.getAddress(), mServer.getPort(), 500));
        assertEquals(1, mServer.numRequestsReceived());
        assertEquals(0, mServer.numRepliesSent());
    }

    public void testIgnoreLeapNoSync() throws Exception {
        final byte[] reply = HexEncoding.decode(WORKING_VERSION4.toCharArray(), false);
        reply[0] |= (byte) 0xc0;
        mServer.setServerReply(reply);
        assertFalse(mClient.requestTime(mServer.getAddress(), mServer.getPort(), 500));
        assertEquals(1, mServer.numRequestsReceived());
        assertEquals(1, mServer.numRepliesSent());
    }

    public void testAcceptOnlyServerAndBroadcastModes() throws Exception {
        final byte[] reply = HexEncoding.decode(WORKING_VERSION4.toCharArray(), false);
        for (int i = 0; i <= 7; i++) {
            final String logMsg = "mode: " + i;
            reply[0] &= (byte) 0xf8;
            reply[0] |= (byte) i;
            mServer.setServerReply(reply);
            final boolean rval = mClient.requestTime(mServer.getAddress(), mServer.getPort(), 500);
            switch (i) {
                case NTP_MODE_SERVER:
                case NTP_MODE_BROADCAST:
                    assertTrue(logMsg, rval);
                    break;
                default:
                    assertFalse(logMsg, rval);
                    break;
            }
            assertEquals(logMsg, 1, mServer.numRequestsReceived());
            assertEquals(logMsg, 1, mServer.numRepliesSent());
        }
    }

    public void testAcceptableStrataOnly() throws Exception {
        final int STRATUM_MIN = 1;
        final int STRATUM_MAX = 15;

        final byte[] reply = HexEncoding.decode(WORKING_VERSION4.toCharArray(), false);
        for (int i = 0; i < 256; i++) {
            final String logMsg = "stratum: " + i;
            reply[1] = (byte) i;
            mServer.setServerReply(reply);
            final boolean rval = mClient.requestTime(mServer.getAddress(), mServer.getPort(), 500);
            if (STRATUM_MIN <= i && i <= STRATUM_MAX) {
                assertTrue(logMsg, rval);
            } else {
                assertFalse(logMsg, rval);
            }
            assertEquals(logMsg, 1, mServer.numRequestsReceived());
            assertEquals(logMsg, 1, mServer.numRepliesSent());
        }
    }

    public void testZeroTransmitTime() throws Exception {
        final byte[] reply = HexEncoding.decode(WORKING_VERSION4.toCharArray(), false);
        Arrays.fill(reply, TRANSMIT_TIME_OFFSET, TRANSMIT_TIME_OFFSET + 8, (byte) 0x00);
        mServer.setServerReply(reply);
        assertFalse(mClient.requestTime(mServer.getAddress(), mServer.getPort(), 500));
        assertEquals(1, mServer.numRequestsReceived());
        assertEquals(1, mServer.numRepliesSent());
    }


    private static class SntpTestServer {
        private final Object mLock = new Object();
        private final DatagramSocket mSocket;
        private final InetAddress mAddress;
        private final int mPort;
        private byte[] mReply;
        private int mRcvd;
        private int mSent;
        private Thread mListeningThread;

        public SntpTestServer() {
            mSocket = makeSocket();
            mAddress = mSocket.getLocalAddress();
            mPort = mSocket.getLocalPort();
            Log.d(TAG, "testing server listening on (" + mAddress + ", " + mPort + ")");

            mListeningThread = new Thread() {
                public void run() {
                    while (true) {
                        byte[] buffer = new byte[512];
                        DatagramPacket ntpMsg = new DatagramPacket(buffer, buffer.length);
                        try {
                            mSocket.receive(ntpMsg);
                        } catch (IOException e) {
                            Log.e(TAG, "datagram receive error: " + e);
                            break;
                        }
                        synchronized (mLock) {
                            mRcvd++;
                            if (mReply == null) { continue; }
                            // Copy transmit timestamp into originate timestamp.
                            // TODO: bounds checking.
                            System.arraycopy(ntpMsg.getData(), TRANSMIT_TIME_OFFSET,
                                             mReply, ORIGINATE_TIME_OFFSET, 8);
                            ntpMsg.setData(mReply);
                            ntpMsg.setLength(mReply.length);
                            try {
                                mSocket.send(ntpMsg);
                            } catch (IOException e) {
                                Log.e(TAG, "datagram send error: " + e);
                                break;
                            }
                            mSent++;
                        }
                    }
                    mSocket.close();
                }
            };
            mListeningThread.start();
        }

        private DatagramSocket makeSocket() {
            DatagramSocket socket;
            try {
                socket = new DatagramSocket(0, InetAddress.getLoopbackAddress());
            } catch (SocketException e) {
                Log.e(TAG, "Failed to create test server socket: " + e);
                return null;
            }
            return socket;
        }

        public void clearServerReply() {
            setServerReply(null);
        }

        public void setServerReply(byte[] reply) {
            synchronized (mLock) {
                mReply = reply;
                mRcvd = 0;
                mSent = 0;
            }
        }

        public InetAddress getAddress() { return mAddress; }
        public int getPort() { return mPort; }
        public int numRequestsReceived() { synchronized (mLock) { return mRcvd; } }
        public int numRepliesSent() { synchronized (mLock) { return mSent; } }
    }
}
