/**
 * 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.aries.spifly.dynamic;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileReader;
import java.io.IOException;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.net.URL;
import java.net.URLClassLoader;
import java.security.ProtectionDomain;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Dictionary;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Hashtable;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

import org.apache.aries.spifly.BaseActivator;
import org.apache.aries.spifly.SpiFlyConstants;
import org.apache.aries.spifly.Streams;
import org.easymock.EasyMock;
import org.easymock.IAnswer;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.osgi.framework.Bundle;
import org.osgi.framework.BundleContext;
import org.osgi.framework.BundleReference;
import org.osgi.framework.Version;
import org.osgi.framework.hooks.weaving.WeavingHook;
import org.osgi.framework.hooks.weaving.WovenClass;
import org.osgi.framework.wiring.BundleRevision;
import org.osgi.framework.wiring.BundleWiring;

public class ClientWeavingHookOSGi43Test  {
    DynamicWeavingActivator activator;

    @Before
    public void setUp() {
        activator = new DynamicWeavingActivator();
        BaseActivator.activator = activator;
    }

    @After
    public void tearDown() {
        BaseActivator.activator = null;
        activator = null;
    }

    @Test
    public void testBasicServiceLoaderUsageWithClassLoaderFromBundleRevision() throws Exception {
        Dictionary<String, String> consumerHeaders = new Hashtable<String, String>();
        consumerHeaders.put(SpiFlyConstants.SPI_CONSUMER_HEADER, "*");

        // Register the bundle that provides the SPI implementation.
        Bundle providerBundle = mockProviderBundle("impl1", 1);
        activator.registerProviderBundle("org.apache.aries.mytest.MySPI", providerBundle, new HashMap<String, Object>());

        Bundle consumerBundle = mockConsumerBundle(consumerHeaders, providerBundle);
        activator.addConsumerWeavingData(consumerBundle, SpiFlyConstants.SPI_CONSUMER_HEADER);

        Bundle spiFlyBundle = mockSpiFlyBundle("spifly", Version.parseVersion("1.9.4"), consumerBundle, providerBundle);
        WeavingHook wh = new ClientWeavingHook(spiFlyBundle.getBundleContext(), activator);

        // Weave the TestClient class.
        URL clsUrl = getClass().getResource("TestClient.class");
        assertNotNull("Precondition", clsUrl);

        String clientClassName = "org.apache.aries.spifly.dynamic.TestClient";
        WovenClass wc = new MyWovenClass(clsUrl, clientClassName, consumerBundle);
        assertEquals("Precondition", 0, wc.getDynamicImports().size());
        wh.weave(wc);
        assertEquals(1, wc.getDynamicImports().size());
        String di1 = "org.apache.aries.spifly";
        String di = wc.getDynamicImports().get(0);
        assertEquals("Weaving should have added a dynamic import", di1, di);

        // Invoke the woven class and check that it properly sets the TCCL so that the
        // META-INF/services/org.apache.aries.mytest.MySPI file from impl1 is visible.
        Class<?> cls = wc.getDefinedClass();
        Method method = cls.getMethod("test", new Class [] {String.class});
        Object result = method.invoke(cls.getDeclaredConstructor().newInstance(), "hello");
        assertEquals(Collections.singleton("olleh"), result);
    }

    private Bundle mockSpiFlyBundle(String bsn, Version version, Bundle ... bundles) throws Exception {
        Bundle spiFlyBundle = EasyMock.createMock(Bundle.class);

        BundleContext spiFlyBundleContext = EasyMock.createMock(BundleContext.class);
        EasyMock.expect(spiFlyBundleContext.getBundle()).andReturn(spiFlyBundle).anyTimes();
        List<Bundle> allBundles = new ArrayList<Bundle>(Arrays.asList(bundles));
        allBundles.add(spiFlyBundle);
        EasyMock.expect(spiFlyBundleContext.getBundles()).andReturn(allBundles.toArray(new Bundle [] {})).anyTimes();
        EasyMock.replay(spiFlyBundleContext);

        EasyMock.expect(spiFlyBundle.getSymbolicName()).andReturn(bsn).anyTimes();
        EasyMock.expect(spiFlyBundle.getVersion()).andReturn(version).anyTimes();
        EasyMock.expect(spiFlyBundle.getBundleId()).andReturn(Long.MAX_VALUE).anyTimes();
        EasyMock.expect(spiFlyBundle.getBundleContext()).andReturn(spiFlyBundleContext).anyTimes();
        EasyMock.replay(spiFlyBundle);

        // Set the bundle context for testing purposes
        Field bcField = BaseActivator.class.getDeclaredField("bundleContext");
        bcField.setAccessible(true);
        bcField.set(activator, spiFlyBundle.getBundleContext());

        return spiFlyBundle;
    }

    private Bundle mockProviderBundle(String subdir, long id) throws Exception {
        return mockProviderBundle(subdir, id, Version.emptyVersion);
    }

    private Bundle mockProviderBundle(String subdir, long id, Version version) throws Exception {
        URL url = getClass().getResource("/" + getClass().getName().replace('.', '/') + ".class");
        File classFile = new File(url.getFile());
        File baseDir = new File(classFile.getParentFile(), subdir);
        File directory = new File(baseDir, "/META-INF/services");
        final List<String> classNames = new ArrayList<String>();

        // Do a directory listing of the applicable META-INF/services directory
        List<String> resources = new ArrayList<String>();
        for (File f : directory.listFiles()) {
            String fileName = f.getName();
            if (fileName.startsWith(".") || fileName.endsWith("."))
                continue;

            classNames.addAll(getClassNames(f));

            // Needs to be something like: META-INF/services/org.apache.aries.mytest.MySPI
            String path = f.getAbsolutePath().substring(baseDir.getAbsolutePath().length());
            path = path.replace('\\', '/');
            if (path.startsWith("/")) {
                path = path.substring(1);
            }
            resources.add(path);
        }

        // Set up the classloader that will be used by the ASM-generated code as the TCCL.
        // It can load a META-INF/services file
        final ClassLoader cl = new TestProviderBundleClassLoader(subdir, resources.toArray(new String [] {}));

        final List<String> classResources = new ArrayList<String>();
        for(String className : classNames) {
            classResources.add("/" + className.replace('.', '/') + ".class");
        }

        Bundle systemBundle = EasyMock.createMock(Bundle.class);
        EasyMock.<Class<?>>expect(systemBundle.loadClass(EasyMock.anyObject(String.class))).andAnswer(new IAnswer<Class<?>>() {
            @Override
            public Class<?> answer() throws Throwable {
                String name = (String) EasyMock.getCurrentArguments()[0];
                return ClientWeavingHookOSGi43Test.class.getClassLoader().loadClass(name);
            }
        }).anyTimes();
        EasyMock.replay(systemBundle);

        BundleContext bc = EasyMock.createNiceMock(BundleContext.class);
        EasyMock.expect(bc.getBundle(0)).andReturn(systemBundle).anyTimes();
        EasyMock.replay(bc);

        BundleWiring bundleWiring = EasyMock.createMock(BundleWiring.class);
        EasyMock.expect(bundleWiring.getClassLoader()).andReturn(cl).anyTimes();
        EasyMock.replay(bundleWiring);

        BundleRevision bundleRevision = EasyMock.createMock(BundleRevision.class);
        EasyMock.expect(bundleRevision.getWiring()).andReturn(bundleWiring).anyTimes();
        EasyMock.replay(bundleRevision);

        Bundle providerBundle = EasyMock.createMock(Bundle.class);
        String bsn = subdir;
        int idx = bsn.indexOf('_');
        if (idx > 0) {
            bsn = bsn.substring(0, idx);
        }
        EasyMock.expect(providerBundle.getSymbolicName()).andReturn(bsn).anyTimes();
        EasyMock.expect(providerBundle.getBundleId()).andReturn(id).anyTimes();
        EasyMock.expect(providerBundle.getBundleContext()).andReturn(bc).anyTimes();
        EasyMock.expect(providerBundle.getVersion()).andReturn(version).anyTimes();
        EasyMock.expect(providerBundle.adapt(BundleRevision.class)).andReturn(bundleRevision).anyTimes();
        EasyMock.<Class<?>>expect(providerBundle.loadClass(EasyMock.anyObject(String.class))).andAnswer(new IAnswer<Class<?>>() {
            @Override
            public Class<?> answer() throws Throwable {
                String name = (String) EasyMock.getCurrentArguments()[0];
                if (!classNames.contains(name)) {
                    throw new ClassCastException(name);
                }
                return cl.loadClass(name);
            }
        }).anyTimes();
        EasyMock.replay(providerBundle);
        return providerBundle;
    }

    private Collection<String> getClassNames(File f) throws IOException {
        List<String> names = new ArrayList<String>();

        BufferedReader br = new BufferedReader(new FileReader(f));
        try {
            String line = null;
            while((line = br.readLine()) != null) {
                names.add(line.trim());
            }
        } finally {
            br.close();
        }
        return names;
    }

    private Bundle mockConsumerBundle(Dictionary<String, String> headers, Bundle ... otherBundles) {
        // Create a mock object for the client bundle which holds the code that uses ServiceLoader.load()
        // or another SPI invocation.
        BundleContext bc = EasyMock.createMock(BundleContext.class);

        Bundle consumerBundle = EasyMock.createMock(Bundle.class);
        EasyMock.expect(consumerBundle.getSymbolicName()).andReturn("testConsumer").anyTimes();
        EasyMock.expect(consumerBundle.getHeaders()).andReturn(headers).anyTimes();
        EasyMock.expect(consumerBundle.getBundleContext()).andReturn(bc).anyTimes();
        EasyMock.expect(consumerBundle.getBundleId()).andReturn(Long.MAX_VALUE).anyTimes();
        EasyMock.expect(consumerBundle.adapt(BundleRevision.class)).andReturn(null).anyTimes();
        EasyMock.replay(consumerBundle);

        List<Bundle> allBundles = new ArrayList<Bundle>(Arrays.asList(otherBundles));
        allBundles.add(consumerBundle);
        EasyMock.expect(bc.getBundles()).andReturn(allBundles.toArray(new Bundle [] {})).anyTimes();
        EasyMock.replay(bc);

        return consumerBundle;
    }

    // A classloader that loads anything starting with org.apache.aries.spifly.dynamic.impl1 from it
    // and the rest from the parent. This is to mimic a bundle that holds a specific SPI implementation.
    public static class TestProviderBundleClassLoader extends URLClassLoader {
        private final List<String> resources;
        private final String prefix;
        private final String classPrefix;
        private final Map<String, Class<?>> loadedClasses = new ConcurrentHashMap<String, Class<?>>();

        public TestProviderBundleClassLoader(String subdir, String ... resources) {
            super(new URL [] {}, TestProviderBundleClassLoader.class.getClassLoader());

            this.prefix = TestProviderBundleClassLoader.class.getPackage().getName().replace('.', '/') + "/" + subdir + "/";
            this.classPrefix = prefix.replace('/', '.');
            this.resources = Arrays.asList(resources);
        }

        @Override
        public Class<?> loadClass(String name) throws ClassNotFoundException {
            if (name.startsWith(classPrefix))
                return loadClassLocal(name);

            return super.loadClass(name);
        }

        @Override
        protected synchronized Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
            if (name.startsWith(classPrefix)) {
                Class<?> cls = loadClassLocal(name);
                if (resolve)
                    resolveClass(cls);

                return cls;
            }

            return super.loadClass(name, resolve);
        }

        protected Class<?> loadClassLocal(String name) throws ClassNotFoundException {
            Class<?> prevLoaded = loadedClasses.get(name);
            if (prevLoaded != null)
                return prevLoaded;

            URL res = TestProviderBundleClassLoader.class.getClassLoader().getResource(name.replace('.', '/') + ".class");
            try {
                byte[] bytes = Streams.suck(res.openStream());
                Class<?> cls = defineClass(name, bytes, 0, bytes.length);
                loadedClasses.put(name, cls);
                return cls;
            } catch (Exception e) {
                throw new ClassNotFoundException(name, e);
            }
        }

        @Override
        public URL findResource(String name) {
            if (resources.contains(name)) {
                return getClass().getClassLoader().getResource(prefix + name);
            } else {
                return super.findResource(name);
            }
        }

        @Override
        public Enumeration<URL> findResources(String name) throws IOException {
            if (resources.contains(name)) {
                return getClass().getClassLoader().getResources(prefix + name);
            } else {
                return super.findResources(name);
            }
        }
    }

    private static class MyWovenClass implements WovenClass {
        byte [] bytes;
        final String className;
        final Bundle bundleContainingOriginalClass;
        List<String> dynamicImports = new ArrayList<String>();
        boolean weavingComplete = false;

        private MyWovenClass(URL clazz, String name, Bundle bundle) throws Exception {
            bytes = Streams.suck(clazz.openStream());
            className = name;
            bundleContainingOriginalClass = bundle;
        }

        @Override
        public byte[] getBytes() {
            return bytes;
        }

        @Override
        public void setBytes(byte[] newBytes) {
            bytes = newBytes;
        }

        @Override
        public List<String> getDynamicImports() {
            return dynamicImports;
        }

        @Override
        public boolean isWeavingComplete() {
            return weavingComplete;
        }

        @Override
        public String getClassName() {
            return className;
        }

        @Override
        public ProtectionDomain getProtectionDomain() {
            return null;
        }

        @Override
        public Class<?> getDefinedClass() {
            try {
                weavingComplete = true;
                return new MyWovenClassClassLoader(className, getBytes(), getClass().getClassLoader(), bundleContainingOriginalClass).loadClass(className);
            } catch (ClassNotFoundException e) {
                e.printStackTrace();
                return null;
            }
        }

        @Override
        public BundleWiring getBundleWiring() {
            BundleWiring bw = EasyMock.createMock(BundleWiring.class);
            EasyMock.expect(bw.getBundle()).andReturn(bundleContainingOriginalClass);
            EasyMock.expect(bw.getClassLoader()).andReturn(getClass().getClassLoader());
            EasyMock.replay(bw);
            return bw;
        }

        @Override
        public int getState() {
            return TRANSFORMED;
        }
    }

    private static class MyWovenClassClassLoader extends ClassLoader implements BundleReference {
        private final String className;
        private final Bundle bundle;
        private final byte [] bytes;
        private Class<?> wovenClass;

        public MyWovenClassClassLoader(String className, byte[] bytes, ClassLoader parent, Bundle bundle) {
            super(parent);

            this.className = className;
            this.bundle = bundle;
            this.bytes = bytes;
        }

        @Override
        protected synchronized Class<?> loadClass(String name, boolean resolve)
                throws ClassNotFoundException {
            if (name.equals(className)) {
                if (wovenClass == null)
                    wovenClass = defineClass(className, bytes, 0, bytes.length);

                return wovenClass;
            } else {
                return super.loadClass(name, resolve);
            }
        }

        @Override
        public Class<?> loadClass(String name) throws ClassNotFoundException {
            return loadClass(name, false);
        }

        @Override
        public Bundle getBundle() {
            return bundle;
        }
    }
}
