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

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertNotSame;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.Assertions.fail;

import java.io.IOException;
import java.io.Serializable;
import java.util.HashMap;
import java.util.Map;

import javax.jms.JMSException;
import javax.jms.MessageFormatException;
import javax.jms.MessageNotReadableException;
import javax.jms.MessageNotWriteableException;
import javax.jms.ObjectMessage;

import org.apache.qpid.jms.message.facade.JmsObjectMessageFacade;
import org.apache.qpid.jms.message.facade.test.JmsTestMessageFactory;
import org.apache.qpid.jms.message.facade.test.JmsTestObjectMessageFacade;
import org.junit.jupiter.api.Test;
import org.mockito.Mockito;

/**
 *
 */
public class JmsObjectMessageTest {

    private final JmsMessageFactory factory = new JmsTestMessageFactory();

    /**
     * Test that attempting to write bytes to a received message (without calling {@link ObjectMessage#clearBody()} first)
     * causes a {@link MessageNotWriteableException} to be thrown due to being read-only.
     *
     * @throws Exception if an error occurs during the test.
     */
    @Test
    public void testReceivedObjectMessageThrowsMessageNotWriteableExceptionOnSetObject() throws Exception {
        String content = "myStringContent";
        JmsObjectMessageFacade facade = new JmsTestObjectMessageFacade();
        facade.setObject(content);
        JmsObjectMessage objectMessage = new JmsObjectMessage(facade);
        objectMessage.onDispatch();

        try {
            objectMessage.setObject("newObject");
            fail("Expected exception to be thrown");
        } catch (MessageNotWriteableException mnwe) {
            // expected
        }
    }

    /**
     * Test that calling {@link ObjectMessage#toString()} returns a meaningful value
     *
     * @throws Exception if an error occurs during the test.
     */
    @Test
    public void testToString() throws Exception {
        String content = "myStringContent";
        JmsObjectMessageFacade facade = new JmsTestObjectMessageFacade();
        facade.setObject(content);
        JmsObjectMessage objectMessage = new JmsObjectMessage(facade);
        objectMessage.onDispatch();

        assertTrue(objectMessage.toString().startsWith("JmsObjectMessageFacade"));
    }

    /**
     * Test that calling {@link ObjectMessage#clearBody()} causes a received
     * message to become writable
     *
     * @throws Exception if an error occurs during the test.
     */
    @Test
    public void testClearBodyOnReceivedObjectMessageMakesMessageWritable() throws Exception {
        String content = "myStringContent";
        JmsObjectMessageFacade facade = new JmsTestObjectMessageFacade();
        facade.setObject(content);
        JmsObjectMessage objectMessage = new JmsObjectMessage(facade);
        objectMessage.onDispatch();

        assertTrue(objectMessage.isReadOnlyBody(), "Message should not be writable");
        objectMessage.clearBody();
        assertFalse(objectMessage.isReadOnlyBody(), "Message should be writable");
    }

    /**
     * Test that calling {@link ObjectMessage#clearBody()} of a received message
     * causes the body of the underlying message facade to be emptied.
     *
     * @throws Exception if an error occurs during the test.
     */
    @Test
    public void testClearBodyOnReceivedObjectMessageClearsUnderlyingMessageBody() throws Exception {
        String content = "myStringContent";
        JmsTestObjectMessageFacade facade = new JmsTestObjectMessageFacade();
        facade.setObject(content);
        JmsObjectMessage objectMessage = new JmsObjectMessage(facade);
        objectMessage.onDispatch();

        assertNotNull(facade.getSerializedObject(), "Expected body section but none was present");
        objectMessage.clearBody();

        // check that the returned object is now null
        assertNull(objectMessage.getObject(), "Unexpected object value");

        // verify the underlying message facade has no body
        assertNull(facade.getSerializedObject(), "Expected no body section");
    }

    /**
     * Test that setting an object on a new message and later getting the value, returns an
     * equal but different object that does not pick up intermediate changes to the set object.
     *
     * @throws Exception if an error occurs during the test.
     */
    @Test
    public void testSetThenGetObjectReturnsSnapshot() throws Exception
    {
        Map<String,String> origMap = new HashMap<String,String>();
        origMap.put("key1", "value1");

        JmsTestObjectMessageFacade facade = new JmsTestObjectMessageFacade();
        facade.setObject((Serializable) origMap);
        JmsObjectMessage objectMessage = new JmsObjectMessage(facade);
        objectMessage.onDispatch();

        // verify we get a different-but-equal object back
        Serializable serialized = objectMessage.getObject();
        assertTrue(serialized instanceof Map<?,?>, "Unexpected object type returned");
        Map<?,?> returnedObject1 = (Map<?,?>) serialized;
        assertNotSame(origMap, returnedObject1, "Expected different objects, due to snapshot being taken");
        assertEquals(origMap, returnedObject1, "Expected equal objects, due to snapshot being taken");

        // mutate the original object
        origMap.put("key2", "value2");

        // verify we get a different-but-equal object back when compared to the previously retrieved object
        Serializable serialized2 = objectMessage.getObject();
        assertTrue(serialized2 instanceof Map<?,?>, "Unexpected object type returned");
        Map<?,?> returnedObject2 = (Map<?,?>) serialized2;
        assertNotSame(origMap, returnedObject2, "Expected different objects, due to snapshot being taken");
        assertEquals(returnedObject1, returnedObject2, "Expected equal objects, due to snapshot being taken");

        // verify the mutated map is a different and not equal object
        assertNotSame(returnedObject1, returnedObject2, "Expected different objects, due to snapshot being taken");
        assertNotEquals(origMap, returnedObject2, "Expected objects to differ, due to snapshot being taken");
    }

    /**
     * Test that setting an object on a new message which contains non-serializable content results
     * in an {@link MessageFormatException} being thrown due to failure to encode the object.
     *
     * @throws Exception if an error occurs during the test.
     */
    @Test
    public void testSetObjectWithNonSerializableThrowsJMSMFE() throws Exception {
        Map<String, Object> origMap = new HashMap<String, Object>();
        origMap.put("key1", "value1");
        origMap.put("notSerializable", new NotSerializable());

        JmsObjectMessage objectMessage = factory.createObjectMessage();

        try {
            objectMessage.setObject((Serializable) origMap);
            fail("Expected exception to be thrown");
        } catch (MessageFormatException mfe) {
            // expected
        }
    }

    // Test class
    private static class NotSerializable {
        public NotSerializable() {}
    }

    /**
     * Test that failure during deserialization of an object in a message results
     * in an {@link MessageFormatException} being throw.
     *
     * @throws Exception if an error occurs during the test.
     */
    @Test
    public void testGetObjectWithFailedDeserialisationThrowsJMSMFE() throws Exception {
        assertThrows(MessageFormatException.class, () -> {
            JmsObjectMessageFacade facade = Mockito.mock(JmsTestObjectMessageFacade.class);
            Mockito.when(facade.getObject()).thenThrow(new ClassCastException("Failed to get object"));
            JmsObjectMessage objectMessage = new JmsObjectMessage(facade);
            objectMessage.getObject();
        });
    }

    @Test
    public void testBytes() throws JMSException, IOException {
        JmsObjectMessage msg = factory.createObjectMessage();
        String str = "testText";
        msg.setObject(str);

        msg = msg.copy();
        assertEquals(msg.getObject(), str);
    }

    @Test
    public void testSetObject() throws JMSException {
        JmsObjectMessage msg = factory.createObjectMessage();
        String str = "testText";
        msg.setObject(str);
        assertEquals(str, msg.getObject());
    }

    @Test
    public void testClearBody() throws JMSException {
        JmsObjectMessage objectMessage = factory.createObjectMessage();
        try {
            objectMessage.setObject("String");
            objectMessage.clearBody();
            assertFalse(objectMessage.isReadOnlyBody());
            assertNull(objectMessage.getObject());
            objectMessage.setObject("String");
            objectMessage.getObject();
        } catch (MessageNotWriteableException mnwe) {
            fail("should be writeable");
        }
    }

    @Test
    public void testReadOnlyBody() throws JMSException {
        JmsObjectMessage msg = factory.createObjectMessage();
        msg.setObject("test");
        msg.setReadOnlyBody(true);
        try {
            msg.getObject();
        } catch (MessageNotReadableException e) {
            fail("should be readable");
        }
        try {
            msg.setObject("test");
            fail("should throw exception");
        } catch (MessageNotWriteableException e) {
        }
    }

    @Test
    public void testWriteOnlyBody() throws JMSException { // should always be readable
        JmsObjectMessage msg = factory.createObjectMessage();
        msg.setReadOnlyBody(false);
        try {
            msg.setObject("test");
            msg.getObject();
        } catch (MessageNotReadableException e) {
            fail("should be readable");
        }
        msg.setReadOnlyBody(true);
        try {
            msg.getObject();
            msg.setObject("test");
            fail("should throw exception");
        } catch (MessageNotReadableException e) {
            fail("should be readable");
        } catch (MessageNotWriteableException mnwe) {
        }
    }
}
