/*
 * Copyright 2002-2016 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
 *
 *      https://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.jmx.access;

import java.beans.PropertyDescriptor;
import java.io.IOException;
import java.lang.reflect.Method;
import java.net.BindException;
import java.util.HashMap;
import java.util.Map;
import javax.management.Descriptor;
import javax.management.MBeanServerConnection;
import javax.management.remote.JMXConnectorServer;
import javax.management.remote.JMXConnectorServerFactory;
import javax.management.remote.JMXServiceURL;

import org.junit.Test;

import org.springframework.jmx.AbstractMBeanServerTests;
import org.springframework.jmx.IJmxTestBean;
import org.springframework.jmx.JmxException;
import org.springframework.jmx.JmxTestBean;
import org.springframework.jmx.export.MBeanExporter;
import org.springframework.jmx.export.assembler.AbstractReflectiveMBeanInfoAssembler;
import org.springframework.tests.Assume;
import org.springframework.tests.TestGroup;
import org.springframework.util.SocketUtils;

import static org.junit.Assert.*;
import static org.junit.Assume.*;

/**
 * To run the tests in the class, set the following Java system property:
 * {@code -DtestGroups=jmxmp}.
 *
 * @author Rob Harrop
 * @author Juergen Hoeller
 * @author Sam Brannen
 * @author Chris Beams
 */
public class MBeanClientInterceptorTests extends AbstractMBeanServerTests {

	protected static final String OBJECT_NAME = "spring:test=proxy";

	protected JmxTestBean target;

	protected boolean runTests = true;

	@Override
	public void onSetUp() throws Exception {
		target = new JmxTestBean();
		target.setAge(100);
		target.setName("Rob Harrop");

		MBeanExporter adapter = new MBeanExporter();
		Map<String, Object> beans = new HashMap<>();
		beans.put(OBJECT_NAME, target);
		adapter.setServer(getServer());
		adapter.setBeans(beans);
		adapter.setAssembler(new ProxyTestAssembler());
		start(adapter);
	}

	protected MBeanServerConnection getServerConnection() throws Exception {
		return getServer();
	}

	protected IJmxTestBean getProxy() throws Exception {
		MBeanProxyFactoryBean factory = new MBeanProxyFactoryBean();
		factory.setServer(getServerConnection());
		factory.setProxyInterface(IJmxTestBean.class);
		factory.setObjectName(OBJECT_NAME);
		factory.afterPropertiesSet();
		return (IJmxTestBean) factory.getObject();
	}

	@Test
	public void testProxyClassIsDifferent() throws Exception {
		assumeTrue(runTests);
		IJmxTestBean proxy = getProxy();
		assertTrue("The proxy class should be different than the base class", (proxy.getClass() != IJmxTestBean.class));
	}

	@Test
	public void testDifferentProxiesSameClass() throws Exception {
		assumeTrue(runTests);
		IJmxTestBean proxy1 = getProxy();
		IJmxTestBean proxy2 = getProxy();

		assertNotSame("The proxies should NOT be the same", proxy1, proxy2);
		assertSame("The proxy classes should be the same", proxy1.getClass(), proxy2.getClass());
	}

	@Test
	public void testGetAttributeValue() throws Exception {
		assumeTrue(runTests);
		IJmxTestBean proxy1 = getProxy();
		int age = proxy1.getAge();
		assertEquals("The age should be 100", 100, age);
	}

	@Test
	public void testSetAttributeValue() throws Exception {
		assumeTrue(runTests);
		IJmxTestBean proxy = getProxy();
		proxy.setName("Rob Harrop");
		assertEquals("The name of the bean should have been updated", "Rob Harrop", target.getName());
	}

	@Test(expected = IllegalArgumentException.class)
	public void testSetAttributeValueWithRuntimeException() throws Exception {
		assumeTrue(runTests);
		IJmxTestBean proxy = getProxy();
		proxy.setName("Juergen");
	}

	@Test(expected = ClassNotFoundException.class)
	public void testSetAttributeValueWithCheckedException() throws Exception {
		assumeTrue(runTests);
		IJmxTestBean proxy = getProxy();
		proxy.setName("Juergen Class");
	}

	@Test(expected = IOException.class)
	public void testSetAttributeValueWithIOException() throws Exception {
		assumeTrue(runTests);
		IJmxTestBean proxy = getProxy();
		proxy.setName("Juergen IO");
	}

	@Test(expected = InvalidInvocationException.class)
	public void testSetReadOnlyAttribute() throws Exception {
		assumeTrue(runTests);
		IJmxTestBean proxy = getProxy();
		proxy.setAge(900);
	}

	@Test
	public void testInvokeNoArgs() throws Exception {
		assumeTrue(runTests);
		IJmxTestBean proxy = getProxy();
		long result = proxy.myOperation();
		assertEquals("The operation should return 1", 1, result);
	}

	@Test
	public void testInvokeArgs() throws Exception {
		assumeTrue(runTests);
		IJmxTestBean proxy = getProxy();
		int result = proxy.add(1, 2);
		assertEquals("The operation should return 3", 3, result);
	}

	@Test(expected = InvalidInvocationException.class)
	public void testInvokeUnexposedMethodWithException() throws Exception {
		assumeTrue(runTests);
		IJmxTestBean bean = getProxy();
		bean.dontExposeMe();
	}

	@Test
	public void testTestLazyConnectionToRemote() throws Exception {
		assumeTrue(runTests);
		Assume.group(TestGroup.JMXMP);

		final int port = SocketUtils.findAvailableTcpPort();

		JMXServiceURL url = new JMXServiceURL("service:jmx:jmxmp://localhost:" + port);
		JMXConnectorServer connector = JMXConnectorServerFactory.newJMXConnectorServer(url, null, getServer());

		MBeanProxyFactoryBean factory = new MBeanProxyFactoryBean();
		factory.setServiceUrl(url.toString());
		factory.setProxyInterface(IJmxTestBean.class);
		factory.setObjectName(OBJECT_NAME);
		factory.setConnectOnStartup(false);
		factory.setRefreshOnConnectFailure(true);
		// should skip connection to the server
		factory.afterPropertiesSet();
		IJmxTestBean bean = (IJmxTestBean) factory.getObject();

		// now start the connector
		try {
			connector.start();
		}
		catch (BindException ex) {
			System.out.println("Skipping remainder of JMX LazyConnectionToRemote test because binding to local port ["
					+ port + "] failed: " + ex.getMessage());
			return;
		}

		// should now be able to access data via the lazy proxy
		try {
			assertEquals("Rob Harrop", bean.getName());
			assertEquals(100, bean.getAge());
		}
		finally {
			connector.stop();
		}

		try {
			bean.getName();
		}
		catch (JmxException ex) {
			// expected
		}

		connector = JMXConnectorServerFactory.newJMXConnectorServer(url, null, getServer());
		connector.start();

		// should now be able to access data via the lazy proxy
		try {
			assertEquals("Rob Harrop", bean.getName());
			assertEquals(100, bean.getAge());
		}
		finally {
			connector.stop();
		}
	}

	/*
	public void testMXBeanAttributeAccess() throws Exception {
		MBeanClientInterceptor interceptor = new MBeanClientInterceptor();
		interceptor.setServer(ManagementFactory.getPlatformMBeanServer());
		interceptor.setObjectName("java.lang:type=Memory");
		interceptor.setManagementInterface(MemoryMXBean.class);
		MemoryMXBean proxy = ProxyFactory.getProxy(MemoryMXBean.class, interceptor);
		assertTrue(proxy.getHeapMemoryUsage().getMax() > 0);
	}

	public void testMXBeanOperationAccess() throws Exception {
		MBeanClientInterceptor interceptor = new MBeanClientInterceptor();
		interceptor.setServer(ManagementFactory.getPlatformMBeanServer());
		interceptor.setObjectName("java.lang:type=Threading");
		ThreadMXBean proxy = ProxyFactory.getProxy(ThreadMXBean.class, interceptor);
		assertTrue(proxy.getThreadInfo(Thread.currentThread().getId()).getStackTrace() != null);
	}

	public void testMXBeanAttributeListAccess() throws Exception {
		MBeanClientInterceptor interceptor = new MBeanClientInterceptor();
		interceptor.setServer(ManagementFactory.getPlatformMBeanServer());
		interceptor.setObjectName("com.sun.management:type=HotSpotDiagnostic");
		HotSpotDiagnosticMXBean proxy = ProxyFactory.getProxy(HotSpotDiagnosticMXBean.class, interceptor);
		assertFalse(proxy.getDiagnosticOptions().isEmpty());
	}
	*/

	private static class ProxyTestAssembler extends AbstractReflectiveMBeanInfoAssembler {

		@Override
		protected boolean includeReadAttribute(Method method, String beanKey) {
			return true;
		}

		@Override
		protected boolean includeWriteAttribute(Method method, String beanKey) {
			if ("setAge".equals(method.getName())) {
				return false;
			}
			return true;
		}

		@Override
		protected boolean includeOperation(Method method, String beanKey) {
			if ("dontExposeMe".equals(method.getName())) {
				return false;
			}
			return true;
		}

		@SuppressWarnings("unused")
		protected String getOperationDescription(Method method) {
			return method.getName();
		}

		@SuppressWarnings("unused")
		protected String getAttributeDescription(PropertyDescriptor propertyDescriptor) {
			return propertyDescriptor.getDisplayName();
		}

		@SuppressWarnings("unused")
		protected void populateAttributeDescriptor(Descriptor descriptor, Method getter, Method setter) {

		}

		@SuppressWarnings("unused")
		protected void populateOperationDescriptor(Descriptor descriptor, Method method) {

		}

		@SuppressWarnings({ "unused", "rawtypes" })
		protected String getDescription(String beanKey, Class beanClass) {
			return "";
		}

		@SuppressWarnings({ "unused", "rawtypes" })
		protected void populateMBeanDescriptor(Descriptor mbeanDescriptor, String beanKey, Class beanClass) {

		}
	}

}
