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

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.Assertions.fail;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.ObjectOutputStream;
import java.util.HashMap;
import java.util.UUID;
import java.util.Vector;

import org.apache.qpid.jms.JmsDestination;
import org.apache.qpid.jms.JmsQueue;
import org.apache.qpid.jms.util.ClassLoadingAwareObjectInputStream;
import org.apache.qpid.jms.util.ClassLoadingAwareObjectInputStream.TrustedClassFilter;
import org.junit.jupiter.api.Test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class JmsDefaultDeserializationPolicyTest {

    private static final Logger LOG = LoggerFactory.getLogger(JmsDefaultDeserializationPolicyTest.class);

    @Test
    public void testIsTrustedType() {
        JmsDestination destination = new JmsQueue("test-queue");
        JmsDefaultDeserializationPolicy policy = new JmsDefaultDeserializationPolicy();

        assertTrue(policy.isTrustedType(destination, null));
        assertTrue(policy.isTrustedType(destination, UUID.class));
        assertTrue(policy.isTrustedType(destination, String.class));
        assertTrue(policy.isTrustedType(destination, Boolean.class));
        assertTrue(policy.isTrustedType(destination, Double.class));
        assertTrue(policy.isTrustedType(destination, Object.class));

        // Only types in lang
        policy.setAllowList("java.lang");

        assertTrue(policy.isTrustedType(destination, null));
        assertFalse(policy.isTrustedType(destination, UUID.class));
        assertTrue(policy.isTrustedType(destination, String.class));
        assertTrue(policy.isTrustedType(destination, Boolean.class));
        assertFalse(policy.isTrustedType(destination, getClass()));

        // Entry must be complete package name prefix to match
        // i.e while "java.n" is a prefix of "java.net", this
        // wont match the socket class below.
        policy.setAllowList("java.n");
        assertFalse(policy.isTrustedType(destination, UUID.class));
        assertFalse(policy.isTrustedType(destination, String.class));
        assertFalse(policy.isTrustedType(destination, java.net.Socket.class));

        // add a non-core package
        policy.setAllowList("java.lang,org.apache.qpid.jms");

        assertFalse(policy.isTrustedType(destination, UUID.class));
        assertTrue(policy.isTrustedType(destination, String.class));
        assertTrue(policy.isTrustedType(destination, getClass()));

        // Try with a class-specific entry
        policy.setAllowList("java.lang.Integer");

        assertTrue(policy.isTrustedType(destination, Integer.class));
        assertFalse(policy.isTrustedType(destination, Boolean.class));

        // Verify deny list overrides allow list
        policy.setAllowList("java.lang.Integer");
        policy.setDenyList("java.lang.Integer");

        assertFalse(policy.isTrustedType(destination, Integer.class));

        // Verify deny list entry prefix overrides allow list
        policy.setAllowList("java.lang.Integer");
        policy.setDenyList("java.lang");

        assertFalse(policy.isTrustedType(destination, Integer.class));

        // Verify deny list catch-all overrides allow list
        policy.setAllowList("java.lang.Integer");
        policy.setDenyList("*");

        assertFalse(policy.isTrustedType(destination, Integer.class));
    }

    @Test
    public void testHashCode() {
        JmsDeserializationPolicy policy1 = new JmsDefaultDeserializationPolicy();
        JmsDeserializationPolicy policy2 = new JmsDefaultDeserializationPolicy();

        assertTrue(policy1.hashCode() != 0);
        assertEquals(policy1.hashCode(), policy2.hashCode());
        assertEquals(policy2.hashCode(), policy1.hashCode());

        ((JmsDefaultDeserializationPolicy) policy1).setAllowList("java.util");

        assertFalse(policy1.hashCode() == policy2.hashCode());
        assertFalse(policy2.hashCode() == policy1.hashCode());

        ((JmsDefaultDeserializationPolicy) policy2).setAllowList("java.util");

        assertTrue(policy1.hashCode() == policy2.hashCode());
        assertTrue(policy2.hashCode() == policy1.hashCode());

        ((JmsDefaultDeserializationPolicy) policy1).setDenyList("java.util");

        assertFalse(policy1.hashCode() == policy2.hashCode());
        assertFalse(policy2.hashCode() == policy1.hashCode());

        ((JmsDefaultDeserializationPolicy) policy2).setDenyList("java.util");

        assertTrue(policy1.hashCode() == policy2.hashCode());
        assertTrue(policy2.hashCode() == policy1.hashCode());
    }

    @SuppressWarnings("unlikely-arg-type")
    @Test
    public void testEqualsObject() {
        JmsDefaultDeserializationPolicy policy1 = new JmsDefaultDeserializationPolicy();
        JmsDefaultDeserializationPolicy policy2 = new JmsDefaultDeserializationPolicy();

        assertTrue(policy1.equals(policy1));
        assertTrue(policy1.equals(policy2));
        assertTrue(policy2.equals(policy1));

        policy1.setAllowList("java.util");

        assertFalse(policy1.equals(policy2));
        assertFalse(policy2.equals(policy1));

        assertFalse(policy1.equals(null));
        assertFalse(policy1.equals(""));
        assertFalse(policy1.equals(this));

        policy2.setAllowList("java.util");
        assertTrue(policy1.equals(policy2));

        policy1.setDenyList("java.util");

        assertFalse(policy1.equals(policy2));
        assertFalse(policy2.equals(policy1));

        policy2.setDenyList("java.util");
        assertTrue(policy1.equals(policy2));
        assertTrue(policy2.equals(policy1));
    }

    @Deprecated
    @Test
    public void testJmsDefaultDeserializationPolicyDeprecated() {
        JmsDefaultDeserializationPolicy policy = new JmsDefaultDeserializationPolicy();

        assertFalse(policy.getWhiteList().isEmpty());
        assertTrue(policy.getBlackList().isEmpty());
    }

    @Test
    public void testJmsDefaultDeserializationPolicy() {
        JmsDefaultDeserializationPolicy policy = new JmsDefaultDeserializationPolicy();

        assertFalse(policy.getAllowList().isEmpty());
        assertTrue(policy.getDenyList().isEmpty());
    }

    @Deprecated
    @Test
    public void testJmsDefaultDeserializationPolicyCopyCtorDeprecated() {
        JmsDefaultDeserializationPolicy policy = new JmsDefaultDeserializationPolicy();

        policy.setWhiteList("a.b.c");
        policy.setBlackList("d.e.f");

        JmsDefaultDeserializationPolicy copy = new JmsDefaultDeserializationPolicy(policy);

        assertEquals("a.b.c", copy.getWhiteList());
        assertEquals("d.e.f", copy.getBlackList());
    }

    @Test
    public void testJmsDefaultDeserializationPolicyCopyCtor() {
        JmsDefaultDeserializationPolicy policy = new JmsDefaultDeserializationPolicy();

        policy.setAllowList("a.b.c");
        policy.setDenyList("d.e.f");

        JmsDefaultDeserializationPolicy copy = new JmsDefaultDeserializationPolicy(policy);

        assertEquals("a.b.c", copy.getAllowList());
        assertEquals("d.e.f", copy.getDenyList());
    }

    @Deprecated
    @Test
    public void testJmsDefaultDeserializationPolicyCopyDeprecated() {
        JmsDefaultDeserializationPolicy policy = new JmsDefaultDeserializationPolicy();

        policy.setWhiteList("a.b.c");
        policy.setBlackList("d.e.f");

        JmsDefaultDeserializationPolicy copy = (JmsDefaultDeserializationPolicy) policy.copy();

        assertEquals("a.b.c", copy.getWhiteList());
        assertEquals("d.e.f", copy.getBlackList());
    }

    @Test
    public void testJmsDefaultDeserializationPolicyCopy() {
        JmsDefaultDeserializationPolicy policy = new JmsDefaultDeserializationPolicy();

        policy.setAllowList("a.b.c");
        policy.setDenyList("d.e.f");

        JmsDefaultDeserializationPolicy copy = (JmsDefaultDeserializationPolicy) policy.copy();

        assertEquals("a.b.c", copy.getAllowList());
        assertEquals("d.e.f", copy.getDenyList());
    }

    @Deprecated
    @Test
    public void testSetWhiteList() {
        JmsDefaultDeserializationPolicy policy = new JmsDefaultDeserializationPolicy();
        assertNotNull(policy.getWhiteList());

        policy.setWhiteList(null);
        assertNotNull(policy.getWhiteList());
        assertTrue(policy.getWhiteList().isEmpty());
        assertNotNull(policy.getAllowList());
        assertTrue(policy.getAllowList().isEmpty());

        policy.setWhiteList("");
        assertNotNull(policy.getWhiteList());
        assertTrue(policy.getWhiteList().isEmpty());
        assertNotNull(policy.getAllowList());
        assertTrue(policy.getAllowList().isEmpty());

        policy.setWhiteList("*");
        assertNotNull(policy.getWhiteList());
        assertFalse(policy.getWhiteList().isEmpty());
        assertNotNull(policy.getAllowList());
        assertFalse(policy.getAllowList().isEmpty());

        policy.setWhiteList("a,b,c");
        assertNotNull(policy.getWhiteList());
        assertNotNull(policy.getAllowList());
        assertFalse(policy.getWhiteList().isEmpty());
        assertFalse(policy.getAllowList().isEmpty());
        assertEquals("a,b,c", policy.getWhiteList());
        assertEquals("a,b,c", policy.getAllowList());
    }

    @Test
    public void testSetAllowList() {
        JmsDefaultDeserializationPolicy policy = new JmsDefaultDeserializationPolicy();
        assertNotNull(policy.getAllowList());

        policy.setAllowList(null);
        assertNotNull(policy.getAllowList());
        assertTrue(policy.getAllowList().isEmpty());

        policy.setAllowList("");
        assertNotNull(policy.getAllowList());
        assertTrue(policy.getAllowList().isEmpty());

        policy.setAllowList("*");
        assertNotNull(policy.getAllowList());
        assertFalse(policy.getAllowList().isEmpty());

        policy.setAllowList("a,b,c");
        assertNotNull(policy.getAllowList());
        assertFalse(policy.getAllowList().isEmpty());
        assertEquals("a,b,c", policy.getAllowList());
    }

    @Deprecated
    @Test
    public void testSetBlackList() {
        JmsDefaultDeserializationPolicy policy = new JmsDefaultDeserializationPolicy();
        assertNotNull(policy.getBlackList());

        policy.setBlackList(null);
        assertNotNull(policy.getBlackList());
        assertTrue(policy.getBlackList().isEmpty());
        assertNotNull(policy.getDenyList());
        assertTrue(policy.getDenyList().isEmpty());

        policy.setBlackList("");
        assertNotNull(policy.getBlackList());
        assertTrue(policy.getBlackList().isEmpty());
        assertNotNull(policy.getDenyList());
        assertTrue(policy.getDenyList().isEmpty());

        policy.setBlackList("*");
        assertNotNull(policy.getBlackList());
        assertFalse(policy.getBlackList().isEmpty());
        assertNotNull(policy.getDenyList());
        assertFalse(policy.getDenyList().isEmpty());

        policy.setBlackList("a,b,c");
        assertNotNull(policy.getBlackList());
        assertFalse(policy.getBlackList().isEmpty());
        assertEquals("a,b,c", policy.getBlackList());
        assertNotNull(policy.getDenyList());
        assertFalse(policy.getDenyList().isEmpty());
        assertEquals("a,b,c", policy.getDenyList());
    }

    @Test
    public void testSetDenyList() {
        JmsDefaultDeserializationPolicy policy = new JmsDefaultDeserializationPolicy();
        assertNotNull(policy.getDenyList());

        policy.setDenyList(null);
        assertNotNull(policy.getDenyList());
        assertTrue(policy.getDenyList().isEmpty());

        policy.setDenyList("");
        assertNotNull(policy.getDenyList());
        assertTrue(policy.getDenyList().isEmpty());

        policy.setDenyList("*");
        assertNotNull(policy.getDenyList());
        assertFalse(policy.getDenyList().isEmpty());

        policy.setDenyList("a,b,c");
        assertNotNull(policy.getDenyList());
        assertFalse(policy.getDenyList().isEmpty());
        assertEquals("a,b,c", policy.getDenyList());
    }

    @Test
    public void testDeserializeVectorUsingPolicy() throws Exception {
        Vector<Object> vector = new Vector<Object>();
        vector.add("pi");
        vector.add(Integer.valueOf(314159));
        vector.add(new Vector<String>());
        vector.add(Boolean.FALSE);

        final JmsDefaultDeserializationPolicy policy = new JmsDefaultDeserializationPolicy();
        ByteArrayInputStream input = new ByteArrayInputStream(serializeObject(vector));
        TrustedClassFilter filter = new TrustedClassFilter() {

            @Override
            public boolean isTrusted(Class<?> clazz) {
                LOG.trace("Check for trust status of class: {}", clazz.getName());
                return policy.isTrustedType(new JmsQueue(), clazz);
            }
        };

        ClassLoadingAwareObjectInputStream reader = new ClassLoadingAwareObjectInputStream(input, filter);

        Object result = null;
        try {
            result = reader.readObject();
        } catch (Exception ex) {
            fail("Should no throw any errors");
        } finally {
            reader.close();
        }

        assertNotNull(result);
        assertTrue(result instanceof Vector);
        assertEquals(4, ((Vector<?>) result).size());
    }

    @SuppressWarnings("unchecked")
    @Test
    public void testDeserializeHashMapUsingPolicy() throws Exception {
        HashMap<Object, Object> map = new HashMap<Object, Object>();

        map.put("a", "Value");
        map.put("b", Integer.valueOf(1));
        map.put("c", new Vector<Object>());
        map.put("d", Boolean.FALSE);

        final JmsDefaultDeserializationPolicy policy = new JmsDefaultDeserializationPolicy();
        ByteArrayInputStream input = new ByteArrayInputStream(serializeObject(map));
        TrustedClassFilter filter = new TrustedClassFilter() {

            @Override
            public boolean isTrusted(Class<?> clazz) {
                LOG.trace("Check for trust status of class: {}", clazz.getName());
                return policy.isTrustedType(new JmsQueue(), clazz);
            }
        };

        ClassLoadingAwareObjectInputStream reader = new ClassLoadingAwareObjectInputStream(input, filter);

        Object result = null;
        try {
            result = reader.readObject();
        } catch (Exception ex) {
            fail("Should no throw any errors");
        } finally {
            reader.close();
        }

        assertNotNull(result);
        assertTrue(result instanceof HashMap);

        map = (HashMap<Object, Object>) result;

        assertEquals(4, map.size());

        assertEquals("Value", map.get("a"));
        assertEquals(Integer.valueOf(1), map.get("b"));
        assertEquals(new Vector<Object>(), map.get("c"));
        assertEquals(Boolean.FALSE, map.get("d"));
    }

    //----- Internal methods -------------------------------------------------//

    private byte[] serializeObject(Object value) throws IOException {
        byte[] result = new byte[0];

        if (value != null) {
            try (ByteArrayOutputStream baos = new ByteArrayOutputStream();
                 ObjectOutputStream oos = new ObjectOutputStream(baos)) {

                oos.writeObject(value);
                oos.flush();
                oos.close();

                result = baos.toByteArray();
            }
        }

        return result;
    }
}
