/*
 * 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.brooklyn.feed.jmx;

import static org.testng.Assert.assertEquals;
import static org.testng.Assert.assertFalse;
import static org.testng.Assert.fail;

import java.io.IOException;
import java.util.List;

import javax.management.DynamicMBean;
import javax.management.MBeanOperationInfo;
import javax.management.MBeanParameterInfo;
import javax.management.Notification;
import javax.management.NotificationListener;
import javax.management.ObjectName;
import javax.management.StandardEmitterMBean;

import org.apache.brooklyn.entity.software.base.test.jmx.GeneralisedDynamicMBean;
import org.apache.brooklyn.entity.software.base.test.jmx.JmxService;
import org.apache.brooklyn.test.Asserts;
import org.apache.brooklyn.util.collections.MutableMap;
import org.apache.brooklyn.util.exceptions.Exceptions;
import org.jclouds.util.Throwables2;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.testng.annotations.AfterMethod;
import org.testng.annotations.BeforeMethod;
import org.testng.annotations.Test;

import com.google.common.base.Function;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Lists;

public class JmxHelperTest {

    private static final Logger log = LoggerFactory.getLogger(JmxHelperTest.class);
    
    // NB: "localhost" resolves to the public-facing IP often which on the occasional box
    // (eg ubuntu-us1 at apache) makes the ports not locally accessible.  so use loopback explicitly.
    static final String LOCALHOST_NAME = "127.0.0.1";
    
    private static final int TIMEOUT_MS = 5000;
    private static final int SHORT_WAIT_MS = 250;

    private JmxService jmxService;
    private JmxHelper jmxHelper;
    
    private String objectName = "Brooklyn:type=MyTestMBean,name=myname";
    private String objectNameWithWildcard = "Brooklyn:type=MyTestMBean,name=mynam*";
    private ObjectName jmxObjectName;
    private ObjectName jmxObjectNameWithWildcard;
    private String attributeName = "myattrib";
    private String opName = "myop";
    
    @BeforeMethod(alwaysRun=true)
    public void setUp() throws Exception {
        jmxObjectName = new ObjectName(objectName);
        jmxObjectNameWithWildcard = new ObjectName(objectNameWithWildcard);
        jmxService = newJmxServiceRetrying(LOCALHOST_NAME, 5);
        jmxHelper = new JmxHelper(jmxService.getUrl());
        jmxHelper.setMinTimeBetweenReconnectAttempts(0);
        jmxHelper.connect(TIMEOUT_MS);
    }
    
    @AfterMethod(alwaysRun=true)
    public void tearDown() throws Exception {
        if (jmxHelper != null) jmxHelper.disconnect();
        if (jmxService != null) jmxService.shutdown();
        jmxHelper = null;
        jmxService = null;
    }

    @Test
    public void testGetAttribute() throws Exception {
        GeneralisedDynamicMBean mbean = jmxService.registerMBean(MutableMap.of("myattr", "myval"), objectName);
        assertEquals(jmxHelper.getAttribute(jmxObjectName, "myattr"), "myval");
    }

    @Test
    public void testGetAttributeUsingObjectNameWildcard() throws Exception {
        GeneralisedDynamicMBean mbean = jmxService.registerMBean(MutableMap.of("myattr", "myval"), objectName);
        assertEquals(jmxHelper.getAttribute(jmxObjectNameWithWildcard, "myattr"), "myval");
    }

    @Test
    public void testSetAttribute() throws Exception {
        DynamicMBean mbean = jmxService.registerMBean(MutableMap.of("myattr", "myval"), objectName);

        jmxHelper.setAttribute(jmxObjectName, "myattr", "abc");
        Object actual = jmxHelper.getAttribute(jmxObjectName, "myattr");
        assertEquals(actual, "abc");
    }

    @Test
    public void testSetAttributeUsingObjectNameWildcard() throws Exception {
        DynamicMBean mbean = jmxService.registerMBean(MutableMap.of("myattr", "myval"), objectName);

        jmxHelper.setAttribute(jmxObjectNameWithWildcard, "myattr", "abc");
        Object actual = jmxHelper.getAttribute(jmxObjectName, "myattr");
        assertEquals(actual, "abc");
    }

    @Test
    public void testInvokeOperationWithNoArgs() throws Exception {
        final String opReturnVal = "my result";
        MBeanOperationInfo opInfo = new MBeanOperationInfo(opName, "my descr", new MBeanParameterInfo[0], String.class.getName(), MBeanOperationInfo.ACTION);
        Function<Object[], String> opImpl = new Function<Object[], String>() {
            @Override public String apply(Object[] args) {
                assertEquals(args.length, 0, "args="+args);
                return opReturnVal;
            }
        };
        GeneralisedDynamicMBean mbean = jmxService.registerMBean(ImmutableMap.of(), ImmutableMap.of(opInfo, opImpl), objectName);
        
        assertEquals(jmxHelper.operation(objectName, opName), opReturnVal);
    }

    @Test
    public void testInvokeOperationUsingObjectNameWildcard() throws Exception {
        final String opReturnVal = "my result";
        MBeanOperationInfo opInfo = new MBeanOperationInfo(opName, "my descr", new MBeanParameterInfo[0], String.class.getName(), MBeanOperationInfo.ACTION);
        Function<Object[], String> opImpl = new Function<Object[], String>() {
            @Override public String apply(Object[] args) {
                assertEquals(args.length, 0, "args="+args);
                return opReturnVal;
            }
        };
        GeneralisedDynamicMBean mbean = jmxService.registerMBean(ImmutableMap.of(), ImmutableMap.of(opInfo, opImpl), objectName);
        
        assertEquals(jmxHelper.operation(objectNameWithWildcard, opName), opReturnVal);
    }

    @Test
    public void testInvokeOperationWithArgs() throws Exception {
        final String opReturnPrefix = "my result prefix/";
        String opParam1 = "my param 1";
        MBeanOperationInfo opInfo = new MBeanOperationInfo(
                opName, 
                "my descr", 
                new MBeanParameterInfo[] {new MBeanParameterInfo("myParam1", String.class.getName(), "my param1 descr")}, 
                String.class.getName(), 
                MBeanOperationInfo.ACTION);
        Function<Object[],String> opImpl = new Function<Object[],String>() {
            @Override
            public String apply(Object[] input) {
                return opReturnPrefix+input[0];
            }
        };
        GeneralisedDynamicMBean mbean = jmxService.registerMBean(ImmutableMap.of(), ImmutableMap.of(opInfo, opImpl), objectName);
        
        assertEquals(jmxHelper.operation(objectName, opName, opParam1), opReturnPrefix+opParam1);
    }

    @Test
    public void testReconnectsOnJmxServerTemporaryFailure() throws Exception {
        int port = jmxService.getJmxPort();
        GeneralisedDynamicMBean mbean = jmxService.registerMBean(MutableMap.of("myattr", "myval"), objectName);
        assertEquals(jmxHelper.getAttribute(jmxObjectName, "myattr"), "myval");
        
        // Simulate temporary network-failure
        jmxService.shutdown();

        // Ensure that we have a failed query while the "network is down"         
        try {
            jmxHelper.getAttribute(jmxObjectName, attributeName);
            fail();
        } catch (Exception e) {
            if (Throwables2.getFirstThrowableOfType(e, IOException.class) == null) {
                throw e;
            }
        }

        // Simulate the network restarting
        jmxService = new JmxService(LOCALHOST_NAME, port);
        
        GeneralisedDynamicMBean mbean2 = jmxService.registerMBean(MutableMap.of("myattr", "myval2"), objectName);
        assertEquals(jmxHelper.getAttribute(jmxObjectName, "myattr"), "myval2");
    }
    
    @Test(expectedExceptions = {IllegalStateException.class})
    public void testJmxCheckInstanceExistsEventuallyThrowsIfNotFound() throws Exception {
        jmxHelper.assertMBeanExistsEventually(new ObjectName("Brooklyn:type=DoesNotExist,name=doesNotExist"), 1L);
    }

    @Test
    public void testJmxObjectCheckExistsEventuallyReturnsIfFoundImmediately() throws Exception {
        GeneralisedDynamicMBean mbean = jmxService.registerMBean(objectName);
        jmxHelper.assertMBeanExistsEventually(jmxObjectName, 1L);
    }

    @Test
    public void testJmxObjectCheckExistsEventuallyTakingLongReturnsIfFoundImmediately() throws Exception {
        GeneralisedDynamicMBean mbean = jmxService.registerMBean(objectName);
        jmxHelper.assertMBeanExistsEventually(jmxObjectName, 1L);
    }

    @Test
    public void testJmxObjectCheckExistsEventuallyReturnsIfCreatedDuringPolling() throws Exception {
        Thread t = new Thread(new Runnable() {
                @Override
                public void run() {
                    try {
                        Thread.sleep(SHORT_WAIT_MS);
                        GeneralisedDynamicMBean mbean = jmxService.registerMBean(objectName);
                    } catch (InterruptedException e) {
                        return; // graceful return
                    } catch (Exception e) {
                        throw Exceptions.propagate(e);
                    }
                }});
        try {
            t.start();
            
            jmxHelper.assertMBeanExistsEventually(jmxObjectName, TIMEOUT_MS);
        } finally {
            t.interrupt();
            t.join(TIMEOUT_MS);
            assertFalse(t.isAlive());
        }        
    }

    @Test
    public void testSubscribeToJmxNotificationsDirectlyWithJmxHelper() throws Exception {
        StandardEmitterMBean mbean = jmxService.registerMBean(ImmutableList.of("one"), objectName);
        int sequence = 0;
        final List<Notification> received = Lists.newArrayList();

        jmxHelper.addNotificationListener(jmxObjectName, new NotificationListener() {
            @Override
            public void handleNotification(Notification notif, Object callback) {
                received.add(notif);
            }});
                    

        final Notification notif = sendNotification(mbean, "one", sequence++, "abc");

        Asserts.succeedsEventually(ImmutableMap.of("timeout", TIMEOUT_MS), new Runnable() {
            @Override
            public void run() {
                assertEquals(received.size(), 1);
                assertNotificationsEqual(received.get(0), notif);
            }});
    }

    // Visual-inspection test that LOG.warn happens only once; TODO setup a listener to the logging output
    @Test
    public void testMBeanNotFoundLoggedOnlyOncePerUrl() throws Exception {
        ObjectName wrongObjectName = new ObjectName("DoesNotExist:type=DoesNotExist");

        // Expect just one log message about:
        //     JMX object DoesNotExist:type=DoesNotExist not found at service:jmx:rmi://localhost:1099/jndi/rmi://localhost:9001/jmxrmi"
        for (int i = 0; i < 10; i++) {
            jmxHelper.findMBean(wrongObjectName);
        }

        jmxService.shutdown();
        jmxHelper.disconnect();
        
        jmxService = newJmxServiceRetrying(LOCALHOST_NAME, 5);
        jmxHelper = new JmxHelper(jmxService.getUrl());
        jmxHelper.connect();
        
        // Expect just one log message about:
        //     JMX object DoesNotExist:type=DoesNotExist not found at service:jmx:rmi://localhost:1099/jndi/rmi://localhost:9001/jmxrmi"
        for (int i = 0; i < 10; i++) {
            jmxHelper.findMBean(wrongObjectName);
        }
    }

    private JmxService newJmxServiceRetrying(String host, int retries) throws Exception {
        Exception lastexception = null;
        for (int i = 0; i < retries; i++) {
            try {
                return new JmxService(host, (int)(11000+(500*Math.random())));
            } catch (Exception e) {
                log.debug("Unable to create JMX service during test - "+retries+" retries remaining");
                lastexception = e;
            }
        }
        throw lastexception;
    }

    private Notification sendNotification(StandardEmitterMBean mbean, String type, long seq, Object userData) {
        Notification notif = new Notification(type, mbean, seq);
        notif.setUserData(userData);
        mbean.sendNotification(notif);
        return notif;
    }
    
    private void assertNotificationsEqual(Notification n1, Notification n2) {
        assertEquals(n1.getType(), n2.getType());
        assertEquals(n1.getSequenceNumber(), n2.getSequenceNumber());
        assertEquals(n1.getUserData(), n2.getUserData());
        assertEquals(n1.getTimeStamp(), n2.getTimeStamp());
        assertEquals(n1.getMessage(), n2.getMessage());
    }
}
