package com.signalfx.codahale.metrics;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;

import java.util.concurrent.TimeUnit;

import org.junit.Before;
import org.junit.Test;

import com.codahale.metrics.Counter;
import com.codahale.metrics.Gauge;
import com.codahale.metrics.Histogram;
import com.codahale.metrics.MetricRegistry;
import com.codahale.metrics.Timer;
import com.google.common.collect.ImmutableSet;
import com.signalfx.codahale.SfxMetrics;
import com.signalfx.codahale.reporter.IncrementalCounter;
import com.signalfx.codahale.reporter.SignalFxReporter;
import com.signalfx.metrics.auth.StaticAuthToken;
import com.signalfx.metrics.connection.StaticDataPointReceiverFactory;
import com.signalfx.metrics.connection.StoredDataPointReceiver;
import com.signalfx.metrics.protobuf.SignalFxProtocolBuffers;

public class SignalFxReporterTest {

    private static final String SOURCE_NAME = "myserver";

    private MetricRegistry metricRegistry;
    private SignalFxReporter reporter;
    private SfxMetrics sfxMetrics;
    private StoredDataPointReceiver dbank;

    @Before
    public void setup() {
        metricRegistry = new MetricRegistry();
        dbank = new StoredDataPointReceiver();
        reporter = new SignalFxReporter
                .Builder(metricRegistry, new StaticAuthToken(""), SOURCE_NAME)
                .setDataPointReceiverFactory(new StaticDataPointReceiverFactory(dbank))
                .setDetailsToAdd(ImmutableSet.of(SignalFxReporter.MetricDetails.COUNT,
                        SignalFxReporter.MetricDetails.MIN,
                        SignalFxReporter.MetricDetails.MAX))
                .build();
        sfxMetrics = new SfxMetrics(metricRegistry, reporter.getMetricMetadata());
    }

    @Test
    public void testEmptyDataBank() {
        StoredDataPointReceiver dbank = new StoredDataPointReceiver();
        assertEquals(0, dbank.addDataPoints.size());
    }

    @Test
    public void testGauge() {
        Gauge<Integer> gauge = sfxMetrics.registerGauge("gauge", new Gauge<Integer>() {
            @Override
            public Integer getValue() {
                return 1;
            }
        });
        reporter.report();
        assertEquals(1, dbank.addDataPoints.size());
        assertTrue(sfxMetrics.unregister(gauge));
    }

    @Test
    public void testCounter() {
        Counter counter = sfxMetrics.counter("counter");
        counter.inc();
        counter.inc();
        reporter.report();
        assertEquals(1, dbank.addDataPoints.size());
        // counter without dimensions doesn't get into MetricMetadata, so we cannot unregister
        // from SfxMetrics. But we can remove it from MetricRegistry.
        assertFalse(sfxMetrics.unregister(counter));
        assertTrue(metricRegistry.remove("counter"));
    }

    @Test
    public void testCounterWithDimensions() {
        Counter counter = sfxMetrics.counter("counter", "dimName", "dimValue");
        counter.inc();
        counter.inc();
        reporter.report();
        assertEquals(1, dbank.addDataPoints.size());
        assertTrue(sfxMetrics.unregister(counter));
    }

    @Test
    public void testRawCounter() {
        Counter rawCounter = reporter.getMetricMetadata()
                .forMetric(metricRegistry.counter("rawCounter"))
                .withMetricType(SignalFxProtocolBuffers.MetricType.COUNTER).metric();
        rawCounter.inc(10);
        reporter.report();
        rawCounter.inc(14);
        reporter.report();
        // Users have to use an IncrementalCounter if they want to see the count value of 14 each time
        assertEquals(24, dbank.lastValueFor(SOURCE_NAME, "rawCounter").getIntValue());
        assertEquals(SignalFxProtocolBuffers.MetricType.COUNTER,
                dbank.registeredMetrics.get("rawCounter"));
    }

    @Test
    public void testIncrementalCounter() {
        IncrementalCounter incrementalCounter = sfxMetrics
                .incrementalCounter("incrementalCounter");
        incrementalCounter.inc(3);
        assertEquals(false, sfxMetrics.unregister(new Counter()));
        reporter.report();
        assertEquals(1, dbank.addDataPoints.size());
        assertEquals(3, dbank.lastValueFor(SOURCE_NAME, "incrementalCounter").getIntValue());
    }

    @Test
    public void testTimer() throws Exception {
        Timer timer = sfxMetrics.timer("timer", "dimName", "dimValue");
        // track time taken.
        for (int i = 0; i < 4; i++) {
            Timer.Context context = timer.time();
            try {
                Thread.sleep(10 + i * 10);
            } finally {
                context.close();
            }
        }
        /*
        Java 7 alternative:
        try (Timer.Context ignored = t.time()) {
            System.out.println("Doing store things");
        }
        */
        reporter.report();
        assertEquals(3, dbank.addDataPoints.size());
        assertEquals(SignalFxProtocolBuffers.MetricType.CUMULATIVE_COUNTER,
                dbank.registeredMetrics.get("timer.count"));
        assertEquals(SignalFxProtocolBuffers.MetricType.GAUGE,
                dbank.registeredMetrics.get("timer.max"));
        assertEquals(SignalFxProtocolBuffers.MetricType.GAUGE,
                dbank.registeredMetrics.get("timer.min"));
        assertTrue(sfxMetrics.unregister(timer));
    }

    @Test
    public void testResettingTimer() {
        Timer resettingTimer = sfxMetrics.resettingTimer("resettingTimer", "dimName", "dimValue");
        resettingTimer.update(20, TimeUnit.MILLISECONDS);
        resettingTimer.update(30, TimeUnit.MILLISECONDS);
        reporter.report();
        assertEquals(3, dbank.addDataPoints.size());
        assertEquals(2, dbank.lastValueFor(SOURCE_NAME, "resettingTimer.count").getIntValue());
        assertEquals(20000000, dbank.lastValueFor(SOURCE_NAME, "resettingTimer.min").getIntValue());
        assertEquals(30000000, dbank.lastValueFor(SOURCE_NAME, "resettingTimer.max").getIntValue());
        dbank.addDataPoints.clear();
        resettingTimer.update(25, TimeUnit.MILLISECONDS);
        reporter.report();
        assertEquals(3, dbank.addDataPoints.size());
        assertEquals(3, dbank.lastValueFor(SOURCE_NAME, "resettingTimer.count").getIntValue());
        assertEquals(25000000, dbank.lastValueFor(SOURCE_NAME, "resettingTimer.min").getIntValue());
        assertEquals(25000000, dbank.lastValueFor(SOURCE_NAME, "resettingTimer.max").getIntValue());
    }

    @Test
    public void testHistogram() {
        Histogram histogram = sfxMetrics.histogram("histogram", "dimName", "dimValue");
        histogram.update(20);
        histogram.update(30);
        reporter.report();
        assertEquals(3, dbank.addDataPoints.size());
        assertEquals(2, dbank.lastValueFor(SOURCE_NAME, "histogram.count").getIntValue());
        assertEquals(20, dbank.lastValueFor(SOURCE_NAME, "histogram.min").getIntValue());
        assertEquals(30, dbank.lastValueFor(SOURCE_NAME, "histogram.max").getIntValue());
        dbank.addDataPoints.clear();
        histogram.update(25);
        reporter.report();
        assertEquals(3, dbank.addDataPoints.size());
        assertEquals(3, dbank.lastValueFor(SOURCE_NAME, "histogram.count").getIntValue());
        assertEquals(20, dbank.lastValueFor(SOURCE_NAME, "histogram.min").getIntValue());
        assertEquals(30, dbank.lastValueFor(SOURCE_NAME, "histogram.max").getIntValue());
    }

    @Test
    public void testResettingHistogram() {
        Histogram resettingHistogram = sfxMetrics.resettingHistogram("resettingHistogram");
        resettingHistogram.update(20);
        resettingHistogram.update(30);
        reporter.report();
        assertEquals(3, dbank.addDataPoints.size());
        assertEquals(2, dbank.lastValueFor(SOURCE_NAME, "resettingHistogram.count").getIntValue());
        assertEquals(20, dbank.lastValueFor(SOURCE_NAME, "resettingHistogram.min").getIntValue());
        assertEquals(30, dbank.lastValueFor(SOURCE_NAME, "resettingHistogram.max").getIntValue());
        dbank.addDataPoints.clear();
        resettingHistogram.update(25);
        reporter.report();
        assertEquals(3, dbank.addDataPoints.size());
        assertEquals(3, dbank.lastValueFor(SOURCE_NAME, "resettingHistogram.count").getIntValue());
        assertEquals(25, dbank.lastValueFor(SOURCE_NAME, "resettingHistogram.min").getIntValue());
        assertEquals(25, dbank.lastValueFor(SOURCE_NAME, "resettingHistogram.max").getIntValue());
    }

    @Test
    public void testReportingGaugeAsCummulativeCounter() {
        String cumulativeCounterCallback = "cumulativeCounterCallback";
        sfxMetrics.registerGaugeAsCumulativeCounter(cumulativeCounterCallback,
                new Gauge<Long>() {
                    private long i = 0;

                    @Override
                    public Long getValue() {
                        return i++;
                    }
                });
        reporter.report();
        assertEquals(0, dbank.lastValueFor(SOURCE_NAME, cumulativeCounterCallback).getIntValue());
        reporter.report();
        reporter.report();
        assertEquals(2, dbank.lastValueFor(SOURCE_NAME, cumulativeCounterCallback).getIntValue());
        assertEquals(SignalFxProtocolBuffers.MetricType.CUMULATIVE_COUNTER,
                dbank.registeredMetrics.get(cumulativeCounterCallback));
    }

    @Test(expected = IllegalArgumentException.class)
    public void testDuplicateMetricRegistration() {
        sfxMetrics.counter("countstuff");
        sfxMetrics.incrementalCounter("countstuff");
        fail("I expect an already registered metric on this name");
    }

    @Test(expected = IllegalArgumentException.class)
    public void testUpdatingMetricTypeToIncompatibleValue() {
        reporter.getMetricMetadata()
                .forBuilder(IncrementalCounter.Builder.INSTANCE)
                .withSourceName(SOURCE_NAME)
                .withMetricName("name")
                .createOrGet(metricRegistry)
                .inc(3);
        reporter.getMetricMetadata()
                .forBuilder(IncrementalCounter.Builder.INSTANCE)
                .withSourceName(SOURCE_NAME)
                .withMetricName("name")
                .withMetricType(SignalFxProtocolBuffers.MetricType.GAUGE)
                .createOrGet(metricRegistry);
        fail("I expect an error if it's a gauge");
    }

    @Test(expected = IllegalArgumentException.class)
    public void testCreateOrGetMetricSameNameDifferentType() {
        reporter.getMetricMetadata().forBuilder(IncrementalCounter.Builder.INSTANCE)
                .withSourceName("asource")
                .withMetricName("name")
                .createOrGet(metricRegistry)
                .inc(1);
        reporter.getMetricMetadata().forBuilder(SettableLongGauge.Builder.INSTANCE)
                .withSourceName("asource")
                .withMetricName("name")
                .createOrGet(metricRegistry);
        fail("We shouldn't be able to make it with the same name and different type");
    }
}