package com.netflix.discovery.junit.resource;

import javax.ws.rs.core.UriBuilder;
import java.net.URI;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.Callable;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import com.google.common.base.Preconditions;
import com.netflix.appinfo.ApplicationInfoManager;
import com.netflix.appinfo.DataCenterInfo;
import com.netflix.appinfo.EurekaInstanceConfig;
import com.netflix.appinfo.InstanceInfo;
import com.netflix.appinfo.LeaseInfo;
import com.netflix.appinfo.MyDataCenterInstanceConfig;
import com.netflix.config.ConfigurationManager;
import com.netflix.discovery.CacheRefreshedEvent;
import com.netflix.discovery.DefaultEurekaClientConfig;
import com.netflix.discovery.DiscoveryClient;
import com.netflix.discovery.DiscoveryManager;
import com.netflix.discovery.EurekaClient;
import com.netflix.discovery.EurekaClientConfig;
import com.netflix.discovery.shared.transport.SimpleEurekaHttpServer;
import com.netflix.discovery.shared.transport.jersey.Jersey1DiscoveryClientOptionalArgs;
import com.netflix.eventbus.impl.EventBusImpl;
import com.netflix.eventbus.spi.EventBus;
import com.netflix.eventbus.spi.InvalidSubscriberException;
import com.netflix.eventbus.spi.Subscribe;
import org.junit.rules.ExternalResource;

/**
 * JUnit rule for discovery client + collection of static methods for setting it up.
 */
public class DiscoveryClientResource extends ExternalResource {

    public static final String REMOTE_REGION = "myregion";
    public static final String REMOTE_ZONE = "myzone";
    public static final int CLIENT_REFRESH_RATE = 10;
    public static final String EUREKA_TEST_NAMESPACE = "eurekaTestNamespace.";

    private static final Set<String> SYSTEM_PROPERTY_TRACKER = new HashSet<>();

    private final boolean registrationEnabled;
    private final boolean registryFetchEnabled;
    private final InstanceInfo instance;

    private final SimpleEurekaHttpServer eurekaHttpServer;
    private final Callable<Integer> portResolverCallable;
    private final List<String> remoteRegions;
    private final String vipFetch;
    private final String userName;
    private final String password;

    private EventBus eventBus;
    private ApplicationInfoManager applicationManager;
    private EurekaClient client;

    private final List<DiscoveryClientResource> forkedDiscoveryClientResources = new ArrayList<>();
    private ApplicationInfoManager applicationInfoManager;

    DiscoveryClientResource(DiscoveryClientRuleBuilder builder) {
        this.registrationEnabled = builder.registrationEnabled;
        this.registryFetchEnabled = builder.registryFetchEnabled;
        this.portResolverCallable = builder.portResolverCallable;
        this.eurekaHttpServer = builder.eurekaHttpServer;
        this.instance = builder.instance;
        this.remoteRegions = builder.remoteRegions;
        this.vipFetch = builder.vipFetch;
        this.userName = builder.userName;
        this.password = builder.password;
    }

    public InstanceInfo getMyInstanceInfo() {
        return createApplicationManager().getInfo();
    }

    public EventBus getEventBus() {
        if (client == null) {
            getClient(); // Lazy initialization
        }
        return eventBus;
    }

    public ApplicationInfoManager getApplicationInfoManager() {
        return applicationInfoManager;
    }

    public EurekaClient getClient() {
        if (client == null) {
            try {
                applicationInfoManager = createApplicationManager();
                EurekaClientConfig clientConfig = createEurekaClientConfig();

                Jersey1DiscoveryClientOptionalArgs optionalArgs = new Jersey1DiscoveryClientOptionalArgs();
                eventBus = new EventBusImpl();
                optionalArgs.setEventBus(eventBus);

                client = new DiscoveryClient(applicationInfoManager, clientConfig, optionalArgs);
            } catch (Exception e) {
                throw new RuntimeException(e);
            }
        }
        return client;
    }

    public boolean awaitCacheUpdate(long timeout, TimeUnit unit) throws InterruptedException {
        final CountDownLatch latch = new CountDownLatch(1);
        Object eventListener = new Object() {
            @Subscribe
            public void consume(CacheRefreshedEvent event) {
                latch.countDown();
            }
        };
        try {
            getEventBus().registerSubscriber(eventListener);
        } catch (InvalidSubscriberException e) {
            throw new IllegalStateException("Unexpected error during subscriber registration", e);
        }
        try {
            return latch.await(timeout, unit);
        } finally {
            getEventBus().unregisterSubscriber(eventListener);
        }
    }

    private ApplicationInfoManager createApplicationManager() {
        if (applicationManager == null) {
            EurekaInstanceConfig instanceConfig = new MyDataCenterInstanceConfig(EUREKA_TEST_NAMESPACE) {
                @Override
                public String getAppname() {
                    return "discoveryClientTest";
                }

                @Override
                public int getLeaseRenewalIntervalInSeconds() {
                    return 1;
                }
            };
            applicationManager = new ApplicationInfoManager(instanceConfig);
        }
        return applicationManager;
    }

    private EurekaClientConfig createEurekaClientConfig() throws Exception {
        // Cluster connectivity
        URI serviceURI;
        if (portResolverCallable != null) {
            serviceURI = new URI("http://localhost:" + portResolverCallable.call() + "/eureka/v2/");
        } else if (eurekaHttpServer != null) {
            serviceURI = eurekaHttpServer.getServiceURI();
        } else {
            throw new IllegalStateException("Either port or EurekaHttpServer must be configured");
        }

        if (userName != null) {
            serviceURI = UriBuilder.fromUri(serviceURI).userInfo(userName + ':' + password).build();
        }

        bindProperty(EUREKA_TEST_NAMESPACE + "serviceUrl.default", serviceURI.toString());
        if (remoteRegions != null && !remoteRegions.isEmpty()) {
            StringBuilder regions = new StringBuilder();
            for (String region : remoteRegions) {
                regions.append(',').append(region);
            }
            bindProperty(EUREKA_TEST_NAMESPACE + "fetchRemoteRegionsRegistry", regions.substring(1));
        }

        // Registration
        bindProperty(EUREKA_TEST_NAMESPACE + "registration.enabled", Boolean.toString(registrationEnabled));
        bindProperty(EUREKA_TEST_NAMESPACE + "appinfo.initial.replicate.time", Integer.toString(0));
        bindProperty(EUREKA_TEST_NAMESPACE + "appinfo.replicate.interval", Integer.toString(1));

        // Registry fetch
        bindProperty(EUREKA_TEST_NAMESPACE + "shouldFetchRegistry", Boolean.toString(registryFetchEnabled));

        bindProperty(EUREKA_TEST_NAMESPACE + "client.refresh.interval", Integer.toString(1));
        if (vipFetch != null) {
            bindProperty(EUREKA_TEST_NAMESPACE + "registryRefreshSingleVipAddress", vipFetch);
        }

        return new DefaultEurekaClientConfig(EUREKA_TEST_NAMESPACE);
    }

    @Override
    protected void after() {
        if (client != null) {
            client.shutdown();
        }
        for (DiscoveryClientResource resource : forkedDiscoveryClientResources) {
            resource.after();
        }
        for (String property : SYSTEM_PROPERTY_TRACKER) {
            ConfigurationManager.getConfigInstance().clearProperty(property);
        }
        clearDiscoveryClientConfig();
    }

    public DiscoveryClientRuleBuilder fork() {
        DiscoveryClientRuleBuilder builder = new DiscoveryClientRuleBuilder() {
            @Override
            public DiscoveryClientResource build() {
                DiscoveryClientResource clientResource = super.build();
                try {
                    clientResource.before();
                } catch (Throwable e) {
                    throw new IllegalStateException("Unexpected error during forking the client resource", e);
                }
                forkedDiscoveryClientResources.add(clientResource);
                return clientResource;
            }
        };
        return builder.withInstanceInfo(instance)
                .connectWith(eurekaHttpServer)
                .withPortResolver(portResolverCallable)
                .withRegistration(registrationEnabled)
                .withRegistryFetch(registryFetchEnabled)
                .withRemoteRegions(remoteRegions.toArray(new String[remoteRegions.size()]));
    }

    public static DiscoveryClientRuleBuilder newBuilder() {
        return new DiscoveryClientRuleBuilder();
    }

    public static void setupDiscoveryClientConfig(int serverPort, String path) {
        ConfigurationManager.getConfigInstance().setProperty("eureka.shouldFetchRegistry", "true");
        ConfigurationManager.getConfigInstance().setProperty("eureka.responseCacheAutoExpirationInSeconds", "10");
        ConfigurationManager.getConfigInstance().setProperty("eureka.client.refresh.interval", CLIENT_REFRESH_RATE);
        ConfigurationManager.getConfigInstance().setProperty("eureka.registration.enabled", "false");
        ConfigurationManager.getConfigInstance().setProperty("eureka.fetchRemoteRegionsRegistry", REMOTE_REGION);
        ConfigurationManager.getConfigInstance().setProperty("eureka.myregion.availabilityZones", REMOTE_ZONE);
        ConfigurationManager.getConfigInstance().setProperty("eureka.serviceUrl.default",
                "http://localhost:" + serverPort + path);
    }

    public static void clearDiscoveryClientConfig() {
        ConfigurationManager.getConfigInstance().clearProperty("eureka.client.refresh.interval");
        ConfigurationManager.getConfigInstance().clearProperty("eureka.registration.enabled");
        ConfigurationManager.getConfigInstance().clearProperty("eureka.fetchRemoteRegionsRegistry");
        ConfigurationManager.getConfigInstance().clearProperty("eureka.myregion.availabilityZones");
        ConfigurationManager.getConfigInstance().clearProperty("eureka.serviceUrl.default");
        ConfigurationManager.getConfigInstance().clearProperty("eureka.shouldEnforceFetchRegistryAtInit");
    }

    public static EurekaClient setupDiscoveryClient(InstanceInfo clientInstanceInfo) {
        DefaultEurekaClientConfig config = new DefaultEurekaClientConfig();
        // setup config in advance, used in initialize converter
        ApplicationInfoManager applicationInfoManager = new ApplicationInfoManager(new MyDataCenterInstanceConfig(), clientInstanceInfo);

        DiscoveryManager.getInstance().setEurekaClientConfig(config);
        EurekaClient client = new DiscoveryClient(applicationInfoManager, config);
        return client;
    }

    public static EurekaClient setupInjector(InstanceInfo clientInstanceInfo) {


        DefaultEurekaClientConfig config = new DefaultEurekaClientConfig();
        // setup config in advance, used in initialize converter
        DiscoveryManager.getInstance().setEurekaClientConfig(config);
        EurekaClient client = new DiscoveryClient(clientInstanceInfo, config);
        ApplicationInfoManager.getInstance().initComponent(new MyDataCenterInstanceConfig());
        return client;
    }

    public static InstanceInfo.Builder newInstanceInfoBuilder(int renewalIntervalInSecs) {
        InstanceInfo.Builder builder = InstanceInfo.Builder.newBuilder();
        builder.setIPAddr("10.10.101.00");
        builder.setHostName("Hosttt");
        builder.setAppName("EurekaTestApp-" + UUID.randomUUID());
        builder.setDataCenterInfo(new DataCenterInfo() {
            @Override
            public Name getName() {
                return Name.MyOwn;
            }
        });
        builder.setLeaseInfo(LeaseInfo.Builder.newBuilder().setRenewalIntervalInSecs(renewalIntervalInSecs).build());
        return builder;
    }

    private static void bindProperty(String propertyName, String value) {
        SYSTEM_PROPERTY_TRACKER.add(propertyName);
        ConfigurationManager.getConfigInstance().setProperty(propertyName, value);
    }

    public static class DiscoveryClientRuleBuilder {
        private boolean registrationEnabled;
        private boolean registryFetchEnabled;
        private Callable<Integer> portResolverCallable;
        private InstanceInfo instance;
        private SimpleEurekaHttpServer eurekaHttpServer;
        private List<String> remoteRegions;
        private String vipFetch;
        private String userName;
        private String password;

        public DiscoveryClientRuleBuilder withInstanceInfo(InstanceInfo instance) {
            this.instance = instance;
            return this;
        }

        public DiscoveryClientRuleBuilder withRegistration(boolean enabled) {
            this.registrationEnabled = enabled;
            return this;
        }

        public DiscoveryClientRuleBuilder withRegistryFetch(boolean enabled) {
            this.registryFetchEnabled = enabled;
            return this;
        }

        public DiscoveryClientRuleBuilder withPortResolver(Callable<Integer> portResolverCallable) {
            this.portResolverCallable = portResolverCallable;
            return this;
        }

        public DiscoveryClientRuleBuilder connectWith(SimpleEurekaHttpServer eurekaHttpServer) {
            this.eurekaHttpServer = eurekaHttpServer;
            return this;
        }

        public DiscoveryClientRuleBuilder withRemoteRegions(String... remoteRegions) {
            if (this.remoteRegions == null) {
                this.remoteRegions = new ArrayList<>();
            }
            Collections.addAll(this.remoteRegions, remoteRegions);
            return this;
        }

        public DiscoveryClientRuleBuilder withVipFetch(String vipFetch) {
            this.vipFetch = vipFetch;
            return this;
        }

        public DiscoveryClientRuleBuilder basicAuthentication(String userName, String password) {
            Preconditions.checkNotNull(userName, "HTTP basic authentication user name is null");
            Preconditions.checkNotNull(password, "HTTP basic authentication password is null");
            this.userName = userName;
            this.password = password;
            return this;
        }

        public DiscoveryClientResource build() {
            return new DiscoveryClientResource(this);
        }
    }
}
