/*
 * 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.JMS_SELECTOR_FILTER_IDS;
import static org.apache.activemq.transport.amqp.AmqpSupport.NO_LOCAL_FILTER_IDS;
import static org.apache.activemq.transport.amqp.AmqpSupport.findFilter;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;

import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;

import org.apache.activemq.broker.jmx.DestinationViewMBean;
import org.apache.activemq.broker.jmx.QueueViewMBean;
import org.apache.activemq.junit.ActiveMQTestRunner;
import org.apache.activemq.junit.Repeat;
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.AmqpMessage;
import org.apache.activemq.transport.amqp.client.AmqpReceiver;
import org.apache.activemq.transport.amqp.client.AmqpSession;
import org.apache.activemq.transport.amqp.client.AmqpUnknownFilterType;
import org.apache.activemq.transport.amqp.client.AmqpValidator;
import org.apache.activemq.util.Wait;
import org.apache.qpid.proton.amqp.DescribedType;
import org.apache.qpid.proton.amqp.Symbol;
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.engine.Receiver;
import org.apache.qpid.proton.engine.Session;
import org.apache.qpid.proton.message.Message;
import org.junit.Test;
import org.junit.runner.RunWith;

/**
 * Test various behaviors of AMQP receivers with the broker.
 */
@RunWith(ActiveMQTestRunner.class)
public class AmqpReceiverTest extends AmqpClientTestSupport {

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

    @Test(timeout = 60000)
    public void testReceiverCloseSendsRemoteClose() throws Exception {
        AmqpClient client = createAmqpClient();
        assertNotNull(client);

        final AtomicBoolean closed = new AtomicBoolean();

        client.setValidator(new AmqpValidator() {

            @Override
            public void inspectClosedResource(Session session) {
                LOG.info("Session closed: {}", session.getContext());
            }

            @Override
            public void inspectDetachedResource(Receiver receiver) {
                markAsInvalid("Broker should not detach receiver linked to closed session.");
            }

            @Override
            public void inspectClosedResource(Receiver receiver) {
                LOG.info("Receiver closed: {}", receiver.getContext());
                closed.set(true);
            }
        });

        AmqpConnection connection = trackConnection(client.connect());
        assertNotNull(connection);
        AmqpSession session = connection.createSession();
        assertNotNull(session);
        AmqpReceiver receiver = session.createReceiver("queue://" + getTestName());
        assertNotNull(receiver);

        receiver.close();

        assertTrue("Did not process remote close as expected", closed.get());
        connection.getStateInspector().assertValid();

        connection.close();
    }

    @Test(timeout = 60000)
    public void testCreateQueueReceiver() throws Exception {
        AmqpClient client = createAmqpClient();
        AmqpConnection connection = trackConnection(client.connect());
        AmqpSession session = connection.createSession();

        assertEquals(0, brokerService.getAdminView().getQueues().length);

        AmqpReceiver receiver = session.createReceiver("queue://" + getTestName());

        assertEquals(1, brokerService.getAdminView().getQueues().length);
        assertNotNull(getProxyToQueue(getTestName()));
        assertEquals(1, brokerService.getAdminView().getQueueSubscribers().length);
        receiver.close();
        assertEquals(0, brokerService.getAdminView().getQueueSubscribers().length);

        connection.close();
    }

    @Test(timeout = 60000)
    public void testCreateQueueReceiverWithJMSSelector() throws Exception {
        AmqpClient client = createAmqpClient();

        client.setValidator(new AmqpValidator() {

            @SuppressWarnings("unchecked")
            @Override
            public void inspectOpenedResource(Receiver receiver) {
                LOG.info("Receiver opened: {}", receiver);

                if (receiver.getRemoteSource() == null) {
                    markAsInvalid("Link opened with null source.");
                }

                Source source = (Source) receiver.getRemoteSource();
                Map<Symbol, Object> filters = source.getFilter();

                if (findFilter(filters, JMS_SELECTOR_FILTER_IDS) == null) {
                    markAsInvalid("Broker did not return the JMS Filter on Attach");
                }
            }
        });

        AmqpConnection connection = trackConnection(client.connect());
        AmqpSession session = connection.createSession();

        assertEquals(0, brokerService.getAdminView().getQueues().length);

        session.createReceiver("queue://" + getTestName(), "JMSPriority > 8");

        assertEquals(1, brokerService.getAdminView().getQueueSubscribers().length);

        connection.getStateInspector().assertValid();
        connection.close();
    }

    @Test(timeout = 60000)
    public void testCreateQueueReceiverWithNoLocalSet() throws Exception {
        AmqpClient client = createAmqpClient();

        client.setValidator(new AmqpValidator() {

            @SuppressWarnings("unchecked")
            @Override
            public void inspectOpenedResource(Receiver receiver) {
                LOG.info("Receiver opened: {}", receiver);

                if (receiver.getRemoteSource() == null) {
                    markAsInvalid("Link opened with null source.");
                }

                Source source = (Source) receiver.getRemoteSource();
                Map<Symbol, Object> filters = source.getFilter();

                if (findFilter(filters, NO_LOCAL_FILTER_IDS) == null) {
                    markAsInvalid("Broker did not return the NoLocal Filter on Attach");
                }
            }
        });

        AmqpConnection connection = trackConnection(client.connect());
        AmqpSession session = connection.createSession();

        assertEquals(0, brokerService.getAdminView().getQueues().length);

        session.createReceiver("queue://" + getTestName(), null, true);

        assertEquals(1, brokerService.getAdminView().getQueueSubscribers().length);

        connection.getStateInspector().assertValid();
        connection.close();
    }

    @Test(timeout = 60000)
    public void testCreateTopicReceiver() throws Exception {
        AmqpClient client = createAmqpClient();
        AmqpConnection connection = trackConnection(client.connect());
        AmqpSession session = connection.createSession();

        assertEquals(0, brokerService.getAdminView().getTopics().length);

        AmqpReceiver receiver = session.createReceiver("topic://" + getTestName());

        assertEquals(1, brokerService.getAdminView().getTopics().length);
        assertNotNull(getProxyToTopic(getTestName()));
        assertEquals(1, brokerService.getAdminView().getTopicSubscribers().length);
        receiver.close();
        assertEquals(0, brokerService.getAdminView().getTopicSubscribers().length);

        connection.close();
    }

    @Test(timeout = 60000)
    public void testQueueReceiverReadMessage() throws Exception {
        sendMessages(getTestName(), 1, false);

        AmqpClient client = createAmqpClient();
        AmqpConnection connection = trackConnection(client.connect());
        AmqpSession session = connection.createSession();

        AmqpReceiver receiver = session.createReceiver("queue://" + getTestName());

        QueueViewMBean queueView = getProxyToQueue(getTestName());
        assertEquals(1, queueView.getQueueSize());
        assertEquals(0, queueView.getDispatchCount());

        receiver.flow(1);
        assertNotNull(receiver.receive(5, TimeUnit.SECONDS));
        receiver.close();

        assertEquals(1, queueView.getQueueSize());

        connection.close();
    }

    @Test(timeout = 60000)
    @Repeat(repetitions = 1)
    public void testPresettledReceiverReadsAllMessages() throws Exception {
        final int MSG_COUNT = 100;
        sendMessages(getTestName(), MSG_COUNT, false);

        AmqpClient client = createAmqpClient();
        AmqpConnection connection = trackConnection(client.connect());
        AmqpSession session = connection.createSession();

        AmqpReceiver receiver = session.createReceiver("queue://" + getTestName(), null, false, true);

        QueueViewMBean queueView = getProxyToQueue(getTestName());
        assertEquals(MSG_COUNT, queueView.getQueueSize());
        assertEquals(0, queueView.getDispatchCount());

        receiver.flow(MSG_COUNT);
        for (int i = 0; i < MSG_COUNT; ++i) {
            assertNotNull(receiver.receive(5, TimeUnit.SECONDS));
        }
        receiver.close();

        assertEquals(0, queueView.getQueueSize());

        connection.close();
    }

    @Test(timeout = 60000)
    @Repeat(repetitions = 1)
    public void testPresettledReceiverReadsAllMessagesInNonFlowBatchQueue() throws Exception {
        doTestPresettledReceiverReadsAllMessagesInNonFlowBatch(false);
    }

    @Test(timeout = 60000)
    @Repeat(repetitions = 1)
    public void testPresettledReceiverReadsAllMessagesInNonFlowBatchTopic() throws Exception {
        doTestPresettledReceiverReadsAllMessagesInNonFlowBatch(true);
    }

    private void doTestPresettledReceiverReadsAllMessagesInNonFlowBatch(boolean topic) throws Exception {

        final String destinationName;
        if (topic) {
            destinationName = "topic://" + getTestName();
        } else {
            destinationName = "queue://" + getTestName();
        }

        final int MSG_COUNT = 100;

        AmqpClient client = createAmqpClient();
        AmqpConnection connection = trackConnection(client.connect());
        AmqpSession session = connection.createSession();

        AmqpReceiver receiver = session.createReceiver(destinationName, null, false, true);

        sendMessages(getTestName(), MSG_COUNT, topic);

        final DestinationViewMBean destinationView;
        if (topic) {
            destinationView = getProxyToTopic(getTestName());
        } else {
            destinationView = getProxyToQueue(getTestName());
        }
        assertEquals(MSG_COUNT, destinationView.getEnqueueCount());
        assertEquals(0, destinationView.getDispatchCount());

        receiver.flow(20);
        // consume less that flow
        for (int j=0;j<10;j++) {
            assertNotNull(receiver.receive(5, TimeUnit.SECONDS));
        }

        // flow more and consume all
        receiver.flow(10);
        for (int j=0;j<20;j++) {
            assertNotNull(receiver.receive(5, TimeUnit.SECONDS));
        }

        // remainder
        receiver.flow(70);
        for (int j=0;j<70;j++) {
            assertNotNull(receiver.receive(5, TimeUnit.SECONDS));
        }

        receiver.close();

        assertEquals(0, destinationView.getEnqueueCount() - destinationView.getDequeueCount());

        connection.close();
    }

    @Test(timeout = 60000)
    @Repeat(repetitions = 1)
    public void testTwoQueueReceiversOnSameConnectionReadMessagesNoDispositions() throws Exception {
        int MSG_COUNT = 4;
        sendMessages(getTestName(), MSG_COUNT, false);

        AmqpClient client = createAmqpClient();
        AmqpConnection connection = trackConnection(client.connect());
        AmqpSession session = connection.createSession();

        AmqpReceiver receiver1 = session.createReceiver("queue://" + getTestName());

        QueueViewMBean queueView = getProxyToQueue(getTestName());
        assertEquals(MSG_COUNT, queueView.getQueueSize());

        receiver1.flow(2);
        assertNotNull(receiver1.receive(5, TimeUnit.SECONDS));
        assertNotNull(receiver1.receive(5, TimeUnit.SECONDS));

        AmqpReceiver receiver2 = session.createReceiver("queue://" + getTestName());

        assertEquals(2, brokerService.getAdminView().getQueueSubscribers().length);

        receiver2.flow(2);
        assertNotNull(receiver2.receive(5, TimeUnit.SECONDS));
        assertNotNull(receiver2.receive(5, TimeUnit.SECONDS));

        assertEquals(MSG_COUNT, queueView.getDispatchCount());
        assertEquals(0, queueView.getDequeueCount());

        receiver1.close();
        receiver2.close();

        assertEquals(MSG_COUNT, queueView.getQueueSize());

        connection.close();
    }

    @Test(timeout = 60000)
    public void testTwoQueueReceiversOnSameConnectionReadMessagesAcceptOnEach() throws Exception {
        int MSG_COUNT = 4;
        sendMessages(getTestName(), MSG_COUNT, false);

        AmqpClient client = createAmqpClient();
        AmqpConnection connection = trackConnection(client.connect());
        AmqpSession session = connection.createSession();

        AmqpReceiver receiver1 = session.createReceiver("queue://" + getTestName());

        final QueueViewMBean queueView = getProxyToQueue(getTestName());
        assertEquals(MSG_COUNT, queueView.getQueueSize());

        receiver1.flow(2);
        AmqpMessage message = receiver1.receive(5, TimeUnit.SECONDS);
        assertNotNull(message);
        message.accept();
        message = receiver1.receive(5, TimeUnit.SECONDS);
        assertNotNull(message);
        message.accept();

        assertTrue("Should have ack'd two", Wait.waitFor(new Wait.Condition() {

            @Override
            public boolean isSatisified() throws Exception {
                return queueView.getDequeueCount() == 2;
            }
        }, TimeUnit.SECONDS.toMillis(5), TimeUnit.MILLISECONDS.toMillis(50)));

        AmqpReceiver receiver2 = session.createReceiver("queue://" + getTestName());

        assertEquals(2, brokerService.getAdminView().getQueueSubscribers().length);

        receiver2.flow(2);
        message = receiver2.receive(5, TimeUnit.SECONDS);
        assertNotNull(message);
        message.accept();
        message = receiver2.receive(5, TimeUnit.SECONDS);
        assertNotNull(message);
        message.accept();

        assertEquals(MSG_COUNT, queueView.getDispatchCount());
        assertTrue("Queue should be empty now", Wait.waitFor(new Wait.Condition() {

            @Override
            public boolean isSatisified() throws Exception {
                return queueView.getDequeueCount() == 4;
            }
        }, TimeUnit.SECONDS.toMillis(15), TimeUnit.MILLISECONDS.toMillis(10)));

        receiver1.close();
        receiver2.close();

        assertEquals(0, queueView.getQueueSize());

        connection.close();
    }

    @Test(timeout = 60000)
    public void testSecondReceiverOnQueueGetsAllUnconsumedMessages() throws Exception {
        int MSG_COUNT = 20;
        sendMessages(getTestName(), MSG_COUNT, false);

        AmqpClient client = createAmqpClient();
        AmqpConnection connection = trackConnection(client.connect());
        AmqpSession session = connection.createSession();

        AmqpReceiver receiver1 = session.createReceiver("queue://" + getTestName());

        final QueueViewMBean queueView = getProxyToQueue(getTestName());
        assertEquals(MSG_COUNT, queueView.getQueueSize());

        receiver1.flow(20);

        assertTrue("Should have dispatch to prefetch", Wait.waitFor(new Wait.Condition() {

            @Override
            public boolean isSatisified() throws Exception {
                return queueView.getInFlightCount() >= 2;
            }
        }, TimeUnit.SECONDS.toMillis(5), TimeUnit.MILLISECONDS.toMillis(50)));

        receiver1.close();

        AmqpReceiver receiver2 = session.createReceiver("queue://" + getTestName());

        assertEquals(1, brokerService.getAdminView().getQueueSubscribers().length);

        receiver2.flow(MSG_COUNT * 2);
        AmqpMessage message = receiver2.receive(5, TimeUnit.SECONDS);
        assertNotNull(message);
        message.accept();
        message = receiver2.receive(5, TimeUnit.SECONDS);
        assertNotNull(message);
        message.accept();

        assertTrue("Should have ack'd two", Wait.waitFor(new Wait.Condition() {

            @Override
            public boolean isSatisified() throws Exception {
                return queueView.getDequeueCount() == 2;
            }
        }, TimeUnit.SECONDS.toMillis(5), TimeUnit.MILLISECONDS.toMillis(50)));

        receiver2.close();

        assertEquals(MSG_COUNT - 2, queueView.getQueueSize());

        connection.close();
    }

    @Test(timeout = 60000)
    public void testUnsupportedFiltersAreNotListedAsSupported() throws Exception {
        AmqpClient client = createAmqpClient();

        client.setValidator(new AmqpValidator() {

            @SuppressWarnings("unchecked")
            @Override
            public void inspectOpenedResource(Receiver receiver) {
                LOG.info("Receiver opened: {}", receiver);

                if (receiver.getRemoteSource() == null) {
                    markAsInvalid("Link opened with null source.");
                }

                Source source = (Source) receiver.getRemoteSource();
                Map<Symbol, Object> filters = source.getFilter();

                if (findFilter(filters, AmqpUnknownFilterType.UNKNOWN_FILTER_IDS) != null) {
                    markAsInvalid("Broker should not return unsupported filter on attach.");
                }
            }
        });

        Map<Symbol, DescribedType> filters = new HashMap<>();
        filters.put(AmqpUnknownFilterType.UNKNOWN_FILTER_NAME, AmqpUnknownFilterType.UNKOWN_FILTER);

        Source source = new Source();
        source.setAddress("queue://" + getTestName());
        source.setFilter(filters);
        source.setDurable(TerminusDurability.NONE);
        source.setExpiryPolicy(TerminusExpiryPolicy.LINK_DETACH);

        AmqpConnection connection = trackConnection(client.connect());
        AmqpSession session = connection.createSession();

        assertEquals(0, brokerService.getAdminView().getQueues().length);

        session.createReceiver(source);

        assertEquals(1, brokerService.getAdminView().getQueueSubscribers().length);

        connection.getStateInspector().assertValid();
        connection.close();
    }

    @Test(timeout = 30000)
    public void testReleasedDisposition() throws Exception {
        sendMessages(getTestName(), 1, false);

        AmqpClient client = createAmqpClient();
        AmqpConnection connection = trackConnection(client.connect());
        AmqpSession session = connection.createSession();

        AmqpReceiver receiver = session.createReceiver(getTestName());
        receiver.flow(2);

        AmqpMessage message = receiver.receive(5, TimeUnit.SECONDS);
        assertNotNull("did not receive message first time", message);

        Message protonMessage = message.getWrappedMessage();
        assertNotNull(protonMessage);
        assertEquals("Unexpected initial value for AMQP delivery-count", 0, protonMessage.getDeliveryCount());

        message.release();

        // Read the message again and validate its state

        message = receiver.receive(10, TimeUnit.SECONDS);
        assertNotNull("did not receive message again", message);

        message.accept();

        protonMessage = message.getWrappedMessage();
        assertNotNull(protonMessage);
        assertEquals("Unexpected updated value for AMQP delivery-count", 0, protonMessage.getDeliveryCount());

        connection.close();
    }

    @Test(timeout = 30000)
    public void testRejectedDisposition() throws Exception {
        sendMessages(getTestName(), 1, false);

        AmqpClient client = createAmqpClient();
        AmqpConnection connection = trackConnection(client.connect());
        AmqpSession session = connection.createSession();

        AmqpReceiver receiver = session.createReceiver(getTestName());
        receiver.flow(2);

        AmqpMessage message = receiver.receive(5, TimeUnit.SECONDS);
        assertNotNull("did not receive message first time", message);

        Message protonMessage = message.getWrappedMessage();
        assertNotNull(protonMessage);
        assertEquals("Unexpected initial value for AMQP delivery-count", 0, protonMessage.getDeliveryCount());

        message.reject();

        // Attempt to read the message again but should not get it.
        message = receiver.receive(2, TimeUnit.SECONDS);
        assertNull("shoudl not receive message again", message);

        connection.close();
    }

    @Test(timeout = 30000)
    public void testModifiedDispositionWithDeliveryFailedWithoutUndeliverableHereFieldsSet() throws Exception {
        doModifiedDispositionTestImpl(Boolean.TRUE, null);
    }

    @Test(timeout = 30000)
    public void testModifiedDispositionWithoutDeliveryFailedWithoutUndeliverableHereFieldsSet() throws Exception {
        doModifiedDispositionTestImpl(null, null);
    }

    @Test(timeout = 30000)
    public void testModifiedDispositionWithoutDeliveryFailedWithUndeliverableHereFieldsSet() throws Exception {
        doModifiedDispositionTestImpl(null, Boolean.TRUE);
    }

    @Test(timeout = 30000)
    public void testModifiedDispositionWithDeliveryFailedWithUndeliverableHereFieldsSet() throws Exception {
        doModifiedDispositionTestImpl(Boolean.TRUE, Boolean.TRUE);
    }

    private void doModifiedDispositionTestImpl(Boolean deliveryFailed, Boolean undeliverableHere) throws Exception {
        int msgCount = 1;
        sendMessages(getTestName(), msgCount, false);

        AmqpClient client = createAmqpClient();
        AmqpConnection connection = trackConnection(client.connect());
        AmqpSession session = connection.createSession();

        AmqpReceiver receiver = session.createReceiver("queue://" + getTestName());
        receiver.flow(2 * msgCount);

        AmqpMessage message = receiver.receive(5, TimeUnit.SECONDS);
        assertNotNull("did not receive message first time", message);

        Message protonMessage = message.getWrappedMessage();
        assertNotNull(protonMessage);
        assertEquals("Unexpected initial value for AMQP delivery-count", 0, protonMessage.getDeliveryCount());

        message.modified(deliveryFailed, undeliverableHere);

        if(Boolean.TRUE.equals(undeliverableHere)) {
            message = receiver.receive(250, TimeUnit.MILLISECONDS);
            assertNull("Should not receive message again", message);
        } else {
            message = receiver.receive(5, TimeUnit.SECONDS);
            assertNotNull("did not receive message again", message);

            int expectedDeliveryCount = 0;
            if(Boolean.TRUE.equals(deliveryFailed)) {
                expectedDeliveryCount = 1;
            }

            message.accept();

            Message protonMessage2 = message.getWrappedMessage();
            assertNotNull(protonMessage2);
            assertEquals("Unexpected updated value for AMQP delivery-count", expectedDeliveryCount, protonMessage2.getDeliveryCount());
        }

        receiver.close();
        connection.close();
    }
}
