/**
 * 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.cxf.jaxrs.impl;

import java.io.IOException;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.logging.Handler;
import java.util.logging.LogRecord;

import javax.ws.rs.ConstrainedTo;
import javax.ws.rs.RuntimeType;
import javax.ws.rs.client.Client;
import javax.ws.rs.client.ClientRequestContext;
import javax.ws.rs.client.ClientRequestFilter;
import javax.ws.rs.client.ClientResponseContext;
import javax.ws.rs.client.ClientResponseFilter;
import javax.ws.rs.container.ContainerRequestContext;
import javax.ws.rs.container.ContainerRequestFilter;
import javax.ws.rs.container.ContainerResponseContext;
import javax.ws.rs.container.ContainerResponseFilter;
import javax.ws.rs.core.Configurable;
import javax.ws.rs.core.Configuration;
import javax.ws.rs.core.Feature;
import javax.ws.rs.core.FeatureContext;
import javax.ws.rs.ext.MessageBodyReader;

import org.apache.cxf.common.logging.LogUtils;

import org.junit.Test;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;

public class ConfigurationImplTest {

    @Test
    public void testIsRegistered() throws Exception {
//        ConfigurationImpl c = new ConfigurationImpl(RuntimeType.SERVER);
//        ContainerResponseFilter filter = new ContainerResponseFilterImpl();
//        assertTrue(c.register(filter,
//                              Collections.<Class<?>, Integer>singletonMap(ContainerResponseFilter.class, 1000)));
//        assertTrue(c.isRegistered(filter));
//        assertFalse(c.isRegistered(new ContainerResponseFilterImpl()));
//        assertTrue(c.isRegistered(ContainerResponseFilterImpl.class));
//        assertFalse(c.isRegistered(ContainerResponseFilter.class));
//        assertFalse(c.register(filter,
//                               Collections.<Class<?>, Integer>singletonMap(ContainerResponseFilter.class, 1000)));
//        assertFalse(c.register(ContainerResponseFilterImpl.class,
//                               Collections.<Class<?>, Integer>singletonMap(ContainerResponseFilter.class, 1000)));
        doTestIsFilterRegistered(new ContainerResponseFilterImpl(), ContainerResponseFilterImpl.class);
    }
    
    @Test
    public void testIsRegisteredSubClass() throws Exception {
        doTestIsFilterRegistered(new ContainerResponseFilterSubClassImpl(), ContainerResponseFilterSubClassImpl.class);
    }

    private void doTestIsFilterRegistered(Object provider, Class<?> providerClass) throws Exception {
        ConfigurationImpl c = new ConfigurationImpl(RuntimeType.SERVER);
        assertTrue(c.register(provider,
                              Collections.<Class<?>, Integer>singletonMap(ContainerResponseFilter.class, 1000)));
        assertTrue(c.isRegistered(provider));
        assertFalse(c.isRegistered(providerClass.newInstance()));
        assertTrue(c.isRegistered(providerClass));
        assertFalse(c.isRegistered(ContainerResponseFilter.class));
        assertFalse(c.register(provider,
                               Collections.<Class<?>, Integer>singletonMap(ContainerResponseFilter.class, 1000)));
        assertFalse(c.register(providerClass,
                               Collections.<Class<?>, Integer>singletonMap(ContainerResponseFilter.class, 1000)));
    }

    @ConstrainedTo(RuntimeType.SERVER)
    public static class ContainerResponseFilterImpl implements ContainerResponseFilter {

        @Override
        public void filter(ContainerRequestContext requestContext, ContainerResponseContext responseContext)
            throws IOException {
        }

    }

    public static class ContainerResponseFilterSubClassImpl extends ContainerResponseFilterImpl { }

    @ConstrainedTo(RuntimeType.CLIENT)
    public static class ClientResponseFilterImpl implements ClientResponseFilter {

        @Override
        public void filter(ClientRequestContext requestContext, ClientResponseContext responseContext)
            throws IOException {
        }

    }
    static class TestHandler extends Handler {

        List<String> messages = new ArrayList<>();

        /** {@inheritDoc}*/
        @Override
        public void publish(LogRecord record) {
            messages.add(record.getLevel().toString() + ": " + record.getMessage());
        }

        /** {@inheritDoc}*/
        @Override
        public void flush() {
            // no-op
        }

        /** {@inheritDoc}*/
        @Override
        public void close() throws SecurityException {
            // no-op
        }
    }

    @Test
    public void testInvalidContract() {
        TestHandler handler = new TestHandler();
        LogUtils.getL7dLogger(ConfigurationImpl.class).addHandler(handler);

        ConfigurationImpl c = new ConfigurationImpl(RuntimeType.SERVER);
        ContainerResponseFilter filter = new ContainerResponseFilterImpl();
        assertFalse(c.register(filter,
                               Collections.<Class<?>, Integer>singletonMap(MessageBodyReader.class, 1000)));

        for (String message : handler.messages) {
            if (message.startsWith("WARN") && message.contains("does not implement specified contract")) {
                return; // success
            }
        }
        fail("did not log expected message");
    }

    public static class TestFilter implements ContainerRequestFilter, ContainerResponseFilter, 
    ClientRequestFilter, ClientResponseFilter {

        @Override
        public void filter(ClientRequestContext paramClientRequestContext,
                           ClientResponseContext paramClientResponseContext)
                               throws IOException {
            // no-op
        }

        @Override
        public void filter(ClientRequestContext paramClientRequestContext) throws IOException {
            // no-op
        }

        @Override
        public void filter(ContainerRequestContext paramContainerRequestContext,
                           ContainerResponseContext paramContainerResponseContext)
                               throws IOException {
            // no-op
        }

        @Override
        public void filter(ContainerRequestContext paramContainerRequestContext) throws IOException {
            // no-op
        }
    }

    public interface MyClientFilter extends ClientRequestFilter, ClientResponseFilter {
        // reduced to just the intermediate layer. Could contain user code
    }

    public static class NestedInterfaceTestFilter implements MyClientFilter {

        @Override
        public void filter(ClientRequestContext requestContext) throws IOException {
            // no-op
        }

        @Override
        public void filter(ClientRequestContext requestContext, ClientResponseContext responseContext)
                throws IOException {
            // no-op
        }
    }

    private Client createClientProxy() {
        return (Client) Proxy.newProxyInstance(this.getClass().getClassLoader(), 
            new Class<?>[]{Client.class},
            new InvocationHandler() {

                @Override
                public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                    return null; //no-op
                } });
    }

    @Test
    public void testSubClassIsRegisteredOnConfigurable() {
        FeatureContextImpl featureContext = new FeatureContextImpl();
        Configurable<FeatureContext> configurable = new ConfigurableImpl<>(featureContext, RuntimeType.SERVER);
        featureContext.setConfigurable(configurable);
        featureContext.register(ContainerResponseFilterSubClassImpl.class);
        Configuration config = configurable.getConfiguration();
        Map<Class<?>, Integer> contracts = config.getContracts(ContainerResponseFilter.class);
        assertEquals(1, contracts.size());
        assertTrue(contracts.containsKey(ContainerResponseFilter.class));
    }
    
    @Test
    public void testServerFilterContractsOnClientIsRejected() {
        try (ConfigurableImpl<Client> configurable 
                = new ConfigurableImpl<>(createClientProxy(), RuntimeType.CLIENT)) {
            Configuration config = configurable.getConfiguration();
            configurable.register(TestFilter.class);
            Map<Class<?>, Integer> contracts = config.getContracts(TestFilter.class);
            assertTrue(contracts.containsKey(ClientRequestFilter.class));
            assertTrue(contracts.containsKey(ClientResponseFilter.class));
            assertFalse(contracts.containsKey(ContainerRequestFilter.class));
            assertFalse(contracts.containsKey(ContainerResponseFilter.class));
        }
    }

    @Test
    public void testClientFilterWithNestedInterfacesIsAccepted() {
        try (ConfigurableImpl<Client> configurable 
                = new ConfigurableImpl<>(createClientProxy(), RuntimeType.CLIENT)) {
            Configuration config = configurable.getConfiguration();
            configurable.register(NestedInterfaceTestFilter.class);
            Map<Class<?>, Integer> contracts = config.getContracts(NestedInterfaceTestFilter.class);
            assertTrue(contracts.containsKey(ClientRequestFilter.class));
            assertTrue(contracts.containsKey(ClientResponseFilter.class));
        }
    }

    @Test
    public void testClientFilterContractsOnServerFeatureIsRejected() {
        FeatureContextImpl featureContext = new FeatureContextImpl();
        Configurable<FeatureContext> configurable = new ConfigurableImpl<>(featureContext, RuntimeType.SERVER);
        featureContext.setConfigurable(configurable);
        featureContext.register(TestFilter.class);
        Configuration config = configurable.getConfiguration();
        Map<Class<?>, Integer> contracts = config.getContracts(TestFilter.class);
        assertFalse(contracts.containsKey(ClientRequestFilter.class));
        assertFalse(contracts.containsKey(ClientResponseFilter.class));
        assertTrue(contracts.containsKey(ContainerRequestFilter.class));
        assertTrue(contracts.containsKey(ContainerResponseFilter.class));
    }

    public static class DisablableFeature implements Feature {

        boolean enabled;

        /** {@inheritDoc}*/
        @Override
        public boolean configure(FeatureContext context) {
            return enabled;
        }

    }

    @Test
    public void testFeatureDisabledClass() {
        FeatureContextImpl featureContext = new FeatureContextImpl();
        Configurable<FeatureContext> configurable = new ConfigurableImpl<>(featureContext, RuntimeType.SERVER);
        featureContext.setConfigurable(configurable);
        featureContext.register(DisablableFeature.class);

        Configuration config = configurable.getConfiguration();
        assertFalse(config.isEnabled(DisablableFeature.class));
    }

    @Test
    public void testFeatureDisabledInstance() {
        FeatureContextImpl featureContext = new FeatureContextImpl();
        Configurable<FeatureContext> configurable = new ConfigurableImpl<>(featureContext, RuntimeType.SERVER);
        featureContext.setConfigurable(configurable);
        Feature feature = new DisablableFeature();
        featureContext.register(feature);

        Configuration config = configurable.getConfiguration();
        assertFalse(config.isEnabled(feature));
    }

    @Test 
    public void testIsEnabledWithMultipleFeaturesOfSameType() {
        FeatureContextImpl featureContext = new FeatureContextImpl();
        Configurable<FeatureContext> configurable = new ConfigurableImpl<>(featureContext, RuntimeType.SERVER);
        featureContext.setConfigurable(configurable);

        featureContext.register(new DisablableFeature());
        featureContext.register(new DisablableFeature());
        featureContext.register(new DisablableFeature());

        Configuration config = configurable.getConfiguration();
        assertEquals(3, config.getInstances().size());
        assertFalse(config.isEnabled(DisablableFeature.class));

        DisablableFeature enabledFeature = new DisablableFeature();
        enabledFeature.enabled = true;

        featureContext.register(enabledFeature);
        assertEquals(4, config.getInstances().size());
        assertTrue(config.isEnabled(DisablableFeature.class));

        featureContext.register(new DisablableFeature());
        assertEquals(5, config.getInstances().size());
        assertTrue(config.isEnabled(DisablableFeature.class));
    }

    @ConstrainedTo(RuntimeType.SERVER)
    public static class ClientFilterConstrainedToServer implements ClientRequestFilter {

        /** {@inheritDoc}*/
        @Override
        public void filter(ClientRequestContext paramClientRequestContext) throws IOException {
            // no-op
        }
        
    }

    @Test
    public void testInvalidConstraintOnProvider() {
        TestHandler handler = new TestHandler();
        LogUtils.getL7dLogger(ConfigurableImpl.class).addHandler(handler);

        try (ConfigurableImpl<Client> configurable 
                = new ConfigurableImpl<>(createClientProxy(), RuntimeType.CLIENT)) {
            Configuration config = configurable.getConfiguration();
    
            configurable.register(ClientFilterConstrainedToServer.class);
    
            assertEquals(0, config.getInstances().size());
    
            for (String message : handler.messages) {
                if (message.startsWith("WARN") && message.contains("cannot be registered in ")) {
                    return; // success
                }
            }
        }
        fail("did not log expected message");
    }

    
    @Test
    public void testChecksConstrainedToAnnotationDuringRegistration() {
        TestHandler handler = new TestHandler();
        LogUtils.getL7dLogger(ConfigurableImpl.class).addHandler(handler);

        try (ConfigurableImpl<Client> configurable 
                = new ConfigurableImpl<>(createClientProxy(), RuntimeType.CLIENT)) {
            Configuration config = configurable.getConfiguration();
    
            configurable.register(ContainerResponseFilterImpl.class);
    
            assertEquals(0, config.getInstances().size());
    
            for (String message : handler.messages) {
                if (message.startsWith("WARN") && message.contains("Null, empty or invalid contracts specified")) {
                    return; // success
                }
            }
        }
        fail("did not log expected message");
    }
}