/*
 * Copyright (C) 2008 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.core;

import junit.framework.Assert;
import junit.framework.TestCase;

import java.io.IOException;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.ServerSocket;
import java.net.Socket;
import java.net.SocketException;
import java.net.SocketTimeoutException;
import java.nio.channels.SocketChannel;
import java.util.concurrent.Semaphore;

import android.test.suitebuilder.annotation.LargeTest;
import android.test.suitebuilder.annotation.SmallTest;
import android.test.suitebuilder.annotation.Suppress;

/**
 * Regression tests for various socket related problems. And a few general
 * socket tests.
 */
public class SocketTest extends TestCase {

    private static final String NON_EXISTING_ADDRESS = "123.123.123.123";

    private static final String KNOW_GOOD_ADDRESS = "209.85.129.147";

    private static final String PACKAGE_DROPPING_ADDRESS = "191.167.0.1";
    
    // Test for basic bind/connect/accept behavior.
    @SmallTest
    public void testSocketSimple() throws Exception {
        ServerSocket ss;
        Socket s, s1;
        int port;

        IOException lastEx = null;

        ss = new ServerSocket();

        for (port = 9900; port < 9999; port++) {
            try {
                ss.bind(new InetSocketAddress("127.0.0.1", port));
                lastEx = null;
                break;
            } catch (IOException ex) {
                lastEx = ex;
            }
        }

        if (lastEx != null) {
            throw lastEx;
        }

        s = new Socket("127.0.0.1", port);

        s1 = ss.accept();

        s.getOutputStream().write(0xa5);

        assertEquals(0xa5, s1.getInputStream().read());

        s1.getOutputStream().write(0x5a);
        assertEquals(0x5a, s.getInputStream().read());
    }
    
    // Regression test for #820068: Wildcard address
    @SmallTest
    public void testWildcardAddress() throws Exception {
        Socket s2 = new Socket();
        s2.bind(new InetSocketAddress((InetAddress) null, 12345));
        byte[] addr = s2.getLocalAddress().getAddress();
        for (int i = 0; i < 4; i++) {
            assertEquals("Not the wildcard address", 0, addr[i]);
        }
    }
    
    // Regression test for #865753: server sockets not closing properly
    @SmallTest
    public void testServerSocketClose() throws Exception {
        ServerSocket s3 = new ServerSocket(23456);
        s3.close();
        ServerSocket s4 = new ServerSocket(23456);
        s4.close();
    }
    
    // Regression test for #876985: SO_REUSEADDR not working properly
    
    private Exception serverError = null;
    
    @LargeTest
    public void testSetReuseAddress() throws IOException {
        InetSocketAddress addr = new InetSocketAddress(8383);

        final ServerSocket serverSock = new ServerSocket();
        serverSock.setReuseAddress(true);
        serverSock.bind(addr);

        final Semaphore semThreadEnd = new Semaphore(0);
        new Thread() {
            @Override
            public void run() {
                try {
                    Socket sock = serverSock.accept();
                    sock.getInputStream().read();
                    sock.close();
                } catch (IOException e) {
                    serverError = e;
                }
                semThreadEnd.release();
            }
        }.start();

        // Give the server a bit of time for startup
        try {
            Thread.sleep(2000);
        } catch (InterruptedException ex) {
            // Ignored.
        }
        
        Socket client = new Socket("localhost", 8383);
        client.getOutputStream().write(1);
        // Just leave this connection open from the client side. It will be
        // closed from the server side so the server stays in the TIME_WAIT
        // state for a while. setReuseAddress() should be able to handle this.

        try {
            semThreadEnd.acquire();
        } catch (InterruptedException e) {
            // ignore
        }
        serverSock.close();

        ServerSocket serverSock2 = new ServerSocket();
        serverSock2.setReuseAddress(true);
        serverSock2.bind(addr);
        serverSock2.close();
        
        if (serverError != null) {
            throw new RuntimeException("Server must complete without error", serverError);
        }
    }

    // Regression for 916701, a wrong exception was thrown after timeout of
    // a ServerSocket.
    @LargeTest
    public void testTimeoutException() throws IOException {
        ServerSocket s = new ServerSocket(9800);
        s.setSoTimeout(2000);
        try {
            s.accept();
        } catch (SocketTimeoutException e) {
            // this is ok.
        }
    }

    // Regression for issue 1001980, openening a SocketChannel threw an Exception
    @SmallTest
    public void testNativeSocketChannelOpen() throws IOException {
        SocketChannel.open();
    }

    // Regression test for issue 1018016, connecting ignored a set timeout.
    @LargeTest
    public void testSocketSetSOTimeout() throws IOException {
        Socket sock = new Socket();
        int timeout = 5000;
        long start = System.currentTimeMillis();
        try {
            sock.connect(new InetSocketAddress(NON_EXISTING_ADDRESS, 80), timeout);
        } catch (SocketTimeoutException e) {
            // expected
            long delay = System.currentTimeMillis() - start;
            if (Math.abs(delay - timeout) > 1000) {
                fail("timeout was not accurate. expected: " + timeout
                        + " actual: " + delay + " miliseconds.");
            }
        } finally {
            try {
                sock.close();
            } catch (IOException ioe) {
                // ignore
            }
        }
    }
    
    /**
     * Regression test for 1062928: Dotted IP addresses (e.g., 192.168.100.1)
     * appear to be broken in the M5 SDK.
     * 
     * Tests that a connection given a ip-addressv4 such as 192.168.100.100 does
     * not fail - sdk m5 seems only to accept dns names instead of ip numbers.
     * ip 209.85.129.147 (one address of www.google.com) on port 80 (http) is
     * used to test the connection.
     */

// Commenting out this test since it is flaky, even at the best of times.  See
// #1191317 for Info.
    @Suppress
    public void disable_testConnectWithIP4IPAddr() {
        // call a Google Web server
        InetSocketAddress scktAddrss = new InetSocketAddress(KNOW_GOOD_ADDRESS,
                80);
        Socket clntSckt = new Socket();
        try {
            clntSckt.connect(scktAddrss, 5000);
        } catch (Throwable e) {
            fail("connection problem:" + e.getClass().getName() + ": "
                    + e.getMessage());
        } finally {
            try {
                clntSckt.close();
            } catch (Exception e) {
                // ignore
            }
        }
    }

    
    // Regression test for #1058962: Socket.close() does not cause
    // socket.connect() to return immediately.
    private Socket client;
    
    private Exception error;
    
    private boolean connected;

// This test isn't working now, but really should work.
// TODO Enable this test again.

    @Suppress
    public void disable_testSocketConnectClose() {
        try {
            client = new Socket();
            
            new Thread() {
                @Override
                public void run() {
                    try {
                        client.connect(new InetSocketAddress(PACKAGE_DROPPING_ADDRESS, 1357));
                    } catch (Exception ex) {
                        error = ex;
                    }
                    
                    connected = true;
                }
            }.start();
            
            Thread.sleep(1000);
            
            Assert.assertNull("Connect must not fail immediately. Maybe try different address.", error);
            Assert.assertFalse("Connect must not succeed. Maybe try different address.", connected);
            
            client.close();
            
            Thread.sleep(1000);

            if (error == null) {
                fail("Socket connect still ongoing");
            } else if (!(error instanceof SocketException)) {
                fail("Socket connect interrupted with wrong error: " + error.toString());
            }
            
        } catch (Exception ex) {
            throw new RuntimeException(ex);
        }
            
    }
    
}
