/*
 * 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.brooklyn.core.mgmt.osgi;

import static org.testng.Assert.assertTrue;

import java.io.File;
import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.util.Arrays;
import java.util.List;

import org.apache.brooklyn.api.entity.Entity;
import org.apache.brooklyn.api.entity.EntitySpec;
import org.apache.brooklyn.api.mgmt.ManagementContext;
import org.apache.brooklyn.api.objs.BrooklynObject;
import org.apache.brooklyn.api.policy.PolicySpec;
import org.apache.brooklyn.api.typereg.RegisteredType;
import org.apache.brooklyn.core.catalog.internal.CatalogEntityItemDto;
import org.apache.brooklyn.core.catalog.internal.CatalogItemBuilder;
import org.apache.brooklyn.core.catalog.internal.CatalogItemDtoAbstract;
import org.apache.brooklyn.core.catalog.internal.CatalogUtils;
import org.apache.brooklyn.core.effector.Effectors;
import org.apache.brooklyn.core.entity.AbstractEntity;
import org.apache.brooklyn.core.entity.Entities;
import org.apache.brooklyn.core.mgmt.internal.LocalManagementContext;
import org.apache.brooklyn.core.mgmt.internal.ManagementContextInternal;
import org.apache.brooklyn.core.objs.proxy.EntityProxy;
import org.apache.brooklyn.core.objs.proxy.InternalEntityFactory;
import org.apache.brooklyn.core.objs.proxy.InternalPolicyFactory;
import org.apache.brooklyn.core.test.entity.LocalManagementContextForTests;
import org.apache.brooklyn.core.test.entity.TestApplication;
import org.apache.brooklyn.test.support.TestResourceUnavailableException;
import org.apache.brooklyn.util.collections.MutableList;
import org.apache.brooklyn.util.core.osgi.OsgiTestBase;
import org.apache.brooklyn.util.core.osgi.Osgis;
import org.apache.brooklyn.util.exceptions.Exceptions;
import org.apache.brooklyn.util.guava.Maybe;
import org.apache.brooklyn.util.os.Os;
import org.apache.brooklyn.util.osgi.OsgiTestResources;
import org.osgi.framework.Bundle;
import org.osgi.framework.BundleException;
import org.osgi.framework.launch.Framework;
import org.testng.Assert;
import org.testng.annotations.AfterMethod;
import org.testng.annotations.BeforeMethod;
import org.testng.annotations.Test;

import com.google.common.base.Optional;
import com.google.common.base.Preconditions;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Iterables;


/** 
 * Tests that OSGi entities load correctly and have the right catalog information set.
 * Note further tests done elsewhere using CAMP YAML (referring to methods in this class).
 */
public class OsgiVersionMoreEntityTest implements OsgiTestResources {
   
    public static final String EXPECTED_SAY_HI_BROOKLYN_RESPONSE_FROM_V1 = "Hi BROOKLYN from V1";
    public static final String EXPECTED_SAY_HI_BROOKLYN_RESPONSE_FROM_V2 = "HI BROOKLYN FROM V2";
    public static final String EXPECTED_SAY_HI_BROOKLYN_RESPONSE_FROM_V2_EVIL_TWIN = "HO BROOKLYN FROM V2 EVIL TWIN";
    
    protected LocalManagementContext mgmt;
    protected TestApplication app;

    @BeforeMethod(alwaysRun=true)
    public void setUp() throws Exception {
        mgmt = LocalManagementContextForTests.builder(true).enableOsgiReusable().build();
        app = TestApplication.Factory.newManagedInstanceForTests(mgmt);
    }

    @AfterMethod(alwaysRun=true)
    public void tearDown() throws BundleException, IOException, InterruptedException {
        Entities.destroyAll(mgmt);
    }
    
    /**
     * Test fix for
     * NoClassDefFoundError: org.apache.brooklyn.api.sensor.AttributeSensor not found by ....brooklyn-test-osgi-entities
     * (see comments in InternalEntityFactory)
     */
    @Test
    public void testEntityProxy() throws Exception {
        TestResourceUnavailableException.throwIfResourceUnavailable(getClass(), BROOKLYN_TEST_OSGI_ENTITIES_PATH);
        
        File storageTempDir = Os.newTempDir("osgi-standalone");
        Framework framework = Osgis.getFramework(storageTempDir.getAbsolutePath(), true);
        
        ManagementContextInternal managementContext = null;
        try {
            managementContext = new LocalManagementContextForTests();
            InternalPolicyFactory policyFactory = new InternalPolicyFactory(managementContext);
            InternalEntityFactory factory = new InternalEntityFactory(managementContext, managementContext.getEntityManager().getEntityTypeRegistry(), policyFactory);

            Bundle bundle = Osgis.install(framework, BROOKLYN_TEST_OSGI_ENTITIES_PATH);
            @SuppressWarnings("unchecked")
            Class<? extends Entity> bundleCls = (Class<? extends Entity>) bundle.loadClass("org.apache.brooklyn.test.osgi.entities.SimpleEntityImpl");
            @SuppressWarnings("unchecked")
            Class<? extends Entity> bundleInterface = (Class<? extends Entity>) bundle.loadClass("org.apache.brooklyn.test.osgi.entities.SimpleEntity");

            @SuppressWarnings("unchecked")
            EntitySpec<Entity> spec = (((EntitySpec<Entity>)EntitySpec.create(bundleInterface))).impl(bundleCls);
            AbstractEntity entityImpl = (AbstractEntity) factory.createEntity(spec, Optional.absent());
            Entity entityProxy = factory.createEntityProxy(spec, entityImpl);
            
            assertTrue(entityProxy instanceof EntityProxy, "proxy="+entityProxy);
            assertTrue(entityProxy instanceof Entity, "proxy="+entityProxy);
            assertTrue(bundleInterface.isInstance(entityProxy), "proxy="+entityProxy);

        } finally {
            try {
                if (managementContext != null) Entities.destroyAll(managementContext);
            } finally {
                OsgiTestBase.tearDownOsgiFramework(framework, storageTempDir);
            }
        }
    }
    
    protected RegisteredType addCatalogItemWithTypeAsName(String type, String version, String ...libraries) {
        return addCatalogItemWithNameAndType(type, version, type, libraries);
    }
    protected RegisteredType addCatalogItemWithNameAndType(String symName, String version, String type, String ...libraries) {
        return addCatalogItemWithNameAndType(mgmt, symName, version, type, libraries);
    }
    
    @SuppressWarnings("deprecation")
    static RegisteredType addCatalogItemWithNameAndType(ManagementContext mgmt, String symName, String version, String type, String ...libraries) {
        OsgiTestBase.preinstallLibrariesLowLevelToPreventCatalogBomParsing(mgmt, libraries);
        CatalogEntityItemDto c1 = newCatalogItemWithNameAndType(symName, version, type, libraries);
        mgmt.getCatalog().addItem(c1);
        RegisteredType c2 = mgmt.getTypeRegistry().get(symName, version);
        Preconditions.checkNotNull(c2, "Item "+type+":"+version+" was not found after adding it");
        return c2;
    }

    static CatalogEntityItemDto newCatalogItemWithTypeAsName(String type, String version, String ...libraries) {
        return newCatalogItemWithNameAndType(type, version, type, libraries);
    }
    static CatalogEntityItemDto newCatalogItemWithNameAndType(String symName, String version, String type, String ...libraries) {
        @SuppressWarnings("deprecation")
        CatalogEntityItemDto c1 = CatalogItemBuilder.newEntity(symName, version)
                .javaType(type)
                .libraries(CatalogItemDtoAbstract.parseLibraries(Arrays.asList(libraries)))
                .build();
        return c1;
    }

    protected Entity addItemFromCatalog(RegisteredType c2) {
        return addItemFromCatalog(mgmt, app, c2);
    }
    
    @SuppressWarnings("unchecked")
    public static Entity addItemFromCatalog(ManagementContext mgmt, TestApplication parent, RegisteredType c2) {
        return parent.createAndManageChild( mgmt.getTypeRegistry().createSpec(c2, null, EntitySpec.class) );
    }

    public static void assertV1MethodCall(Entity me) throws IllegalAccessException, InvocationTargetException, NoSuchMethodException {
        Assert.assertEquals(doMethodCallBrooklyn(me), EXPECTED_SAY_HI_BROOKLYN_RESPONSE_FROM_V1);
    }
    public static void assertV2MethodCall(BrooklynObject me) throws IllegalAccessException, InvocationTargetException, NoSuchMethodException {
        Assert.assertEquals(doMethodCallBrooklyn(me), EXPECTED_SAY_HI_BROOKLYN_RESPONSE_FROM_V2);
    }
    public static void assertV2EvilTwinMethodCall(Entity me) throws IllegalAccessException, InvocationTargetException, NoSuchMethodException {
        Assert.assertEquals(doMethodCallBrooklyn(me), EXPECTED_SAY_HI_BROOKLYN_RESPONSE_FROM_V2_EVIL_TWIN);
    }

    public static Object doMethodCallBrooklyn(BrooklynObject me) throws IllegalAccessException, InvocationTargetException, NoSuchMethodException {
        return me.getClass().getMethod("sayHI", String.class).invoke(me, "Brooklyn");
    }

    public static void assertV1EffectorCall(Entity me) {
        Assert.assertEquals(doEffectorCallBrooklyn(me), EXPECTED_SAY_HI_BROOKLYN_RESPONSE_FROM_V1);
    }
    public static void assertV2EffectorCall(Entity me) {
        Assert.assertEquals(doEffectorCallBrooklyn(me), EXPECTED_SAY_HI_BROOKLYN_RESPONSE_FROM_V2);
    }
    public static void assertV2EvilTwinEffectorCall(Entity me) {
        Assert.assertEquals(doEffectorCallBrooklyn(me), EXPECTED_SAY_HI_BROOKLYN_RESPONSE_FROM_V2_EVIL_TWIN);
    }

    public static String doEffectorCallBrooklyn(Entity me) {
        return me.invoke(Effectors.effector(String.class, "sayHI").buildAbstract(), ImmutableMap.of("name", "brooklyn")).getUnchecked();
    }

    public static RegisteredType addMoreEntityV1(ManagementContext mgmt, String versionToRegister) {
        TestResourceUnavailableException.throwIfResourceUnavailable(OsgiVersionMoreEntityTest.class, BROOKLYN_TEST_MORE_ENTITIES_V1_PATH);
        return addCatalogItemWithNameAndType(mgmt,
            OsgiTestResources.BROOKLYN_TEST_MORE_ENTITIES_MORE_ENTITY,
            versionToRegister,
            OsgiTestResources.BROOKLYN_TEST_MORE_ENTITIES_MORE_ENTITY,
            BROOKLYN_TEST_MORE_ENTITIES_V1_URL);
    }
    public static RegisteredType addMoreEntityV2(ManagementContext mgmt, String versionToRegister) {
        TestResourceUnavailableException.throwIfResourceUnavailable(OsgiVersionMoreEntityTest.class, BROOKLYN_TEST_MORE_ENTITIES_V1_PATH);
        return addCatalogItemWithNameAndType(mgmt,
            OsgiTestResources.BROOKLYN_TEST_MORE_ENTITIES_MORE_ENTITY,
            versionToRegister,
            OsgiTestResources.BROOKLYN_TEST_MORE_ENTITIES_MORE_ENTITY,
            BROOKLYN_TEST_MORE_ENTITIES_V2_URL,
            BROOKLYN_TEST_OSGI_ENTITIES_URL);
    }

    @Test
    public void testMoreEntitiesV1() throws Exception {
        TestResourceUnavailableException.throwIfResourceUnavailable(getClass(), BROOKLYN_TEST_MORE_ENTITIES_V1_PATH);

        RegisteredType c2 = addMoreEntityV1(mgmt, TEST_VERSION);
        
        // test load and instantiate
        Entity me = addItemFromCatalog(c2);
        Assert.assertEquals(me.getCatalogItemId(), CatalogUtils.getVersionedId(OsgiTestResources.BROOKLYN_TEST_MORE_ENTITIES_MORE_ENTITY, TEST_VERSION));
        
        assertV1MethodCall(me);
        assertV1EffectorCall(me);
        
        // test adding a child gets the right type; this time by entity parent hierarchy
        @SuppressWarnings({ "unchecked" })
        Entity me2 = me.addChild( mgmt.getTypeRegistry().createSpec(c2, null, EntitySpec.class) );
        Assert.assertEquals(me2.getCatalogItemId(), CatalogUtils.getVersionedId(OsgiTestResources.BROOKLYN_TEST_MORE_ENTITIES_MORE_ENTITY, TEST_VERSION));
    }

    protected PolicySpec<?> getPolicySpec(RegisteredType cp) {
        return mgmt.getTypeRegistry().createSpec(cp, null, PolicySpec.class);
    }

    @Test
    public void testMoreEntitiesV1Policy() throws Exception {
        TestResourceUnavailableException.throwIfResourceUnavailable(getClass(), BROOKLYN_TEST_MORE_ENTITIES_V1_PATH);
        TestResourceUnavailableException.throwIfResourceUnavailable(getClass(), BROOKLYN_TEST_OSGI_ENTITIES_PATH);

        RegisteredType c2 = addCatalogItemWithTypeAsName(
                OsgiTestResources.BROOKLYN_TEST_MORE_ENTITIES_MORE_ENTITY,
                TEST_VERSION,
                BROOKLYN_TEST_MORE_ENTITIES_V1_URL);
        
        // test load and instantiate
        Entity me = addItemFromCatalog(c2);

        RegisteredType cp = addCatalogItemWithTypeAsName(
                OsgiTestResources.BROOKLYN_TEST_OSGI_ENTITIES_SIMPLE_POLICY,
                TEST_VERSION,
                BROOKLYN_TEST_OSGI_ENTITIES_URL);
        me.policies().add(getPolicySpec(cp));
        
        Assert.assertEquals(me.policies().size(), 1, "Wrong number of policies: "+MutableList.copyOf(me.policies()));
        
        String catalogItemId = Iterables.getOnlyElement( me.policies() ).getCatalogItemId();
        Assert.assertNotNull(catalogItemId);
        // must be the actual source bundle
        Assert.assertFalse(catalogItemId.equals(me.getCatalogItemId()), "catalog item id is: "+catalogItemId);
        Assert.assertTrue(catalogItemId.equals(CatalogUtils.getVersionedId(OsgiTestResources.BROOKLYN_TEST_OSGI_ENTITIES_SIMPLE_POLICY, TEST_VERSION)), "catalog item id is: "+catalogItemId);
    }

    @Test
    public void testMoreEntitiesV2FailsWithoutBasicTestOsgiEntitiesBundle() throws Exception {
        TestResourceUnavailableException.throwIfResourceUnavailable(getClass(), BROOKLYN_TEST_MORE_ENTITIES_V2_PATH);

        RegisteredType c2 = addCatalogItemWithTypeAsName(
                OsgiTestResources.BROOKLYN_TEST_MORE_ENTITIES_MORE_ENTITY,
                TEST_VERSION,
                BROOKLYN_TEST_MORE_ENTITIES_V2_URL);
        
        // test load and instantiate
        try {
            Entity me = addItemFromCatalog(c2);
            Assert.fail("Should have failed, with unresolved dependency; instead got "+me);
        } catch (Exception e) {
            Assert.assertTrue(e.toString().toLowerCase().contains("unresolved requirements"), "Missing expected text in error: "+e);
            Assert.assertTrue(e.toString().toLowerCase().contains("wiring.package"), "Missing expected text in error: "+e);
            Assert.assertTrue(e.toString().toLowerCase().contains("org.apache.brooklyn.test.osgi.entities"), "Missing expected text in error: "+e);
        }
    }
    
    // V2 works with dependency declared, and can load
    // and has policy, with policy item catalog ID is reasonable
    @Test
    public void testMoreEntitiesV2() throws Exception {
        TestResourceUnavailableException.throwIfResourceUnavailable(getClass(), BROOKLYN_TEST_MORE_ENTITIES_V2_PATH);

        RegisteredType c2 = addCatalogItemWithTypeAsName(
                OsgiTestResources.BROOKLYN_TEST_MORE_ENTITIES_MORE_ENTITY,
                TEST_VERSION,
                BROOKLYN_TEST_MORE_ENTITIES_V2_URL,
                BROOKLYN_TEST_OSGI_ENTITIES_URL);
        
        // test load and instantiate
        Entity me = addItemFromCatalog(c2);
        Assert.assertEquals(me.getCatalogItemId(), CatalogUtils.getVersionedId(OsgiTestResources.BROOKLYN_TEST_MORE_ENTITIES_MORE_ENTITY, TEST_VERSION));
        
        assertV2MethodCall(me);
        assertV2EffectorCall(me);
        Assert.assertEquals(me.policies().size(), 1, "Wrong number of policies: "+MutableList.copyOf(me.policies()));
        
        String catalogItemId = Iterables.getOnlyElement( me.policies() ).getCatalogItemId();
        Assert.assertNotNull(catalogItemId);
        // allow either me's bundle (more) or the actual source bundle
        Assert.assertTrue(catalogItemId.equals(me.getCatalogItemId()) || catalogItemId.startsWith("brooklyn-test-osgi-entities"));
    }

    @Test
    public void testMoreEntitiesV1ThenV2GivesV2() throws Exception {
        TestResourceUnavailableException.throwIfResourceUnavailable(getClass(), BROOKLYN_TEST_MORE_ENTITIES_V1_PATH);
        TestResourceUnavailableException.throwIfResourceUnavailable(getClass(), BROOKLYN_TEST_MORE_ENTITIES_V2_PATH);

        addCatalogItemWithTypeAsName(
                OsgiTestResources.BROOKLYN_TEST_MORE_ENTITIES_MORE_ENTITY,
                TEST_VERSION,
                BROOKLYN_TEST_MORE_ENTITIES_V1_URL);
        addCatalogItemWithTypeAsName(
                OsgiTestResources.BROOKLYN_TEST_MORE_ENTITIES_MORE_ENTITY,
                TEST_VERSION,
                BROOKLYN_TEST_MORE_ENTITIES_V2_URL, BROOKLYN_TEST_OSGI_ENTITIES_URL);
        
        // test load and instantiate
        Entity me = addItemFromCatalog( mgmt.getTypeRegistry().get(OsgiTestResources.BROOKLYN_TEST_MORE_ENTITIES_MORE_ENTITY, TEST_VERSION) );
        
        assertV2MethodCall(me);
        assertV2EffectorCall(me);
        Assert.assertEquals(me.policies().size(), 1, "Wrong number of policies: "+MutableList.copyOf(me.policies()));
    }

    @Test
    public void testMoreEntitiesV2ThenV1GivesV1() throws Exception {
        TestResourceUnavailableException.throwIfResourceUnavailable(getClass(), BROOKLYN_TEST_MORE_ENTITIES_V1_PATH);
        TestResourceUnavailableException.throwIfResourceUnavailable(getClass(), BROOKLYN_TEST_MORE_ENTITIES_V2_PATH);

        addCatalogItemWithTypeAsName(
                OsgiTestResources.BROOKLYN_TEST_MORE_ENTITIES_MORE_ENTITY,
                TEST_VERSION,
                BROOKLYN_TEST_MORE_ENTITIES_V2_URL, BROOKLYN_TEST_OSGI_ENTITIES_URL);
        addCatalogItemWithTypeAsName(
                OsgiTestResources.BROOKLYN_TEST_MORE_ENTITIES_MORE_ENTITY,
                TEST_VERSION,
                BROOKLYN_TEST_MORE_ENTITIES_V1_URL);
        
        // test load and instantiate
        Entity me = addItemFromCatalog( mgmt.getTypeRegistry().get(OsgiTestResources.BROOKLYN_TEST_MORE_ENTITIES_MORE_ENTITY, TEST_VERSION) );
        /*
         * WARNING - Weird maven-bundle-plugin and OSGi behaviour.  Some caveats:
         * <p>
         * (1) When "import-package" declares a version, that is the *minimum* version;
         *     it may be that semantic versioning is applied, so 1.3.1 = [1.3.1,4.0.0);
         *     or it may be just a minimum 1.3.1 = [1.3.1,) ... i can't find good docs
         *     [but see http://www.christianposta.com/blog/?p=241]
         * <p>
         * (2) Different versions of maven-bundle-plugin do wildly different things.
         *     * v1.4.0 attaches the version to import-package (so you get the minimum
         *       which can cause this test to fail);
         *     * v2.x does not seem to declare the exported package at all in import-package
         *       (violating the so-called best practice, see
         *       http://blog.osgi.org/2007/04/importance-of-exporting-nd-importing.html )
         *     * v2.4.0 gives a huge list in import/export package, with semver ranges;
         *       but the other versions seem not to list much and they do NOT have versions
         * <p>
         * The tests are all working with 2.5.3 but if version dependencies become any
         * more intertwined maven-bundle-plugin will almost certainly NOT do the wrong
         * thing because packages do not have versions. (Ironically, 1.4.0 seems the
         * best behaved, but for the minimum/semver package version behaviour, and
         * even that wouldn't be so bad if you're doing semver, or if you figure out
         * how to override with a _versionpolicy tag!)
         */
        assertV1MethodCall(me);
        assertV1EffectorCall(me);
        Assert.assertEquals(me.policies().size(), 0, "Wrong number of policies: "+MutableList.copyOf(me.policies()));
    }

    @Test
    public void testUnfazedByMoreEntitiesV1AndV2AndV2EvilTwin() throws Exception {
        TestResourceUnavailableException.throwIfResourceUnavailable(getClass(), BROOKLYN_TEST_MORE_ENTITIES_V1_PATH);
        TestResourceUnavailableException.throwIfResourceUnavailable(getClass(), BROOKLYN_TEST_MORE_ENTITIES_V2_PATH);
        TestResourceUnavailableException.throwIfResourceUnavailable(getClass(), BROOKLYN_TEST_MORE_ENTITIES_V2_EVIL_TWIN_PATH);

        addCatalogItemWithNameAndType("v1",
                TEST_VERSION,
                OsgiTestResources.BROOKLYN_TEST_MORE_ENTITIES_MORE_ENTITY,
                BROOKLYN_TEST_MORE_ENTITIES_V1_URL);
        addCatalogItemWithNameAndType("v2",
                TEST_VERSION,
                OsgiTestResources.BROOKLYN_TEST_MORE_ENTITIES_MORE_ENTITY,
                BROOKLYN_TEST_MORE_ENTITIES_V2_URL, BROOKLYN_TEST_OSGI_ENTITIES_URL);
        addCatalogItemWithNameAndType("v2-evil", 
                TEST_VERSION,
                OsgiTestResources.BROOKLYN_TEST_MORE_ENTITIES_MORE_ENTITY,
                BROOKLYN_TEST_MORE_ENTITIES_V2_EVIL_TWIN_URL, BROOKLYN_TEST_OSGI_ENTITIES_URL);

        // test osgi finding
        
        List<Bundle> bundles = Osgis.bundleFinder(mgmt.getOsgiManager().get().getFramework())
            .symbolicName(OsgiTestResources.BROOKLYN_TEST_MORE_ENTITIES_SYMBOLIC_NAME_FULL).findAll();
        Assert.assertEquals(bundles.size(), 3, "Wrong list of bundles: "+bundles);
        
        Maybe<Bundle> preferredVersion = Osgis.bundleFinder(mgmt.getOsgiManager().get().getFramework())
            .symbolicName(OsgiTestResources.BROOKLYN_TEST_MORE_ENTITIES_SYMBOLIC_NAME_FULL).find();
        Assert.assertTrue(preferredVersion.isPresent());
        Assert.assertEquals(preferredVersion.get().getVersion().toString(), "0.2.0");
        
        Maybe<Bundle> evilVersion = Osgis.bundleFinder(mgmt.getOsgiManager().get().getFramework()).
            preferringFromUrl(BROOKLYN_TEST_MORE_ENTITIES_V2_EVIL_TWIN_URL).find();
        Assert.assertTrue(evilVersion.isPresent());
        Assert.assertEquals(evilVersion.get().getVersion().toString(), "0.2.0");

        // test preferredUrl vs requiredUrl

        Maybe<Bundle> versionIgnoresInvalidPreferredUrl = Osgis.bundleFinder(mgmt.getOsgiManager().get().getFramework())
            .symbolicName(OsgiTestResources.BROOKLYN_TEST_MORE_ENTITIES_SYMBOLIC_NAME_FULL)
            .version(TEST_VERSION)
            .preferringFromUrl(BROOKLYN_TEST_MORE_ENTITIES_V2_EVIL_TWIN_URL).find();
        Assert.assertTrue(versionIgnoresInvalidPreferredUrl.isPresent());
        Assert.assertEquals(versionIgnoresInvalidPreferredUrl.get().getVersion().toString(), TEST_VERSION);

        Maybe<Bundle> versionRespectsInvalidRequiredUrl = Osgis.bundleFinder(mgmt.getOsgiManager().get().getFramework())
            .symbolicName(OsgiTestResources.BROOKLYN_TEST_MORE_ENTITIES_SYMBOLIC_NAME_FULL)
            .version(TEST_VERSION)
            .requiringFromUrl(BROOKLYN_TEST_MORE_ENTITIES_V2_EVIL_TWIN_URL).find();
        Assert.assertFalse(versionRespectsInvalidRequiredUrl.isPresent());

        // test entity resolution
        
        Entity v2 = addItemFromCatalog( mgmt.getTypeRegistry().get("v2", TEST_VERSION) );
        assertV2MethodCall(v2);
        assertV2EffectorCall(v2);
        Assert.assertEquals(v2.policies().size(), 1, "Wrong number of policies: "+MutableList.copyOf(v2.policies()));

        Entity v2_evil = addItemFromCatalog( mgmt.getTypeRegistry().get("v2-evil", TEST_VERSION) );
        assertV2EvilTwinMethodCall(v2_evil);
        assertV2EvilTwinEffectorCall(v2_evil);
        Assert.assertEquals(v2_evil.policies().size(), 1, "Wrong number of policies: "+MutableList.copyOf(v2_evil.policies()));

        Entity v1 = addItemFromCatalog( mgmt.getTypeRegistry().get("v1", TEST_VERSION) );
        assertV1MethodCall(v1);
        assertV1EffectorCall(v1);
        Assert.assertEquals(v1.policies().size(), 0, "Wrong number of policies: "+MutableList.copyOf(v1.policies()));
    }

    // TODO versioning (WIP until #92), install both V1 and V2 with version number, and test that both work
        
}
