package org.apache.maven.shared.test.plugin;

/*
 * 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.io.Reader;
import java.io.Writer;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Properties;

import org.apache.maven.artifact.Artifact;
import org.apache.maven.artifact.UnknownRepositoryLayoutException;
import org.apache.maven.artifact.factory.ArtifactFactory;
import org.apache.maven.artifact.handler.ArtifactHandler;
import org.apache.maven.artifact.handler.manager.ArtifactHandlerManager;
import org.apache.maven.artifact.repository.ArtifactRepository;
import org.apache.maven.artifact.repository.ArtifactRepositoryFactory;
import org.apache.maven.model.Build;
import org.apache.maven.model.DeploymentRepository;
import org.apache.maven.model.DistributionManagement;
import org.apache.maven.model.Model;
import org.apache.maven.model.Plugin;
import org.apache.maven.model.Repository;
import org.apache.maven.model.Site;
import org.apache.maven.model.io.xpp3.MavenXpp3Reader;
import org.apache.maven.model.io.xpp3.MavenXpp3Writer;
import org.apache.maven.project.DefaultProjectBuildingRequest;
import org.apache.maven.project.MavenProject;
import org.apache.maven.project.ProjectBuilder;
import org.apache.maven.project.ProjectBuildingException;
import org.apache.maven.project.ProjectBuildingRequest;
import org.apache.maven.project.artifact.ProjectArtifactMetadata;
import org.apache.maven.repository.internal.MavenRepositorySystemSession;
import org.codehaus.plexus.component.annotations.Component;
import org.codehaus.plexus.component.annotations.Requirement;
import org.codehaus.plexus.util.FileUtils;
import org.codehaus.plexus.util.IOUtil;
import org.codehaus.plexus.util.ReaderFactory;
import org.codehaus.plexus.util.WriterFactory;
import org.codehaus.plexus.util.xml.Xpp3Dom;
import org.codehaus.plexus.util.xml.pull.XmlPullParserException;

/**
 * Testing tool used to read MavenProject instances from pom.xml files, and to create plugin jar
 * files (package phase of the normal build process) for distribution to a test local repository
 * directory.
 *
 * @author jdcasey
 * @version $Id: ProjectTool.java 1345442 2012-06-02 05:26:01Z hboutemy $
 */
@Component( role = ProjectTool.class )
public class ProjectTool
{
    /** Plexus role */
    public static final String ROLE = ProjectTool.class.getName();

    public static final String INTEGRATION_TEST_DEPLOYMENT_REPO_URL = "integration-test.deployment.repo.url";

    @Requirement
    private BuildTool buildTool;

    @Requirement
    private RepositoryTool repositoryTool;

    @Requirement
    private ProjectBuilder projectBuilder;

    @Requirement
    private ArtifactHandlerManager artifactHandlerManager;

    @Requirement
    private ArtifactFactory artifactFactory;

    @Requirement
    private ArtifactRepositoryFactory artifactRepositoryFactory;

    /**
     * Construct a MavenProject instance from the specified POM file.
     *
     * @param pomFile current POM file
     * @return the Maven project from a file
     * @throws TestToolsException if any
     */
    public MavenProject readProject( File pomFile )
        throws TestToolsException
    {
        return readProject( pomFile, repositoryTool.findLocalRepositoryDirectory() );
    }

    /**
     * Construct a MavenProject instance from the specified POM file, using the specified local
     * repository directory to resolve ancestor POMs as needed.
     *
     * @param pomFile current POM file
     * @param localRepositoryBasedir
     * @return the Maven project from a file and a local repo
     * @throws TestToolsException if any
     */
    public MavenProject readProject( File pomFile, File localRepositoryBasedir )
        throws TestToolsException
    {
        try
        {
            ArtifactRepository localRepository = repositoryTool
                .createLocalArtifactRepositoryInstance( localRepositoryBasedir );

            ProjectBuildingRequest request = new DefaultProjectBuildingRequest();
            return projectBuilder.build( pomFile, request ).getProject();
        }
        catch ( ProjectBuildingException e )
        {
            throw new TestToolsException( "Error building MavenProject instance from test pom: " + pomFile, e );
        }
    }

    /**
     * Construct a MavenProject instance from the specified POM file with dependencies.
     *
     * @param pomFile current POM file
     * @return the Maven project with dependencies from a file
     * @throws TestToolsException if any
     */
    public MavenProject readProjectWithDependencies( File pomFile )
        throws TestToolsException
    {
        return readProjectWithDependencies( pomFile, repositoryTool.findLocalRepositoryDirectory() );
    }

    /**
     * Construct a MavenProject instance from the specified POM file with dependencies, using the specified local
     * repository directory to resolve ancestor POMs as needed.
     *
     * @param pomFile current POM file
     * @param localRepositoryBasedir
     * @return the Maven project with dependencies from a file and a local repo
     * @throws TestToolsException if any
     */
    public MavenProject readProjectWithDependencies( File pomFile, File localRepositoryBasedir )
        throws TestToolsException
    {
        try
        {
            ArtifactRepository localRepository = repositoryTool
                .createLocalArtifactRepositoryInstance( localRepositoryBasedir );

            ProjectBuildingRequest request = new DefaultProjectBuildingRequest();
            return projectBuilder.build( pomFile, request ).getProject();
        }
        catch ( ProjectBuildingException e )
        {
            throw new TestToolsException( "Error building MavenProject instance from test pom: " + pomFile, e );
        }
    }

    /**
     * Run the plugin's Maven build up to the package phase, in order to produce a jar file for
     * distribution to a test-time local repository. The testVersion parameter specifies the version
     * to be used in the &lt;version/&gt; element of the plugin configuration, and also in fully
     * qualified, unambiguous goal invocations (as in
     * org.apache.maven.plugins:maven-eclipse-plugin:test:eclipse).
     *
     * @param pomFile The plugin's POM
     * @param testVersion The version to use for testing this plugin. To promote test resiliency,
     *   this version should remain unchanged, regardless of what plugin version is under
     *   development.
     * @param skipUnitTests In cases where test builds occur during the unit-testing phase (usually
     *   a bad testing smell), the plugin jar must be produced <b>without</b> running unit tests.
     *   Otherwise, the testing process will result in a recursive loop of building a plugin jar and
     *   trying to unit test it during the build. In these cases, set this flag to <code>true</code>.
     * @return The resulting MavenProject, after the test version and skip flag (for unit tests)
     *   have been appropriately configured.
     * @throws TestToolsException if any
     */
    public MavenProject packageProjectArtifact( File pomFile, String testVersion, boolean skipUnitTests )
        throws TestToolsException
    {
        return packageProjectArtifact( pomFile, testVersion, skipUnitTests, null );
    }

    /**
     * Run the plugin's Maven build up to the package phase, in order to produce a jar file for
     * distribution to a test-time local repository. The testVersion parameter specifies the version
     * to be used in the &lt;version/&gt; element of the plugin configuration, and also in fully
     * qualified, unambiguous goal invocations (as in
     * org.apache.maven.plugins:maven-eclipse-plugin:test:eclipse).
     *
     * @param pomFile The plugin's POM
     * @param testVersion The version to use for testing this plugin. To promote test resiliency,
     *   this version should remain unchanged, regardless of what plugin version is under
     *   development.
     * @param skipUnitTests In cases where test builds occur during the unit-testing phase (usually
     *   a bad testing smell), the plugin jar must be produced <b>without</b> running unit tests.
     *   Otherwise, the testing process will result in a recursive loop of building a plugin jar and
     *   trying to unit test it during the build. In these cases, set this flag to <code>true</code>.
     * @param logFile The file to which build output should be logged, in order to allow later
     *   inspection in case this build fails.
     * @return The resulting MavenProject, after the test version and skip flag (for unit tests)
     *   have been appropriately configured.
     * @throws TestToolsException if any
     */
    public MavenProject packageProjectArtifact( File pomFile, String testVersion, boolean skipUnitTests, File logFile )
        throws TestToolsException
    {
        PomInfo pomInfo = manglePomForTesting( pomFile, testVersion, skipUnitTests );

        Properties properties = new Properties();

        List<String> goals = new ArrayList<String>();
        goals.add( "package" );

        File buildLog = logFile == null ? pomInfo.getBuildLogFile() : logFile;
        System.out.println( "Now Building test version of the plugin...\nUsing staged plugin-pom: "
            + pomInfo.getPomFile().getAbsolutePath() );

        buildTool.executeMaven( pomInfo.getPomFile(), properties, goals, buildLog );

        File artifactFile = new File( pomInfo.getPomFile().getParentFile(),
                                      pomInfo.getBuildDirectory() + "/" + pomInfo.getFinalName() );
        System.out.println( "Using IT Plugin Jar: " + artifactFile.getAbsolutePath() );
        try
        {
            ProjectBuildingRequest request = new DefaultProjectBuildingRequest();
            request.setLocalRepository( artifactRepositoryFactory.createArtifactRepository( "local", new File( "target/localrepo" ).getCanonicalFile().toURL().toExternalForm(), "default", null, null ) );
            request.setRepositorySession( new MavenRepositorySystemSession() );
            MavenProject project = projectBuilder.build( pomInfo.getPomFile(), request ).getProject();

            Artifact artifact = artifactFactory.createArtifact( project.getGroupId(), project.getArtifactId(), project
                .getVersion(), null, project.getPackaging() );

            artifact.setFile( artifactFile );
            artifact.addMetadata( new ProjectArtifactMetadata( artifact, project.getFile() ) );

            project.setArtifact( artifact );

            return project;
        }
        catch ( ProjectBuildingException e )
        {
            throw new TestToolsException(
                                          "Error building MavenProject instance from test pom: " + pomInfo.getPomFile(),
                                          e );
        }
        catch ( UnknownRepositoryLayoutException e )
        {
            throw new TestToolsException(
                                         "Error building ArtifactRepository instance from test pom: " + pomInfo.getPomFile(),
                                         e );
        }
        catch ( IOException e )
        {
            throw new TestToolsException(
                                         "Error building ArtifactRepository instance from test pom: " + pomInfo.getPomFile(),
                                         e );
        }
    }

    /**
     * Inject a special version for testing, to allow tests to unambiguously reference the plugin
     * currently under test. If test builds will be executed from the unit-testing phase, also inject
     * &lt;skip&gt;true&lt;/skip&gt; into the configuration of the <code>maven-surefire-plugin</code>
     * to allow production of a test-only version of the plugin jar without running unit tests.
     *
     * @param pomFile The plugin POM
     * @param testVersion The version that allows test builds to reference the plugin under test
     * @param skipUnitTests If true, configure the surefire plugin to skip unit tests
     * @return Information about mangled POM, including the temporary file to which it was written.
     * @throws TestToolsException if any
     */
    protected PomInfo manglePomForTesting( File pomFile, String testVersion, boolean skipUnitTests )
        throws TestToolsException
    {
        File input = pomFile;

        File output = new File( pomFile.getParentFile(), "pom-" + testVersion + ".xml" );
        output.deleteOnExit();

        Reader reader = null;
        Writer writer = null;

        Model model = null;
        String finalName = null;
        String buildDirectory = null;

        try
        {
            reader = ReaderFactory.newXmlReader( input );

            model = new MavenXpp3Reader().read( reader );
        }
        catch ( IOException e )
        {
            throw new TestToolsException( "Error creating test-time version of POM for: " + input, e );
        }
        catch ( XmlPullParserException e )
        {
            throw new TestToolsException( "Error creating test-time version of POM for: " + input, e );
        }
        finally
        {
            IOUtil.close( reader );
        }

        try
        {
            model.setVersion( testVersion );

            Build build = model.getBuild();
            if ( build == null )
            {
                build = new Build();
            }
            buildDirectory = build.getDirectory();

            if ( buildDirectory == null )
            {
                buildDirectory = "target";
            }

            buildDirectory = ( buildDirectory + File.separatorChar + "it-build-target" );
            build.setDirectory( buildDirectory );
            build.setOutputDirectory( buildDirectory + File.separatorChar + "classes" );
            System.out.println( "Using " + build.getDirectory() + " and " + build.getOutputDirectory()
                + " to build IT version of plugin" );
            model.setBuild( build );

            finalName = build.getFinalName();

            if ( finalName == null )
            {
                ArtifactHandler handler = artifactHandlerManager.getArtifactHandler( model.getPackaging() );

                String ext = handler.getExtension();

                finalName = model.getArtifactId() + "-" + model.getVersion() + "." + ext;
            }

            DistributionManagement distMgmt = new DistributionManagement();

            DeploymentRepository deployRepo = new DeploymentRepository();

            deployRepo.setId( "integration-test.output" );

            File tmpDir = FileUtils.createTempFile( "integration-test-repo", "", null );
            String tmpUrl = tmpDir.toURL().toExternalForm();

            deployRepo.setUrl( tmpUrl );

            distMgmt.setRepository( deployRepo );
            distMgmt.setSnapshotRepository( deployRepo );

            Repository localAsRemote = new Repository();
            localAsRemote.setId( "testing.mainLocalAsRemote" );

            File localRepoDir = repositoryTool.findLocalRepositoryDirectory();
            localAsRemote.setUrl( localRepoDir.toURL().toExternalForm() );

            model.addRepository( localAsRemote );
            model.addPluginRepository( localAsRemote );

            Site site = new Site();

            site.setId( "integration-test.output" );
            site.setUrl( tmpUrl );

            distMgmt.setSite( site );

            model.setDistributionManagement( distMgmt );

            model.addProperty( INTEGRATION_TEST_DEPLOYMENT_REPO_URL, tmpUrl );

            if ( skipUnitTests )
            {
                List<Plugin> plugins = build.getPlugins();
                Plugin plugin = null;
                for ( Plugin plug : plugins )
                {
                    if ( "maven-surefire-plugin".equals( plug.getArtifactId() ) )
                    {
                        plugin = plug;
                        break;
                    }
                }

                if ( plugin == null )
                {
                    plugin = new Plugin();
                    plugin.setArtifactId( "maven-surefire-plugin" );
                    build.addPlugin( plugin );
                }

                Xpp3Dom configDom = (Xpp3Dom) plugin.getConfiguration();
                if ( configDom == null )
                {
                    configDom = new Xpp3Dom( "configuration" );
                    plugin.setConfiguration( configDom );
                }

                Xpp3Dom skipDom = new Xpp3Dom( "skip" );
                skipDom.setValue( "true" );

                configDom.addChild( skipDom );
            }

            writer = WriterFactory.newXmlWriter( output );

            new MavenXpp3Writer().write( writer, model );
        }
        catch ( IOException e )
        {
            throw new TestToolsException( "Error creating test-time version of POM for: " + input, e );
        }
        finally
        {
            IOUtil.close( writer );
        }

        return new PomInfo( output, model.getGroupId(), model.getArtifactId(), model.getVersion(),
                            model.getBuild().getDirectory(), model.getBuild().getOutputDirectory(), finalName );
    }

    static final class PomInfo
    {
        private final File pomFile;

        private final String groupId;

        private final String artifactId;

        private final String version;

        private final String finalName;

        private final String buildDirectory;

        private final String buildOutputDirectory;

        PomInfo( File pomFile, String groupId, String artifactId, String version, String buildDirectory,
                 String buildOutputDirectory, String finalName )
        {
            this.pomFile = pomFile;
            this.groupId = groupId;
            this.artifactId = artifactId;
            this.version = version;
            this.buildDirectory = buildDirectory;
            this.buildOutputDirectory = buildOutputDirectory;
            this.finalName = finalName;
        }

        File getPomFile()
        {
            return pomFile;
        }

        String getBuildDirectory()
        {
            return buildDirectory;
        }

        String getBuildOutputDirectory()
        {
            return buildOutputDirectory;
        }

        String getFinalName()
        {
            return finalName;
        }

        File getBuildLogFile()
        {
            return new File( buildDirectory + "/test-build-logs/" + groupId + "_" + artifactId + "_" + version
                + ".build.log" );
        }
    }
}
