/*
 * 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.ignite.internal.metric;

import java.lang.management.ManagementFactory;
import javax.management.MBeanServer;
import javax.management.MBeanServerInvocationHandler;
import javax.management.MalformedObjectNameException;
import javax.management.ObjectName;
import org.apache.ignite.IgniteException;
import org.apache.ignite.configuration.CacheConfiguration;
import org.apache.ignite.configuration.DataStorageConfiguration;
import org.apache.ignite.configuration.IgniteConfiguration;
import org.apache.ignite.internal.IgniteEx;
import org.apache.ignite.internal.processors.metric.MetricRegistry;
import org.apache.ignite.internal.processors.metric.MetricsMxBeanImpl;
import org.apache.ignite.internal.processors.metric.impl.HistogramMetric;
import org.apache.ignite.internal.processors.metric.impl.HitRateMetric;
import org.apache.ignite.internal.util.typedef.internal.U;
import org.apache.ignite.mxbean.MetricsMxBean;
import org.apache.ignite.testframework.junits.common.GridCommonAbstractTest;
import org.junit.Test;

import static org.apache.ignite.configuration.WALMode.FSYNC;
import static org.apache.ignite.internal.processors.cache.transactions.TransactionMetricsAdapter.METRIC_SYSTEM_TIME_HISTOGRAM;
import static org.apache.ignite.internal.processors.metric.GridMetricManager.HISTOGRAM_CFG_PREFIX;
import static org.apache.ignite.internal.processors.metric.GridMetricManager.HITRATE_CFG_PREFIX;
import static org.apache.ignite.internal.processors.metric.GridMetricManager.TX_METRICS;
import static org.apache.ignite.internal.processors.metric.impl.MetricUtils.cacheMetricsRegistryName;
import static org.apache.ignite.internal.processors.metric.impl.MetricUtils.metricName;
import static org.apache.ignite.testframework.GridTestUtils.assertThrowsWithCause;
import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertNotEquals;

/** Tests metrics configuration. */
public class MetricsConfigurationTest extends GridCommonAbstractTest {
    /** Test metric registry. */
    public static final String TEST_REG = "testReg";

    /** Test hitrate metric name. */
    public static final String HITRATE_NAME = "hitrate";

    /** Test histogram metric name. */
    public static final String HISTOGRAM_NAME = "histogram";

    /** Test bounds. */
    public static final long[] BOUNDS = new long[] {50, 100};

    /** {@inheritDoc} */
    @Override protected void beforeTest() throws Exception {
        cleanPersistenceDir();
    }

    /** {@inheritDoc} */
    @Override protected IgniteConfiguration getConfiguration(String igniteInstanceName) throws Exception {
        IgniteConfiguration cfg = super.getConfiguration(igniteInstanceName);

        if (igniteInstanceName.startsWith("persistent")) {
            DataStorageConfiguration dsCfg = new DataStorageConfiguration();

            dsCfg.getDefaultDataRegionConfiguration().setPersistenceEnabled(true);
            dsCfg.setWalMode(FSYNC);

            cfg.setDataStorageConfiguration(dsCfg);
        }

        return cfg;
    }

    /** Tests configuration of {@link HitRateMetric}. */
    @Test
    public void testHitRateConfiguration() throws Exception {
        try (IgniteEx g = startGrid(0)) {
            MetricsMxBean bean = metricsBean(g);

            //Empty name.
            assertThrowsWithCause(
                () -> bean.configureHitRateMetric(null, 1),
                NullPointerException.class);

            //Wrong rateTimeInterval value.
            assertThrowsWithCause(
                () -> bean.configureHitRateMetric("io.dataregion.default.AllocationRate", 0),
                IllegalArgumentException.class);

            assertThrowsWithCause(
                () -> bean.configureHitRateMetric("io.dataregion.default.AllocationRate", -1),
                IllegalArgumentException.class);

            bean.configureHitRateMetric("io.dataregion.default.AllocationRate", 5000);

            HitRateMetric allocationRate = g.context().metric().registry(metricName("io.dataregion.default"))
                .findMetric("AllocationRate");

            assertEquals(5000, allocationRate.rateTimeInterval());
        }
    }

    /** Tests configuration of {@link HistogramMetric}. */
    @Test
    public void testHistogramConfiguration() throws Exception {
        try (IgniteEx g = startGrid(0)) {
            MetricsMxBean bean = metricsBean(g);

            //Empty name.
            assertThrowsWithCause(() -> bean.configureHistogramMetric(null, BOUNDS), NullPointerException.class);

            //Wrong bounds value.
            assertThrowsWithCause(
                () -> bean.configureHistogramMetric(metricName(TX_METRICS, METRIC_SYSTEM_TIME_HISTOGRAM), null),
                NullPointerException.class);

            assertThrowsWithCause(
                () -> bean.configureHistogramMetric(metricName(TX_METRICS, METRIC_SYSTEM_TIME_HISTOGRAM), new long[0]),
                IllegalArgumentException.class);

            bean.configureHistogramMetric(metricName(TX_METRICS, METRIC_SYSTEM_TIME_HISTOGRAM), BOUNDS);

            HistogramMetric systemTime = g.context().metric().registry(TX_METRICS)
                .findMetric(METRIC_SYSTEM_TIME_HISTOGRAM);

            assertArrayEquals(BOUNDS, systemTime.bounds());
        }
    }

    /** Tests metric configuration applied on all nodes. */
    @Test
    public void testConfigurationSeveralNodes() throws Exception {
        try (IgniteEx g0 = startGrid(0); IgniteEx g1 = startGrid(1)) {
            assertNotEquals(BOUNDS.length, g0.context().metric().registry(TX_METRICS)
                .<HistogramMetric>findMetric(METRIC_SYSTEM_TIME_HISTOGRAM).bounds().length);

            assertNotEquals(BOUNDS.length, g1.context().metric().registry(TX_METRICS)
                .<HistogramMetric>findMetric(METRIC_SYSTEM_TIME_HISTOGRAM).bounds().length);

            metricsBean(g0).configureHistogramMetric(metricName(TX_METRICS, METRIC_SYSTEM_TIME_HISTOGRAM), BOUNDS);

            assertArrayEquals(BOUNDS, g0.context().metric().registry(TX_METRICS)
                .<HistogramMetric>findMetric(METRIC_SYSTEM_TIME_HISTOGRAM).bounds());

            assertArrayEquals(BOUNDS, g1.context().metric().registry(TX_METRICS)
                .<HistogramMetric>findMetric(METRIC_SYSTEM_TIME_HISTOGRAM).bounds());
        }
    }

    /** Tests metric configuration applied on all nodes. */
    @Test
    public void testNodeRestart() throws Exception {
        checkOnStartAndRestart((g0, g1) -> {
            assertNotEquals(BOUNDS.length, g0.context().metric().registry(TX_METRICS)
                .<HistogramMetric>findMetric(METRIC_SYSTEM_TIME_HISTOGRAM).bounds().length);

            assertNotEquals(BOUNDS.length, g1.context().metric().registry(TX_METRICS)
                .<HistogramMetric>findMetric(METRIC_SYSTEM_TIME_HISTOGRAM).bounds().length);

            metricsBean(g0).configureHistogramMetric(metricName(TX_METRICS, METRIC_SYSTEM_TIME_HISTOGRAM), BOUNDS);

            assertArrayEquals(BOUNDS, g0.context().metric().registry(TX_METRICS)
                .<HistogramMetric>findMetric(METRIC_SYSTEM_TIME_HISTOGRAM).bounds());

            assertArrayEquals(BOUNDS, g1.context().metric().registry(TX_METRICS)
                .<HistogramMetric>findMetric(METRIC_SYSTEM_TIME_HISTOGRAM).bounds());
        }, (g0, g1) -> {
            assertArrayEquals(BOUNDS, g0.context().metric().registry(TX_METRICS)
                .<HistogramMetric>findMetric(METRIC_SYSTEM_TIME_HISTOGRAM).bounds());

            assertArrayEquals(BOUNDS, g1.context().metric().registry(TX_METRICS)
                .<HistogramMetric>findMetric(METRIC_SYSTEM_TIME_HISTOGRAM).bounds());
        });
    }

    /** Tests metric configuration removed on registry remove. */
    @Test
    public void testConfigRemovedOnRegistryRemove() throws Exception {
        checkOnStartAndRestart((g0, g1) -> {
            MetricRegistry mreg = g0.context().metric().registry(TEST_REG);

            mreg.hitRateMetric(HITRATE_NAME, "test", 10000, 5);
            mreg.histogram(HISTOGRAM_NAME, new long[] {250, 500}, "test");

            metricsBean(g0).configureHistogramMetric(metricName(TEST_REG, HISTOGRAM_NAME), BOUNDS);
            metricsBean(g0).configureHitRateMetric(metricName(TEST_REG, HITRATE_NAME), 1000);
        }, (g0, g1) -> {
            MetricRegistry mreg = g0.context().metric().registry(TEST_REG);

            HitRateMetric hitRate = mreg.hitRateMetric(HITRATE_NAME, "test", 10000, 5);
            HistogramMetric histogram = mreg.histogram(HISTOGRAM_NAME, new long[] {250, 500}, "test");

            assertEquals(1000, hitRate.rateTimeInterval());
            assertArrayEquals(BOUNDS, histogram.bounds());

            assertEquals((Long)1000L,
                g0.context().distributedMetastorage().read(metricName(HITRATE_CFG_PREFIX, TEST_REG, HITRATE_NAME)));

            assertArrayEquals(BOUNDS,
                g0.context().distributedMetastorage().read(metricName(HISTOGRAM_CFG_PREFIX, TEST_REG, HISTOGRAM_NAME)));

            assertEquals((Long)1000L,
                g1.context().distributedMetastorage().read(metricName(HITRATE_CFG_PREFIX, TEST_REG, HITRATE_NAME)));

            assertArrayEquals(BOUNDS,
                g1.context().distributedMetastorage().read(metricName(HISTOGRAM_CFG_PREFIX, TEST_REG, HISTOGRAM_NAME)));

            g0.context().metric().remove(TEST_REG);

            assertNull(
                g0.context().distributedMetastorage().read(metricName(HITRATE_CFG_PREFIX, TEST_REG, HITRATE_NAME)));
            assertNull(
                g0.context().distributedMetastorage().read(metricName(HISTOGRAM_CFG_PREFIX, TEST_REG, HISTOGRAM_NAME)));

            assertNull(
                g1.context().distributedMetastorage().read(metricName(HITRATE_CFG_PREFIX, TEST_REG, HITRATE_NAME)));
            assertNull(
                g1.context().distributedMetastorage().read(metricName(HISTOGRAM_CFG_PREFIX, TEST_REG, HISTOGRAM_NAME)));
        });
    }

    /** Tests metric configuration removed on registry remove. */
    @Test
    public void testConfigRemovedOnCacheRemove() throws Exception {
        String cacheRegName = cacheMetricsRegistryName("test", false);

        checkOnStartAndRestart((g0, g1) -> {
            g0.createCache("test");

            awaitPartitionMapExchange();

            HistogramMetric getTime = g0.context().metric().registry(cacheRegName).findMetric("GetTime");

            assertNotEquals(BOUNDS.length, getTime.bounds().length);

            metricsBean(g0).configureHistogramMetric(metricName(cacheRegName, "GetTime"), BOUNDS);

            assertArrayEquals(BOUNDS,
                g0.context().metric().registry(cacheRegName).<HistogramMetric>findMetric("GetTime").bounds());

            assertArrayEquals(BOUNDS,
                g1.context().metric().registry(cacheRegName).<HistogramMetric>findMetric("GetTime").bounds());
        }, (g0, g1) -> {
            assertArrayEquals(BOUNDS,
                g0.context().metric().registry(cacheRegName).<HistogramMetric>findMetric("GetTime").bounds());

            assertArrayEquals(BOUNDS,
                g1.context().metric().registry(cacheRegName).<HistogramMetric>findMetric("GetTime").bounds());

            g0.destroyCache("test");

            awaitPartitionMapExchange();

            assertNull(g0.context().distributedMetastorage().read(metricName(cacheRegName, "GetTime")));
            assertNull(g1.context().distributedMetastorage().read(metricName(cacheRegName, "GetTime")));
        });
    }

    /** Tests metric configuration removed on registry remove. */
    @Test
    public void testConfigRemovedOnCacheGroupRemove() throws Exception {
        String cacheRegName = cacheMetricsRegistryName("test", false);
        String mname = metricName(cacheRegName, "GetTime");

        checkOnStartAndRestart((g0, g1) -> {
            CacheConfiguration<String, String> ccfg = new CacheConfiguration<>("test");

            ccfg.setGroupName("group");

            g0.createCache(ccfg);

            awaitPartitionMapExchange();

            HistogramMetric getTime = g0.context().metric().registry(cacheRegName).findMetric("GetTime");

            assertNotEquals(BOUNDS.length, getTime.bounds().length);

            metricsBean(g0).configureHistogramMetric(mname, BOUNDS);

            assertArrayEquals(BOUNDS,
                g0.context().metric().registry(cacheRegName).<HistogramMetric>findMetric("GetTime").bounds());

            assertArrayEquals(BOUNDS,
                g1.context().metric().registry(cacheRegName).<HistogramMetric>findMetric("GetTime").bounds());
        }, (g0, g1) -> {
            assertArrayEquals(BOUNDS,
                g0.context().metric().registry(cacheRegName).<HistogramMetric>findMetric("GetTime").bounds());

            assertArrayEquals(BOUNDS,
                g1.context().metric().registry(cacheRegName).<HistogramMetric>findMetric("GetTime").bounds());

            g0.destroyCache("test");

            awaitPartitionMapExchange();

            assertNull(g0.context().distributedMetastorage().read(mname));
            assertNull(g1.context().distributedMetastorage().read(mname));
        });
    }

    /** Executes provided closures after cluster start and after cluster restart. */
    public void checkOnStartAndRestart(IgniteBiInClosureX<IgniteEx, IgniteEx> afterStart,
        IgniteBiInClosureX<IgniteEx, IgniteEx> afterRestart) throws Exception {

        IgniteEx g0 = startGrid("persistent-0");
        IgniteEx g1 = startGrid("persistent-1");

        try {
            g0.cluster().active(true);

            afterStart.apply(g0, g1);

            g0.close();
            g1.close();

            g0 = startGrid("persistent-0");
            g1 = startGrid("persistent-1");

            g0.cluster().active(true);

            afterRestart.apply(g0, g1);
        }
        finally {
            g0.close();
            g1.close();
        }
    }

    /** */
    public static MetricsMxBean metricsBean(IgniteEx g) {
        try {
            ObjectName mbeanName = U.makeMBeanName(g.name(), "Metrics", MetricsMxBeanImpl.class.getSimpleName());

            MBeanServer mbeanSrv = ManagementFactory.getPlatformMBeanServer();

            if (!mbeanSrv.isRegistered(mbeanName))
                throw new IgniteException("MBean not registered.");

            return MBeanServerInvocationHandler.newProxyInstance(mbeanSrv, mbeanName, MetricsMxBean.class, false);
        }
        catch (MalformedObjectNameException e) {
            throw new IgniteException(e);
        }
    }

    /** */
    public interface IgniteBiInClosureX<E1, E2> {
        /**
         * Closure body.
         *
         * @param e1 First parameter.
         * @param e2 Second parameter.
         */
        public default void apply(E1 e1, E2 e2) {
            try {
                applyx(e1, e2);
            }
            catch (Exception e) {
                throw new RuntimeException(e);
            }
        }

        /**
         * Closure body that can throw exception.
         *
         * @param e1 First parameter.
         * @param e2 Second parameter.
         */
        public void applyx(E1 e1, E2 e2) throws Exception;
    }
}
