/*
 * 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.statistics;

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

import java.io.File;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.List;
import java.util.Properties;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;

import org.apache.logging.log4j.Logger;
import org.junit.After;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.experimental.categories.Category;
import org.junit.rules.TemporaryFolder;
import org.junit.rules.TestName;

import org.apache.geode.Statistics;
import org.apache.geode.StatisticsType;
import org.apache.geode.distributed.DistributedSystem;
import org.apache.geode.distributed.internal.InternalDistributedSystem;
import org.apache.geode.internal.GemFireVersion;
import org.apache.geode.internal.PureJavaMode;
import org.apache.geode.internal.cache.control.HeapMemoryMonitor;
import org.apache.geode.internal.logging.LogService;
import org.apache.geode.internal.net.SocketCreator;
import org.apache.geode.internal.statistics.GemFireStatSampler.LocalStatListenerImpl;
import org.apache.geode.internal.statistics.platform.OsStatisticsFactory;
import org.apache.geode.internal.statistics.platform.ProcessStats;
import org.apache.geode.internal.stats50.VMStats50;
import org.apache.geode.internal.util.StopWatch;
import org.apache.geode.test.junit.categories.FlakyTest;
import org.apache.geode.test.junit.categories.IntegrationTest;

/**
 * Integration tests for {@link GemFireStatSampler}.
 *
 * @since GemFire 7.0
 */
@Category(IntegrationTest.class)
public class GemFireStatSamplerIntegrationTest extends StatSamplerTestCase {

  private static final Logger logger = LogService.getLogger();

  private static final int STAT_SAMPLE_RATE = 1000;

  private DistributedSystem system;
  private File testDir;

  @Rule
  public TemporaryFolder temporaryFolder = new TemporaryFolder();

  @Rule
  public TestName testName = new TestName();

  @Before
  public void setUp() throws Exception {
    this.testDir = this.temporaryFolder.getRoot();
    assertTrue(this.testDir.exists());
  }

  /**
   * Removes the loner DistributedSystem at the end of each test.
   */
  @After
  public void tearDown() throws Exception {
    System.clearProperty(GemFireStatSampler.TEST_FILE_SIZE_LIMIT_IN_KB_PROPERTY);
    disconnect();
  }

  /**
   * Tests the majority of getters and the basic functionality of the sampler.
   * 
   * This test is skipped when running on Windows 7 because ProcessStats is not created for this OS.
   * See #45395.
   */
  @Test
  public void testBasics() throws Exception {
    final String osName = System.getProperty("os.name", "unknown");
    assumeFalse(osName.equals("Windows 7"));

    connect(createGemFireProperties());

    GemFireStatSampler statSampler = getGemFireStatSampler();
    assertTrue(statSampler.waitForInitialization(5000));

    assertEquals(0, statSampler.getArchiveFileSizeLimit());
    assertEquals(0, statSampler.getArchiveDiskSpaceLimit());
    assertEquals(STAT_SAMPLE_RATE, statSampler.getSampleRate());
    assertEquals(true, statSampler.isSamplingEnabled());

    int statsCount = statSampler.getStatisticsManager().getStatisticsCount();

    assertEquals(statsCount, statSampler.getStatisticsModCount());
    assertEquals(statsCount, statSampler.getStatisticsManager().getStatisticsCount());
    assertEquals(statsCount, statSampler.getStatistics().length);

    Assert.assertEquals(getStatisticsManager().getId(), statSampler.getSystemId());
    assertTrue(statSampler.getSystemStartTime() < System.currentTimeMillis());
    Assert.assertEquals(SocketCreator.getHostName(SocketCreator.getLocalHost()),
        statSampler.getSystemDirectoryPath());

    AllStatistics allStats = new AllStatistics(statSampler);

    VMStatsContract vmStats = statSampler.getVMStats();
    assertNotNull(vmStats);
    assertTrue(vmStats instanceof VMStats50);
    /*
     * NOTE: VMStats50 is not an instance of Statistics but instead its instance contains 3
     * instances of Statistics: 1) vmStats 2) heapMemStats 3) nonHeapMemStats
     */

    Method getProcessStats = getGemFireStatSampler().getClass().getMethod("getProcessStats");
    assertNotNull(getProcessStats);

    ProcessStats processStats = statSampler.getProcessStats();
    if (osName.equals("SunOS")) {
      assertNotNull(processStats);
      assertTrue(PureJavaMode.osStatsAreAvailable());
      assertTrue(allStats.containsStatisticsType("SolarisProcessStats"));
      assertTrue(allStats.containsStatisticsType("SolarisSystemStats"));
    } else if (osName.startsWith("Windows")) {
      // fails on Windows 7: 45395 "ProcessStats are not created on Windows 7"
      assertNotNull("ProcessStats were not created on " + osName, processStats);
      assertTrue(PureJavaMode.osStatsAreAvailable());
      assertTrue(allStats.containsStatisticsType("WindowsProcessStats"));
      assertTrue(allStats.containsStatisticsType("WindowsSystemStats"));
    } else if (osName.startsWith("Linux")) {
      assertNotNull(processStats);
      assertTrue(PureJavaMode.osStatsAreAvailable());
      assertTrue(allStats.containsStatisticsType("LinuxProcessStats"));
      assertTrue(allStats.containsStatisticsType("LinuxSystemStats"));
    } else if (osName.equals("Mac OS X")) {
      assertNull(processStats);
      assertFalse(PureJavaMode.osStatsAreAvailable());
      assertFalse(allStats.containsStatisticsType("OSXProcessStats"));
      assertFalse(allStats.containsStatisticsType("OSXSystemStats"));
    } else {
      assertNull(processStats);
    }

    String productDesc = statSampler.getProductDescription();
    assertTrue(productDesc.contains(GemFireVersion.getGemFireVersion()));
    assertTrue(productDesc.contains(GemFireVersion.getBuildId()));
    assertTrue(productDesc.contains(GemFireVersion.getSourceDate()));
  }

  /**
   * Tests that the configured archive file is created and exists.
   */
  @Test
  public void testArchiveFileExists() throws Exception {
    final String dir = this.testDir.getAbsolutePath();
    final String archiveFileName = dir + File.separator + this.testName.getMethodName() + ".gfs";

    final File archiveFile1 =
        new File(dir + File.separator + this.testName.getMethodName() + ".gfs");

    Properties props = createGemFireProperties();
    props.setProperty(STATISTIC_ARCHIVE_FILE, archiveFileName);
    connect(props);

    GemFireStatSampler statSampler = getGemFireStatSampler();
    assertTrue(statSampler.waitForInitialization(5000));

    final File archiveFile = statSampler.getArchiveFileName();
    assertNotNull(archiveFile);
    assertEquals(archiveFile1, archiveFile);

    waitForFileToExist(archiveFile, 5000, 10);

    assertTrue(
        "File name incorrect: archiveFile.getName()=" + archiveFile.getName()
            + " archiveFile.getAbsolutePath()=" + archiveFile.getAbsolutePath()
            + " getCanonicalPath()" + archiveFile.getCanonicalPath(),
        archiveFileName.contains(archiveFile.getName()));
  }

  /**
   * Tests the statistics sample rate within an acceptable margin of error.
   */
  @Test
  public void testSampleRate() throws Exception {
    connect(createGemFireProperties());

    GemFireStatSampler statSampler = getGemFireStatSampler();
    assertTrue(statSampler.waitForInitialization(5000));

    assertEquals(STAT_SAMPLE_RATE, statSampler.getSampleRate());

    assertTrue(getStatisticsManager().getStatListModCount() > 0);

    List<Statistics> statistics = getStatisticsManager().getStatsList();
    assertNotNull(statistics);
    assertTrue(statistics.size() > 0);

    StatisticsType statSamplerType = getStatisticsManager().findType("StatSampler");
    Statistics[] statsArray = getStatisticsManager().findStatisticsByType(statSamplerType);
    assertEquals(1, statsArray.length);

    final Statistics statSamplerStats = statsArray[0];
    final int initialSampleCount = statSamplerStats.getInt("sampleCount");
    final int expectedSampleCount = initialSampleCount + 2;

    waitForExpectedStatValue(statSamplerStats, "sampleCount", expectedSampleCount, 5000, 10);
  }

  /**
   * Adds a LocalStatListener for an individual stat. Validates that it receives notifications.
   * Removes the listener and validates that it was in fact removed and no longer receives
   * notifications.
   */
  @Test
  public void testLocalStatListener() throws Exception {
    connect(createGemFireProperties());

    GemFireStatSampler statSampler = getGemFireStatSampler();
    assertTrue(statSampler.waitForInitialization(5000));

    Method getLocalListeners = getGemFireStatSampler().getClass().getMethod("getLocalListeners");
    assertNotNull(getLocalListeners);

    Method addLocalStatListener = getGemFireStatSampler().getClass()
        .getMethod("addLocalStatListener", LocalStatListener.class, Statistics.class, String.class);
    assertNotNull(addLocalStatListener);

    Method removeLocalStatListener = getGemFireStatSampler().getClass()
        .getMethod("removeLocalStatListener", LocalStatListener.class);
    assertNotNull(removeLocalStatListener);

    // validate that there are no listeners
    assertTrue(statSampler.getLocalListeners().isEmpty());

    // add a listener for sampleCount stat in StatSampler statistics
    StatisticsType statSamplerType = getStatisticsManager().findType("StatSampler");
    Statistics[] statsArray = getStatisticsManager().findStatisticsByType(statSamplerType);
    assertEquals(1, statsArray.length);

    final Statistics statSamplerStats = statsArray[0];
    final String statName = "sampleCount";
    final AtomicInteger sampleCountValue = new AtomicInteger(0);
    final AtomicInteger sampleCountChanged = new AtomicInteger(0);

    LocalStatListener listener = new LocalStatListener() {
      public void statValueChanged(double value) {
        sampleCountValue.set((int) value);
        sampleCountChanged.incrementAndGet();
      }
    };

    statSampler.addLocalStatListener(listener, statSamplerStats, statName);
    assertTrue(statSampler.getLocalListeners().size() == 1);

    // there's a level of indirection here and some protected member fields
    LocalStatListenerImpl lsli =
        (LocalStatListenerImpl) statSampler.getLocalListeners().iterator().next();
    assertEquals("sampleCount", lsli.stat.getName());

    // wait for the listener to update 4 times
    final int expectedChanges = 4;
    boolean done = false;
    try {
      for (StopWatch time = new StopWatch(true); !done && time.elapsedTimeMillis() < 5000; done =
          (sampleCountChanged.get() >= expectedChanges)) {
        Thread.sleep(10);
      }
    } catch (InterruptedException e) {
      Thread.currentThread().interrupt();
    }
    assertTrue("Waiting for sampleCountChanged >= " + expectedChanges, done);

    // validate that the listener fired and updated the value
    assertTrue(sampleCountValue.get() > 0);
    assertTrue(sampleCountChanged.get() >= expectedChanges);

    // remove the listener
    statSampler.removeLocalStatListener(listener);
    final int expectedSampleCountValue = sampleCountValue.get();
    final int expectedSampleCountChanged = sampleCountChanged.get();

    // validate that there are no listeners now
    assertTrue(statSampler.getLocalListeners().isEmpty());

    // wait for 2 stat samples to occur
    waitForStatSample(statSamplerStats, expectedSampleCountValue, 5000, 10);

    // validate that the listener did not fire
    assertEquals(expectedSampleCountValue, sampleCountValue.get());
    assertEquals(expectedSampleCountChanged, sampleCountChanged.get());
  }

  /**
   * Invokes stop() and then validates that the sampler did in fact stop.
   */
  @Test
  public void testStop() throws Exception {
    connect(createGemFireProperties());

    GemFireStatSampler statSampler = getGemFireStatSampler();
    assertTrue(statSampler.waitForInitialization(5000));

    // validate the stat sampler is running
    StatisticsType statSamplerType = getStatisticsManager().findType("StatSampler");
    Statistics[] statsArray = getStatisticsManager().findStatisticsByType(statSamplerType);
    assertEquals(1, statsArray.length);

    final Statistics statSamplerStats = statsArray[0];
    final int initialSampleCount = statSamplerStats.getInt("sampleCount");
    final int expectedSampleCount = initialSampleCount + 2;

    waitForStatSample(statSamplerStats, expectedSampleCount, 20000, 10);

    // stop the stat sampler
    statSampler.stop();

    // validate the stat sampler has stopped
    final int stoppedSampleCount = statSamplerStats.getInt("sampleCount");

    // the following should timeout rather than complete
    assertStatValueDoesNotChange(statSamplerStats, "sampleCount", stoppedSampleCount, 5000, 10);

    assertEquals(stoppedSampleCount, statSamplerStats.getInt("sampleCount"));
  }

  /**
   * Verifies that archive rolling works correctly when archive-file-size-limit is specified.
   */
  @Test
  public void testArchiveRolling() throws Exception {
    final String dirName = this.testDir.getAbsolutePath() + File.separator + this.testName;
    new File(dirName).mkdirs();
    final String archiveFileName = dirName + File.separator + this.testName + ".gfs";

    final File archiveFile = new File(archiveFileName);
    final File archiveFile1 = new File(dirName + File.separator + this.testName + "-01-01.gfs");
    final File archiveFile2 = new File(dirName + File.separator + this.testName + "-01-02.gfs");
    final File archiveFile3 = new File(dirName + File.separator + this.testName + "-01-03.gfs");

    // set the system property to use KB instead of MB for file size
    System.setProperty(HostStatSampler.TEST_FILE_SIZE_LIMIT_IN_KB_PROPERTY, "true");
    Properties props = createGemFireProperties();
    props.setProperty(ARCHIVE_FILE_SIZE_LIMIT, "1");
    props.setProperty(ARCHIVE_DISK_SPACE_LIMIT, "0");
    props.setProperty(STATISTIC_ARCHIVE_FILE, archiveFileName);
    connect(props);

    assertTrue(getGemFireStatSampler().waitForInitialization(5000));

    boolean done = false;
    try {
      for (StopWatch time = new StopWatch(true); !done && time.elapsedTimeMillis() < 4000; done =
          (getSampleCollector() != null && getSampleCollector().getStatArchiveHandler() != null)) {
        Thread.sleep(10);
      }
    } catch (InterruptedException e) {
      Thread.currentThread().interrupt();
    }
    assertTrue("Waiting for getSampleCollector().getStatArchiveHandler() to not be null", done);

    StatArchiveHandler statArchiveHandler = getSampleCollector().getStatArchiveHandler();
    StatArchiveHandlerConfig config = statArchiveHandler.getStatArchiveHandlerConfig();
    assertEquals(1 * 1024, config.getArchiveFileSizeLimit());

    waitForFileToExist(archiveFile, 4000, 10);
    waitForFileToExist(archiveFile1, 4000, 10);
    waitForFileToExist(archiveFile2, 4000, 10);
    waitForFileToExist(archiveFile3, 4000, 10);
  }

  /**
   * Verifies that archive removal works correctly when archive-disk-space-limit is specified.
   */
  @Category(FlakyTest.class) // GEODE-2286: need to rewrite with Awaitility and longer timeouts
  @Test
  public void testArchiveRemoval() throws Exception {
    final String dirName = this.testDir.getAbsolutePath();// + File.separator + this.testName;
    new File(dirName).mkdirs();
    final String archiveFileName = dirName + File.separator + this.testName + ".gfs";

    final File archiveFile = new File(archiveFileName);
    final File archiveFile1 = new File(dirName + File.separator + this.testName + "-01-01.gfs");
    final File archiveFile2 = new File(dirName + File.separator + this.testName + "-01-02.gfs");
    final File archiveFile3 = new File(dirName + File.separator + this.testName + "-01-03.gfs");
    final File archiveFile4 = new File(dirName + File.separator + this.testName + "-01-04.gfs");

    final int sampleRate = 1000;

    // set the system property to use KB instead of MB for file size
    System.setProperty(HostStatSampler.TEST_FILE_SIZE_LIMIT_IN_KB_PROPERTY, "true");
    Properties props = createGemFireProperties();
    props.setProperty(STATISTIC_ARCHIVE_FILE, archiveFileName);
    props.setProperty(ARCHIVE_FILE_SIZE_LIMIT, "1");
    props.setProperty(ARCHIVE_DISK_SPACE_LIMIT, "12");
    props.setProperty(STATISTIC_SAMPLE_RATE, String.valueOf(sampleRate));
    connect(props);

    assertTrue(getGemFireStatSampler().waitForInitialization(5000));

    boolean exists1 = false;
    boolean exists2 = false;
    boolean exists3 = false;
    boolean exists4 = false;
    boolean exists = false;
    boolean done = false;
    try {
      for (StopWatch time = new StopWatch(true); !done
          && time.elapsedTimeMillis() < 10 * sampleRate;) {
        exists1 = exists1 || archiveFile1.exists();
        exists2 = exists2 || archiveFile2.exists();
        exists3 = exists3 || archiveFile3.exists();
        exists4 = exists4 || archiveFile4.exists();
        exists = exists || archiveFile.exists();
        done = exists1 && exists2 && exists3 && exists4 && exists;
        if (!done) {
          Thread.sleep(10);
        }
      }
    } catch (InterruptedException e) {
      Thread.currentThread().interrupt();
    }
    assertTrue("Waiting for archive files to exist:" + " exists1=" + exists1 + " exists2=" + exists2
        + " exists3=" + exists3 + " exists4=" + exists4 + " exists=" + exists, done);

    waitForFileToDelete(archiveFile1, 10 * sampleRate, 10);
  }

  @Test
  public void testLocalStatListenerRegistration() throws Exception {
    connect(createGemFireProperties());

    final GemFireStatSampler statSampler = getGemFireStatSampler();
    statSampler.waitForInitialization(5000);

    final AtomicBoolean flag = new AtomicBoolean(false);
    final LocalStatListener listener = new LocalStatListener() {
      public void statValueChanged(double value) {
        flag.set(true);
      }
    };

    final String tenuredPoolName = HeapMemoryMonitor.getTenuredMemoryPoolMXBean().getName();
    logger.info("TenuredPoolName: {}", tenuredPoolName);

    final List<Statistics> list = ((StatisticsManager) this.system).getStatsList();
    assertFalse(list.isEmpty());

    boolean done = false;
    try {
      for (StopWatch time = new StopWatch(true); !done && time.elapsedTimeMillis() < 5000;) {
        Thread.sleep(10);
        int i = 0;
        synchronized (list) {
          for (Object obj : list) {
            ++i;
            logger.info("List:{}:{}", i, obj);
            if (obj instanceof StatisticsImpl) {
              StatisticsImpl si = (StatisticsImpl) obj;
              logger.info("stat:{}", si.getTextId());
              if (si.getTextId().contains(tenuredPoolName)) {
                statSampler.addLocalStatListener(listener, si, "currentUsedMemory");
                done = true;
              }
            }
          }
        }
        // done = false;
      }
    } catch (InterruptedException e) {
      Thread.currentThread().interrupt();
    }
    assertTrue("Waiting for " + tenuredPoolName + " statistics to be added to create listener for",
        done);

    assertTrue(
        "expected at least one stat listener, found " + statSampler.getLocalListeners().size(),
        statSampler.getLocalListeners().size() > 0);

    long maxTenuredMemory = HeapMemoryMonitor.getTenuredMemoryPoolMXBean().getUsage().getMax();

    // byte[] bytes = new byte[1024 * 1024 * 10];
    byte[] bytes = new byte[(int) (maxTenuredMemory * 0.01)];
    Arrays.fill(bytes, Byte.MAX_VALUE);

    done = false;
    try {
      for (StopWatch time = new StopWatch(true); !done && time.elapsedTimeMillis() < 5000; done =
          (flag.get())) {
        Thread.sleep(10);
      }
    } catch (InterruptedException e) {
      Thread.currentThread().interrupt();
    }
    assertTrue("Waiting for listener to set flag to true", done);
  }

  @Override
  protected StatisticsManager getStatisticsManager() {
    return (InternalDistributedSystem) this.system;
  }

  protected OsStatisticsFactory getOsStatisticsFactory() {
    return (InternalDistributedSystem) this.system;
  }

  private GemFireStatSampler getGemFireStatSampler() {
    return ((InternalDistributedSystem) this.system).getStatSampler();
  }

  private SampleCollector getSampleCollector() {
    return getGemFireStatSampler().getSampleCollector();
  }

  private Properties createGemFireProperties() {
    Properties props = new Properties();
    props.setProperty(STATISTIC_SAMPLING_ENABLED, "true"); // TODO: test true/false
    props.setProperty(ENABLE_TIME_STATISTICS, "true"); // TODO: test true/false
    props.setProperty(STATISTIC_SAMPLE_RATE, String.valueOf(STAT_SAMPLE_RATE));
    props.setProperty(ARCHIVE_FILE_SIZE_LIMIT, "0");
    props.setProperty(ARCHIVE_DISK_SPACE_LIMIT, "0");
    props.setProperty(MCAST_PORT, "0");
    props.setProperty(LOCATORS, "");
    return props;
  }

  /**
   * Creates a fresh loner DistributedSystem for each test. Note that the DistributedSystem is the
   * StatisticsManager/Factory/etc.
   */
  @SuppressWarnings("deprecation")
  private void connect(Properties props) {
    this.system = DistributedSystem.connect(props);
  }

  @SuppressWarnings("deprecation")
  private void disconnect() {
    if (this.system != null) {
      this.system.disconnect();
      this.system = null;
    }
  }

  // public static class AsyncInvoker {
  // public static AsyncInvocation invokeAsync(Runnable r) {
  // return invokeAsync(r, "run", new Object[0]);
  // }
  // public static AsyncInvocation invokeAsync(Callable c) {
  // return invokeAsync(c, "call", new Object[0]);
  // }
  // public static AsyncInvocation invokeAsync(
  // final Object o, final String methodName, final Object[] args) {
  // AsyncInvocation ai =
  // new AsyncInvocation(o, methodName, new Runnable() {
  // public void run() {
  // MethExecutorResult result =
  // MethExecutor.executeObject(o, methodName, args);
  // if (result.exceptionOccurred()) {
  // throw new AsyncInvocationException(result.getException());
  // }
  // AsyncInvocation.setReturnValue(result.getResult());
  // }
  // });
  // ai.start();
  // return ai;
  // }
  //
  // public static class AsyncInvocationException extends RuntimeException {
  // private static final long serialVersionUID = -5522299018650622945L;
  // /**
  // * Creates a new <code>AsyncInvocationException</code>.
  // */
  // public AsyncInvocationException(String message) {
  // super(message);
  // }
  //
  // /**
  // * Creates a new <code>AsyncInvocationException</code> that was
  // * caused by a given exception
  // */
  // public AsyncInvocationException(String message, Throwable thr) {
  // super(message, thr);
  // }
  //
  // /**
  // * Creates a new <code>AsyncInvocationException</code> that was
  // * caused by a given exception
  // */
  // public AsyncInvocationException(Throwable thr) {
  // super(thr.getMessage(), thr);
  // }
  // }
  // }
}
