/*
 * The MIT License
 *
 * Copyright (c) 2004-2009, Sun Microsystems, Inc., Alan Harder
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */
package hudson.tasks;

import static org.junit.Assert.*;
import hudson.Launcher;
import hudson.maven.MavenModuleSet;
import hudson.maven.MavenModuleSetBuild;
import hudson.model.AbstractBuild;
import hudson.model.AbstractProject;
import hudson.model.Action;
import hudson.model.BuildListener;
import hudson.model.Cause;
import hudson.model.Computer;
import hudson.model.FreeStyleBuild;
import hudson.model.FreeStyleProject;
import hudson.model.DependencyGraph;
import hudson.model.DependencyGraph.Dependency;
import hudson.model.Item;
import hudson.model.Result;
import hudson.model.Run;
import hudson.model.TaskListener;
import hudson.model.User;
import hudson.security.ACL;
import hudson.security.AuthorizationMatrixProperty;
import hudson.security.LegacySecurityRealm;
import hudson.security.Permission;
import hudson.security.ProjectMatrixAuthorizationStrategy;
import hudson.util.FormValidation;

import java.io.File;
import java.io.IOException;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import java.util.List;

import javax.annotation.CheckForNull;

import jenkins.model.Jenkins;
import jenkins.security.QueueItemAuthenticatorConfiguration;
import jenkins.triggers.ReverseBuildTriggerTest;

import org.acegisecurity.Authentication;
import org.acegisecurity.context.SecurityContext;
import org.acegisecurity.context.SecurityContextHolder;
import org.dom4j.DocumentException;
import org.dom4j.io.SAXReader;
import org.junit.Assume;
import org.junit.ClassRule;
import org.junit.Rule;
import org.junit.Test;
import org.jvnet.hudson.test.BuildWatcher;
import org.jvnet.hudson.test.ExtractResourceSCM;
import org.jvnet.hudson.test.Issue;
import org.jvnet.hudson.test.JenkinsRule;
import org.jvnet.hudson.test.JenkinsRule.WebClient;
import org.jvnet.hudson.test.TestBuilder;
import org.jvnet.hudson.test.MockBuilder;
import org.jvnet.hudson.test.MockQueueItemAuthenticator;
import org.jvnet.hudson.test.ToolInstallations;
import org.xml.sax.SAXException;

public class BuildTriggerTest {

    @Rule
    public JenkinsRule j = new JenkinsRule();

    @ClassRule
    public static BuildWatcher buildWatcher = new BuildWatcher();

    private FreeStyleProject createDownstreamProject() throws Exception {
        FreeStyleProject dp = j.createFreeStyleProject("downstream");
        dp.setQuietPeriod(0);
        return dp;
    }

    private void doTriggerTest(boolean evenWhenUnstable, Result triggerResult,
            Result dontTriggerResult) throws Exception {
        FreeStyleProject p = j.createFreeStyleProject();
        FreeStyleProject dp = createDownstreamProject();
        p.getPublishersList().add(new BuildTrigger("downstream", evenWhenUnstable));
        p.getBuildersList().add(new MockBuilder(dontTriggerResult));
        j.jenkins.rebuildDependencyGraph();

        // First build should not trigger downstream job
        FreeStyleBuild b = p.scheduleBuild2(0).get();
        assertNoDownstreamBuild(dp, b);

        // Next build should trigger downstream job
        p.getBuildersList().replace(new MockBuilder(triggerResult));
        b = p.scheduleBuild2(0).get();
        assertDownstreamBuild(dp, b);
    }

    private void assertNoDownstreamBuild(FreeStyleProject dp, Run<?,?> b) throws Exception {
        for (int i = 0; i < 3; i++) {
            Thread.sleep(200);
            assertTrue("downstream build should not run!  upstream log: " + b.getLog(),
                       !dp.isInQueue() && !dp.isBuilding() && dp.getLastBuild()==null);
        }
    }

    private FreeStyleBuild assertDownstreamBuild(FreeStyleProject dp, Run<?,?> b) throws Exception {
        // Wait for downstream build
        for (int i = 0; dp.getLastBuild()==null && i < 20; i++) Thread.sleep(100);
        assertNotNull("downstream build didn't run.. upstream log: " + b.getLog(), dp.getLastBuild());
        return dp.getLastBuild();
    }

    @Test
    public void buildTrigger() throws Exception {
        doTriggerTest(false, Result.SUCCESS, Result.UNSTABLE);
    }

    @Test
    public void triggerEvenWhenUnstable() throws Exception {
        doTriggerTest(true, Result.UNSTABLE, Result.FAILURE);
    }

    private void doMavenTriggerTest(boolean evenWhenUnstable) throws Exception {
        File problematic = new File(System.getProperty("user.home"), ".m2/repository/org/apache/maven/plugins/maven-surefire-plugin/2.4.3/maven-surefire-plugin-2.4.3.pom");
        if (problematic.isFile()) {
            try {
                new SAXReader().read(problematic);
            } catch (DocumentException x) {
                x.printStackTrace();
                // somehow maven-surefire-plugin-2.4.3.pom got corrupted on CI builders
                Assume.assumeNoException(x);
            }
        }
        FreeStyleProject dp = createDownstreamProject();
        ToolInstallations.configureDefaultMaven();
        MavenModuleSet m = j.jenkins.createProject(MavenModuleSet.class, "p");
        m.getPublishersList().add(new BuildTrigger("downstream", evenWhenUnstable));
        if (!evenWhenUnstable) {
            // Configure for UNSTABLE
            m.setGoals("clean test");
            m.setScm(new ExtractResourceSCM(getClass().getResource("maven-test-failure.zip")));
        } // otherwise do nothing which gets FAILURE
        // First build should not trigger downstream project
        MavenModuleSetBuild b = m.scheduleBuild2(0).get();
        assertNoDownstreamBuild(dp, b);

        if (evenWhenUnstable) {
            // Configure for UNSTABLE
            m.setGoals("clean test");
            m.setScm(new ExtractResourceSCM(getClass().getResource("maven-test-failure.zip")));
        } else {
            // Configure for SUCCESS
            m.setGoals("clean");
            m.setScm(new ExtractResourceSCM(getClass().getResource("maven-empty.zip")));
        }
        // Next build should trigger downstream project
        b = m.scheduleBuild2(0).get();
        assertDownstreamBuild(dp, b);
    }

    @Test
    public void mavenBuildTrigger() throws Exception {
        doMavenTriggerTest(false);
    }

    @Test
    public void mavenTriggerEvenWhenUnstable() throws Exception {
        doMavenTriggerTest(true);
    }

    /** @see ReverseBuildTriggerTest#upstreamProjectSecurity */
    @Test
    public void downstreamProjectSecurity() throws Exception {
        j.jenkins.setSecurityRealm(new LegacySecurityRealm());
        ProjectMatrixAuthorizationStrategy auth = new ProjectMatrixAuthorizationStrategy();
        auth.add(Jenkins.READ, "alice");
        auth.add(Computer.BUILD, "alice");
        auth.add(Computer.BUILD, "anonymous");
        j.jenkins.setAuthorizationStrategy(auth);
        final FreeStyleProject upstream =j. createFreeStyleProject("upstream");
        Authentication alice = User.get("alice").impersonate();
        QueueItemAuthenticatorConfiguration.get().getAuthenticators().add(new MockQueueItemAuthenticator(Collections.singletonMap("upstream", alice)));
        Map<Permission,Set<String>> perms = new HashMap<Permission,Set<String>>();
        perms.put(Item.READ, Collections.singleton("alice"));
        perms.put(Item.CONFIGURE, Collections.singleton("alice"));
        upstream.addProperty(new AuthorizationMatrixProperty(perms));
        String downstreamName = "d0wnstr3am"; // do not clash with English messages!
        FreeStyleProject downstream = j.createFreeStyleProject(downstreamName);
        upstream.getPublishersList().add(new BuildTrigger(downstreamName, Result.SUCCESS));
        j.jenkins.rebuildDependencyGraph();
        /* The long way:
        WebClient wc = createWebClient();
        wc.login("alice");
        HtmlPage page = wc.getHistoryPageFilter(upstream, "configure");
        HtmlForm config = page.getFormByName("config");
        config.getButtonByCaption("Add post-build action").click(); // lib/hudson/project/config-publishers2.jelly
        page.getAnchorByText("Build other projects").click();
        HtmlTextInput childProjects = config.getInputByName("buildTrigger.childProjects");
        childProjects.setValueAttribute(downstreamName);
        submit(config);
        */
        assertEquals(Collections.singletonList(downstream), upstream.getDownstreamProjects());
        // Downstream projects whose existence we are not aware of will silently not be triggered:
        assertDoCheck(alice, Messages.BuildTrigger_NoSuchProject(downstreamName, "upstream"), upstream, downstreamName);
        assertDoCheck(alice, null, null, downstreamName);
        FreeStyleBuild b = j.buildAndAssertSuccess(upstream);
        j.assertLogNotContains(downstreamName, b);
        j.waitUntilNoActivity();
        assertNull(downstream.getLastBuild());
        // If we can see them, but not build them, that is a warning (but this is in cleanUp so the build is still considered a success):
        Map<Permission,Set<String>> grantedPermissions = new HashMap<Permission,Set<String>>();
        grantedPermissions.put(Item.READ, Collections.singleton("alice"));
        AuthorizationMatrixProperty amp = new AuthorizationMatrixProperty(grantedPermissions);
        downstream.addProperty(amp);
        assertDoCheck(alice, Messages.BuildTrigger_you_have_no_permission_to_build_(downstreamName), upstream, downstreamName);
        assertDoCheck(alice, null, null, downstreamName);
        b = j.buildAndAssertSuccess(upstream);
        j.assertLogContains(downstreamName, b);
        j.waitUntilNoActivity();
        assertNull(downstream.getLastBuild());
        // If we can build them, then great:
        grantedPermissions.put(Item.BUILD, Collections.singleton("alice"));
        downstream.removeProperty(amp);
        amp = new AuthorizationMatrixProperty(grantedPermissions);
        downstream.addProperty(amp);
        assertDoCheck(alice, null, upstream, downstreamName);
        assertDoCheck(alice, null, null, downstreamName);
        b = j.buildAndAssertSuccess(upstream);
        j.assertLogContains(downstreamName, b);
        j.waitUntilNoActivity();
        FreeStyleBuild b2 = downstream.getLastBuild();
        assertNotNull(b2);
        Cause.UpstreamCause cause = b2.getCause(Cause.UpstreamCause.class);
        assertNotNull(cause);
        assertEquals(b, cause.getUpstreamRun());
        // Now if we have configured some QIA’s but they are not active on this job, we should run as anonymous. Which would normally have no permissions:
        QueueItemAuthenticatorConfiguration.get().getAuthenticators().replace(new MockQueueItemAuthenticator(Collections.<String, Authentication>emptyMap()));
        assertDoCheck(alice, Messages.BuildTrigger_you_have_no_permission_to_build_(downstreamName), upstream, downstreamName);
        assertDoCheck(alice, null, null, downstreamName);
        b = j.buildAndAssertSuccess(upstream);
        j.assertLogNotContains(downstreamName, b);
        j.assertLogContains(Messages.BuildTrigger_warning_this_build_has_no_associated_aut(), b);
        j.waitUntilNoActivity();
        assertEquals(1, downstream.getLastBuild().number);
        // Unless we explicitly granted them:
        grantedPermissions.put(Item.READ, Collections.singleton("anonymous"));
        grantedPermissions.put(Item.BUILD, Collections.singleton("anonymous"));
        downstream.removeProperty(amp);
        amp = new AuthorizationMatrixProperty(grantedPermissions);
        downstream.addProperty(amp);
        assertDoCheck(alice, null, upstream, downstreamName);
        assertDoCheck(alice, null, null, downstreamName);
        b = j.buildAndAssertSuccess(upstream);
        j.assertLogContains(downstreamName, b);
        j.waitUntilNoActivity();
        assertEquals(2, downstream.getLastBuild().number);
        FreeStyleProject simple = j.createFreeStyleProject("simple");
        FreeStyleBuild b3 = j.buildAndAssertSuccess(simple);
        // See discussion in BuildTrigger for why this is necessary:
        j.assertLogContains(Messages.BuildTrigger_warning_this_build_has_no_associated_aut(), b3);
        // Finally, in legacy mode we run as SYSTEM:
        grantedPermissions.clear(); // similar behavior but different message if DescriptorImpl removed
        downstream.removeProperty(amp);
        amp = new AuthorizationMatrixProperty(grantedPermissions);
        downstream.addProperty(amp);
        QueueItemAuthenticatorConfiguration.get().getAuthenticators().clear();
        assertDoCheck(alice, Messages.BuildTrigger_NoSuchProject(downstreamName, "upstream"), upstream, downstreamName);
        assertDoCheck(alice, null, null, downstreamName);
        b = j.buildAndAssertSuccess(upstream);
        j.assertLogContains(downstreamName, b);
        j.assertLogContains(Messages.BuildTrigger_warning_access_control_for_builds_in_glo(), b);
        j.waitUntilNoActivity();
        assertEquals(3, downstream.getLastBuild().number);
        b3 = j.buildAndAssertSuccess(simple);
        j.assertLogNotContains(Messages.BuildTrigger_warning_access_control_for_builds_in_glo(), b3);
    }
    private void assertDoCheck(Authentication auth, @CheckForNull String expectedError, AbstractProject<?, ?> project, String value) {
        FormValidation result;
        SecurityContext orig = ACL.impersonate(auth);
        try {
            result = j.jenkins.getDescriptorByType(BuildTrigger.DescriptorImpl.class).doCheck(project, value);
        } finally {
            SecurityContextHolder.setContext(orig);
        }
        if (expectedError == null) {
            assertEquals(result.renderHtml(), FormValidation.Kind.OK, result.kind);
        } else {
            assertEquals(result.renderHtml(), FormValidation.Kind.ERROR, result.kind);
            assertEquals(result.renderHtml(), expectedError);
        }
    }

    @Test @Issue("JENKINS-20989")
    public void downstreamProjectShouldObserveCompletedParent() throws Exception {
        j.jenkins.setNumExecutors(2);

        final FreeStyleProject us = j.createFreeStyleProject();
        us.getPublishersList().add(new BuildTrigger("downstream", true));

        FreeStyleProject ds = createDownstreamProject();
        ds.getBuildersList().add(new AssertTriggerBuildCompleted(us, j.createWebClient()));

        j.jenkins.rebuildDependencyGraph();

        j.buildAndAssertSuccess(us);

        j.waitUntilNoActivity();
        final FreeStyleBuild dsb = ds.getBuildByNumber(1);
        assertNotNull(dsb);
        j.waitForCompletion(dsb);
        j.assertBuildStatusSuccess(dsb);
    }

    @Test @Issue("JENKINS-20989")
    public void allDownstreamProjectsShouldObserveCompletedParent() throws Exception {
        j.jenkins.setNumExecutors(3);

        final FreeStyleProject us = j.createFreeStyleProject();
        us.getPublishersList().add(new SlowTrigger("downstream,downstream2"));

        FreeStyleProject ds = createDownstreamProject();
        ds.getBuildersList().add(new AssertTriggerBuildCompleted(us, j.createWebClient()));
        FreeStyleProject ds2 = j.createFreeStyleProject("downstream2");
        ds2.setQuietPeriod(0);
        ds2.getBuildersList().add(new AssertTriggerBuildCompleted(us, j.createWebClient()));

        j.jenkins.rebuildDependencyGraph();

        FreeStyleBuild upstream = j.buildAndAssertSuccess(us);

        FreeStyleBuild dsb = assertDownstreamBuild(ds, upstream);
        j.waitForCompletion(dsb);
        j.assertBuildStatusSuccess(dsb);

        dsb = assertDownstreamBuild(ds2, upstream);
        j.waitForCompletion(dsb);
        j.assertBuildStatusSuccess(dsb);
    }

    // Trigger that goes through dependencies very slowly
    private static final class SlowTrigger extends BuildTrigger {

        private static final class Dep extends Dependency {
            private static boolean block = false;
            private Dep(AbstractProject upstream, AbstractProject downstream) {
                super(upstream, downstream);
            }

            @Override
            public boolean shouldTriggerBuild(AbstractBuild build, TaskListener listener, List<Action> actions) {
                if (block) {
                    try {
                        Thread.sleep(5000);
                    } catch (InterruptedException ex) {
                        throw new AssertionError(ex);
                    }
                }
                block = true;
                final boolean should = super.shouldTriggerBuild(build, listener, actions);
                return should;
            }
        }

        public SlowTrigger(String childProjects) {
            super(childProjects, true);
        }

        @Override @SuppressWarnings("rawtypes")
        public void buildDependencyGraph(AbstractProject owner, DependencyGraph graph) {
            for (AbstractProject ch: getChildProjects(owner)) {
                graph.addDependency(new Dep(owner, ch));
            }
        }
    }

    // Fail downstream build if upstream is not completed yet
    private static final class AssertTriggerBuildCompleted extends TestBuilder {
        private final FreeStyleProject us;
        private final WebClient wc;

        private AssertTriggerBuildCompleted(FreeStyleProject us, WebClient wc) {
            this.us = us;
            this.wc = wc;
        }

        @Override
        public boolean perform(AbstractBuild<?, ?> build, Launcher launcher, BuildListener listener) throws InterruptedException, IOException {
            FreeStyleBuild success = us.getLastSuccessfulBuild();
            FreeStyleBuild last = us.getLastBuild();
            try {
                assertFalse("Upstream build is not completed after downstream started", last.isBuilding());
                assertNotNull("Upstream build permalink not correctly updated", success);
                assertEquals(1, success.getNumber());
            } catch (AssertionError ex) {
                System.err.println("Upstream build log: " + last.getLog());
                throw ex;
            }

            try {
                wc.getPage(us, "lastSuccessfulBuild");
            } catch (SAXException ex) {
                throw new AssertionError(ex);
            }
            return true;
        }
    }
}
