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

import org.apache.flink.configuration.ConfigConstants;
import org.apache.flink.configuration.Configuration;
import org.apache.flink.configuration.MetricOptions;
import org.apache.flink.metrics.Counter;
import org.apache.flink.metrics.Metric;
import org.apache.flink.metrics.MetricConfig;
import org.apache.flink.metrics.MetricGroup;
import org.apache.flink.metrics.SimpleCounter;
import org.apache.flink.metrics.reporter.MetricReporter;
import org.apache.flink.metrics.reporter.Scheduled;
import org.apache.flink.runtime.akka.AkkaUtils;
import org.apache.flink.runtime.metrics.groups.MetricGroupTest;
import org.apache.flink.runtime.metrics.groups.TaskManagerMetricGroup;
import org.apache.flink.runtime.metrics.scope.ScopeFormats;
import org.apache.flink.runtime.metrics.util.TestReporter;
import org.apache.flink.util.TestLogger;

import akka.actor.ActorNotFound;
import akka.actor.ActorRef;
import akka.actor.ActorSystem;
import org.junit.Assert;
import org.junit.Test;

import java.util.List;
import java.util.concurrent.TimeUnit;

import scala.concurrent.Await;
import scala.concurrent.duration.FiniteDuration;

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

/**
 * Tests for the {@link MetricRegistryImpl}.
 */
public class MetricRegistryImplTest extends TestLogger {

	private static final char GLOBAL_DEFAULT_DELIMITER = '.';

	@Test
	public void testIsShutdown() throws Exception {
		MetricRegistryImpl metricRegistry = new MetricRegistryImpl(MetricRegistryConfiguration.defaultMetricRegistryConfiguration());

		Assert.assertFalse(metricRegistry.isShutdown());

		metricRegistry.shutdown().get();

		Assert.assertTrue(metricRegistry.isShutdown());
	}

	/**
	 * Reporter that exposes whether open() was called.
	 */
	protected static class TestReporter1 extends TestReporter {
		public static boolean wasOpened = false;

		@Override
		public void open(MetricConfig config) {
			wasOpened = true;
		}
	}

	/**
	 * Verifies that multiple reporters are instantiated correctly.
	 */
	@Test
	public void testMultipleReporterInstantiation() throws Exception {
		Configuration config = new Configuration();

		config.setString(ConfigConstants.METRICS_REPORTER_PREFIX + "test1." + ConfigConstants.METRICS_REPORTER_CLASS_SUFFIX, TestReporter11.class.getName());
		config.setString(ConfigConstants.METRICS_REPORTER_PREFIX + "test2." + ConfigConstants.METRICS_REPORTER_CLASS_SUFFIX, TestReporter12.class.getName());
		config.setString(ConfigConstants.METRICS_REPORTER_PREFIX + "test3." + ConfigConstants.METRICS_REPORTER_CLASS_SUFFIX, TestReporter13.class.getName());

		MetricRegistryImpl metricRegistry = new MetricRegistryImpl(MetricRegistryConfiguration.fromConfiguration(config));

		assertTrue(metricRegistry.getReporters().size() == 3);

		Assert.assertTrue(TestReporter11.wasOpened);
		Assert.assertTrue(TestReporter12.wasOpened);
		Assert.assertTrue(TestReporter13.wasOpened);

		metricRegistry.shutdown().get();
	}

	/**
	 * Reporter that exposes whether open() was called.
	 */
	protected static class TestReporter11 extends TestReporter {
		public static boolean wasOpened = false;

		@Override
		public void open(MetricConfig config) {
			wasOpened = true;
		}
	}

	/**
	 * Reporter that exposes whether open() was called.
	 */
	protected static class TestReporter12 extends TestReporter {
		public static boolean wasOpened = false;

		@Override
		public void open(MetricConfig config) {
			wasOpened = true;
		}
	}

	/**
	 * Reporter that exposes whether open() was called.
	 */
	protected static class TestReporter13 extends TestReporter {
		public static boolean wasOpened = false;

		@Override
		public void open(MetricConfig config) {
			wasOpened = true;
		}
	}

	/**
	 * Reporter that exposes the {@link MetricConfig} it was given.
	 */
	protected static class TestReporter2 extends TestReporter {
		static MetricConfig mc;
		@Override
		public void open(MetricConfig config) {
			mc = config;
		}
	}

	/**
	 * Verifies that reporters implementing the Scheduled interface are regularly called to report the metrics.
	 */
	@Test
	public void testReporterScheduling() throws Exception {
		Configuration config = new Configuration();

		config.setString(ConfigConstants.METRICS_REPORTER_PREFIX + "test." + ConfigConstants.METRICS_REPORTER_CLASS_SUFFIX, TestReporter3.class.getName());
		config.setString(ConfigConstants.METRICS_REPORTER_PREFIX + "test.arg1", "hello");
		config.setString(ConfigConstants.METRICS_REPORTER_PREFIX + "test." + ConfigConstants.METRICS_REPORTER_INTERVAL_SUFFIX, "50 MILLISECONDS");

		MetricRegistryImpl registry = new MetricRegistryImpl(MetricRegistryConfiguration.fromConfiguration(config));

		long start = System.currentTimeMillis();

		// only start counting from now on
		TestReporter3.reportCount = 0;

		for (int x = 0; x < 10; x++) {
			Thread.sleep(100);
			int reportCount = TestReporter3.reportCount;
			long curT = System.currentTimeMillis();
			/**
			 * Within a given time-frame T only T/500 reports may be triggered due to the interval between reports.
			 * This value however does not not take the first triggered report into account (=> +1).
			 * Furthermore we have to account for the mis-alignment between reports being triggered and our time
			 * measurement (=> +1); for T=200 a total of 4-6 reports may have been
			 * triggered depending on whether the end of the interval for the first reports ends before
			 * or after T=50.
			 */
			long maxAllowedReports = (curT - start) / 50 + 2;
			Assert.assertTrue("Too many reports were triggered.", maxAllowedReports >= reportCount);
		}
		Assert.assertTrue("No report was triggered.", TestReporter3.reportCount > 0);

		registry.shutdown().get();
	}

	/**
	 * Reporter that exposes how often report() was called.
	 */
	protected static class TestReporter3 extends TestReporter implements Scheduled {
		public static int reportCount = 0;

		@Override
		public void report() {
			reportCount++;
		}
	}

	/**
	 * Verifies that reporters are notified of added/removed metrics.
	 */
	@Test
	public void testReporterNotifications() throws Exception {
		Configuration config = new Configuration();
		config.setString(ConfigConstants.METRICS_REPORTER_PREFIX + "test1." + ConfigConstants.METRICS_REPORTER_CLASS_SUFFIX, TestReporter6.class.getName());
		config.setString(ConfigConstants.METRICS_REPORTER_PREFIX + "test2." + ConfigConstants.METRICS_REPORTER_CLASS_SUFFIX, TestReporter7.class.getName());

		MetricRegistryImpl registry = new MetricRegistryImpl(MetricRegistryConfiguration.fromConfiguration(config));

		TaskManagerMetricGroup root = new TaskManagerMetricGroup(registry, "host", "id");
		root.counter("rootCounter");

		assertTrue(TestReporter6.addedMetric instanceof Counter);
		assertEquals("rootCounter", TestReporter6.addedMetricName);

		assertTrue(TestReporter7.addedMetric instanceof Counter);
		assertEquals("rootCounter", TestReporter7.addedMetricName);

		root.close();

		assertTrue(TestReporter6.removedMetric instanceof Counter);
		assertEquals("rootCounter", TestReporter6.removedMetricName);

		assertTrue(TestReporter7.removedMetric instanceof Counter);
		assertEquals("rootCounter", TestReporter7.removedMetricName);

		registry.shutdown().get();
	}

	/**
	 * Reporter that exposes the name and metric instance of the last metric that was added or removed.
	 */
	protected static class TestReporter6 extends TestReporter {
		static Metric addedMetric;
		static String addedMetricName;

		static Metric removedMetric;
		static String removedMetricName;

		@Override
		public void notifyOfAddedMetric(Metric metric, String metricName, MetricGroup group) {
			addedMetric = metric;
			addedMetricName = metricName;
		}

		@Override
		public void notifyOfRemovedMetric(Metric metric, String metricName, MetricGroup group) {
			removedMetric = metric;
			removedMetricName = metricName;
		}
	}

	/**
	 * Reporter that exposes the name and metric instance of the last metric that was added or removed.
	 */
	protected static class TestReporter7 extends TestReporter {
		static Metric addedMetric;
		static String addedMetricName;

		static Metric removedMetric;
		static String removedMetricName;

		@Override
		public void notifyOfAddedMetric(Metric metric, String metricName, MetricGroup group) {
			addedMetric = metric;
			addedMetricName = metricName;
		}

		@Override
		public void notifyOfRemovedMetric(Metric metric, String metricName, MetricGroup group) {
			removedMetric = metric;
			removedMetricName = metricName;
		}
	}

	/**
	 * Verifies that the scope configuration is properly extracted.
	 */
	@Test
	public void testScopeConfig() {
		Configuration config = new Configuration();

		config.setString(MetricOptions.SCOPE_NAMING_TM, "A");
		config.setString(MetricOptions.SCOPE_NAMING_TM_JOB, "B");
		config.setString(MetricOptions.SCOPE_NAMING_TASK, "C");
		config.setString(MetricOptions.SCOPE_NAMING_OPERATOR, "D");

		ScopeFormats scopeConfig = ScopeFormats.fromConfig(config);

		assertEquals("A", scopeConfig.getTaskManagerFormat().format());
		assertEquals("B", scopeConfig.getTaskManagerJobFormat().format());
		assertEquals("C", scopeConfig.getTaskFormat().format());
		assertEquals("D", scopeConfig.getOperatorFormat().format());
	}

	@Test
	public void testConfigurableDelimiter() throws Exception {
		Configuration config = new Configuration();
		config.setString(MetricOptions.SCOPE_DELIMITER, "_");
		config.setString(MetricOptions.SCOPE_NAMING_TM, "A.B.C.D.E");

		MetricRegistryImpl registry = new MetricRegistryImpl(MetricRegistryConfiguration.fromConfiguration(config));

		TaskManagerMetricGroup tmGroup = new TaskManagerMetricGroup(registry, "host", "id");
		assertEquals("A_B_C_D_E_name", tmGroup.getMetricIdentifier("name"));

		registry.shutdown().get();
	}

	@Test
	public void testConfigurableDelimiterForReporters() throws Exception {
		Configuration config = new Configuration();
		config.setString(ConfigConstants.METRICS_REPORTER_PREFIX + "test1." + ConfigConstants.METRICS_REPORTER_SCOPE_DELIMITER, "_");
		config.setString(ConfigConstants.METRICS_REPORTER_PREFIX + "test1." + ConfigConstants.METRICS_REPORTER_CLASS_SUFFIX, TestReporter.class.getName());
		config.setString(ConfigConstants.METRICS_REPORTER_PREFIX + "test2." + ConfigConstants.METRICS_REPORTER_SCOPE_DELIMITER, "-");
		config.setString(ConfigConstants.METRICS_REPORTER_PREFIX + "test2." + ConfigConstants.METRICS_REPORTER_CLASS_SUFFIX, TestReporter.class.getName());
		config.setString(ConfigConstants.METRICS_REPORTER_PREFIX + "test3." + ConfigConstants.METRICS_REPORTER_SCOPE_DELIMITER, "AA");
		config.setString(ConfigConstants.METRICS_REPORTER_PREFIX + "test3." + ConfigConstants.METRICS_REPORTER_CLASS_SUFFIX, TestReporter.class.getName());

		MetricRegistryImpl registry = new MetricRegistryImpl(MetricRegistryConfiguration.fromConfiguration(config));

		assertEquals(GLOBAL_DEFAULT_DELIMITER, registry.getDelimiter());
		assertEquals('_', registry.getDelimiter(0));
		assertEquals('-', registry.getDelimiter(1));
		assertEquals(GLOBAL_DEFAULT_DELIMITER, registry.getDelimiter(2));
		assertEquals(GLOBAL_DEFAULT_DELIMITER, registry.getDelimiter(3));
		assertEquals(GLOBAL_DEFAULT_DELIMITER, registry.getDelimiter(-1));

		registry.shutdown().get();
	}

	@Test
	public void testConfigurableDelimiterForReportersInGroup() throws Exception {
		Configuration config = new Configuration();
		config.setString(ConfigConstants.METRICS_REPORTER_PREFIX + "test1." + ConfigConstants.METRICS_REPORTER_SCOPE_DELIMITER, "_");
		config.setString(ConfigConstants.METRICS_REPORTER_PREFIX + "test1." + ConfigConstants.METRICS_REPORTER_CLASS_SUFFIX, TestReporter8.class.getName());
		config.setString(ConfigConstants.METRICS_REPORTER_PREFIX + "test2." + ConfigConstants.METRICS_REPORTER_SCOPE_DELIMITER, "-");
		config.setString(ConfigConstants.METRICS_REPORTER_PREFIX + "test2." + ConfigConstants.METRICS_REPORTER_CLASS_SUFFIX, TestReporter8.class.getName());
		config.setString(ConfigConstants.METRICS_REPORTER_PREFIX + "test3." + ConfigConstants.METRICS_REPORTER_SCOPE_DELIMITER, "AA");
		config.setString(ConfigConstants.METRICS_REPORTER_PREFIX + "test3." + ConfigConstants.METRICS_REPORTER_CLASS_SUFFIX, TestReporter8.class.getName());
		config.setString(ConfigConstants.METRICS_REPORTER_PREFIX + "test4." + ConfigConstants.METRICS_REPORTER_CLASS_SUFFIX, TestReporter8.class.getName());
		config.setString(MetricOptions.SCOPE_NAMING_TM, "A.B");

		MetricRegistryImpl registry = new MetricRegistryImpl(MetricRegistryConfiguration.fromConfiguration(config));
		List<MetricReporter> reporters = registry.getReporters();
		((TestReporter8) reporters.get(0)).expectedDelimiter = '_'; //test1  reporter
		((TestReporter8) reporters.get(1)).expectedDelimiter = '-'; //test2 reporter
		((TestReporter8) reporters.get(2)).expectedDelimiter = GLOBAL_DEFAULT_DELIMITER; //test3 reporter, because 'AA' - not correct delimiter
		((TestReporter8) reporters.get(3)).expectedDelimiter = GLOBAL_DEFAULT_DELIMITER; //for test4 reporter use global delimiter

		TaskManagerMetricGroup group = new TaskManagerMetricGroup(registry, "host", "id");
		group.counter("C");
		group.close();
		registry.shutdown().get();
		assertEquals(4, TestReporter8.numCorrectDelimitersForRegister);
		assertEquals(4, TestReporter8.numCorrectDelimitersForUnregister);
	}

	/**
	 * Tests that the query actor will be stopped when the MetricRegistry is shut down.
	 */
	@Test
	public void testQueryActorShutdown() throws Exception {
		final FiniteDuration timeout = new FiniteDuration(10L, TimeUnit.SECONDS);

		MetricRegistryImpl registry = new MetricRegistryImpl(MetricRegistryConfiguration.defaultMetricRegistryConfiguration());

		final ActorSystem actorSystem = AkkaUtils.createDefaultActorSystem();

		registry.startQueryService(actorSystem, null);

		ActorRef queryServiceActor = registry.getQueryService();

		registry.shutdown().get();

		try {
			Await.result(actorSystem.actorSelection(queryServiceActor.path()).resolveOne(timeout), timeout);

			fail("The query actor should be terminated resulting in a ActorNotFound exception.");
		} catch (ActorNotFound e) {
			// we expect the query actor to be shut down
		}
	}

	/**
	 * Reporter that verifies that the configured delimiter is applied correctly when generating the metric identifier.
	 */
	public static class TestReporter8 extends TestReporter {
		char expectedDelimiter;
		public static int numCorrectDelimitersForRegister = 0;
		public static int numCorrectDelimitersForUnregister = 0;

		@Override
		public void notifyOfAddedMetric(Metric metric, String metricName, MetricGroup group) {
			String expectedMetric = "A" + expectedDelimiter + "B" + expectedDelimiter + "C";
			assertEquals(expectedMetric, group.getMetricIdentifier(metricName, this));
			assertEquals(expectedMetric, group.getMetricIdentifier(metricName));
			numCorrectDelimitersForRegister++;
		}

		@Override
		public void notifyOfRemovedMetric(Metric metric, String metricName, MetricGroup group) {
			String expectedMetric = "A" + expectedDelimiter + "B" + expectedDelimiter + "C";
			assertEquals(expectedMetric, group.getMetricIdentifier(metricName, this));
			assertEquals(expectedMetric, group.getMetricIdentifier(metricName));
			numCorrectDelimitersForUnregister++;
		}
	}

	@Test
	public void testExceptionIsolation() throws Exception {

		Configuration config = new Configuration();
		config.setString(ConfigConstants.METRICS_REPORTER_PREFIX + "test1." + ConfigConstants.METRICS_REPORTER_CLASS_SUFFIX, FailingReporter.class.getName());
		config.setString(ConfigConstants.METRICS_REPORTER_PREFIX + "test2." + ConfigConstants.METRICS_REPORTER_CLASS_SUFFIX, TestReporter7.class.getName());

		MetricRegistryImpl registry = new MetricRegistryImpl(MetricRegistryConfiguration.fromConfiguration(config));

		Counter metric = new SimpleCounter();
		registry.register(metric, "counter", new MetricGroupTest.DummyAbstractMetricGroup(registry));

		assertEquals(metric, TestReporter7.addedMetric);
		assertEquals("counter", TestReporter7.addedMetricName);

		registry.unregister(metric, "counter", new MetricGroupTest.DummyAbstractMetricGroup(registry));

		assertEquals(metric, TestReporter7.removedMetric);
		assertEquals("counter", TestReporter7.removedMetricName);

		registry.shutdown().get();
	}

	/**
	 * Reporter that throws an exception when it is notified of an added or removed metric.
	 */
	protected static class FailingReporter extends TestReporter {
		@Override
		public void notifyOfAddedMetric(Metric metric, String metricName, MetricGroup group) {
			throw new RuntimeException();
		}

		@Override
		public void notifyOfRemovedMetric(Metric metric, String metricName, MetricGroup group) {
			throw new RuntimeException();
		}
	}
}