/*
 *  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.vysper.xmpp.modules.extension.xep0045_muc.handler;

import java.util.ArrayList;
import java.util.List;

import org.apache.vysper.xml.fragment.Attribute;
import org.apache.vysper.xml.fragment.XMLElement;
import org.apache.vysper.xml.fragment.XMLElementBuilder;
import org.apache.vysper.xml.fragment.XMLElementVerifier;
import org.apache.vysper.xmpp.addressing.Entity;
import org.apache.vysper.xmpp.modules.extension.xep0045_muc.TestSessionContext;
import org.apache.vysper.xmpp.modules.extension.xep0045_muc.model.Affiliation;
import org.apache.vysper.xmpp.modules.extension.xep0045_muc.model.Occupant;
import org.apache.vysper.xmpp.modules.extension.xep0045_muc.model.Role;
import org.apache.vysper.xmpp.modules.extension.xep0045_muc.model.Room;
import org.apache.vysper.xmpp.modules.extension.xep0045_muc.model.RoomType;
import org.apache.vysper.xmpp.modules.extension.xep0045_muc.stanzas.History;
import org.apache.vysper.xmpp.modules.extension.xep0045_muc.stanzas.Password;
import org.apache.vysper.xmpp.modules.extension.xep0045_muc.stanzas.X;
import org.apache.vysper.xmpp.protocol.NamespaceURIs;
import org.apache.vysper.xmpp.protocol.ProtocolException;
import org.apache.vysper.xmpp.protocol.ResponseStanzaContainer;
import org.apache.vysper.xmpp.protocol.StanzaHandler;
import org.apache.vysper.xmpp.server.SessionContext;
import org.apache.vysper.xmpp.stanza.MessageStanza;
import org.apache.vysper.xmpp.stanza.MessageStanzaType;
import org.apache.vysper.xmpp.stanza.Stanza;
import org.apache.vysper.xmpp.stanza.StanzaBuilder;
import org.apache.vysper.xmpp.stanza.StanzaErrorCondition;
import org.apache.vysper.xmpp.stanza.StanzaErrorType;

/**
 */
public class EnterRoomTestCase extends AbstractMUCHandlerTestCase {

    private Stanza enterRoom(Entity occupantJid, Entity roomJid) throws ProtocolException {
        return enterRoom(occupantJid, roomJid, null, null, false);
    }

    private Stanza enterRoom(Entity occupantJid, Entity roomJid, String password, History history, boolean oldProtocol)
            throws ProtocolException {
        SessionContext userSessionContext;
        if (occupantJid.equals(OCCUPANT1_JID)) {
            userSessionContext = sessionContext;
        } else {
            userSessionContext = sessionContext2;
        }

        StanzaBuilder stanzaBuilder = StanzaBuilder.createPresenceStanza(occupantJid, roomJid, null, null, null, null);
        if (!oldProtocol) {
            List<XMLElement> xInnerElms = new ArrayList<XMLElement>();
            if (password != null) {
                xInnerElms.add(new Password(password));
            }
            if (history != null) {
                xInnerElms.add(history);
            }
            stanzaBuilder.addPreparedElement(new X(xInnerElms));
        }
        Stanza presenceStanza = stanzaBuilder.build();
        ResponseStanzaContainer container = handler.execute(presenceStanza, userSessionContext
                .getServerRuntimeContext(), true, userSessionContext, null);
        if (container != null) {
            return container.getResponseStanza();
        } else {
            return null;
        }
    }

    protected TestSessionContext sessionContext2;

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

        sessionContext2 = TestSessionContext.createWithStanzaReceiverRelayAuthenticated();
        sessionContext2.setInitiatingEntity(OCCUPANT2_JID);
    }

    @Override
    protected StanzaHandler createHandler() {
        return new MUCPresenceHandler(conference);
    }

    public void testEnterExistingRoom() throws Exception {
        Room room = conference.findRoom(ROOM1_JID);
        assertEquals(0, room.getOccupantCount());

        enterRoom(OCCUPANT1_JID, ROOM1_JID_WITH_NICK);

        assertEquals(1, room.getOccupantCount());
        Occupant occupant = room.getOccupants().iterator().next();

        assertEquals(OCCUPANT1_JID, occupant.getJid());
        assertEquals("nick", occupant.getNick());
    }

    public void testEnterWithGroupchatProtocol() throws Exception {
        Room room = conference.findRoom(ROOM1_JID);
        assertEquals(0, room.getOccupantCount());

        // enter using the old groupchat protocol
        enterRoom(OCCUPANT1_JID, ROOM1_JID_WITH_NICK, null, null, true);

        assertEquals(1, room.getOccupantCount());
        Occupant occupant = room.getOccupants().iterator().next();

        assertEquals(OCCUPANT1_JID, occupant.getJid());
        assertEquals("nick", occupant.getNick());
    }

    public void testEnterAsAdmin() throws Exception {
        Room room = conference.findRoom(ROOM1_JID);
        room.getAffiliations().add(OCCUPANT1_JID, Affiliation.Admin);

        enterRoom(OCCUPANT1_JID, ROOM1_JID_WITH_NICK);

        Occupant occupant = room.getOccupants().iterator().next();

        assertEquals(Affiliation.Admin, occupant.getAffiliation());
    }

    public void testEnterAsOutcast() throws Exception {
        Room room = conference.findRoom(ROOM1_JID);
        room.getAffiliations().add(OCCUPANT1_JID, Affiliation.Outcast);

        Stanza error = enterRoom(OCCUPANT1_JID, ROOM1_JID_WITH_NICK);
        assertPresenceErrorStanza(error, ROOM1_JID, OCCUPANT1_JID, StanzaErrorType.AUTH, StanzaErrorCondition.FORBIDDEN);

        assertEquals(0, room.getOccupantCount());
    }

    public void testEnterAsNonMember() throws Exception {
        Room room = conference.createRoom(ROOM2_JID, "Room", RoomType.MembersOnly);

        Stanza error = enterRoom(OCCUPANT1_JID, ROOM2_JID_WITH_NICK);
        assertPresenceErrorStanza(error, ROOM2_JID, OCCUPANT1_JID, StanzaErrorType.AUTH, StanzaErrorCondition.REGISTRATION_REQUIRED);

        assertEquals(0, room.getOccupantCount());
    }

    public void testEnterRoomWithDuplicateNick() throws Exception {
        Room room1 = conference.findRoom(ROOM1_JID);
        room1.setRewriteDuplicateNick(false); // do not rewrite existing nick, second join will fail
        assertNull(enterRoom(OCCUPANT1_JID, ROOM1_JID_WITH_NICK)); // 1st join                                   
        Stanza error = enterRoom(OCCUPANT2_JID, ROOM1_JID_WITH_NICK); // 2nd join

        assertNotNull(error);
    }

    public void testEnterRoomWithDuplicateNick_Rewrite() throws Exception {
        Room room1 = conference.findRoom(ROOM1_JID);
        room1.setRewriteDuplicateNick(true); // do rewrite nick, second join will succeed
        assertNull(enterRoom(OCCUPANT2_JID, ROOM1_JID_WITH_NICK)); // 1st join                                   
        Stanza error = enterRoom(OCCUPANT1_JID, ROOM1_JID_WITH_NICK); // 2nd join
        assertNull(error);
        final Stanza stanza1 = occupant1Queue.getNext();
        final Stanza stanza2 = occupant1Queue.getNext();
        final XMLElementVerifier verifier = stanza2.getVerifier();
        final X x = X.fromStanza(stanza2);
        final List<XMLElement> statuses = x.getInnerElementsNamed("status");
        boolean rewriteCode = false;
        for (XMLElement status : statuses) {
            final String code = status.getAttributeValue("code");
            if ("210".equals(code)) rewriteCode = true;
        }
        assertTrue(rewriteCode);
    }

    public void testEnterNonExistingRoom() throws Exception {
        Room room = conference.findRoom(ROOM2_JID);
        assertNull(room);

        enterRoom(OCCUPANT1_JID, ROOM2_JID_WITH_NICK);

        room = conference.findRoom(ROOM2_JID);
        assertNotNull(room);
        assertEquals(1, room.getOccupantCount());
        Occupant occupant = room.getOccupants().iterator().next();

        assertEquals(OCCUPANT1_JID, occupant.getJid());
        assertEquals("nick", occupant.getNick());
        assertEquals(Role.Moderator, occupant.getRole());
    }

    public void testEnterWithoutNick() throws Exception {
        // try entering without a nick
        Stanza response = enterRoom(OCCUPANT1_JID, ROOM1_JID);

        assertPresenceErrorStanza(response, ROOM1_JID, OCCUPANT1_JID, StanzaErrorType.MODIFY, StanzaErrorCondition.JID_MALFORMED);
    }

    public void testEnterWithPassword() throws Exception {
        Room room = conference.createRoom(ROOM2_JID, "Room 1", RoomType.PasswordProtected);
        room.setPassword("secret");

        // no error should be returned
        assertNull(enterRoom(OCCUPANT1_JID, ROOM2_JID_WITH_NICK, "secret", null, false));
        assertEquals(1, room.getOccupantCount());
    }

    public void testEnterWithoutPassword() throws Exception {
        Room room = conference.createRoom(ROOM2_JID, "Room 1", RoomType.PasswordProtected);
        room.setPassword("secret");

        // try entering without a password
        Stanza response = enterRoom(OCCUPANT1_JID, ROOM2_JID_WITH_NICK);

        assertPresenceErrorStanza(response, ROOM2_JID, OCCUPANT1_JID, StanzaErrorType.AUTH, StanzaErrorCondition.NOT_AUTHORIZED);
    }

    private void assertPresenceErrorStanza(Stanza response, Entity from, Entity to, StanzaErrorType type, StanzaErrorCondition errorName) {
        XMLElement xElement = new XMLElementBuilder("x", NamespaceURIs.XEP0045_MUC).build();
        assertErrorStanza(response, "presence", from, to, type, errorName, xElement);
    }

    public void testEnterRoomWithRelays() throws Exception {

        // add one occupant to the room
        Room room = conference.findOrCreateRoom(ROOM1_JID, "Room 1");
        room.addOccupant(OCCUPANT2_JID, "Some nick");

        // now, let user 1 enter room
        enterRoom(OCCUPANT1_JID, ROOM1_JID_WITH_NICK);

        // verify stanzas to existing occupants on the new user
        Stanza user1JoinedStanza = occupant2Queue.getNext();
        // should be from room + nick name
        assertEquals(ROOM1_JID.getFullQualifiedName() + "/nick", user1JoinedStanza.getFrom().getFullQualifiedName());
        // should be to the existing user
        assertEquals(OCCUPANT2_JID, user1JoinedStanza.getTo());

        XMLElement xElement = user1JoinedStanza.getSingleInnerElementsNamed("x");
        assertEquals(NamespaceURIs.XEP0045_MUC_USER, xElement.getNamespaceURI());

        // since this room is non-anonymous, x must contain an item element with the users full JID
        XMLElement itemElement = xElement.getSingleInnerElementsNamed("item");
        assertEquals(OCCUPANT1_JID.getFullQualifiedName(), itemElement.getAttributeValue("jid"));
        assertEquals("none", itemElement.getAttributeValue("affiliation"));
        assertEquals("participant", itemElement.getAttributeValue("role"));

        // verify stanzas to the new user on all existing users, including himself with status=110 element
        // own presence must be sent last
        // assert the stanza from the already existing user
        Stanza stanza = occupant1Queue.getNext();
        assertNotNull(stanza);
        assertEquals(ROOM1_JID.getFullQualifiedName() + "/Some nick", stanza.getFrom().getFullQualifiedName());
        assertEquals(OCCUPANT1_JID, stanza.getTo());

        // assert stanza from the joining user, must have extra status=110 element        
        stanza = occupant1Queue.getNext();
        assertNotNull(stanza);
        assertEquals(ROOM1_JID_WITH_NICK, stanza.getFrom());
        assertEquals(OCCUPANT1_JID, stanza.getTo());
        List<XMLElement> statusElements = stanza.getFirstInnerElement().getInnerElementsNamed("status");
        assertEquals(2, statusElements.size());
        assertEquals("100", statusElements.get(0).getAttributeValue("code"));
        assertEquals("110", statusElements.get(1).getAttributeValue("code"));

    }

    public void testDiscussionHistory() throws Exception {
        // add some messages
        Room room = conference.findOrCreateRoom(ROOM1_JID, "Room 1");
        room.getHistory().append(
                StanzaBuilder.createMessageStanza(OCCUPANT2_JID, ROOM1_JID, MessageStanzaType.GROUPCHAT, null, "Body")
                        .build(), new Occupant(OCCUPANT2_JID, "nick2", room, Role.Participant));
        room.getHistory().append(
                StanzaBuilder.createMessageStanza(OCCUPANT2_JID, ROOM1_JID, MessageStanzaType.GROUPCHAT, null, "Body2")
                        .build(), new Occupant(OCCUPANT2_JID, "nick2", room, Role.Participant));
        room.getHistory().append(
                StanzaBuilder.createMessageStanza(OCCUPANT2_JID, ROOM1_JID, MessageStanzaType.GROUPCHAT, null, "Body3")
                        .build(), new Occupant(OCCUPANT2_JID, "nick2", room, Role.Participant));

        // now, let user 1 enter room
        enterRoom(OCCUPANT1_JID, ROOM1_JID_WITH_NICK, null, new History(2, null, null, null), false);

        Stanza stanza = occupant1Queue.getNext();
        // first stanza should be room presence
        assertNotNull(stanza);
        assertEquals("presence", stanza.getName());

        stanza = occupant1Queue.getNext();
        // here we get the message history
        assertNotNull(stanza);
        assertEquals("message", stanza.getName());
        MessageStanza msgStanza = (MessageStanza) MessageStanza.getWrapper(stanza);
        assertEquals("Body2", msgStanza.getBody(null));

        stanza = occupant1Queue.getNext();
        // first stanza should be room presence
        assertNotNull(stanza);
        assertEquals("message", stanza.getName());
        msgStanza = (MessageStanza) MessageStanza.getWrapper(stanza);
        assertEquals("Body3", msgStanza.getBody(null));

        // we only requested two messages
        assertNull(occupant1Queue.getNext());
    }

}
