/**
 *
 * Copyright 2010 Jive Software, 2022 Florian Schmaus.
 *
 * 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.jivesoftware.smack;

import java.io.IOException;
import java.net.InetAddress;
import java.util.Date;
import java.util.Random;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.TimeUnit;

import org.jivesoftware.smack.SmackException.OutgoingQueueFullException;
import org.jivesoftware.smack.packet.ExtensionElement;
import org.jivesoftware.smack.packet.Stanza;
import org.jivesoftware.smack.packet.TopLevelStreamElement;

import org.jxmpp.jid.EntityFullJid;
import org.jxmpp.jid.JidTestUtil;
import org.jxmpp.jid.impl.JidCreate;
import org.jxmpp.jid.parts.Resourcepart;
import org.jxmpp.stringprep.XmppStringprepException;

/**
 * A dummy implementation of {@link XMPPConnection}, intended to be used during
 * unit tests.
 *
 * Instances store any packets that are delivered to be send using the
 * {@link #sendStanza(Stanza)} method in a blocking queue. The content of this queue
 * can be inspected using {@link #getSentPacket()}. Typically these queues are
 * used to retrieve a message that was generated by the client.
 *
 * Packets that should be processed by the client to simulate a received stanza
 * can be delivered using the {@linkplain #processStanza(Stanza)} method.
 * It invokes the registered stanza interceptors and listeners.
 *
 * @see XMPPConnection
 * @author Guenther Niess
 */
public class DummyConnection extends AbstractXMPPConnection {

    private final BlockingQueue<TopLevelStreamElement> queue = new LinkedBlockingQueue<TopLevelStreamElement>();

    public static DummyConnectionConfiguration.Builder getDummyConfigurationBuilder() {
        return DummyConnectionConfiguration.builder().setXmppDomain(JidTestUtil.EXAMPLE_ORG).setUsernameAndPassword("dummy",
                        "dummypass");
    }

    public DummyConnection() {
        this(getDummyConfigurationBuilder().build());
    }

    public DummyConnection(CharSequence username, String password, String serviceName) throws XmppStringprepException {
        this(getDummyConfigurationBuilder().setUsernameAndPassword(username, password).setXmppDomain(
                                        JidCreate.domainBareFrom(serviceName)).build());
    }

    private EntityFullJid getUserJid() {
        try {
            return JidCreate.entityFullFrom(config.getUsername()
                            + "@"
                            + config.getXMPPServiceDomain()
                            + "/"
                            + (config.getResource() != null ? config.getResource() : "Test"));
        }
        catch (XmppStringprepException e) {
            throw new IllegalStateException(e);
        }
    }

    public DummyConnection(DummyConnectionConfiguration configuration) {
        super(configuration);

        for (ConnectionCreationListener listener : XMPPConnectionRegistry.getConnectionCreationListeners()) {
            listener.connectionCreated(this);
        }
        user = getUserJid();
    }

    @Override
    protected void connectInternal() {
        connected = true;
        streamId = "dummy-" + new Random(new Date().getTime()).nextInt();
    }

    @Override
    protected void shutdown() {
        user = null;
        authenticated = false;

        callConnectionClosedListener();
    }

    @Override
    public void instantShutdown() {
        shutdown();
    }

    @Override
    public boolean isSecureConnection() {
        return false;
    }

    @Override
    public boolean isUsingCompression() {
        return false;
    }

    @Override
    protected void loginInternal(String username, String password, Resourcepart resource)
            throws XMPPException {
        user = getUserJid();
        authenticated = true;
    }

    @Override
    protected void sendInternal(TopLevelStreamElement element) {
        queue.add(element);
    }

    @Override
    protected void sendNonBlockingInternal(TopLevelStreamElement element) throws OutgoingQueueFullException {
        boolean enqueued = queue.add(element);
        if (!enqueued) {
            throw new OutgoingQueueFullException();
        }
    }

    @Override
    public InetAddress getLocalAddress() {
        return null;
    }

    /**
     * Returns the number of packets that's sent through {@link #sendStanza(Stanza)} and
     * that has not been returned by {@link #getSentPacket()}.
     *
     * @return the number of packets which are in the queue.
     */
    public int getNumberOfSentPackets() {
        return queue.size();
    }

    /**
     * Returns the first stanza that's sent through {@link #sendStanza(Stanza)}
     * and that has not been returned by earlier calls to this method.
     *
     * @param <P> the top level stream element class.
     * @return a sent packet.
     */
    public <P extends TopLevelStreamElement> P getSentPacket() {
        return getSentPacket(5 * 60);
    }

    /**
     * Returns the first stanza that's sent through {@link #sendStanza(Stanza)}
     * and that has not been returned by earlier calls to this method. This
     * method will block for up to the specified number of seconds if no packets
     * have been sent yet.
     *
     * @param wait how long to wait in seconds.
     * @param <P> the top level stream element class.
     * @return a sent packet.
     */
    @SuppressWarnings("unchecked")
    public <P extends TopLevelStreamElement> P getSentPacket(int wait) {
        try {
            return (P) queue.poll(wait, TimeUnit.SECONDS);
        }
        catch (InterruptedException e) {
            throw new IllegalStateException(e);
        }
    }

    /**
     * Processes a stanza through the installed stanza collectors and listeners
     * and letting them examine the stanza to see if they are a match with the
     * filter.
     *
     * @param packet the stanza to process.
     */
    @Override
    public void processStanza(Stanza packet) {
        invokeStanzaCollectorsAndNotifyRecvListeners(packet);
    }

    /**
     * Enable stream feature.
     *
     * @param streamFeature the stream feature.
     * @since 4.2
     */
    public void enableStreamFeature(ExtensionElement streamFeature) {
        addStreamFeature(streamFeature);
    }

    public static DummyConnection newConnectedDummyConnection() {
        DummyConnection dummyConnection = new DummyConnection();
        try {
            dummyConnection.connect();
            dummyConnection.login();
        }
        catch (InterruptedException | SmackException | IOException | XMPPException e) {
            throw new IllegalStateException(e);
        }
        return dummyConnection;
    }

    public static class DummyConnectionConfiguration extends ConnectionConfiguration {
        protected DummyConnectionConfiguration(Builder builder) {
            super(builder);
        }

        public static Builder builder() {
            return new Builder();
        }

        public static final class Builder
                        extends
                        ConnectionConfiguration.Builder<Builder, DummyConnectionConfiguration> {

            private Builder() {
                setSecurityMode(SecurityMode.disabled);
            }

            @Override
            public DummyConnectionConfiguration build() {
                return new DummyConnectionConfiguration(this);
            }

            @Override
            protected Builder getThis() {
                return this;
            }
        }
    }

}
