/*
 * 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.Version;
import org.opensearch.cluster.ClusterState;
import org.opensearch.cluster.health.ClusterHealthStatus;
import org.opensearch.cluster.health.ClusterStateHealth;
import org.opensearch.cluster.metadata.IndexMetadata;
import org.opensearch.cluster.metadata.Metadata;
import org.opensearch.cluster.routing.RoutingNodes;
import org.opensearch.cluster.routing.RoutingTable;
import org.opensearch.cluster.routing.ShardRoutingState;
import org.opensearch.index.IndexModule;

public class RemoteShardsMoveShardsTests extends RemoteShardsBalancerBaseTestCase {

    /**
     * Test reroute terminates gracefully if shards cannot move out of the excluded node
     */
    public void testExcludeNodeIdMoveBlocked() {
        int localOnlyNodes = 7;
        int remoteCapableNodes = 2;
        int localIndices = 10;
        int remoteIndices = 13;
        ClusterState clusterState = createInitialCluster(localOnlyNodes, remoteCapableNodes, localIndices, remoteIndices);
        AllocationService service = this.createRemoteCapableAllocationService();
        clusterState = allocateShardsAndBalance(clusterState, service);
        assertEquals(ClusterHealthStatus.GREEN, (new ClusterStateHealth(clusterState)).getStatus());

        // Exclude a node
        final String excludedNodeID = getNodeId(0, true);
        service = createRemoteCapableAllocationService(excludedNodeID);
        clusterState = allocateShardsAndBalance(clusterState, service);

        RoutingNodes routingNodes = clusterState.getRoutingNodes();
        assertEquals(ClusterHealthStatus.GREEN, (new ClusterStateHealth(clusterState)).getStatus());
        assertEquals(0, routingNodes.unassigned().size());
        assertTrue(routingNodes.node(excludedNodeID).size() > 0);
    }

    /**
     * Test move operations for index level allocation settings.
     * Supported for local indices, not supported for remote indices.
     */
    public void testIndexLevelExclusions() throws InterruptedException {
        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();

        assertEquals(ClusterHealthStatus.GREEN, (new ClusterStateHealth(clusterState)).getStatus());
        assertEquals(0, routingNodes.unassigned().size());

        final String excludedLocalOnlyNode = getNodeId(0, false);
        final String excludedRemoteCapableNode = getNodeId(0, true);
        final String localIndex = routingNodes.node(excludedLocalOnlyNode).shardsWithState(ShardRoutingState.STARTED).get(0).getIndexName();
        final String remoteIndex = routingNodes.node(excludedRemoteCapableNode)
            .shardsWithState(ShardRoutingState.STARTED)
            .stream()
            .filter(shardRouting -> shardRouting.getIndexName().startsWith(REMOTE_IDX_PREFIX))
            .findFirst()
            .get()
            .getIndexName();

        Metadata.Builder mb = Metadata.builder(clusterState.metadata());
        mb.put(
            IndexMetadata.builder(clusterState.metadata().index(localIndex))
                .settings(
                    settings(Version.CURRENT).put("index.number_of_shards", PRIMARIES)
                        .put("index.number_of_replicas", REPLICAS)
                        .put("index.routing.allocation.exclude._name", excludedLocalOnlyNode)
                        .build()
                )
        );
        mb.put(
            IndexMetadata.builder(clusterState.metadata().index(remoteIndex))
                .settings(
                    settings(Version.CURRENT).put("index.number_of_shards", PRIMARIES)
                        .put("index.number_of_replicas", REPLICAS)
                        .put(IndexModule.INDEX_STORE_TYPE_SETTING.getKey(), IndexModule.Type.REMOTE_SNAPSHOT.getSettingsKey())
                        .put("index.routing.allocation.exclude._name", excludedRemoteCapableNode)
                        .build()
                )
        );
        clusterState = ClusterState.builder(clusterState).metadata(mb.build()).build();

        clusterState = allocateShardsAndBalance(clusterState, service);
        assertEquals(ClusterHealthStatus.GREEN, (new ClusterStateHealth(clusterState)).getStatus());
        RoutingTable routingTable = clusterState.routingTable();

        // No shard of updated local index should be on excluded local capable node
        assertTrue(routingTable.allShards(localIndex).stream().noneMatch(shard -> shard.currentNodeId().equals(excludedLocalOnlyNode)));

        // Since remote index shards are untouched, at least one shard should
        // continue to stay on the excluded remote capable node
        assertTrue(routingTable.allShards(remoteIndex).stream().anyMatch(shard -> shard.currentNodeId().equals(excludedRemoteCapableNode)));
    }
}
