/*
 * Copyright 2014 the original author or authors.
 *
 * 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.github.sps.metrics;

import com.codahale.metrics.*;
import com.codahale.metrics.Timer;
import com.github.sps.metrics.opentsdb.OpenTsdb;
import com.github.sps.metrics.opentsdb.OpenTsdbMetric;

import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
import org.mockito.Mock;
import org.mockito.runners.MockitoJUnitRunner;

import java.util.*;
import java.util.concurrent.TimeUnit;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import static org.mockito.Mockito.*;

/**
 * @author Sean Scanlon <[email protected]>
 */
@RunWith(MockitoJUnitRunner.class)
public class OpenTsdbReporterTest {

    private OpenTsdbReporter reporter;

    @Mock
    private OpenTsdb opentsdb;

    @Mock
    private MetricRegistry registry;

    @Mock
    private Gauge gauge;

    @Mock
    private Counter counter;

    @Mock
    private Clock clock;
    
    @Mock
    private Timer timer;
    
    @Mock
    Timer.Context context;

    private final long timestamp = 1000198;

    private ArgumentCaptor<Set> captor;

    @Before
    public void setUp() throws Exception {
        captor = ArgumentCaptor.forClass(Set.class);
        when(timer.time()).thenReturn(context);
        when(registry.timer(anyString())).thenReturn(timer);
        reporter = OpenTsdbReporter.forRegistry(registry)
                .withClock(clock)
                .prefixedWith("prefix")
                .convertRatesTo(TimeUnit.SECONDS)
                .convertDurationsTo(TimeUnit.MILLISECONDS)
                .filter(MetricFilter.ALL)
                .withTags(Collections.singletonMap("foo", "bar"))
                .withBatchSize(100)
                .build(opentsdb);

        when(clock.getTime()).thenReturn(timestamp * 1000);
        
    }

    @Test
    public void testReportGauges() {
        when(gauge.getValue()).thenReturn(1L);
        reporter.report(this.map("gauge", gauge), this.<Counter>map(), this.<Histogram>map(), this.<Meter>map(), this.<Timer>map());
        verify(opentsdb).send(captor.capture());

        final Set<OpenTsdbMetric> metrics = captor.getValue();
        assertEquals(1, metrics.size());
        OpenTsdbMetric metric = metrics.iterator().next();
        assertEquals("prefix.gauge.value", metric.getMetric());
        assertEquals(1L, metric.getValue());
        assertEquals((Long) timestamp, metric.getTimestamp());
    }

    @Test
    public void testReportCounters() {

        when(counter.getCount()).thenReturn(2L);
        reporter.report(this.<Gauge>map(), this.map("counter", counter), this.<Histogram>map(), this.<Meter>map(), this.<Timer>map());
        verify(opentsdb).send(captor.capture());

        final Set<OpenTsdbMetric> metrics = captor.getValue();
        assertEquals(1, metrics.size());
        OpenTsdbMetric metric = metrics.iterator().next();
        assertEquals("prefix.counter.count", metric.getMetric());
        assertEquals((Long) timestamp, metric.getTimestamp());
        assertEquals(2L, metric.getValue());
    }

    @Test
    public void testReportHistogram() {

        final Histogram histogram = mock(Histogram.class);
        when(histogram.getCount()).thenReturn(1L);

        final Snapshot snapshot = mock(Snapshot.class);
        when(snapshot.getMax()).thenReturn(2L);
        when(snapshot.getMean()).thenReturn(3.0);
        when(snapshot.getMin()).thenReturn(4L);
        when(snapshot.getStdDev()).thenReturn(5.0);
        when(snapshot.getMedian()).thenReturn(6.0);
        when(snapshot.get75thPercentile()).thenReturn(7.0);
        when(snapshot.get95thPercentile()).thenReturn(8.0);
        when(snapshot.get98thPercentile()).thenReturn(9.0);
        when(snapshot.get99thPercentile()).thenReturn(10.0);
        when(snapshot.get999thPercentile()).thenReturn(11.0);

        when(histogram.getSnapshot()).thenReturn(snapshot);

        reporter.report(this.<Gauge>map(), this.<Counter>map(), this.map("histogram", histogram), this.<Meter>map(), this.<Timer>map());

        verify(opentsdb).send(captor.capture());

        final Set<OpenTsdbMetric> metrics = captor.getValue();
        assertEquals(11, metrics.size());

        final OpenTsdbMetric metric = metrics.iterator().next();
        assertEquals((Long) timestamp, metric.getTimestamp());

        final Map<String, Object> histMap = new HashMap<String, Object>();
        for (OpenTsdbMetric m : metrics) {
            histMap.put(m.getMetric(), m.getValue());
        }

        assertEquals(histMap.get("prefix.histogram.count"), 1L);
        assertEquals(histMap.get("prefix.histogram.max"), 2L);
        assertEquals(histMap.get("prefix.histogram.mean"), 3.0);
        assertEquals(histMap.get("prefix.histogram.min"), 4L);

        assertEquals((Double) histMap.get("prefix.histogram.stddev"), 5.0, 0.0001);
        assertEquals((Double) histMap.get("prefix.histogram.median"), 6.0, 0.0001);
        assertEquals((Double) histMap.get("prefix.histogram.p75"), 7.0, 0.0001);
        assertEquals((Double) histMap.get("prefix.histogram.p95"), 8.0, 0.0001);
        assertEquals((Double) histMap.get("prefix.histogram.p98"), 9.0, 0.0001);
        assertEquals((Double) histMap.get("prefix.histogram.p99"), 10.0, 0.0001);
        assertEquals((Double) histMap.get("prefix.histogram.p999"), 11.0, 0.0001);

    }

    @Test
    public void testReportTimers() {

        final Timer timer = mock(Timer.class);
        when(timer.getCount()).thenReturn(1L);
        when(timer.getMeanRate()).thenReturn(1.0);
        when(timer.getOneMinuteRate()).thenReturn(2.0);
        when(timer.getFiveMinuteRate()).thenReturn(3.0);
        when(timer.getFifteenMinuteRate()).thenReturn(4.0);

        final Snapshot snapshot = mock(Snapshot.class);
        when(snapshot.getMax()).thenReturn(2L);
        when(snapshot.getMin()).thenReturn(4L);
        when(snapshot.getMean()).thenReturn(3.0);
        when(snapshot.getStdDev()).thenReturn(5.0);
        when(snapshot.getMedian()).thenReturn(6.0);
        when(snapshot.get75thPercentile()).thenReturn(7.0);
        when(snapshot.get95thPercentile()).thenReturn(8.0);
        when(snapshot.get98thPercentile()).thenReturn(9.0);
        when(snapshot.get99thPercentile()).thenReturn(10.0);
        when(snapshot.get999thPercentile()).thenReturn(11.0);

        when(timer.getSnapshot()).thenReturn(snapshot);

        reporter.report(this.<Gauge>map(), this.<Counter>map(), this.<Histogram>map(), this.<Meter>map(), this.map("timer", timer));

        verify(opentsdb).send(captor.capture());

        final Set<OpenTsdbMetric> metrics = captor.getValue();
        assertEquals(15, metrics.size());

        final OpenTsdbMetric metric = metrics.iterator().next();
        assertEquals((Long) timestamp, metric.getTimestamp());

        final Map<String, Object> timerMap = new HashMap<String, Object>();
        for (OpenTsdbMetric m : metrics) {
            timerMap.put(m.getMetric(), m.getValue());
        }

        assertEquals(timerMap.get("prefix.timer.count"), 1L);

        // duration should be in milliseconds, so we convert them to 1E-6 before output
        assertEquals((Double) timerMap.get("prefix.timer.max"), 2E-6, 0.0001);
        assertEquals((Double) timerMap.get("prefix.timer.mean"), 3.0E-6, 0.0001);
        assertEquals((Double) timerMap.get("prefix.timer.min"), 4E-6, 0.0001);
        assertEquals((Double) timerMap.get("prefix.timer.stddev"), 5.0E-6, 0.0001);
        assertEquals((Double) timerMap.get("prefix.timer.p75"), 7.0E-6, 0.0001);
        assertEquals((Double) timerMap.get("prefix.timer.p95"), 8.0E-6, 0.0001);
        assertEquals((Double) timerMap.get("prefix.timer.p98"), 9.0E-6, 0.0001);
        assertEquals((Double) timerMap.get("prefix.timer.p99"), 10.0E-6, 0.0001);
        assertEquals((Double) timerMap.get("prefix.timer.p999"), 11.0E-6, 0.0001);
        assertEquals((Double) timerMap.get("prefix.timer.median"), 6.0E-6, 0.0001);

        //convert rate to seconds,
        assertEquals((Double) timerMap.get("prefix.timer.mean_rate"), 1.0, 0.0001);
        assertEquals((Double) timerMap.get("prefix.timer.m1"), 2.0, 0.0001);
        assertEquals((Double) timerMap.get("prefix.timer.m5"), 3.0, 0.0001);
        assertEquals((Double) timerMap.get("prefix.timer.m15"), 4.0, 0.0001);
    }


    @Test
    public void testReportMeter() {

        final Meter meter = mock(Meter.class);
        when(meter.getCount()).thenReturn(1L);
        when(meter.getMeanRate()).thenReturn(1.0);
        when(meter.getOneMinuteRate()).thenReturn(2.0);
        when(meter.getFiveMinuteRate()).thenReturn(3.0);
        when(meter.getFifteenMinuteRate()).thenReturn(4.0);

        reporter.report(this.<Gauge>map(), this.<Counter>map(), this.<Histogram>map(), this.map("meter", meter), this.<Timer>map());

        verify(opentsdb).send(captor.capture());

        final Set<OpenTsdbMetric> metrics = captor.getValue();
        assertEquals(5, metrics.size());

        final OpenTsdbMetric metric = metrics.iterator().next();
        assertEquals((Long) timestamp, metric.getTimestamp());

        final Map<String, Object> meterMap = new HashMap<String, Object>();
        for (OpenTsdbMetric m : metrics) {
            meterMap.put(m.getMetric(), m.getValue());
        }

        assertEquals(meterMap.get("prefix.meter.count"), 1L);

        //convert rate to seconds,
        assertEquals((Double) meterMap.get("prefix.meter.mean_rate"), 1.0, 0.0001);
        assertEquals((Double) meterMap.get("prefix.meter.m1"), 2.0, 0.0001);
        assertEquals((Double) meterMap.get("prefix.meter.m5"), 3.0, 0.0001);
        assertEquals((Double) meterMap.get("prefix.meter.m15"), 4.0, 0.0001);
    }
    
    @Test
    public void testTaggedMetrics() {
    	final Map<String, String> tags = new HashMap<String, String>();
    	tags.put("a", "b");
    	
    	TaggedGauge<Integer> gauge = new TaggedGauge<Integer>() {
			@Override
			public Integer getValue() {
				return 1;
			}
			@Override
			public Map<String, String> getTags() {
				return tags;
			}
		};
    	
    	final TaggedCounter counter = new TaggedCounter(tags);
    	final TaggedHistogram histogram = mock(TaggedHistogram.class);
        when(histogram.getCount()).thenReturn(1L);
        when(histogram.getTags()).thenReturn(tags);
        final Snapshot snapshot = mock(Snapshot.class);
        when(snapshot.getMax()).thenReturn(2L);
        when(snapshot.getMean()).thenReturn(3.0);
        when(snapshot.getMin()).thenReturn(4L);
        when(snapshot.getStdDev()).thenReturn(5.0);
        when(snapshot.getMedian()).thenReturn(6.0);
        when(snapshot.get75thPercentile()).thenReturn(7.0);
        when(snapshot.get95thPercentile()).thenReturn(8.0);
        when(snapshot.get98thPercentile()).thenReturn(9.0);
        when(snapshot.get99thPercentile()).thenReturn(10.0);
        when(snapshot.get999thPercentile()).thenReturn(11.0);

        when(histogram.getSnapshot()).thenReturn(snapshot);
    	final TaggedMeter meter = new TaggedMeter(tags);
    	final TaggedTimer timer = new TaggedTimer(tags);
    	
    	SortedMap<String, Gauge> gauges = new TreeMap<String,Gauge>();
    	gauges.put("gauge", gauge);
    	reporter.report(gauges, this.<Counter>map("counter", counter), this.<Histogram>map("histogram", histogram), this.<Meter>map("meter", meter),this.<Timer>map("timer", timer));
    	verify(opentsdb).send(captor.capture());
    	final Set<OpenTsdbMetric> metrics = captor.getValue();
    	
    	final Map<String, String> expectedTags = new HashMap<String, String>();
    	expectedTags.putAll(tags);
    	expectedTags.put("foo", "bar");
    	
        for(OpenTsdbMetric metric : metrics) {
        	assertEquals(expectedTags, metric.getTags());
        }
    }
    
    @Test
    public void testTaggedMetricsNull() {
    	final Map<String, String> tags = null;
    	
    	TaggedGauge<Integer> gauge = new TaggedGauge<Integer>() {
			@Override
			public Integer getValue() {
				return 1;
			}
			@Override
			public Map<String, String> getTags() {
				return tags;
			}
		};
    	
    	final TaggedCounter counter = new TaggedCounter(tags);
    	final TaggedHistogram histogram = mock(TaggedHistogram.class);
        when(histogram.getCount()).thenReturn(1L);
        when(histogram.getTags()).thenReturn(tags);
        final Snapshot snapshot = mock(Snapshot.class);
        when(snapshot.getMax()).thenReturn(2L);
        when(snapshot.getMean()).thenReturn(3.0);
        when(snapshot.getMin()).thenReturn(4L);
        when(snapshot.getStdDev()).thenReturn(5.0);
        when(snapshot.getMedian()).thenReturn(6.0);
        when(snapshot.get75thPercentile()).thenReturn(7.0);
        when(snapshot.get95thPercentile()).thenReturn(8.0);
        when(snapshot.get98thPercentile()).thenReturn(9.0);
        when(snapshot.get99thPercentile()).thenReturn(10.0);
        when(snapshot.get999thPercentile()).thenReturn(11.0);

        when(histogram.getSnapshot()).thenReturn(snapshot);
    	final TaggedMeter meter = new TaggedMeter(tags);
    	final TaggedTimer timer = new TaggedTimer(tags);
    	
    	SortedMap<String, Gauge> gauges = new TreeMap<String,Gauge>();
    	gauges.put("gauge", gauge);
    	reporter.report(gauges, this.<Counter>map("counter", counter), this.<Histogram>map("histogram", histogram), this.<Meter>map("meter", meter),this.<Timer>map("timer", timer));
    	verify(opentsdb).send(captor.capture());
    	final Set<OpenTsdbMetric> metrics = captor.getValue();
    	
    	final Map<String, String> expectedTags = new HashMap<String, String>();
    	expectedTags.put("foo", "bar");
    	
        for(OpenTsdbMetric metric : metrics) {
        	assertEquals(expectedTags, metric.getTags());
        }
    }

    /**
     * This tests that empty metrics sets will not be sent to the OpenTSDB server which
     * (at the time of authoring this test) fails to parse the JSON even though it is perfectly valid.
     * it is a particular problem with dropwizard's jvm.threads.deadlocks metric which contains an empty Set<String>
     * under normal operating conditions (i.e.: no deadlocks)
     */
    @Test
    public void testEmptyGaugeSet() {
        final Gauge gauge = mock(Gauge.class);
        when(gauge.getValue()).thenReturn(new HashSet<String>());
        reporter.report(this.map("gauge", gauge), this.<Counter>map(), this.<Histogram>map(), this.<Meter>map(), this.<Timer>map());

        verify(opentsdb).send(captor.capture());

        final Set<OpenTsdbMetric> metrics = captor.getValue();
        assertEquals(0, metrics.size());
    }

    @Test
    public void testPerMetricTags() {

        when(counter.getCount()).thenReturn(2L);
        String encodedName = OpenTsdbMetric.encodeTagsInName("counter", Collections.singletonMap("foo2", "bar2"));
        reporter.report(this.<Gauge>map(), this.map(encodedName, counter), this.<Histogram>map(), this.<Meter>map(), this.<Timer>map());
        verify(opentsdb).send(captor.capture());

        final Set<OpenTsdbMetric> metrics = captor.getValue();
        assertEquals(1, metrics.size());
        OpenTsdbMetric metric = metrics.iterator().next();
        assertEquals("prefix.counter.count", metric.getMetric());
        assertEquals((Long) timestamp, metric.getTimestamp());
        assertEquals(2L, metric.getValue());

        Map<String,String> tags = metric.getTags();
        assertEquals(2, tags.size());
        assertTrue(tags.containsKey("foo")); // applied to all metrics
        assertEquals("bar", tags.get("foo"));
        assertTrue(tags.containsKey("foo2")); // applied to just this counter
        assertEquals("bar2", tags.get("foo2"));
    }


    @Test
    public void testWithNoPrefix() {
        reporter = OpenTsdbReporter.forRegistry(registry)
                .withClock(clock)
                .convertRatesTo(TimeUnit.SECONDS)
                .convertDurationsTo(TimeUnit.MILLISECONDS)
                .filter(MetricFilter.ALL)
                .withTags(Collections.singletonMap("foo", "bar"))
                .withBatchSize(100)
                .build(opentsdb);

        when(counter.getCount()).thenReturn(2L);
        String encodedName = OpenTsdbMetric.encodeTagsInName("counter", Collections.singletonMap("foo2", "bar2"));
        reporter.report(this.<Gauge>map(), this.map(encodedName, counter), this.<Histogram>map(), this.<Meter>map(), this.<Timer>map());
        verify(opentsdb).send(captor.capture());

        final Set<OpenTsdbMetric> metrics = captor.getValue();
        assertEquals(1, metrics.size());
        OpenTsdbMetric metric = metrics.iterator().next();
        assertEquals("counter.count", metric.getMetric());
        assertEquals((Long) timestamp, metric.getTimestamp());
        assertEquals(2L, metric.getValue());
    }


    @Test
    public void testDecorateDisabledCounter() {
        reporter = OpenTsdbReporter.forRegistry(registry)
                .withClock(clock)
                .prefixedWith("prefix")
                .convertRatesTo(TimeUnit.SECONDS)
                .convertDurationsTo(TimeUnit.MILLISECONDS)
                .filter(MetricFilter.ALL)
                .withTags(Collections.singletonMap("foo", "bar"))
                .withBatchSize(100)
                .withCounterGaugeDecorations(false)
                .build(opentsdb);

        when(counter.getCount()).thenReturn(2L);
        String encodedName = OpenTsdbMetric.encodeTagsInName("counter", Collections.singletonMap("foo2", "bar2"));
        reporter.report(this.<Gauge>map(), this.map(encodedName, counter), this.<Histogram>map(), this.<Meter>map(), this.<Timer>map());
        verify(opentsdb).send(captor.capture());

        final Set<OpenTsdbMetric> metrics = captor.getValue();
        assertEquals(1, metrics.size());
        OpenTsdbMetric metric = metrics.iterator().next();
        assertEquals("prefix.counter", metric.getMetric());
        assertEquals((Long) timestamp, metric.getTimestamp());
        assertEquals(2L, metric.getValue());
    }

    @Test
    public void testDecorateDisabledGauge() {
        reporter = OpenTsdbReporter.forRegistry(registry)
                .withClock(clock)
                .prefixedWith("prefix")
                .convertRatesTo(TimeUnit.SECONDS)
                .convertDurationsTo(TimeUnit.MILLISECONDS)
                .filter(MetricFilter.ALL)
                .withTags(Collections.singletonMap("foo", "bar"))
                .withBatchSize(100)
                .withCounterGaugeDecorations(false)
                .build(opentsdb);

        when(gauge.getValue()).thenReturn(1L);
        reporter.report(this.map("gauge", gauge), this.<Counter>map(), this.<Histogram>map(), this.<Meter>map(), this.<Timer>map());
        verify(opentsdb).send(captor.capture());
        final Set<OpenTsdbMetric> metrics = captor.getValue();
        assertEquals(1, metrics.size());
        OpenTsdbMetric metric = metrics.iterator().next();
        assertEquals("prefix.gauge", metric.getMetric());
        assertEquals(1L, metric.getValue());
        assertEquals((Long) timestamp, metric.getTimestamp());
    }


    private <T> SortedMap<String, T> map() {
        return new TreeMap<String, T>();
    }

    private <T> SortedMap<String, T> map(String name, T metric) {
        final TreeMap<String, T> map = new TreeMap<String, T>();
        map.put(name, metric);
        return map;
    }
}