package org.jolokia.backend.executor;

/*
 * Copyright 2009-2013 Roland Huss
 *
 * 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.
 */

import java.io.IOException;
import java.util.*;

import javax.management.*;

import org.testng.annotations.BeforeMethod;
import org.testng.annotations.Test;

import static org.testng.Assert.*;

/**
 * @author roland
 * @since 23.01.13
 */
public class AbstractMBeanServerExecutorTest {

    TestExecutor executor;

    @BeforeMethod
    public void setup() throws MalformedObjectNameException, NotCompliantMBeanException, InstanceAlreadyExistsException, MBeanRegistrationException, InterruptedException, InstanceNotFoundException, IOException {
        executor = new TestExecutor();
    }

    @Test
    public void jolokiaServer() throws MalformedObjectNameException, NotCompliantMBeanException, InstanceAlreadyExistsException, MBeanRegistrationException {
        assertNotNull(executor.getJolokiaMBeanServer());

        AbstractMBeanServerExecutor executorNull = new AbstractMBeanServerExecutor() {
            @Override
            protected Set<MBeanServerConnection> getMBeanServers() {
                return null;
            }
        };
        assertNull(executorNull.getJolokiaMBeanServer());
    }

    @Test
    public void eachNull() throws MalformedObjectNameException, NotCompliantMBeanException, InstanceAlreadyExistsException, MBeanException, IOException, ReflectionException {
        executor.each(null, new MBeanServerExecutor.MBeanEachCallback() {
            public void callback(MBeanServerConnection pConn, ObjectName pName) throws ReflectionException, InstanceNotFoundException, IOException, MBeanException {
                checkHiddenMBeans(pConn, pName);
            }
        });
    }


    @Test
    public void eachObjectName() throws MalformedObjectNameException, MBeanException, IOException, ReflectionException, NotCompliantMBeanException, InstanceAlreadyExistsException {
        for (final ObjectName name : new ObjectName[] { new ObjectName("test:type=one"), new ObjectName("test:type=two") }) {
            executor.each(name,new MBeanServerExecutor.MBeanEachCallback() {
                public void callback(MBeanServerConnection pConn, ObjectName pName) throws ReflectionException, InstanceNotFoundException, IOException, MBeanException {
                assertEquals(pName,name);
                    checkHiddenMBeans(pConn,pName);
                }
            });
        }
    }

    @Test
    public void updateChangeTest() throws MalformedObjectNameException, NotCompliantMBeanException, InstanceAlreadyExistsException, MBeanRegistrationException, InstanceNotFoundException, InterruptedException, IOException {
        executor.registerForMBeanNotifications();
        try {
            assertTrue(executor.hasMBeansListChangedSince(0),"updatedSince: When 0 is given, always return true");
            long time = currentTime();
            assertFalse(executor.hasMBeansListChangedSince(time), "No update yet");
            for (int id = 1; id <=2; id++) {
                time = currentTime();
                executor.addMBean(id);
                try {
                    assertTrue(executor.hasMBeansListChangedSince(0),"updatedSince: For 0, always return true");
                    assertTrue(executor.hasMBeansListChangedSince(time),"MBean has been added in the same second, hence it has been updated");
                    // Wait at a least a second
                    time = currentTime() + 1;
                    assertFalse(executor.hasMBeansListChangedSince(time),"No updated since the last call");
                } finally {
                    executor.rmMBean(id);
                }
            }
        } finally {
            executor.unregisterFromMBeanNotifications();
        }
    }

    @Test
    public void destroyWithoutPriorRegistration() throws NoSuchFieldException, IllegalAccessException {
        // Should always work, even when no registration has happened. Non exisiting listeners will be simplu ignored, since we didnt do any registration before
        executor.unregisterFromMBeanNotifications();
    }

    private long currentTime() {
        return System.currentTimeMillis() / 1000;
    }

    @Test
    public void call() throws MalformedObjectNameException, MBeanException, InstanceAlreadyExistsException, NotCompliantMBeanException, IOException, ReflectionException, AttributeNotFoundException, InstanceNotFoundException {
        String name = getAttribute(executor,"test:type=one","Name");
        assertEquals(name,"jolokia");
    }

    private String getAttribute(AbstractMBeanServerExecutor pExecutor, String name, String attribute) throws IOException, ReflectionException, MBeanException, MalformedObjectNameException, AttributeNotFoundException, InstanceNotFoundException {
        return (String) pExecutor.call(new ObjectName(name),new MBeanServerExecutor.MBeanAction<Object>() {
                public Object execute(MBeanServerConnection pConn, ObjectName pName, Object... extraArgs) throws ReflectionException, InstanceNotFoundException, IOException, MBeanException, AttributeNotFoundException {
                    return pConn.getAttribute(pName, (String) extraArgs[0]);
                }
            },attribute);
    }

    @Test(expectedExceptions = InstanceNotFoundException.class,expectedExceptionsMessageRegExp = ".*test:type=bla.*")
    public void callWithInvalidObjectName() throws MalformedObjectNameException, NotCompliantMBeanException, InstanceAlreadyExistsException, MBeanException, IOException, ReflectionException, AttributeNotFoundException, InstanceNotFoundException {
        getAttribute(executor,"test:type=bla","Name");
    }

    @Test(expectedExceptions = AttributeNotFoundException.class,expectedExceptionsMessageRegExp = ".*Bla.*")
    public void callWithInvalidAttributeName() throws MalformedObjectNameException, NotCompliantMBeanException, InstanceAlreadyExistsException, MBeanException, IOException, ReflectionException, AttributeNotFoundException, InstanceNotFoundException {
        getAttribute(executor,"test:type=one","Bla");
    }

    @Test
    public void queryNames() throws IOException, MalformedObjectNameException, NotCompliantMBeanException, InstanceAlreadyExistsException, MBeanRegistrationException {
        Set<ObjectName> names = executor.queryNames(null);
        assertTrue(names.contains(new ObjectName("test:type=one")));
        assertTrue(names.contains(new ObjectName("test:type=two")));
        assertEquals(names.size(), 3);
    }

    private void checkHiddenMBeans(MBeanServerConnection pConn, ObjectName pName) throws MBeanException, InstanceNotFoundException, ReflectionException, IOException {
        try {
            if (!pName.equals(new ObjectName("JMImplementation:type=MBeanServerDelegate"))) {
                assertEquals(pConn.getAttribute(pName,"Name"),"jolokia");
            }
        } catch (AttributeNotFoundException e) {
            fail("Name should be accessible on all MBeans");
        } catch (MalformedObjectNameException e) {
            // wont happen
        }

        try {
            pConn.getAttribute(pName, "Age");
            fail("No access to hidden MBean allowed");
        } catch (AttributeNotFoundException exp) {
            // Expected
        }
    }
    class TestExecutor extends AbstractMBeanServerExecutor {
        private       MBeanServer                jolokiaMBeanServer;
        private final Set<MBeanServerConnection> servers;
        private final MBeanServer                otherMBeanServer;

        private Testing jOne = new Testing(), oTwo = new Testing();
        private Hidden hidden = new Hidden();

        TestExecutor() throws MalformedObjectNameException, NotCompliantMBeanException, InstanceAlreadyExistsException, MBeanRegistrationException, InterruptedException, InstanceNotFoundException, IOException {

            jolokiaMBeanServer = MBeanServerFactory.newMBeanServer();
            otherMBeanServer = MBeanServerFactory.newMBeanServer();
            servers = new LinkedHashSet<MBeanServerConnection>(Arrays.asList(jolokiaMBeanServer, otherMBeanServer));

            jolokiaMBeanServer.registerMBean(jOne, new ObjectName("test:type=one"));
            otherMBeanServer.registerMBean(hidden, new ObjectName("test:type=one"));
            otherMBeanServer.registerMBean(oTwo, new ObjectName("test:type=two"));

        }

        @Override
        protected Set<MBeanServerConnection> getMBeanServers() {
            return servers;
        }

        @Override
        protected MBeanServerConnection getJolokiaMBeanServer() {
            return jolokiaMBeanServer;
        }

        void addMBean(int id) throws MalformedObjectNameException, NotCompliantMBeanException, InstanceAlreadyExistsException, MBeanRegistrationException {
            MBeanServer server = getMBeanServerShuffled(id);
            server.registerMBean(new Testing(),new ObjectName("test:type=update,id=" + id));
        }

        void rmMBean(int id) throws MalformedObjectNameException, MBeanRegistrationException, InstanceNotFoundException {
            MBeanServer server = getMBeanServerShuffled(id);
            server.unregisterMBean(new ObjectName("test:type=update,id=" + id));
        }

        private MBeanServer getMBeanServerShuffled(int pId) {
            if (pId % 2 == 0) {
                return jolokiaMBeanServer;
            } else {
                return otherMBeanServer;
            }
        }
    }

    public interface TestingMBean {
        String getName();
    }

    public static class Testing implements TestingMBean {

        public String getName() {
            return "jolokia";
        }
    }

    public interface HiddenMBean {
        int getAge();
    }

    public static class Hidden implements HiddenMBean {

        public int getAge() {
            return 1;
        }
    }
}
