/*
 * The MIT License
 *
 * Copyright 2013 Red Hat, Inc.
 *
 * 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.model;

import com.gargoylesoftware.htmlunit.HttpMethod;
import com.gargoylesoftware.htmlunit.WebRequest;
import hudson.*;
import hudson.model.queue.QueueTaskFuture;
import hudson.security.AccessDeniedException2;
import hudson.tasks.*;
import hudson.security.HudsonPrivateSecurityRealm;
import hudson.security.GlobalMatrixAuthorizationStrategy;

import java.io.Closeable;
import java.net.HttpURLConnection;
import java.net.URL;
import java.util.Collections;

import hudson.util.Scrambler;
import org.jvnet.hudson.reactor.ReactorException;
import org.jvnet.hudson.test.FakeChangeLogSCM;
import hudson.scm.SCMRevisionState;
import hudson.scm.PollingResult;
import hudson.Launcher.RemoteLauncher;
import hudson.scm.NullSCM;
import hudson.scm.SCM;
import hudson.model.queue.SubTaskContributor;
import hudson.model.Queue.Executable;
import hudson.model.Queue.Task;
import hudson.model.queue.SubTask;
import hudson.model.AbstractProject.BecauseOfUpstreamBuildInProgress;
import hudson.model.AbstractProject.BecauseOfDownstreamBuildInProgress;
import jenkins.model.WorkspaceWriter;
import jenkins.model.Jenkins;
import antlr.ANTLRException;
import hudson.triggers.SCMTrigger;
import hudson.model.Cause.LegacyCodeCause;
import com.gargoylesoftware.htmlunit.html.HtmlElement;
import com.gargoylesoftware.htmlunit.html.HtmlForm;
import com.gargoylesoftware.htmlunit.html.HtmlPage;
import java.io.Serializable;
import jenkins.scm.DefaultSCMCheckoutStrategyImpl;
import jenkins.scm.SCMCheckoutStrategy;
import java.io.File;

import hudson.slaves.EnvironmentVariablesNodeProperty;
import hudson.model.labels.LabelAtom;
import hudson.scm.SCMDescriptor;
import hudson.security.ACL;
import hudson.security.ACLContext;
import hudson.slaves.Cloud;
import hudson.slaves.DumbSlave;
import hudson.slaves.NodeProvisioner;
import org.jvnet.hudson.test.TestExtension;
import java.util.List;
import java.util.ArrayList;
import java.io.IOException;
import java.nio.charset.Charset;
import java.util.Collection;
import org.junit.Rule;
import org.junit.Test;
import org.jvnet.hudson.test.JenkinsRule;
import static org.junit.Assert.*;

import java.util.Map;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.logging.Logger;
import jenkins.model.BlockedBecauseOfBuildInProgress;

import org.junit.Ignore;
import org.jvnet.hudson.test.Issue;
import org.jvnet.hudson.test.TestBuilder;

/**
 *
 * @author Lucie Votypkova
 */
public class ProjectTest {
    
    @Rule public JenkinsRule j = new JenkinsRule();
    public static boolean createAction = false;
    public static boolean getFilePath = false;
    public static boolean createSubTask = false;
    
    @Test
    public void testSave() throws IOException, InterruptedException, ReactorException {
        FreeStyleProject p = j.createFreeStyleProject("project");
        p.disabled = true;
        p.nextBuildNumber = 5;
        p.description = "description";
        p.save();
        j.jenkins.reload();
        assertEquals("All persistent data should be saved.", "description", p.description);
        assertEquals("All persistent data should be saved.", 5, p.nextBuildNumber);
        assertEquals("All persistent data should be saved", true, p.disabled);
    }
    
    @Test
    public void testOnCreateFromScratch() throws IOException, Exception{
        FreeStyleProject p = j.createFreeStyleProject("project");
        j.buildAndAssertSuccess(p);
        p.removeRun(p.getLastBuild());
        createAction = true;
        p.onCreatedFromScratch();
        assertNotNull("Project should have last build.", p.getLastBuild());
        assertNotNull("Project should have transient action TransientAction.", p.getAction(TransientAction.class));
        createAction = false;
    }
    
    @Test
    public void testOnLoad() throws IOException, Exception{
        FreeStyleProject p = j.createFreeStyleProject("project");
        j.buildAndAssertSuccess(p);
        p.removeRun(p.getLastBuild());
        createAction = true;
        p.onLoad(j.jenkins, "project");
        assertTrue("Project should have a build.", p.getLastBuild()!=null);
        assertTrue("Project should have a scm.", p.getScm()!=null);
        assertTrue("Project should have Transient Action TransientAction.", p.getAction(TransientAction.class)!=null);
        createAction = false;
    }
    
    @Test
    public void testGetEnvironment() throws Exception{
        FreeStyleProject p = j.createFreeStyleProject("project");
        Slave slave = j.createOnlineSlave();
        EnvironmentVariablesNodeProperty.Entry entry = new EnvironmentVariablesNodeProperty.Entry("jdk","some_java");
        slave.getNodeProperties().add(new EnvironmentVariablesNodeProperty(entry));
        EnvVars var = p.getEnvironment(slave, TaskListener.NULL);
        assertEquals("Environment should have set jdk.", "some_java", var.get("jdk"));
    }
    
    @Test
    public void testPerformDelete() throws IOException, Exception{
        FreeStyleProject p = j.createFreeStyleProject("project");
        p.performDelete();
        assertFalse("Project should be deleted from disk.", p.getConfigFile().exists());
        assertTrue("Project should be disabled when deleting start.", p.isDisabled());
    }
    
    @Test
    public void testGetAssignedLabel() throws Exception{
        FreeStyleProject p = j.createFreeStyleProject("project");
        p.setAssignedLabel(j.jenkins.getSelfLabel());
        Slave slave = j.createOnlineSlave();
        assertEquals("Project should have Jenkins's self label.", j.jenkins.getSelfLabel(), p.getAssignedLabel());
        p.setAssignedLabel(null);
        assertNull("Project should not have any label.", p.getAssignedLabel());
        p.setAssignedLabel(slave.getSelfLabel());
        assertEquals("Project should have self label of slave", slave.getSelfLabel(), p.getAssignedLabel());
    }
    
    @Test
    public void testGetAssignedLabelString() throws Exception{
        FreeStyleProject p = j.createFreeStyleProject("project");
        Slave slave = j.createOnlineSlave();
        assertNull("Project should not have any label.", p.getAssignedLabelString());
        p.setAssignedLabel(j.jenkins.getSelfLabel());
        assertNull("Project should return null, because assigned label is Jenkins.", p.getAssignedLabelString());
        p.setAssignedLabel(slave.getSelfLabel());
        assertEquals("Project should return name of slave.", slave.getSelfLabel().name, p.getAssignedLabelString());
    }

    
    @Test
    public void testGetSomeWorkspace() throws Exception{
        FreeStyleProject p = j.createFreeStyleProject("project");
        assertNull("Project which has never run should not have any workspace.", p.getSomeWorkspace());
        getFilePath = true;
        assertNotNull("Project should have any workspace because WorkspaceBrowser find some.", p.getSomeWorkspace());
        getFilePath = false;
        String cmd = "echo ahoj > some.log";
        p.getBuildersList().add(Functions.isWindows() ? new BatchFile(cmd) : new Shell(cmd));
        j.buildAndAssertSuccess(p);
        assertNotNull("Project should has any workspace.", p.getSomeWorkspace());
    }
    
    @Test
    public void testGetSomeBuildWithWorkspace() throws Exception{
        FreeStyleProject p = j.createFreeStyleProject("project");
        String cmd = "echo ahoj > some.log";
        p.getBuildersList().add(Functions.isWindows() ? new BatchFile(cmd) : new Shell(cmd));
        assertNull("Project which has never run should not have any build with workspace.", p.getSomeBuildWithWorkspace());
        j.buildAndAssertSuccess(p);
        assertEquals("Last build should have workspace.", p.getLastBuild(), p.getSomeBuildWithWorkspace());
        p.getLastBuild().delete();
        assertNull("Project should not have build with some workspace.", p.getSomeBuildWithWorkspace());
    }

    @Issue("JENKINS-10450")
    @Test public void workspaceBrowsing() throws Exception {
        FreeStyleProject p = j.createFreeStyleProject("project");
        String cmd = "echo ahoj > some.log";
        p.getBuildersList().add(Functions.isWindows() ? new BatchFile(cmd) : new Shell(cmd));
        j.buildAndAssertSuccess(p);
        JenkinsRule.WebClient wc = j.createWebClient();
        wc.goTo("job/project/ws/some.log", "text/plain");
        wc.assertFails("job/project/ws/other.log", 404);
        p.doDoWipeOutWorkspace();
        wc.assertFails("job/project/ws/some.log", 404);
    }
    
    @Test
    public void testGetQuietPeriod() throws IOException{
        FreeStyleProject p = j.createFreeStyleProject("project");
        assertEquals("Quiet period should be default.", j.jenkins.getQuietPeriod(), p.getQuietPeriod());
        j.jenkins.setQuietPeriod(0);
        assertEquals("Quiet period is not set so it should be the same as global quiet period.", 0, p.getQuietPeriod());
        p.setQuietPeriod(10);
        assertEquals("Quiet period was set.",p.getQuietPeriod(),10);
    }
    
    @Test
    public void testGetScmCheckoutStrategy() throws IOException{
        FreeStyleProject p = j.createFreeStyleProject("project");
        p.setScmCheckoutStrategy(null);
        assertTrue("Project should return default checkout strategy if scm checkout strategy is not set.", p.getScmCheckoutStrategy() instanceof DefaultSCMCheckoutStrategyImpl);
        SCMCheckoutStrategy strategy = new SCMCheckoutStrategyImpl();
        p.setScmCheckoutStrategy(strategy);
        assertEquals("Project should return its scm checkout strategy if this strategy is not null", strategy, p.getScmCheckoutStrategy());
    }
    
    @Test
    public void testGetScmCheckoutRetryCount() throws Exception{
        FreeStyleProject p = j.createFreeStyleProject("project");
        assertEquals("Scm retry count should be default.", j.jenkins.getScmCheckoutRetryCount(), p.getScmCheckoutRetryCount());
        j.jenkins.setScmCheckoutRetryCount(6);
        assertEquals("Scm retry count should be the same as global scm retry count.", 6, p.getScmCheckoutRetryCount());
        HtmlForm form = j.createWebClient().goTo(p.getUrl() + "/configure").getFormByName("config");
        ((HtmlElement)form.getByXPath("//div[@class='advancedLink']//button").get(0)).click();
        form.getInputByName("hasCustomScmCheckoutRetryCount").click();
        form.getInputByName("scmCheckoutRetryCount").setValueAttribute("7");
        j.submit(form);
        assertEquals("Scm retry count was set.", 7, p.getScmCheckoutRetryCount());
    }
    
    @Test
    public void isBuildable() throws IOException{
        FreeStyleProject p = j.createFreeStyleProject("project");
        assertTrue("Project should be buildable.", p.isBuildable());
        p.disable();
        assertFalse("Project should not be buildable if it is disabled.", p.isBuildable());
        p.enable();
        AbstractProject p2 = (AbstractProject) j.jenkins.copy(j.jenkins.getItem("project"), "project2");
        assertFalse("Project should not be buildable until is saved.", p2.isBuildable());
        p2.save();
        assertTrue("Project should be buildable after save.", p2.isBuildable());
    }
    
    @Test
    public void testMakeDisabled() throws IOException{
        FreeStyleProject p = j.createFreeStyleProject("project");
        p.makeDisabled(false);
        assertFalse("Project should be enabled.", p.isDisabled());
        p.makeDisabled(true);
        assertTrue("Project should be disabled.", p.isDisabled());
        p.makeDisabled(false);
        p.setAssignedLabel(j.jenkins.getLabel("nonExist"));
        p.scheduleBuild2(0);
        p.makeDisabled(true);
        assertNull("Project should be canceled.", Queue.getInstance().getItem(p));
    }
    
    @Test
    public void testAddProperty() throws IOException{
        FreeStyleProject p = j.createFreeStyleProject("project");
        JobProperty prop = new JobPropertyImp();
        createAction = true;
        p.addProperty(prop);
        assertNotNull("Project does not contain added property.", p.getProperty(prop.getClass()));
        assertNotNull("Project did not update transient actions.", p.getAction(TransientAction.class));
    }
    
    @Test
    public void testScheduleBuild2() throws IOException, InterruptedException{
        FreeStyleProject p = j.createFreeStyleProject("project");
        p.setAssignedLabel(j.jenkins.getLabel("nonExist"));
        p.scheduleBuild(0, new LegacyCodeCause(), new Action[0]);
        assertNotNull("Project should be in queue.", Queue.getInstance().getItem(p));
        p.setAssignedLabel(null);
        int count = 0;
        while(count<5 && p.getLastBuild()==null){
            Thread.sleep(1000); //give some time to start build
            count++;
        }
        assertNotNull("Build should be done or in progress.", p.getLastBuild());
    }

    
    @Test
    public void testSchedulePolling() throws IOException, ANTLRException{
        FreeStyleProject p = j.createFreeStyleProject("project");
        assertFalse("Project should not schedule polling because no scm trigger is set.",p.schedulePolling());
        SCMTrigger trigger = new SCMTrigger("0 0 * * *");
        p.addTrigger(trigger);
        trigger.start(p, true);
        assertTrue("Project should schedule polling.", p.schedulePolling());
        p.disable();
        assertFalse("Project should not schedule polling because project is disabled.", p.schedulePolling());
    }
    
    @Test
    public void testSaveAfterSet() throws Exception, ReactorException {
        FreeStyleProject p = j.createFreeStyleProject("project");
        p.setScm(new NullSCM());
        p.setScmCheckoutStrategy(new SCMCheckoutStrategyImpl());
        p.setQuietPeriod(15);
        p.setBlockBuildWhenDownstreamBuilding(true);
        p.setBlockBuildWhenUpstreamBuilding(true);
        j.jenkins.getJDKs().add(new JDK("jdk", "path"));
        j.jenkins.save();
        p.setJDK(j.jenkins.getJDK("jdk"));
        p.setCustomWorkspace("/some/path");
        j.jenkins.reload();
        assertNotNull("Project did not save scm.", p.getScm());
        assertTrue("Project did not save scm checkout strategy.", p.getScmCheckoutStrategy() instanceof SCMCheckoutStrategyImpl);
        assertEquals("Project did not save quiet period.", 15, p.getQuietPeriod());
        assertTrue("Project did not save block if downstream is building.", p.blockBuildWhenDownstreamBuilding());
        assertTrue("Project did not save block if upstream is building.", p.blockBuildWhenUpstreamBuilding());
        assertNotNull("Project did not save jdk", p.getJDK());
        assertEquals("Project did not save custom workspace.", "/some/path", p.getCustomWorkspace());
    }
    
    @Test
    public void testGetActions() throws IOException{
        FreeStyleProject p = j.createFreeStyleProject("project");
        createAction = true;
        p.updateTransientActions();
        assertNotNull("Action should contain transient actions too.", p.getAction(TransientAction.class));
        createAction = false;
    }

// for debugging
//    static {
//        Logger.getLogger("").getHandlers()[0].setFormatter(new MilliSecLogFormatter());
//    }

    @Test
    public void testGetCauseOfBlockage() throws Exception {
        FreeStyleProject p = j.createFreeStyleProject("project");
        p.getBuildersList().add(Functions.isWindows() ? new BatchFile("ping -n 10 127.0.0.1 >nul") : new Shell("sleep 10"));
        QueueTaskFuture<FreeStyleBuild> b1 = waitForStart(p);
        assertInstanceOf("Build can not start because previous build has not finished: " + p.getCauseOfBlockage(), p.getCauseOfBlockage(), BlockedBecauseOfBuildInProgress.class);
        p.getLastBuild().getExecutor().interrupt();
        b1.get();   // wait for it to finish

        FreeStyleProject downstream = j.createFreeStyleProject("project-downstream");
        downstream.getBuildersList().add(Functions.isWindows() ? new BatchFile("ping -n 10 127.0.0.1 >nul") : new Shell("sleep 10"));
        p.getPublishersList().add(new BuildTrigger(Collections.singleton(downstream), Result.SUCCESS));
        Jenkins.getInstance().rebuildDependencyGraph();
        p.setBlockBuildWhenDownstreamBuilding(true);
        QueueTaskFuture<FreeStyleBuild> b2 = waitForStart(downstream);
        assertInstanceOf("Build can not start because build of downstream project has not finished.", p.getCauseOfBlockage(), BecauseOfDownstreamBuildInProgress.class);
        downstream.getLastBuild().getExecutor().interrupt();
        b2.get();

        downstream.setBlockBuildWhenUpstreamBuilding(true);
        waitForStart(p);
        assertInstanceOf("Build can not start because build of upstream project has not finished.", downstream.getCauseOfBlockage(), BecauseOfUpstreamBuildInProgress.class);
    }

    private static final Logger LOGGER = Logger.getLogger(ProjectTest.class.getName());

    private QueueTaskFuture<FreeStyleBuild> waitForStart(FreeStyleProject p) throws InterruptedException, ExecutionException {
        long start = System.nanoTime();
        LOGGER.info("Scheduling "+p);
        QueueTaskFuture<FreeStyleBuild> f = p.scheduleBuild2(0);
        f.waitForStart();
        LOGGER.info("Wait:"+ TimeUnit.NANOSECONDS.toMillis(System.nanoTime()-start));
        return f;
    }

    private void assertInstanceOf(String msg, Object o, Class t) {
        if (t.isInstance(o))
            return;
        fail(msg + ": " + o);
    }
    
    @Test
    public void testGetSubTasks() throws IOException{
        FreeStyleProject p = j.createFreeStyleProject("project");
        p.addProperty(new JobPropertyImp());
        createSubTask = true;
        List<SubTask> subtasks = p.getSubTasks();
        boolean containsSubTaskImpl = false;
        boolean containsSubTaskImpl2 = false;
        for(SubTask sub: subtasks){
            if(sub instanceof SubTaskImpl)
                containsSubTaskImpl = true;
            if(sub instanceof SubTaskImpl2)
                containsSubTaskImpl2 = true;
        }
        createSubTask = false;
        assertTrue("Project should return subtasks provided by SubTaskContributor.", containsSubTaskImpl2);
        assertTrue("Project should return subtasks provided by JobProperty.", containsSubTaskImpl);
        
    }
    
    @Test
    public void testCreateExecutable() throws IOException{
        FreeStyleProject p = j.createFreeStyleProject("project");
        Build build = p.createExecutable();
        assertNotNull("Project should create executable.", build);
        assertEquals("CreatedExecutable should be the last build.", build, p.getLastBuild());
        assertEquals("Next build number should be increased.", 2, p.nextBuildNumber);
        p.disable();
        build = p.createExecutable();
        assertNull("Disabled project should not create executable.", build);
        assertEquals("Next build number should not be increased.", 2, p.nextBuildNumber);
        
    }
    
    @Test
    public void testCheckout() throws IOException, Exception{
        SCM scm = new NullSCM();
        FreeStyleProject p = j.createFreeStyleProject("project");
        Slave slave = j.createOnlineSlave();
        AbstractBuild build = p.createExecutable();
        FilePath ws = slave.getWorkspaceFor(p);
        assertNotNull(ws);
        FilePath path = slave.toComputer().getWorkspaceList().allocate(ws, build).path;
        build.setWorkspace(path);
        BuildListener listener = new StreamBuildListener(BuildListener.NULL.getLogger(), Charset.defaultCharset());
        assertTrue("Project with null smc should perform checkout without problems.", p.checkout(build, new RemoteLauncher(listener, slave.getChannel(), true), listener, new File(build.getRootDir(),"changelog.xml")));
        p.setScm(scm);
        assertTrue("Project should perform checkout without problems.",p.checkout(build, new RemoteLauncher(listener, slave.getChannel(), true), listener, new File(build.getRootDir(),"changelog.xml")));
    }

    @Ignore("randomly failed: Project should have polling result no change expected:<NONE> but was:<INCOMPARABLE>")
    @Test
    public void testPoll() throws Exception{
        FreeStyleProject p = j.createFreeStyleProject("project");
        SCM scm = new NullSCM();
        p.setScm(null);
        SCM alwaysChange = new AlwaysChangedSCM();
        assertEquals("Project with null scm should have have polling result no change.", PollingResult.Change.NONE, p.poll(TaskListener.NULL).change);
        p.setScm(scm);
        p.disable();
        assertEquals("Project which is disabled should have have polling result no change.", PollingResult.Change.NONE, p.poll(TaskListener.NULL).change);
        p.enable();
        assertEquals("Project which has no builds should have have polling result incomparable.", PollingResult.Change.INCOMPARABLE, p.poll(TaskListener.NULL).change);
        p.setAssignedLabel(j.jenkins.getLabel("nonExist"));
        p.scheduleBuild2(0);
        assertEquals("Project which build is building should have polling result result no change.", PollingResult.Change.NONE, p.poll(TaskListener.NULL).change);
        p.setAssignedLabel(null);
        while(p.getLastBuild()==null)
            Thread.sleep(100); //wait until build start
        assertEquals("Project should have polling result no change", PollingResult.Change.NONE, p.poll(TaskListener.NULL).change);
        p.setScm(alwaysChange);
        j.buildAndAssertSuccess(p);
        assertEquals("Project should have polling result significant", PollingResult.Change.SIGNIFICANT, p.poll(TaskListener.NULL).change);
    }
    
    @Test
    public void testHasParticipant() throws Exception{
        User user = User.get("John Smith", true, Collections.emptyMap());
        FreeStyleProject project = j.createFreeStyleProject("project");
        FreeStyleProject project2 = j.createFreeStyleProject("project2");
        FakeChangeLogSCM scm = new FakeChangeLogSCM();
        project2.setScm(scm);
        j.buildAndAssertSuccess(project2);
        assertFalse("Project should not have any participant.", project2.hasParticipant(user));
        scm.addChange().withAuthor(user.getId());
        project.setScm(scm); 
        j.buildAndAssertSuccess(project);
        assertTrue("Project should have participant.", project.hasParticipant(user));
    }
    
    @Test
    public void testGetRelationship() throws Exception{
        final FreeStyleProject upstream = j.createFreeStyleProject("upstream");
        FreeStyleProject downstream = j.createFreeStyleProject("downstream");
        j.buildAndAssertSuccess(upstream);
        j.buildAndAssertSuccess(upstream);
        j.buildAndAssertSuccess(downstream);
        assertTrue("Project upstream should not have any relationship with downstream", upstream.getRelationship(downstream).isEmpty());

        upstream.getPublishersList().add(new Fingerprinter("change.log", true));
        upstream.getBuildersList().add(new WorkspaceWriter("change.log", "hello"));
        upstream.getPublishersList().add(new ArtifactArchiver("change.log"));
        downstream.getPublishersList().add(new Fingerprinter("change.log", false));
        downstream.getBuildersList().add(new TestBuilder() {
            @Override public boolean perform(AbstractBuild<?, ?> build, Launcher launcher, BuildListener listener) throws InterruptedException, IOException {
                for (Run<?, ?>.Artifact a: upstream.getLastBuild().getArtifacts()) {
                    Util.copyFile(a.getFile(), new File(build.getWorkspace().child(a.getFileName()).getRemote()));
                }
                return true;
            }
        });

        j.buildAndAssertSuccess(upstream);
        j.buildAndAssertSuccess(downstream);
        j.buildAndAssertSuccess(upstream);
        j.buildAndAssertSuccess(downstream);
        upstream.getBuildersList().add(new WorkspaceWriter("change.log", "helloWorld"));
        j.buildAndAssertSuccess(upstream);
        j.buildAndAssertSuccess(downstream);

        Map<Integer,Fingerprint.RangeSet> relationship = upstream.getRelationship(downstream);
        assertFalse("Project upstream should have relationship with downstream", relationship.isEmpty());
        assertTrue("Relationship should contain upstream #3", relationship.keySet().contains(3));
        assertFalse("Relationship should not contain upstream #4 because previous fingerprinted file was not changed since #3", relationship.keySet().contains(4));
        assertEquals("downstream #2 should be the first build which depends on upstream #3", 2, relationship.get(3).min());
        assertEquals("downstream #3 should be the last build which depends on upstream #3", 3, relationship.get(3).max()-1);
        assertEquals("downstream #4 should depend only on upstream #5", 4, relationship.get(5).min());
        assertEquals("downstream #4 should depend only on upstream #5", 4, relationship.get(5).max()-1);
    }
    
    @Test
    public void testDoCancelQueue() throws Exception{
        FreeStyleProject project = j.createFreeStyleProject("project");
        GlobalMatrixAuthorizationStrategy auth = new GlobalMatrixAuthorizationStrategy();   
        j.jenkins.setAuthorizationStrategy(auth);
        j.jenkins.setCrumbIssuer(null);
        HudsonPrivateSecurityRealm realm = new HudsonPrivateSecurityRealm(false);
        j.jenkins.setSecurityRealm(realm); 
        User user = realm.createAccount("John Smith", "password");
        try (ACLContext as = ACL.as(user)) {
            project.doCancelQueue(null, null);
            fail("User should not have permission to build project");
        }
        catch(Exception e){
            if(!(e.getClass().isAssignableFrom(AccessDeniedException2.class))){
               fail("AccessDeniedException should be thrown.");
            }
        } 
    }
    
    @Test
    public void testDoDoDelete() throws Exception{
        FreeStyleProject project = j.createFreeStyleProject("project");
        GlobalMatrixAuthorizationStrategy auth = new GlobalMatrixAuthorizationStrategy();   
        j.jenkins.setAuthorizationStrategy(auth);
        j.jenkins.setCrumbIssuer(null);
        j.jenkins.setSecurityRealm(j.createDummySecurityRealm());
        User user = User.getById("john", true);
        try (ACLContext as = ACL.as(user)) {
            project.doDoDelete(null, null);
            fail("User should not have permission to build project");
        }
        catch(Exception e){
            if(!(e.getClass().isAssignableFrom(AccessDeniedException2.class))){
               fail("AccessDeniedException should be thrown.");
            }
        } 
        auth.add(Jenkins.READ, user.getId());
        auth.add(Job.READ, user.getId());
        auth.add(Job.DELETE, user.getId());

        // use Basic to speedup the test, normally it's pure UI testing
        JenkinsRule.WebClient wc = j.createWebClient();
        wc.withBasicCredentials(user.getId());
        HtmlPage p = wc.goTo(project.getUrl() + "delete");

        List<HtmlForm> forms = p.getForms();
        for(HtmlForm form:forms){
            if("doDelete".equals(form.getAttribute("action"))){
                j.submit(form);
            }
        }
        assertNull("Project should be deleted form memory.", j.jenkins.getItem(project.getDisplayName()));
        assertFalse("Project should be deleted form disk.", project.getRootDir().exists());
    }
    
    @Test
    public void testDoDoWipeOutWorkspace() throws Exception{
        FreeStyleProject project = j.createFreeStyleProject("project");
        GlobalMatrixAuthorizationStrategy auth = new GlobalMatrixAuthorizationStrategy();   
        j.jenkins.setAuthorizationStrategy(auth);
        j.jenkins.setCrumbIssuer(null);
        HudsonPrivateSecurityRealm realm = new HudsonPrivateSecurityRealm(false);
        j.jenkins.setSecurityRealm(realm); 
        User user = realm.createAccount("John Smith", "password");
        try (ACLContext as = ACL.as(user)) {
            project.doDoWipeOutWorkspace();
            fail("User should not have permission to build project");
        }
        catch(Exception e){
            if(!(e.getClass().isAssignableFrom(AccessDeniedException2.class))){
               fail("AccessDeniedException should be thrown.");
            }
        } 
        auth.add(Job.READ, user.getId());
        auth.add(Job.BUILD, user.getId());
        auth.add(Job.WIPEOUT, user.getId());
        auth.add(Jenkins.READ, user.getId());
        Slave slave = j.createOnlineSlave();
        project.setAssignedLabel(slave.getSelfLabel());
        String cmd = "echo hello > change.log";
        project.getBuildersList().add(Functions.isWindows()? new BatchFile(cmd) : new Shell(cmd));
        j.buildAndAssertSuccess(project);

        JenkinsRule.WebClient wc = j.createWebClient();
        wc.withBasicCredentials(user.getId(), "password");
        WebRequest request = new WebRequest(new URL(wc.getContextPath() + project.getUrl() + "doWipeOutWorkspace"), HttpMethod.POST);
        HtmlPage p = wc.getPage(request);
        assertEquals(p.getWebResponse().getStatusCode(), 200);

        Thread.sleep(500);
        assertFalse("Workspace should not exist.", project.getSomeWorkspace().exists());
    }
    
    @Test
    public void testDoDisable() throws Exception{
        FreeStyleProject project = j.createFreeStyleProject("project");
        GlobalMatrixAuthorizationStrategy auth = new GlobalMatrixAuthorizationStrategy();   
        j.jenkins.setAuthorizationStrategy(auth);
        j.jenkins.setCrumbIssuer(null);
        HudsonPrivateSecurityRealm realm = new HudsonPrivateSecurityRealm(false);
        j.jenkins.setSecurityRealm(realm); 
        User user = realm.createAccount("John Smith", "password");
        try (ACLContext as = ACL.as(user)) {
            project.doDisable();
            fail("User should not have permission to build project");
        }
        catch(Exception e){
            if(!(e.getClass().isAssignableFrom(AccessDeniedException2.class))){
               fail("AccessDeniedException should be thrown.");
            }
        } 
        auth.add(Job.READ, user.getId());
        auth.add(Job.CONFIGURE, user.getId());
        auth.add(Jenkins.READ, user.getId());

        JenkinsRule.WebClient wc = j.createWebClient();
        wc.withBasicCredentials(user.getId(), "password");
        HtmlPage p = wc.goTo(project.getUrl());

        List<HtmlForm> forms = p.getForms();
        for(HtmlForm form:forms){
            if("disable".equals(form.getAttribute("action"))){
                j.submit(form);
            }
        }
       assertTrue("Project should be disabled.", project.isDisabled());
    }
    
    @Test
    public void testDoEnable() throws Exception{
        FreeStyleProject project = j.createFreeStyleProject("project");
        GlobalMatrixAuthorizationStrategy auth = new GlobalMatrixAuthorizationStrategy();   
        j.jenkins.setAuthorizationStrategy(auth);
        j.jenkins.setCrumbIssuer(null);
        HudsonPrivateSecurityRealm realm = new HudsonPrivateSecurityRealm(false);
        j.jenkins.setSecurityRealm(realm);
        User user = realm.createAccount("John Smith", "password");
        try (ACLContext as = ACL.as(user)) {
            project.disable();
        }
        try (ACLContext as = ACL.as(user)) {
            project.doEnable();
            fail("User should not have permission to build project");
        }
        catch(Exception e){
            if(!(e.getClass().isAssignableFrom(AccessDeniedException2.class))){
               fail("AccessDeniedException should be thrown.");
            }
        } 
        auth.add(Job.READ, user.getId());
        auth.add(Job.CONFIGURE, user.getId());
        auth.add(Jenkins.READ, user.getId());

        JenkinsRule.WebClient wc = j.createWebClient();
        wc.withBasicCredentials(user.getId(), "password");
        HtmlPage p = wc.goTo(project.getUrl());

        List<HtmlForm> forms = p.getForms();
        for(HtmlForm form:forms){
            if("enable".equals(form.getAttribute("action"))){
                j.submit(form);
            }
        }
       assertFalse("Project should be enabled.", project.isDisabled());
    }
    
    /**
     * Job is un-restricted (no nabel), this is submitted to queue, which spawns an on demand slave
     * @throws Exception 
     */
    @Test
    public void testJobSubmittedShouldSpawnCloud() throws Exception {
        /**
         * Setup a project with an SCM. Jenkins should have no executors in itself. 
         */
        FreeStyleProject proj = j.createFreeStyleProject("JENKINS-21394-spawn");        
        RequiresWorkspaceSCM requiresWorkspaceScm = new RequiresWorkspaceSCM(true);
        proj.setScm(requiresWorkspaceScm);        
        j.jenkins.setNumExecutors(0);        
        /*
         * We have a cloud
         */
        DummyCloudImpl2 c2 = new DummyCloudImpl2(j, 0);
        c2.label = new LabelAtom("test-cloud-label");        
        j.jenkins.clouds.add(c2);
        
        SCMTrigger t = new SCMTrigger("@daily", true);
        t.start(proj, true);
        proj.addTrigger(t);
        t.new Runner().run();
        
        Thread.sleep(1000);
        //Assert that the job IS submitted to Queue.
        assertEquals(1, j.jenkins.getQueue().getItems().length);        
    }
    
    /**
     * Job is restricted, but label can not be provided by any cloud, only normal slaves. Then job will not submit, because no slave is available.
     * @throws Exception
     */
    @Test
    public void testUnrestrictedJobNoLabelByCloudNoQueue() throws Exception {
        assertTrue(j.jenkins.clouds.isEmpty());
        //Create slave. (Online)
        Slave s1 = j.createOnlineSlave();
        
        //Create a project, and bind the job to the created slave
        FreeStyleProject proj = j.createFreeStyleProject("JENKINS-21394-noqueue");
        proj.setAssignedLabel(s1.getSelfLabel());
                        
        //Add an SCM to the project. We require a workspace for the poll
        RequiresWorkspaceSCM requiresWorkspaceScm = new RequiresWorkspaceSCM(true);
        proj.setScm(requiresWorkspaceScm);
 
        j.buildAndAssertSuccess(proj);        

        //Now create another slave. And restrict the job to that slave. The slave is offline, leaving the job with no assignable nodes.
        //We tell our mock SCM to return that it has got changes. But since there are no slaves, we get the desired result. 
        Slave s2 = j.createSlave();
        proj.setAssignedLabel(s2.getSelfLabel());
        requiresWorkspaceScm.hasChange = true;
        
        //Poll (We now should have NO online slaves, this should now return NO_CHANGES.
        PollingResult pr = proj.poll(j.createTaskListener());
        assertFalse(pr.hasChanges());
        
        SCMTrigger t = new SCMTrigger("@daily", true);
        t.start(proj, true);
        proj.addTrigger(t);
        
        t.new Runner().run();
        
        /**
         * Assert that the log contains the correct message.
         */
        HtmlPage log = j.createWebClient().getPage(proj, "scmPollLog");
        String logastext = log.asText();
        assertTrue(logastext.contains("(" + AbstractProject.WorkspaceOfflineReason.all_suitable_nodes_are_offline.name() + ")"));
        
    }
    
    /**
     * Job is restricted. Label is on slave that can be started in cloud. Job is submitted to queue, which spawns an on demand slave.
     * @throws Exception 
     */
    @Test
    public void testRestrictedLabelOnSlaveYesQueue() throws Exception {        
        FreeStyleProject proj = j.createFreeStyleProject("JENKINS-21394-yesqueue");
        RequiresWorkspaceSCM requiresWorkspaceScm = new RequiresWorkspaceSCM(true);
        proj.setScm(requiresWorkspaceScm);        
        j.jenkins.setNumExecutors(0);
        
        /*
         * We have a cloud
         */
        DummyCloudImpl2 c2 = new DummyCloudImpl2(j, 0);
        c2.label = new LabelAtom("test-cloud-label");        
        j.jenkins.clouds.add(c2);
        proj.setAssignedLabel(c2.label);
        
        SCMTrigger t = new SCMTrigger("@daily", true);
        t.start(proj, true);
        proj.addTrigger(t);
        t.new Runner().run();
        
        Thread.sleep(1000);
        //The job should be in queue
        assertEquals(1, j.jenkins.getQueue().getItems().length);    
    }

    @Issue("JENKINS-22750")
    @Test
    public void testMasterJobPutInQueue() throws Exception {
        FreeStyleProject proj = j.createFreeStyleProject("JENKINS-21394-yes-master-queue");
        RequiresWorkspaceSCM requiresWorkspaceScm = new RequiresWorkspaceSCM(true);
        proj.setAssignedLabel(null);        
        proj.setScm(requiresWorkspaceScm);        
        j.jenkins.setNumExecutors(1);    
        proj.setScm(requiresWorkspaceScm);
        
        //First build is not important
        j.buildAndAssertSuccess(proj);

        SCMTrigger t = new SCMTrigger("@daily", true);
        t.start(proj, true);
        proj.addTrigger(t);
        t.new Runner().run();


        assertFalse(j.jenkins.getQueue().isEmpty());
    }

    public static class TransientAction extends InvisibleAction{
        
    }
    
    @TestExtension
    public static class TransientActionFactoryImpl extends TransientProjectActionFactory{

        @Override
        public Collection<? extends Action> createFor(AbstractProject target) {
            List<Action> actions = new ArrayList<Action>();
            if(createAction)
                actions.add(new TransientAction());
            return actions;
        }
        
    }
    
    @TestExtension 
    public static class RequiresWorkspaceSCM extends NullSCM {
        
        public boolean hasChange = false;
        
        public RequiresWorkspaceSCM() { }
         
        public RequiresWorkspaceSCM(boolean hasChange) {
            this.hasChange = hasChange;
        }
        
        @Override
        public boolean pollChanges(AbstractProject<?, ?> project, Launcher launcher, FilePath workspace, TaskListener listener) throws IOException, InterruptedException {
            return hasChange;
        }
                       
        @Override
        public boolean requiresWorkspaceForPolling(){
            return true;
        }
        @Override public SCMDescriptor<?> getDescriptor() {
            return new SCMDescriptor<SCM>(null) {};
        }
        
        @Override
        protected PollingResult compareRemoteRevisionWith(AbstractProject project, Launcher launcher, FilePath workspace, TaskListener listener, SCMRevisionState baseline) throws IOException, InterruptedException {            
            if(!hasChange) {
                return PollingResult.NO_CHANGES;
            }
            return PollingResult.SIGNIFICANT;
        }
    }
    
    @TestExtension
    public static class AlwaysChangedSCM extends NullSCM {

        @Override
        public boolean pollChanges(AbstractProject<?, ?> project, Launcher launcher, FilePath workspace, TaskListener listener) throws IOException, InterruptedException {
            return true;
        }
        
        @Override
        public boolean requiresWorkspaceForPolling(){
            return false;
        }

        @Override
        protected PollingResult compareRemoteRevisionWith(AbstractProject project, Launcher launcher, FilePath workspace, TaskListener listener, SCMRevisionState baseline) throws IOException, InterruptedException {
            return PollingResult.SIGNIFICANT;
        }
        
    }
    
    @TestExtension
    public static class WorkspaceBrowserImpl extends WorkspaceBrowser{

        @Override
        public FilePath getWorkspace(Job job) {
            if(getFilePath)
                return new FilePath(new File("some_file_path"));
            return null;
        }
        
    }
    

    public static class SCMCheckoutStrategyImpl extends DefaultSCMCheckoutStrategyImpl implements Serializable{
        
        public SCMCheckoutStrategyImpl(){
            
        }

    }
    
    public static class JobPropertyImp extends JobProperty{

        @Override
        public Collection getSubTasks() {
            ArrayList<SubTask> list = new ArrayList<SubTask>();
            list.add(new SubTaskImpl());
            return list;
        }
        
        
    }
    
    @TestExtension
    public static class SubTaskContributorImpl extends SubTaskContributor{

        @Override
        public Collection<? extends SubTask> forProject(AbstractProject<?, ?> p) {
            ArrayList<SubTask> list = new ArrayList<SubTask>();
            if(createSubTask){
                list.add(new SubTaskImpl2());
            }
            return list;
        }
    }
    
    public static class SubTaskImpl2 extends SubTaskImpl{
        
    }
    
    public static class SubTaskImpl implements SubTask{
        
        public String projectName;

        @Override
        public Executable createExecutable() throws IOException {
            return null;
        }

        @Override
        public Task getOwnerTask() {
            return (Task) Jenkins.getInstance().getItem(projectName);
        }

        @Override
        public String getDisplayName() {
            return "some task";
        }

        
    }
    
    public class ActionImpl extends InvisibleAction{
        
    }
    
    @TestExtension
    public static class DummyCloudImpl2 extends Cloud {
        private final transient JenkinsRule caller;

        /**
         * Configurable delay between the {@link Cloud#provision(Label,int)} and the actual launch of a slave,
         * to emulate a real cloud that takes some time for provisioning a new system.
         *
         * <p>
         * Number of milliseconds.
         */
        private final int delay;

        // stats counter to perform assertions later
        public int numProvisioned;

        /**
         * Only reacts to provisioning for this label.
         */
        public Label label;
        
        public DummyCloudImpl2() { 
            super("test");
            this.delay = 0;
            this.caller = null;
        }

        public DummyCloudImpl2(JenkinsRule caller, int delay) {
            super("test");
            this.caller = caller;
            this.delay = delay;
        }

        @Override
        public Collection<NodeProvisioner.PlannedNode> provision(Label label, int excessWorkload) {
            List<NodeProvisioner.PlannedNode> r = new ArrayList<NodeProvisioner.PlannedNode>();

            //Always provision...even if there is no workload.
            while(excessWorkload >= 0) {
                System.out.println("Provisioning");
                numProvisioned++;
                Future<Node> f = Computer.threadPoolForRemoting.submit(new ProjectTest.DummyCloudImpl2.Launcher(delay));
                r.add(new NodeProvisioner.PlannedNode(name+" #"+numProvisioned,f,1));
                excessWorkload-=1;
            }
            return r;
        }

        @Override
        public boolean canProvision(Label label) {
            //This cloud can ALWAYS provision 
           return true;
            /* return label==this.label; */
        }

        private final class Launcher implements Callable<Node> {
            private final long time;
            /**
             * This is so that we can find out the status of Callable from the debugger.
             */
            private volatile Computer computer;

            private Launcher(long time) {
                this.time = time;
            }

            @Override
            public Node call() throws Exception {
                // simulate the delay in provisioning a new slave,
                // since it's normally some async operation.
                Thread.sleep(time);

                System.out.println("launching slave");
                DumbSlave slave = caller.createSlave(label);
                computer = slave.toComputer();
                computer.connect(false).get();
                synchronized (ProjectTest.DummyCloudImpl2.this) {
                    System.out.println(computer.getName()+" launch"+(computer.isOnline()?"ed successfully":" failed"));
                    System.out.println(computer.getLog());
                }
                return slave;
            }
        }

        @Override
        public Descriptor<Cloud> getDescriptor() {
            throw new UnsupportedOperationException();
        }
    }
}
