/*
 * Copyright 2019 Red Hat, Inc. and/or its affiliates
 * and other contributors as indicated by the @author tags.
 *
 *   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 io.smallrye.metrics.exporters;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;

import java.io.StringReader;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.TimeUnit;

import javax.json.Json;
import javax.json.JsonObject;

import org.eclipse.microprofile.metrics.ConcurrentGauge;
import org.eclipse.microprofile.metrics.Counter;
import org.eclipse.microprofile.metrics.Gauge;
import org.eclipse.microprofile.metrics.Histogram;
import org.eclipse.microprofile.metrics.Metadata;
import org.eclipse.microprofile.metrics.MetadataBuilder;
import org.eclipse.microprofile.metrics.Meter;
import org.eclipse.microprofile.metrics.MetricFilter;
import org.eclipse.microprofile.metrics.MetricID;
import org.eclipse.microprofile.metrics.MetricRegistry;
import org.eclipse.microprofile.metrics.MetricType;
import org.eclipse.microprofile.metrics.MetricUnits;
import org.eclipse.microprofile.metrics.Tag;
import org.eclipse.microprofile.metrics.Timer;
import org.junit.After;
import org.junit.Test;

import io.smallrye.metrics.MetricRegistries;
import io.smallrye.metrics.app.ExponentiallyDecayingReservoir;
import io.smallrye.metrics.app.HistogramImpl;
import io.smallrye.metrics.app.MeterImpl;
import io.smallrye.metrics.app.TimerImpl;

public class JsonExporterTest {

    private static final String LINE_SEPARATOR = "\n";

    @After
    public void cleanupApplicationMetrics() {
        MetricRegistries.get(MetricRegistry.Type.APPLICATION).removeMatching(MetricFilter.ALL);
        MetricRegistries.get(MetricRegistry.Type.BASE).removeMatching(MetricFilter.ALL);
        MetricRegistries.get(MetricRegistry.Type.VENDOR).removeMatching(MetricFilter.ALL);
    }

    @Test
    public void testExportOfDifferentMeterImplementations() {

        JsonExporter exporter = new JsonExporter();
        MetricRegistry applicationRegistry = MetricRegistries.get(MetricRegistry.Type.APPLICATION);

        // the export should behave identical for any class derived from Meter
        Meter[] meters = { new MeterImpl(), new SomeMeter() };
        int idx = 0;
        for (Meter m : meters) {
            String name = "meter_" + idx++;
            applicationRegistry.register(name, m);
            StringBuilder out = exporter.exportOneMetric(MetricRegistry.Type.APPLICATION, new MetricID(name));
            assertNotNull(out);
            List<String> lines = Arrays.asList(out.toString().split(LINE_SEPARATOR));
            assertEquals(1, lines.stream().filter(line -> line.contains("\"" + name + "\"")).count());
            assertEquals(1, lines.stream().filter(line -> line.contains("\"count\": 0")).count());
        }
    }

    @Test
    public void testExportOfDifferentHistogramImplementations() {

        JsonExporter exporter = new JsonExporter();
        MetricRegistry applicationRegistry = MetricRegistries.get(MetricRegistry.Type.APPLICATION);

        // the export should behave identical for any class derived from Histogram
        Histogram[] histograms = { new HistogramImpl(new ExponentiallyDecayingReservoir()), new SomeHistogram() };
        int idx = 0;
        for (Histogram h : histograms) {
            String name = "histo_" + idx++;
            applicationRegistry.register(name, h);
            StringBuilder out = exporter.exportOneMetric(MetricRegistry.Type.APPLICATION, new MetricID(name));
            assertNotNull(out);
            System.out.println(out.toString());
            List<String> lines = Arrays.asList(out.toString().split(LINE_SEPARATOR));
            assertEquals(1, lines.stream().filter(line -> line.contains("\"" + name + "\"")).count());
            assertEquals(1, lines.stream().filter(line -> line.contains("\"count\": 0")).count());
        }
    }

    @Test
    public void testExportOfDifferentTimerImplementations() {

        JsonExporter exporter = new JsonExporter();
        MetricRegistry applicationRegistry = MetricRegistries.get(MetricRegistry.Type.APPLICATION);

        // the export should behave identical for any class derived from Timer
        Timer[] timers = { new TimerImpl(), new SomeTimer() };
        int idx = 0;
        for (Timer t : timers) {
            String name = "json_timer_" + idx++;
            applicationRegistry.register(name, t);
            StringBuilder out = exporter.exportOneMetric(MetricRegistry.Type.APPLICATION, new MetricID(name));
            assertNotNull(out);
            List<String> lines = Arrays.asList(out.toString().split(LINE_SEPARATOR));
            assertEquals(1, lines.stream().filter(line -> line.contains("\"" + name + "\"")).count());
            assertEquals(1, lines.stream().filter(line -> line.contains("\"count\": 0")).count());
        }
    }

    @Test
    public void testGauges() {
        JsonExporter exporter = new JsonExporter();
        MetricRegistry registry = MetricRegistries.get(MetricRegistry.Type.APPLICATION);

        Gauge<Long> gaugeWithoutTags = () -> 1L;
        Gauge<Long> gaugeRed = () -> 2L;
        Gauge<Long> gaugeBlue = () -> 3L;

        final Metadata metadata = new MetadataBuilder()
                .withType(MetricType.GAUGE)
                .withName("mygauge")
                .build();

        registry.register(metadata, gaugeWithoutTags);
        registry.register(metadata, gaugeRed, new Tag("color", "red"));
        registry.register(metadata, gaugeBlue, new Tag("color", "blue"), new Tag("foo", "bar"));

        String result = exporter.exportMetricsByName(MetricRegistry.Type.APPLICATION, "mygauge").toString();
        System.out.println(result);
        JsonObject json = Json.createReader(new StringReader(result)).read().asJsonObject();

        assertEquals(1, json.getInt("mygauge"));
        assertEquals(2, json.getInt("mygauge;color=red"));
        assertEquals(3, json.getInt("mygauge;color=blue;foo=bar"));
    }

    @Test
    public void testCounters() {
        JsonExporter exporter = new JsonExporter();
        MetricRegistry registry = MetricRegistries.get(MetricRegistry.Type.APPLICATION);

        Counter counterWithoutTags = registry.counter("mycounter");
        Counter counterRed = registry.counter("mycounter", new Tag("color", "red"));
        Counter counterBlue = registry.counter("mycounter", new Tag("color", "blue"), new Tag("foo", "bar"));

        counterWithoutTags.inc(1);
        counterRed.inc(2);
        counterBlue.inc(3);

        String result = exporter.exportMetricsByName(MetricRegistry.Type.APPLICATION, "mycounter").toString();
        JsonObject json = Json.createReader(new StringReader(result)).read().asJsonObject();

        assertEquals(1, json.getInt("mycounter"));
        assertEquals(2, json.getInt("mycounter;color=red"));
        assertEquals(3, json.getInt("mycounter;color=blue;foo=bar"));
    }

    @Test
    public void testConcurrentGauges() {
        JsonExporter exporter = new JsonExporter();
        MetricRegistry registry = MetricRegistries.get(MetricRegistry.Type.APPLICATION);

        ConcurrentGauge cGaugeWithoutTags = registry.concurrentGauge("mycgauge");
        ConcurrentGauge cGaugeRed = registry.concurrentGauge("mycgauge", new Tag("color", "red"));
        ConcurrentGauge cGaugeBlue = registry.concurrentGauge("mycgauge", new Tag("color", "blue"), new Tag("foo", "bar"));

        cGaugeWithoutTags.inc();
        cGaugeRed.inc();
        cGaugeRed.inc();
        cGaugeBlue.inc();
        cGaugeBlue.inc();
        cGaugeBlue.inc();

        String result = exporter.exportMetricsByName(MetricRegistry.Type.APPLICATION, "mycgauge").toString();
        JsonObject json = Json.createReader(new StringReader(result)).read().asJsonObject();

        JsonObject myCgaugeObject = json.getJsonObject("mycgauge");

        assertEquals(1, myCgaugeObject.getInt("current"));
        assertEquals(2, myCgaugeObject.getInt("current;color=red"));
        assertEquals(3, myCgaugeObject.getInt("current;color=blue;foo=bar"));

        assertNotNull(myCgaugeObject.getJsonNumber("min"));
        assertNotNull(myCgaugeObject.getJsonNumber("min;color=red"));
        assertNotNull(myCgaugeObject.getJsonNumber("min;color=blue;foo=bar"));

        assertNotNull(myCgaugeObject.getJsonNumber("max"));
        assertNotNull(myCgaugeObject.getJsonNumber("max;color=red"));
        assertNotNull(myCgaugeObject.getJsonNumber("max;color=blue;foo=bar"));
    }

    @Test
    public void testMeters() {
        JsonExporter exporter = new JsonExporter();
        MetricRegistry registry = MetricRegistries.get(MetricRegistry.Type.APPLICATION);

        Meter meterWithoutTags = registry.meter("mymeter");
        Meter meterRed = registry.meter("mymeter", new Tag("color", "red"));
        Meter meterBlue = registry.meter("mymeter", new Tag("color", "blue"), new Tag("foo", "bar"));

        meterWithoutTags.mark(1);
        meterRed.mark(2);
        meterBlue.mark(3);

        String result = exporter.exportMetricsByName(MetricRegistry.Type.APPLICATION, "mymeter").toString();
        JsonObject json = Json.createReader(new StringReader(result)).read().asJsonObject();

        JsonObject myMeterObject = json.getJsonObject("mymeter");
        assertEquals(1, myMeterObject.getInt("count"));
        assertEquals(2, myMeterObject.getInt("count;color=red"));
        assertEquals(3, myMeterObject.getInt("count;color=blue;foo=bar"));

        assertNotNull(myMeterObject.getJsonNumber("meanRate"));
        assertNotNull(myMeterObject.getJsonNumber("meanRate;color=red"));
        assertNotNull(myMeterObject.getJsonNumber("meanRate;color=blue;foo=bar"));

        assertNotNull(myMeterObject.getJsonNumber("oneMinRate"));
        assertNotNull(myMeterObject.getJsonNumber("oneMinRate;color=red"));
        assertNotNull(myMeterObject.getJsonNumber("oneMinRate;color=blue;foo=bar"));

        assertNotNull(myMeterObject.getJsonNumber("fiveMinRate"));
        assertNotNull(myMeterObject.getJsonNumber("fiveMinRate;color=red"));
        assertNotNull(myMeterObject.getJsonNumber("fiveMinRate;color=blue;foo=bar"));

        assertNotNull(myMeterObject.getJsonNumber("fifteenMinRate"));
        assertNotNull(myMeterObject.getJsonNumber("fifteenMinRate;color=red"));
        assertNotNull(myMeterObject.getJsonNumber("fifteenMinRate;color=blue;foo=bar"));
    }

    @Test
    public void testHistograms() {
        JsonExporter exporter = new JsonExporter();
        MetricRegistry registry = MetricRegistries.get(MetricRegistry.Type.APPLICATION);

        Histogram histogramWithoutTags = registry.histogram("myhistogram");
        Histogram histogramRed = registry.histogram("myhistogram", new Tag("color", "red"));
        Histogram histogramBlue = registry.histogram("myhistogram", new Tag("color", "blue"), new Tag("foo", "bar"));

        histogramWithoutTags.update(1);
        histogramRed.update(2);
        histogramBlue.update(3);

        String result = exporter.exportMetricsByName(MetricRegistry.Type.APPLICATION, "myhistogram").toString();
        JsonObject json = Json.createReader(new StringReader(result)).read().asJsonObject();

        JsonObject myhistogramObject = json.getJsonObject("myhistogram");

        assertEquals(1.0, myhistogramObject.getJsonNumber("count").doubleValue(), 1e-10);
        assertEquals(1.0, myhistogramObject.getJsonNumber("count;color=red").doubleValue(), 1e-10);
        assertEquals(1.0, myhistogramObject.getJsonNumber("count;color=blue;foo=bar").doubleValue(), 1e-10);

        assertEquals(1.0, myhistogramObject.getJsonNumber("p50").doubleValue(), 1e-10);
        assertEquals(2.0, myhistogramObject.getJsonNumber("p50;color=red").doubleValue(), 1e-10);
        assertEquals(3.0, myhistogramObject.getJsonNumber("p50;color=blue;foo=bar").doubleValue(), 1e-10);

        assertEquals(1.0, myhistogramObject.getJsonNumber("p75").doubleValue(), 1e-10);
        assertEquals(2.0, myhistogramObject.getJsonNumber("p75;color=red").doubleValue(), 1e-10);
        assertEquals(3.0, myhistogramObject.getJsonNumber("p75;color=blue;foo=bar").doubleValue(), 1e-10);

        assertEquals(1.0, myhistogramObject.getJsonNumber("p95").doubleValue(), 1e-10);
        assertEquals(2.0, myhistogramObject.getJsonNumber("p95;color=red").doubleValue(), 1e-10);
        assertEquals(3.0, myhistogramObject.getJsonNumber("p95;color=blue;foo=bar").doubleValue(), 1e-10);

        assertEquals(1.0, myhistogramObject.getJsonNumber("p98").doubleValue(), 1e-10);
        assertEquals(2.0, myhistogramObject.getJsonNumber("p98;color=red").doubleValue(), 1e-10);
        assertEquals(3.0, myhistogramObject.getJsonNumber("p98;color=blue;foo=bar").doubleValue(), 1e-10);

        assertEquals(1.0, myhistogramObject.getJsonNumber("p99").doubleValue(), 1e-10);
        assertEquals(2.0, myhistogramObject.getJsonNumber("p99;color=red").doubleValue(), 1e-10);
        assertEquals(3.0, myhistogramObject.getJsonNumber("p99;color=blue;foo=bar").doubleValue(), 1e-10);

        assertEquals(1.0, myhistogramObject.getJsonNumber("p999").doubleValue(), 1e-10);
        assertEquals(2.0, myhistogramObject.getJsonNumber("p999;color=red").doubleValue(), 1e-10);
        assertEquals(3.0, myhistogramObject.getJsonNumber("p999;color=blue;foo=bar").doubleValue(), 1e-10);

        assertEquals(1.0, myhistogramObject.getJsonNumber("min").doubleValue(), 1e-10);
        assertEquals(2.0, myhistogramObject.getJsonNumber("min;color=red").doubleValue(), 1e-10);
        assertEquals(3.0, myhistogramObject.getJsonNumber("min;color=blue;foo=bar").doubleValue(), 1e-10);

        assertEquals(1.0, myhistogramObject.getJsonNumber("mean").doubleValue(), 1e-10);
        assertEquals(2.0, myhistogramObject.getJsonNumber("mean;color=red").doubleValue(), 1e-10);
        assertEquals(3.0, myhistogramObject.getJsonNumber("mean;color=blue;foo=bar").doubleValue(), 1e-10);

        assertEquals(1.0, myhistogramObject.getJsonNumber("max").doubleValue(), 1e-10);
        assertEquals(2.0, myhistogramObject.getJsonNumber("max;color=red").doubleValue(), 1e-10);
        assertEquals(3.0, myhistogramObject.getJsonNumber("max;color=blue;foo=bar").doubleValue(), 1e-10);

        assertEquals(0.0, myhistogramObject.getJsonNumber("stddev").doubleValue(), 1e-10);
        assertEquals(0.0, myhistogramObject.getJsonNumber("stddev;color=red").doubleValue(), 1e-10);
        assertEquals(0.0, myhistogramObject.getJsonNumber("stddev;color=blue;foo=bar").doubleValue(), 1e-10);
    }

    @Test
    public void testTimers() {
        JsonExporter exporter = new JsonExporter();
        MetricRegistry registry = MetricRegistries.get(MetricRegistry.Type.APPLICATION);
        Metadata metadata = new MetadataBuilder()
                .withUnit(MetricUnits.SECONDS)
                .withName("mytimer")
                .build();

        Timer timerWithoutTags = registry.timer(metadata);
        Timer timerRed = registry.timer(metadata, new Tag("color", "red"));
        Timer timerBlue = registry.timer(metadata, new Tag("color", "blue"), new Tag("foo", "bar"));

        timerWithoutTags.update(1, TimeUnit.SECONDS);
        timerRed.update(2, TimeUnit.SECONDS);
        timerBlue.update(3, TimeUnit.SECONDS);

        String result = exporter.exportMetricsByName(MetricRegistry.Type.APPLICATION, "mytimer").toString();
        JsonObject json = Json.createReader(new StringReader(result)).read().asJsonObject();

        JsonObject mytimerObject = json.getJsonObject("mytimer");

        assertEquals(1.0, mytimerObject.getJsonNumber("count").doubleValue(), 1e-10);
        assertEquals(1.0, mytimerObject.getJsonNumber("count;color=red").doubleValue(), 1e-10);
        assertEquals(1.0, mytimerObject.getJsonNumber("count;color=blue;foo=bar").doubleValue(), 1e-10);

        assertEquals(1.0, mytimerObject.getJsonNumber("p50").doubleValue(), 1e-10);
        assertEquals(2.0, mytimerObject.getJsonNumber("p50;color=red").doubleValue(), 1e-10);
        assertEquals(3.0, mytimerObject.getJsonNumber("p50;color=blue;foo=bar").doubleValue(), 1e-10);

        assertEquals(1.0, mytimerObject.getJsonNumber("p75").doubleValue(), 1e-10);
        assertEquals(2.0, mytimerObject.getJsonNumber("p75;color=red").doubleValue(), 1e-10);
        assertEquals(3.0, mytimerObject.getJsonNumber("p75;color=blue;foo=bar").doubleValue(), 1e-10);

        assertEquals(1.0, mytimerObject.getJsonNumber("p95").doubleValue(), 1e-10);
        assertEquals(2.0, mytimerObject.getJsonNumber("p95;color=red").doubleValue(), 1e-10);
        assertEquals(3.0, mytimerObject.getJsonNumber("p95;color=blue;foo=bar").doubleValue(), 1e-10);

        assertEquals(1.0, mytimerObject.getJsonNumber("p98").doubleValue(), 1e-10);
        assertEquals(2.0, mytimerObject.getJsonNumber("p98;color=red").doubleValue(), 1e-10);
        assertEquals(3.0, mytimerObject.getJsonNumber("p98;color=blue;foo=bar").doubleValue(), 1e-10);

        assertEquals(1.0, mytimerObject.getJsonNumber("p99").doubleValue(), 1e-10);
        assertEquals(2.0, mytimerObject.getJsonNumber("p99;color=red").doubleValue(), 1e-10);
        assertEquals(3.0, mytimerObject.getJsonNumber("p99;color=blue;foo=bar").doubleValue(), 1e-10);

        assertEquals(1.0, mytimerObject.getJsonNumber("p999").doubleValue(), 1e-10);
        assertEquals(2.0, mytimerObject.getJsonNumber("p999;color=red").doubleValue(), 1e-10);
        assertEquals(3.0, mytimerObject.getJsonNumber("p999;color=blue;foo=bar").doubleValue(), 1e-10);

        assertEquals(1.0, mytimerObject.getJsonNumber("min").doubleValue(), 1e-10);
        assertEquals(2.0, mytimerObject.getJsonNumber("min;color=red").doubleValue(), 1e-10);
        assertEquals(3.0, mytimerObject.getJsonNumber("min;color=blue;foo=bar").doubleValue(), 1e-10);

        assertEquals(1.0, mytimerObject.getJsonNumber("mean").doubleValue(), 1e-10);
        assertEquals(2.0, mytimerObject.getJsonNumber("mean;color=red").doubleValue(), 1e-10);
        assertEquals(3.0, mytimerObject.getJsonNumber("mean;color=blue;foo=bar").doubleValue(), 1e-10);

        assertEquals(1.0, mytimerObject.getJsonNumber("max").doubleValue(), 1e-10);
        assertEquals(2.0, mytimerObject.getJsonNumber("max;color=red").doubleValue(), 1e-10);
        assertEquals(3.0, mytimerObject.getJsonNumber("max;color=blue;foo=bar").doubleValue(), 1e-10);

        assertEquals(0.0, mytimerObject.getJsonNumber("stddev").doubleValue(), 1e-10);
        assertEquals(0.0, mytimerObject.getJsonNumber("stddev;color=red").doubleValue(), 1e-10);
        assertEquals(0.0, mytimerObject.getJsonNumber("stddev;color=blue;foo=bar").doubleValue(), 1e-10);
    }

    @Test
    public void testSemicolonInTagValue() {
        JsonExporter exporter = new JsonExporter();
        MetricRegistry registry = MetricRegistries.get(MetricRegistry.Type.APPLICATION);

        registry.counter("counter1",
                new Tag("tag1", "i;have;semicolons"),
                new Tag("tag2", "i;have;semicolons;as;well"));

        String result = exporter.exportMetricsByName(MetricRegistry.Type.APPLICATION, "counter1").toString();
        JsonObject json = Json.createReader(new StringReader(result)).read().asJsonObject();

        assertNotNull("Semicolons in tag values should be converted to underscores",
                json.getJsonNumber("counter1;tag1=i_have_semicolons;tag2=i_have_semicolons_as_well"));
    }

    @Test
    public void testDoubleQuotesInTagValue() {
        JsonExporter exporter = new JsonExporter();
        MetricRegistry registry = MetricRegistries.get(MetricRegistry.Type.APPLICATION);

        registry.counter("counter1",
                new Tag("tag1", "i_have\"quotes\""));

        String result = exporter.exportMetricsByName(MetricRegistry.Type.APPLICATION, "counter1").toString();
        JsonObject json = Json.createReader(new StringReader(result)).read().asJsonObject();

        assertEquals("Double quotes in tag values should be escaped", "counter1;tag1=i_have\"quotes\"",
                json.keySet().stream().findFirst().get());
    }

    @Test
    public void testNewlineCharacterInTagValue() {
        JsonExporter exporter = new JsonExporter();
        MetricRegistry registry = MetricRegistries.get(MetricRegistry.Type.APPLICATION);

        registry.counter("counter1",
                new Tag("tag1", "i_have\n_two_lines"));

        String result = exporter.exportMetricsByName(MetricRegistry.Type.APPLICATION, "counter1").toString();
        JsonObject json = Json.createReader(new StringReader(result)).read().asJsonObject();

        assertEquals("Newline chars in tag values should be escaped as \\n",
                "counter1;tag1=i_have\n_two_lines", json.keySet().stream().findFirst().get());
    }

    @Test
    public void testAllScopes() {
        JsonExporter exporter = new JsonExporter();
        MetricRegistries.get(MetricRegistry.Type.APPLICATION).counter("c1");
        MetricRegistries.get(MetricRegistry.Type.BASE).counter("b1");
        MetricRegistries.get(MetricRegistry.Type.VENDOR).counter("a1");

        String result = exporter.exportAllScopes().toString();
        JsonObject json = Json.createReader(new StringReader(result)).read().asJsonObject();

        assertEquals(0, json.getJsonObject("application").getInt("c1"));
        assertEquals(0, json.getJsonObject("base").getInt("b1"));
        assertEquals(0, json.getJsonObject("vendor").getInt("a1"));
    }

    @Test
    public void testOneScope() {
        JsonExporter exporter = new JsonExporter();
        MetricRegistries.get(MetricRegistry.Type.VENDOR).counter("c1");

        String result = exporter.exportOneScope(MetricRegistry.Type.VENDOR).toString();
        JsonObject json = Json.createReader(new StringReader(result)).read().asJsonObject();

        assertEquals(0, json.getInt("c1"));
    }

}
