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

import com.google.common.cache.Cache;
import com.google.common.cache.CacheBuilder;
import java.lang.ref.WeakReference;
import java.util.UUID;

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

import com.google.common.base.Predicate;

import com.typesafe.config.ConfigFactory;

import lombok.AllArgsConstructor;

import org.apache.gobblin.metrics.callback.NotificationStore;
import org.apache.gobblin.metrics.notification.MetricContextCleanupNotification;
import org.apache.gobblin.metrics.notification.NewMetricContextNotification;
import org.apache.gobblin.metrics.notification.Notification;
import org.apache.gobblin.metrics.test.ContextStoreReporter;


/**
 * Tests for {@link org.apache.gobblin.metrics.RootMetricContext}
 */
public class RootMetricContextTest {

  private static final Cache<String, int[]> CACHE = CacheBuilder.newBuilder().softValues().build();

  @BeforeMethod
  public void setUp() throws Exception {
    System.gc();
    RootMetricContext.get().clearNotificationTargets();
  }

  @AfterMethod
  public void tearDown() throws Exception {
    CACHE.invalidateAll();
    System.gc();
    RootMetricContext.get().clearNotificationTargets();
  }

  @Test
  public void testGet() throws Exception {
    Assert.assertNotNull(RootMetricContext.get());
    Assert.assertEquals(RootMetricContext.get(), RootMetricContext.get());
    Assert.assertEquals(RootMetricContext.get().getName(), RootMetricContext.ROOT_METRIC_CONTEXT);
  }

  @Test
  public void testReporterCanBeAddedToStartedContext() throws Exception {
    RootMetricContext.get().startReporting();

    ContextStoreReporter reporter = new ContextStoreReporter("testReporter", ConfigFactory.empty());
    Assert.assertTrue(reporter.isStarted());

    RootMetricContext.get().stopReporting();
  }

  @Test
  public void testMetricContextLifecycle() throws Exception {

    String name = UUID.randomUUID().toString();
    NotificationStore store = new NotificationStore(new ContextNamePredicate(name));
    RootMetricContext.get().addNotificationTarget(store);

    // Create a new metric context
    MetricContext metricContext = MetricContext.builder(name).build();
    WeakReference<MetricContext> contextWeakReference = new WeakReference<MetricContext>(metricContext);
    InnerMetricContext innerMetricContext = metricContext.getInnerMetricContext();
    WeakReference<InnerMetricContext> innerMetricContextWeakReference =
        new WeakReference<InnerMetricContext>(innerMetricContext);
    innerMetricContext = null;
    // Check that existence of a reporter does not prevent GC
    ContextStoreReporter reporter = new ContextStoreReporter("testReporter", ConfigFactory.empty());

    // Check that metric context is a child of root metric context
    Assert.assertTrue(RootMetricContext.get().getChildContextsAsMap().containsKey(name));
    Assert.assertEquals(RootMetricContext.get().getChildContextsAsMap().get(name), metricContext);

    // Check that notification on new metric context was generated
    Assert.assertEquals(store.getNotificationList().size(), 1);
    Assert.assertEquals(store.getNotificationList().get(0).getClass(), NewMetricContextNotification.class);
    Assert.assertEquals(((NewMetricContextNotification) store.getNotificationList().get(0)).getMetricContext(),
        metricContext);
    store.getNotificationList().clear();

    // Create a counter
    ContextAwareCounter counter1 = metricContext.contextAwareCounter("textCounter1");

    // If losing reference of counter, should not be GCed while context is present
    WeakReference<ContextAwareCounter> counterWeakReference1 = new WeakReference<ContextAwareCounter>(counter1);
    counter1 = null;
    ensureNotGarbageCollected(counterWeakReference1);

    // Create some more metrics
    ContextAwareCounter counter2 = metricContext.contextAwareCounter("testCounter");
    WeakReference<ContextAwareCounter> counterWeakReference2 = new WeakReference<ContextAwareCounter>(counter2);
    ContextAwareMeter meter = metricContext.contextAwareMeter("testMeter");
    WeakReference<ContextAwareMeter> meterWeakReference = new WeakReference<ContextAwareMeter>(meter);
    meter.mark();
    ContextAwareHistogram histogram = metricContext.contextAwareHistogram("testHistogram");
    WeakReference<ContextAwareHistogram> histogramWeakReference = new WeakReference<ContextAwareHistogram>(histogram);
    ContextAwareTimer timer = metricContext.contextAwareTimer("testTimer");
    WeakReference<ContextAwareTimer> timerWeakReference = new WeakReference<ContextAwareTimer>(timer);


    // If losing reference to context, should not be GCed while reference to metric is present
    metricContext = null;
    ensureNotGarbageCollected(contextWeakReference);
    ensureNotGarbageCollected(counterWeakReference2);
    ensureNotGarbageCollected(meterWeakReference);
    ensureNotGarbageCollected(timerWeakReference);
    ensureNotGarbageCollected(histogramWeakReference);

    // After losing reference to context and all metrics, context and all metrics should be GCed
    store.getNotificationList().clear();
    reporter.getReportedContexts().clear();
    counter2 = null;
    meter = null;
    histogram = null;
    timer = null;

    ensureGarbageCollected(contextWeakReference);
    ensureGarbageCollected(counterWeakReference1);
    ensureGarbageCollected(counterWeakReference2);
    ensureGarbageCollected(meterWeakReference);
    ensureGarbageCollected(timerWeakReference);
    ensureGarbageCollected(histogramWeakReference);

    // Inner metric context should not be GCed
    ensureNotGarbageCollected(innerMetricContextWeakReference);

    // Notification on removal of metric context should be available
    int maxWait = 10;
    while(store.getNotificationList().isEmpty() && maxWait > 0) {
      Thread.sleep(1000);
      maxWait--;
    }
    Assert.assertEquals(store.getNotificationList().size(), 1);
    Assert.assertEquals(store.getNotificationList().get(0).getClass(), MetricContextCleanupNotification.class);
    Assert.assertEquals(((MetricContextCleanupNotification) store.getNotificationList().get(0)).getMetricContext(),
        innerMetricContextWeakReference.get());

    // Reporter should have attempted to report metric context
    Assert.assertEquals(reporter.getReportedContexts().size(), 1);
    Assert.assertEquals(reporter.getReportedContexts().get(0), innerMetricContextWeakReference.get());

    // Metrics in deleted metric context should still be readable
    Assert.assertEquals(innerMetricContextWeakReference.get().getCounters().size(), 2);
    Assert.assertEquals(innerMetricContextWeakReference.get().getMeters().size(), 1);
    Assert.assertEquals(innerMetricContextWeakReference.get().getTimers().size(), 2);
    Assert.assertEquals(innerMetricContextWeakReference.get().getHistograms().size(), 1);
    Assert.assertEquals(innerMetricContextWeakReference.get().getMeters().get("testMeter").getCount(), 1);

    // After clearing notification, inner metric context should be GCed
    store.getNotificationList().clear();
    reporter.getReportedContexts().clear();
    ensureGarbageCollected(innerMetricContextWeakReference);

    RootMetricContext.get().removeReporter(reporter);
  }

  private void triggerGarbageCollection(WeakReference<?> weakReference) {
    System.gc();

    // System.gc() might not clean up the object being checked, so allocate some memory and call System.gc() again
    for (int i = 0; weakReference.get() != null && i <= 10000; i++) {
      CACHE.put(Integer.toString(i), new int[10000]);

      if (i % 1000 == 0) {
        System.gc();
      }
    }
  }
  private void ensureGarbageCollected(WeakReference<?> weakReference) {
    triggerGarbageCollection(weakReference);

    Assert.assertNull(weakReference.get());
  }

  private void ensureNotGarbageCollected(WeakReference<?> weakReference) {
    triggerGarbageCollection(weakReference);

    Assert.assertNotNull(weakReference.get());
  }

  @AllArgsConstructor
  private class ContextNamePredicate implements Predicate<Notification> {

    private final String name;

    @Override
    public boolean apply(Notification input) {
      if (input instanceof NewMetricContextNotification &&
          ((NewMetricContextNotification) input).getInnerMetricContext().getName().equals(this.name)) {
        return true;
      }
      if (input instanceof MetricContextCleanupNotification &&
          ((MetricContextCleanupNotification) input).getMetricContext().getName().equals(this.name)) {
        return true;
      }
      return false;
    }
  }
}
