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

import com.carrotsearch.randomizedtesting.generators.RandomInts;
import org.elasticsearch.Build;
import org.elasticsearch.ElasticsearchException;
import org.elasticsearch.Version;
import org.elasticsearch.action.admin.cluster.node.info.NodeInfo;
import org.elasticsearch.action.admin.cluster.node.info.NodesInfoResponse;
import org.elasticsearch.cluster.ClusterName;
import org.elasticsearch.cluster.node.DiscoveryNode;
import org.elasticsearch.common.component.Lifecycle;
import org.elasticsearch.common.component.LifecycleListener;
import org.elasticsearch.common.transport.BoundTransportAddress;
import org.elasticsearch.common.transport.TransportAddress;
import org.elasticsearch.transport.*;

import java.io.IOException;
import java.util.Collections;
import java.util.Map;
import java.util.Random;
import java.util.Set;
import java.util.concurrent.CopyOnWriteArraySet;
import java.util.concurrent.atomic.AtomicInteger;

abstract class FailAndRetryMockTransport<Response extends TransportResponse> implements Transport {

    private final Random random;

    private boolean connectMode = true;

    private TransportServiceAdapter transportServiceAdapter;

    private final AtomicInteger connectTransportExceptions = new AtomicInteger();
    private final AtomicInteger failures = new AtomicInteger();
    private final AtomicInteger successes = new AtomicInteger();
    private final Set<DiscoveryNode> triedNodes = new CopyOnWriteArraySet<>();

    FailAndRetryMockTransport(Random random) {
        this.random = new Random(random.nextLong());
    }

    @Override
    @SuppressWarnings("unchecked")
    public void sendRequest(DiscoveryNode node, long requestId, String action, TransportRequest request, TransportRequestOptions options) throws IOException, TransportException {

        //we make sure that nodes get added to the connected ones when calling addTransportAddress, by returning proper nodes info
        if (connectMode) {
            TransportResponseHandler transportResponseHandler = transportServiceAdapter.onResponseReceived(requestId);
            NodeInfo nodeInfo = new NodeInfo(Version.CURRENT, Build.CURRENT, node, null, null, null, null, null, null, null, null, null, null);
            NodesInfoResponse nodesInfoResponse = new NodesInfoResponse(ClusterName.DEFAULT, new NodeInfo[]{nodeInfo});
            transportResponseHandler.handleResponse(nodesInfoResponse);
            return;
        }

        //once nodes are connected we'll just return errors for each sendRequest call
        triedNodes.add(node);

        if (RandomInts.randomInt(random, 100) > 10) {
            connectTransportExceptions.incrementAndGet();
            throw new ConnectTransportException(node, "node not available");
        } else {
            if (random.nextBoolean()) {
                failures.incrementAndGet();
                //throw whatever exception that is not a subclass of ConnectTransportException
                throw new IllegalStateException();
            } else {
                TransportResponseHandler transportResponseHandler = transportServiceAdapter.onResponseReceived(requestId);
                if (random.nextBoolean()) {
                    successes.incrementAndGet();
                    transportResponseHandler.handleResponse(newResponse());
                } else {
                    failures.incrementAndGet();
                    transportResponseHandler.handleException(new TransportException("transport exception"));
                }
            }
        }
    }

    protected abstract Response newResponse();

    public void endConnectMode() {
        this.connectMode = false;
    }

    public int connectTransportExceptions() {
        return connectTransportExceptions.get();
    }

    public int failures() {
        return failures.get();
    }

    public int successes() {
        return successes.get();
    }

    public Set<DiscoveryNode> triedNodes() {
        return triedNodes;
    }

    @Override
    public void transportServiceAdapter(TransportServiceAdapter transportServiceAdapter) {
        this.transportServiceAdapter = transportServiceAdapter;
    }

    @Override
    public BoundTransportAddress boundAddress() {
        return null;
    }

    @Override
    public TransportAddress[] addressesFromString(String address) throws Exception {
        throw new UnsupportedOperationException();
    }

    @Override
    public boolean addressSupported(Class<? extends TransportAddress> address) {
        throw new UnsupportedOperationException();
    }

    @Override
    public boolean nodeConnected(DiscoveryNode node) {
        return false;
    }

    @Override
    public void connectToNode(DiscoveryNode node) throws ConnectTransportException {

    }

    @Override
    public void connectToNodeLight(DiscoveryNode node) throws ConnectTransportException {

    }

    @Override
    public void disconnectFromNode(DiscoveryNode node) {

    }

    @Override
    public long serverOpen() {
        return 0;
    }

    @Override
    public Lifecycle.State lifecycleState() {
        return null;
    }

    @Override
    public void addLifecycleListener(LifecycleListener listener) {
        throw new UnsupportedOperationException();
    }

    @Override
    public void removeLifecycleListener(LifecycleListener listener) {
        throw new UnsupportedOperationException();
    }

    @Override
    public Transport start() throws ElasticsearchException {
        return null;
    }

    @Override
    public Transport stop() throws ElasticsearchException {
        return null;
    }

    @Override
    public void close() throws ElasticsearchException {

    }

    @Override
    public Map<String, BoundTransportAddress> profileBoundAddresses() {
        return Collections.EMPTY_MAP;
    }
}
