/*
 * 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.internal.statistics.StatArchiveFormat.NANOS_PER_MILLI;
import static org.apache.geode.internal.statistics.StatUtils.findResourceInsts;
import static org.apache.geode.internal.statistics.TestStatArchiveWriter.WRITER_INITIAL_DATE_MILLIS;
import static org.apache.geode.internal.statistics.TestStatArchiveWriter.WRITER_PREVIOUS_TIMESTAMP_NANOS;
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.io.IOException;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;

import org.apache.logging.log4j.LogManager;
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.rules.TemporaryFolder;
import org.junit.rules.TestName;

import org.apache.geode.StatisticDescriptor;
import org.apache.geode.Statistics;
import org.apache.geode.StatisticsType;
import org.apache.geode.internal.statistics.StatArchiveReader.ResourceInst;
import org.apache.geode.internal.statistics.StatArchiveReader.StatValue;

/**
 * Generates the stat archive file that is committed under src/test/resources for
 * {@link StatArchiveWithConsecutiveResourceInstIntegrationTest} to load.
 *
 * <p>
 * The generated gfs file is used to confirm GEODE-1782 and its fix.
 *
 * @since Geode 1.0
 */
public class StatArchiveWithConsecutiveResourceInstGenerator {

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

  protected static final String STATS_TYPE_NAME = "TestStats";
  protected static final String STATS_SPEC_STRING = ":" + STATS_TYPE_NAME;

  protected static final String TEST_NAME =
      StatArchiveWithConsecutiveResourceInstIntegrationTest.class.getSimpleName();
  protected static final String ARCHIVE_FILE_NAME = TEST_NAME + ".gfs";

  private File dir;
  private Map<String, String> statisticTypes;
  private Map<String, Map<String, Number>> allStatistics;
  protected String archiveFileName;

  private TestStatisticsManager manager;
  private TestStatisticsSampler sampler;
  private SampleCollector sampleCollector;
  private StatArchiveWriter writer;

  @Rule
  public TemporaryFolder temporaryFolder = new TemporaryFolder();

  @Rule
  public TestName testName = new TestName();

  @Before
  public void setUpGenerator() throws Exception {
    this.statisticTypes = new HashMap<>();
    this.allStatistics = new HashMap<>();
    this.dir = this.temporaryFolder.getRoot();
    this.archiveFileName =
        new File(this.dir.getAbsolutePath(), ARCHIVE_FILE_NAME).getAbsolutePath();

    this.manager = new TestStatisticsManager(1, getUniqueName(), WRITER_INITIAL_DATE_MILLIS);
    StatArchiveDescriptor archiveDescriptor =
        new StatArchiveDescriptor.Builder().setArchiveName(this.archiveFileName).setSystemId(1)
            .setSystemStartTime(WRITER_INITIAL_DATE_MILLIS - 2000).setSystemDirectoryPath(TEST_NAME)
            .setProductDescription(TEST_NAME).build();
    this.writer = new TestStatArchiveWriter(archiveDescriptor);
    this.sampler = new TestStatisticsSampler(manager);
    this.sampleCollector = new SampleCollector(sampler);
    this.sampleCollector.addSampleHandler(this.writer);
  }

  @After
  public void tearDown() throws Exception {
    StatisticsTypeFactoryImpl.clear();
  }

  @Test
  public void generateStatArchiveFile() throws Exception {

    long sampleTimeNanos = WRITER_PREVIOUS_TIMESTAMP_NANOS + NANOS_PER_MILLI * 1000;

    // 1) create statistics

    StatisticsType type =
        createStatisticsType(STATS_TYPE_NAME, "description of " + STATS_TYPE_NAME);
    Statistics statistics1 = createStatistics(type, STATS_TYPE_NAME + "1", 1);

    // 2) sample changing stat

    for (int i = 0; i < 100; i++) {
      incInt(statistics1, "stat", 1);
      this.sampleCollector.sample(sampleTimeNanos += (1000 * NANOS_PER_MILLI));
    }

    // 3) close statistics

    statistics1.close();

    // 4) recreate statistics

    Statistics statistics2 = createStatistics(type, STATS_TYPE_NAME + "1", 1);

    // 5) sample changing stat again

    for (int i = 0; i < 100; i++) {
      incInt(statistics2, "stat", 1);
      this.sampleCollector.sample(sampleTimeNanos += (1000 * NANOS_PER_MILLI));
    }

    // close the writer

    this.writer.close();

    // validate that stat archive file exists

    File actual = new File(this.archiveFileName);
    assertTrue(actual.exists());

    // validate content of stat archive file using StatArchiveReader

    StatArchiveReader reader = new StatArchiveReader(new File[] {actual}, null, false);

    // compare all resourceInst values against what was printed above

    for (final Iterator iter = reader.getResourceInstList().iterator(); iter.hasNext();) {
      StatArchiveReader.ResourceInst ri = (StatArchiveReader.ResourceInst) iter.next();
      String resourceName = ri.getName();
      assertNotNull(resourceName);

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

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

      StatValue[] statValues = ri.getStatValues();
      for (int i = 0; i < statValues.length; i++) {
        final 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.01);
      }
    }

    validateArchiveFile();
  }

  protected void validateArchiveFile() throws IOException {
    final File archiveFile = new File(this.archiveFileName);
    assertTrue(archiveFile.exists());

    logger.info("ArchiveFile: {}", archiveFile.getAbsolutePath());
    logger.info("ArchiveFile length: {}", archiveFile.length());

    for (ResourceInst resourceInst : findResourceInsts(archiveFile, STATS_SPEC_STRING)) {
      logger.info("ResourceInst: {}", resourceInst);
    }
  }

  private String getUniqueName() {
    return StatArchiveWithConsecutiveResourceInstGenerator.class + "_"
        + this.testName.getMethodName();
  }

  private StatisticsType createStatisticsType(final String name, final String description) {
    StatisticDescriptor[] descriptors = new StatisticDescriptor[] {
        manager.createIntCounter("stat", "description of stat", "units"),};
    return manager.createType(name, description, descriptors);
  }

  private Statistics createStatistics(final StatisticsType type, final String textId,
      final long numericId) {
    return manager.createAtomicStatistics(type, textId, 1);
  }

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