/*
 * SPDX-License-Identifier: Apache-2.0
 *
 * The OpenSearch Contributors require contributions made to
 * this file be licensed under the Apache-2.0 license or a
 * compatible open source license.
 */

package org.opensearch.cluster.routing.allocation;

import org.opensearch.cluster.ClusterState;
import org.opensearch.cluster.routing.RoutingNode;
import org.opensearch.cluster.routing.RoutingNodes;
import org.opensearch.cluster.routing.RoutingPool;
import org.opensearch.cluster.routing.ShardRouting;
import org.opensearch.cluster.routing.UnassignedInfo;
import org.opensearch.cluster.routing.allocation.allocator.RemoteShardsBalancer;

import java.util.HashMap;
import java.util.Map;

import static org.opensearch.cluster.routing.UnassignedInfo.AllocationStatus.DECIDERS_NO;
import static org.opensearch.cluster.routing.UnassignedInfo.AllocationStatus.DECIDERS_THROTTLED;
import static org.opensearch.cluster.routing.UnassignedInfo.AllocationStatus.NO_ATTEMPT;

public class RemoteShardsAllocateUnassignedTests extends RemoteShardsBalancerBaseTestCase {

    /**
     * Test Remote Shards Balancer initialization.
     */
    public void testInit() {
        int localOnlyNodes = 7;
        int remoteCapableNodes = 3;
        int localIndices = 10;
        int remoteIndices = 15;
        ClusterState clusterState = createInitialCluster(localOnlyNodes, remoteCapableNodes, localIndices, remoteIndices);
        RoutingNodes routingNodes = new RoutingNodes(clusterState, false);
        RoutingAllocation allocation = getRoutingAllocation(clusterState, routingNodes);

        RemoteShardsBalancer remoteShardsBalancer = new RemoteShardsBalancer(logger, allocation);
        Map<String, RemoteShardsBalancer.UnassignedIndexShards> unassignedShardMap = remoteShardsBalancer.groupUnassignedShardsByIndex();

        assertEquals(remoteIndices, unassignedShardMap.size());
        for (String index : unassignedShardMap.keySet()) {
            assertTrue(index.startsWith(REMOTE_IDX_PREFIX));
            RemoteShardsBalancer.UnassignedIndexShards indexShards = unassignedShardMap.get(index);
            assertEquals(5, indexShards.getPrimaries().size());
            for (ShardRouting shard : indexShards.getPrimaries()) {
                assertTrue(shard.primary());
                assertEquals(shard.getIndexName(), index);
            }
            assertEquals(5, indexShards.getReplicas().size());
            for (ShardRouting shard : indexShards.getReplicas()) {
                assertFalse(shard.primary());
                assertEquals(shard.getIndexName(), index);
            }
        }
    }

    /**
     * Test remote unassigned shard allocation for standard new cluster setup.
     */
    public void testPrimaryAllocation() {
        int localOnlyNodes = 7;
        int remoteCapableNodes = 3;
        int localIndices = 10;
        int remoteIndices = 13;
        ClusterState clusterState = createInitialCluster(localOnlyNodes, remoteCapableNodes, localIndices, remoteIndices);
        AllocationService service = this.createRemoteCapableAllocationService();
        clusterState = allocateShardsAndBalance(clusterState, service);
        RoutingNodes routingNodes = clusterState.getRoutingNodes();
        RoutingAllocation allocation = getRoutingAllocation(clusterState, routingNodes);

        assertEquals(0, routingNodes.unassigned().size());

        final Map<String, Integer> nodePrimariesCounter = new HashMap<>();
        for (ShardRouting shard : clusterState.getRoutingTable().allShards()) {
            assertFalse(shard.unassigned());
            RoutingNode node = routingNodes.node(shard.currentNodeId());
            RoutingPool nodePool = RoutingPool.getNodePool(node);
            RoutingPool shardPool = RoutingPool.getShardPool(shard, allocation);
            if (RoutingPool.REMOTE_CAPABLE.equals(shardPool)) {
                assertEquals(nodePool, shardPool);
            }
            if (RoutingPool.getNodePool(node) == RoutingPool.REMOTE_CAPABLE
                && RoutingPool.REMOTE_CAPABLE.equals(RoutingPool.getShardPool(shard, allocation))
                && shard.primary()) {
                nodePrimariesCounter.compute(node.nodeId(), (k, v) -> (v == null) ? 1 : v + 1);
            }
        }
        final int indexShardLimit = (int) Math.ceil(totalPrimaries(remoteIndices) / (float) remoteCapableNodes);
        for (int primaries : nodePrimariesCounter.values()) {
            assertTrue(primaries <= indexShardLimit);
        }
    }

    /**
     * Test remote unassigned shard allocation when deciders make NO or THROTTLED decision.
     */
    public void testNoRemoteAllocation() {
        final int localOnlyNodes = 10;
        final int remoteCapableNodes = 5;
        final int localIndices = 2;
        final int remoteIndices = 1;
        final ClusterState oldState = createInitialCluster(localOnlyNodes, remoteCapableNodes, localIndices, remoteIndices);
        final boolean throttle = randomBoolean();
        final AllocationService service = this.createRejectRemoteAllocationService(throttle);
        final ClusterState newState = allocateShardsAndBalance(oldState, service);
        final RoutingNodes routingNodes = newState.getRoutingNodes();
        final RoutingAllocation allocation = getRoutingAllocation(newState, routingNodes);

        assertEquals(totalShards(remoteIndices), routingNodes.unassigned().size());

        for (ShardRouting shard : newState.getRoutingTable().allShards()) {
            if (RoutingPool.getShardPool(shard, allocation) == RoutingPool.REMOTE_CAPABLE) {
                assertTrue(shard.unassigned());
                if (shard.primary()) {
                    final UnassignedInfo.AllocationStatus expect = throttle ? DECIDERS_THROTTLED : DECIDERS_NO;
                    assertEquals(expect, shard.unassignedInfo().getLastAllocationStatus());
                } else {
                    assertEquals(NO_ATTEMPT, shard.unassignedInfo().getLastAllocationStatus());
                }
            } else {
                assertFalse(shard.unassigned());
            }
        }
    }

    /**
     * Test remote unassigned shard allocation when remote capable nodes fail to come up.
     */
    public void testAllocationRemoteCapableNodesUnavailable() {
        int localOnlyNodes = 7;
        int remoteCapableNodes = 0;
        int localIndices = 10;
        int remoteIndices = 13;
        ClusterState clusterState = createInitialCluster(localOnlyNodes, remoteCapableNodes, localIndices, remoteIndices);
        AllocationService service = this.createRemoteCapableAllocationService();
        clusterState = allocateShardsAndBalance(clusterState, service);
        RoutingNodes routingNodes = clusterState.getRoutingNodes();
        RoutingAllocation allocation = getRoutingAllocation(clusterState, routingNodes);

        assertEquals(totalShards(remoteIndices), routingNodes.unassigned().size());

        for (ShardRouting shard : clusterState.getRoutingTable().allShards()) {
            if (RoutingPool.getShardPool(shard, allocation) == RoutingPool.REMOTE_CAPABLE) {
                assertTrue(shard.unassigned());
            } else {
                assertFalse(shard.unassigned());
                RoutingNode node = routingNodes.node(shard.currentNodeId());
                assertEquals(RoutingPool.LOCAL_ONLY, RoutingPool.getNodePool(node));
            }
        }
        for (RoutingNode node : routingNodes) {
            if (RoutingPool.getNodePool(node) == RoutingPool.REMOTE_CAPABLE) {
                assertEquals(0, node.size());
            }
        }
    }
}
