/*
 * 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 WARRANTIESOR 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.aries.subsystem.modelling.utils;


import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertThrows;
import static org.junit.Assert.assertTrue;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.jar.Attributes;

import org.apache.aries.subsystem.modelling.DeployedBundles;
import org.apache.aries.subsystem.modelling.ExportedService;
import org.apache.aries.subsystem.modelling.ImportedBundle;
import org.apache.aries.subsystem.modelling.ImportedService;
import org.apache.aries.subsystem.modelling.InvalidAttributeException;
import org.apache.aries.subsystem.modelling.ModelledResource;
import org.apache.aries.subsystem.modelling.ResolverException;
import org.apache.aries.subsystem.modelling.impl.ExportedServiceImpl;
import org.apache.aries.subsystem.modelling.impl.ImportedBundleImpl;
import org.apache.aries.subsystem.modelling.impl.ImportedServiceImpl;
import org.apache.aries.subsystem.modelling.impl.ModelledResourceImpl;
import org.apache.aries.subsystem.modelling.impl.ModellingHelperImpl;
import org.apache.aries.util.manifest.ManifestHeaderProcessor;
import org.apache.aries.util.manifest.ManifestHeaderProcessor.NameValuePair;
import org.junit.Test;
import org.osgi.framework.Constants;


public final class DeployedBundlesTest
{
  
  private DeployedBundles validDeployedBundles() throws Exception {

    Collection<ImportedBundle> content = new ArrayList<ImportedBundle>();
    Collection<ImportedBundle> uses = new ArrayList<ImportedBundle>();

    content.add(new ImportedBundleImpl("bundle.a", "1.0.0"));
    content.add(new ImportedBundleImpl("bundle.b", "1.0.0"));
    
    uses.add(new ImportedBundleImpl("bundle.c", "1.0.0"));
    uses.add(new ImportedBundleImpl("bundle.d", "1.0.0"));
    
    return new ModellingHelperImpl().createDeployedBundles("test",content, uses, null);
  }
  
  private void basicResolve(DeployedBundles db, boolean cPersistent) throws InvalidAttributeException {
    db.addBundle(createModelledResource("bundle.a", "1.0.0", 
        Arrays.asList("package.b", "package.c"), Arrays.asList("package.a;version=1.0.0")));
    db.addBundle(createModelledResource("bundle.b", "1.0.0", 
        Arrays.asList("package.d;version=1.0.0", "package.e;version=\"[1.0.0,2.0.0)\"", "package.g"),
        Arrays.asList("package.b;version=1.0.0")));
    
    db.addBundle(createModelledResource("bundle.c", "1.0.0", 
        (cPersistent) ? Arrays.asList("package.d;version=\"[1.0.0,2.0.0)\"", "javax.persistence;version=1.1.0") : 
          Arrays.asList("package.d;version=\"[1.0.0,2.0.0)\""), Arrays.asList("package.c;version=1.0.0")));
    db.addBundle(createModelledResource("bundle.d", "1.0.0", 
        Arrays.asList("package.e;version=\"[1.0.0,1.0.0]\""), Arrays.asList("package.d;version=1.0.0")));

  }
  
  private void packagesResolve(DeployedBundles db) throws InvalidAttributeException {
    basicResolve(db, false);
    
    db.addBundle(createModelledResource("bundle.e", "1.0.0",
        new ArrayList<String>(), Arrays.asList("package.e;version=1.0.0")));
  }
  
  public static ModelledResource createModelledResource(String bundleName, String bundleVersion, 
      Collection<String> importedPackages, Collection<String> exportedPackages) throws InvalidAttributeException {
    Attributes att = new Attributes();
    att.put(new Attributes.Name(Constants.BUNDLE_SYMBOLICNAME), bundleName);
    att.put(new Attributes.Name(Constants.BUNDLE_VERSION), bundleVersion);
    att.put(new Attributes.Name(Constants.BUNDLE_MANIFESTVERSION), "2");
    
    StringBuilder builder = new StringBuilder();
    for(String iPackage : importedPackages) {
      builder.append(iPackage).append(",");
    }
    if(builder.length() > 0) {
      builder.deleteCharAt(builder.length() - 1);
      att.put(new Attributes.Name(Constants.IMPORT_PACKAGE), builder.toString());
    }
    
    builder = new StringBuilder();
    for(String ePackage : exportedPackages) {
      builder.append(ePackage).append(",");
    }
    if(builder.length() > 0) {
      builder.deleteCharAt(builder.length() - 1);
      att.put(new Attributes.Name(Constants.EXPORT_PACKAGE), builder.toString());
    }
    return new ModelledResourceImpl(null, att, null, null);
  }
  
  public static ModelledResource createModelledServiceBundle(String bundleName, String bundleVersion,
      Collection<String> importService, Collection<String> exportService) throws InvalidAttributeException 
  {
    Attributes att = new Attributes();
    att.put(new Attributes.Name(Constants.BUNDLE_SYMBOLICNAME), bundleName);
    att.put(new Attributes.Name(Constants.BUNDLE_VERSION), bundleVersion);
    att.put(new Attributes.Name(Constants.BUNDLE_MANIFESTVERSION), "2");
    
    List<ImportedService> importedServices = new ArrayList<ImportedService>();
    for (String s : importService) {
      importedServices.add(new ImportedServiceImpl(false, s, null, null, null, false));
    }
    
    List<ExportedService> exportedServices = new ArrayList<ExportedService>();
    for (String s : exportService) {
      exportedServices.add(new ExportedServiceImpl(null, 0, Collections.singleton(s), Collections.<String,Object>emptyMap()));
    }
    
    return new ModelledResourceImpl(null, att, importedServices, exportedServices);
  }
  
  /**
   * Check the actual results match the expected values, regardless of order of the parts.
   * @param actual the actual manifest entry.
   * @param expected the expected manifest entry.
   * @return true if they match; false otherwise.
   */
  private static boolean isEqual(String actual, String expected)
  {
    Map<NameValuePair, Integer> actualEntries = parseEntries(actual);
    Map<NameValuePair, Integer> expectedEntries = parseEntries(expected);
    return actualEntries.equals(expectedEntries);
  }

  /**
   * Parse manifest entries into a set of values and associated attributes, which can
   * be directly compared for equality regardless of ordering.
   * <p>
   * Example manifest entry format: value1;attrName1=attrValue1;attrName2=attrValue2,value2;attrName1=attrValue1
   * @param entries a manifest header entry.
   * @return a set of parsed entries.
   */
  private static Map<NameValuePair, Integer> parseEntries(String entries)
  {
    Map<NameValuePair, Integer> result = new HashMap<NameValuePair, Integer>();
    for (NameValuePair entry : ManifestHeaderProcessor.parseExportString(entries))
    {
      Integer count = result.get(entry);
      if (count != null)
      {
        // This entry already exists to increment the count.
        count++;
      }
      else
      {
        count = 1;
      }
      result.put(entry, count);
    }
    
    return result;
  }

  @Test
  public void testGetContent_Valid() throws Exception
  {
    // Get a valid set of deployment information.
    DeployedBundles deployedBundles = validDeployedBundles();
    packagesResolve(deployedBundles);
    
    // Check the deployed content entry is correct.
    String contentEntry = deployedBundles.getContent();
    String expectedResult = "bundle.a;deployed-version=1.0.0,bundle.b;deployed-version=1.0.0";
    assertTrue("Content=" + contentEntry, isEqual(contentEntry, expectedResult));
  }

  @Test
  public void testGetUseBundle_Valid() throws Exception
  {
    // Get a valid set of deployment information.
    DeployedBundles deployedBundles = validDeployedBundles();
    packagesResolve(deployedBundles);
    
    // Check the deployed use bundle entry is correct.
    String useBundleEntry = deployedBundles.getUseBundle();
    String expectedResult = "bundle.c;deployed-version=1.0.0,bundle.d;deployed-version=1.0.0";
    assertTrue("UseBundle=" + useBundleEntry, isEqual(useBundleEntry, expectedResult));
  }

  @Test
  public void testGetProvisionBundle_Valid() throws Exception
  {
    // Check the provision bundle entry is correct.
    DeployedBundles deployedBundles = validDeployedBundles();
    packagesResolve(deployedBundles);
    String provisionBundleEntry = deployedBundles.getProvisionBundle();
    String expectedResult = "bundle.e;deployed-version=1.0.0";
    assertTrue("ProvisionBundle=" + provisionBundleEntry, isEqual(provisionBundleEntry, expectedResult));
  }

  @Test
  public void testGetImportPackage_Valid() throws Exception
  {
    // Check the import package entry is correct.
      DeployedBundles deployedBundles = validDeployedBundles();
      packagesResolve(deployedBundles);

      String importPackageEntry = deployedBundles.getImportPackage();
    
    String expectedResult = "package.c;version=\"1.0.0\";bundle-symbolic-name=\"bundle.c\";bundle-version=\"[1.0.0,1.0.0]\","
      + "package.d;version=\"1.0.0\";bundle-symbolic-name=\"bundle.d\";bundle-version=\"[1.0.0,1.0.0]\"," 
      + "package.e;version=\"[1.0.0,2.0.0)\","
      + "package.g;version=\"0.0.0\"";
    
    
    /*
     * String expectedResult = "package.c;bundle-symbolic-name=bundle.c;bundle-version=\"[1.0.0,1.0.0]\""
     
        + ",package.d;version=\"1.0.0\";bundle-symbolic-name=bundle.d;bundle-version=\"[1.0.0,1.0.0]\""
        + ",package.e;version=\"[1.0.0,2.0.0)\""
        + ",package.g";
     */
    assertTrue("ImportPackage=" + importPackageEntry, isEqual(importPackageEntry, expectedResult));
  }
  
  
  

  private enum ternary { CONTENT,USES,NONE }
  
  private DeployedBundles getSimpleDeployedBundles(ternary a, ternary b, ternary c) throws InvalidAttributeException
  {
    Collection<ImportedBundle> content = new ArrayList<ImportedBundle>();
    Collection<ImportedBundle> uses = new ArrayList<ImportedBundle>();

    if(a == ternary.CONTENT)
      content.add(new ImportedBundleImpl("bundle.a", "1.0.0"));
    else if (a == ternary.USES)
      uses.add(new ImportedBundleImpl("bundle.a", "1.0.0"));
    if (b == ternary.CONTENT)
      content.add(new ImportedBundleImpl("bundle.b", "1.0.0"));
    else if (b == ternary.USES)
      uses.add(new ImportedBundleImpl("bundle.b", "1.0.0"));
    if (c == ternary.CONTENT)
      content.add(new ImportedBundleImpl("bundle.c", "1.0.0"));
    else if (c == ternary.USES)
      uses.add(new ImportedBundleImpl("bundle.c", "1.0.0"));
    
    // In a unit test we could go straight to the static method; choosing not to in this case. 
    return new ModellingHelperImpl().createDeployedBundles("test",content, uses, null);
  }
  
  @Test
  public void testGetImportPackage_ValidDuplicates() throws Exception
  {
    DeployedBundles deployedBundles = getSimpleDeployedBundles(ternary.CONTENT, ternary.CONTENT, ternary.CONTENT);
    
    deployedBundles.addBundle(createModelledResource("bundle.a", "1.0.0",
        Arrays.asList("package.d;version=\"[1.0.0,3.0.0)\""), new ArrayList<String>()));
    deployedBundles.addBundle(createModelledResource("bundle.b", "1.0.0",
        Arrays.asList("package.d;version=\"2.0.0\""), new ArrayList<String>()));
    deployedBundles.addBundle(createModelledResource("bundle.c", "1.0.0",
        Arrays.asList("package.d;version=\"1.0.0\""), new ArrayList<String>()));
    deployedBundles.addBundle(createModelledResource("bundle.d", "1.0.0",
        new ArrayList<String>(), Arrays.asList("package.d;version=2.0.1")));
    
    // Check that package D is not duplicated in Import-Package, and that the version range
    // has been narrowed to the intersection of the original requirements.
      String importPackageEntry = deployedBundles.getImportPackage();
    String expectedResult = "package.d;version=\"[2.0.0,3.0.0)\"";
    assertTrue("ImportPackage=" + importPackageEntry, isEqual(importPackageEntry, expectedResult));
  }

  @Test
  public void testGetImportPackage_ValidDuplicatesWithAttributes() throws Exception
  {
    DeployedBundles deployedBundles = getSimpleDeployedBundles(ternary.CONTENT, ternary.CONTENT, ternary.NONE);
    
    deployedBundles.addBundle(createModelledResource("bundle.a", "1.0.0",
        Arrays.asList("package.c;version=1.0.0;was_internal=true"), new ArrayList<String>()));
    deployedBundles.addBundle(createModelledResource("bundle.b", "1.0.0",
        Arrays.asList("package.c;version=2.0.0;was_internal=true"), new ArrayList<String>()));
    deployedBundles.addBundle(createModelledResource("bundle.c", "1.0.0",
        new ArrayList<String>(), Arrays.asList("package.c;version=2.0.0;was_internal=true")));

    // Check that package C is not duplicated in Import-Package, and that the version range
    // has been narrowed to the intersection of the original requirements.
      String importPackageEntry = deployedBundles.getImportPackage();
    String expectedResult = "package.c;was_internal=\"true\";version=\"2.0.0\"";
    assertTrue("ImportPackage=" + importPackageEntry, isEqual(importPackageEntry, expectedResult));
  }

  @Test
  public void testGetImportPackage_InvalidDuplicates() throws Exception
  {
    DeployedBundles deployedBundles = getSimpleDeployedBundles(ternary.CONTENT, ternary.CONTENT, ternary.NONE);
    
    deployedBundles.addBundle(createModelledResource("bundle.a", "1.0.0",
        Arrays.asList("package.c;version=\"[1.0.0,2.0.0)\""), new ArrayList<String>()));
    deployedBundles.addBundle(createModelledResource("bundle.b", "1.0.0",
        Arrays.asList("package.c;version=2.0.0"), new ArrayList<String>()));
    deployedBundles.addBundle(createModelledResource("bundle.c", "1.0.0",
        new ArrayList<String>(), Arrays.asList("package.c;version=2.0.0;was_internal=true")));
    
    // Check that the incompatible version requirements cannot be resolved.
      assertThrows(ResolverException.class, () -> deployedBundles.getImportPackage());
  }

  @Test
  public void testGetImportPackage_InvalidDuplicatesWithAttributes() throws Exception
  {
    DeployedBundles deployedBundles = getSimpleDeployedBundles(ternary.CONTENT, ternary.CONTENT, ternary.NONE);
    
    deployedBundles.addBundle(createModelledResource("bundle.a", "1.0.0",
        Arrays.asList("package.c;version=1.0.0;was_internal=true"), new ArrayList<String>()));
    deployedBundles.addBundle(createModelledResource("bundle.b", "1.0.0",
        Arrays.asList("package.c;version=2.0.0"), new ArrayList<String>()));
    deployedBundles.addBundle(createModelledResource("bundle.c", "1.0.0",
        new ArrayList<String>(), Arrays.asList("package.c;version=2.0.0;was_internal=true")));

    // Check that the incompatible package requirement attributes cause an exception.
      assertThrows(ResolverException.class, () -> deployedBundles.getImportPackage());
  }

  
  @Test
  public void testGetImportPackage_bundleSymbolicNameOK() throws Exception
  {
    DeployedBundles deployedBundles = getSimpleDeployedBundles(ternary.CONTENT, ternary.CONTENT, ternary.NONE);
    
    deployedBundles.addBundle(createModelledResource("bundle.a", "1.0.0",
        Arrays.asList("package.b;version=1.0.0;bundle-symbolic-name=bundle.b;bundle-version=\"[1.0.0,2.0.0)\""), new ArrayList<String>()));
    deployedBundles.addBundle(createModelledResource("bundle.b", "1.0.0",
        new ArrayList<String>(), Arrays.asList("package.b;version=2.0.0")));
    
    // Check that the bundle-symbolic-name attribute for a bundle within deployed-content is ok. 
      String importPackageEntry = deployedBundles.getImportPackage();
    String expectedResult = "";  // All packages are satisfied internally
    assertTrue("ImportPackage=" + importPackageEntry, isEqual(importPackageEntry, expectedResult));

  }
  
  @Test
  public void testGetImportPackage_rfc138PreventsBundleSymbolicNameWorking() throws Exception
  {
    DeployedBundles deployedBundles = getSimpleDeployedBundles(ternary.CONTENT, ternary.USES, ternary.NONE);
    
    deployedBundles.addBundle(createModelledResource("bundle.a", "1.0.0",
        Arrays.asList("package.b;version=1.0.0;bundle-symbolic-name=bundle.b"), new ArrayList<String>()));
    deployedBundles.addBundle(createModelledResource("bundle.b", "1.0.0",
        new ArrayList<String>(), Arrays.asList("package.b;version=2.0.0")));

    
    // Check that the bundle-symbolic-name attribute for a bundle outside use-bundle causes an exception.
      assertThrows(ResolverException.class, () -> deployedBundles.getImportPackage());
  }
  
  @Test
  public void testGetImportPackage_rfc138PreventsBundleVersionWorking() throws Exception
  {
    DeployedBundles deployedBundles = getSimpleDeployedBundles(ternary.CONTENT, ternary.NONE, ternary.NONE);
    
    deployedBundles.addBundle(createModelledResource("bundle.a", "1.0.0",
        Arrays.asList("package.b;version=1.0.0;bundle-version=1.0.0"), new ArrayList<String>()));
    deployedBundles.addBundle(createModelledResource("bundle.b", "1.0.0",
        new ArrayList<String>(), Arrays.asList("package.b;version=2.0.0")));

    
    // Check that the bundle-symbolic-name attribute for a bundle outside use-bundle causes an exception.
      assertThrows(ResolverException.class, () -> deployedBundles.getImportPackage());
  }
  
  @Test
  public void testGetImportPackage_ValidResolutionAttribute() throws Exception
  {
    DeployedBundles deployedBundles = getSimpleDeployedBundles(ternary.CONTENT, ternary.CONTENT, ternary.NONE);
    
    deployedBundles.addBundle(createModelledResource("bundle.a", "1.0.0",
        Arrays.asList("package.c;version=1.0.0;resolution:=optional"), new ArrayList<String>()));
    deployedBundles.addBundle(createModelledResource("bundle.b", "1.0.0",
         Arrays.asList("package.c;version=1.0.0"), new ArrayList<String>()));
    deployedBundles.addBundle(createModelledResource("bundle.c", "1.0.0",
        new ArrayList<String>(), Arrays.asList("package.c;version=1.0.0")));
    
    // Check that the resulting resolution directive is not optional.
      String importPackageEntry = deployedBundles.getImportPackage();
    String expectedResult = "package.c;version=1.0.0";
    assertTrue("ImportPackage=" + importPackageEntry, isEqual(importPackageEntry, expectedResult));
  }

  @Test
  public void testGetRequiredUseBundle_RedundantEntry() throws Exception
  {
    // Bundle A requires package B from bundle B with no version requirement.
    // Bundle B requires package C from bundle C with no version requirement.
    // Bundle C requires package B from bundle B with explicit version requirement.
    
    DeployedBundles deployedBundles = getSimpleDeployedBundles(ternary.CONTENT, ternary.USES, ternary.USES);
    
    deployedBundles.addBundle(createModelledResource("bundle.a", "1.0.0",
        Arrays.asList("package.b"), new ArrayList<String>()));
    deployedBundles.addBundle(createModelledResource("bundle.b", "1.0.0",
         Arrays.asList("package.c"), Arrays.asList("package.b;version=1.0.0")));
    deployedBundles.addBundle(createModelledResource("bundle.c", "1.0.0",
        Arrays.asList("package.b;version=1.0.0"), Arrays.asList("package.c;version=1.0.0")));
      
    // Check the redundant use-bundle entry is identified.
    // Bundle C is not required by app content, although it is specified in use-bundle.
      Collection<ModelledResource> requiredUseBundle = deployedBundles.getRequiredUseBundle();
      assertEquals("RequiredUseBundle=" + requiredUseBundle, 1, requiredUseBundle.size());
  }

  @Test
  public void testGetRequiredUseBundle_Valid() throws Exception
  {
    // Get a valid set of deployment information.
    DeployedBundles deployedBundles = validDeployedBundles();
    packagesResolve(deployedBundles);
    
    // Check all the use-bundle entries are required.
      Collection<ModelledResource> requiredUseBundle = deployedBundles.getRequiredUseBundle();
      assertEquals("RequiredUseBundle=" + requiredUseBundle, 2, requiredUseBundle.size());
  }
  
  //Inside cannot bundle-symbolic-name an outside bundle until the new RFC 138!
  @Test
  public void testGetImportPackage_InvalidBundleVersion() throws Exception
  {
    DeployedBundles deployedBundles = getSimpleDeployedBundles(ternary.CONTENT, ternary.USES, ternary.NONE);
    
    deployedBundles.addBundle(createModelledResource("bundle.a", "1.0.0",
        Arrays.asList("package.b;version=\"[1.0.0,1.0.0]\";bundle-symbolic-name=bundle.b;bundle-version=\"[0.0.0,1.0.0)\"")
        , new ArrayList<String>()));
    deployedBundles.addBundle(createModelledResource("bundle.b", "1.0.0",
         new ArrayList<String>(), Arrays.asList("package.b;version=1.0.0")));

    // Check that the bundle version requirement generates an error because it doesn't match the bundle in use-bundle.
      assertThrows(ResolverException.class, () -> deployedBundles.getImportPackage());
  }
  
  
  @Test
  public void testImportedService() throws Exception
  {
    DeployedBundles deployedBundles = getSimpleDeployedBundles(ternary.CONTENT, ternary.NONE, ternary.NONE);

    deployedBundles.addBundle(createModelledServiceBundle("bundle.a", "1.0.0", 
        Collections.singleton("java.util.List"), Collections.<String>emptyList()));

    deployedBundles.addBundle(createModelledServiceBundle("bundle.b", "1.0.0", 
        Collections.singleton("java.util.Set"), Collections.singleton("java.util.List")));

    deployedBundles.addBundle(createModelledServiceBundle("bundle.c", "1.0.0", 
        Collections.<String>emptyList(), Collections.singleton("java.util.Set")));
    
    assertEquals("(objectClass=java.util.List)", deployedBundles.getDeployedImportService());
  }

}
