/*
 * 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.discovery.zen;

import org.elasticsearch.ElasticsearchException;
import org.elasticsearch.Version;
import org.elasticsearch.cluster.ClusterName;
import org.elasticsearch.cluster.ClusterState;
import org.elasticsearch.cluster.node.DiscoveryNode;
import org.elasticsearch.cluster.node.DiscoveryNodes;
import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.discovery.zen.PendingClusterStatesQueue;
import org.elasticsearch.discovery.zen.PendingClusterStatesQueue.ClusterStateContext;
import org.elasticsearch.test.ESTestCase;

import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import static java.util.Collections.emptyMap;
import static java.util.Collections.emptySet;
import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.empty;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.hasKey;
import static org.hamcrest.Matchers.notNullValue;
import static org.hamcrest.Matchers.nullValue;
import static org.hamcrest.Matchers.sameInstance;

public class PendingClusterStatesQueueTests extends ESTestCase {

    public void testSelectNextStateToProcess_empty() {
        PendingClusterStatesQueue queue = new PendingClusterStatesQueue(logger, randomIntBetween(1, 200));
        assertThat(queue.getNextClusterStateToProcess(), nullValue());
    }

    public void testDroppingStatesAtCapacity() {
        List<ClusterState> states = randomStates(scaledRandomIntBetween(10, 300), "master1", "master2", "master3", "master4");
        Collections.shuffle(states, random());
        // insert half of the states
        final int numberOfStateToDrop = states.size() / 2;
        List<ClusterState> stateToDrop = states.subList(0, numberOfStateToDrop);
        final int queueSize = states.size() - numberOfStateToDrop;
        PendingClusterStatesQueue queue = createQueueWithStates(stateToDrop, queueSize);
        List<ClusterStateContext> committedContexts = randomCommitStates(queue);
        for (ClusterState state : states.subList(numberOfStateToDrop, states.size())) {
            queue.addPending(state);
        }

        assertThat(queue.pendingClusterStates().length, equalTo(queueSize));
        // check all committed states got a failure due to the drop
        for (ClusterStateContext context : committedContexts) {
            assertThat(((MockListener) context.listener).failure, notNullValue());
        }

        // all states that should have dropped are indeed dropped.
        for (ClusterState state : stateToDrop) {
            assertThat(queue.findState(state.stateUUID()), nullValue());
        }

    }

    public void testSimpleQueueSameMaster() {
        final int numUpdates = scaledRandomIntBetween(50, 100);
        List<ClusterState> states = randomStates(numUpdates, "master");
        Collections.shuffle(states, random());
        PendingClusterStatesQueue queue;
        queue = createQueueWithStates(states);

        // no state is committed yet
        assertThat(queue.getNextClusterStateToProcess(), nullValue());

        ClusterState highestCommitted = null;
        for (ClusterStateContext context : randomCommitStates(queue)) {
            if (highestCommitted == null || context.state.supersedes(highestCommitted)) {
                highestCommitted = context.state;
            }
        }

        assertThat(queue.getNextClusterStateToProcess(), sameInstance(highestCommitted));

        queue.markAsProcessed(highestCommitted);

        // now there is nothing more to process
        assertThat(queue.getNextClusterStateToProcess(), nullValue());
    }

    public void testProcessedStateCleansStatesFromOtherMasters() {
        List<ClusterState> states = randomStates(scaledRandomIntBetween(10, 300), "master1", "master2", "master3", "master4");
        PendingClusterStatesQueue queue = createQueueWithStates(states);
        List<ClusterStateContext> committedContexts = randomCommitStates(queue);
        ClusterState randomCommitted = randomFrom(committedContexts).state;
        queue.markAsProcessed(randomCommitted);
        final String processedMaster = randomCommitted.nodes().getMasterNodeId();

        // now check that queue doesn't contain anything pending from another master
        for (ClusterStateContext context : queue.pendingStates) {
            final String pendingMaster = context.state.nodes().getMasterNodeId();
            assertThat("found a cluster state from [" + pendingMaster
                            + "], after a state from [" + processedMaster + "] was processed",
                    pendingMaster, equalTo(processedMaster));
        }
        // and check all committed contexts from another master were failed
        for (ClusterStateContext context : committedContexts) {
            if (context.state.nodes().getMasterNodeId().equals(processedMaster) == false) {
                assertThat(((MockListener) context.listener).failure, notNullValue());
            }
        }
    }

    public void testFailedStateCleansSupersededStatesOnly() {
        List<ClusterState> states = randomStates(scaledRandomIntBetween(10, 50), "master1", "master2", "master3", "master4");
        PendingClusterStatesQueue queue = createQueueWithStates(states);
        List<ClusterStateContext> committedContexts = randomCommitStates(queue);
        ClusterState toFail = randomFrom(committedContexts).state;
        queue.markAsFailed(toFail, new ElasticsearchException("boo!"));
        final Map<String, ClusterStateContext> committedContextsById = new HashMap<>();
        for (ClusterStateContext context : committedContexts) {
            committedContextsById.put(context.stateUUID(), context);
        }

        // now check that queue doesn't contain superseded states
        for (ClusterStateContext context : queue.pendingStates) {
            if (context.committed()) {
                assertFalse("found a committed cluster state, which is superseded by a failed state.\nFound:" +
                        context.state + "\nfailed:" + toFail,
                        toFail.supersedes(context.state));
            }
        }
        // check no state has been erroneously removed
        for (ClusterState state : states) {
            ClusterStateContext pendingContext = queue.findState(state.stateUUID());
            if (pendingContext != null) {
                continue;
            }
            if (state.equals(toFail)) {
                continue;
            }
            assertThat("non-committed states should never be removed", committedContextsById, hasKey(state.stateUUID()));
            final ClusterStateContext context = committedContextsById.get(state.stateUUID());
            assertThat("removed state is not superseded by failed state. \nRemoved state:" + context + "\nfailed: " + toFail,
                    toFail.supersedes(context.state), equalTo(true));
            assertThat("removed state was failed with wrong exception", ((MockListener) context.listener).failure, notNullValue());
            assertThat("removed state was failed with wrong exception", ((MockListener) context.listener).failure.getMessage(),
                    containsString("boo"));
        }
    }

    public void testFailAllAndClear() {
        List<ClusterState> states = randomStates(scaledRandomIntBetween(10, 50), "master1", "master2", "master3", "master4");
        PendingClusterStatesQueue queue = createQueueWithStates(states);
        List<ClusterStateContext> committedContexts = randomCommitStates(queue);
        queue.failAllStatesAndClear(new ElasticsearchException("boo!"));
        assertThat(queue.pendingStates, empty());
        assertThat(queue.getNextClusterStateToProcess(), nullValue());
        for (ClusterStateContext context : committedContexts) {
            assertThat("state was failed with wrong exception", ((MockListener) context.listener).failure, notNullValue());
            assertThat("state was failed with wrong exception", ((MockListener) context.listener).failure.getMessage(),
                    containsString("boo"));
        }
    }

    public void testQueueStats() {
        List<ClusterState> states = randomStates(scaledRandomIntBetween(10, 100), "master");
        PendingClusterStatesQueue queue = createQueueWithStates(states);
        assertThat(queue.stats().getTotal(), equalTo(states.size()));
        assertThat(queue.stats().getPending(), equalTo(states.size()));
        assertThat(queue.stats().getCommitted(), equalTo(0));

        List<ClusterStateContext> committedContexts = randomCommitStates(queue);
        assertThat(queue.stats().getTotal(), equalTo(states.size()));
        assertThat(queue.stats().getPending(), equalTo(states.size() - committedContexts.size()));
        assertThat(queue.stats().getCommitted(), equalTo(committedContexts.size()));

        ClusterState highestCommitted = null;
        for (ClusterStateContext context : committedContexts) {
            if (highestCommitted == null || context.state.supersedes(highestCommitted)) {
                highestCommitted = context.state;
            }
        }
        assert highestCommitted != null;

        queue.markAsProcessed(highestCommitted);
        assertThat((long)queue.stats().getTotal(), equalTo(states.size() - (1 + highestCommitted.version())));
        assertThat((long)queue.stats().getPending(), equalTo(states.size() - (1 + highestCommitted.version())));
        assertThat(queue.stats().getCommitted(), equalTo(0));
    }

    protected List<ClusterStateContext> randomCommitStates(PendingClusterStatesQueue queue) {
        List<ClusterStateContext> committedContexts = new ArrayList<>();
        for (int iter = randomInt(queue.pendingStates.size() - 1); iter >= 0; iter--) {
            ClusterState state = queue.markAsCommitted(randomFrom(queue.pendingStates).stateUUID(), new MockListener());
            if (state != null) {
                // null cluster state means we committed twice
                committedContexts.add(queue.findState(state.stateUUID()));
            }
        }
        return committedContexts;
    }

    PendingClusterStatesQueue createQueueWithStates(List<ClusterState> states) {
        return createQueueWithStates(states, states.size() * 2); // we don't care about limits (there are dedicated tests for that)
    }

    PendingClusterStatesQueue createQueueWithStates(List<ClusterState> states, int maxQueueSize) {
        PendingClusterStatesQueue queue;
        queue = new PendingClusterStatesQueue(logger, maxQueueSize);
        for (ClusterState state : states) {
            queue.addPending(state);
        }
        return queue;
    }

    List<ClusterState> randomStates(int count, String... masters) {
        ArrayList<ClusterState> states = new ArrayList<>(count);
        ClusterState[] lastClusterStatePerMaster = new ClusterState[masters.length];
        for (; count > 0; count--) {
            int masterIndex = randomInt(masters.length - 1);
            ClusterState state = lastClusterStatePerMaster[masterIndex];
            if (state == null) {
                state = ClusterState.builder(ClusterName.CLUSTER_NAME_SETTING.getDefault(Settings.EMPTY)).nodes(DiscoveryNodes.builder()
                                .add(new DiscoveryNode(masters[masterIndex], buildNewFakeTransportAddress(),
                                        emptyMap(), emptySet(),Version.CURRENT)).masterNodeId(masters[masterIndex]).build()
                ).build();
            } else {
                state = ClusterState.builder(state).incrementVersion().build();
            }
            states.add(state);
            lastClusterStatePerMaster[masterIndex] = state;
        }
        return states;
    }

    static class MockListener implements PendingClusterStatesQueue.StateProcessedListener {
        volatile boolean processed;
        volatile Throwable failure;

        @Override
        public void onNewClusterStateProcessed() {
            processed = true;
        }

        @Override
        public void onNewClusterStateFailed(Exception e) {
            failure = e;
        }
    }

}
