/*
 * Copyright Terracotta, Inc.
 *
 * Licensed 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.ehcache.clustered.management;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.tc.net.proxy.TCPProxy;
import org.ehcache.Cache;
import org.ehcache.CacheManager;
import org.ehcache.Status;
import org.ehcache.clustered.ClusteredTests;
import org.ehcache.clustered.util.TCPProxyUtil;
import org.ehcache.config.units.EntryUnit;
import org.ehcache.config.units.MemoryUnit;
import org.ehcache.management.registry.DefaultManagementRegistryConfiguration;
import org.ehcache.testing.TestRetryer;
import org.ehcache.testing.TestRetryer.OutputIs;
import org.hamcrest.Matchers;
import org.junit.Assert;
import org.junit.BeforeClass;
import org.junit.ClassRule;
import org.junit.Rule;
import org.junit.Test;
import org.terracotta.management.model.capabilities.descriptors.Settings;

import java.net.URI;
import java.time.Duration;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Stream;

import static java.time.Duration.ofSeconds;
import static java.time.temporal.ChronoUnit.SECONDS;
import static java.util.Collections.unmodifiableMap;
import static java.util.EnumSet.of;
import static org.ehcache.clustered.client.config.builders.ClusteredResourcePoolBuilder.clusteredDedicated;
import static org.ehcache.clustered.client.config.builders.ClusteringServiceConfigurationBuilder.cluster;
import static org.ehcache.clustered.management.AbstractClusteringManagementTest.waitForAllNotifications;
import static org.ehcache.clustered.util.TCPProxyUtil.setDelay;
import static org.ehcache.config.builders.CacheConfigurationBuilder.newCacheConfigurationBuilder;
import static org.ehcache.config.builders.CacheManagerBuilder.newCacheManagerBuilder;
import static org.ehcache.config.builders.ResourcePoolsBuilder.newResourcePoolsBuilder;
import static org.ehcache.testing.TestRetryer.tryValues;
import static org.hamcrest.CoreMatchers.equalTo;
import static org.hamcrest.CoreMatchers.is;
import static org.junit.Assert.assertThat;
import static org.terracotta.testing.rules.BasicExternalClusterBuilder.newCluster;
import static org.terracotta.utilities.test.matchers.Eventually.within;

public class ManagementClusterConnectionTest extends ClusteredTests {

  protected static CacheManager cacheManager;
  protected static ObjectMapper mapper = new ObjectMapper();

  private static final List<TCPProxy> proxies = new ArrayList<>();
  private static final Map<String, Long> resources;
  static {
    HashMap<String, Long> map = new HashMap<>();
    map.put("primary-server-resource", 64L);
    map.put("secondary-server-resource", 64L);
    resources = unmodifiableMap(map);
  }

  @ClassRule @Rule
  public static TestRetryer<Duration, ClusterWithManagement> CLUSTER = tryValues(
    Stream.of(ofSeconds(1), ofSeconds(10), ofSeconds(30)),
    leaseLength -> new ClusterWithManagement(
      newCluster().in(clusterPath()).withServiceFragment(
        offheapResources(resources) + leaseLength(leaseLength)).build()),
    of(OutputIs.CLASS_RULE));

  @BeforeClass
  public static void beforeClass() throws Exception {

    mapper.configure(SerializationFeature.INDENT_OUTPUT, true);

    CLUSTER.get().getCluster().getClusterControl().waitForActive();

    URI connectionURI = TCPProxyUtil.getProxyURI(CLUSTER.get().getCluster().getConnectionURI(), proxies);

    cacheManager = newCacheManagerBuilder()
            // cluster config
            .with(cluster(connectionURI.resolve("/my-server-entity-1"))
                  .autoCreate(server -> server
                    .defaultServerResource("primary-server-resource")
                    .resourcePool("resource-pool-a", 10, MemoryUnit.MB, "secondary-server-resource") // <2>
                    .resourcePool("resource-pool-b", 10, MemoryUnit.MB))) // will take from primary-server-resource
            // management config
            .using(new DefaultManagementRegistryConfiguration()
                    .addTags("webapp-1", "server-node-1")
                    .setCacheManagerAlias("my-super-cache-manager"))
            // cache config
            .withCache("dedicated-cache-1", newCacheConfigurationBuilder(
                    String.class, String.class,
                    newResourcePoolsBuilder()
                            .heap(10, EntryUnit.ENTRIES)
                            .offheap(1, MemoryUnit.MB)
                            .with(clusteredDedicated("primary-server-resource", 4, MemoryUnit.MB)))
                    .build())
            .build(true);

    // ensure the CM is running and get its client id
    assertThat(cacheManager.getStatus(), equalTo(Status.AVAILABLE));

    // test_notifs_sent_at_CM_init
    waitForAllNotifications(CLUSTER.get().getNmsService(),
            "CLIENT_CONNECTED",
            "CLIENT_PROPERTY_ADDED",
            "CLIENT_PROPERTY_ADDED",
            "CLIENT_REGISTRY_AVAILABLE",
            "CLIENT_TAGS_UPDATED",
            "EHCACHE_RESOURCE_POOLS_CONFIGURED",
            "EHCACHE_SERVER_STORE_CREATED",
            "ENTITY_REGISTRY_AVAILABLE", "ENTITY_REGISTRY_AVAILABLE",
            "SERVER_ENTITY_CREATED", "SERVER_ENTITY_CREATED", "SERVER_ENTITY_CREATED", "SERVER_ENTITY_CREATED",
            "SERVER_ENTITY_DESTROYED",
            "SERVER_ENTITY_FETCHED", "SERVER_ENTITY_FETCHED", "SERVER_ENTITY_FETCHED", "SERVER_ENTITY_FETCHED", "SERVER_ENTITY_FETCHED", "SERVER_ENTITY_FETCHED",
            "SERVER_ENTITY_UNFETCHED"
    );
  }

  @Test
  public void test_reconnection() throws Exception {
    long count = CLUSTER.get().getNmsService().readTopology().clientStream()
            .filter(client -> client.getName()
                    .startsWith("Ehcache:") && client.isManageable() && client.getTags()
                    .containsAll(Arrays.asList("webapp-1", "server-node-1")))
            .count();

    Assert.assertThat(count, Matchers.equalTo(1L));

    String instanceId = getInstanceId();

    long delay = CLUSTER.input().plusSeconds(1L).toMillis();
    setDelay(delay, proxies);
    try {
      Thread.sleep(delay);
    } finally {
      setDelay(0L, proxies);
    }

    Cache<String, String> cache = cacheManager.getCache("dedicated-cache-1", String.class, String.class);
    String initiate_reconnect = cache.get("initiate reconnect");

    assertThat(initiate_reconnect, Matchers.nullValue());

    assertThat(() -> {
      try {
        return CLUSTER.get().getNmsService().readTopology().clientStream()
                  .filter(client -> client.getName()
                          .startsWith("Ehcache:") && client.isManageable() && client.getTags()
                          .containsAll(Arrays.asList("webapp-1", "server-node-1")))
                  .count();
      } catch (Exception e) {
        throw new AssertionError(e);
      }
    }, within(Duration.ofSeconds(30)).is(1L));
    assertThat(getInstanceId(), equalTo(instanceId));
  }

  private String getInstanceId() throws Exception {
    return CLUSTER.get().getNmsService().readTopology().clientStream()
      .filter(client -> client.getName().startsWith("Ehcache:") && client.isManageable())
      .findFirst().get()
      .getManagementRegistry().get()
      .getCapability("SettingsCapability").get()
      .getDescriptors(Settings.class).stream()
      .filter(settings -> settings.containsKey("instanceId"))
      .map(settings -> settings.getString("instanceId"))
      .findFirst().get();
  }
}