/*
 * 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.
 */

/*
 * 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.
 */
/*
 * Modifications Copyright OpenSearch Contributors. See
 * GitHub history for details.
 */

package org.opensearch.cluster.routing.allocation.decider;

import org.opensearch.cluster.ClusterState;
import org.opensearch.cluster.metadata.IndexMetadata;
import org.opensearch.cluster.routing.ShardRoutingState;
import org.opensearch.common.settings.Settings;
import org.opensearch.test.OpenSearchIntegTestCase;

import java.util.Set;

import static org.opensearch.cluster.routing.allocation.decider.SameShardAllocationDecider.CLUSTER_ROUTING_ALLOCATION_SAME_HOST_SETTING;
import static org.opensearch.test.hamcrest.OpenSearchAssertions.assertAcked;
import static org.hamcrest.Matchers.equalTo;

/**
 * An integration test for testing updating shard allocation/routing settings and
 * ensuring the updated settings take effect as expected.
 */
@OpenSearchIntegTestCase.ClusterScope(scope = OpenSearchIntegTestCase.Scope.TEST, numDataNodes = 0)
public class UpdateShardAllocationSettingsIT extends OpenSearchIntegTestCase {

    /**
     * Tests that updating the {@link EnableAllocationDecider} related settings works as expected.
     */
    public void testEnableRebalance() throws InterruptedException {
        final String firstNode = internalCluster().startNode();
        client().admin()
            .cluster()
            .prepareUpdateSettings()
            .setTransientSettings(
                Settings.builder()
                    .put(EnableAllocationDecider.CLUSTER_ROUTING_REBALANCE_ENABLE_SETTING.getKey(), EnableAllocationDecider.Rebalance.NONE)
            )
            .get();
        // we test with 2 shards since otherwise it's pretty fragile if there are difference in the num or shards such that
        // all shards are relocated to the second node which is not what we want here. It's solely a test for the settings to take effect
        final int numShards = 2;
        assertAcked(
            prepareCreate("test").setSettings(
                Settings.builder().put(IndexMetadata.SETTING_NUMBER_OF_REPLICAS, 0).put(IndexMetadata.SETTING_NUMBER_OF_SHARDS, numShards)
            )
        );
        assertAcked(
            prepareCreate("test_1").setSettings(
                Settings.builder().put(IndexMetadata.SETTING_NUMBER_OF_REPLICAS, 0).put(IndexMetadata.SETTING_NUMBER_OF_SHARDS, numShards)
            )
        );
        ensureGreen();
        assertAllShardsOnNodes("test", firstNode);
        assertAllShardsOnNodes("test_1", firstNode);

        final String secondNode = internalCluster().startNode();
        // prevent via index setting but only on index test
        client().admin()
            .indices()
            .prepareUpdateSettings("test")
            .setSettings(
                Settings.builder()
                    .put(EnableAllocationDecider.INDEX_ROUTING_REBALANCE_ENABLE_SETTING.getKey(), EnableAllocationDecider.Rebalance.NONE)
            )
            .get();
        client().admin().cluster().prepareReroute().get();
        ensureGreen();
        assertAllShardsOnNodes("test", firstNode);
        assertAllShardsOnNodes("test_1", firstNode);

        // now enable the index test to relocate since index settings override cluster settings
        client().admin()
            .indices()
            .prepareUpdateSettings("test")
            .setSettings(
                Settings.builder()
                    .put(
                        EnableAllocationDecider.INDEX_ROUTING_REBALANCE_ENABLE_SETTING.getKey(),
                        randomBoolean() ? EnableAllocationDecider.Rebalance.PRIMARIES : EnableAllocationDecider.Rebalance.ALL
                    )
            )
            .get();
        logger.info("--> balance index [test]");
        client().admin().cluster().prepareReroute().get();
        ensureGreen("test");
        Set<String> test = assertAllShardsOnNodes("test", firstNode, secondNode);
        assertThat("index: [test] expected to be rebalanced on both nodes", test.size(), equalTo(2));

        // flip the cluster wide setting such that we can also balance for index
        // test_1 eventually we should have one shard of each index on each node
        client().admin()
            .cluster()
            .prepareUpdateSettings()
            .setTransientSettings(
                Settings.builder()
                    .put(
                        EnableAllocationDecider.CLUSTER_ROUTING_REBALANCE_ENABLE_SETTING.getKey(),
                        randomBoolean() ? EnableAllocationDecider.Rebalance.PRIMARIES : EnableAllocationDecider.Rebalance.ALL
                    )
            )
            .get();
        logger.info("--> balance index [test_1]");
        client().admin().cluster().prepareReroute().get();
        ensureGreen("test_1");
        Set<String> test_1 = assertAllShardsOnNodes("test_1", firstNode, secondNode);
        assertThat("index: [test_1] expected to be rebalanced on both nodes", test_1.size(), equalTo(2));

        test = assertAllShardsOnNodes("test", firstNode, secondNode);
        assertThat("index: [test] expected to be rebalanced on both nodes", test.size(), equalTo(2));
    }

    /**
     * Tests that updating the {@link SameShardAllocationDecider#CLUSTER_ROUTING_ALLOCATION_SAME_HOST_SETTING} setting works as expected.
     */
    public void testUpdateSameHostSetting() {
        internalCluster().startNodes(2);
        // same same_host to true, since 2 nodes are started on the same host,
        // only primaries should be assigned
        client().admin()
            .cluster()
            .prepareUpdateSettings()
            .setTransientSettings(Settings.builder().put(CLUSTER_ROUTING_ALLOCATION_SAME_HOST_SETTING.getKey(), true))
            .get();
        final String indexName = "idx";
        client().admin()
            .indices()
            .prepareCreate(indexName)
            .setSettings(Settings.builder().put(IndexMetadata.SETTING_NUMBER_OF_SHARDS, 1).put(IndexMetadata.SETTING_NUMBER_OF_REPLICAS, 1))
            .get();
        ClusterState clusterState = client().admin().cluster().prepareState().get().getState();
        assertFalse(
            "replica should be unassigned",
            clusterState.getRoutingTable().index(indexName).shardsWithState(ShardRoutingState.UNASSIGNED).isEmpty()
        );
        // now, update the same_host setting to allow shards to be allocated to multiple nodes on
        // the same host - the replica should get assigned
        client().admin()
            .cluster()
            .prepareUpdateSettings()
            .setTransientSettings(Settings.builder().put(CLUSTER_ROUTING_ALLOCATION_SAME_HOST_SETTING.getKey(), false))
            .get();
        clusterState = client().admin().cluster().prepareState().get().getState();
        assertTrue(
            "all shards should be assigned",
            clusterState.getRoutingTable().index(indexName).shardsWithState(ShardRoutingState.UNASSIGNED).isEmpty()
        );
    }
}
