package org.apache.maven.plugin.testing;

/*
 * 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.
 */

import java.io.File;
import java.io.IOException;
import java.lang.reflect.Field;
import java.util.HashSet;
import java.util.Set;

import org.apache.maven.artifact.Artifact;
import org.apache.maven.artifact.DefaultArtifact;
import org.apache.maven.artifact.handler.ArtifactHandler;
import org.apache.maven.artifact.versioning.VersionRange;
import org.apache.maven.plugin.testing.stubs.DefaultArtifactHandlerStub;
import org.codehaus.plexus.archiver.Archiver;
import org.codehaus.plexus.archiver.ArchiverException;
import org.codehaus.plexus.archiver.manager.ArchiverManager;
import org.codehaus.plexus.archiver.manager.NoSuchArchiverException;
import org.codehaus.plexus.archiver.war.WarArchiver;
import org.codehaus.plexus.util.FileUtils;
import org.codehaus.plexus.util.ReflectionUtils;
import org.codehaus.plexus.util.StringUtils;

/**
 * This class creates artifacts to be used for testing purposes. It can optionally create actual files on the local disk
 * for things like copying. It can create these files as archives with named files inside to be used for testing things
 * like unpack. Also provided are some utility methods to quickly get a set of artifacts distinguished by various things
 * like group,artifact,type,scope, etc It was originally developed for the dependency plugin, but can be useful in other
 * plugins that need to simulate artifacts for unit tests.
 *
 * @author <a href="mailto:brianf@apache.org">Brian Fox</a>
 * @version $Id: ArtifactStubFactory.java 1340745 2012-05-20 14:48:44Z hboutemy $
 */
public class ArtifactStubFactory
{
    private File workingDir;

    private boolean createFiles;

    private File srcFile;

    private boolean createUnpackableFile;

    private ArchiverManager archiverManager;

    /**
     * Default constructor. This should be used only if real files aren't needed...just the artifact objects
     */
    public ArtifactStubFactory()
    {
        this.workingDir = null;
        this.createFiles = false;
    }

    /**
     * This constructor is to be used if files are needed and to set a working dir
     *
     * @param workingDir
     * @param createFiles
     */
    public ArtifactStubFactory( File workingDir, boolean createFiles )
    {
        this.workingDir = new File( workingDir, "localTestRepo" );
        this.createFiles = createFiles;
    }

    /**
     * If set, the file will be created as a zip/jar/war with a file inside that can be checked to exist after
     * unpacking.
     *
     * @param archiverManager
     */
    public void setUnpackableFile( ArchiverManager archiverManager )
    {
        this.createUnpackableFile = true;
        this.archiverManager = archiverManager;
    }

    /**
     * @param groupId
     * @param artifactId
     * @param version
     * @return a <code>DefaultArtifact</code> instance for the given parameters
     * @throws IOException if any
     * @see #createArtifact(String, String, String, String, String, String)
     */
    public Artifact createArtifact( String groupId, String artifactId, String version )
        throws IOException
    {
        return createArtifact( groupId, artifactId, version, Artifact.SCOPE_COMPILE, "jar", "" );
    }

    /**
     * @param groupId
     * @param artifactId
     * @param version
     * @param scope
     * @return a <code>DefaultArtifact</code> instance for the given parameters
     * @throws IOException if any
     * @see #createArtifact(String, String, String, String, String, String)
     */
    public Artifact createArtifact( String groupId, String artifactId, String version, String scope )
        throws IOException
    {
        return createArtifact( groupId, artifactId, version, scope, "jar", "" );
    }

    /**
     * @param groupId
     * @param artifactId
     * @param version
     * @param scope
     * @param type
     * @param classifier
     * @return a <code>DefaultArtifact</code> instance for the given parameters
     * @throws IOException if any
     * @see #createArtifact(String, String, VersionRange, String, String, String, boolean)
     */
    public Artifact createArtifact( String groupId, String artifactId, String version, String scope, String type,
                                    String classifier )
        throws IOException
    {
        VersionRange vr = VersionRange.createFromVersion( version );
        return createArtifact( groupId, artifactId, vr, scope, type, classifier, false );
    }

    /**
     * @param groupId not null
     * @param artifactId not null
     * @param versionRange not null
     * @param scope not null
     * @param type not null
     * @param classifier
     * @param optional not null
     * @return a <code>DefaultArtifact</code> instance
     * @throws IOException if any
     */
    public Artifact createArtifact( String groupId, String artifactId, VersionRange versionRange, String scope,
                                    String type, String classifier, boolean optional )
        throws IOException
    {
        ArtifactHandler ah = new DefaultArtifactHandlerStub( type, classifier );

        Artifact artifact =
            new DefaultArtifact( groupId, artifactId, versionRange, scope, type, classifier, ah, optional );

        // i have no idea why this needs to be done manually when isSnapshot is able to figure it out.
        artifact.setRelease( !artifact.isSnapshot() );

        if ( createFiles )
        {
            setArtifactFile( artifact, this.workingDir, this.srcFile, this.createUnpackableFile );
        }
        return artifact;
    }

    /**
     * Creates a new empty file and attaches it to the artifact.
     *
     * @param artifact to attach the file to.
     * @param workingDir where to locate the new file
     * @throws IOException
     */
    public void setArtifactFile( Artifact artifact, File workingDir )
        throws IOException
    {
        setArtifactFile( artifact, workingDir, null, false );
    }

    /**
     * Copyies the srcFile to the workingDir and then attaches it to the artifact. If srcFile is null, a new empty file
     * will be created.
     *
     * @param artifact to attach
     * @param workingDir where to copy the srcFile.
     * @param srcFile file to be attached.
     * @throws IOException
     */
    public void setArtifactFile( Artifact artifact, File workingDir, File srcFile )
        throws IOException
    {
        setArtifactFile( artifact, workingDir, srcFile, false );
    }

    /**
     * Creates an unpackable file (zip,jar etc) containing an empty file.
     *
     * @param artifact to attach
     * @param workingDir where to create the file.
     * @throws IOException
     */
    public void setUnpackableArtifactFile( Artifact artifact, File workingDir )
        throws IOException
    {
        setArtifactFile( artifact, workingDir, null, true );
    }

    /**
     * Creates an unpackable file (zip,jar etc) containing the srcFile. If srcFile is null, a new empty file will be
     * created.
     *
     * @param artifact to attach
     * @param workingDir where to create the file.
     * @param srcFile
     * @throws IOException if any
     */
    public void setUnpackableArtifactFile( Artifact artifact, File workingDir, File srcFile )
        throws IOException
    {
        setArtifactFile( artifact, workingDir, srcFile, true );
    }

    /**
     * Creates a file that can be copied or unpacked based on the passed in artifact
     *
     * @param artifact
     * @param workingDir
     * @param srcFile
     * @param createUnpackableFile
     * @throws IOException if any
     */
    private void setArtifactFile( Artifact artifact, File workingDir, File srcFile, boolean createUnpackableFile )
        throws IOException
    {
        if ( workingDir == null )
        {
            throw new IllegalArgumentException(
                                                "The workingDir must be set." );
        }

        String fileName = getFormattedFileName( artifact, false );

        File theFile = new File( workingDir, fileName );
        theFile.getParentFile().mkdirs();

        if ( srcFile == null )
        {
            theFile.createNewFile();
        }
        else if ( createUnpackableFile )
        {
            try
            {
                createUnpackableFile( artifact, theFile );
            }
            catch ( NoSuchArchiverException e )
            {
                throw new IOException( "NoSuchArchiverException: " + e.getMessage() );
            }
            catch ( ArchiverException e )
            {
                throw new IOException( "ArchiverException: " + e.getMessage() );
            }
        }
        else
        {
            FileUtils.copyFile( srcFile, theFile );
        }

        artifact.setFile( theFile );
    }

    /**
     * @param artifact
     * @return
     */
    public static String getUnpackableFileName( Artifact artifact )
    {
        return "" + artifact.getGroupId() + "-" + artifact.getArtifactId() + "-" + artifact.getVersion() + "-"
            + artifact.getClassifier() + "-" + artifact.getType() + ".txt";
    }

    /**
     * @param artifact
     * @param destFile
     * @throws NoSuchArchiverException
     * @throws ArchiverException if any
     * @throws IOException if any
     */
    public void createUnpackableFile( Artifact artifact, File destFile )
        throws NoSuchArchiverException, ArchiverException, IOException
    {
        Archiver archiver = archiverManager.getArchiver( destFile );

        archiver.setDestFile( destFile );
        archiver.addFile( srcFile, getUnpackableFileName( artifact ) );

        try
        {
            setVariableValueToObject( archiver, "logger", new SilentLog() );
        }
        catch ( IllegalAccessException e )
        {
            System.out.println( "Unable to override logger with silent log." );
            e.printStackTrace();
        }
        if ( archiver instanceof WarArchiver )
        {
            WarArchiver war = (WarArchiver) archiver;
            // the use of this is counter-intuitive:
            // http://jira.codehaus.org/browse/PLX-286
            war.setIgnoreWebxml( false );
        }
        archiver.createArchive();
    }

    /**
     * @return a <code>DefaultArtifact</code> instance for <code>testGroupId:release:jar:1.0</code>
     * @throws IOException if any
     */
    public Artifact getReleaseArtifact()
        throws IOException
    {
        return createArtifact( "testGroupId", "release", "1.0" );
    }

    /**
     * @return a default <code>DefaultArtifact</code> instance for <code>testGroupId:snapshot:jar:2.0-SNAPSHOT</code>
     * @throws IOException if any
     */
    public Artifact getSnapshotArtifact()
        throws IOException
    {
        return createArtifact( "testGroupId", "snapshot", "2.0-SNAPSHOT" );
    }

    /**
     * @return a default set of release and snapshot <code>DefaultArtifact</code>, i.e.:
     * <code>testGroupId:snapshot:jar:2.0-SNAPSHOT, testGroupId:release:jar:1.0</code>
     * @throws IOException if any
     * @see #getReleaseArtifact()
     * @see #getSnapshotArtifact()
     */
    public Set<Artifact> getReleaseAndSnapshotArtifacts()
        throws IOException
    {
        Set<Artifact> set = new HashSet<Artifact>();
        set.add( getReleaseArtifact() );
        set.add( getSnapshotArtifact() );
        return set;
    }

    /**
     * @return a default set of <code>DefaultArtifact</code>, i.e.:
     * <code>g:provided:jar:1.0, g:compile:jar:1.0, g:system:jar:1.0, g:test:jar:1.0, g:runtime:jar:1.0</code>
     * @throws IOException if any
     */
    public Set<Artifact> getScopedArtifacts()
        throws IOException
    {
        Set<Artifact> set = new HashSet<Artifact>();
        set.add( createArtifact( "g", "compile", "1.0", Artifact.SCOPE_COMPILE ) );
        set.add( createArtifact( "g", "provided", "1.0", Artifact.SCOPE_PROVIDED ) );
        set.add( createArtifact( "g", "test", "1.0", Artifact.SCOPE_TEST ) );
        set.add( createArtifact( "g", "runtime", "1.0", Artifact.SCOPE_RUNTIME ) );
        set.add( createArtifact( "g", "system", "1.0", Artifact.SCOPE_SYSTEM ) );
        return set;
    }

    /**
     * @return a set of <code>DefaultArtifact</code>, i.e.:
     * <code>g:d:zip:1.0, g:a:war:1.0, g:b:jar:1.0, g:c:sources:1.0, g:e:rar:1.0</code>
     * @throws IOException if any
     */
    public Set<Artifact> getTypedArtifacts()
        throws IOException
    {
        Set<Artifact> set = new HashSet<Artifact>();
        set.add( createArtifact( "g", "a", "1.0", Artifact.SCOPE_COMPILE, "war", null ) );
        set.add( createArtifact( "g", "b", "1.0", Artifact.SCOPE_COMPILE, "jar", null ) );
        set.add( createArtifact( "g", "c", "1.0", Artifact.SCOPE_COMPILE, "sources", null ) );
        set.add( createArtifact( "g", "d", "1.0", Artifact.SCOPE_COMPILE, "zip", null ) );
        set.add( createArtifact( "g", "e", "1.0", Artifact.SCOPE_COMPILE, "rar", null ) );
        return set;
    }

    /**
     * @return a set of <code>DefaultArtifact</code>, i.e.:
     * <code>g:c:jar:three:1.0, g:b:jar:two:1.0, g:d:jar:four:1.0, g:a:jar:one:1.0</code>
     * @throws IOException if any
     */
    public Set<Artifact> getClassifiedArtifacts()
        throws IOException
    {
        Set<Artifact> set = new HashSet<Artifact>();
        set.add( createArtifact( "g", "a", "1.0", Artifact.SCOPE_COMPILE, "jar", "one" ) );
        set.add( createArtifact( "g", "b", "1.0", Artifact.SCOPE_COMPILE, "jar", "two" ) );
        set.add( createArtifact( "g", "c", "1.0", Artifact.SCOPE_COMPILE, "jar", "three" ) );
        set.add( createArtifact( "g", "d", "1.0", Artifact.SCOPE_COMPILE, "jar", "four" ) );
        return set;
    }

    /**
     * @return a set of <code>DefaultArtifact</code>, i.e.:
     * <code>g:d:zip:1.0, g:a:war:1.0, g:b:jar:1.0, g:e:rar:1.0</code>
     * @throws IOException if any
     */
    public Set<Artifact> getTypedArchiveArtifacts()
        throws IOException
    {
        Set<Artifact> set = new HashSet<Artifact>();
        set.add( createArtifact( "g", "a", "1.0", Artifact.SCOPE_COMPILE, "war", null ) );
        set.add( createArtifact( "g", "b", "1.0", Artifact.SCOPE_COMPILE, "jar", null ) );
        set.add( createArtifact( "g", "d", "1.0", Artifact.SCOPE_COMPILE, "zip", null ) );
        set.add( createArtifact( "g", "e", "1.0", Artifact.SCOPE_COMPILE, "rar", null ) );
        return set;
    }

    /**
     * @return a set of <code>DefaultArtifact</code>, i.e.:
     * <code>g:one:jar:a:1.0, g:two:jar:a:1.0, g:four:jar:a:1.0, g:three:jar:a:1.0</code>
     * @throws IOException if any
     */
    public Set<Artifact> getArtifactArtifacts()
        throws IOException
    {
        Set<Artifact> set = new HashSet<Artifact>();
        set.add( createArtifact( "g", "one", "1.0", Artifact.SCOPE_COMPILE, "jar", "a" ) );
        set.add( createArtifact( "g", "two", "1.0", Artifact.SCOPE_COMPILE, "jar", "a" ) );
        set.add( createArtifact( "g", "three", "1.0", Artifact.SCOPE_COMPILE, "jar", "a" ) );
        set.add( createArtifact( "g", "four", "1.0", Artifact.SCOPE_COMPILE, "jar", "a" ) );
        return set;
    }

    /**
     * @return a set of <code>DefaultArtifact</code>, i.e.:
     * <code>one:group-one:jar:a:1.0, three:group-three:jar:a:1.0, four:group-four:jar:a:1.0,
     * two:group-two:jar:a:1.0</code>
     * @throws IOException if any
     */
    public Set<Artifact> getGroupIdArtifacts()
        throws IOException
    {
        Set<Artifact> set = new HashSet<Artifact>();
        set.add( createArtifact( "one", "group-one", "1.0", Artifact.SCOPE_COMPILE, "jar", "a" ) );
        set.add( createArtifact( "two", "group-two", "1.0", Artifact.SCOPE_COMPILE, "jar", "a" ) );
        set.add( createArtifact( "three", "group-three", "1.0", Artifact.SCOPE_COMPILE, "jar", "a" ) );
        set.add( createArtifact( "four", "group-four", "1.0", Artifact.SCOPE_COMPILE, "jar", "a" ) );
        return set;
    }

    /**
     * @return a set of <code>DefaultArtifact</code>
     * @throws IOException if any
     * @see #getTypedArtifacts()
     * @see #getScopedArtifacts()
     * @see #getReleaseAndSnapshotArtifacts()
     */
    public Set<Artifact> getMixedArtifacts()
        throws IOException
    {
        Set<Artifact> set = new HashSet<Artifact>();
        set.addAll( getTypedArtifacts() );
        set.addAll( getScopedArtifacts() );
        set.addAll( getReleaseAndSnapshotArtifacts() );
        return set;
    }

    /**
     * @return Returns the createFiles.
     */
    public boolean isCreateFiles()
    {
        return this.createFiles;
    }

    /**
     * @param createFiles The createFiles to set.
     */
    public void setCreateFiles( boolean createFiles )
    {
        this.createFiles = createFiles;
    }

    /**
     * @return Returns the workingDir.
     */
    public File getWorkingDir()
    {
        return this.workingDir;
    }

    /**
     * @param workingDir The workingDir to set.
     */
    public void setWorkingDir( File workingDir )
    {
        this.workingDir = workingDir;
    }

    /**
     * @return Returns the srcFile.
     */
    public File getSrcFile()
    {
        return this.srcFile;
    }

    /**
     * @param srcFile The srcFile to set.
     */
    public void setSrcFile( File srcFile )
    {
        this.srcFile = srcFile;
    }

    /**
     * Convenience method to set values to variables in objects that don't have setters
     *
     * @param object
     * @param variable
     * @param value
     * @throws IllegalAccessException
     */
    public static void setVariableValueToObject( Object object, String variable, Object value )
        throws IllegalAccessException
    {
        Field field = ReflectionUtils.getFieldByNameIncludingSuperclasses( variable, object.getClass() );

        field.setAccessible( true );

        field.set( object, value );
    }

    /**
     * Builds the file name. If removeVersion is set, then the file name must be reconstructed from the artifactId,
     * Classifier (if used) and Type. Otherwise, this method returns the artifact file name.
     *
     * @param artifact File to be formatted.
     * @param removeVersion Specifies if the version should be removed from the file name.
     * @return Formatted file name in the format artifactId-[version]-[classifier].[type]
     */
    public static String getFormattedFileName( Artifact artifact, boolean removeVersion )
    {
        String destFileName = null;

        // if there is a file and we aren't stripping the version, just get the
        // name directly
        if ( artifact.getFile() != null && !removeVersion )
        {
            destFileName = artifact.getFile().getName();
        }
        else
        // if offline
        {
            String versionString = null;
            if ( !removeVersion )
            {
                versionString = "-" + artifact.getVersion();
            }
            else
            {
                versionString = "";
            }

            String classifierString = "";

            if ( StringUtils.isNotEmpty( artifact.getClassifier() ) )
            {
                classifierString = "-" + artifact.getClassifier();
            }

            destFileName = artifact.getArtifactId() + versionString + classifierString + "."
                + artifact.getArtifactHandler().getExtension();
        }
        return destFileName;
    }

}
