/*
 * Licensed to Elasticsearch under one or more contributor
 * license agreements. See the NOTICE file distributed with
 * this work for additional information regarding copyright
 * ownership. Elasticsearch licenses this file to you 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 org.elasticsearch.common.network;

import org.elasticsearch.common.collect.Tuple;
import org.elasticsearch.test.ESTestCase;

import java.util.ArrayList;
import java.util.List;

import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.hasToString;

public class CidrsTests extends ESTestCase {
    public void testNullCidr() {
        try {
            Cidrs.cidrMaskToMinMax(null);
            fail("expected NullPointerException");
        } catch (NullPointerException e) {
            assertThat(e, hasToString(containsString("cidr")));
        }
    }

    public void testSplittingSlash() {
        List<String> cases = new ArrayList<>();
        cases.add("1.2.3.4");
        cases.add("1.2.3.4/32/32");
        cases.add("1.2.3.4/");
        cases.add("/");
        for (String test : cases) {
            try {
                Cidrs.cidrMaskToMinMax(test);
                fail("expected IllegalArgumentException after splitting");
            } catch (IllegalArgumentException e) {
                assertThat(e, hasToString(containsString("expected [a.b.c.d, e]")));
                assertThat(e, hasToString(containsString("splitting on \"/\"")));
            }
        }
    }

    public void testSplittingDot() {
        List<String> cases = new ArrayList<>();
        cases.add("1.2.3/32");
        cases.add("1/32");
        cases.add("1./32");
        cases.add("1../32");
        cases.add("1.../32");
        cases.add("1.2.3.4.5/32");
        cases.add("/32");
        for (String test : cases) {
            try {
                Cidrs.cidrMaskToMinMax(test);
                fail("expected IllegalArgumentException after splitting");
            } catch (IllegalArgumentException e) {
                assertThat(e, hasToString(containsString("unable to parse")));
                assertThat(e, hasToString(containsString("as an IP address literal")));
            }
        }
    }

    public void testValidSpecificCases() {
        List<Tuple<String, long[]>> cases = new ArrayList<>();
        cases.add(new Tuple<>("192.168.0.0/24", new long[]{(192L << 24) + (168 << 16), (192L << 24) + (168 << 16) + (1 << 8)}));
        cases.add(new Tuple<>("192.168.128.0/17", new long[]{(192L << 24) + (168 << 16) + (128 << 8), (192L << 24) + (168 << 16) + (128 << 8) + (1 << 15)}));
        cases.add(new Tuple<>("128.0.0.0/1", new long[]{128L << 24, (128L << 24) + (1L << 31)})); // edge case
        cases.add(new Tuple<>("0.0.0.0/0", new long[]{0, 1L << 32})); // edge case
        cases.add(new Tuple<>("0.0.0.0/1", new long[]{0, 1L << 31})); // edge case
        cases.add(new Tuple<>(
                "192.168.1.1/32",
                new long[]{(192L << 24) + (168L << 16) + (1L << 8) + 1L, (192L << 24) + (168L << 16) + (1L << 8) + 1L + 1})
        ); // edge case
        for (Tuple<String, long[]> test : cases) {
            long[] actual = Cidrs.cidrMaskToMinMax(test.v1());
            assertArrayEquals(test.v1(), test.v2(), actual);
        }
    }

    public void testInvalidSpecificOctetCases() {
        List<String> cases = new ArrayList<>();
        cases.add("256.0.0.0/8"); // first octet out of range
        cases.add("255.256.0.0/16"); // second octet out of range
        cases.add("255.255.256.0/24"); // third octet out of range
        cases.add("255.255.255.256/32"); // fourth octet out of range
        cases.add("abc.0.0.0/8"); // octet that can not be parsed
        cases.add("-1.0.0.0/8"); // first octet out of range
        cases.add("128.-1.0.0/16"); // second octet out of range
        cases.add("128.128.-1.0/24"); // third octet out of range
        cases.add("128.128.128.-1/32"); // fourth octet out of range

        for (String test : cases) {
            try {
                Cidrs.cidrMaskToMinMax(test);
                fail("expected invalid address");
            } catch (IllegalArgumentException e) {
                assertThat(e, hasToString(containsString("unable to parse")));
                assertThat(e, hasToString(containsString("as an IP address literal")));
            }
        }
    }

    public void testInvalidSpecificNetworkMaskCases() {
        List<String> cases = new ArrayList<>();
        cases.add("128.128.128.128/-1"); // network mask out of range
        cases.add("128.128.128.128/33"); // network mask out of range
        cases.add("128.128.128.128/abc"); // network mask that can not be parsed

        for (String test : cases) {
            try {
                Cidrs.cidrMaskToMinMax(test);
                fail("expected invalid network mask");
            } catch (IllegalArgumentException e) {
                assertThat(e, hasToString(containsString("network mask")));
            }
        }
    }

    public void testValidCombinations() {
        for (long i = 0; i < (1 << 16); i++) {
            String octetsString = Cidrs.octetsToString(Cidrs.longToOctets(i << 16));
            for (int mask = 16; mask <= 32; mask++) {
                String test = octetsString + "/" + mask;
                long[] actual = Cidrs.cidrMaskToMinMax(test);
                assertNotNull(test, actual);
                assertEquals(test, 2, actual.length);
                assertEquals(test, i << 16, actual[0]);
                assertEquals(test, (i << 16) + (1L << (32 - mask)), actual[1]);
            }
        }
    }

    public void testInvalidCombinations() {
        List<String> cases = new ArrayList<>();
        cases.add("192.168.0.1/24"); // invalid because fourth octet is not zero
        cases.add("192.168.1.0/16"); // invalid because third octet is not zero
        cases.add("192.1.0.0/8"); // invalid because second octet is not zero
        cases.add("128.0.0.0/0"); // invalid because first octet is not zero
        // create cases that have a bit set outside of the network mask
        int value = 1;
        for (int i = 0; i < 31; i++) {
            cases.add(Cidrs.octetsToCIDR(Cidrs.longToOctets(value), 32 - i - 1));
            value <<= 1;
        }

        for (String test : cases) {
            try {
                Cidrs.cidrMaskToMinMax(test);
                fail("expected invalid combination");
            } catch (IllegalArgumentException e) {
                assertThat(test, e, hasToString(containsString("invalid address/network mask combination")));
            }
        }
    }

    public void testRandomValidCombinations() {
        List<Tuple<String, Integer>> cases = new ArrayList<>();
        // random number of strings with valid octets and valid network masks
        for (int i = 0; i < randomIntBetween(1, 1024); i++) {
            int networkMask = randomIntBetween(0, 32);
            long mask = (1L << (32 - networkMask)) - 1;
            long address = randomLongInIPv4Range() & ~mask;
            cases.add(new Tuple<>(Cidrs.octetsToCIDR(Cidrs.longToOctets(address), networkMask), networkMask));
        }

        for (Tuple<String, Integer> test : cases) {
            long[] actual = Cidrs.cidrMaskToMinMax(test.v1());
            assertNotNull(test.v1(), actual);
            assertEquals(test.v1(), 2, actual.length);
            // assert the resulting block has the right size
            assertEquals(test.v1(), 1L << (32 - test.v2()), actual[1] - actual[0]);
        }
    }

    private long randomLongInIPv4Range() {
        return randomLong() & 0x00000000FFFFFFFFL;
    }
}
