/*
 * 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.felix.scr.impl.inject.methods;


import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.List;
import java.util.Map;

import org.apache.felix.scr.impl.inject.ActivatorParameter;
import org.apache.felix.scr.impl.inject.internal.ComponentMethodsImpl;
import org.apache.felix.scr.impl.logger.ComponentLogger;
import org.apache.felix.scr.impl.logger.MockComponentLogger;
import org.apache.felix.scr.impl.manager.ComponentActivator;
import org.apache.felix.scr.impl.manager.ComponentContainer;
import org.apache.felix.scr.impl.manager.ComponentContextImpl;
import org.apache.felix.scr.impl.manager.SingleComponentManager;
import org.apache.felix.scr.impl.metadata.ComponentMetadata;
import org.apache.felix.scr.impl.metadata.DSVersion;
import org.apache.felix.scr.impl.metadata.instances.AcceptMethod;
import org.apache.felix.scr.impl.metadata.instances.BaseObject;
import org.apache.felix.scr.impl.metadata.instances.Level1Object;
import org.apache.felix.scr.impl.metadata.instances.Level3Object;
import org.apache.felix.scr.impl.metadata.instances2.Level2Object;
import org.mockito.Mockito;
import org.osgi.framework.Bundle;
import org.osgi.framework.BundleContext;
import org.osgi.service.component.ComponentContext;

import junit.framework.TestCase;


public class ActivateMethodTest extends TestCase
{

    private static final Class ACCEPT_METHOD_CLASS = AcceptMethod.class;

    private Bundle m_bundle;

    BaseObject base = new BaseObject();

    Level1Object level1 = new Level1Object();

    Level2Object level2 = new Level2Object();

    Level3Object level3 = new Level3Object();

    @Override
    protected void setUp() throws Exception
    {
        super.setUp();
        m_bundle = Mockito.mock( Bundle.class );
        BundleContext context = Mockito.mock( BundleContext.class );
        Mockito.when(context.getBundle()).thenReturn(m_bundle);
    }


    public void test_private_no_arg() throws Exception
    {
        checkMethod( base, "activate_no_arg" );

        // activate_no_arg is private to BaseObject and must not be
        // accessible from extensions
        ensureMethodNotFoundMethod( level1, "activate_no_arg" );
        ensureMethodNotFoundMethod( level2, "activate_no_arg" );
        ensureMethodNotFoundMethod( level3, "activate_no_arg" );
    }


    public void test_protected_activate_comp() throws Exception
    {
        // activate_comp is protected in BaseObject and must be accessible
        // in all instances
        checkMethod( base, "activate_comp" );
        checkMethod( level1, "activate_comp" );
        checkMethod( level2, "activate_comp" );
        checkMethod( level3, "activate_comp" );
    }


    public void test_private_activate_level1_bundle() throws Exception
    {
        // activate_level1_bundle is private in Level1Object and must be
        // accessible in Level1Object only
        ensureMethodNotFoundMethod( base, "activate_level1_bundle" );
        checkMethod( level1, "activate_level1_bundle" );
        ensureMethodNotFoundMethod( level2, "activate_level1_bundle" );
        ensureMethodNotFoundMethod( level3, "activate_level1_bundle" );
    }


    public void test_protected_activate_level1_map() throws Exception
    {
        // activate_level1_map is protected in Level1Object and must be
        // accessible in Level1Object and extensions but not in BaseObject
        ensureMethodNotFoundMethod( base, "activate_level1_map" );
        checkMethod( level1, "activate_level1_map" );
        checkMethod( level2, "activate_level1_map" );
        checkMethod( level3, "activate_level1_map" );
    }


    public void test_private_activate_comp_map() throws Exception
    {
        // private_activate_comp_map is private in Level2Object and must be
        // accessible in Level2Object only
        ensureMethodNotFoundMethod( base, "activate_comp_map" );
        ensureMethodNotFoundMethod( level1, "activate_comp_map" );
        checkMethod( level2, "activate_comp_map" );
        checkMethod( level3, "activate_comp_map" );
    }


    public void test_public_activate_collision() throws Exception
    {
        // activate_collision is private in Level2Object and must be
        // accessible in Level2Object only.
        // also the method is available taking no arguments and a single
        // map argument which takes precedence and which we expect
        ensureMethodNotFoundMethod( base, "activate_collision" );
        ensureMethodNotFoundMethod( level1, "activate_collision" );
        checkMethod( level2, "activate_collision" );
        checkMethod( level3, "activate_collision" );
    }


    public void test_package_activate_comp_bundle() throws Exception
    {
        // activate_comp_bundle is package private and thus visible in
        // base and level1 but not in level 2 (different package) and
        // level 3 (inheritance through different package)

        checkMethod( base, "activate_comp_bundle" );
        checkMethod( level1, "activate_comp_bundle" );
        ensureMethodNotFoundMethod( level2, "activate_comp_bundle" );
        ensureMethodNotFoundMethod( level3, "activate_comp_bundle" );
    }


    public void test_getPackage() throws Exception
    {
        Class dpc = getClass().getClassLoader().loadClass( "DefaultPackageClass" );
        assertEquals( "", BaseMethod.getPackageName( dpc ) );

        assertEquals( "org.apache.felix.scr.impl.metadata.instances", BaseMethod.getPackageName( base.getClass() ) );
    }


    public void test_accept() throws Exception
    {
        // public visible unless returning non-void
        assertMethod( true, "public_void", false, false );
        assertMethod( false, "public_string", false, false );

        // protected visible unless returning non-void
        assertMethod( true, "protected_void", false, false );
        assertMethod( false, "protected_string", false, false );

        // private not visible
        assertMethod( false, "private_void", false, false );
        assertMethod( false, "private_string", false, false );

        // private visible unless returning non-void
        assertMethod( true, "private_void", true, false );
        assertMethod( false, "private_string", true, false );
        assertMethod( true, "private_void", true, true );
        assertMethod( false, "private_string", true, true );

        // private not visible, accept package is ignored
        assertMethod( false, "private_void", false, true );
        assertMethod( false, "private_string", false, true );

        // package not visible
        assertMethod( false, "package_void", false, false );
        assertMethod( false, "package_string", false, false );

        // package visible unless returning non-void
        assertMethod( true, "package_void", false, true );
        assertMethod( false, "package_string", false, true );
        assertMethod( true, "package_void", true, true );
        assertMethod( false, "package_string", true, true );

        // package not visible, accept private is ignored
        assertMethod( false, "package_void", true, false );
        assertMethod( false, "package_string", true, false );
    }


    public void test_suitable_method_selection() throws Exception
    {
        // this would be the protected BaseObject.activate_suitable
        checkMethod( base, "activate_suitable" );
        checkMethod( level1, "activate_suitable" );

        // this would be the private Level2Object.activate_suitable
        checkMethod( level2, "activate_suitable" );

        // this must fail to find a method, since Level2Object's activate_suitable
        // is private and terminates the search for Level3Object
        ensureMethodNotFoundMethod( level3, "activate_suitable" );
    }

    public void test_unsuitable_method_selection() throws Exception
    {
        //check that finding an unsuitable method does not prevent finding
        // a lower precedence suitable method.

        checkMethod( level2, "activate_comp_unsuitable" );

        checkMethod( level3, "activate_comp_unsuitable" );
    }

    public void test_precedence() throws Exception
    {
        //All tested methods are only in base.  They differ in arguments and visibility.
        //R4.2 compendium  112.5.8
        //private method, arg ComponentContext
        checkMethod( base, "activate_precedence_1", "activate_precedence_1_comp" );
        //package method, arg BundleContext
        checkMethod( level1, "activate_precedence_1", "activate_precedence_1_bundleContext" );
        //protected method, arg Map
        checkMethod( level2, "activate_precedence_1", "activate_precedence_1_map" );

        //private method, arg Map
        checkMethod( base, "activate_precedence_2", "activate_precedence_2_map" );
        //package method, args ComponentContext and Map
        checkMethod( level1, "activate_precedence_2", "activate_precedence_2_comp_bundleContext" );
        //protected method, no args
        checkMethod( level2, "activate_precedence_2", "activate_precedence_2_empty" );
    }

    //---------- internal

    /**
     * Checks whether a method with the given name can be found for the
     * activate/deactivate method parameter list and whether the method returns
     * its name when called.
     *
     * @param obj
     * @param methodName
     */
    private void checkMethod( BaseObject obj, String methodName )
    {
        checkMethod( obj, methodName, methodName, DSVersion.DS11 );
    }

    /**
     * Checks whether a method with the given name can be found for the
     * activate/deactivate method parameter list and whether the method returns
     * the expected description when called.
     *
     * @param obj
     * @param methodName
     * @param methodDesc
     */
    private void checkMethod( BaseObject obj, String methodName, String methodDesc )
    {
        checkMethod(obj, methodName, methodDesc, DSVersion.DS11);
    }


    /**
     * Checks whether a method with the given name can be found for the
     * activate/deactivate method parameter list and whether the method returns
     * the expected description when called.
     *
     * @param obj
     * @param methodName
     * @param methodDesc
     * @param version DSVersion tested
     */
    private void checkMethod( BaseObject obj, String methodName, String methodDesc, DSVersion version )
    {
        ComponentContainer<?> container = newContainer();
        SingleComponentManager<?> icm = new SingleComponentManager( container, new ComponentMethodsImpl() );
        ActivateMethod am = new ActivateMethod( methodName, methodName != null, obj.getClass(), version, false, false );

        am.invoke( obj, new ActivatorParameter( new ComponentContextImpl(icm, m_bundle, null), -1 ), null );
        Method m = am.getMethod();
        assertNotNull( m );
        assertEquals( methodName, m.getName() );
        assertEquals( methodDesc, obj.getCalledMethod() );
    }


    private ComponentContainer newContainer()
    {
        final ComponentMetadata metadata = newMetadata();
        ComponentContainer container = new ComponentContainer() {

            @Override
            public ComponentActivator getActivator()
            {
                final ComponentActivator ca = Mockito.mock(ComponentActivator.class);
                Mockito.when(ca.getBundleContext()).thenReturn(Mockito.mock(BundleContext.class));
                return ca;
            }

            @Override
            public ComponentMetadata getComponentMetadata()
            {
                return metadata;
            }

            @Override
            public void disposed(SingleComponentManager component)
            {
            }

            public boolean isEnabled()
            {
                return false;
            }

            @Override
            public ComponentLogger getLogger() {
                return new MockComponentLogger();
            }
        };
        return container;
    }


	private ComponentMetadata newMetadata() {
		ComponentMetadata metadata = new ComponentMetadata( DSVersion.DS11 );
        metadata.setName("foo");
        metadata.setImplementationClassName(Object.class.getName());
        metadata.validate();
		return metadata;
	}


    /**
     * Ensures no method with the given name accepting any of the
     * activate/deactive method parameters can be found.
     *
     * @param obj
     * @param methodName
     * @throws InvocationTargetException
     * @throws IllegalAccessException
     */
    private void ensureMethodNotFoundMethod( BaseObject obj, String methodName )
    {
        ensureMethodNotFoundMethod(obj, methodName, DSVersion.DS11);
    }


    /**
     * Ensures no method with the given name accepting any of the
     * activate/deactive method parameters can be found.
     *
     * @param obj
     * @param methodName
     * @param version DS version tested
     * @throws InvocationTargetException
     * @throws IllegalAccessException
     */
    private void ensureMethodNotFoundMethod( BaseObject obj, String methodName, DSVersion version )
    {
        ComponentContainer container = newContainer();
        SingleComponentManager icm = new SingleComponentManager( container, new ComponentMethodsImpl() );
        ActivateMethod am = new ActivateMethod( methodName, methodName != null, obj.getClass(), version, false, false );
        am.invoke( obj, new ActivatorParameter( new ComponentContextImpl(icm, m_bundle, null), -1 ), null );
        Method m = am.getMethod();
        assertNull( m );
        assertNull( obj.getCalledMethod() );
    }


    private void assertMethod( boolean expected, String methodName, boolean acceptPrivate, boolean acceptPackage )
        throws NoSuchMethodException
    {
        Method method = ACCEPT_METHOD_CLASS.getDeclaredMethod( methodName, null );
        boolean accepted = BaseMethod.accept( method, acceptPrivate, acceptPackage, false );
        assertEquals( expected, accepted );
    }

    private static @interface Ann{}
    private static class Sort
    {
        public void a(Ann ann) {};
        public void a(int c) {};
        public void a(Integer c) {};
        public void a(BundleContext c) {};
        public void a(Map m) {};
        public void a() {};
        public void a(ComponentContext cc) {};
        public void a(ComponentContext cc, BundleContext c) {};
        public void b() {};

    }
    public void testMethodSorting() throws Exception
    {
        ActivateMethod am = new ActivateMethod( "a", true, Sort.class, DSVersion.DS11, false, false );
        List<Method> ms = am.getSortedMethods(Sort.class);
        assertEquals(8, ms.size());
        assertEquals(1, ms.get(0).getParameterTypes().length);
        assertEquals(ComponentContext.class, ms.get(0).getParameterTypes()[0]);
        assertEquals(1, ms.get(1).getParameterTypes().length);
        assertEquals(BundleContext.class, ms.get(1).getParameterTypes()[0]);
        assertEquals(1, ms.get(2).getParameterTypes().length);
        assertEquals(Ann.class, ms.get(2).getParameterTypes()[0]);
        assertEquals(1, ms.get(3).getParameterTypes().length);
        assertEquals(Map.class, ms.get(3).getParameterTypes()[0]);
        assertEquals(1, ms.get(4).getParameterTypes().length);
        assertEquals(int.class, ms.get(4).getParameterTypes()[0]);
        assertEquals(1, ms.get(5).getParameterTypes().length);
        assertEquals(Integer.class, ms.get(5).getParameterTypes()[0]);
        assertEquals(2, ms.get(6).getParameterTypes().length);
        assertEquals(0, ms.get(7).getParameterTypes().length);
    }

    public void test_13_annos() throws Exception
    {
        checkMethod(base, "activate_13_2_annotations", "activate_13_2_annotations", DSVersion.DS13 );
    }

}
