/*
 * Copyright 2012-2017 the original author or authors.
 *
 * 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 org.springframework.boot.actuate.endpoint.jmx;

import java.text.SimpleDateFormat;
import java.util.Arrays;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Properties;

import javax.management.MBeanException;
import javax.management.MBeanInfo;
import javax.management.MalformedObjectNameException;
import javax.management.ObjectName;

import com.fasterxml.jackson.databind.ObjectMapper;
import org.junit.After;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;

import org.springframework.beans.MutablePropertyValues;
import org.springframework.beans.factory.config.ConstructorArgumentValues;
import org.springframework.beans.factory.support.RootBeanDefinition;
import org.springframework.boot.actuate.endpoint.AbstractEndpoint;
import org.springframework.boot.actuate.endpoint.LoggersEndpoint;
import org.springframework.boot.logging.logback.LogbackLoggingSystem;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.GenericApplicationContext;
import org.springframework.jmx.export.MBeanExporter;
import org.springframework.jmx.support.ObjectNameManager;
import org.springframework.util.ObjectUtils;

import static org.assertj.core.api.Assertions.assertThat;
import static org.hamcrest.Matchers.containsString;
import static org.junit.internal.matchers.ThrowableMessageMatcher.hasMessage;

/**
 * Tests for {@link EndpointMBeanExporter}
 *
 * @author Christian Dupuis
 * @author Andy Wilkinson
 * @author Stephane Nicoll
 */
public class EndpointMBeanExporterTests {

	@Rule
	public ExpectedException thrown = ExpectedException.none();

	GenericApplicationContext context = null;

	@After
	public void close() {
		if (this.context != null) {
			this.context.close();
		}
	}

	@Test
	public void testRegistrationOfOneEndpoint() throws Exception {
		this.context = new GenericApplicationContext();
		this.context.registerBeanDefinition("endpointMbeanExporter",
				new RootBeanDefinition(EndpointMBeanExporter.class));
		this.context.registerBeanDefinition("endpoint1",
				new RootBeanDefinition(TestEndpoint.class));
		this.context.refresh();
		MBeanExporter mbeanExporter = this.context.getBean(EndpointMBeanExporter.class);
		MBeanInfo mbeanInfo = mbeanExporter.getServer()
				.getMBeanInfo(getObjectName("endpoint1", this.context));
		assertThat(mbeanInfo).isNotNull();
		assertThat(mbeanInfo.getOperations().length).isEqualTo(3);
		assertThat(mbeanInfo.getAttributes().length).isEqualTo(3);
	}

	@Test
	public void testSkipRegistrationOfDisabledEndpoint() throws Exception {
		this.context = new GenericApplicationContext();
		this.context.registerBeanDefinition("endpointMbeanExporter",
				new RootBeanDefinition(EndpointMBeanExporter.class));
		MutablePropertyValues mpv = new MutablePropertyValues();
		mpv.add("enabled", Boolean.FALSE);
		this.context.registerBeanDefinition("endpoint1",
				new RootBeanDefinition(TestEndpoint.class, null, mpv));
		this.context.refresh();
		MBeanExporter mbeanExporter = this.context.getBean(EndpointMBeanExporter.class);
		assertThat(mbeanExporter.getServer()
				.isRegistered(getObjectName("endpoint1", this.context))).isFalse();
	}

	@Test
	public void testRegistrationOfEnabledEndpoint() throws Exception {
		this.context = new GenericApplicationContext();
		this.context.registerBeanDefinition("endpointMbeanExporter",
				new RootBeanDefinition(EndpointMBeanExporter.class));
		MutablePropertyValues mpv = new MutablePropertyValues();
		mpv.add("enabled", Boolean.TRUE);
		this.context.registerBeanDefinition("endpoint1",
				new RootBeanDefinition(TestEndpoint.class, null, mpv));
		this.context.refresh();
		MBeanExporter mbeanExporter = this.context.getBean(EndpointMBeanExporter.class);
		assertThat(mbeanExporter.getServer()
				.isRegistered(getObjectName("endpoint1", this.context))).isTrue();
	}

	@Test
	public void testRegistrationTwoEndpoints() throws Exception {
		this.context = new GenericApplicationContext();
		this.context.registerBeanDefinition("endpointMbeanExporter",
				new RootBeanDefinition(EndpointMBeanExporter.class));
		this.context.registerBeanDefinition("endpoint1",
				new RootBeanDefinition(TestEndpoint.class));
		this.context.registerBeanDefinition("endpoint2",
				new RootBeanDefinition(TestEndpoint2.class));
		this.context.refresh();
		MBeanExporter mbeanExporter = this.context.getBean(EndpointMBeanExporter.class);
		assertThat(mbeanExporter.getServer()
				.getMBeanInfo(getObjectName("endpoint1", this.context))).isNotNull();
		assertThat(mbeanExporter.getServer()
				.getMBeanInfo(getObjectName("endpoint2", this.context))).isNotNull();
	}

	@Test
	public void testRegistrationWithDifferentDomain() throws Exception {
		this.context = new GenericApplicationContext();
		this.context.registerBeanDefinition("endpointMbeanExporter",
				new RootBeanDefinition(EndpointMBeanExporter.class, null,
						new MutablePropertyValues(
								Collections.singletonMap("domain", "test-domain"))));
		this.context.registerBeanDefinition("endpoint1",
				new RootBeanDefinition(TestEndpoint.class));
		this.context.refresh();
		MBeanExporter mbeanExporter = this.context.getBean(EndpointMBeanExporter.class);
		assertThat(mbeanExporter.getServer().getMBeanInfo(
				getObjectName("test-domain", "endpoint1", false, this.context)))
						.isNotNull();
	}

	@Test
	public void testRegistrationWithDifferentDomainAndIdentity() throws Exception {
		Map<String, Object> properties = new HashMap<>();
		properties.put("domain", "test-domain");
		properties.put("ensureUniqueRuntimeObjectNames", true);
		this.context = new GenericApplicationContext();
		this.context.registerBeanDefinition("endpointMbeanExporter",
				new RootBeanDefinition(EndpointMBeanExporter.class, null,
						new MutablePropertyValues(properties)));
		this.context.registerBeanDefinition("endpoint1",
				new RootBeanDefinition(TestEndpoint.class));
		this.context.refresh();
		MBeanExporter mbeanExporter = this.context.getBean(EndpointMBeanExporter.class);
		assertThat(mbeanExporter.getServer().getMBeanInfo(
				getObjectName("test-domain", "endpoint1", true, this.context)))
						.isNotNull();
	}

	@Test
	public void testRegistrationWithDifferentDomainAndIdentityAndStaticNames()
			throws Exception {
		Map<String, Object> properties = new HashMap<>();
		properties.put("domain", "test-domain");
		properties.put("ensureUniqueRuntimeObjectNames", true);
		Properties staticNames = new Properties();
		staticNames.put("key1", "value1");
		staticNames.put("key2", "value2");
		properties.put("objectNameStaticProperties", staticNames);
		this.context = new GenericApplicationContext();
		this.context.registerBeanDefinition("endpointMbeanExporter",
				new RootBeanDefinition(EndpointMBeanExporter.class, null,
						new MutablePropertyValues(properties)));
		this.context.registerBeanDefinition("endpoint1",
				new RootBeanDefinition(TestEndpoint.class));
		this.context.refresh();
		MBeanExporter mbeanExporter = this.context.getBean(EndpointMBeanExporter.class);
		assertThat(mbeanExporter.getServer().getMBeanInfo(ObjectNameManager.getInstance(
				getObjectName("test-domain", "endpoint1", true, this.context).toString()
						+ ",key1=value1,key2=value2"))).isNotNull();
	}

	@Test
	public void testRegistrationWithParentContext() throws Exception {
		this.context = new GenericApplicationContext();
		this.context.registerBeanDefinition("endpointMbeanExporter",
				new RootBeanDefinition(EndpointMBeanExporter.class));
		this.context.registerBeanDefinition("endpoint1",
				new RootBeanDefinition(TestEndpoint.class));
		GenericApplicationContext parent = new GenericApplicationContext();
		this.context.setParent(parent);
		parent.refresh();
		this.context.refresh();
		MBeanExporter mbeanExporter = this.context.getBean(EndpointMBeanExporter.class);
		assertThat(mbeanExporter.getServer()
				.getMBeanInfo(getObjectName("endpoint1", this.context))).isNotNull();
		parent.close();
	}

	@Test
	public void jsonMapConversionWithDefaultObjectMapper() throws Exception {
		this.context = new GenericApplicationContext();
		this.context.registerBeanDefinition("endpointMbeanExporter",
				new RootBeanDefinition(EndpointMBeanExporter.class));
		this.context.registerBeanDefinition("endpoint1",
				new RootBeanDefinition(JsonMapConversionEndpoint.class));
		this.context.refresh();
		MBeanExporter mbeanExporter = this.context.getBean(EndpointMBeanExporter.class);
		Object response = mbeanExporter.getServer().invoke(
				getObjectName("endpoint1", this.context), "getData", new Object[0],
				new String[0]);
		assertThat(response).isInstanceOf(Map.class);
		assertThat(((Map<?, ?>) response).get("date")).isInstanceOf(Long.class);
	}

	@Test
	public void jsonMapConversionWithCustomObjectMapper() throws Exception {
		this.context = new GenericApplicationContext();
		ConstructorArgumentValues constructorArgs = new ConstructorArgumentValues();
		ObjectMapper objectMapper = new ObjectMapper();
		objectMapper.setDateFormat(new SimpleDateFormat("yyyy-MM-dd"));
		constructorArgs.addIndexedArgumentValue(0, objectMapper);
		this.context.registerBeanDefinition("endpointMbeanExporter",
				new RootBeanDefinition(EndpointMBeanExporter.class, constructorArgs,
						null));
		this.context.registerBeanDefinition("endpoint1",
				new RootBeanDefinition(JsonMapConversionEndpoint.class));
		this.context.refresh();
		MBeanExporter mbeanExporter = this.context.getBean(EndpointMBeanExporter.class);
		Object response = mbeanExporter.getServer().invoke(
				getObjectName("endpoint1", this.context), "getData", new Object[0],
				new String[0]);
		assertThat(response).isInstanceOf(Map.class);
		assertThat(((Map<?, ?>) response).get("date")).isInstanceOf(String.class);
	}

	@Test
	public void jsonListConversion() throws Exception {
		this.context = new GenericApplicationContext();
		this.context.registerBeanDefinition("endpointMbeanExporter",
				new RootBeanDefinition(EndpointMBeanExporter.class));
		this.context.registerBeanDefinition("endpoint1",
				new RootBeanDefinition(JsonListConversionEndpoint.class));
		this.context.refresh();
		MBeanExporter mbeanExporter = this.context.getBean(EndpointMBeanExporter.class);
		Object response = mbeanExporter.getServer().invoke(
				getObjectName("endpoint1", this.context), "getData", new Object[0],
				new String[0]);
		assertThat(response).isInstanceOf(List.class);
		assertThat(((List<?>) response).get(0)).isInstanceOf(Long.class);
	}

	@Test
	public void loggerEndpointLowerCaseLogLevel() throws Exception {
		MBeanExporter mbeanExporter = registerLoggersEndpoint();
		Object response = mbeanExporter.getServer().invoke(
				getObjectName("loggersEndpoint", this.context), "setLogLevel",
				new Object[] { "com.example", "trace" },
				new String[] { String.class.getName(), String.class.getName() });
		assertThat(response).isNull();
	}

	@Test
	public void loggerEndpointUnknownLogLevel() throws Exception {
		MBeanExporter mbeanExporter = registerLoggersEndpoint();
		this.thrown.expect(MBeanException.class);
		this.thrown.expectCause(hasMessage(containsString("No enum constant")));
		this.thrown.expectCause(hasMessage(containsString("LogLevel.INVALID")));
		mbeanExporter.getServer().invoke(getObjectName("loggersEndpoint", this.context),
				"setLogLevel", new Object[] { "com.example", "invalid" },
				new String[] { String.class.getName(), String.class.getName() });
	}

	private MBeanExporter registerLoggersEndpoint() {
		this.context = new GenericApplicationContext();
		this.context.registerBeanDefinition("endpointMbeanExporter",
				new RootBeanDefinition(EndpointMBeanExporter.class));
		RootBeanDefinition bd = new RootBeanDefinition(LoggersEndpoint.class);
		ConstructorArgumentValues values = new ConstructorArgumentValues();
		values.addIndexedArgumentValue(0,
				new LogbackLoggingSystem(getClass().getClassLoader()));
		bd.setConstructorArgumentValues(values);
		this.context.registerBeanDefinition("loggersEndpoint", bd);
		this.context.refresh();
		return this.context.getBean(EndpointMBeanExporter.class);
	}

	private ObjectName getObjectName(String beanKey, GenericApplicationContext context)
			throws MalformedObjectNameException {
		return getObjectName("org.springframework.boot", beanKey, false, context);
	}

	private ObjectName getObjectName(String domain, String beanKey,
			boolean includeIdentity, ApplicationContext applicationContext)
					throws MalformedObjectNameException {
		if (includeIdentity) {
			return ObjectNameManager.getInstance(String.format(
					"%s:type=Endpoint,name=%s,identity=%s", domain, beanKey, ObjectUtils
							.getIdentityHexString(applicationContext.getBean(beanKey))));
		}
		return ObjectNameManager
				.getInstance(String.format("%s:type=Endpoint,name=%s", domain, beanKey));
	}

	public static class TestEndpoint extends AbstractEndpoint<String> {

		public TestEndpoint() {
			super("test");
		}

		@Override
		public String invoke() {
			return "hello world";
		}

	}

	public static class TestEndpoint2 extends TestEndpoint {

	}

	public static class JsonMapConversionEndpoint
			extends AbstractEndpoint<Map<String, Object>> {

		public JsonMapConversionEndpoint() {
			super("json_map_conversion");
		}

		@Override
		public Map<String, Object> invoke() {
			Map<String, Object> result = new LinkedHashMap<>();
			result.put("date", new Date());
			return result;
		}

	}

	public static class JsonListConversionEndpoint
			extends AbstractEndpoint<List<Object>> {

		public JsonListConversionEndpoint() {
			super("json_list_conversion");
		}

		@Override
		public List<Object> invoke() {
			return Arrays.<Object>asList(new Date());
		}

	}

}
