/*
 *
 * 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.qpid.interop.coordinator;

import junit.framework.Test;
import junit.framework.TestResult;

import org.apache.log4j.Logger;

import org.apache.qpid.interop.coordinator.sequencers.DistributedTestSequencer;
import org.apache.qpid.interop.coordinator.sequencers.FanOutTestSequencer;
import org.apache.qpid.interop.coordinator.sequencers.InteropTestSequencer;
import org.apache.qpid.util.ConversationFactory;

import uk.co.thebadgerset.junit.extensions.WrappedSuiteTestDecorator;

import javax.jms.Connection;
import javax.jms.JMSException;
import javax.jms.Message;
import javax.jms.MessageListener;

import java.util.Collection;
import java.util.Iterator;
import java.util.Set;

/**
 * FanOutTestDecorator is an {@link DistributedTestDecorator} that runs one test client in the sender role, and the remainder
 * in the receivers role. It also has the capability to listen for new test cases joining the test beyond the initial start
 * point. This feature can be usefull when experimenting with adding more load, in the form of more test clients, to assess
 * its impact on a running test.
 *
 * <p><table id="crc"><caption>CRC Card</caption>
 * <tr><th> Responsibilities <th> Collaborations
 * <tr><td> Execute coordinated test cases. <td> {@link DistributedTestCase}
 * <tr><td> Accept test clients joining a running test.
 * </table>
 */
public class FanOutTestDecorator extends DistributedTestDecorator implements MessageListener
{
    /** Used for debugging. */
    private static final Logger log = Logger.getLogger(FanOutTestDecorator.class);

    /** Holds the currently running test case. */
    DistributedTestCase currentTest = null;

    /**
     * Creates a wrapped suite test decorator from another one.
     *
     * @param suite               The test suite.
     * @param availableClients    The list of all clients that responded to the compulsory invite.
     * @param controlConversation The conversation helper for the control level, test coordination conversation.
     * @param controlConnection   The connection that the coordination messages are sent over.
     */
    public FanOutTestDecorator(WrappedSuiteTestDecorator suite, Set<TestClientDetails> availableClients,
        ConversationFactory controlConversation, Connection controlConnection)
    {
        super(suite, availableClients, controlConversation, controlConnection);

        log.debug("public DistributedTestDecorator(WrappedSuiteTestDecorator suite, Set<TestClientDetails> allClients = "
            + availableClients + ", ConversationHelper controlConversation = " + controlConversation + "): called");

        testSuite = suite;
        allClients = availableClients;
        conversationFactory = controlConversation;
        connection = controlConnection;
    }

    /**
     * Broadcasts a test invitation and accepts enlists from participating clients. The wrapped test cases are run
     * with one test client in the sender role, and the remaining test clients in the receiving role.
     *
     * <p/>Any JMSExceptions during the invite/enlist conversation will be allowed to fall through as runtime
     * exceptions, resulting in the non-completion of the test run.
     *
     * @param testResult The the results object to monitor the test results with.
     *
     * @todo Better error recovery for failure of the invite/enlist conversation could be added.
     */
    public void run(TestResult testResult)
    {
        log.debug("public void run(TestResult testResult): called");

        Collection<Test> tests = testSuite.getAllUnderlyingTests();

        // Listen for late joiners on the control topic.
        try
        {
            conversationFactory.getSession().createConsumer(controlTopic).setMessageListener(this);
        }
        catch (JMSException e)
        {
            throw new RuntimeException("Unable to set up the message listener on the control topic.", e);
        }

        // Run all of the test cases in the test suite.
        for (Test test : tests)
        {
            DistributedTestCase coordTest = (DistributedTestCase) test;

            // Get all of the clients able to participate in the test.
            Set<TestClientDetails> enlists = signupClients(coordTest);

            // Check that there were some clients available.
            if (enlists.size() == 0)
            {
                throw new RuntimeException("No clients to test with");
            }

            // Create a distributed test sequencer for the test.
            DistributedTestSequencer sequencer = getDistributedTestSequencer();

            // Set up the first client in the sender role, and the remainder in the receivers role.
            Iterator<TestClientDetails> clients = enlists.iterator();
            sequencer.setSender(clients.next());

            while (clients.hasNext())
            {
                // Set the sending and receiving client details on the test case.
                sequencer.setReceiver(clients.next());
            }

            // Pass down the connection to hold the coordinating conversation over.
            sequencer.setConversationFactory(conversationFactory);

            // If the current test case is a drop-in test, set it up as the currently running test for late joiners to
            // add in to. Otherwise the current test field is set to null, to indicate that late joiners are not allowed.
            currentTest = (coordTest instanceof DropInTest) ? coordTest : null;

            // Execute the test case.
            coordTest.setTestSequencer(sequencer);
            coordTest.run(testResult);

            currentTest = null;
        }
    }

    /**
     * Should provide the distributed test sequencer to pass to {@link org.apache.qpid.test.framework.FrameworkBaseCase}
     * tests.
     *
     * @return A distributed test sequencer.
     */
    public DistributedTestSequencer getDistributedTestSequencer()
    {
        return new FanOutTestSequencer();
    }

    /**
     * Listens to incoming messages on the control topic. If the messages are 'join' messages, signalling a new
     * test client wishing to join the current test, then the new client will be added to the current test in the
     * receivers role.
     *
     * @param message The incoming control message.
     */
    public void onMessage(Message message)
    {
        try
        {
            // Check if the message is from a test client attempting to join a running test, and join it to the current
            // test case if so.
            if (message.getStringProperty("CONTROL_TYPE").equals("JOIN") && (currentTest != null))
            {
                ((DropInTest) currentTest).lateJoin(message);
            }
        }
        // There is not a lot can be done with this error, so it is deliberately ignored.
        catch (JMSException e)
        {
            log.debug("Unable to process message:" + message);
        }
    }

    /**
     * Prints a string summarizing this test decorator, mainly for debugging purposes.
     *
     * @return String representation for debugging purposes.
     */
    public String toString()
    {
        return "FanOutTestDecorator: [ testSuite = " + testSuite + " ]";
    }
}
