/*
 * 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.activemq.transport.amqp.interop;

import static org.apache.activemq.transport.amqp.AmqpSupport.COPY;
import static org.apache.activemq.transport.amqp.AmqpSupport.JMS_SELECTOR_NAME;
import static org.apache.activemq.transport.amqp.AmqpSupport.NO_LOCAL_NAME;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;

import org.apache.activemq.broker.jmx.BrokerViewMBean;
import org.apache.activemq.transport.amqp.client.AmqpClient;
import org.apache.activemq.transport.amqp.client.AmqpClientTestSupport;
import org.apache.activemq.transport.amqp.client.AmqpConnection;
import org.apache.activemq.transport.amqp.client.AmqpFrameValidator;
import org.apache.activemq.transport.amqp.client.AmqpReceiver;
import org.apache.activemq.transport.amqp.client.AmqpSession;
import org.apache.activemq.transport.amqp.ParallelTest;
import org.apache.qpid.proton.amqp.Binary;
import org.apache.qpid.proton.amqp.DescribedType;
import org.apache.qpid.proton.amqp.messaging.Source;
import org.apache.qpid.proton.amqp.messaging.TerminusDurability;
import org.apache.qpid.proton.amqp.messaging.TerminusExpiryPolicy;
import org.apache.qpid.proton.amqp.transport.Detach;
import org.apache.qpid.proton.engine.Receiver;
import org.junit.Test;
import org.junit.experimental.categories.Category;

/**
 * Tests for broker side support of the Durable Subscription mapping for JMS.
 */
@Category(ParallelTest.class)
public class AmqpDurableReceiverTest extends AmqpClientTestSupport {

    private final String SELECTOR_STRING = "color = red";

    @Override
    protected boolean isUseOpenWireConnector() {
        return true;
    }

    @Override
    protected boolean isPersistent() {
        return true;
    }

    @Test(timeout = 60000)
    public void testCreateDurableReceiver() throws Exception {

        AmqpClient client = createAmqpClient();
        AmqpConnection connection = trackConnection(client.createConnection());
        connection.setContainerId(getTestName());
        connection.connect();

        AmqpSession session = connection.createSession();
        session.createDurableReceiver("topic://" + getTestName(), getTestName());

        final BrokerViewMBean brokerView = getProxyToBroker();

        assertEquals(1, brokerView.getDurableTopicSubscribers().length);

        connection.close();
    }

    @Test(timeout = 60000)
    public void testDetachedDurableReceiverRemainsActive() throws Exception {

        AmqpClient client = createAmqpClient();
        AmqpConnection connection = trackConnection(client.createConnection());
        connection.setContainerId(getTestName());
        connection.connect();

        connection.setReceivedFrameInspector(new AmqpFrameValidator() {

            @Override
            public void inspectDetach(Detach detach, Binary encoded) {
                if (detach.getClosed()) {
                    markAsInvalid("Remote should have detached but closed instead.");
                }
            }
        });

        connection.setSentFrameInspector(new AmqpFrameValidator() {

            @Override
            public void inspectDetach(Detach detach, Binary encoded) {
                if (detach.getClosed()) {
                    markAsInvalid("Client should have detached but closed instead.");
                }
            }
        });

        AmqpSession session = connection.createSession();
        AmqpReceiver receiver = session.createDurableReceiver("topic://" + getTestName(), getTestName());

        final BrokerViewMBean brokerView = getProxyToBroker();

        assertEquals(1, brokerView.getDurableTopicSubscribers().length);
        assertEquals(0, brokerView.getInactiveDurableTopicSubscribers().length);

        receiver.detach();

        assertEquals(0, brokerView.getDurableTopicSubscribers().length);
        assertEquals(1, brokerView.getInactiveDurableTopicSubscribers().length);

        connection.getSentFrameInspector().assertValid();
        connection.getReceivedFrameInspector().assertValid();

        connection.close();
    }

    @Test(timeout = 60000)
    public void testCloseDurableReceiverRemovesSubscription() throws Exception {

        AmqpClient client = createAmqpClient();
        AmqpConnection connection = trackConnection(client.createConnection());
        connection.setContainerId(getTestName());
        connection.connect();

        AmqpSession session = connection.createSession();
        AmqpReceiver receiver = session.createDurableReceiver("topic://" + getTestName(), getTestName());

        final BrokerViewMBean brokerView = getProxyToBroker();

        assertEquals(1, brokerView.getDurableTopicSubscribers().length);

        receiver.close();

        assertEquals(0, brokerView.getDurableTopicSubscribers().length);
        assertEquals(0, brokerView.getInactiveDurableTopicSubscribers().length);

        connection.close();
    }

    @Test(timeout = 60000)
    public void testReattachToDurableNode() throws Exception {

        final BrokerViewMBean brokerView = getProxyToBroker();

        AmqpClient client = createAmqpClient();
        AmqpConnection connection = trackConnection(client.createConnection());
        connection.setContainerId(getTestName());
        connection.connect();

        AmqpSession session = connection.createSession();
        AmqpReceiver receiver = session.createDurableReceiver("topic://" + getTestName(), getTestName());

        assertEquals(1, brokerView.getDurableTopicSubscribers().length);
        assertEquals(0, brokerView.getInactiveDurableTopicSubscribers().length);

        receiver.detach();

        assertEquals(0, brokerView.getDurableTopicSubscribers().length);
        assertEquals(1, brokerView.getInactiveDurableTopicSubscribers().length);

        receiver = session.createDurableReceiver("topic://" + getTestName(), getTestName());

        assertEquals(1, brokerView.getDurableTopicSubscribers().length);
        assertEquals(0, brokerView.getInactiveDurableTopicSubscribers().length);

        receiver.close();

        assertEquals(0, brokerView.getDurableTopicSubscribers().length);
        assertEquals(0, brokerView.getInactiveDurableTopicSubscribers().length);

        connection.close();
    }

    @Test(timeout = 60000)
    public void testReattachToDurableNodeAfterRestart() throws Exception {

        final BrokerViewMBean brokerView = getProxyToBroker();

        AmqpClient client = createAmqpClient();
        AmqpConnection connection = trackConnection(client.createConnection());
        connection.setContainerId(getTestName());
        connection.connect();

        AmqpSession session = connection.createSession();
        AmqpReceiver receiver = session.createDurableReceiver("topic://" + getTestName(), getTestName());

        assertEquals(1, brokerView.getDurableTopicSubscribers().length);
        assertEquals(0, brokerView.getInactiveDurableTopicSubscribers().length);

        receiver.detach();

        connection.close();

        restartBroker();

        connection = client.createConnection();
        connection.setContainerId(getTestName());
        connection.connect();
        session = connection.createSession();

        assertEquals(0, brokerView.getDurableTopicSubscribers().length);
        assertEquals(1, brokerView.getInactiveDurableTopicSubscribers().length);

        receiver = session.createDurableReceiver("topic://" + getTestName(), getTestName());

        assertEquals(1, brokerView.getDurableTopicSubscribers().length);
        assertEquals(0, brokerView.getInactiveDurableTopicSubscribers().length);

        receiver.close();

        assertEquals(0, brokerView.getDurableTopicSubscribers().length);
        assertEquals(0, brokerView.getInactiveDurableTopicSubscribers().length);

        connection.close();
    }

    @Test(timeout = 60000)
    public void testLookupExistingSubscription() throws Exception {

        final BrokerViewMBean brokerView = getProxyToBroker();

        AmqpClient client = createAmqpClient();
        AmqpConnection connection = trackConnection(client.createConnection());
        connection.setContainerId(getTestName());
        connection.connect();

        AmqpSession session = connection.createSession();
        AmqpReceiver receiver = session.createDurableReceiver("topic://" + getTestName(), getTestName());

        assertEquals(1, brokerView.getDurableTopicSubscribers().length);
        assertEquals(0, brokerView.getInactiveDurableTopicSubscribers().length);

        receiver.detach();

        assertEquals(0, brokerView.getDurableTopicSubscribers().length);
        assertEquals(1, brokerView.getInactiveDurableTopicSubscribers().length);

        receiver = session.lookupSubscription(getTestName());

        assertNotNull(receiver);

        Receiver protonReceiver = receiver.getReceiver();
        assertNotNull(protonReceiver.getRemoteSource());
        Source remoteSource = (Source) protonReceiver.getRemoteSource();

        if (remoteSource.getFilter() != null) {
            assertFalse(remoteSource.getFilter().containsKey(NO_LOCAL_NAME));
            assertFalse(remoteSource.getFilter().containsKey(JMS_SELECTOR_NAME));
        }

        assertEquals(TerminusExpiryPolicy.NEVER, remoteSource.getExpiryPolicy());
        assertEquals(TerminusDurability.UNSETTLED_STATE, remoteSource.getDurable());
        assertEquals(COPY, remoteSource.getDistributionMode());

        assertEquals(1, brokerView.getDurableTopicSubscribers().length);
        assertEquals(0, brokerView.getInactiveDurableTopicSubscribers().length);

        receiver.close();

        assertEquals(0, brokerView.getDurableTopicSubscribers().length);
        assertEquals(0, brokerView.getInactiveDurableTopicSubscribers().length);

        connection.close();
    }

    @Test(timeout = 60000)
    public void testLookupExistingSubscriptionWithSelector() throws Exception {

        final BrokerViewMBean brokerView = getProxyToBroker();

        AmqpClient client = createAmqpClient();
        AmqpConnection connection = trackConnection(client.createConnection());
        connection.setContainerId(getTestName());
        connection.connect();

        AmqpSession session = connection.createSession();
        AmqpReceiver receiver = session.createDurableReceiver("topic://" + getTestName(), getTestName(), SELECTOR_STRING);

        assertEquals(1, brokerView.getDurableTopicSubscribers().length);
        assertEquals(0, brokerView.getInactiveDurableTopicSubscribers().length);

        receiver.detach();

        assertEquals(0, brokerView.getDurableTopicSubscribers().length);
        assertEquals(1, brokerView.getInactiveDurableTopicSubscribers().length);

        receiver = session.lookupSubscription(getTestName());

        assertNotNull(receiver);

        Receiver protonReceiver = receiver.getReceiver();
        assertNotNull(protonReceiver.getRemoteSource());
        Source remoteSource = (Source) protonReceiver.getRemoteSource();

        assertNotNull(remoteSource.getFilter());
        assertFalse(remoteSource.getFilter().containsKey(NO_LOCAL_NAME));
        assertTrue(remoteSource.getFilter().containsKey(JMS_SELECTOR_NAME));
        assertEquals(SELECTOR_STRING, ((DescribedType) remoteSource.getFilter().get(JMS_SELECTOR_NAME)).getDescribed());

        assertEquals(TerminusExpiryPolicy.NEVER, remoteSource.getExpiryPolicy());
        assertEquals(TerminusDurability.UNSETTLED_STATE, remoteSource.getDurable());
        assertEquals(COPY, remoteSource.getDistributionMode());

        assertEquals(1, brokerView.getDurableTopicSubscribers().length);
        assertEquals(0, brokerView.getInactiveDurableTopicSubscribers().length);

        receiver.close();

        assertEquals(0, brokerView.getDurableTopicSubscribers().length);
        assertEquals(0, brokerView.getInactiveDurableTopicSubscribers().length);

        connection.close();
    }

    @Test(timeout = 60000)
    public void testLookupExistingSubscriptionWithNoLocal() throws Exception {

        final BrokerViewMBean brokerView = getProxyToBroker();

        AmqpClient client = createAmqpClient();
        AmqpConnection connection = trackConnection(client.createConnection());
        connection.setContainerId(getTestName());
        connection.connect();

        AmqpSession session = connection.createSession();
        AmqpReceiver receiver = session.createDurableReceiver("topic://" + getTestName(), getTestName(), null, true);

        assertEquals(1, brokerView.getDurableTopicSubscribers().length);
        assertEquals(0, brokerView.getInactiveDurableTopicSubscribers().length);

        receiver.detach();

        assertEquals(0, brokerView.getDurableTopicSubscribers().length);
        assertEquals(1, brokerView.getInactiveDurableTopicSubscribers().length);

        receiver = session.lookupSubscription(getTestName());

        assertNotNull(receiver);

        Receiver protonReceiver = receiver.getReceiver();
        assertNotNull(protonReceiver.getRemoteSource());
        Source remoteSource = (Source) protonReceiver.getRemoteSource();

        assertNotNull(remoteSource.getFilter());
        assertTrue(remoteSource.getFilter().containsKey(NO_LOCAL_NAME));
        assertFalse(remoteSource.getFilter().containsKey(JMS_SELECTOR_NAME));

        assertEquals(TerminusExpiryPolicy.NEVER, remoteSource.getExpiryPolicy());
        assertEquals(TerminusDurability.UNSETTLED_STATE, remoteSource.getDurable());
        assertEquals(COPY, remoteSource.getDistributionMode());

        assertEquals(1, brokerView.getDurableTopicSubscribers().length);
        assertEquals(0, brokerView.getInactiveDurableTopicSubscribers().length);

        receiver.close();

        assertEquals(0, brokerView.getDurableTopicSubscribers().length);
        assertEquals(0, brokerView.getInactiveDurableTopicSubscribers().length);

        connection.close();
    }

    @Test(timeout = 60000)
    public void testLookupExistingSubscriptionWithSelectorAndNoLocal() throws Exception {

        final BrokerViewMBean brokerView = getProxyToBroker();

        AmqpClient client = createAmqpClient();
        AmqpConnection connection = trackConnection(client.createConnection());
        connection.setContainerId(getTestName());
        connection.connect();

        AmqpSession session = connection.createSession();
        AmqpReceiver receiver = session.createDurableReceiver("topic://" + getTestName(), getTestName(), SELECTOR_STRING, true);

        assertEquals(1, brokerView.getDurableTopicSubscribers().length);
        assertEquals(0, brokerView.getInactiveDurableTopicSubscribers().length);

        receiver.detach();

        assertEquals(0, brokerView.getDurableTopicSubscribers().length);
        assertEquals(1, brokerView.getInactiveDurableTopicSubscribers().length);

        receiver = session.lookupSubscription(getTestName());

        assertNotNull(receiver);

        Receiver protonReceiver = receiver.getReceiver();
        assertNotNull(protonReceiver.getRemoteSource());
        Source remoteSource = (Source) protonReceiver.getRemoteSource();

        assertNotNull(remoteSource.getFilter());
        assertTrue(remoteSource.getFilter().containsKey(NO_LOCAL_NAME));
        assertTrue(remoteSource.getFilter().containsKey(JMS_SELECTOR_NAME));
        assertEquals(SELECTOR_STRING, ((DescribedType) remoteSource.getFilter().get(JMS_SELECTOR_NAME)).getDescribed());

        assertEquals(TerminusExpiryPolicy.NEVER, remoteSource.getExpiryPolicy());
        assertEquals(TerminusDurability.UNSETTLED_STATE, remoteSource.getDurable());
        assertEquals(COPY, remoteSource.getDistributionMode());

        assertEquals(1, brokerView.getDurableTopicSubscribers().length);
        assertEquals(0, brokerView.getInactiveDurableTopicSubscribers().length);

        receiver.close();

        assertEquals(0, brokerView.getDurableTopicSubscribers().length);
        assertEquals(0, brokerView.getInactiveDurableTopicSubscribers().length);

        connection.close();
    }

    @Test(timeout = 60000)
    public void testLookupExistingSubscriptionAfterRestartWithSelectorAndNoLocal() throws Exception {

        final BrokerViewMBean brokerView = getProxyToBroker();

        AmqpClient client = createAmqpClient();
        AmqpConnection connection = trackConnection(client.createConnection());
        connection.setContainerId(getTestName());
        connection.connect();

        AmqpSession session = connection.createSession();
        AmqpReceiver receiver = session.createDurableReceiver("topic://" + getTestName(), getTestName(), SELECTOR_STRING, true);

        assertEquals(1, brokerView.getDurableTopicSubscribers().length);
        assertEquals(0, brokerView.getInactiveDurableTopicSubscribers().length);

        receiver.detach();

        assertEquals(0, brokerView.getDurableTopicSubscribers().length);
        assertEquals(1, brokerView.getInactiveDurableTopicSubscribers().length);

        restartBroker();

        connection = client.createConnection();
        connection.setContainerId(getTestName());
        connection.connect();

        session = connection.createSession();
        receiver = session.lookupSubscription(getTestName());

        assertNotNull(receiver);

        Receiver protonReceiver = receiver.getReceiver();
        assertNotNull(protonReceiver.getRemoteSource());
        Source remoteSource = (Source) protonReceiver.getRemoteSource();

        assertNotNull(remoteSource.getFilter());
        assertTrue(remoteSource.getFilter().containsKey(NO_LOCAL_NAME));
        assertTrue(remoteSource.getFilter().containsKey(JMS_SELECTOR_NAME));
        assertEquals(SELECTOR_STRING, ((DescribedType) remoteSource.getFilter().get(JMS_SELECTOR_NAME)).getDescribed());

        assertEquals(TerminusExpiryPolicy.NEVER, remoteSource.getExpiryPolicy());
        assertEquals(TerminusDurability.UNSETTLED_STATE, remoteSource.getDurable());
        assertEquals(COPY, remoteSource.getDistributionMode());

        assertEquals(1, brokerView.getDurableTopicSubscribers().length);
        assertEquals(0, brokerView.getInactiveDurableTopicSubscribers().length);

        receiver.close();

        assertEquals(0, brokerView.getDurableTopicSubscribers().length);
        assertEquals(0, brokerView.getInactiveDurableTopicSubscribers().length);

        connection.close();
    }

    @Test(timeout = 60000)
    public void testLookupNonExistingSubscription() throws Exception {

        final BrokerViewMBean brokerView = getProxyToBroker();

        AmqpClient client = createAmqpClient();
        AmqpConnection connection = trackConnection(client.createConnection());
        connection.setContainerId(getTestName());
        connection.connect();

        AmqpSession session = connection.createSession();

        assertEquals(0, brokerView.getDurableTopicSubscribers().length);
        assertEquals(0, brokerView.getInactiveDurableTopicSubscribers().length);

        try {
            session.lookupSubscription(getTestName());
            fail("Should throw an exception since there is not subscription");
        } catch (Exception e) {
            LOG.info("Error on lookup: {}", e.getMessage());
        }

        connection.close();
    }
}
