/**
 * 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.openejb.server.discovery;

import junit.framework.TestCase;
import org.apache.openejb.monitoring.LocalMBeanServer;
import org.apache.openejb.monitoring.ManagedMBean;
import org.apache.openejb.monitoring.ObjectNameBuilder;
import org.apache.openejb.server.DiscoveryListener;
import org.apache.openejb.server.DiscoveryRegistry;
import org.apache.openejb.util.Join;

import javax.management.MBeanServer;
import javax.management.ObjectName;
import java.io.UnsupportedEncodingException;
import java.net.URI;
import java.util.ArrayList;
import java.util.List;
import java.util.Properties;
import java.util.Set;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.logging.ConsoleHandler;
import java.util.logging.ErrorManager;
import java.util.logging.Filter;
import java.util.logging.Formatter;
import java.util.logging.Handler;
import java.util.logging.Level;
import java.util.logging.LogRecord;
import java.util.logging.Logger;

/**
 * @version $Rev$ $Date$
 */
public class MultipointDiscoveryAgentTest extends TestCase {


    public void test() throws Exception {
        if ("true".equals(System.getProperty("skipMulticastTests"))) {
            Logger.getLogger(this.getClass().getName()).warning("Skipping MulticastTest " + this.getClass().getName());
            return;
        }

        final URI testService = new URI("green://localhost:0");

        final String[] names = {"red"};
        final int PEERS = names.length;

        final CountDownLatch[] latches = {
            new CountDownLatch(PEERS + 1),
            new CountDownLatch(PEERS + 1)
        };

        final DiscoveryListener listener = new DiscoveryListener() {
            public void serviceAdded(final URI service) {
                System.out.println("added = " + service);
                if (testService.equals(service)) {
                    latches[0].countDown();
                }
            }

            public void serviceRemoved(final URI service) {
                System.out.println("removed = " + service);
                if (testService.equals(service)) {
                    latches[1].countDown();
                }
            }
        };

        final List<Node> nodes = new ArrayList<Node>();
        final Node root = new Node(0, listener, "root");

        nodes.add(root);

        for (final String name : names) {
            final Node node = new Node(0, listener, name, root.getURI());
            nodes.add(node);
        }

        final Node owner = nodes.get(nodes.size() / 2);


        for (int i = 0; i < 3; i++) {
            latches[0] = new CountDownLatch(PEERS + 1);
            latches[1] = new CountDownLatch(PEERS + 1);

            // OK, do the broadcast
            owner.getRegistry().registerService(testService);

            // Notification should have reached all participants
            assertTrue("round=" + i + ". Add failed", latches[0].await(30, TimeUnit.SECONDS));

            owner.getRegistry().unregisterService(testService);

            assertTrue("round=" + i + ". Remove failed", latches[1].await(60, TimeUnit.SECONDS));

            for (final Node node : nodes) {
                final Set<URI> services = node.getRegistry().getServices();
                assertEquals("round=" + i + ". Service retained", 0, services.size());
            }

        }
    }

    public void _debug() throws Exception {
        System.setProperty("log4j.category.OpenEJB.server.discovery", "debug");

        System.setProperty("log4j.appender.C.layout", "org.apache.log4j.PatternLayout");
        System.setProperty("log4j.appender.C.layout.ConversionPattern", "%d - %m%n");

        final URI greenService = new URI("green://localhost:5555");
        final Node green = new Node(5555, new Listener("green"), true, "green", 100, 2);

        green.getRegistry().registerService(greenService);

//        launch(green, "blue", 4444);
//        launch(green, "red", 6666);
//        launch(green, "yellow", 8888);
        final Node orange = launch(green, "orange", 7777);

        Thread.sleep(500000);

        orange.getAgent().stop();

        Thread.sleep(5000);

    }

    private Node launch(final Node green, final String color, final int port) throws Exception {
        final URI orangeService = new URI(color + "://localhost:" + port);
        final Node orange = new Node(port, new Listener(color), color, green.getURI());
        orange.getRegistry().registerService(orangeService);
        Thread.sleep(100);
        return orange;
    }

    public static class Node {
        private final MultipointDiscoveryAgent agent;
        private final DiscoveryRegistry registry;
        private final String name;

        public Node(final int p, final DiscoveryListener listener, String name, URI... uris) throws Exception {
            this(p, listener, false, name, 100, 2, uris);
        }

        public Node(final int p, final DiscoveryListener listener, final boolean debug, String name, int heartRate, int maxMissHeartBeats, URI... uris) throws Exception {
            this.agent = new MultipointDiscoveryAgent(debug, name);
            this.name = name;
            final Properties props = new Properties();
            props.put("port", p + "");

            props.put("initialServers", Join.join(",", uris));
            props.put("max_missed_heartbeats", "1");
            props.put("heart_rate", "" + heartRate);
            props.put("max_missed_heartbeats", "" + maxMissHeartBeats);
            agent.init(props);

            this.registry = new DiscoveryRegistry(agent);
            this.registry.addDiscoveryListener(listener);


            agent.start();

//            final ObjectNameBuilder jmxName = new ObjectNameBuilder("openejb.management");
//            jmxName.set("type", "ServerService");
//            jmxName.set("name", "multipoint");
//            jmxName.set("port", Integer.toString(agent.getPort()));
//
//            MBeanServer server = LocalMBeanServer.get();
//            try {
//                final ObjectName objectName = jmxName.build();
//                server.registerMBean(new ManagedMBean(agent), objectName);
//            } catch (Exception e) {
//                e.printStackTrace();
//            }

        }

        public MultipointDiscoveryAgent getAgent() {
            return agent;
        }

        public DiscoveryRegistry getRegistry() {
            return registry;
        }

        public int getPort() {
            return agent.getPort();
        }

        public URI getURI() {
            return URI.create("conn://localhost:" + getPort() + "/" + name);
        }
    }

    private static class Listener implements DiscoveryListener {
        private final String name;

        public String getName() {
            return name;
        }

        private Listener(final String name) {
            this.name = name;
        }

        public void serviceAdded(final URI service) {
//            System.out.printf("[%s] added = %s\n", name, service);
        }

        public void serviceRemoved(final URI service) {
//            System.out.printf("[%s] removed = %s\n", name, service);
        }

        @Override
        public String toString() {
            return name;
        }
    }


    public static interface LogRecordFilter {
        public boolean accept(LogRecord record);
    }

    public static class FilteredHandler extends Handler {

        private final Handler handler;
        private final LogRecordFilter filter;

        public FilteredHandler(final Handler handler, LogRecordFilter filter) {
            this.handler = handler;
            this.filter = filter;
        }

        @Override
        public void publish(final LogRecord record) {
            handler.publish(record);
//            if (filter.accept(record)) {
//            }
        }

        @Override
        public void flush() {
            handler.flush();
        }

        @Override
        public void close() throws SecurityException {
            handler.close();
        }

        @Override
        public void setFormatter(final Formatter newFormatter) throws SecurityException {
            handler.setFormatter(newFormatter);
        }

        @Override
        public Formatter getFormatter() {
            return handler.getFormatter();
        }

        @Override
        public void setEncoding(final String encoding) throws SecurityException, UnsupportedEncodingException {
            handler.setEncoding(encoding);
        }

        @Override
        public String getEncoding() {
            return handler.getEncoding();
        }

        @Override
        public void setFilter(final Filter newFilter) throws SecurityException {
            handler.setFilter(newFilter);
        }

        @Override
        public Filter getFilter() {
            return handler.getFilter();
        }

        @Override
        public void setErrorManager(final ErrorManager em) {
            handler.setErrorManager(em);
        }

        @Override
        public ErrorManager getErrorManager() {
            return handler.getErrorManager();
        }

        @Override
        public void setLevel(final Level newLevel) throws SecurityException {
            handler.setLevel(newLevel);
        }

        @Override
        public Level getLevel() {
            return handler.getLevel();
        }

        @Override
        public boolean isLoggable(final LogRecord record) {
            return handler.isLoggable(record);
        }
    }
}
