/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF 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.apache.ignite.internal.client.util;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeSet;
import java.util.UUID;
import org.apache.ignite.testframework.junits.common.GridCommonAbstractTest;
import org.junit.Test;

/**
 * Test for consistent hash management class.
 */
public class ClientConsistentHashSelfTest extends GridCommonAbstractTest {
    /** Replicas count. */
    private static final int REPLICAS = 512;

    /**
     * Test hash codes collisions.
     *
     * @throws Exception In case of any exception.
     */
    @Test
    public void testCollisions() throws Exception {
        Map<Integer, Set<UUID>> map = new HashMap<>();

        // Different nodes, but collide hash codes.
        Collection<UUID> nodes = new LinkedHashSet<>();

        // Generate several nodes with collide hash codes.
        while (nodes.size() < 10) {
            UUID uuid = UUID.randomUUID();
            int hashCode = uuid.hashCode();

            Set<UUID> set = map.get(hashCode);

            if (set == null)
                map.put(hashCode, set = new LinkedHashSet<>());

            set.add(uuid);

            if (set.size() > 1)
                nodes.addAll(set);
        }

        map.clear(); // Clean up.

        GridClientConsistentHash<UUID> hash = new GridClientConsistentHash<>();

        hash.addNodes(nodes, REPLICAS);

        boolean fail = false;

        for (UUID exp : nodes) {
            UUID act = hash.node(0, Arrays.asList(exp));

            if (exp.equals(act))
                info("Validation succeed [exp=" + exp + ", act=" + act + ']');
            else {
                info("Validation failed  [exp=" + exp + ", act=" + act + ']');

                fail = true;
            }
        }

        if (fail)
            fail("Failed to resolve consistent hash node, when node's hash codes collide: " + nodes);
    }

    /**
     * Test restrictions from internal {@link TreeSet} usage.
     *
     * @throws Exception In case of any exception.
     */
    @Test
    public void testTreeSetRestrictions() throws Exception {
        // Constructs hash without explicit node's comparator.
        GridClientConsistentHash<Object> hash = new GridClientConsistentHash<>();

        try {
            // Add several objects with the same hash without neither natural ordering nor comparator.
            hash.addNode(new Object() {
                @Override public int hashCode() {
                    return 0;
                }
            }, 1);
            hash.addNode(new Object() {
                @Override public int hashCode() {
                    return 0;
                }
            }, 1);

            fail("Expects failed due to internal TreeSet requires comparator or natural ordering.");
        }
        catch (ClassCastException e) {
            info("Expected fail due to internal TreeSet requires comparator or natural ordering: " + e.getMessage());
        }

        // Constructs hash with explicit node's comparator.
        hash = new GridClientConsistentHash<>(new Comparator<Object>() {
            @Override public int compare(Object o1, Object o2) {
                // Such comparator is invalid for production code, but acceptable for current test purposes.
                return System.identityHashCode(o1) - System.identityHashCode(o2);
            }
        }, null);

        // Add several objects with the same hash into consistent hash with explicit comparator.
        hash.addNode(new Object() {
            @Override public int hashCode() {
                return 0;
            }
        }, 1);
        hash.addNode(new Object() {
            @Override public int hashCode() {
                return 0;
            }
        }, 1);

        info("Expected pass due to internal TreeSet has explicit comparator.");
    }

    /**
     * Validate generated hashes.<p>
     * Note! This test should be ported into all supported platforms.
     */
    @Test
    public void testHashGeneraton() {
        // Validate strings.
        checkHash("", -1484017934);
        checkHash("1", -80388575);
        checkHash("a", -873690096);
        checkHash("Hadoop\u3092\u6bba\u3059", -695300527);
        checkHash("key1", -2067461682);

        // Validate primitives.
        checkHash(true, 1669973725);
        checkHash(false, -1900934144);

        checkHash(3, 386050343);
        checkHash(1000000000, -547312286);
        checkHash(0x7fffffff, 473949739);
        checkHash(0xffffffff, -1399925094);
        checkHash(0x7fffffffffffffffL, 201097861);
        checkHash(0xffffffffffffffffL, -1484017934);
        checkHash(1.4e-45f, 1262722378);
        checkHash(3.4028235e+38f, 1313755354);
        checkHash(4.9e-324, 1262722378);
        checkHash(1.7976931348623157e+308, -783615357);

        // Validate objects.
        checkHash(UUID.fromString("4d180911-21c9-48f2-a1e6-7bc1daf588a0"), -440525148);

        checkUUID("224ea4cd-f449-4dcb-869a-5317c63bd619", 806670090);
        checkUUID("fdc9ec54-ff53-4fdb-8239-5a3ac1fb31bd", -354375826);
        checkUUID("0f9c9b94-02ae-45a6-9d5c-a066dbdf2636", -1312538272);
        checkUUID("d8f1f916-4357-4cfe-a7df-49d4721690bf", -482944041);
        checkUUID("d67eb652-4e76-47fb-ad4e-cd902d9b868a", -449444069);
        checkUUID("c77ffeae-78a1-4ee6-a0fd-8d197a794412", -168980875);
        checkUUID("35de9f21-3c9b-4f4a-a7d5-3e2c6cb01564", -383915637);
    }

    /**
     * Test mapping to nodes.
     */
    @Test
    public void testMappingToNodes() {
        String n1 = "node #1";
        String n2 = "node #2";
        String n3 = "node #3";
        String n4 = "node #4";

        List<String> nodes = Arrays.asList(n1, n2, n3, n4);

        GridClientConsistentHash<String> hash = new GridClientConsistentHash<>();

        for (String node : nodes)
            hash.addNode(node, 5);

        Map<Object, String> data = new LinkedHashMap<>();

        data.put("", n1);
        data.put("asdf", n3);
        data.put("224ea4cd-f449-4dcb-869a-5317c63bd619", n2);
        data.put("fdc9ec54-ff53-4fdb-8239-5a3ac1fb31bd", n4);
        data.put("0f9c9b94-02ae-45a6-9d5c-a066dbdf2636", n1);
        data.put("d8f1f916-4357-4cfe-a7df-49d4721690bf", n3);
        data.put("c77ffeae-78a1-4ee6-a0fd-8d197a794412", n4);
        data.put("35de9f21-3c9b-4f4a-a7d5-3e2c6cb01564", n4);
        data.put("d67eb652-4e76-47fb-ad4e-cd902d9b868a", n4);

        data.put(0, n1);
        data.put(1, n4);
        data.put(12, n3);
        data.put(123, n3);
        data.put(1234, n3);
        data.put(12345, n4);
        data.put(123456, n3);
        data.put(1234567, n4);
        data.put(12345678, n4);
        data.put(123456789, n4);
        data.put(1234567890, n3);
        data.put(1234567890L, n3);
        data.put(12345678901L, n4);
        data.put(123456789012L, n2);
        data.put(1234567890123L, n4);
        data.put(12345678901234L, n1);
        data.put(123456789012345L, n1);
        data.put(1234567890123456L, n3);
        data.put(-23456789012345L, n2);
        data.put(-2345678901234L, n1);
        data.put(-234567890123L, n4);
        data.put(-23456789012L, n3);
        data.put(-2345678901L, n3);
        data.put(-234567890L, n1);
        data.put(-234567890, n4);
        data.put(-23456789, n4);
        data.put(-2345678, n4);
        data.put(-234567, n4);
        data.put(-23456, n4);
        data.put(-2345, n1);
        data.put(-234, n4);
        data.put(-23, n3);
        data.put(-2, n4);

        data.put(0x80000000, n2);
        data.put(0x7fffffff, n4);
        data.put(0x8000000000000000L, n2);
        data.put(0x7fffffffffffffffL, n2);

        data.put(+1.1, n1);
        data.put(-10.01, n3);
        data.put(+100.001, n3);
        data.put(-1000.0001, n4);
        data.put(+1.7976931348623157E+308, n4);
        data.put(-1.7976931348623157E+308, n4);
        data.put(+4.9E-324, n4);
        data.put(-4.9E-324, n3);

        for (Map.Entry<Object, String> entry : data.entrySet())
            assertEquals("Validate key '" + entry.getKey() + "'.", entry.getValue(), hash.node(entry.getKey()));

        for (Map.Entry<Object, String> entry : data.entrySet())
            assertEquals("Validate key '" + entry.getKey() + "'.", entry.getValue(), hash.node(entry.getKey(), nodes));

        //
        // Change order nodes were added.
        //

        nodes = new ArrayList<>(nodes);

        Collections.reverse(nodes);

        // Reset consistent hash with new nodes order.
        hash = new GridClientConsistentHash<>();

        for (String node : nodes)
            hash.addNode(node, 5);

        for (Map.Entry<Object, String> entry : data.entrySet())
            assertEquals("Validate key '" + entry.getKey() + "'.", entry.getValue(), hash.node(entry.getKey()));

        for (Map.Entry<Object, String> entry : data.entrySet())
            assertEquals("Validate key '" + entry.getKey() + "'.", entry.getValue(), hash.node(entry.getKey(), nodes));
    }

    /**
     * Check unique id and generated hash code.
     *
     * @param uuid String presentation of unique id.
     * @param code Expected hash code.
     */
    private void checkUUID(String uuid, int code) {
        checkHash(UUID.fromString(uuid), code);
    }

    /**
     * Check hash generation for the specified object.
     *
     * @param o Object to verify hash generation for.
     * @param code Expected hash code.
     */
    private void checkHash(Object o, int code) {
        int i = GridClientConsistentHash.hash(o);

        assertEquals("Check affinity for object: " + o, code, i);
    }
}
