/*
 * Copyright (c) 2015.  Airbnb.com
 *
 *  Licensed 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 com.airbnb.metrics;


import java.util.EnumSet;
import java.util.Random;
import java.util.concurrent.Callable;
import java.util.concurrent.TimeUnit;

import com.timgroup.statsd.StatsDClient;
import com.yammer.metrics.core.Clock;
import com.yammer.metrics.core.Counter;
import com.yammer.metrics.core.Gauge;
import com.yammer.metrics.core.Histogram;
import com.yammer.metrics.core.Meter;
import com.yammer.metrics.core.Metered;
import com.yammer.metrics.core.Metric;
import com.yammer.metrics.core.MetricName;
import com.yammer.metrics.core.MetricProcessor;
import com.yammer.metrics.core.MetricsRegistry;
import com.yammer.metrics.core.Sampling;
import com.yammer.metrics.core.Summarizable;
import com.yammer.metrics.core.Timer;
import com.yammer.metrics.reporting.AbstractPollingReporter;
import com.yammer.metrics.stats.Snapshot;
import org.junit.Before;
import org.junit.Test;
import org.mockito.Matchers;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.mockito.invocation.InvocationOnMock;
import org.mockito.stubbing.Answer;
import org.mockito.stubbing.Stubber;

import static org.junit.Assert.assertTrue;
import static org.mockito.Matchers.any;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

public class StatsDReporterTest {

  private static final String METRIC_BASE_NAME = "java.lang.Object.metric";
  @Mock
  private Clock clock;
  @Mock
  private StatsDClient statsD;
  private AbstractPollingReporter reporter;
  private TestMetricsRegistry registry;

  protected static class TestMetricsRegistry extends MetricsRegistry {
    public <T extends Metric> T add(MetricName name, T metric) {
      return getOrAdd(name, metric);
    }
  }

  @Before
  public void init() throws Exception {
    MockitoAnnotations.initMocks(this);
    when(clock.tick()).thenReturn(1234L);
    when(clock.time()).thenReturn(5678L);
    registry = new TestMetricsRegistry();
    reporter = new StatsDReporter(registry,
        statsD,
        EnumSet.allOf(Dimension.class)
    );
  }

  @Test
  public void isTaggedTest() {
    registry.add(new MetricName("kafka.common", "AppInfo", "Version", null, "kafka.common:type=AppInfo,name=Version"),
        new Gauge<String>() {
          public String value() {
            return "0.8.2";
          }
        });
    assertTrue(((StatsDReporter) reporter).isTagged(registry.allMetrics()));
  }

  protected <T extends Metric> void addMetricAndRunReporter(Callable<T> action) throws Exception {
    // Invoke the callable to trigger (ie, mark()/inc()/etc) and return the metric
    final T metric = action.call();
    try {
      // Add the metric to the registry, run the reporter and flush the result
      registry.add(new MetricName(Object.class, "metric"), metric);
      reporter.run();
    } finally {
      reporter.shutdown();
    }
  }

  private void verifySend(String metricNameSuffix, double metricValue) {
    verify(statsD).gauge(METRIC_BASE_NAME + "." + metricNameSuffix,
        metricValue);
  }

  private void verifySend(double metricValue) {
    verify(statsD).gauge(METRIC_BASE_NAME, metricValue);
  }

  private void verifySend(long metricValue) {
    verify(statsD).gauge(METRIC_BASE_NAME, metricValue);
  }

  private void verifySend(String metricNameSuffix, String metricValue) {
    verify(statsD).gauge(METRIC_BASE_NAME + "." + metricNameSuffix,
        Double.valueOf(metricValue));
  }

  public void verifyTimer() {
    verifySend("count", "1");
    verifySend("meanRate", "2.00");
    verifySend("1MinuteRate", "1.00");
    verifySend("5MinuteRate", "5.00");
    verifySend("15MinuteRate", "15.00");
    verifySend("min", "1.00");
    verifySend("max", "3.00");
    verifySend("mean", "2.00");
    verifySend("stddev", "1.50");
    verifySend("median", "0.50");
    verifySend("p75", "0.7505");
    verifySend("p95", "0.9509");
    verifySend("p98", "0.98096");
    verifySend("p99", "0.99098");
    verifySend("p999", "0.999998");
  }

  public void verifyMeter() {
    verifySend("count", 1);
    verifySend("meanRate", 2.00);
    verifySend("1MinuteRate", 1.00);
    verifySend("5MinuteRate", 5.00);
    verifySend("15MinuteRate", 15.00);
  }

  public void verifyHistogram() {
    verifySend("min", 1.00);
    verifySend("max", 3.00);
    verifySend("mean", 2.00);
    verifySend("stddev", 1.50);
    verifySend("median", 0.50);
    verifySend("p75", "0.7505");
    verifySend("p95", "0.9509");
    verifySend("p98", "0.98096");
    verifySend("p99", "0.99098");
    verifySend("p999", "0.999998");
  }

  public void verifyCounter(long count) {
    verifySend(count);
  }

  @Test
  public final void counter() throws Exception {
    final long count = new Random().nextInt(Integer.MAX_VALUE);
    addMetricAndRunReporter(
        new Callable<Counter>() {
          @Override
          public Counter call() throws Exception {
            return createCounter(count);
          }
        });
    verifyCounter(count);
  }

  @Test
  public final void histogram() throws Exception {
    addMetricAndRunReporter(
        new Callable<Histogram>() {
          @Override
          public Histogram call() throws Exception {
            return createHistogram();
          }
        });
    verifyHistogram();
  }

  @Test
  public final void meter() throws Exception {
    addMetricAndRunReporter(
        new Callable<Meter>() {
          @Override
          public Meter call() throws Exception {
            return createMeter();
          }
        });
    verifyMeter();
  }

  @Test
  public final void timer() throws Exception {
    addMetricAndRunReporter(
        new Callable<Timer>() {
          @Override
          public Timer call() throws Exception {
            return createTimer();
          }
        });
    verifyTimer();
  }

  @Test
  public final void longGauge() throws Exception {
    final long value = 0xdeadbeef;
    addMetricAndRunReporter(
        new Callable<Gauge<Object>>() {
          @Override
          public Gauge<Object> call() throws Exception {
            return createGauge(value);
          }
        });
    verifySend(value);
  }

  @Test
  public void stringGauge() throws Exception {
    final String value = "The Metric";
    addMetricAndRunReporter(
        new Callable<Gauge<Object>>() {
          @Override
          public Gauge<Object> call() throws Exception {
            return createGauge(value);
          }
        });
    verify(statsD, never()).gauge(Matchers.anyString(), Matchers.anyDouble());
  }

  static Counter createCounter(long count) throws Exception {
    final Counter mock = mock(Counter.class);
    when(mock.count()).thenReturn(count);
    return configureMatcher(mock, doAnswer(new MetricsProcessorAction() {
      @Override
      void delegateToProcessor(MetricProcessor<Object> processor, MetricName name, Object context) throws Exception {
        processor.processCounter(name, mock, context);
      }
    }));
  }

  static Histogram createHistogram() throws Exception {
    final Histogram mock = mock(Histogram.class);
    setupSummarizableMock(mock);
    setupSamplingMock(mock);
    return configureMatcher(mock, doAnswer(new MetricsProcessorAction() {
      @Override
      void delegateToProcessor(MetricProcessor<Object> processor, MetricName name, Object context) throws Exception {
        processor.processHistogram(name, mock, context);
      }
    }));
  }


  static Gauge<Object> createGauge(Object value) throws Exception {
    @SuppressWarnings("unchecked")
    final Gauge<Object> mock = mock(Gauge.class);
    when(mock.value()).thenReturn(value);
    return configureMatcher(mock, doAnswer(new MetricsProcessorAction() {
      @Override
      void delegateToProcessor(MetricProcessor<Object> processor, MetricName name, Object context) throws Exception {
        processor.processGauge(name, mock, context);
      }
    }));
  }


  static Timer createTimer() throws Exception {
    final Timer mock = mock(Timer.class);
    when(mock.durationUnit()).thenReturn(TimeUnit.MILLISECONDS);
    setupSummarizableMock(mock);
    setupMeteredMock(mock);
    setupSamplingMock(mock);
    return configureMatcher(mock, doAnswer(new MetricsProcessorAction() {
      @Override
      void delegateToProcessor(MetricProcessor<Object> processor, MetricName name, Object context) throws Exception {
        processor.processTimer(name, mock, context);
      }
    }));
  }

  static Meter createMeter() throws Exception {
    final Meter mock = mock(Meter.class);
    setupMeteredMock(mock);
    return configureMatcher(mock, doAnswer(new MetricsProcessorAction() {
      @Override
      void delegateToProcessor(MetricProcessor<Object> processor, MetricName name, Object context) throws Exception {
        processor.processMeter(name, mock, context);
      }
    }));
  }

  @SuppressWarnings("unchecked")
  static <T extends Metric> T configureMatcher(T mock, Stubber stub) throws Exception {
    stub.when(mock).processWith(any(MetricProcessor.class), any(MetricName.class), any());
    return mock;
  }

  static abstract class MetricsProcessorAction implements Answer<Object> {
    @Override
    public Object answer(InvocationOnMock invocation) throws Throwable {
      @SuppressWarnings("unchecked")
      final MetricProcessor<Object> processor = (MetricProcessor<Object>) invocation.getArguments()[0];
      final MetricName name = (MetricName) invocation.getArguments()[1];
      final Object context = invocation.getArguments()[2];
      delegateToProcessor(processor, name, context);
      return null;
    }

    abstract void delegateToProcessor(MetricProcessor<Object> processor, MetricName name, Object context) throws Exception;
  }

  static void setupSummarizableMock(Summarizable summarizable) {
    when(summarizable.min()).thenReturn(1d);
    when(summarizable.max()).thenReturn(3d);
    when(summarizable.mean()).thenReturn(2d);
    when(summarizable.stdDev()).thenReturn(1.5d);
  }

  static void setupMeteredMock(Metered metered) {
    when(metered.count()).thenReturn(1L);
    when(metered.oneMinuteRate()).thenReturn(1d);
    when(metered.fiveMinuteRate()).thenReturn(5d);
    when(metered.fifteenMinuteRate()).thenReturn(15d);
    when(metered.meanRate()).thenReturn(2d);
    when(metered.eventType()).thenReturn("eventType");
    when(metered.rateUnit()).thenReturn(TimeUnit.SECONDS);
  }

  static void setupSamplingMock(Sampling sampling) {  //be careful how snapshot defines statistics
    final double[] values = new double[1001];
    for (int i = 0; i < values.length; i++) {
      values[i] = i / 1000.0;
    }
    when(sampling.getSnapshot()).thenReturn(new Snapshot(values));
  }
}