/*
 * 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.camp.brooklyn.catalog;

import static org.testng.Assert.assertEquals;

import java.util.Map;

import org.apache.brooklyn.api.effector.Effector;
import org.apache.brooklyn.api.entity.Entity;
import org.apache.brooklyn.api.entity.EntitySpec;
import org.apache.brooklyn.api.location.Location;
import org.apache.brooklyn.api.location.LocationSpec;
import org.apache.brooklyn.api.mgmt.classloading.BrooklynClassLoadingContext;
import org.apache.brooklyn.api.policy.Policy;
import org.apache.brooklyn.api.typereg.ManagedBundle;
import org.apache.brooklyn.api.typereg.RegisteredType;
import org.apache.brooklyn.camp.brooklyn.AbstractYamlRebindTest;
import org.apache.brooklyn.core.catalog.internal.CatalogUtils;
import org.apache.brooklyn.core.effector.Effectors;
import org.apache.brooklyn.core.entity.EntityInternal;
import org.apache.brooklyn.core.entity.StartableApplication;
import org.apache.brooklyn.core.entity.trait.Startable;
import org.apache.brooklyn.core.mgmt.ha.OsgiBundleInstallationResult;
import org.apache.brooklyn.core.mgmt.internal.ManagementContextInternal;
import org.apache.brooklyn.core.mgmt.osgi.OsgiVersionMoreEntityTest;
import org.apache.brooklyn.core.mgmt.rebind.RebindOptions;
import org.apache.brooklyn.core.test.entity.TestEntity;
import org.apache.brooklyn.entity.group.DynamicCluster;
import org.apache.brooklyn.entity.stock.BasicApplication;
import org.apache.brooklyn.test.Asserts;
import org.apache.brooklyn.test.support.TestResourceUnavailableException;
import org.apache.brooklyn.util.collections.MutableMap;
import org.apache.brooklyn.util.core.ClassLoaderUtils;
import org.apache.brooklyn.util.core.ResourceUtils;
import org.apache.brooklyn.util.exceptions.ReferenceWithError;
import org.apache.brooklyn.util.javalang.Reflections;
import org.apache.brooklyn.util.osgi.OsgiTestResources;
import org.apache.brooklyn.util.osgi.VersionedName;
import org.apache.brooklyn.util.text.Strings;
import org.osgi.framework.Bundle;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.testng.Assert;
import org.testng.annotations.Test;

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

/** Many of the same tests as per {@link OsgiVersionMoreEntityTest} but using YAML for catalog and entities, so catalog item ID is set automatically */
public class CatalogOsgiVersionMoreEntityRebindTest extends AbstractYamlRebindTest implements OsgiTestResources {
    
    @SuppressWarnings("unused")
    private static final Logger log = LoggerFactory.getLogger(CatalogOsgiVersionMoreEntityRebindTest.class);

    @Override
    protected boolean useOsgi() {
        return true;
    }
    
    @Test
    public void testRebindAppIncludingBundleAllWorksAndPreservesChecksum() throws Exception {
        TestResourceUnavailableException.throwIfResourceUnavailable(getClass(), OsgiTestResources.BROOKLYN_TEST_MORE_ENTITIES_V1_PATH);
        ((ManagementContextInternal)mgmt()).getOsgiManager().get().install( 
            new ResourceUtils(getClass()).getResourceFromUrl(BROOKLYN_TEST_MORE_ENTITIES_V1_URL) );
        
        RegisteredType item = mgmt().getTypeRegistry().get(BROOKLYN_TEST_MORE_ENTITIES_MORE_ENTITY);
        Assert.assertNotNull(item);
        Assert.assertEquals(item.getContainingBundle(), OsgiTestResources.BROOKLYN_TEST_MORE_ENTITIES_SYMBOLIC_NAME_FULL+":"+"0.1.0");
        
        ManagedBundle mb = ((ManagementContextInternal)mgmt()).getOsgiManager().get().getManagedBundle(VersionedName.fromString(item.getContainingBundle()));
        Assert.assertNotNull(mb);
        String c1 = mb.getChecksum();
        Assert.assertTrue(Strings.isNonBlank(c1), "Missing checksum for bundle");

        Map<String, ManagedBundle> bundles1 = ((ManagementContextInternal)mgmt()).getOsgiManager().get().getManagedBundles();
        
        createAndStartApplication("services: [ { type: "+BROOKLYN_TEST_MORE_ENTITIES_MORE_ENTITY+" } ]");
        
        StartableApplication newApp = rebind();

        // bundles installed
        Map<String, ManagedBundle> bundles = ((ManagementContextInternal)mgmt()).getOsgiManager().get().getManagedBundles();
        Asserts.assertEquals(bundles, bundles1);
        
        //item installed
        item = mgmt().getTypeRegistry().get(BROOKLYN_TEST_MORE_ENTITIES_MORE_ENTITY);
        Assert.assertNotNull(item);
        Assert.assertEquals(item.getContainingBundle(), OsgiTestResources.BROOKLYN_TEST_MORE_ENTITIES_SYMBOLIC_NAME_FULL+":"+"0.1.0");
        
        // containing bundle set, matches, and checksum matches
        mb = ((ManagementContextInternal)mgmt()).getOsgiManager().get().getManagedBundle(VersionedName.fromString(item.getContainingBundle()));
        Assert.assertEquals(mb, bundles.get(mb.getId()));
        Assert.assertEquals(mb.getChecksum(), c1, "checksums should be the same after rebinding");
        
        Assert.assertNotNull(newApp);
    }
    
    @Test
    public void testPolicyInBundleReferencedByStockCatalogItem() throws Exception {
        TestResourceUnavailableException.throwIfResourceUnavailable(getClass(), OsgiTestResources.BROOKLYN_TEST_OSGI_ENTITIES_COM_EXAMPLE_PATH);
        
        String policyType = OsgiTestResources.BROOKLYN_TEST_OSGI_ENTITIES_COM_EXAMPLE_POLICY;
        
        addCatalogItems(
                "brooklyn.catalog:",
                "  id: wrapped-entity",
                "  version: 1.0",
                "  item:",
                "    services:",
                "    - type: " + TestEntity.class.getName());
    
        addCatalogItems(
                "brooklyn.catalog:",
                "  id: with-policy-from-library",
                "  version: 1.0",
                "  brooklyn.libraries:",
                "  - classpath:" + OsgiTestResources.BROOKLYN_TEST_OSGI_ENTITIES_COM_EXAMPLE_PATH,
                "  item:",
                "    services:",
                "    - type: " + BasicApplication.class.getName(),
                "      brooklyn.children:",
                "      - type: wrapped-entity:1.0",
                "        brooklyn.policies:",
                "        - type: " + policyType);
        
        Entity app = createAndStartApplication("services: [ { type: 'with-policy-from-library:1.0' } ]");
        Entity entity = Iterables.getOnlyElement(app.getChildren());
        Policy policy = Iterables.getOnlyElement(entity.policies());
        assertEquals(policy.getPolicyType().getName(), policyType);
        
        StartableApplication newApp = rebind();
        Entity newEntity = Iterables.getOnlyElement(newApp.getChildren());
        Policy newPolicy = Iterables.getOnlyElement(newEntity.policies());
        assertEquals(newPolicy.getPolicyType().getName(), policyType);
    }
    
    // See https://issues.apache.org/jira/browse/BROOKLYN-410
    @Test
    @SuppressWarnings("unchecked")
    public void testRebindsLocationFromBundle() throws Exception {
        TestResourceUnavailableException.throwIfResourceUnavailable(getClass(), OsgiTestResources.BROOKLYN_TEST_OSGI_ENTITIES_COM_EXAMPLE_PATH);
        
        String locationType = OsgiTestResources.BROOKLYN_TEST_OSGI_ENTITIES_COM_EXAMPLE_LOCATION;
        String locationTypeWithBundlePrefix = OsgiTestResources.BROOKLYN_TEST_OSGI_ENTITIES_COM_EXAMPLE_SYMBOLIC_NAME_FULL + ":" + locationType;

        addCatalogItems(
                "brooklyn.catalog:",
                "  id: with-library",
                "  version: 1.0",
                "  brooklyn.libraries:",
                "  - classpath:" + OsgiTestResources.BROOKLYN_TEST_OSGI_ENTITIES_COM_EXAMPLE_PATH,
                "  item:",
                "    services:",
                "    - type: " + BasicApplication.class.getName(),
                "      brooklyn.children:",
                "      - type: " + TestEntity.class.getName());
        
        Entity app = createAndStartApplication("services: [ { type: 'with-library:1.0' } ]");
        Entity entity = Iterables.getOnlyElement(app.getChildren());
        
        // Add a location that can only be classloaded from the `brooklyn.libraries` bundle
        Reflections reflections = new Reflections(CatalogOsgiVersionMoreEntityRebindTest.class.getClassLoader());
        Class<? extends Location> locationClazz = (Class<? extends Location>) new ClassLoaderUtils(reflections.getClassLoader(), mgmt()).loadClass(locationTypeWithBundlePrefix);
        Location loc = mgmt().getLocationManager().createLocation(LocationSpec.create(locationClazz));
        ((EntityInternal)entity).addLocations(ImmutableList.of(loc));

        // Confirm that we can rebind, and thus instantiate that location
        StartableApplication newApp = rebind();
        Entity newEntity = Iterables.getOnlyElement(newApp.getChildren());
        Location newLoc = Iterables.getOnlyElement(newEntity.getLocations());
        assertEquals(newLoc.getClass().getName(), locationType);
    }
    
    @Test
    public void testEffectorInBundleReferencedByStockCatalogItem() throws Exception {
        TestResourceUnavailableException.throwIfResourceUnavailable(getClass(), OsgiTestResources.BROOKLYN_TEST_OSGI_ENTITIES_COM_EXAMPLE_PATH);
        
        String effectorType = OsgiTestResources.BROOKLYN_TEST_OSGI_ENTITIES_COM_EXAMPLE_EFFECTOR;
        
        addCatalogItems(
                "brooklyn.catalog:",
                "  id: wrapped-entity",
                "  version: 1.0",
                "  item:",
                "    services:",
                "    - type: " + TestEntity.class.getName());
    
        addCatalogItems(
                "brooklyn.catalog:",
                "  id: with-effector-from-library",
                "  version: 1.0",
                "  brooklyn.libraries:",
                "  - classpath:" + OsgiTestResources.BROOKLYN_TEST_OSGI_ENTITIES_COM_EXAMPLE_PATH,
                "  item:",
                "    services:",
                "    - type: " + BasicApplication.class.getName(),
                "      brooklyn.children:",
                "      - type: wrapped-entity:1.0",
                "        brooklyn.initializers:",
                "        - type: " + effectorType);

        Entity app = createAndStartApplication("services: [ { type: 'with-effector-from-library:1.0' } ]");
        Entity entity = Iterables.getOnlyElement(app.getChildren());
        Effector<?> effector = entity.getEntityType().getEffectorByName("myEffector").get();
        entity.invoke(effector, ImmutableMap.<String, Object>of()).get();
        
        StartableApplication newApp = rebind();
        Entity newEntity = Iterables.getOnlyElement(newApp.getChildren());
        Effector<?> newEffector = newEntity.getEntityType().getEffectorByName("myEffector").get();
        newEntity.invoke(newEffector, ImmutableMap.<String, Object>of()).get();
    }
    
    @Test
    public void testClassAccessAfterUninstall() throws Exception {
        TestResourceUnavailableException.throwIfResourceUnavailable(getClass(), BROOKLYN_TEST_OSGI_MORE_ENTITIES_0_1_0_PATH);
        
        // install dependency
        ((ManagementContextInternal)mgmt()).getOsgiManager().get().install( 
            new ResourceUtils(getClass()).getResourceFromUrl(BROOKLYN_TEST_OSGI_ENTITIES_URL) );

        // now the v2 bundle
        OsgiBundleInstallationResult b = ((ManagementContextInternal)mgmt()).getOsgiManager().get().install( 
            new ResourceUtils(getClass()).getResourceFromUrl(BROOKLYN_TEST_MORE_ENTITIES_V2_URL) ).get();

        Assert.assertEquals(b.getVersionedName().toString(), BROOKLYN_TEST_MORE_ENTITIES_SYMBOLIC_NAME_FULL+":"+"0.2.0");
        
        String yaml = Strings.lines("name: simple-app-yaml",
                "services:",
                "- type: " + BROOKLYN_TEST_MORE_ENTITIES_MORE_ENTITY);
        Entity app = createAndStartApplication(yaml);
        Entity more = Iterables.getOnlyElement( app.getChildren() );
        
        Assert.assertEquals(
            more.invoke(Effectors.effector(String.class, "sayHI").buildAbstract(), MutableMap.of("name", "Bob")).get(),
            "HI BOB FROM V2");
        
        ((ManagementContextInternal)mgmt()).getOsgiManager().get().uninstallUploadedBundle(b.getMetadata());
        Assert.assertEquals(b.getBundle().getState(), Bundle.UNINSTALLED);

        // can still call things
        Assert.assertEquals(
            more.invoke(Effectors.effector(String.class, "sayHI").buildAbstract(), MutableMap.of("name", "Claudia")).get(),
            "HI CLAUDIA FROM V2");
        
        // but still uninstalled, and attempt to create makes error 
        Assert.assertEquals(b.getBundle().getState(), Bundle.UNINSTALLED);
        try {
            Entity app2 = createAndStartApplication(yaml);
            Asserts.shouldHaveFailedPreviously("Expected deployment to fail after uninstall; instead got "+app2);
        } catch (Exception e) {
            Asserts.expectedFailureContainsIgnoreCase(e, "unable to match", BROOKLYN_TEST_MORE_ENTITIES_MORE_ENTITY);
        }
        
        try {
            // Expect dangling reference(s) to catalog item from uninstalled bundle
            RebindOptions rebindOptionsWithDefaultExceptionHandler = RebindOptions.create().exceptionHandler(null);
            StartableApplication app2 = rebind(rebindOptionsWithDefaultExceptionHandler);
            Asserts.shouldHaveFailedPreviously("Expected deployment to fail rebind; instead got "+app2);
        } catch (Exception e) {
            // should fail to rebind this entity
            Asserts.expectedFailureContainsIgnoreCase(e, more.getId(), "class", BROOKLYN_TEST_MORE_ENTITIES_MORE_ENTITY, "not found");
        }
    }
    
    // @Test
    public void testClassAccessAfterUpgrade() throws Exception {
        TestResourceUnavailableException.throwIfResourceUnavailable(getClass(), BROOKLYN_TEST_OSGI_MORE_ENTITIES_0_1_0_PATH);
        
        // install dependency
        ((ManagementContextInternal)mgmt()).getOsgiManager().get().install( 
            new ResourceUtils(getClass()).getResourceFromUrl(BROOKLYN_TEST_OSGI_ENTITIES_URL) ).checkNoError();

        // now the v2 bundle
        OsgiBundleInstallationResult b2a = ((ManagementContextInternal)mgmt()).getOsgiManager().get().install( 
            new ResourceUtils(getClass()).getResourceFromUrl(BROOKLYN_TEST_MORE_ENTITIES_V2_URL) ).get();

        Assert.assertEquals(b2a.getVersionedName().toString(), BROOKLYN_TEST_MORE_ENTITIES_SYMBOLIC_NAME_FULL+":"+"0.2.0");
        Assert.assertEquals(b2a.getCode(), OsgiBundleInstallationResult.ResultCode.INSTALLED_NEW_BUNDLE);
        
        String yaml = Strings.lines("name: simple-app-yaml",
                "services:",
                "- type: " + BROOKLYN_TEST_MORE_ENTITIES_MORE_ENTITY);
        Entity app = createAndStartApplication(yaml);
        Entity more = Iterables.getOnlyElement( app.getChildren() );
        
        Assert.assertEquals(
            more.invoke(Effectors.effector(String.class, "sayHI").buildAbstract(), MutableMap.of("name", "Bob")).get(),
            "HI BOB FROM V2");
        
        // unforced upgrade should report already installed
        ReferenceWithError<OsgiBundleInstallationResult> installEvilTwin = ((ManagementContextInternal)mgmt()).getOsgiManager().get().install(
            new ResourceUtils(getClass()).getResourceFromUrl(BROOKLYN_TEST_MORE_ENTITIES_V2_EVIL_TWIN_URL) );
        Assert.assertTrue(installEvilTwin.hasError());
        Assert.assertEquals(installEvilTwin.getWithoutError().getCode(),
            OsgiBundleInstallationResult.ResultCode.ERROR_PREPARING_BUNDLE);
        
        // force upgrade
        OsgiBundleInstallationResult b2b = ((ManagementContextInternal)mgmt()).getOsgiManager().get().install(b2a.getMetadata(), 
            new ResourceUtils(getClass()).getResourceFromUrl(BROOKLYN_TEST_MORE_ENTITIES_V2_EVIL_TWIN_URL), true, true, true).get();
        Assert.assertEquals(b2a.getBundle(), b2b.getBundle());
        Assert.assertEquals(b2b.getCode(), OsgiBundleInstallationResult.ResultCode.UPDATED_EXISTING_BUNDLE);

        // calls to things previously instantiated get the old behaviour
        Assert.assertEquals(
            more.invoke(Effectors.effector(String.class, "sayHI").buildAbstract(), MutableMap.of("name", "Claudia")).get(),
            "HI CLAUDIA FROM V2");
        
        // but new deployment gets the new behaviour 
        StartableApplication app2 = (StartableApplication) createAndStartApplication(yaml);
        Entity more2 = Iterables.getOnlyElement( app2.getChildren() );
        Assert.assertEquals(
            more2.invoke(Effectors.effector(String.class, "sayHI").buildAbstract(), MutableMap.of("name", "Daphne")).get(),
            "HO DAPHNE FROM V2 EVIL TWIN");
        app2.stop();
        
        // and after rebind on the old we get new behaviour
        StartableApplication app1 = rebind();
        Entity more1 = Iterables.getOnlyElement( app1.getChildren() );
        Assert.assertEquals(
            more1.invoke(Effectors.effector(String.class, "sayHI").buildAbstract(), MutableMap.of("name", "Eric")).get(),
            "HO ERIC FROM V2 EVIL TWIN");
    }
    
    @Test
    public void testClusterWithEntitySpecFromOsgi() throws Exception {

        TestResourceUnavailableException.throwIfResourceUnavailable(getClass(), BROOKLYN_TEST_OSGI_ENTITIES_PATH);
        TestResourceUnavailableException.throwIfResourceUnavailable(getClass(), BROOKLYN_TEST_MORE_ENTITIES_V2_PATH);

        // install dependencies
        ((ManagementContextInternal)mgmt()).getOsgiManager().get().install( 
            new ResourceUtils(getClass()).getResourceFromUrl(BROOKLYN_TEST_OSGI_ENTITIES_URL) ).checkNoError();
        ((ManagementContextInternal)mgmt()).getOsgiManager().get().install( 
            new ResourceUtils(getClass()).getResourceFromUrl(BROOKLYN_TEST_MORE_ENTITIES_V2_URL) ).get();
        
        RegisteredType ci = Preconditions.checkNotNull( mgmt().getTypeRegistry().get(BROOKLYN_TEST_MORE_ENTITIES_MORE_ENTITY) );
        EntitySpec<DynamicCluster> clusterSpec = EntitySpec.create(DynamicCluster.class)
            .configure(DynamicCluster.INITIAL_SIZE, 1)
            .configure(DynamicCluster.MEMBER_SPEC, origManagementContext.getTypeRegistry().createSpec(ci, null, EntitySpec.class));
        
        final Entity app = mgmt().getEntityManager().createEntity(EntitySpec.create(BasicApplication.class).child(clusterSpec));
        app.invoke(Startable.START, MutableMap.of()).get();
        
        rebind();
    }

    protected RegisteredType installWrappedMoreEntity() throws Exception {

        TestResourceUnavailableException.throwIfResourceUnavailable(getClass(), BROOKLYN_TEST_OSGI_ENTITIES_PATH);
        TestResourceUnavailableException.throwIfResourceUnavailable(getClass(), BROOKLYN_TEST_MORE_ENTITIES_V2_PATH);

        ((ManagementContextInternal)mgmt()).getOsgiManager().get().install( 
            new ResourceUtils(getClass()).getResourceFromUrl(BROOKLYN_TEST_OSGI_ENTITIES_URL) ).checkNoError();
        ((ManagementContextInternal)mgmt()).getOsgiManager().get().install( 
            new ResourceUtils(getClass()).getResourceFromUrl(BROOKLYN_TEST_MORE_ENTITIES_V2_URL) ).get();
        addCatalogItems(
            "brooklyn.catalog:",
            "  id: wrapped-more-entity",
            "  version: 1.0",
            "  item:",
            "    services:",
            "    - type: " + BROOKLYN_TEST_MORE_ENTITIES_MORE_ENTITY);
        
        RegisteredType ci = Preconditions.checkNotNull( mgmt().getTypeRegistry().get("wrapped-more-entity") );
        return ci;
    }
    
    @Test
    public void testRebindsClusterWithEntitySpecWrappingOsgi() throws Exception {
        RegisteredType ci = installWrappedMoreEntity();
        EntitySpec<DynamicCluster> clusterSpec = EntitySpec.create(DynamicCluster.class)
            .configure(DynamicCluster.INITIAL_SIZE, 1)
            .configure(DynamicCluster.MEMBER_SPEC, origManagementContext.getTypeRegistry().createSpec(ci, null, EntitySpec.class));
        
        final Entity app = mgmt().getEntityManager().createEntity(EntitySpec.create(BasicApplication.class).child(clusterSpec));
        app.invoke(Startable.START, MutableMap.of()).get();
        
        rebind();
    }

    /** Does the class loader for the wrapped-more-entity type inherit more-entity's class loader?
     * Was tempting to say yes but the implementation is hard as we need to add API methods to find the
     * supertype (or supertypes).  We might do that at which point we could change these semantics if we wished.
     * However there is also an argument that the instantiation engine determines where inherited loaders
     * behave transitively and where they don't, so we shouldn't have a blanket rule that you can always
     * see someone else's loaders just by extending them. In any case the "compelling use case" for 
     * considering this (and noticing it, and adding comments to {@link RegisteredType#getLibraries()})
     * is xml deserialization of entity specs in persisted state, and: 
     * (a) there are other ways to do that, and 
     * (b) we'd like to move away from that and use the same yaml-based instantiation engine used for initial construction. */
    @Test
    public void testWrappedEntityClassLoaderDoesntHaveAncestorClassLoader() throws Exception {
        RegisteredType ci = installWrappedMoreEntity();
        BrooklynClassLoadingContext clc = CatalogUtils.newClassLoadingContext(mgmt(), ci);
        try {
            clc.loadClass(BROOKLYN_TEST_MORE_ENTITIES_MORE_ENTITY);
            Asserts.shouldHaveFailedPreviously();
        } catch (Exception e) {
            Asserts.expectedFailureContainsIgnoreCase(e, "unable to load", BROOKLYN_TEST_MORE_ENTITIES_MORE_ENTITY);
        }
    }

    // could support this now that we have a local cache; but probably not needed; see BasicBrooklynCatalog.scanAnnotationsInBundle
//    @Test
//    public void testRebindJavaScanningBundleInCatalog() throws Exception {
//        CatalogScanOsgiTest.installJavaScanningMoreEntitiesV2(mgmt(), this);
//        rebind();
//        RegisteredType item = mgmt().getTypeRegistry().get(OsgiTestResources.BROOKLYN_TEST_MORE_ENTITIES_MORE_ENTITY);
//        Assert.assertNotNull(item, "Scanned item should have been available after rebind");
//    }
    
}