/*
 * Copyright (C) 2011 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 libcore.java.net;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNotSame;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;

import java.net.Inet4Address;
import java.net.Inet6Address;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.NetworkInterface;
import java.net.UnknownHostException;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.Set;
import junitparams.JUnitParamsRunner;
import junitparams.Parameters;
import libcore.libcore.util.SerializationTester;
import libcore.net.InetAddressUtils;
import org.junit.Test;
import org.junit.runner.RunWith;

@RunWith(JUnitParamsRunner.class)
public class InetAddressTest {
    private static final byte[] LOOPBACK4_BYTES = new byte[] { 127, 0, 0, 1 };
    private static final byte[] LOOPBACK6_BYTES = { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1 };

    private static final String[] INVALID_IPv4_AND_6_NUMERIC_ADDRESSES = new String[] {
        // IPv4 addresses may not be surrounded by square brackets.
        "[127.0.0.1]",

        // Trailing dots are not allowed.
        "1.2.3.4.",
        // Nor is any kind of trailing junk.
        "1.2.3.4hello",

        // Out of range.
        "256.2.3.4",
        "1.256.3.4",
        "1.2.256.4",
        "1.2.3.256",

        // Deprecated.
        "1.2.3",
        "1.2",
        "1",
        "1234",
        "0", // Single out the deprecated form of the ANY address.

        // Older Harmony tests expected this to be resolved to 255.255.255.255.
        "4294967295", // 0xffffffffL,

        // Hex. Not supported by Android but supported by the RI.
        "0x1.0x2.0x3.0x4",
        "0x7f.0x00.0x00.0x01",
        "7f.0.0.1",

        // Octal. Not supported by Android but supported by the RI. In the RI, if any of the numbers
        // cannot be treated as a decimal the entire IP is interpreted differently, leading to
        // "0177.00.00.01" -> 177.0.0.1, but "0177.0x0.00.01" -> 127.0.0.1.
        // Android does not do this.
        "0256.00.00.01", // Historically, this could have been interpreted as 174.0.0.1.

        // Negative numbers.
        "-1.0.0.1",
        "1.-1.0.1",
        "1.0.-1.1",
        "1.0.0.-1",

        // Invalid IPv6 addresses
        "FFFF:FFFF",
    };

    private static final String VALID_IPv6_ADDRESSES[] = {
        "::1.2.3.4",
        "::",
        "::",
        "1::0",
        "1::",
        "::1",
        "FFFF:FFFF:FFFF:FFFF:FFFF:FFFF:FFFF:FFFF",
        "FFFF:FFFF:FFFF:FFFF:FFFF:FFFF:255.255.255.255",
        "0:0:0:0:0:0:0:0",
        "0:0:0:0:0:0:0.0.0.0",
        "::255.255.255.255",
        "::FFFF:0.0.0.0",
        "F:F:F:F:F:F:F:F",
    };

    private static Inet6Address loopback6() throws Exception {
        return (Inet6Address) InetAddress.getByAddress(LOOPBACK6_BYTES);
    }

    private static Inet6Address localhost6() throws Exception {
        return (Inet6Address) InetAddress.getByAddress("ip6-localhost", LOOPBACK6_BYTES);
    }

    public static String[][] validNumericAddressesAndStringRepresentation() {
        return new String[][]{
            // Regular IPv4.
            { "1.2.3.4", "/1.2.3.4" },

            // Regular IPv6.
            { "2001:4860:800d::68", "/2001:4860:800d::68" },

            // Mapped IPv4
            { "::ffff:127.0.0.1", "/127.0.0.1" },

            // Optional square brackets around IPv6 addresses, including mapped IPv4.
            { "[2001:4860:800d::68]", "/2001:4860:800d::68" },
            { "[::ffff:127.0.0.1]", "/127.0.0.1" },

            // Android does not recognize Octal (leading 0) cases: they are treated as decimal.
            { "0177.00.00.01", "/177.0.0.1" },
        };
    }

    @Parameters(method = "validNumericAddressesAndStringRepresentation")
    @Test
    public void test_parseNumericAddress(String address, String expectedString) throws Exception {
        assertEquals(expectedString, InetAddress.parseNumericAddress(address).toString());
    }

    @Test
    public void test_parseNumericAddress_notNumeric() throws Exception {
        try {
            InetAddress.parseNumericAddress("example.com"); // Not numeric.
            fail();
        } catch (IllegalArgumentException expected) {
        }

        // Strange special cases, for compatibility with InetAddress.getByName.
        assertTrue(InetAddress.parseNumericAddress(null).isLoopbackAddress());
        assertTrue(InetAddress.parseNumericAddress("").isLoopbackAddress());
    }

    @Parameters(method = "invalidNumericAddresses")
    @Test
    public void test_parseNumericAddress_invalid(String invalid) throws Exception {
        try {
            InetAddress.parseNumericAddress(invalid);
            fail(invalid);
        } catch (IllegalArgumentException expected) {
        }
    }

    public static String[] validNumericAddresses() {
        return new String[] {
            // IPv4
            "1.2.3.4",
            "127.0.0.1",

            // IPv6
            "::1",
            "2001:4860:800d::68",

            // Mapped IPv4
            "::ffff:127.0.0.1",

            // Optional square brackets around IPv6 addresses, including mapped IPv4.
            "[2001:4860:800d::68]",
            "[::ffff:127.0.0.1]",

            // Android does not handle Octal (leading 0) cases: they are treated as decimal.
            "0177.00.00.01",
        };
    }

    @Parameters(method = "validNumericAddresses")
    @Test
    public void test_isNumeric(String valid) throws Exception {
        assertTrue(InetAddress.isNumeric(valid));
    }

    @Test
    public void test_isNumeric_notNumeric_null() throws Exception {
        try {
            boolean result = InetAddress.isNumeric(null);
            fail("Expected isNumeric(null) to throw a NPE but instead returned " + result);
        } catch (NullPointerException expected) {
        }
    }

    @Test
    public void test_isNumeric_notNumeric_empty() throws Exception {
        assertFalse(InetAddress.isNumeric(""));
    }

    @Test
    public void test_isNumeric_notNumeric() throws Exception {
        // Negative test
        assertFalse(InetAddress.isNumeric("example.com"));
    }

    @Parameters(method = "invalidNumericAddresses")
    @Test
    public void test_isNumeric_invalid(String invalid) {
        assertFalse(invalid, InetAddress.isNumeric(invalid));
    }

    @Test
    public void test_isLinkLocalAddress() throws Exception {
        assertFalse(InetAddress.getByName("127.0.0.1").isLinkLocalAddress());
        assertFalse(InetAddress.getByName("::ffff:127.0.0.1").isLinkLocalAddress());
        assertTrue(InetAddress.getByName("169.254.1.2").isLinkLocalAddress());

        assertFalse(InetAddress.getByName("fec0::").isLinkLocalAddress());
        assertTrue(InetAddress.getByName("fe80::").isLinkLocalAddress());
    }

    @Test
    public void test_isMCSiteLocalAddress() throws Exception {
        assertFalse(InetAddress.getByName("239.254.255.255").isMCSiteLocal());
        assertTrue(InetAddress.getByName("239.255.0.0").isMCSiteLocal());
        assertTrue(InetAddress.getByName("239.255.255.255").isMCSiteLocal());
        assertFalse(InetAddress.getByName("240.0.0.0").isMCSiteLocal());

        assertFalse(InetAddress.getByName("ff06::").isMCSiteLocal());
        assertTrue(InetAddress.getByName("ff05::").isMCSiteLocal());
        assertTrue(InetAddress.getByName("ff15::").isMCSiteLocal());
    }

    @Test
    public void test_isReachable() throws Exception {
        // http://code.google.com/p/android/issues/detail?id=20203
        String s = "aced0005737200146a6176612e6e65742e496e6574416464726573732d9b57af"
                + "9fe3ebdb0200034900076164647265737349000666616d696c794c0008686f737"
                + "44e616d657400124c6a6176612f6c616e672f537472696e673b78704a7d9d6300"
                + "00000274000e7777772e676f6f676c652e636f6d";
        InetAddress inetAddress = InetAddress.getByName("www.google.com");
        new SerializationTester<InetAddress>(inetAddress, s) {
            @Override protected void verify(InetAddress deserialized) throws Exception {
                deserialized.isReachable(500);
                for (NetworkInterface nif
                        : Collections.list(NetworkInterface.getNetworkInterfaces())) {
                    deserialized.isReachable(nif, 20, 500);
                }
            }
            @Override protected boolean equals(InetAddress a, InetAddress b) {
                return a.getHostName().equals(b.getHostName());
            }
        }.test();
    }

    @Test
    public void test_isReachable_neverThrows() throws Exception {
        InetAddress inetAddress = InetAddress.getByName("www.google.com");

        final NetworkInterface netIf = NetworkInterface.getByName("fake0");
        if (netIf == null) {
            System.logI("Skipping test_isReachable_neverThrows because fake0 isn't available");
            return;
        }

        assertFalse(inetAddress.isReachable(netIf, 256, 500));
    }

    // IPPROTO_ICMP socket kind requires setting ping_group_range. This is set on boot on Android.
    // When running on host, make sure you run the command:
    //   sudo sysctl -w net.ipv4.ping_group_range="0 65535"
    @Test
    public void test_isReachable_by_ICMP() throws Exception {
        InetAddress[] inetAddresses = InetAddress.getAllByName("www.google.com");
        for (InetAddress ia : inetAddresses) {
            // ICMP is not reliable, allow 5 attempts to each IP address before failing.
            // If any address is reachable then that's sufficient.
            if (ia.isReachableByICMP(5 * 1000 /* ICMP timeout */)) {
                return;
            }
        }
        fail();
    }

    @Test
    public void test_inUnreachable() throws Exception {
        // IPv6 discard prefix. RFC 6666.
        final InetAddress blackholeAddress = InetAddress.getByName("100::1");
        assertFalse(blackholeAddress.isReachable(1000));
    }

    @Test
    public void test_isSiteLocalAddress() throws Exception {
        assertFalse(InetAddress.getByName("144.32.32.1").isSiteLocalAddress());
        assertTrue(InetAddress.getByName("10.0.0.1").isSiteLocalAddress());
        assertTrue(InetAddress.getByName("172.16.0.1").isSiteLocalAddress());
        assertFalse(InetAddress.getByName("172.32.0.1").isSiteLocalAddress());
        assertTrue(InetAddress.getByName("192.168.0.1").isSiteLocalAddress());

        assertFalse(InetAddress.getByName("fc00::").isSiteLocalAddress());
        assertTrue(InetAddress.getByName("fec0::").isSiteLocalAddress());
    }

    public static String[] invalidNumericAddresses() {
        return INVALID_IPv4_AND_6_NUMERIC_ADDRESSES;
    }

    @SuppressWarnings("ResultOfMethodCallIgnored")
    @Parameters(method = "invalidNumericAddresses")
    @Test
    public void test_getByName_invalid(String invalid) throws Exception {
        try {
            InetAddress.getByName(invalid);
            String msg = "Invalid IP address incorrectly recognized as valid: \"" + invalid + "\"";
            if (InetAddressUtils.parseNumericAddressNoThrowStripOptionalBrackets(invalid) == null) {
                msg += " (it was probably unexpectedly resolved by this network's DNS)";
            }
            msg += ".";
            fail(msg);
        } catch (UnknownHostException expected) {
        }

        // exercise negative cache
        try {
            InetAddress.getByName(invalid);
            fail("Invalid IP address incorrectly recognized as valid: "
                + invalid);
        } catch (Exception expected) {
        }
    }

    public static String[] validIPv6Addresses() {
        return VALID_IPv6_ADDRESSES;
    }

    @Parameters(method = "validIPv6Addresses")
    @Test
    public void test_getByName_valid(String valid) throws Exception {
        InetAddress.getByName(valid);

        // exercise positive cache
        InetAddress.getByName(valid);

        // when wrapped in [..]
        String tempIPAddress = "[" + valid + "]";
        InetAddress.getByName(tempIPAddress);
    }

    @Test
    public void test_getLoopbackAddress() throws Exception {
        assertTrue(InetAddress.getLoopbackAddress().isLoopbackAddress());
    }

    @Test
    public void test_equals() throws Exception {
        InetAddress addr = InetAddress.getByName("239.191.255.255");
        assertTrue(addr.equals(addr));
        assertTrue(loopback6().equals(localhost6()));
        assertFalse(addr.equals(loopback6()));

        assertTrue(Inet4Address.LOOPBACK.equals(Inet4Address.LOOPBACK));

        // http://b/4328294 - the scope id isn't included when comparing Inet6Address instances.
        byte[] bs = new byte[16];
        assertEquals(Inet6Address.getByAddress("1", bs, 1), Inet6Address.getByAddress("2", bs, 2));
    }

    @Test
    public void test_getHostAddress() throws Exception {
        assertEquals("::1", localhost6().getHostAddress());
        assertEquals("::1", InetAddress.getByName("::1").getHostAddress());

        assertEquals("127.0.0.1", Inet4Address.LOOPBACK.getHostAddress());

        // IPv4 mapped address
        assertEquals("127.0.0.1", InetAddress.getByName("::ffff:127.0.0.1").getHostAddress());

        InetAddress aAddr = InetAddress.getByName("224.0.0.0");
        assertEquals("224.0.0.0", aAddr.getHostAddress());


        try {
            InetAddress.getByName("1");
            fail();
        } catch (UnknownHostException expected) {
        }

        byte[] bAddr = {
            (byte) 0xFE, (byte) 0x80, (byte) 0x00, (byte) 0x00,
            (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,
            (byte) 0x02, (byte) 0x11, (byte) 0x25, (byte) 0xFF,
            (byte) 0xFE, (byte) 0xF8, (byte) 0x7C, (byte) 0xB2
        };
        aAddr = Inet6Address.getByAddress(bAddr);
        String aString = aAddr.getHostAddress();
        assertTrue(aString.equals("fe80:0:0:0:211:25ff:fef8:7cb2") || aString.equals("fe80::211:25ff:fef8:7cb2"));

        byte[] cAddr = {
            (byte) 0xFF, (byte) 0xFF, (byte) 0xFF, (byte) 0xFF,
            (byte) 0xFF, (byte) 0xFF, (byte) 0xFF, (byte) 0xFF,
            (byte) 0xFF, (byte) 0xFF, (byte) 0xFF, (byte) 0xFF,
            (byte) 0xFF, (byte) 0xFF, (byte) 0xFF, (byte) 0xFF
        };
        aAddr = Inet6Address.getByAddress(cAddr);
        assertEquals("ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff", aAddr.getHostAddress());

        byte[] dAddr = {
            (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,
            (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,
            (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,
            (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00
        };
        aAddr = Inet6Address.getByAddress(dAddr);
        aString = aAddr.getHostAddress();
        assertTrue(aString.equals("0:0:0:0:0:0:0:0") || aString.equals("::"));

        byte[] eAddr = {
            (byte) 0x00, (byte) 0x01, (byte) 0x02, (byte) 0x03,
            (byte) 0x04, (byte) 0x05, (byte) 0x06, (byte) 0x07,
            (byte) 0x08, (byte) 0x09, (byte) 0x0a, (byte) 0x0b,
            (byte) 0x0c, (byte) 0x0d, (byte) 0x0e, (byte) 0x0f
        };
        aAddr = Inet6Address.getByAddress(eAddr);
        assertEquals("1:203:405:607:809:a0b:c0d:e0f", aAddr.getHostAddress());

        byte[] fAddr = {
            (byte) 0x00, (byte) 0x10, (byte) 0x20, (byte) 0x30,
            (byte) 0x40, (byte) 0x50, (byte) 0x60, (byte) 0x70,
            (byte) 0x80, (byte) 0x90, (byte) 0xa0, (byte) 0xb0,
            (byte) 0xc0, (byte) 0xd0, (byte) 0xe0, (byte) 0xf0
        };
        aAddr = Inet6Address.getByAddress(fAddr);
        assertEquals("10:2030:4050:6070:8090:a0b0:c0d0:e0f0", aAddr.getHostAddress());
    }

    @Test
    public void test_hashCode() throws Exception {
        InetAddress addr1 = InetAddress.getByName("1.0.0.1");
        InetAddress addr2 = InetAddress.getByName("1.0.0.1");
        assertTrue(addr1.hashCode() == addr2.hashCode());

        assertTrue(loopback6().hashCode() == localhost6().hashCode());
    }

    public static String[][] validAddressesAndStringRepresentation() {
        return new String[][] {
            { "::1.2.3.4", "/::1.2.3.4" },
            { "::", "/::" },
            { "1::0", "/1::" },
            { "1::", "/1::" },
            { "::1", "/::1" },
            { "FFFF:FFFF:FFFF:FFFF:FFFF:FFFF:FFFF:FFFF", "/ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff" },
            { "FFFF:FFFF:FFFF:FFFF:FFFF:FFFF:255.255.255.255", "/ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff" },
            { "0:0:0:0:0:0:0:0", "/::" },
            { "0:0:0:0:0:0:0.0.0.0", "/::" },
        };
    }

    @Parameters(method = "validAddressesAndStringRepresentation")
    @Test
    public void test_toString(String address, String expectedString) throws Exception {
        InetAddress ia = InetAddress.getByName(address);
        String result = ia.toString();
        assertNotNull(result);
        assertEquals(expectedString, result);
    }

    @Test
    public void test_getHostNameCaches() throws Exception {
        InetAddress inetAddress = InetAddress.getByAddress(LOOPBACK6_BYTES);

        // There should be no cached name.
        assertEquals("::1", getHostStringWithoutReverseDns(inetAddress));

        // Force the reverse-DNS lookup.
        assertEquals("ip6-localhost", inetAddress.getHostName());

        // The cached name should now be different.
        assertEquals("ip6-localhost", getHostStringWithoutReverseDns(inetAddress));
    }

    @Test
    public void test_getByAddress_loopbackIpv4() throws Exception {
        InetAddress inetAddress = InetAddress.getByAddress(LOOPBACK4_BYTES);
        checkInetAddress(LOOPBACK4_BYTES, "localhost", inetAddress);
        assertTrue(inetAddress.isLoopbackAddress());
    }

    @Test
    public void test_getByAddress_loopbackIpv6() throws Exception {
        InetAddress inetAddress = InetAddress.getByAddress(LOOPBACK6_BYTES);
        checkInetAddress(LOOPBACK6_BYTES, "ip6-localhost", inetAddress);
        assertTrue(inetAddress.isLoopbackAddress());
    }

    @Test
    public void test_getByName_loopbackIpv4() throws Exception {
        InetAddress inetAddress = InetAddress.getByName("127.0.0.1");
        checkInetAddress(LOOPBACK4_BYTES, "localhost", inetAddress);
        assertTrue(inetAddress.isLoopbackAddress());
    }

    @Test
    public void test_getByName_loopbackIpv6() throws Exception {
        InetAddress inetAddress = InetAddress.getByName("::1");
        checkInetAddress(LOOPBACK6_BYTES, "ip6-localhost", inetAddress);
        assertTrue(inetAddress.isLoopbackAddress());
    }

    @Test
    public void test_getByName_empty() throws Exception {
        InetAddress inetAddress = InetAddress.getByName("");
        checkInetAddress(LOOPBACK6_BYTES, "ip6-localhost", inetAddress);
        assertTrue(inetAddress.isLoopbackAddress());
    }

    @Test
    public void test_getAllByName_localhost() throws Exception {
        InetAddress[] inetAddresses = InetAddress.getAllByName("localhost");
        assertEquals(1, inetAddresses.length);
        InetAddress inetAddress = inetAddresses[0];
        checkInetAddress(LOOPBACK4_BYTES, "localhost", inetAddress);
        assertTrue(inetAddress.isLoopbackAddress());
    }

    @Test
    public void test_getAllByName_ip6_localhost() throws Exception {
        InetAddress[] inetAddresses = InetAddress.getAllByName("ip6-localhost");
        assertEquals(1, inetAddresses.length);
        InetAddress inetAddress = inetAddresses[0];
        checkInetAddress(LOOPBACK6_BYTES, "ip6-localhost", inetAddress);
        assertTrue(inetAddress.isLoopbackAddress());
    }

    @Test
    public void test_getByName_v6loopback() throws Exception {
        InetAddress inetAddress = InetAddress.getByName("::1");

        Set<InetAddress> expectedLoopbackAddresses =
                createSet(Inet4Address.LOOPBACK, Inet6Address.LOOPBACK);
        assertTrue(expectedLoopbackAddresses.contains(inetAddress));
    }

    @Test
    public void test_getByName_cloning() throws Exception {
        InetAddress[] addresses = InetAddress.getAllByName(null);
        InetAddress[] addresses2 = InetAddress.getAllByName(null);
        assertNotNull(addresses[0]);
        assertNotNull(addresses[1]);
        assertNotSame(addresses, addresses2);

        // Also assert that changes to the return value do not affect the cache
        // etc. i.e, that we return a copy.
        addresses[0] = null;
        addresses2 = InetAddress.getAllByName(null);
        assertNotNull(addresses2[0]);
        assertNotNull(addresses2[1]);
    }

    @Test
    public void test_getAllByName_null() throws Exception {
        InetAddress[] inetAddresses = InetAddress.getAllByName(null);
        assertEquals(2, inetAddresses.length);
        Set<InetAddress> expectedLoopbackAddresses =
                createSet(Inet4Address.LOOPBACK, Inet6Address.LOOPBACK);
        assertEquals(expectedLoopbackAddresses, createSet(inetAddresses));
    }

    // http://b/29311351
    @Test
    public void test_loopbackConstantsPreInitializedNames() {
        // Note: Inet6Address / Inet4Address equals() does not check host name.
        assertEquals("ip6-localhost", getHostStringWithoutReverseDns(Inet6Address.LOOPBACK));
        assertEquals("localhost", getHostStringWithoutReverseDns(Inet4Address.LOOPBACK));
    }

    private static void checkInetAddress(
        byte[] expectedAddressBytes, String expectedHostname, InetAddress actual) {
        assertArrayEquals(expectedAddressBytes, actual.getAddress());
        assertEquals(expectedHostname, actual.getHostName());

    }

    private static void assertArrayEquals(byte[] expected, byte[] actual) {
        assertTrue("Expected=" + Arrays.toString(expected) + ", actual=" + Arrays.toString(actual),
                Arrays.equals(expected, actual));
    }

    private static Set<InetAddress> createSet(InetAddress... members) {
        return new HashSet<InetAddress>(Arrays.asList(members));
    }

    private static String getHostStringWithoutReverseDns(InetAddress inetAddress) {
        // The InetAddress API provides no way of avoiding a DNS lookup, but InetSocketAddress
        // does via InetSocketAddress.getHostString().
        InetSocketAddress inetSocketAddress = new InetSocketAddress(inetAddress, 9999);
        return inetSocketAddress.getHostString();
    }
}
