/*
 * Licensed to the Apache Software Foundation (ASF) under one or more contributor license
 * agreements. See the NOTICE file distributed with this work for additional information regarding
 * copyright ownership. The ASF 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.apache.geode.internal.cache.snapshot;

import static org.apache.geode.distributed.ConfigurationProperties.*;
import static org.junit.Assert.*;

import java.io.File;
import java.io.IOException;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Properties;
import java.util.TreeMap;

import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.experimental.categories.Category;

import org.apache.geode.cache.Cache;
import org.apache.geode.cache.CacheFactory;
import org.apache.geode.cache.DataPolicy;
import org.apache.geode.cache.Region;
import org.apache.geode.cache.RegionFactory;
import org.apache.geode.cache.Scope;
import org.apache.geode.cache.client.ClientCache;
import org.apache.geode.cache.client.ClientCacheFactory;
import org.apache.geode.cache.client.ClientRegionShortcut;
import org.apache.geode.cache.snapshot.SnapshotIterator;
import org.apache.geode.cache.snapshot.SnapshotOptions.SnapshotFormat;
import org.apache.geode.cache.snapshot.SnapshotReader;
import org.apache.geode.distributed.LocatorLauncher;
import org.apache.geode.distributed.ServerLauncher;
import org.apache.geode.internal.AvailablePortHelper;
import org.apache.geode.pdx.ReflectionBasedAutoSerializer;
import org.apache.geode.test.dunit.DistributedTestUtils;
import org.apache.geode.test.dunit.Host;
import org.apache.geode.test.dunit.NetworkUtils;
import org.apache.geode.test.dunit.VM;
import org.apache.geode.test.dunit.internal.JUnit4DistributedTestCase;
import org.apache.geode.test.junit.categories.DistributedTest;
import org.apache.geode.test.junit.rules.serializable.SerializableTemporaryFolder;

@Category(DistributedTest.class)
public class GFSnapshotDUnitTest extends JUnit4DistributedTestCase {

  private VM locator;
  private VM server;
  private VM client;
  private Host host;

  @Rule
  public SerializableTemporaryFolder temporaryFolder = new SerializableTemporaryFolder();

  @Before
  public void before() {
    host = Host.getHost(0);
    locator = host.getVM(0);
    server = host.getVM(1);
    client = host.getVM(2);
  }

  @Test
  public void testDataExportAndIterate() throws IOException, ClassNotFoundException {
    int locatorPort = AvailablePortHelper.getRandomAvailableTCPPort();
    String serverHostName = NetworkUtils.getServerHostName(host);

    Properties properties = configureCommonProperties(new Properties());

    locator.invoke("Start Locator",
        () -> configureAndStartLocator(locatorPort, serverHostName, properties));
    server.invoke("Start Server",
        () -> configureAndStartServer(locatorPort, serverHostName, properties));
    client.invoke("Start client", () -> {
      createAndStartClient(locatorPort, serverHostName);
      return null;
    });
    client.invoke("Populate data", () -> populateDataOnClient());
    String snapshotFilePath = server.invoke("Export data snapshot", () -> createSnapshot());
    client.invoke("Iterate over snapshot", () -> {
      ClientCache clientCache = ClientCacheFactory.getAnyInstance();
      clientCache.close();
      createAndStartClient(locatorPort, serverHostName);
      iterateOverSnapshot(snapshotFilePath);
    });
  }

  private void createAndStartClient(final int locatorPort, final String serverHostName) {
    ClientCacheFactory clientCacheFactory = new ClientCacheFactory();
    clientCacheFactory.set("log-level", "config").addPoolLocator(serverHostName, locatorPort);
    ClientCache clientCache = clientCacheFactory.create();
    clientCache.createClientRegionFactory(ClientRegionShortcut.PROXY).create("TestRegion");
  }

  private Object populateDataOnClient() {
    ClientCache clientCache = ClientCacheFactory.getAnyInstance();
    Region testRegion = clientCache.getRegion("TestRegion");
    for (int i = 0; i < 100; i++) {
      testRegion.put(i, new TestObject(i, "owner_" + i));
    }
    return null;
  }

  private String createSnapshot() throws IOException {
    final String memberName = getUniqueName() + "-server";
    File file = temporaryFolder.newFolder(memberName + "-snapshot");
    Cache cache = CacheFactory.getAnyInstance();
    cache.getSnapshotService().save(file, SnapshotFormat.GEMFIRE);
    return file.getAbsolutePath();
  }

  private void iterateOverSnapshot(final String snapshotFilePath)
      throws IOException, ClassNotFoundException {

    File mySnapshot = new File(snapshotFilePath + "/snapshot-TestRegion.gfd");
    SnapshotIterator<Integer, TestObject> snapshotIterator = SnapshotReader.read(mySnapshot);

    Map<Integer, TestObject> result = new TreeMap<>();

    try {
      while (snapshotIterator.hasNext()) {
        Entry<Integer, TestObject> entry = snapshotIterator.next();
        int key = entry.getKey();
        TestObject value = entry.getValue();
        result.put(key, value);
      }
    } finally {
      snapshotIterator.close();
    }
    assertEquals(100, result.size());
    int count = 0;
    for (Entry<Integer, TestObject> entry : result.entrySet()) {
      assertEquals(count, (int) entry.getKey());
      assertEquals(new TestObject(count, "owner_" + count), entry.getValue());
      count++;
    }
  }

  private Properties configureCommonProperties(final Properties properties) {
    properties.setProperty(MCAST_PORT, "0");
    properties.setProperty(ENABLE_CLUSTER_CONFIGURATION, "false");
    properties.setProperty(USE_CLUSTER_CONFIGURATION, "false");
    return properties;
  }

  private void configureAndStartLocator(final int locatorPort, final String serverHostName,
      final Properties properties) throws IOException {
    DistributedTestUtils.deleteLocatorStateFile();

    final String memberName = getUniqueName() + "-locator";
    final File workingDirectory = temporaryFolder.newFolder(memberName);

    LocatorLauncher.Builder builder = new LocatorLauncher.Builder();

    for (String propertyName : properties.stringPropertyNames()) {
      builder.set(propertyName, properties.getProperty(propertyName));
    }
    LocatorLauncher locatorLauncher = builder.setBindAddress(serverHostName)
        .setHostnameForClients(serverHostName).setMemberName(memberName).setPort(locatorPort)
        .setWorkingDirectory(workingDirectory.getCanonicalPath()).build();
    locatorLauncher.start();

  }

  private void configureAndStartServer(final int locatorPort, final String serverHostName,
      final Properties properties) throws IOException {
    final String memberName = getUniqueName() + "-server";
    final File workingDirectory = temporaryFolder.newFolder(memberName);
    final File pdxDirectory = temporaryFolder.newFolder(memberName + "-pdx");
    final File diskStoreDirectory = temporaryFolder.newFolder(memberName + "-disk");


    ServerLauncher.Builder builder = new ServerLauncher.Builder();

    for (String propertyName : properties.stringPropertyNames()) {
      builder.set(propertyName, properties.getProperty(propertyName));
    }

    ServerLauncher serverLauncher =
        builder.set("locators", serverHostName + "[" + locatorPort + "]").setMemberName(memberName)
            .set("log-level", "config").setHostNameForClients(serverHostName)
            .setServerBindAddress(serverHostName).setServerPort(0)
            .setWorkingDirectory(workingDirectory.getCanonicalPath()).setPdxDiskStore("pdxDS")
            .setPdxPersistent(true).build();
    serverLauncher.start();

    Cache cache = CacheFactory.getAnyInstance();

    cache.createDiskStoreFactory().setDiskDirsAndSizes(new File[] {pdxDirectory}, new int[] {16000})
        .create("pdxDS");

    cache.createDiskStoreFactory()
        .setDiskDirsAndSizes(new File[] {diskStoreDirectory}, new int[] {16000})
        .create("diskStore");

    RegionFactory<Object, Object> regionFactory = cache.createRegionFactory();
    regionFactory.setDataPolicy(DataPolicy.PERSISTENT_REPLICATE).setScope(Scope.DISTRIBUTED_ACK)
        .setDiskStoreName("diskStore").create("TestRegion");
  }


}
