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

import static org.junit.Assert.assertNotNull;

import java.lang.reflect.InvocationTargetException;
import java.util.Hashtable;

import org.apache.felix.scr.integration.components.SimpleComponent;
import org.apache.felix.scr.integration.components.SimpleService;
import org.apache.felix.scr.integration.components.SimpleServiceImpl;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.ops4j.pax.exam.junit.PaxExam;
import org.osgi.framework.InvalidSyntaxException;
import org.osgi.service.component.ComponentFactory;
import org.osgi.service.component.ComponentInstance;
import org.osgi.service.component.runtime.dto.ComponentConfigurationDTO;
import org.osgi.service.log.LogService;

import junit.framework.TestCase;

@RunWith(PaxExam.class)
public class PersistentComponentFactoryTest extends ComponentTestBase
{

    static
    {
        descriptorFile = "/integration_test_persistent_factory_components.xml";
        // uncomment to enable debugging of this test class
        //        paxRunnerVmOption = DEBUG_VM_OPTION;
    }

    @Test
    public void test_component_factory() throws Exception
    {
        final String componentname = "factory.component";
        final String componentfactory = "factory.component.factory";

        getConfigurationsDisabledThenEnable( componentname, 0, -1 );

        TestCase.assertNull( SimpleComponent.INSTANCE );

        final ComponentInstance instance = createFactoryComponentInstance( componentfactory );

        // check registered components
        checkConfigurationCount( componentname, 1, ComponentConfigurationDTO.ACTIVE );

        instance.dispose();
        TestCase.assertNull( SimpleComponent.INSTANCE );
        TestCase.assertNull( instance.getInstance() ); // SCR 112.12.6.2

        checkConfigurationCount( componentname, 0, ComponentConfigurationDTO.ACTIVE );

    }

    @Test
    public void test_component_factory_disable_factory() throws Exception
    {
        // tests components remain alive after factory has been disabled

        final String componentname = "factory.component";
        final String componentfactory = "factory.component.factory";

        getConfigurationsDisabledThenEnable( componentname, 0, -1 );

        TestCase.assertNull( SimpleComponent.INSTANCE );

        final ComponentInstance instance = createFactoryComponentInstance( componentfactory );

        checkConfigurationCount( componentname, 1, ComponentConfigurationDTO.ACTIVE );

        // disable the factory
        disableAndCheck( componentname );
        delay();

        // factory is disabled but the instance is still alive
        TestCase.assertNotNull( SimpleComponent.INSTANCE );

        instance.dispose();
        TestCase.assertNull( SimpleComponent.INSTANCE );
        TestCase.assertNull( instance.getInstance() ); // SCR 112.12.6.2

    }

    @Test
    public void test_component_factory_newInstance_failure() throws Exception
    {
        final String componentname = "factory.component";
        final String componentfactory = "factory.component.factory";

        getConfigurationsDisabledThenEnable( componentname, 0, -1 );
        TestCase.assertNull( SimpleComponent.INSTANCE );

        Hashtable<String, String> props = new Hashtable<String, String>();
        props.put( PROP_NAME_FACTORY, PROP_NAME_FACTORY );
        props.put( SimpleComponent.PROP_ACTIVATE_FAILURE, "Requested Failure" );
        final ComponentFactory factory = getComponentFactory( componentfactory );

        final ComponentInstance instance = factory.newInstance( props );
        TestCase.assertNotNull( instance );

        checkConfigurationCount( componentname, 1, ComponentConfigurationDTO.FAILED_ACTIVATION );
    }

    @Test
    public void test_component_factory_require_configuration() throws Exception
    {
        final String componentname = "factory.component.configuration";
        final String componentfactory = "factory.component.factory.configuration";

        testConfiguredFactory( componentname, componentfactory, false, false );

    }

    @Test
    public void test_component_factory_optional_configuration() throws Exception
    {
        final String componentname = "factory.component.configuration.optional";
        final String componentfactory = "factory.component.factory.configuration.optional";

        testConfiguredFactory( componentname, componentfactory, true, true );

    }

    @Test
    public void test_component_factory_optional_configuration_13() throws Exception
    {
        final String componentname = "factory.component.configuration.optional.13";
        final String componentfactory = "factory.component.factory.configuration.optional.13";

        testConfiguredFactory( componentname, componentfactory, true, true );
    }

    @Test
    public void test_component_factory_optional_configuration_nomodify() throws Exception
    {
        final String componentname = "factory.component.configuration.optional.nomodify";
        final String componentfactory = "factory.component.factory.configuration.optional.nomodify";

        testConfiguredFactory( componentname, componentfactory, true, true );

    }

    private ComponentInstance testConfiguredFactory(final String componentname, final String componentfactory,
            boolean optional, boolean expectComponent)
                    throws InvocationTargetException, InterruptedException, InvalidSyntaxException
    {
        // ensure there is no configuration for the component
        deleteConfig( componentname );
        delay();

        getConfigurationsDisabledThenEnable( componentname, 0, -1 );
        TestCase.assertNull( SimpleComponent.INSTANCE );

        // At this point, since we don't have created the configuration, then the ComponentFactory
        // should not be available.

        checkFactory( componentfactory, optional );

        // supply configuration now and ensure active
        configure( componentname );
        delay();

        checkConfigurationCount( componentname, 0, ComponentConfigurationDTO.ACTIVE );
        TestCase.assertNull( SimpleComponent.INSTANCE );

        // get the component factory service
        final ComponentInstance instance = createFactoryComponentInstance( componentfactory );

        final Object instanceObject = instance.getInstance();
        TestCase.assertNotNull( instanceObject );
        TestCase.assertEquals( SimpleComponent.INSTANCE, instanceObject );
        TestCase.assertEquals( PROP_NAME_FACTORY, SimpleComponent.INSTANCE.getProperty( PROP_NAME_FACTORY ) );
        TestCase.assertEquals( PROP_NAME, SimpleComponent.INSTANCE.getProperty( PROP_NAME ) );

        checkConfigurationCount( componentname, 1, ComponentConfigurationDTO.ACTIVE );

        // delete config, ensure factory is not active anymore and component instance gone
        //(configuration required >> dispose of instance.  Also for pre-1.3 components, removing config unconditionally
        //deactivates component.
        deleteConfig( componentname );
        delay();

        checkFactory( componentfactory, optional );

        if ( expectComponent )
        {
            TestCase.assertNotNull( instance.getInstance() );
            TestCase.assertNotNull( SimpleComponent.INSTANCE );

            // with removal of the factory, the created instance should also be removed
            checkConfigurationCount( componentname, 1, ComponentConfigurationDTO.ACTIVE );
        }
        else
        {
            TestCase.assertNull( instance.getInstance() );
            TestCase.assertNull( SimpleComponent.INSTANCE );

            // with removal of the factory, the created instance should also be removed
            checkConfigurationCount( componentname, 0, ComponentConfigurationDTO.ACTIVE );
        }
        return instance;
    }

    @Test
    public void test_component_factory_reference() throws Exception
    {
        final String componentname = "factory.component.reference";
        final String componentfactory = "factory.component.factory.reference";

        SimpleServiceImpl.create( bundleContext, "ignored" ).setFilterProperty( "ignored" );

        getConfigurationsDisabledThenEnable( componentname, 0, -1 );
        TestCase.assertNull( SimpleComponent.INSTANCE );

        // register a service : filterprop=match
        SimpleServiceImpl match = SimpleServiceImpl.create( bundleContext, "required" ).setFilterProperty( "required" );
        delay();

        TestCase.assertNull( SimpleComponent.INSTANCE );

        final ComponentInstance instance = createFactoryComponentInstance( componentfactory );
        TestCase.assertEquals( 1, SimpleComponent.INSTANCE.m_multiRef.size() );
        TestCase.assertTrue( SimpleComponent.INSTANCE.m_multiRef.contains( match ) );

        // check registered components
        checkConfigurationCount( componentname, 1, ComponentConfigurationDTO.ACTIVE );

        instance.dispose();
        TestCase.assertNull( SimpleComponent.INSTANCE );
        TestCase.assertNull( instance.getInstance() ); // SCR 112.12.6.2
        checkConfigurationCount( componentname, 0, ComponentConfigurationDTO.ACTIVE );

        // overwritten filterprop
        Hashtable<String, String> propsNonMatch = new Hashtable<String, String>();
        propsNonMatch.put( PROP_NAME_FACTORY, PROP_NAME_FACTORY );
        propsNonMatch.put( "ref.target", "(filterprop=nomatch)" );
        ComponentFactory factory = getComponentFactory( componentfactory );
        final ComponentInstance instanceNonMatch = factory.newInstance( propsNonMatch );//works even without required reference
        checkConfigurationCount( componentname, 1, ComponentConfigurationDTO.UNSATISFIED_REFERENCE );

        final SimpleServiceImpl noMatch = SimpleServiceImpl.create( bundleContext, "nomatch" ).setFilterProperty(
                "nomatch" );
        delay();

        TestCase.assertNotNull( instanceNonMatch.getInstance() );
        TestCase.assertEquals( SimpleComponent.INSTANCE, instanceNonMatch.getInstance() );
        TestCase.assertEquals( PROP_NAME_FACTORY, SimpleComponent.INSTANCE.getProperty( PROP_NAME_FACTORY ) );

        TestCase.assertEquals( 1, SimpleComponent.INSTANCE.m_multiRef.size() );
        TestCase.assertTrue( SimpleComponent.INSTANCE.m_multiRef.contains( noMatch ) );

        // check registered components
        checkConfigurationCount( componentname, 1, ComponentConfigurationDTO.ACTIVE );

        match.getRegistration().unregister();
        delay();

        // check registered components (ComponentFactory is still present)
        checkConfigurationCount( componentname, 1, ComponentConfigurationDTO.ACTIVE );

        //it has already been deactivated.... this should cause an exception?
        noMatch.getRegistration().unregister();
        delay();

        // check registered components (ComponentFactory is still present)
        checkConfigurationCount( componentname, 1, ComponentConfigurationDTO.UNSATISFIED_REFERENCE );

        // deactivated due to unsatisfied reference
        TestCase.assertNull( instanceNonMatch.getInstance() );
        TestCase.assertNull( SimpleComponent.INSTANCE );

        //Check that calling dispose on a deactivated instance has no effect
        instanceNonMatch.dispose();
        TestCase.assertNull( SimpleComponent.INSTANCE );
        TestCase.assertNull( instanceNonMatch.getInstance() ); // SCR 112.12.6.2
    }

    @Test
    public void test_component_factory_referredTo() throws Exception
    {
        //set up the component that refers to the service the factory will create.
        final String referringComponentName = "ComponentReferringToFactoryObject";
        getConfigurationsDisabledThenEnable( referringComponentName, 1,
                ComponentConfigurationDTO.UNSATISFIED_REFERENCE );

        final String componentname = "factory.component.referred";
        final String componentfactory = "factory.component.factory.referred";

        getConfigurationsDisabledThenEnable( componentname, 0, -1 );
        TestCase.assertNull( SimpleComponent.INSTANCE );

        Hashtable<String, String> props = new Hashtable<String, String>();
        props.put( "service.pid", "myFactoryInstance" );
        final ComponentFactory factory = getComponentFactory( componentfactory );

        final ComponentInstance instance = factory.newInstance( props );
        TestCase.assertNotNull( instance );

        TestCase.assertNotNull( instance.getInstance() );
        TestCase.assertTrue( instance.getInstance() instanceof SimpleService );
        //The referring service should now be active
        checkConfigurationCount( referringComponentName, 1, ComponentConfigurationDTO.ACTIVE );

        instance.dispose();
        TestCase.assertNull( instance.getInstance() ); // SCR 112.12.6.2

        //make sure it's unsatisfied (service is no longer available)
        checkConfigurationCount( referringComponentName, 1, ComponentConfigurationDTO.UNSATISFIED_REFERENCE );
    }

    @Test
    public void test_component_factory_with_target_filters() throws Exception
    {
        final String componentfactory = "factory.component.reference.targetfilter";
        getConfigurationsDisabledThenEnable( componentfactory, 0, -1 );

        SimpleServiceImpl s1 = SimpleServiceImpl.create( bundleContext, "service1" );
        SimpleServiceImpl s2 = SimpleServiceImpl.create( bundleContext, "service2" );

        // supply configuration now and ensure active
        configure( componentfactory );
        delay();

        TestCase.assertNull( SimpleComponent.INSTANCE );

        Hashtable<String, String> props = new Hashtable<String, String>();
        props.put( PROP_NAME_FACTORY, PROP_NAME_FACTORY );
        props.put( "ref.target", "(value=service2)" );
        final ComponentInstance instance = createFactoryComponentInstance( componentfactory, props );

        log.log( LogService.LOG_WARNING, "Bound Services: " + SimpleComponent.INSTANCE.m_multiRef );
        TestCase.assertFalse( SimpleComponent.INSTANCE.m_multiRef.contains( s1 ) );
        TestCase.assertTrue( SimpleComponent.INSTANCE.m_multiRef.contains( s2 ) );

        instance.dispose();
        TestCase.assertNull( SimpleComponent.INSTANCE );
        TestCase.assertNull( instance.getInstance() ); // SCR 112.12.6.2

        s2.drop();
        s1.drop();
    }

    @Test
    public void test_component_factory_set_bundle_location() throws Exception
    {
        final String componentfactoryPid = "factory.component.configuration";
        final String componentFactoryName = "factory.component.factory.configuration";
        getConfigurationsDisabledThenEnable( componentfactoryPid, 0, -1 );
        checkFactory( componentFactoryName, false );

        org.osgi.service.cm.Configuration config = configure( componentfactoryPid );
        delay();
        assertNotNull( getComponentFactory( componentFactoryName ) );

        config.setBundleLocation( "foo" );
        delay();
        checkFactory( componentFactoryName, false );

        config.setBundleLocation( bundle.getLocation() );
        delay();
        assertNotNull( getComponentFactory( componentFactoryName ) );

        config.setBundleLocation( "foo" );
        delay();
        checkFactory( componentFactoryName, false );

        config.setBundleLocation( "?" );
        delay();
        assertNotNull( getComponentFactory( componentFactoryName ) );

    }

}
