/***********************************************************************
 * Copyright (c) 2006-2007 The Apache Software Foundation.             *
 * All rights reserved.                                                *
 * ------------------------------------------------------------------- *
 * 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.                      *
 ***********************************************************************/

package org.apache.vysper.xmpp.protocol;

import junit.framework.TestCase;

import org.apache.vysper.xml.fragment.XMLElement;
import org.apache.vysper.xml.fragment.XMLElementVerifier;
import org.apache.vysper.xml.fragment.XMLSemanticError;
import org.apache.vysper.xmpp.addressing.Entity;
import org.apache.vysper.xmpp.addressing.EntityImpl;
import org.apache.vysper.xmpp.delivery.StanzaReceiverRelay;
import org.apache.vysper.xmpp.server.DefaultServerRuntimeContext;
import org.apache.vysper.xmpp.server.SessionState;
import org.apache.vysper.xmpp.server.TestSessionContext;
import org.apache.vysper.xmpp.stanza.IQStanza;
import org.apache.vysper.xmpp.stanza.IQStanzaType;
import org.apache.vysper.xmpp.stanza.Stanza;
import org.apache.vysper.xmpp.stanza.StanzaBuilder;
import org.apache.vysper.xmpp.stanza.XMPPCoreStanza;
import org.apache.vysper.xmpp.state.resourcebinding.BindException;

/**
 * test basic behavior of ProtocolWorker.processStanza()
 */
public class ProtocolWorkerProcessTestCase extends TestCase {
    private ProtocolWorker protocolWorker;

    private NamespaceHandlerDictionary namespaceHandlerDictionary;

    private DefaultServerRuntimeContext serverRuntimeContext;

    private TestSessionContext sessionContext;

    private static Entity serverEnitity = new EntityImpl(null, "test", null);

    private SessionStateHolder sessionStateHolder;

    @Override
    protected void setUp() throws Exception {
        super.setUp();

        protocolWorker = new ProtocolWorker();
        namespaceHandlerDictionary = new NamespaceHandlerDictionary("testNSURI");
        StanzaReceiverRelay receiverRelay = new StanzaReceiverRelay();
        serverRuntimeContext = new DefaultServerRuntimeContext(serverEnitity, receiverRelay);
        receiverRelay.setServerRuntimeContext(serverRuntimeContext);
        serverRuntimeContext.addDictionary(namespaceHandlerDictionary);
        sessionStateHolder = new SessionStateHolder();
        sessionContext = new TestSessionContext(serverRuntimeContext, sessionStateHolder);
    }

    public void testProcessUnknownStanza() {

        sessionContext.setSessionState(SessionState.AUTHENTICATED);

        Stanza stanza = new StanzaBuilder("ProtocolWorkerProcessTestCase", "testNSURI").build();
        protocolWorker.processStanza(sessionContext.getServerRuntimeContext(), sessionContext, stanza,
                sessionStateHolder);

        Stanza recordedResponse = sessionContext.getNextRecordedResponse();
        XMLElementVerifier verifier = recordedResponse.getVerifier();
        assertTrue("error", verifier.nameEquals("error"));
        assertTrue("unsupported stanza type", verifier.subElementPresent(StreamErrorCondition.UNSUPPORTED_STANZA_TYPE
                .value()));
    }

    public void testProcessStanzaNoResponse() {

        sessionContext.setSessionState(SessionState.AUTHENTICATED);

        CallTestStanzaHandler stanzaHandler = new CallTestStanzaHandler("ProtocolWorkerProcessTestCase");
        namespaceHandlerDictionary.register(stanzaHandler);

        Stanza stanza = new StanzaBuilder("ProtocolWorkerProcessTestCase", "testNSURI").build();

        protocolWorker.processStanza(sessionContext.getServerRuntimeContext(), sessionContext, stanza,
                sessionStateHolder);

        stanzaHandler.assertHandlerCalled();
        Stanza recordedResponse = sessionContext.getNextRecordedResponse();
        assertNull("no emmitter, no response", recordedResponse);
    }

    public void testProcessStanzaWithResponse() {

        sessionContext.setSessionState(SessionState.AUTHENTICATED);

        CallTestStanzaHandlerResponse stanzaHandler = new CallTestStanzaHandlerResponse("ProtocolWorkerProcessTestCase");
        namespaceHandlerDictionary.register(stanzaHandler);

        Stanza stanza = new StanzaBuilder("ProtocolWorkerProcessTestCase", "testNSURI").build();
        Stanza responseStanza = new StanzaBuilder("response").build();

        stanzaHandler.setResponseStanza(responseStanza);

        protocolWorker.processStanza(sessionContext.getServerRuntimeContext(), sessionContext, stanza,
                sessionStateHolder);

        stanzaHandler.assertHandlerCalled();
        Stanza recordedResponse = sessionContext.getNextRecordedResponse();
        assertEquals("response handled", responseStanza, recordedResponse);

        stanzaHandler.setResponseStanza(null);
        protocolWorker.processStanza(sessionContext.getServerRuntimeContext(), sessionContext, stanza,
                sessionStateHolder);
        assertNull("handler emmitted null as response", stanzaHandler.getResponseStanza());

    }

    public void testHandlerThrowProtocolException() {

        sessionContext.setSessionState(SessionState.AUTHENTICATED);

        CallTestStanzaHandler stanzaHandler = new CallTestStanzaHandler("ProtocolWorkerProcessTestCase");
        namespaceHandlerDictionary.register(stanzaHandler);
        stanzaHandler.setProtocolException(new ProtocolException("forced error"));

        Stanza stanza = new StanzaBuilder("ProtocolWorkerProcessTestCase", "testNSURI").build();

        protocolWorker.processStanza(sessionContext.getServerRuntimeContext(), sessionContext, stanza,
                sessionStateHolder);

        Stanza recordedResponse = sessionContext.getNextRecordedResponse();
        assertEquals("bad format", "error", recordedResponse.getName());
        assertTrue("closed", sessionContext.isClosed());
    }

    public void testProcessWrongStartStanza() {

        sessionContext.setSessionState(SessionState.INITIATED);

        CallTestStanzaHandler stanzaHandler = new CallTestStanzaHandler("ProtocolWorkerProcessTestCase");
        namespaceHandlerDictionary.register(stanzaHandler);

        Stanza stanza = new StanzaBuilder("ProtocolWorkerProcessTestCase", "testNSURI").build();

        protocolWorker.processStanza(sessionContext.getServerRuntimeContext(), sessionContext, stanza,
                sessionStateHolder);

        try {
            stanzaHandler.assertHandlerCalled();
            fail("handler called");
        } catch (Exception e) {
            // not called, OK
        }
        Stanza recordedResponse = sessionContext.getNextRecordedResponse();
        assertEquals("open stream", "stream", recordedResponse.getName());
        XMLElementVerifier xmlElementVerifier = recordedResponse.getVerifier();
        assertTrue("error embedded", xmlElementVerifier.subElementPresent("error"));
        XMLElement error = (XMLElement) recordedResponse.getInnerFragments().get(0);
        assertEquals("bad format", "error", error.getName());
        assertTrue("closed", sessionContext.isClosed());
    }

    public void testDetectWrongFromAddress() throws XMLSemanticError {

        Entity server = sessionContext.getServerRuntimeContext().getServerEnitity();
        sessionContext.setSessionState(SessionState.AUTHENTICATED);
        // the session is running for 'mark'
        sessionContext.setInitiatingEntity(new EntityImpl("mark", server.getDomain(), null));

        CallTestStanzaHandler stanzaHandler = new CallTestStanzaHandler("iq");
        namespaceHandlerDictionary.register(stanzaHandler);

        StanzaBuilder stanzaBuilder = StanzaBuilder.createIQStanza(null, null, IQStanzaType.GET, "test");
        // we set a different from user name 'ernest'!
        stanzaBuilder.addAttribute("from", new EntityImpl("ernest", server.getDomain(), null).getFullQualifiedName());
        stanzaBuilder.startInnerElement("query", "jabber:iq:roster").endInnerElement();

        protocolWorker.processStanza(sessionContext.getServerRuntimeContext(), sessionContext, stanzaBuilder.build(),
                sessionStateHolder);

        try {
            stanzaHandler.assertHandlerCalled();
            fail("handler called");
        } catch (Exception e) {
            // not called, OK
        }
        Stanza recordedResponse = sessionContext.getNextRecordedResponse();
        assertUnknownSenderError(recordedResponse);
    }

    public void testAllowProperFromResourceId() throws XMLSemanticError, BindException {

        Entity server = sessionContext.getServerRuntimeContext().getServerEnitity();
        sessionContext.setSessionState(SessionState.AUTHENTICATED);
        // the session is running for 'charlotte'
        sessionContext.setInitiatingEntity(new EntityImpl("charlotte", server.getDomain(), null));

        CallTestStanzaHandler stanzaHandler = new CallTestStanzaHandler("iq", "testNSURI");
        namespaceHandlerDictionary.register(stanzaHandler);

        String onlyBoundResource = sessionContext.bindResource();

        StanzaBuilder stanzaBuilder = StanzaBuilder.createIQStanza(null, null, IQStanzaType.GET, "test");
        stanzaBuilder.addAttribute("from", new EntityImpl("charlotte", server.getDomain(), onlyBoundResource)
                .getFullQualifiedName());
        stanzaBuilder.startInnerElement("query", "testNSURI").endInnerElement();

        protocolWorker.processStanza(sessionContext.getServerRuntimeContext(), sessionContext, stanzaBuilder.build(),
                sessionStateHolder);

        stanzaHandler.assertHandlerCalled();
    }

    public void testDetectWrongFromResourceId() throws XMLSemanticError, BindException {

        Entity server = sessionContext.getServerRuntimeContext().getServerEnitity();
        sessionContext.setSessionState(SessionState.AUTHENTICATED);
        // the session is running for 'charlotte'
        sessionContext.setInitiatingEntity(new EntityImpl("charlotte", server.getDomain(), null));

        CallTestStanzaHandler stanzaHandler = new CallTestStanzaHandler("iq", "testNSURI");
        namespaceHandlerDictionary.register(stanzaHandler);

        String onlyBoundResource = sessionContext.bindResource();
        String arbitraryUnboundResource = "unboundResourceID";

        StanzaBuilder stanzaBuilder = StanzaBuilder.createIQStanza(null, null, IQStanzaType.GET, "test");

        stanzaBuilder.addAttribute("from", new EntityImpl("charlotte", server.getDomain(), arbitraryUnboundResource)
                .getFullQualifiedName());
        stanzaBuilder.startInnerElement("query", "testNSURI").endInnerElement();

        protocolWorker.processStanza(sessionContext.getServerRuntimeContext(), sessionContext, stanzaBuilder.build(),
                sessionStateHolder);

        try {
            stanzaHandler.assertHandlerCalled();
            fail("handler called");
        } catch (Exception e) {
            // not called, OK
        }
        Stanza recordedResponse = sessionContext.getNextRecordedResponse();
        assertUnknownSenderError(recordedResponse);
    }

    public void testAllowBareFromEntityOnlyOnSingleBoundResource() throws XMLSemanticError, BindException {
        // from="client@vysper.org" is only allowed when a single resource is bound
        // if there is more than one resource bound in the same session, from must come 
        // fully qualified, e.g. from="client@vysper.org/resourceId"

        Entity server = sessionContext.getServerRuntimeContext().getServerEnitity();
        sessionContext.setSessionState(SessionState.AUTHENTICATED);
        // the session is running for 'lea'
        sessionContext.setInitiatingEntity(new EntityImpl("lea", server.getDomain(), null));

        CallTestStanzaHandler stanzaHandler = new CallTestStanzaHandler("iq", "testNSURI");
        namespaceHandlerDictionary.register(stanzaHandler);

        String firstBoundResource = sessionContext.bindResource();
        String secondBoundResource = sessionContext.bindResource();

        StanzaBuilder stanzaBuilder = StanzaBuilder.createIQStanza(null, null, IQStanzaType.GET, "test");
        stanzaBuilder.addAttribute("from", new EntityImpl("lea", server.getDomain(), null).getFullQualifiedName());
        stanzaBuilder.startInnerElement("query", "testNSURI").endInnerElement();

        protocolWorker.processStanza(sessionContext.getServerRuntimeContext(), sessionContext, stanzaBuilder.build(),
                sessionStateHolder);
        Stanza recordedResponse = sessionContext.getNextRecordedResponse();
        assertUnknownSenderError(recordedResponse); // not allowed, bare id without resource and two resources bound
        sessionContext.reset();

        // unbind second resource, leaving only one
        boolean noResourceRemains = sessionContext.getServerRuntimeContext().getResourceRegistry().unbindResource(
                secondBoundResource);
        assertFalse(noResourceRemains);

        // bare id allowed, only one resource is bound
        stanzaBuilder = StanzaBuilder.createIQStanza(null, null, IQStanzaType.GET, "test");
        stanzaBuilder.addAttribute("from", new EntityImpl("lea", server.getDomain(), null).getFullQualifiedName());
        stanzaBuilder.startInnerElement("query", "testNSURI").endInnerElement();
        protocolWorker.processStanza(sessionContext.getServerRuntimeContext(), sessionContext, stanzaBuilder.build(),
                sessionStateHolder);
        stanzaHandler.assertHandlerCalled();
        sessionContext.reset();

        // second resource is now invalid and cannot be used anymore in a full qualified entity
        stanzaBuilder = StanzaBuilder.createIQStanza(null, null, IQStanzaType.GET, "test");
        stanzaBuilder.addAttribute("from", new EntityImpl("lea", server.getDomain(), secondBoundResource)
                .getFullQualifiedName());
        stanzaBuilder.startInnerElement("query", "testNSURI").endInnerElement();
        protocolWorker.processStanza(sessionContext.getServerRuntimeContext(), sessionContext, stanzaBuilder.build(),
                sessionStateHolder);
        recordedResponse = sessionContext.getNextRecordedResponse();
        assertUnknownSenderError(recordedResponse);

    }

    private void assertUnknownSenderError(Stanza recordedResponse) throws XMLSemanticError {
        XMLElementVerifier verifier = recordedResponse.getVerifier();
        assertEquals("iq stanza error", "iq", recordedResponse.getName());
        IQStanza iqStanza = (IQStanza) XMPPCoreStanza.getWrapper(recordedResponse);
        assertEquals("error", iqStanza.getType());
        assertTrue("error embedded", verifier.subElementPresent("error"));
        XMLElement errorInner = recordedResponse.getSingleInnerElementsNamed("error");
        assertEquals("modify", errorInner.getAttributeValue("type"));
        XMLElementVerifier errorVerifier = errorInner.getVerifier();
        errorVerifier.subElementPresent("unknown-sender");
    }

}
