package com.ctrip.framework.apollo.integration;

import static org.hamcrest.core.IsEqual.equalTo;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertThat;
import static org.junit.Assert.assertTrue;

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.file.Files;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.eclipse.jetty.server.Request;
import org.eclipse.jetty.server.handler.AbstractHandler;
import org.eclipse.jetty.server.handler.ContextHandler;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.springframework.test.util.ReflectionTestUtils;

import com.ctrip.framework.apollo.BaseIntegrationTest;
import com.ctrip.framework.apollo.Config;
import com.ctrip.framework.apollo.ConfigChangeListener;
import com.ctrip.framework.apollo.ConfigService;
import com.ctrip.framework.apollo.build.ApolloInjector;
import com.ctrip.framework.apollo.core.ConfigConsts;
import com.ctrip.framework.apollo.core.dto.ApolloConfig;
import com.ctrip.framework.apollo.core.dto.ApolloConfigNotification;
import com.ctrip.framework.apollo.core.utils.ClassLoaderUtil;
import com.ctrip.framework.apollo.internals.RemoteConfigLongPollService;
import com.ctrip.framework.apollo.model.ConfigChangeEvent;
import com.google.common.base.Joiner;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.common.util.concurrent.SettableFuture;

/**
 * @author Jason Song(song_s@ctrip.com)
 */
public class ConfigIntegrationTest extends BaseIntegrationTest {
  private String someReleaseKey;
  private File configDir;
  private String defaultNamespace;
  private String someOtherNamespace;
  private RemoteConfigLongPollService remoteConfigLongPollService;

  @Before
  public void setUp() throws Exception {
    super.setUp();

    defaultNamespace = ConfigConsts.NAMESPACE_APPLICATION;
    someOtherNamespace = "someOtherNamespace";
    someReleaseKey = "1";
    configDir = new File(ClassLoaderUtil.getClassPath() + "config-cache");
    if (configDir.exists()) {
      configDir.delete();
    }
    configDir.mkdirs();
    remoteConfigLongPollService = ApolloInjector.getInstance(RemoteConfigLongPollService.class);
  }

  @Override
  @After
  public void tearDown() throws Exception {
    ReflectionTestUtils.invokeMethod(remoteConfigLongPollService, "stopLongPollingRefresh");
    recursiveDelete(configDir);
    super.tearDown();
  }

  private void recursiveDelete(File file) {
    if (!file.exists()) {
      return;
    }
    if (file.isDirectory()) {
      for (File f : file.listFiles()) {
        recursiveDelete(f);
      }
    }
    try {
      Files.deleteIfExists(file.toPath());
    } catch (IOException e) {
      e.printStackTrace();
    }

  }

  @Test
  public void testGetConfigWithNoLocalFileButWithRemoteConfig() throws Exception {
    String someKey = "someKey";
    String someValue = "someValue";
    String someNonExistedKey = "someNonExistedKey";
    String someDefaultValue = "someDefaultValue";
    ApolloConfig apolloConfig = assembleApolloConfig(ImmutableMap.of(someKey, someValue));
    ContextHandler handler = mockConfigServerHandler(HttpServletResponse.SC_OK, apolloConfig);
    startServerWithHandlers(handler);

    Config config = ConfigService.getAppConfig();

    assertEquals(someValue, config.getProperty(someKey, null));
    assertEquals(someDefaultValue, config.getProperty(someNonExistedKey, someDefaultValue));
  }

  @Test
  public void testGetConfigWithLocalFileAndWithRemoteConfig() throws Exception {
    String someKey = "someKey";
    String someValue = "someValue";
    String anotherValue = "anotherValue";
    Properties properties = new Properties();
    properties.put(someKey, someValue);
    createLocalCachePropertyFile(properties);

    ApolloConfig apolloConfig = assembleApolloConfig(ImmutableMap.of(someKey, anotherValue));
    ContextHandler handler = mockConfigServerHandler(HttpServletResponse.SC_OK, apolloConfig);
    startServerWithHandlers(handler);

    Config config = ConfigService.getAppConfig();

    assertEquals(anotherValue, config.getProperty(someKey, null));
  }

  @Test
  public void testGetConfigWithNoLocalFileAndRemoteConfigError() throws Exception {
    ContextHandler handler =
        mockConfigServerHandler(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, null);
    startServerWithHandlers(handler);

    Config config = ConfigService.getAppConfig();

    String someKey = "someKey";
    String someDefaultValue = "defaultValue" + Math.random();

    assertEquals(someDefaultValue, config.getProperty(someKey, someDefaultValue));
  }

  @Test
  public void testGetConfigWithLocalFileAndRemoteConfigError() throws Exception {
    String someKey = "someKey";
    String someValue = "someValue";
    Properties properties = new Properties();
    properties.put(someKey, someValue);
    createLocalCachePropertyFile(properties);

    ContextHandler handler =
        mockConfigServerHandler(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, null);
    startServerWithHandlers(handler);

    Config config = ConfigService.getAppConfig();
    assertEquals(someValue, config.getProperty(someKey, null));
  }

  @Test
  public void testGetConfigWithNoLocalFileAndRemoteMetaServiceRetry() throws Exception {
    String someKey = "someKey";
    String someValue = "someValue";
    ApolloConfig apolloConfig = assembleApolloConfig(ImmutableMap.of(someKey, someValue));
    ContextHandler configHandler = mockConfigServerHandler(HttpServletResponse.SC_OK, apolloConfig);
    boolean failAtFirstTime = true;
    ContextHandler metaServerHandler = mockMetaServerHandler(failAtFirstTime);
    startServerWithHandlers(metaServerHandler, configHandler);

    Config config = ConfigService.getAppConfig();

    assertEquals(someValue, config.getProperty(someKey, null));
  }

  @Test
  public void testGetConfigWithNoLocalFileAndRemoteConfigServiceRetry() throws Exception {
    String someKey = "someKey";
    String someValue = "someValue";
    ApolloConfig apolloConfig = assembleApolloConfig(ImmutableMap.of(someKey, someValue));
    boolean failedAtFirstTime = true;
    ContextHandler handler =
        mockConfigServerHandler(HttpServletResponse.SC_OK, apolloConfig, failedAtFirstTime);
    startServerWithHandlers(handler);

    Config config = ConfigService.getAppConfig();

    assertEquals(someValue, config.getProperty(someKey, null));
  }

  @Test
  public void testRefreshConfig() throws Exception {
    final String someKey = "someKey";
    final String someValue = "someValue";
    final String anotherValue = "anotherValue";

    int someRefreshInterval = 500;
    TimeUnit someRefreshTimeUnit = TimeUnit.MILLISECONDS;

    setRefreshInterval(someRefreshInterval);
    setRefreshTimeUnit(someRefreshTimeUnit);

    Map<String, String> configurations = Maps.newHashMap();
    configurations.put(someKey, someValue);
    ApolloConfig apolloConfig = assembleApolloConfig(configurations);
    ContextHandler handler = mockConfigServerHandler(HttpServletResponse.SC_OK, apolloConfig);
    startServerWithHandlers(handler);

    Config config = ConfigService.getAppConfig();
    final List<ConfigChangeEvent> changeEvents = Lists.newArrayList();

    final SettableFuture<Boolean> refreshFinished = SettableFuture.create();
    config.addChangeListener(new ConfigChangeListener() {
      AtomicInteger counter = new AtomicInteger(0);

      @Override
      public void onChange(ConfigChangeEvent changeEvent) {
        //only need to assert once
        if (counter.incrementAndGet() > 1) {
          return;
        }
        assertEquals(1, changeEvent.changedKeys().size());
        assertTrue(changeEvent.isChanged(someKey));
        assertEquals(someValue, changeEvent.getChange(someKey).getOldValue());
        assertEquals(anotherValue, changeEvent.getChange(someKey).getNewValue());
        // if there is any assertion failed above, this line won't be executed
        changeEvents.add(changeEvent);
        refreshFinished.set(true);
      }
    });

    apolloConfig.getConfigurations().put(someKey, anotherValue);

    refreshFinished.get(someRefreshInterval * 5, someRefreshTimeUnit);

    assertThat(
        "Change event's size should equal to one or there must be some assertion failed in change listener",
        1, equalTo(changeEvents.size()));
    assertEquals(anotherValue, config.getProperty(someKey, null));
  }

  @Test
  public void testLongPollRefresh() throws Exception {
    final String someKey = "someKey";
    final String someValue = "someValue";
    final String anotherValue = "anotherValue";
    long someNotificationId = 1;

    long pollTimeoutInMS = 50;
    Map<String, String> configurations = Maps.newHashMap();
    configurations.put(someKey, someValue);
    ApolloConfig apolloConfig = assembleApolloConfig(configurations);
    ContextHandler configHandler = mockConfigServerHandler(HttpServletResponse.SC_OK, apolloConfig);
    ContextHandler pollHandler =
        mockPollNotificationHandler(pollTimeoutInMS, HttpServletResponse.SC_OK,
            Lists.newArrayList(
                new ApolloConfigNotification(apolloConfig.getNamespaceName(), someNotificationId)),
            false);

    startServerWithHandlers(configHandler, pollHandler);

    Config config = ConfigService.getAppConfig();
    assertEquals(someValue, config.getProperty(someKey, null));

    final SettableFuture<Boolean> longPollFinished = SettableFuture.create();

    config.addChangeListener(new ConfigChangeListener() {
      @Override
      public void onChange(ConfigChangeEvent changeEvent) {
        longPollFinished.set(true);
      }
    });

    apolloConfig.getConfigurations().put(someKey, anotherValue);

    longPollFinished.get(pollTimeoutInMS * 20, TimeUnit.MILLISECONDS);

    assertEquals(anotherValue, config.getProperty(someKey, null));
  }

  @Test
  public void testLongPollRefreshWithMultipleNamespacesAndOnlyOneNamespaceNotified() throws Exception {
    final String someKey = "someKey";
    final String someValue = "someValue";
    final String anotherValue = "anotherValue";
    long someNotificationId = 1;

    long pollTimeoutInMS = 50;
    Map<String, String> configurations = Maps.newHashMap();
    configurations.put(someKey, someValue);
    ApolloConfig apolloConfig = assembleApolloConfig(configurations);
    ContextHandler configHandler = mockConfigServerHandler(HttpServletResponse.SC_OK, apolloConfig);
    ContextHandler pollHandler =
        mockPollNotificationHandler(pollTimeoutInMS, HttpServletResponse.SC_OK,
            Lists.newArrayList(
                new ApolloConfigNotification(apolloConfig.getNamespaceName(), someNotificationId)),
            false);

    startServerWithHandlers(configHandler, pollHandler);

    Config someOtherConfig = ConfigService.getConfig(someOtherNamespace);
    Config config = ConfigService.getAppConfig();
    assertEquals(someValue, config.getProperty(someKey, null));
    assertEquals(someValue, someOtherConfig.getProperty(someKey, null));

    final SettableFuture<Boolean> longPollFinished = SettableFuture.create();

    config.addChangeListener(new ConfigChangeListener() {
      @Override
      public void onChange(ConfigChangeEvent changeEvent) {
        longPollFinished.set(true);
      }
    });

    apolloConfig.getConfigurations().put(someKey, anotherValue);

    longPollFinished.get(5000, TimeUnit.MILLISECONDS);

    assertEquals(anotherValue, config.getProperty(someKey, null));

    TimeUnit.MILLISECONDS.sleep(pollTimeoutInMS * 10);
    assertEquals(someValue, someOtherConfig.getProperty(someKey, null));
  }

  @Test
  public void testLongPollRefreshWithMultipleNamespacesAndMultipleNamespaceNotified() throws Exception {
    final String someKey = "someKey";
    final String someValue = "someValue";
    final String anotherValue = "anotherValue";
    long someNotificationId = 1;

    long pollTimeoutInMS = 50;
    Map<String, String> configurations = Maps.newHashMap();
    configurations.put(someKey, someValue);
    ApolloConfig apolloConfig = assembleApolloConfig(configurations);
    ContextHandler configHandler = mockConfigServerHandler(HttpServletResponse.SC_OK, apolloConfig);
    ContextHandler pollHandler =
        mockPollNotificationHandler(pollTimeoutInMS, HttpServletResponse.SC_OK,
            Lists.newArrayList(
                new ApolloConfigNotification(apolloConfig.getNamespaceName(), someNotificationId),
                new ApolloConfigNotification(someOtherNamespace, someNotificationId)),
            false);

    startServerWithHandlers(configHandler, pollHandler);

    Config config = ConfigService.getAppConfig();
    Config someOtherConfig = ConfigService.getConfig(someOtherNamespace);
    assertEquals(someValue, config.getProperty(someKey, null));
    assertEquals(someValue, someOtherConfig.getProperty(someKey, null));

    final SettableFuture<Boolean> longPollFinished = SettableFuture.create();
    final SettableFuture<Boolean> someOtherNamespacelongPollFinished = SettableFuture.create();

    config.addChangeListener(new ConfigChangeListener() {
      @Override
      public void onChange(ConfigChangeEvent changeEvent) {
        longPollFinished.set(true);
      }
    });
    someOtherConfig.addChangeListener(new ConfigChangeListener() {
      @Override
      public void onChange(ConfigChangeEvent changeEvent) {
        someOtherNamespacelongPollFinished.set(true);
      }
    });

    apolloConfig.getConfigurations().put(someKey, anotherValue);

    longPollFinished.get(5000, TimeUnit.MILLISECONDS);
    someOtherNamespacelongPollFinished.get(5000, TimeUnit.MILLISECONDS);

    assertEquals(anotherValue, config.getProperty(someKey, null));
    assertEquals(anotherValue, someOtherConfig.getProperty(someKey, null));

  }

  private ContextHandler mockPollNotificationHandler(final long pollResultTimeOutInMS,
                                                     final int statusCode,
                                                     final List<ApolloConfigNotification> result,
                                                     final boolean failedAtFirstTime) {
    ContextHandler context = new ContextHandler("/notifications/v2");
    context.setHandler(new AbstractHandler() {
      AtomicInteger counter = new AtomicInteger(0);

      @Override
      public void handle(String target, Request baseRequest, HttpServletRequest request,
                         HttpServletResponse response) throws IOException, ServletException {
        if (failedAtFirstTime && counter.incrementAndGet() == 1) {
          response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
          baseRequest.setHandled(true);
          return;
        }

        try {
          TimeUnit.MILLISECONDS.sleep(pollResultTimeOutInMS);
        } catch (InterruptedException e) {
        }

        response.setContentType("application/json;charset=UTF-8");
        response.setStatus(statusCode);
        response.getWriter().println(gson.toJson(result));
        baseRequest.setHandled(true);
      }
    });

    return context;
  }

  private ContextHandler mockConfigServerHandler(final int statusCode, final ApolloConfig result,
                                                 final boolean failedAtFirstTime) {
    ContextHandler context = new ContextHandler("/configs/*");
    context.setHandler(new AbstractHandler() {
      AtomicInteger counter = new AtomicInteger(0);

      @Override
      public void handle(String target, Request baseRequest, HttpServletRequest request,
                         HttpServletResponse response) throws IOException, ServletException {
        if (failedAtFirstTime && counter.incrementAndGet() == 1) {
          response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
          baseRequest.setHandled(true);
          return;
        }

        response.setContentType("application/json;charset=UTF-8");
        response.setStatus(statusCode);
        response.getWriter().println(gson.toJson(result));
        baseRequest.setHandled(true);
      }
    });
    return context;
  }


  private ContextHandler mockConfigServerHandler(int statusCode, ApolloConfig result) {
    return mockConfigServerHandler(statusCode, result, false);
  }

  private ApolloConfig assembleApolloConfig(Map<String, String> configurations) {
    ApolloConfig apolloConfig =
        new ApolloConfig(someAppId, someClusterName, defaultNamespace, someReleaseKey);

    apolloConfig.setConfigurations(configurations);

    return apolloConfig;
  }

  private File createLocalCachePropertyFile(Properties properties) throws IOException {
    File file = new File(configDir, assembleLocalCacheFileName());
    try (FileOutputStream in = new FileOutputStream(file)) {
      properties.store(in, "Persisted by ConfigIntegrationTest");
    }
    return file;
  }

  private String assembleLocalCacheFileName() {
    return String.format("%s.properties", Joiner.on(ConfigConsts.CLUSTER_NAMESPACE_SEPARATOR)
        .join(someAppId, someClusterName, defaultNamespace));
  }
}
