/**
 * 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.bugs;

import java.io.PrintWriter;
import java.io.StringWriter;
import java.util.Arrays;
import java.util.Random;

import jakarta.jms.Connection;
import jakarta.jms.JMSException;
import jakarta.jms.MessageConsumer;
import jakarta.jms.MessageProducer;
import jakarta.jms.Queue;
import jakarta.jms.Session;

import org.apache.activemq.ActiveMQConnection;
import org.apache.activemq.ActiveMQConnectionFactory;
import org.apache.activemq.broker.BrokerService;
import org.apache.activemq.command.ActiveMQBytesMessage;
import org.apache.activemq.command.ActiveMQMessage;
import org.junit.After;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TestName;

public class AMQ5381Test {

    public static final byte[] ORIG_MSG_CONTENT = randomByteArray();
    public static final String AMQ5381_EXCEPTION_MESSAGE = "java.util.zip.DataFormatException: incorrect header check";

    private BrokerService brokerService;
    private String brokerURI;

    @Rule public TestName name = new TestName();

    @Before
    public void startBroker() throws Exception {
        brokerService = new BrokerService();
        brokerService.setPersistent(false);
        brokerService.setUseJmx(false);
        brokerService.addConnector("tcp://localhost:0");
        brokerService.start();
        brokerService.waitUntilStarted();

        brokerURI = brokerService.getTransportConnectorByScheme("tcp").getPublishableConnectString();
    }

    @After
    public void stopBroker() throws Exception {
        if (brokerService != null) {
            brokerService.stop();
        }
    }

    private ActiveMQConnection createConnection(boolean useCompression) throws Exception {
        ActiveMQConnectionFactory factory = new ActiveMQConnectionFactory(brokerURI);
        factory.setUseCompression(useCompression);
        Connection connection = factory.createConnection();
        connection.start();
        return (ActiveMQConnection) connection;
    }

    @Test
    public void amq5381Test() throws Exception {

        // Consumer Configured for (useCompression=true)
        final ActiveMQConnection consumerConnection = createConnection(true);
        final Session consumerSession = consumerConnection.createSession(false, Session.AUTO_ACKNOWLEDGE);
        final Queue consumerQueue = consumerSession.createQueue(name.getMethodName());
        final MessageConsumer consumer = consumerSession.createConsumer(consumerQueue);

        // Producer Configured for (useCompression=false)
        final ActiveMQConnection producerConnection = createConnection(false);
        final Session producerSession = producerConnection.createSession(false, Session.AUTO_ACKNOWLEDGE);
        final Queue producerQueue = producerSession.createQueue(name.getMethodName());

        try {

            final ActiveMQBytesMessage messageProduced = (ActiveMQBytesMessage) producerSession.createBytesMessage();
            messageProduced.writeBytes(ORIG_MSG_CONTENT);
            Assert.assertFalse(messageProduced.isReadOnlyBody());

            Assert.assertFalse(
                "Produced Message's 'compressed' flag should remain false until the message is sent (where it will be compressed, if necessary)",
                messageProduced.isCompressed());

            final MessageProducer producer = producerSession.createProducer(null);
            producer.send(producerQueue, messageProduced);

            Assert.assertEquals("Once sent, the produced Message's 'compressed' flag should match its Connection's 'useCompression' flag",
                producerConnection.isUseCompression(), messageProduced.isCompressed());

            final ActiveMQBytesMessage messageConsumed = (ActiveMQBytesMessage) consumer.receive();
            Assert.assertNotNull(messageConsumed);
            Assert.assertTrue("Consumed Message should be read-only", messageConsumed.isReadOnlyBody());
            Assert.assertEquals("Consumed Message's 'compressed' flag should match the produced Message's 'compressed' flag",
                                messageProduced.isCompressed(), messageConsumed.isCompressed());

            // ensure consumed message content matches what was originally set
            final byte[] consumedMsgContent = new byte[(int) messageConsumed.getBodyLength()];
            messageConsumed.readBytes(consumedMsgContent);

            Assert.assertTrue("Consumed Message content should match the original Message content", Arrays.equals(ORIG_MSG_CONTENT, consumedMsgContent));

            // make message writable so the consumer can modify and reuse it
            makeWritable(messageConsumed);

            // modify message, attempt to trigger DataFormatException due
            // to old incorrect compression logic
            try {
                messageConsumed.setStringProperty(this.getClass().getName(), "test");
            } catch (JMSException jmsE) {
                if (AMQ5381_EXCEPTION_MESSAGE.equals(jmsE.getMessage())) {
                    StringWriter sw = new StringWriter();
                    PrintWriter pw = new PrintWriter(sw);
                    jmsE.printStackTrace(pw);

                    Assert.fail("AMQ5381 Error State Achieved: attempted to decompress BytesMessage contents that are not compressed\n" + sw.toString());
                } else {
                    throw jmsE;
                }
            }

            Assert.assertEquals(
                "The consumed Message's 'compressed' flag should still match the produced Message's 'compressed' flag after it has been made writable",
                messageProduced.isCompressed(), messageConsumed.isCompressed());

            // simulate re-publishing message
            simulatePublish(messageConsumed);

            // ensure consumed message content matches what was originally set
            final byte[] modifiedMsgContent = new byte[(int) messageConsumed.getBodyLength()];
            messageConsumed.readBytes(modifiedMsgContent);

            Assert.assertTrue(
                "After the message properties are modified and it is re-published, its message content should still match the original message content",
                Arrays.equals(ORIG_MSG_CONTENT, modifiedMsgContent));
        } finally {
            producerSession.close();
            producerConnection.close();
            consumerSession.close();
            consumerConnection.close();
        }
    }

    protected static final int MAX_RANDOM_BYTE_ARRAY_SIZE_KB = 128;

    protected static byte[] randomByteArray() {
        final Random random = new Random();
        final byte[] byteArray = new byte[random.nextInt(MAX_RANDOM_BYTE_ARRAY_SIZE_KB * 1024)];
        random.nextBytes(byteArray);

        return byteArray;
    }

    protected static void makeWritable(final ActiveMQMessage message) {
        message.setReadOnlyBody(false);
        message.setReadOnlyProperties(false);
    }

    protected static void simulatePublish(final ActiveMQBytesMessage message) throws JMSException {
        message.reset();
        message.onSend();
    }
}
