/*
 *
 * 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.qpid.jms.integration;

import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.greaterThan;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertTrue;

import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;

import javax.jms.Connection;
import javax.jms.ConnectionFactory;

import org.apache.qpid.jms.JmsConnection;
import org.apache.qpid.jms.JmsConnectionFactory;
import org.apache.qpid.jms.test.QpidJmsTestCase;
import org.apache.qpid.jms.test.Wait;
import org.apache.qpid.jms.test.testpeer.AmqpPeerRunnable;
import org.apache.qpid.jms.test.testpeer.TestAmqpPeer;
import org.apache.qpid.proton.amqp.UnsignedInteger;
import org.hamcrest.Matchers;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.Timeout;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class IdleTimeoutIntegrationTest extends QpidJmsTestCase {

    public static final Logger LOGGER = LoggerFactory.getLogger(IdleTimeoutIntegrationTest.class);

    @Test
    @Timeout(20)
    public void testIdleTimeoutIsAdvertisedByDefault() throws Exception {
        try (TestAmqpPeer testPeer = new TestAmqpPeer();) {
            testPeer.expectSaslAnonymous();
            testPeer.expectOpen(null, greaterThan(UnsignedInteger.valueOf(0)), null, false);
            // Each connection creates a session for managing temporary destinations etc
            testPeer.expectBegin();

            ConnectionFactory factory = new JmsConnectionFactory("amqp://localhost:" + testPeer.getServerPort());
            Connection connection = factory.createConnection();
            // Set a clientID to provoke the actual AMQP connection process to occur.
            connection.setClientID("clientName");

            testPeer.waitForAllHandlersToComplete(1000);
            assertNull(testPeer.getThrowable());

            testPeer.expectClose();
            connection.close();
        }
    }

    @Test
    @Timeout(20)
    public void testAdvertisedIdleTimeoutIsHalfOfActualTimeoutValue() throws Exception {
        try (TestAmqpPeer testPeer = new TestAmqpPeer();) {
            int configuredTimeout = 54320;
            int advertisedValue = configuredTimeout / 2;

            testPeer.expectSaslAnonymous();
            testPeer.expectOpen(null, greaterThan(UnsignedInteger.valueOf(advertisedValue)), null, false);

            // Each connection creates a session for managing temporary destinations etc
            testPeer.expectBegin();

            ConnectionFactory factory = new JmsConnectionFactory("amqp://localhost:" + testPeer.getServerPort() + "?amqp.idleTimeout=" + configuredTimeout);
            Connection connection = factory.createConnection();
            // Set a clientID to provoke the actual AMQP connection process to occur.
            connection.setClientID("clientName");

            testPeer.waitForAllHandlersToComplete(1000);
            assertNull(testPeer.getThrowable());

            testPeer.expectClose();
            connection.close();
        }
    }

    @Test
    @Timeout(20)
    public void testClientSendsEmptyFramesWhenPeerAdvertisesIdleTimeout() throws Exception {
        try (TestAmqpPeer testPeer = new TestAmqpPeer();) {
            int period = 20;
            int idleSleep =  100;
            int advertisedTimeout = period * 2;

            testPeer.setAdvertisedIdleTimeout(advertisedTimeout);

            testPeer.expectSaslAnonymous();
            testPeer.expectOpen();

            // Each connection creates a session for managing temporary destinations etc
            testPeer.expectBegin();

            ConnectionFactory factory = new JmsConnectionFactory("amqp://localhost:" + testPeer.getServerPort());
            Connection connection = factory.createConnection();
            // Set a clientID to provoke the actual AMQP connection process to occur.
            connection.setClientID("clientName");

            testPeer.waitForAllHandlersToComplete(1000);
            assertNull(testPeer.getThrowable());

            // Sleep for a bit, let the idle handling work
            Thread.sleep(idleSleep);

            testPeer.expectClose();

            connection.close();

            // Verify that *any* empty frames were received by the peer.
            // We will verify additional behaviours with slower tests.
            assertThat(testPeer.getEmptyFrameCount(), Matchers.greaterThan(0));
        }
    }

    //TODO: Could use JUnit categories to make this slowish test skipable?
    //      If so, make it slower still and more granular.
    @Test
    @Timeout(20)
    public void testClientSendsEmptyFramesWithExpectedFrequency() throws Exception {
        try (TestAmqpPeer testPeer = new TestAmqpPeer();) {
            int period = 250;
            int advertisedTimeout = period * 2;
            int cycles = 10;
            int idleSleep =  cycles * period;
            int offset = 2;

            testPeer.setAdvertisedIdleTimeout(advertisedTimeout);

            testPeer.expectSaslAnonymous();
            testPeer.expectOpen();

            // Each connection creates a session for managing temporary destinations etc
            testPeer.expectBegin();

            ConnectionFactory factory = new JmsConnectionFactory("amqp://localhost:" + testPeer.getServerPort());
            Connection connection = factory.createConnection();
            // Set a clientID to provoke the actual AMQP connection process to occur.
            connection.setClientID("clientName");

            testPeer.waitForAllHandlersToComplete(1000);
            assertNull(testPeer.getThrowable());

            // Sleep for a bit, let the idle handling work
            Thread.sleep(idleSleep);

            testPeer.expectClose();

            connection.close();

            assertThat(testPeer.getEmptyFrameCount(), Matchers.greaterThanOrEqualTo(cycles - offset));
            assertThat(testPeer.getEmptyFrameCount(), Matchers.lessThanOrEqualTo(cycles + offset));
        }
    }

    @Test
    @Timeout(20)
    public void testConnectionSetFailedWhenPeerNeglectsToSendEmptyFrames() throws Exception {
        try (TestAmqpPeer testPeer = new TestAmqpPeer();) {
            int configuredTimeout = 200;

            testPeer.expectSaslAnonymous();
            testPeer.expectOpen();

            // Each connection creates a session for managing temporary destinations etc
            testPeer.expectBegin();

            JmsConnectionFactory factory = new JmsConnectionFactory("amqp://localhost:" + testPeer.getServerPort() + "?amqp.idleTimeout=" + configuredTimeout);
            final JmsConnection connection = (JmsConnection) factory.createConnection();
            // Set a clientID to provoke the actual AMQP connection process to occur.
            connection.setClientID("clientName");

            testPeer.waitForAllHandlersToComplete(1000);
            // The peer is still connected, so it will get the close frame with error
            testPeer.expectClose(Matchers.notNullValue(), false);
            assertNull(testPeer.getThrowable());
            testPeer.setSuppressReadExceptionOnClose(true);

            boolean failed = Wait.waitFor(new Wait.Condition() {
                @Override
                public boolean isSatisfied() throws Exception {
                    return connection.isFailed();
                }
            }, 10000, 10);

            assertTrue(failed, "connection didnt fail in expected timeframe");
            testPeer.waitForAllHandlersToComplete(1000);

            connection.close();
        }
    }

    @Test
    @Timeout(20)
    public void testConnectionNotMarkedFailedWhenPeerSendsEmptyFrames() throws Exception {
        try (TestAmqpPeer testPeer = new TestAmqpPeer();) {
            int configuredTimeout = 2000;
            int period = 500;
            int cycles = 6;

            final CountDownLatch latch = new CountDownLatch(cycles);

            testPeer.expectSaslAnonymous();
            testPeer.expectOpen();

            // Each connection creates a session for managing temporary destinations etc
            testPeer.expectBegin();

            // Start to emit idle frames when the connection is set up, this should stop it timing out
            testPeer.runAfterLastHandler(new EmptyFrameSender(latch, period, cycles, testPeer));

            JmsConnectionFactory factory = new JmsConnectionFactory("amqp://localhost:" + testPeer.getServerPort() + "?amqp.idleTimeout=" + configuredTimeout);
            final JmsConnection connection = (JmsConnection) factory.createConnection();
            // Set a clientID to provoke the actual AMQP connection process to occur.
            connection.setClientID("clientName");

            boolean framesSent = latch.await(cycles * period * 2, TimeUnit.MILLISECONDS);
            assertTrue(framesSent, "idle frames were not sent as expected");

            assertFalse(connection.isFailed(), "connection shouldnt fail");
            testPeer.expectClose();
            connection.close();

            testPeer.waitForAllHandlersToComplete(1000);
            assertNull(testPeer.getThrowable());
        }
    }

    private static class EmptyFrameSender implements AmqpPeerRunnable
    {
        private int delay;
        private int cycles;
        private CountDownLatch latch;
        private TestAmqpPeer testPeer;

        public EmptyFrameSender(CountDownLatch latch, int delay, int cycles, TestAmqpPeer testPeer) {
            this.cycles = cycles;
            this.delay = delay;
            this.latch = latch;
            this.testPeer = testPeer;
        }

        @Override
        public void run() {
            for (int i = 0; i < cycles; i++) {
                LOGGER.info("Delaying before empty frame: {}", i + 1);
                try {
                    Thread.sleep(delay);
                } catch (InterruptedException e) {
                    // pass
                }
                LOGGER.info("Sending empty frame: {}", i + 1);
                testPeer.sendEmptyFrame(false);
                latch.countDown();
            }
        }
    }
}
