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

import org.apache.lucene.util.IOUtils;
import org.elasticsearch.Version;
import org.elasticsearch.cluster.ClusterName;
import org.elasticsearch.cluster.ClusterState;
import org.elasticsearch.cluster.node.DiscoveryNodes;
import org.elasticsearch.cluster.service.ClusterService;
import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.common.unit.TimeValue;
import org.elasticsearch.discovery.zen.PingContextProvider;
import org.elasticsearch.discovery.zen.UnicastHostsProvider;
import org.elasticsearch.discovery.zen.UnicastZenPing;
import org.elasticsearch.discovery.zen.ZenPing;
import org.elasticsearch.test.ESIntegTestCase;
import org.elasticsearch.test.InternalTestCluster;
import org.elasticsearch.test.NodeConfigurationSource;
import org.elasticsearch.test.transport.MockTransportService;
import org.elasticsearch.threadpool.TestThreadPool;
import org.elasticsearch.transport.MockTcpTransportPlugin;
import org.elasticsearch.transport.TransportService;

import java.io.Closeable;
import java.io.IOException;
import java.util.Collections;
import java.util.Stack;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CountDownLatch;
import java.util.function.Function;

import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.not;

@ESIntegTestCase.ClusterScope(
        scope = ESIntegTestCase.Scope.TEST,
        numDataNodes = 1,
        numClientNodes = 0,
        supportsDedicatedMasters = false,
        autoMinMasterNodes = false)
public class SingleNodeDiscoveryIT extends ESIntegTestCase {

    @Override
    protected Settings nodeSettings(int nodeOrdinal) {
        return Settings
                .builder()
                .put(super.nodeSettings(nodeOrdinal))
                .put("discovery.type", "single-node")
                .put("transport.tcp.port", "0")
                .build();
    }

    public void testDoesNotRespondToZenPings() throws Exception {
        final Settings settings =
                Settings.builder().put("cluster.name", internalCluster().getClusterName()).build();
        final Version version = Version.CURRENT;
        final Stack<Closeable> closeables = new Stack<>();
        final TestThreadPool threadPool = new TestThreadPool(getClass().getName());
        try {
            final MockTransportService pingTransport =
                    MockTransportService.createNewService(settings, version, threadPool, null);
            pingTransport.start();
            closeables.push(pingTransport);
            final TransportService nodeTransport =
                    internalCluster().getInstance(TransportService.class);
            // try to ping the single node directly
            final UnicastHostsProvider provider =
                    () -> Collections.singletonList(nodeTransport.getLocalNode());
            final CountDownLatch latch = new CountDownLatch(1);
            final UnicastZenPing unicastZenPing =
                    new UnicastZenPing(settings, threadPool, pingTransport, provider) {
                        @Override
                        protected void finishPingingRound(PingingRound pingingRound) {
                            latch.countDown();
                            super.finishPingingRound(pingingRound);
                        }
                    };
            final DiscoveryNodes nodes =
                    DiscoveryNodes.builder().add(pingTransport.getLocalNode()).build();
            final ClusterName clusterName = new ClusterName(internalCluster().getClusterName());
            final ClusterState state = ClusterState.builder(clusterName).nodes(nodes).build();
            unicastZenPing.start(new PingContextProvider() {
                @Override
                public ClusterState clusterState() {
                    return state;
                }

                @Override
                public DiscoveryNodes nodes() {
                    return DiscoveryNodes
                            .builder()
                            .add(nodeTransport.getLocalNode())
                            .add(pingTransport.getLocalNode())
                            .localNodeId(pingTransport.getLocalNode().getId())
                            .build();
                }
            });
            closeables.push(unicastZenPing);
            final CompletableFuture<ZenPing.PingCollection> responses = new CompletableFuture<>();
            unicastZenPing.ping(responses::complete, TimeValue.timeValueSeconds(3));
            latch.await();
            responses.get();
            assertThat(responses.get().size(), equalTo(0));
        } finally {
            while (!closeables.isEmpty()) {
                IOUtils.closeWhileHandlingException(closeables.pop());
            }
            terminate(threadPool);
        }
    }

    public void testSingleNodesDoNotDiscoverEachOther() throws IOException, InterruptedException {
        final TransportService service = internalCluster().getInstance(TransportService.class);
        final int port = service.boundAddress().publishAddress().getPort();
        final NodeConfigurationSource configurationSource = new NodeConfigurationSource() {
            @Override
            public Settings nodeSettings(int nodeOrdinal) {
                return Settings
                        .builder()
                        .put("discovery.type", "single-node")
                        .put("http.enabled", false)
                        .put("transport.type", "mock-socket-network")
                        /*
                         * We align the port ranges of the two as then with zen discovery these two
                         * nodes would find each other.
                         */
                        .put("transport.tcp.port", port + "-" + (port + 5 - 1))
                        .build();
            }
        };
        try (InternalTestCluster other =
                new InternalTestCluster(
                        randomLong(),
                        createTempDir(),
                        false,
                        false,
                        1,
                        1,
                        internalCluster().getClusterName(),
                        configurationSource,
                        0,
                        false,
                        "other",
                        Collections.singletonList(MockTcpTransportPlugin.class),
                        Function.identity())) {
            other.beforeTest(random(), 0);
            final ClusterState first = internalCluster().getInstance(ClusterService.class).state();
            final ClusterState second = other.getInstance(ClusterService.class).state();
            assertThat(first.nodes().getSize(), equalTo(1));
            assertThat(second.nodes().getSize(), equalTo(1));
            assertThat(
                    first.nodes().getMasterNodeId(),
                    not(equalTo(second.nodes().getMasterNodeId())));
            assertThat(
                    first.metaData().clusterUUID(),
                    not(equalTo(second.metaData().clusterUUID())));
        }
    }

}
