/*******************************************************************************
 * Copyright (c) 2010, 2015 Sonatype, Inc.
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License v1.0
 * which accompanies this distribution, and is available at
 * http://www.eclipse.org/legal/epl-v10.html
 *
 * Contributors:
 *    Stuart McCulloch (Sonatype, Inc.) - initial API and implementation
 *******************************************************************************/
package org.eclipse.sisu.plexus;

import java.lang.annotation.Annotation;
import java.lang.reflect.Modifier;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.Collections;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Map;
import java.util.logging.Level;
import java.util.logging.Logger;

import junit.framework.TestCase;

import org.codehaus.plexus.component.annotations.Component;
import org.codehaus.plexus.component.annotations.Requirement;
import org.eclipse.sisu.bean.BeanProperty;
import org.eclipse.sisu.inject.DeferredClass;
import org.eclipse.sisu.inject.DeferredProvider;
import org.eclipse.sisu.space.ClassSpace;
import org.eclipse.sisu.space.LoadedClass;
import org.eclipse.sisu.space.URLClassSpace;
import org.eclipse.sisu.space.asm.ClassWriter;
import org.eclipse.sisu.space.asm.MethodVisitor;
import org.eclipse.sisu.space.asm.Opcodes;

import com.google.inject.TypeLiteral;

public class PlexusXmlScannerTest
    extends TestCase
{
    static class NamedProperty
        implements BeanProperty<Object>
    {
        final String name;

        final TypeLiteral<Object> type;

        public NamedProperty( final String name )
        {
            this.name = name;
            type = TypeLiteral.get( Object.class );
        }

        @SuppressWarnings( { "unchecked", "rawtypes" } )
        public NamedProperty( final String name, final TypeLiteral type )
        {
            this.name = name;
            this.type = type;
        }

        public <A extends Annotation> A getAnnotation( final Class<A> annotationType )
        {
            return null;
        }

        public String getName()
        {
            return name;
        }

        public TypeLiteral<Object> getType()
        {
            return type;
        }

        public <B> void set( final B bean, final Object value )
        {
        }
    }

    interface Bean
    {
    }

    protected static class DefaultBean
    {
    }

    static class DebugBean
    {
    }

    static class AnotherBean
    {
    }

    static class EmptyClassSpace
        implements ClassSpace
    {
        public Class<?> loadClass( final String name )
        {
            try
            {
                return Class.forName( name );
            }
            catch ( final ClassNotFoundException e )
            {
                throw new TypeNotPresentException( name, e );
            }
        }

        @SuppressWarnings( { "unchecked", "rawtypes" } )
        public DeferredClass<?> deferLoadClass( final String name )
        {
            return new LoadedClass( loadClass( name ) );
        }

        public URL getResource( final String name )
        {
            return null;
        }

        public Enumeration<URL> getResources( final String name )
        {
            // hide components.xml so we can just test plexus.xml parsing
            return Collections.enumeration( Collections.<URL> emptyList() );
        }

        public Enumeration<URL> findEntries( final String path, final String glob, final boolean recurse )
        {
            // hide components.xml so we can just test plexus.xml parsing
            return Collections.enumeration( Collections.<URL> emptyList() );
        }
    }

    public void testLoadOnStart()
    {
        final URL plexusXml = getClass().getResource( "/META-INF/plexus/plexus.xml" );
        final PlexusXmlScanner scanner = new PlexusXmlScanner( null, plexusXml, null );

        final Map<Component, DeferredClass<?>> componentMap = scanner.scan( new EmptyClassSpace(), true );

        assertEquals( 2, componentMap.size() );

        final Component component1 =
            new ComponentImpl( DefaultBean.class, Hints.DEFAULT_HINT, Strategies.LOAD_ON_START, "" );
        assertEquals( DefaultBean.class, componentMap.get( component1 ).load() );

        final Component component2 = new ComponentImpl( Bean.class, "debug", Strategies.LOAD_ON_START, "For debugging" );
        assertEquals( DebugBean.class, componentMap.get( component2 ).load() );
    }

    public void testBadPlexusXml()
    {
        final ClassSpace space = new URLClassSpace( PlexusXmlScannerTest.class.getClassLoader() );
        final URL plexusXml = getClass().getResource( "/META-INF/plexus/bad_plexus_1.xml" );
        new PlexusXmlScanner( null, plexusXml, null ).scan( space, true );
    }

    @SuppressWarnings( "deprecation" )
    public void testComponents()
    {
        final ClassSpace space = new URLClassSpace( PlexusXmlScannerTest.class.getClassLoader() );

        final Map<String, PlexusBeanMetadata> metadata = new HashMap<String, PlexusBeanMetadata>();
        final PlexusXmlScanner scanner = new PlexusXmlScanner( null, null, metadata );

        final Map<Component, DeferredClass<?>> componentMap = scanner.scan( space, true );

        assertEquals( 6, componentMap.size() );

        final Component component1 =
            new ComponentImpl( DefaultBean.class, Hints.DEFAULT_HINT, Strategies.PER_LOOKUP, "" );
        assertEquals( DefaultBean.class, componentMap.get( component1 ).load() );

        final Component component2 = new ComponentImpl( Bean.class, "debug", Strategies.SINGLETON, "For debugging" );
        assertEquals( DebugBean.class, componentMap.get( component2 ).load() );

        final Component component3 = new ComponentImpl( Bean.class, Hints.DEFAULT_HINT, Strategies.SINGLETON, "" );
        assertEquals( AnotherBean.class, componentMap.get( component3 ).load() );

        final Component component4 = new ComponentImpl( Bean.class, "clone", Strategies.SINGLETON, "" );
        assertEquals( DefaultBean.class, componentMap.get( component4 ).load().getSuperclass() );
        final Class<?> proxy = CustomTestClassLoader.proxy( componentMap.get( component4 ).load() );

        try
        {
            assertNotNull( proxy.getMethod( "TestMe" ) );
        }
        catch ( final NoSuchMethodException e )
        {
            fail( "Proxied class is missing 'TestMe' method" );
        }

        final PlexusBeanMetadata metadata1 = metadata.get( DefaultBean.class.getName() );

        assertFalse( metadata1.isEmpty() );

        assertEquals( new ConfigurationImpl( "someFieldName", "<some-field-name><item>PRIMARY</item></some-field-name>" ),
                      metadata1.getConfiguration( new NamedProperty( "someFieldName" ) ) );

        assertEquals( new ConfigurationImpl( "simple", "value" ),
                      metadata1.getConfiguration( new NamedProperty( "simple" ) ) );

        assertEquals( new ConfigurationImpl( "value", "<value with=\"attribute\"></value>" ),
                      metadata1.getConfiguration( new NamedProperty( "value" ) ) );

        assertEquals( new ConfigurationImpl( "emptyValue1", "<empty-value1 with=\"attribute\" />" ),
                      metadata1.getConfiguration( new NamedProperty( "emptyValue1" ) ) );

        assertEquals( new ConfigurationImpl( "emptyValue2", "" ),
                      metadata1.getConfiguration( new NamedProperty( "emptyValue2" ) ) );

        assertFalse( metadata1.isEmpty() );

        assertEquals( new RequirementImpl( Bean.class, true, "debug" ),
                      metadata1.getRequirement( new NamedProperty( "bean", TypeLiteral.get( Bean.class ) ) ) );

        assertFalse( metadata1.isEmpty() );

        metadata1.getConfiguration( new NamedProperty( "foo" ) );

        assertEquals( new RequirementImpl( Bean.class, false, Hints.DEFAULT_HINT, "debug" ),
                      metadata1.getRequirement( new NamedProperty( "beanMap" ) ) );

        assertFalse( metadata1.isEmpty() );

        assertEquals( new RequirementImpl( Bean.class, false ),
                      metadata1.getRequirement( new NamedProperty( "beanField" ) ) );

        assertTrue( metadata1.isEmpty() );

        assertNotNull( metadata.get( AnotherBean.class.getName() ) );
        assertNull( metadata.get( DebugBean.class.getName() ) );
    }

    static class FixedClassSpace
        implements ClassSpace
    {
        final String fixedResourceName;

        FixedClassSpace( final String fixedResourceName )
        {
            this.fixedResourceName = fixedResourceName;
        }

        public Class<?> loadClass( final String name )
        {
            try
            {
                return Class.forName( name );
            }
            catch ( final ClassNotFoundException e )
            {
                throw new TypeNotPresentException( name, e );
            }
        }

        @SuppressWarnings( "rawtypes" )
        public DeferredClass<?> deferLoadClass( final String name )
        {
            return new DeferredClass()
            {
                public Class load()
                {
                    return loadClass( name );
                }

                public String getName()
                {
                    return name;
                }

                public DeferredProvider asProvider()
                {
                    throw new UnsupportedOperationException();
                }
            };
        }

        public URL getResource( final String name )
        {
            return getClass().getResource( fixedResourceName );
        }

        public Enumeration<URL> getResources( final String name )
        {
            return Collections.enumeration( Collections.singleton( getClass().getResource( fixedResourceName ) ) );
        }

        public Enumeration<URL> findEntries( final String path, final String glob, final boolean recurse )
        {
            return Collections.enumeration( Collections.singleton( getClass().getResource( fixedResourceName ) ) );
        }
    }

    public void testBadComponentsXml()
    {
        ClassSpace space;

        space = new FixedClassSpace( "/META-INF/plexus/bad_components_1.xml" );
        new PlexusXmlScanner( null, null, null ).scan( space, true );

        space = new FixedClassSpace( "/META-INF/plexus/bad_components_2.xml" );
        new PlexusXmlScanner( null, null, null ).scan( space, true );

        try
        {
            space = new FixedClassSpace( "/META-INF/plexus/bad_components_3.xml" );
            final Map<String, PlexusBeanMetadata> metadata = new HashMap<String, PlexusBeanMetadata>();
            final PlexusXmlScanner scanner = new PlexusXmlScanner( null, null, metadata );

            scanner.scan( space, true );

            final Requirement badReq =
                metadata.get( DefaultBean.class.getName() ).getRequirement( new NamedProperty( "no.such.class" ) );

            badReq.role();
            fail( "Expected TypeNotPresentException" );
        }
        catch ( final TypeNotPresentException e )
        {
        }

        space = new FixedClassSpace( "/META-INF/plexus/bad_components_4.xml" );
        final PlexusXmlScanner scanner = new PlexusXmlScanner( null, null, null );
        assertTrue( scanner.scan( space, true ).isEmpty() );
    }

    public void testInterpolatedComponentsXml()
    {
        final ClassSpace space = new FixedClassSpace( "/META-INF/plexus/variable_components.xml" );

        final Map<String, PlexusBeanMetadata> metadata = new HashMap<String, PlexusBeanMetadata>();

        new PlexusXmlScanner( null, null, metadata ).scan( space, true );

        assertEquals( "${some.value}",
                      metadata.get( DefaultBean.class.getName() ).getConfiguration( new NamedProperty( "variable" ) ).value() );

        final Map<?, ?> variables = Collections.singletonMap( "some.value", "INTERPOLATED" );

        new PlexusXmlScanner( variables, null, metadata ).scan( space, true );

        assertEquals( "INTERPOLATED",
                      metadata.get( DefaultBean.class.getName() ).getConfiguration( new NamedProperty( "variable" ) ).value() );
    }

    public void testLocalizedXmlScanning()
    {
        final ClassSpace space = new URLClassSpace( PlexusXmlScannerTest.class.getClassLoader(), null );

        assertFalse( new PlexusXmlScanner( null, null, null ).scan( space, true ).isEmpty() );
        assertTrue( new PlexusXmlScanner( null, null, null ).scan( space, false ).isEmpty() );
    }

    public void testOptionalLogging()
        throws Exception
    {
        final Level level = Logger.getLogger( "" ).getLevel();
        try
        {
            Logger.getLogger( "" ).setLevel( Level.SEVERE );

            // check everything still works without any SLF4J jars
            final ClassLoader noLoggingLoader =
                new URLClassLoader( new URLClassSpace( getClass().getClassLoader() ).getURLs(), null )
                {
                    @Override
                    protected synchronized Class<?> loadClass( final String name, final boolean resolve )
                        throws ClassNotFoundException
                    {
                        if ( name.contains( "slf4j" ) )
                        {
                            throw new ClassNotFoundException( name );
                        }
                        if ( name.contains( "cobertura" ) )
                        {
                            return PlexusXmlScannerTest.class.getClassLoader().loadClass( name );
                        }
                        return super.loadClass( name, resolve );
                    }
                };

            noLoggingLoader.loadClass( SimpleScanningExample.class.getName() ).newInstance();
        }
        finally
        {
            Logger.getLogger( "" ).setLevel( level );
        }
    }

    static final class CustomTestClassLoader
        extends ClassLoader
    {
        private static final String PROXY_MARKER = "$proxy";

        CustomTestClassLoader( final ClassLoader parent )
        {
            super( parent );
        }

        static Class<?> proxy( final Class<?> clazz )
        {
            try
            {
                return new CustomTestClassLoader( clazz.getClassLoader() ).loadClass( clazz.getName() + PROXY_MARKER );
            }
            catch ( final ClassNotFoundException e )
            {
                throw new TypeNotPresentException( clazz.getName(), e );
            }
        }

        @Override
        protected synchronized Class<?> loadClass( final String name, final boolean resolve )
            throws ClassNotFoundException
        {
            return super.loadClass( name, resolve );
        }

        @Override
        protected Class<?> findClass( final String name )
            throws ClassNotFoundException
        {
            final String proxyName = name.replace( '.', '/' );
            final String superName = proxyName.substring( 0, proxyName.length() - PROXY_MARKER.length() );

            final ClassWriter cw = new ClassWriter( ClassWriter.COMPUTE_MAXS );
            cw.visit( Opcodes.V1_5, Modifier.PUBLIC | Modifier.FINAL, proxyName, null, superName, null );
            MethodVisitor mv = cw.visitMethod( Modifier.PUBLIC, "<init>", "()V", null, null );

            mv.visitCode();
            mv.visitVarInsn( Opcodes.ALOAD, 0 );
            mv.visitMethodInsn( Opcodes.INVOKESPECIAL, superName, "<init>", "()V", false );
            mv.visitInsn( Opcodes.RETURN );
            mv.visitMaxs( 0, 0 );
            mv.visitEnd();

            mv = cw.visitMethod( Modifier.PUBLIC, "TestMe", "()V", null, null );

            mv.visitCode();
            mv.visitInsn( Opcodes.RETURN );
            mv.visitMaxs( 0, 0 );
            mv.visitEnd();
            cw.visitEnd();

            final byte[] buf = cw.toByteArray();

            return defineClass( name, buf, 0, buf.length );
        }
    }
}
