/*
 * 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.gobblin.metrics.reporter;

import java.util.Properties;
import java.util.Set;
import java.util.concurrent.TimeUnit;

import javax.annotation.Nullable;

import org.testng.Assert;
import org.testng.annotations.Test;

import com.google.common.base.Function;
import com.google.common.collect.Iterables;
import com.google.common.collect.Sets;
import com.typesafe.config.Config;

import org.apache.gobblin.metrics.MetricContext;
import org.apache.gobblin.metrics.context.ReportableContext;
import org.apache.gobblin.metrics.context.filter.ContextFilterFactory;
import org.apache.gobblin.metrics.test.ContextStoreReporter;
import org.apache.gobblin.util.ConfigUtils;


/**
 * Test for {@link org.apache.gobblin.metrics.reporter.ScheduledReporter}
 */
public class ScheduledReporterTest {

  @Test
  public void testPeriodParser() {

    Assert.assertEquals(ScheduledReporter.parsePeriodToSeconds("1s"), 1);
    Assert.assertEquals(ScheduledReporter.parsePeriodToSeconds("2m"), 120);
    Assert.assertEquals(ScheduledReporter.parsePeriodToSeconds("3h"), 3 * 3600);
    Assert.assertEquals(ScheduledReporter.parsePeriodToSeconds("1m2s"), 62);
    Assert.assertEquals(ScheduledReporter.parsePeriodToSeconds("1h1s"), 3601);
    Assert.assertEquals(ScheduledReporter.parsePeriodToSeconds("1h2m3s"), 3600 + 120 + 3);

    try {
      ScheduledReporter.parsePeriodToSeconds("1000000h");
      Assert.fail();
    } catch (RuntimeException re) {
      // fail unless exception is thrown
    }
  }

  @Test
  public void testScheduledReporter() throws Exception {

    long reportingIntervalMillis = 1000;

    String context1Name = ScheduledReporterTest.class.getSimpleName() + "_1";
    String context2Name = ScheduledReporterTest.class.getSimpleName() + "_2";
    String context3Name = "SomeOtherName";

    // Create a context name (to check that initialized reporter gets existing contexts correctly)
    MetricContext context1 = MetricContext.builder(context1Name).build();

    // Set up config for reporter
    Properties props = new Properties();
    ScheduledReporter.setReportingInterval(props, reportingIntervalMillis, TimeUnit.MILLISECONDS);
    Config config = ConfigUtils.propertiesToConfig(props);
    config = PrefixContextFilter.setPrefixString(config, ScheduledReporterTest.class.getSimpleName());
    config = ContextFilterFactory.setContextFilterClass(config, PrefixContextFilter.class);

    // Create reporter
    ContextStoreReporter reporter = new ContextStoreReporter("testContext", config);

    // Check that reporter correctly found created context
    Set<String> contextNames = getContextNames(reporter);
    Assert.assertEquals(contextNames.size(), 1);
    Assert.assertTrue(contextNames.contains(context1Name));

    // Create two more contexts
    MetricContext context2 = context1.childBuilder(context2Name).build();
    context1.childBuilder(context3Name).build();

    // Check that reporter correctly found new reporter, but skipped the one that does not satisfy filter
    contextNames = getContextNames(reporter);
    Assert.assertEquals(contextNames.size(), 2);
    Assert.assertTrue(contextNames.contains(context1Name));
    Assert.assertTrue(contextNames.contains(context2Name));

    // Check that nothing has been reported
    Assert.assertEquals(reporter.getReportedContexts().size(), 0);

    // Start reporter
    reporter.start();

    // Wait for up to 10 reporting intervals for 3 reports to run
    long maxWaitMillis = 10 * reportingIntervalMillis;
    long totalWait = 0;
    while(reporter.getReportedContexts().size() < 6 && maxWaitMillis > 0) {
      long wait = 100;
      Thread.sleep(wait);
      maxWaitMillis -= wait;
      totalWait += wait;
    }

    // stop reporter
    reporter.stop();

    // Check wait makes sense given reporting interval (e.g. if wait = 100 millis, then 2 reports in 100 millis,
    // something is wrong with schedule).
    Assert.assertTrue(totalWait > reportingIntervalMillis);
    Assert.assertTrue(reporter.getReportedContexts().size() >= 6);
    // Check that it didn't report excessively
    Assert.assertTrue(reporter.getReportedContexts().size() <= 10);

    // Check that first report indeed reported the correct contexts
    Set<String> firstReport = Sets.newHashSet(reporter.getReportedContexts().get(0).getName(),
        reporter.getReportedContexts().get(1).getName());
    Assert.assertEquals(firstReport, Sets.newHashSet(context1Name, context2Name));

    // Check that second report indeed reported the correct contexts
    Set<String> secondReport = Sets.newHashSet(reporter.getReportedContexts().get(2).getName(),
        reporter.getReportedContexts().get(3).getName());
    Assert.assertEquals(secondReport, Sets.newHashSet(context1Name, context2Name));

    int totalReports = reporter.getReportedContexts().size();

    // Wait for reporting interval to make sure reporting has actually stopped
    Thread.sleep(2 * reportingIntervalMillis);
    Assert.assertEquals(reporter.getReportedContexts().size(), totalReports);
    reporter.getReportedContexts().clear();

    // Dereference context 2 to ensure that it gets reported
    context2 = null;

    // Wait for context to be GCed
    maxWaitMillis = 2000;
    System.gc();
    while(reporter.getReportedContexts().size() < 1 && maxWaitMillis > 0) {
      System.gc();
      long wait = 100;
      Thread.sleep(wait);
      maxWaitMillis -= wait;
    }

    // Check that GCed context was reported
    Assert.assertEquals(reporter.getReportedContexts().size(), 1);
    Assert.assertEquals(reporter.getReportedContexts().get(0).getName(), context2Name);

    // Test close method
    reporter.close();
  }

  private Set<String> getContextNames(ContextStoreReporter reporter) {
    return Sets.newHashSet(Iterables.transform(reporter.getContextsToReport(),
        new Function<ReportableContext, String>() {
          @Nullable @Override public String apply(ReportableContext input) {
            return input.getName();
          }
        }));
  }
}
