package com.netflix.eureka;

import javax.annotation.Nullable;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;

import com.google.common.collect.Lists;
import com.netflix.appinfo.AmazonInfo;
import com.netflix.appinfo.ApplicationInfoManager;
import com.netflix.appinfo.DataCenterInfo;
import com.netflix.appinfo.InstanceInfo;
import com.netflix.appinfo.LeaseInfo;
import com.netflix.appinfo.MyDataCenterInstanceConfig;
import com.netflix.config.ConfigurationManager;
import com.netflix.discovery.DefaultEurekaClientConfig;
import com.netflix.discovery.DiscoveryClient;
import com.netflix.discovery.EurekaClient;
import com.netflix.discovery.EurekaClientConfig;
import com.netflix.discovery.shared.Application;
import com.netflix.discovery.shared.Pair;
import com.netflix.eureka.cluster.PeerEurekaNodes;
import com.netflix.eureka.mock.MockRemoteEurekaServer;
import com.netflix.eureka.registry.PeerAwareInstanceRegistryImpl;
import com.netflix.eureka.resources.DefaultServerCodecs;
import com.netflix.eureka.resources.ServerCodecs;
import com.netflix.eureka.test.async.executor.SingleEvent;
import org.junit.After;
import org.junit.Before;

import static org.hamcrest.CoreMatchers.equalTo;
import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.core.IsNull.notNullValue;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.spy;

/**
 * @author Nitesh Kant
 */
public class AbstractTester {

    public static final String REMOTE_REGION_NAME = "us-east-1";
    public static final String REMOTE_REGION_APP_NAME = "MYAPP";
    public static final String REMOTE_REGION_INSTANCE_1_HOSTNAME = "blah";
    public static final String REMOTE_REGION_INSTANCE_2_HOSTNAME = "blah2";
    public static final String LOCAL_REGION_APP_NAME = "MYLOCAPP";
    public static final String LOCAL_REGION_INSTANCE_1_HOSTNAME = "blahloc";
    public static final String LOCAL_REGION_INSTANCE_2_HOSTNAME = "blahloc2";
    public static final String REMOTE_ZONE = "us-east-1c";

    protected final List<Pair<String, String>> registeredApps = new ArrayList<>();
    protected final Map<String, Application> remoteRegionApps = new HashMap<>();
    protected final Map<String, Application> remoteRegionAppsDelta = new HashMap<>();

    protected MockRemoteEurekaServer mockRemoteEurekaServer;
    protected EurekaServerConfig serverConfig;
    protected EurekaServerContext serverContext;
    protected EurekaClient client;
    protected PeerAwareInstanceRegistryImpl registry;

    @Before
    public void setUp() throws Exception {
        ConfigurationManager.getConfigInstance().clearProperty("eureka.remoteRegion.global.appWhiteList");
        ConfigurationManager.getConfigInstance().setProperty("eureka.responseCacheAutoExpirationInSeconds", "10");
        ConfigurationManager.getConfigInstance().clearProperty("eureka.remoteRegion." + REMOTE_REGION_NAME + ".appWhiteList");
        ConfigurationManager.getConfigInstance().setProperty("eureka.deltaRetentionTimerIntervalInMs", "600000");
        ConfigurationManager.getConfigInstance().setProperty("eureka.remoteRegion.registryFetchIntervalInSeconds", "5");
        ConfigurationManager.getConfigInstance().setProperty("eureka.renewalThresholdUpdateIntervalMs", "5000");
        ConfigurationManager.getConfigInstance().setProperty("eureka.evictionIntervalTimerInMs", "10000");

        populateRemoteRegistryAtStartup();
        mockRemoteEurekaServer = newMockRemoteServer();
        mockRemoteEurekaServer.start();

        ConfigurationManager.getConfigInstance().setProperty("eureka.remoteRegionUrlsWithName",
                REMOTE_REGION_NAME + ";http://localhost:" + mockRemoteEurekaServer.getPort() + MockRemoteEurekaServer.EUREKA_API_BASE_PATH);

        serverConfig = spy(new DefaultEurekaServerConfig());
        InstanceInfo.Builder builder = InstanceInfo.Builder.newBuilder();
        builder.setIPAddr("10.10.101.00");
        builder.setHostName("Hosttt");
        builder.setAppName("EurekaTestApp-" + UUID.randomUUID());
        builder.setLeaseInfo(LeaseInfo.Builder.newBuilder().build());
        builder.setDataCenterInfo(getDataCenterInfo());

        ConfigurationManager.getConfigInstance().setProperty("eureka.serviceUrl.default",
                "http://localhost:" + mockRemoteEurekaServer.getPort() + MockRemoteEurekaServer.EUREKA_API_BASE_PATH);

        DefaultEurekaClientConfig clientConfig = new DefaultEurekaClientConfig();
        // setup config in advance, used in initialize converter
        ApplicationInfoManager applicationInfoManager = new ApplicationInfoManager(new MyDataCenterInstanceConfig(), builder.build());

        client = new DiscoveryClient(applicationInfoManager, clientConfig);

        ServerCodecs serverCodecs = new DefaultServerCodecs(serverConfig);
        registry = makePeerAwareInstanceRegistry(serverConfig, clientConfig, serverCodecs, client);
        serverContext = new DefaultEurekaServerContext(
                serverConfig,
                serverCodecs,
                registry,
                mock(PeerEurekaNodes.class),
                applicationInfoManager
        );

        serverContext.initialize();

        registry.openForTraffic(applicationInfoManager, 1);
    }

    protected DataCenterInfo getDataCenterInfo() {
        return new DataCenterInfo() {
            @Override
            public Name getName() {
                return Name.MyOwn;
            }
        };
    }

    protected PeerAwareInstanceRegistryImpl makePeerAwareInstanceRegistry(EurekaServerConfig serverConfig,
                                                                      EurekaClientConfig clientConfig,
                                                                      ServerCodecs serverCodecs,
                                                                      EurekaClient eurekaClient) {
        return new TestPeerAwareInstanceRegistry(serverConfig, clientConfig, serverCodecs, eurekaClient);
    }

    protected MockRemoteEurekaServer newMockRemoteServer() {
        return new MockRemoteEurekaServer(0 /* use ephemeral */, remoteRegionApps, remoteRegionAppsDelta);
    }

    @After
    public void tearDown() throws Exception {
        for (Pair<String, String> registeredApp : registeredApps) {
            System.out.println("Canceling application: " + registeredApp.first() + " from local registry.");
            registry.cancel(registeredApp.first(), registeredApp.second(), false);
        }
        serverContext.shutdown();
        mockRemoteEurekaServer.stop();
        remoteRegionApps.clear();
        remoteRegionAppsDelta.clear();
        ConfigurationManager.getConfigInstance().clearProperty("eureka.remoteRegionUrls");
        ConfigurationManager.getConfigInstance().clearProperty("eureka.deltaRetentionTimerIntervalInMs");
    }

    private static Application createRemoteApps() {
        Application myapp = new Application(REMOTE_REGION_APP_NAME);
        InstanceInfo instanceInfo = createRemoteInstance(REMOTE_REGION_INSTANCE_1_HOSTNAME);
        //instanceInfo.setActionType(InstanceInfo.ActionType.MODIFIED);
        myapp.addInstance(instanceInfo);
        return myapp;
    }

    private static Application createRemoteAppsDelta() {
        Application myapp = new Application(REMOTE_REGION_APP_NAME);
        InstanceInfo instanceInfo = createRemoteInstance(REMOTE_REGION_INSTANCE_1_HOSTNAME);
        myapp.addInstance(instanceInfo);
        return myapp;
    }

    protected static InstanceInfo createRemoteInstance(String instanceHostName) {
        InstanceInfo.Builder instanceBuilder = InstanceInfo.Builder.newBuilder();
        instanceBuilder.setAppName(REMOTE_REGION_APP_NAME);
        instanceBuilder.setHostName(instanceHostName);
        instanceBuilder.setIPAddr("10.10.101.1");
        instanceBuilder.setDataCenterInfo(getAmazonInfo(REMOTE_ZONE, instanceHostName));
        instanceBuilder.setLeaseInfo(LeaseInfo.Builder.newBuilder().build());
        return instanceBuilder.build();
    }

    protected static InstanceInfo createLocalInstance(String hostname) {
        return createLocalInstanceWithStatus(hostname, InstanceInfo.InstanceStatus.UP);
    }

    protected static InstanceInfo createLocalStartingInstance(String hostname) {
        return createLocalInstanceWithStatus(hostname, InstanceInfo.InstanceStatus.STARTING);
    }

    protected static InstanceInfo createLocalOutOfServiceInstance(String hostname) {
        return createLocalInstanceWithStatus(hostname, InstanceInfo.InstanceStatus.OUT_OF_SERVICE);
    }

    protected static InstanceInfo createLocalInstanceWithIdAndStatus(String hostname, String id, InstanceInfo.InstanceStatus status) {
        InstanceInfo.Builder instanceBuilder = InstanceInfo.Builder.newBuilder();
        instanceBuilder.setInstanceId(id);
        instanceBuilder.setAppName(LOCAL_REGION_APP_NAME);
        instanceBuilder.setHostName(hostname);
        instanceBuilder.setIPAddr("10.10.101.1");
        instanceBuilder.setDataCenterInfo(getAmazonInfo(null, hostname));
        instanceBuilder.setLeaseInfo(LeaseInfo.Builder.newBuilder().build());
        instanceBuilder.setStatus(status);
        return instanceBuilder.build();
    }

    private static InstanceInfo createLocalInstanceWithStatus(String hostname, InstanceInfo.InstanceStatus status) {
        InstanceInfo.Builder instanceBuilder = InstanceInfo.Builder.newBuilder();
        instanceBuilder.setInstanceId("foo");
        instanceBuilder.setAppName(LOCAL_REGION_APP_NAME);
        instanceBuilder.setHostName(hostname);
        instanceBuilder.setIPAddr("10.10.101.1");
        instanceBuilder.setDataCenterInfo(getAmazonInfo(null, hostname));
        instanceBuilder.setLeaseInfo(LeaseInfo.Builder.newBuilder().build());
        instanceBuilder.setStatus(status);
        return instanceBuilder.build();
    }

    private static AmazonInfo getAmazonInfo(@Nullable String availabilityZone, String instanceHostName) {
        AmazonInfo.Builder azBuilder = AmazonInfo.Builder.newBuilder();
        azBuilder.addMetadata(AmazonInfo.MetaDataKey.availabilityZone, null == availabilityZone ? "us-east-1a" : availabilityZone);
        azBuilder.addMetadata(AmazonInfo.MetaDataKey.instanceId, instanceHostName);
        azBuilder.addMetadata(AmazonInfo.MetaDataKey.amiId, "XXX");
        azBuilder.addMetadata(AmazonInfo.MetaDataKey.instanceType, "XXX");
        azBuilder.addMetadata(AmazonInfo.MetaDataKey.localIpv4, "XXX");
        azBuilder.addMetadata(AmazonInfo.MetaDataKey.publicIpv4, "XXX");
        azBuilder.addMetadata(AmazonInfo.MetaDataKey.publicHostname, instanceHostName);
        return azBuilder.build();
    }

    private void populateRemoteRegistryAtStartup() {
        Application myapp = createRemoteApps();
        Application myappDelta = createRemoteAppsDelta();
        remoteRegionApps.put(REMOTE_REGION_APP_NAME, myapp);
        remoteRegionAppsDelta.put(REMOTE_REGION_APP_NAME, myappDelta);
    }

    private static class TestPeerAwareInstanceRegistry extends PeerAwareInstanceRegistryImpl {

        public TestPeerAwareInstanceRegistry(EurekaServerConfig serverConfig,
                                             EurekaClientConfig clientConfig,
                                             ServerCodecs serverCodecs,
                                             EurekaClient eurekaClient) {
            super(serverConfig, clientConfig, serverCodecs, eurekaClient);
        }

        @Override
        public InstanceInfo getNextServerFromEureka(String virtualHostname, boolean secure) {
            return null;
        }
    }

    protected void verifyLocalInstanceStatus(String id, InstanceInfo.InstanceStatus status) {
        InstanceInfo instanceInfo = registry.getApplication(LOCAL_REGION_APP_NAME).getByInstanceId(id);
        assertThat("InstanceInfo with id " + id + " not found", instanceInfo, is(notNullValue()));
        assertThat("Invalid InstanceInfo state", instanceInfo.getStatus(), is(equalTo(status)));
    }

    protected void registerInstanceLocally(InstanceInfo remoteInstance) {
        registry.register(remoteInstance, 10000000, false);
        registeredApps.add(new Pair<String, String>(LOCAL_REGION_APP_NAME, remoteInstance.getId()));
    }

    protected void registerInstanceLocallyWithLeaseDurationInSecs(InstanceInfo remoteInstance, int leaseDurationInSecs) {
        registry.register(remoteInstance, leaseDurationInSecs, false);
        registeredApps.add(new Pair<String, String>(LOCAL_REGION_APP_NAME, remoteInstance.getId()));
    }


    /**
     * Send renewal request to Eureka server to renew lease for 45 instances.
     *
     * @return action.
     */
    protected SingleEvent.Action renewPartOfTheWholeInstancesAction() {
        return new SingleEvent.Action() {
            @Override
            public void execute() {
                for (int j = 0; j < 45; j++) {
                    registry.renew(LOCAL_REGION_APP_NAME,
                        LOCAL_REGION_INSTANCE_1_HOSTNAME + j, false);
                }
            }
        };
    }

    /**
     * Build one single event.
     *
     * @param intervalTimeInSecs the interval time from previous event.
     * @param action             action to take.
     * @return single event.
     */
    protected SingleEvent buildEvent(int intervalTimeInSecs, SingleEvent.Action action) {
        return SingleEvent.Builder.newBuilder()
            .withIntervalTimeInMs(intervalTimeInSecs * 1000)
            .withAction(action).build();
    }

    /**
     * Build multiple {@link SingleEvent}.
     *
     * @param intervalTimeInSecs the interval time between those events.
     * @param eventCount         total event count.
     * @param action             action to take for every event.
     * @return list consisting of multiple single events.
     */
    protected List<SingleEvent> buildEvents(int intervalTimeInSecs, int eventCount, SingleEvent.Action action) {
        List<SingleEvent> result = Lists.newArrayListWithCapacity(eventCount);
        for (int i = 0; i < eventCount; i++) {
            result.add(SingleEvent.Builder.newBuilder()
                .withIntervalTimeInMs(intervalTimeInSecs * 1000)
                .withAction(action).build());
        }
        return result;
    }
}
