/*
 *
 * 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.cassandra.metrics;

import java.lang.management.ManagementFactory;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;

import com.codahale.metrics.Meter;
import com.codahale.metrics.Metric;
import com.codahale.metrics.Timer;
import com.codahale.metrics.jvm.BufferPoolMetricSet;
import com.codahale.metrics.jvm.GarbageCollectorMetricSet;
import com.codahale.metrics.jvm.MemoryUsageGaugeSet;

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

import org.apache.cassandra.metrics.CassandraMetricsRegistry.MetricName;
import org.apache.cassandra.utils.EstimatedHistogram;

import static com.codahale.metrics.MetricRegistry.name;
import static org.apache.cassandra.metrics.CassandraMetricsRegistry.resolveShortMetricName;
import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;


public class CassandraMetricsRegistryTest
{
    // A class with a name ending in '$'
    private static class StrangeName$
    {
    }

    @Before
    public void prepare()
    {
        CassandraMetricsRegistry.Metrics.getNames().forEach(CassandraMetricsRegistry.Metrics::remove);
    }

    @Test
    public void testChooseType()
    {
        assertEquals("StrangeName", MetricName.chooseType(null, StrangeName$.class));
        assertEquals("StrangeName", MetricName.chooseType("", StrangeName$.class));
        assertEquals("String", MetricName.chooseType(null, String.class));
        assertEquals("String", MetricName.chooseType("", String.class));

        assertEquals("a", MetricName.chooseType("a", StrangeName$.class));
        assertEquals("b", MetricName.chooseType("b", String.class));
    }

    @Test
    public void testMetricName()
    {
        MetricName name = new MetricName(StrangeName$.class, "NaMe", "ScOpE");
        assertEquals("StrangeName", name.getType());
    }

    @Test
    public void testJvmMetricsRegistration()
    {
        CassandraMetricsRegistry registry = CassandraMetricsRegistry.Metrics;

        // Same registration as CassandraDaemon
        registry.register("jvm.buffers", new BufferPoolMetricSet(ManagementFactory.getPlatformMBeanServer()));
        registry.register("jvm.gc", new GarbageCollectorMetricSet());
        registry.register("jvm.memory", new MemoryUsageGaugeSet());

        Collection<String> names = registry.getNames();

        // No metric with ".." in name
        assertTrue(names.stream()
                        .filter(name -> name.contains(".."))
                        .count()
                   == 0);

        // There should be several metrics within each category
        for (String category : new String[]{"jvm.buffers","jvm.gc","jvm.memory"})
        {
            assertTrue(names.stream()
                            .filter(name -> name.startsWith(category+'.'))
                            .count() > 1);
        }
    }

    @Test(expected = IllegalArgumentException.class)
    public void testMetricSetRegistration()
    {
        CassandraMetricsRegistry registry = CassandraMetricsRegistry.Metrics;
        DefaultNameFactory factory = new DefaultNameFactory("ThreadPools", "TestMetricSetRegistration");
        registry.register(factory.createMetricName("jvm.buffers"), new BufferPoolMetricSet(ManagementFactory.getPlatformMBeanServer()));
    }

    @Test
    public void testDeltaBaseCase()
    {
        long[] last = new long[10];
        long[] now = new long[]{0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
        // difference between all zeros and a value should be the value
        assertArrayEquals(now, CassandraMetricsRegistry.delta(now, last));
        // the difference between itself should be all 0s
        assertArrayEquals(last, CassandraMetricsRegistry.delta(now, now));
        // verifying each value is calculated
        assertArrayEquals(new long[] {1, 1, 1, 1, 1, 1, 1, 1, 1, 1},
                CassandraMetricsRegistry.delta(new long[] {1, 2, 3, 4, 5, 6, 7, 8, 9, 10}, now));
    }

    @Test
    public void testDeltaHistogramSizeChange()
    {
        long[] count = new long[]{0, 1, 2, 3, 4, 5};
        assertArrayEquals(count, CassandraMetricsRegistry.delta(count, new long[3]));
        assertArrayEquals(new long[6], CassandraMetricsRegistry.delta(count, new long[]{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}));
    }

    /**
     * Test the updated timer values are estimated correctly (i.e., in the valid range, 1.2) in the micros based histogram.
     */
    @Test
    public void testTimer()
    {
        long[] offsets = EstimatedHistogram.newOffsets(DecayingEstimatedHistogramReservoir.LOW_BUCKET_COUNT, false);
        Timer timer = new Timer(CassandraMetricsRegistry.createReservoir(TimeUnit.MICROSECONDS));
        timer.update(42, TimeUnit.NANOSECONDS);
        timer.update(100, TimeUnit.NANOSECONDS);
        timer.update(42, TimeUnit.MICROSECONDS);
        timer.update(100, TimeUnit.MICROSECONDS);
        timer.update(42, TimeUnit.MILLISECONDS);
        timer.update(100, TimeUnit.MILLISECONDS);
        timer.update(100, TimeUnit.SECONDS);
        timer.update(100, TimeUnit.MINUTES);
        int maxSeconds = 21356;
        timer.update(maxSeconds, TimeUnit.SECONDS);
        long[] counts = timer.getSnapshot().getValues();
        int expectedBucketsWithValues = 8;
        int bucketsWithValues = 0;
        for (int i = 0; i < counts.length; i++)
        {
            if (counts[i] != 0)
            {
                bucketsWithValues ++;
                assertTrue(
                inRange(offsets[i], TimeUnit.NANOSECONDS.toMicros(42), 1.2)
                || inRange(offsets[i], TimeUnit.NANOSECONDS.toMicros(100), 1.2)
                || inRange(offsets[i], TimeUnit.MICROSECONDS.toMicros(42), 1.2)
                || inRange(offsets[i], TimeUnit.MICROSECONDS.toMicros(100), 1.2)
                || inRange(offsets[i], TimeUnit.MILLISECONDS.toMicros(42), 1.2)
                || inRange(offsets[i], TimeUnit.MILLISECONDS.toMicros(100), 1.2)
                || inRange(offsets[i], TimeUnit.SECONDS.toMicros(100), 1.2)
                || inRange(offsets[i], TimeUnit.MINUTES.toMicros(100), 1.2)
                || inRange(offsets[i], TimeUnit.SECONDS.toMicros(maxSeconds), 1.2)
                );
            }
        }
        assertEquals("42 and 100 nanos should both be put in the first bucket",
                            2, counts[0]);
        assertEquals(expectedBucketsWithValues, bucketsWithValues);
    }

    @Test
    public void testMetricAliasesRemoveByCondition()
    {
        LinkedList<MetricName> aliases = new LinkedList<>();
        int size = 10;
        DefaultNameFactory factory = new DefaultNameFactory("Table", "FirstScope");
        DefaultNameFactory aliasFactory = new DefaultNameFactory("Table", "SecondScope");
        MetricName first = factory.createMetricName("FirstTestMetricAliasesOrder");
        for (int i = 0; i < size; i++)
            aliases.add(aliasFactory.createMetricName("AliasFirstTestMetricAliasesOrder_" + UUID.randomUUID()));

        Meter metric = CassandraMetricsRegistry.Metrics.meter(first);
        CassandraMetricsRegistry.Metrics.register(first, metric, aliases.toArray(new MetricName[size]));
        List<String> all = CassandraMetricsRegistry.Metrics.getMetrics().keySet().
                                                           stream()
                                                           .filter(m -> m.contains("FirstTestMetricAliasesOrder"))
                                                           .collect(Collectors.toList());

        assertNotNull(all);
        assertEquals(size + 1, all.size());

        CassandraMetricsRegistry.Metrics.removeIfMatch(fullName ->
                                                           resolveShortMetricName(fullName, DefaultNameFactory.GROUP_NAME, "Table", null),
                                                       factory::createMetricName,
                                                       m -> {});
        Map<String, Metric> metrics = CassandraMetricsRegistry.Metrics.getMetrics();
        assertEquals(size, metrics.size());
        assertTrue(metrics.keySet().stream().allMatch(m -> m.startsWith(name(DefaultNameFactory.GROUP_NAME, "Table", "AliasFirstTestMetricAliasesOrder_"))));

        CassandraMetricsRegistry.Metrics.removeIfMatch(fullName ->
                                                           resolveShortMetricName(fullName, DefaultNameFactory.GROUP_NAME, "Table", null),
                                                       aliasFactory::createMetricName,
                                                       m -> {});
        assertTrue(CassandraMetricsRegistry.Metrics.getMetrics().isEmpty());
    }

    @Test
    public void testMetricAliasesRegistration()
    {
        CassandraMetricsRegistry registry = CassandraMetricsRegistry.Metrics;
        MetricNameFactory factory = new DefaultNameFactory("Table", "MetricsTestScope");
        MetricNameFactory aliasFactory = new DefaultNameFactory("Table", "MetricsTestAliasScope");

        MetricName meter = factory.createMetricName("TestMeter");
        MetricName meterAlias = aliasFactory.createMetricName("TestMeterAlias");
        registry.meter(meter, meterAlias);

        MetricName histogram = factory.createMetricName("TestHistogram");
        MetricName histogramAlias = aliasFactory.createMetricName("TestHistogramAlias");
        registry.histogram(histogram, histogramAlias, false);

        MetricName counter = factory.createMetricName("TestCounter");
        MetricName counterAlias = aliasFactory.createMetricName("TestCounterAlias");
        registry.counter(counter, counterAlias);

        MetricName timer = factory.createMetricName("TestTimer");
        MetricName timerAlias = aliasFactory.createMetricName("TestTimerAlias");
        registry.timer(timer, timerAlias);

        Set<String> aliases = new HashSet<>(Arrays.asList(meter.getMetricName(), meterAlias.getMetricName(),
                                                          histogram.getMetricName(), histogramAlias.getMetricName(),
                                                          counter.getMetricName(), counterAlias.getMetricName(),
                                                          timer.getMetricName(), timerAlias.getMetricName()));

        assertTrue(registry.getMetrics().keySet().containsAll(aliases));
        assertTrue(registry.getNames().containsAll(aliases));
    }

    private boolean inRange(long anchor, long input, double range)
    {
        return input / ((double) anchor) < range;
    }
}
