/*
 * 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.test.awaitility.GeodeAwaitility.await;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;

import java.io.File;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.apache.logging.log4j.Logger;
import org.junit.After;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.contrib.java.lang.system.RestoreSystemProperties;
import org.junit.experimental.categories.Category;
import org.junit.rules.TemporaryFolder;
import org.junit.rules.TestName;

import org.apache.geode.CancelCriterion;
import org.apache.geode.StatisticDescriptor;
import org.apache.geode.Statistics;
import org.apache.geode.StatisticsType;
import org.apache.geode.internal.statistics.StatArchiveReader.StatValue;
import org.apache.geode.logging.internal.log4j.api.LogService;
import org.apache.geode.test.junit.categories.StatisticsTest;

/**
 * Integration tests for statistics sampling.
 *
 * @since GemFire 7.0
 */
@Category({StatisticsTest.class})
public class StatSamplerIntegrationTest {

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

  private Map<String, String> statisticTypes;
  private Map<String, Map<String, Number>> allStatistics;

  @Rule
  public RestoreSystemProperties restoreSystemProperties = new RestoreSystemProperties();

  @Rule
  public TemporaryFolder temporaryFolder = new TemporaryFolder();

  @Rule
  public TestName testName = new TestName();

  @Before
  public void setUp() {
    statisticTypes = new HashMap<>();
    allStatistics = new HashMap<>();
  }

  @After
  public void tearDown() {
    statisticTypes = null;
    allStatistics = null;
    StatisticsTypeFactoryImpl.clear();
    StatArchiveWriter.clearTraceFilter();
  }

  private String getName() {
    return getClass().getSimpleName() + "_" + testName.getMethodName();
  }

  @Test
  public void testStatSampler() throws Exception {
    StatArchiveWriter.setTraceFilter("st1_1", "ST1");

    File folder = temporaryFolder.newFolder();
    String archiveFileName = folder.getAbsolutePath() + File.separator + getName() + ".gfs";

    System.setProperty("stats.disable", "false");
    System.setProperty("stats.name", getName());
    System.setProperty("stats.archive-file", archiveFileName);
    System.setProperty("stats.file-size-limit", "0");
    System.setProperty("stats.disk-space-limit", "0");
    System.setProperty("stats.sample-rate", "100");

    final CancelCriterion stopper = new CancelCriterion() {
      @Override
      public String cancelInProgress() {
        return null;
      }

      @Override
      public RuntimeException generateCancelledException(Throwable e) {
        return null;
      }
    };
    final LocalStatisticsFactory factory = new LocalStatisticsFactory(stopper);
    final StatisticDescriptor[] statsST1 =
        new StatisticDescriptor[] {factory.createDoubleCounter("double_counter_1", "d1", "u1"),
            factory.createDoubleCounter("double_counter_2", "d2", "u2", true),
            factory.createDoubleGauge("double_gauge_3", "d3", "u3"),
            factory.createDoubleGauge("double_gauge_4", "d4", "u4", false),
            factory.createIntCounter("int_counter_5", "d5", "u5"),
            factory.createIntCounter("int_counter_6", "d6", "u6", true),
            factory.createIntGauge("int_gauge_7", "d7", "u7"),
            factory.createIntGauge("int_gauge_8", "d8", "u8", false),
            factory.createLongCounter("long_counter_9", "d9", "u9"),
            factory.createLongCounter("long_counter_10", "d10", "u10", true),
            factory.createLongGauge("long_gauge_11", "d11", "u11"),
            factory.createLongGauge("long_gauge_12", "d12", "u12", false),
            factory.createLongGauge("sampled_long", "d13", "u13", false),
            factory.createIntGauge("sampled_int", "d14", "u14", false),
            factory.createDoubleGauge("sampled_double", "d15", "u15", false)};
    final StatisticsType ST1 = factory.createType("ST1", "ST1", statsST1);
    final Statistics st1_1 = factory.createAtomicStatistics(ST1, "st1_1", 1);
    st1_1.setIntSupplier("sampled_int", () -> 5);
    getOrCreateExpectedValueMap(st1_1).put("sampled_int", 5);
    st1_1.setLongSupplier("sampled_long", () -> 6);
    getOrCreateExpectedValueMap(st1_1).put("sampled_long", 6);
    st1_1.setDoubleSupplier("sampled_double", () -> 7.0);
    getOrCreateExpectedValueMap(st1_1).put("sampled_double", 7.0);

    await("awaiting StatSampler readiness")
        .until(() -> hasSamplerStatsInstances(factory));

    Statistics[] samplerStatsInstances = factory.findStatisticsByTextId("statSampler");
    assertNotNull(samplerStatsInstances);
    assertEquals(1, samplerStatsInstances.length);
    final Statistics samplerStats = samplerStatsInstances[0];

    incDouble(st1_1, "double_counter_1", 1);
    incDouble(st1_1, "double_gauge_3", 3);
    incInt(st1_1, "int_counter_5", 5);
    incInt(st1_1, "int_gauge_7", 7);
    incLong(st1_1, "long_counter_9", 9);
    incLong(st1_1, "long_gauge_11", 11);

    waitForStatSamplerToRun(samplerStats);

    incDouble(st1_1, "double_counter_1", 1);
    incDouble(st1_1, "double_counter_2", 1);
    incDouble(st1_1, "double_gauge_3", 1);
    incDouble(st1_1, "double_gauge_4", 1);
    incInt(st1_1, "int_counter_5", 1);
    incInt(st1_1, "int_counter_6", 1);

    waitForStatSamplerToRun(samplerStats);

    incDouble(st1_1, "double_counter_1", 1);
    incDouble(st1_1, "double_counter_2", 1);
    incDouble(st1_1, "double_gauge_3", 1);
    incDouble(st1_1, "double_gauge_4", 1);
    incInt(st1_1, "int_counter_5", 1);
    incInt(st1_1, "int_counter_6", 1);
    incInt(st1_1, "int_gauge_7", 1);
    incInt(st1_1, "int_gauge_8", 1);
    incLong(st1_1, "long_counter_9", 1);
    incLong(st1_1, "long_counter_10", 1);
    incLong(st1_1, "long_gauge_11", 1);
    incLong(st1_1, "long_gauge_12", 1);

    waitForStatSamplerToRun(samplerStats);

    incDouble(st1_1, "double_counter_1", 1);
    incDouble(st1_1, "double_counter_2", 1);
    incDouble(st1_1, "double_gauge_3", -1);
    incDouble(st1_1, "double_gauge_4", 1);
    incInt(st1_1, "int_counter_5", 1);
    incInt(st1_1, "int_counter_6", 1);
    incInt(st1_1, "int_gauge_7", -1);
    incInt(st1_1, "int_gauge_8", 1);
    incLong(st1_1, "long_counter_9", 1);
    incLong(st1_1, "long_counter_10", 1);
    incLong(st1_1, "long_gauge_11", -1);
    incLong(st1_1, "long_gauge_12", 1);

    waitForStatSamplerToRun(samplerStats);

    incDouble(st1_1, "double_counter_1", 1);
    incDouble(st1_1, "double_counter_2", 1);
    incDouble(st1_1, "double_gauge_3", 1);
    incDouble(st1_1, "double_gauge_4", 1);
    incInt(st1_1, "int_counter_5", 1);
    incInt(st1_1, "int_counter_6", 1);
    incInt(st1_1, "int_gauge_7", 1);
    incInt(st1_1, "int_gauge_8", 1);
    incLong(st1_1, "long_counter_9", 1);
    incLong(st1_1, "long_counter_10", 1);
    incLong(st1_1, "long_gauge_11", 1);
    incLong(st1_1, "long_gauge_12", 1);

    waitForStatSamplerToRun(samplerStats);
    waitForStatSamplerToRun(samplerStats);
    waitForStatSamplerToRun(samplerStats);

    incDouble(st1_1, "double_counter_1", 1);
    incDouble(st1_1, "double_gauge_3", 3);
    incInt(st1_1, "int_counter_5", 5);
    incInt(st1_1, "int_gauge_7", 7);
    incLong(st1_1, "long_counter_9", 9);
    incLong(st1_1, "long_gauge_11", 11);

    waitForStatSamplerToRun(samplerStats);

    incDouble(st1_1, "double_counter_1", 1);
    incDouble(st1_1, "double_counter_2", 1);
    incDouble(st1_1, "double_gauge_3", 1);
    incDouble(st1_1, "double_gauge_4", 1);
    incInt(st1_1, "int_counter_5", 1);
    incInt(st1_1, "int_counter_6", 1);

    waitForStatSamplerToRun(samplerStats);

    incDouble(st1_1, "double_counter_1", 1);
    incDouble(st1_1, "double_counter_2", 1);
    incDouble(st1_1, "double_gauge_3", 1);
    incDouble(st1_1, "double_gauge_4", 1);
    incInt(st1_1, "int_counter_5", 1);
    incInt(st1_1, "int_counter_6", 1);
    incInt(st1_1, "int_gauge_7", 1);
    incInt(st1_1, "int_gauge_8", 1);
    incLong(st1_1, "long_counter_9", 1);
    incLong(st1_1, "long_counter_10", 1);
    incLong(st1_1, "long_gauge_11", 1);
    incLong(st1_1, "long_gauge_12", 1);

    waitForStatSamplerToRun(samplerStats);

    incDouble(st1_1, "double_counter_1", 1);
    incDouble(st1_1, "double_counter_2", 1);
    incDouble(st1_1, "double_gauge_3", -1);
    incDouble(st1_1, "double_gauge_4", 1);
    incInt(st1_1, "int_counter_5", 1);
    incInt(st1_1, "int_counter_6", 1);
    incInt(st1_1, "int_gauge_7", -1);
    incInt(st1_1, "int_gauge_8", 1);
    incLong(st1_1, "long_counter_9", 1);
    incLong(st1_1, "long_counter_10", 1);
    incLong(st1_1, "long_gauge_11", -1);
    incLong(st1_1, "long_gauge_12", 1);

    waitForStatSamplerToRun(samplerStats);

    incDouble(st1_1, "double_counter_1", 1);
    incDouble(st1_1, "double_counter_2", 1);
    incDouble(st1_1, "double_gauge_3", 1);
    incDouble(st1_1, "double_gauge_4", 1);
    incInt(st1_1, "int_counter_5", 1);
    incInt(st1_1, "int_counter_6", 1);
    incInt(st1_1, "int_gauge_7", 1);
    incInt(st1_1, "int_gauge_8", 1);
    incLong(st1_1, "long_counter_9", 1);
    incLong(st1_1, "long_counter_10", 1);
    incLong(st1_1, "long_gauge_11", 1);
    incLong(st1_1, "long_gauge_12", 1);

    /*
     * After updating all stats we should wait for the stat sampler to run at least twice. The
     * first statSampler iteration may be running as the stats are being updated. The second
     * statSampler iteration
     * makes sure that all the stats we care about have been sampled and flush to the stat archive
     */
    waitForStatSamplerToRun(samplerStats, 2);

    factory.close();

    final File archiveFile = new File(System.getProperty("stats.archive-file"));
    assertTrue(archiveFile.exists());
    final StatArchiveReader reader = new StatArchiveReader(new File[] {archiveFile}, null, false);

    List resources = reader.getResourceInstList();
    for (Object resource : resources) {
      StatArchiveReader.ResourceInst ri = (StatArchiveReader.ResourceInst) resource;
      String resourceName = ri.getName();
      assertNotNull(resourceName);

      if (!resourceName.equals("st1_1")) {
        logger.info("testStatSampler skipping {}", resourceName);
        continue;
      }

      String expectedStatsType = statisticTypes.get(resourceName);
      assertNotNull(expectedStatsType);
      assertEquals(expectedStatsType, ri.getType().getName());

      Map<String, Number> expectedStatValues = allStatistics.get(resourceName);
      assertNotNull(expectedStatValues);

      StatValue[] statValues = ri.getStatValues();
      for (int i = 0; i < statValues.length; i++) {
        String statName = ri.getType().getStats()[i].getName();
        assertNotNull(statName);
        assertNotNull(expectedStatValues.get(statName));

        assertEquals(statName, statValues[i].getDescriptor().getName());

        statValues[i].setFilter(StatValue.FILTER_NONE);
        // double[] rawSnapshots = statValues[i].getRawSnapshots();
        assertEquals("Value " + i + " for " + statName + " is wrong: " + expectedStatValues,
            expectedStatValues.get(statName).doubleValue(), statValues[i].getSnapshotsMostRecent(),
            0);
      }
    }
  }

  private boolean hasSamplerStatsInstances(final LocalStatisticsFactory factory) {
    Statistics[] samplerStatsInstances = factory.findStatisticsByTextId("statSampler");
    return samplerStatsInstances != null && samplerStatsInstances.length > 0;
  }

  private void waitForStatSamplerToRun(final Statistics samplerStats, final int timesToRun) {
    final int startSampleCount = samplerStats.getInt("sampleCount");
    await("waiting for the StatSampler to run")
        .until(() -> samplerStats.getInt("sampleCount") >= startSampleCount + timesToRun);
  }

  private void waitForStatSamplerToRun(final Statistics samplerStats) {
    waitForStatSamplerToRun(samplerStats, 1);
  }

  private void incDouble(Statistics statistics, String stat, double value) {
    assertFalse(statistics.isClosed());
    Map<String, Number> statValues = getOrCreateExpectedValueMap(statistics);
    statistics.incDouble(stat, value);
    statValues.put(stat, statistics.getDouble(stat));
    if (statisticTypes.get(statistics.getTextId()) == null) {
      statisticTypes.put(statistics.getTextId(), statistics.getType().getName());
    }
  }

  private void incInt(Statistics statistics, String stat, int value) {
    assertFalse(statistics.isClosed());
    Map<String, Number> statValues = getOrCreateExpectedValueMap(statistics);
    statistics.incInt(stat, value);
    statValues.put(stat, statistics.getInt(stat));
    if (statisticTypes.get(statistics.getTextId()) == null) {
      statisticTypes.put(statistics.getTextId(), statistics.getType().getName());
    }
  }

  private Map<String, Number> getOrCreateExpectedValueMap(final Statistics statistics) {
    return allStatistics.computeIfAbsent(statistics.getTextId(), k -> new HashMap<>());
  }

  private void incLong(Statistics statistics, String stat, long value) {
    assertFalse(statistics.isClosed());
    Map<String, Number> statValues = getOrCreateExpectedValueMap(statistics);
    statistics.incLong(stat, value);
    statValues.put(stat, statistics.getLong(stat));
    if (statisticTypes.get(statistics.getTextId()) == null) {
      statisticTypes.put(statistics.getTextId(), statistics.getType().getName());
    }
  }
}
