/*
 * Copyright 2023 Thoughtworks, Inc.
 *
 * Licensed 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.
 */
package com.thoughtworks.go.config.materials;

import com.thoughtworks.go.config.*;
import com.thoughtworks.go.config.materials.dependency.DependencyMaterialConfig;
import com.thoughtworks.go.config.materials.git.GitMaterialConfig;
import com.thoughtworks.go.config.materials.mercurial.HgMaterialConfig;
import com.thoughtworks.go.config.materials.perforce.P4MaterialConfig;
import com.thoughtworks.go.config.materials.svn.SvnMaterialConfig;
import com.thoughtworks.go.config.materials.tfs.TfsMaterialConfig;
import com.thoughtworks.go.config.remote.ConfigRepoConfig;
import com.thoughtworks.go.config.remote.FileConfigOrigin;
import com.thoughtworks.go.config.remote.RepoConfigOrigin;
import com.thoughtworks.go.domain.ConfigErrors;
import com.thoughtworks.go.domain.materials.MaterialConfig;
import com.thoughtworks.go.domain.packagerepository.PackageDefinitionMother;
import com.thoughtworks.go.domain.scm.SCMMother;
import com.thoughtworks.go.helper.GoConfigMother;
import com.thoughtworks.go.helper.MaterialConfigsMother;
import com.thoughtworks.go.security.GoCipher;
import com.thoughtworks.go.util.command.UrlArgument;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

import java.io.IOException;
import java.util.HashMap;
import java.util.Map;

import static com.thoughtworks.go.helper.MaterialConfigsMother.*;
import static com.thoughtworks.go.util.TestUtils.contains;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.*;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;

public class MaterialConfigsTest {
    private GoConfigMother goConfigMother;

    @BeforeEach
    public void setUp() throws Exception {
        goConfigMother = new GoConfigMother();
    }

    @Test
    public void shouldNotAllowMoreThanOneDependencyWithSameName() throws Exception {
        CruiseConfig config = GoConfigMother.configWithPipelines("pipeline1", "pipeline2", "pipeline3", "go");
        DependencyMaterialConfig one = new DependencyMaterialConfig(new CaseInsensitiveString("sameName"), new CaseInsensitiveString("pipeline2"), new CaseInsensitiveString("stage"));
        DependencyMaterialConfig another = new DependencyMaterialConfig(new CaseInsensitiveString("sameName"), new CaseInsensitiveString("pipeline3"), new CaseInsensitiveString("stage"));
        MaterialConfigs materialConfigs = new MaterialConfigs(one, another);
        ValidationContext validationContext = ConfigSaveValidationContext.forChain(config);

        materialConfigs.validate(validationContext);

        assertThat(one.errors().isEmpty(), is(false));
        assertThat(one.errors().on("materialName"), containsString("You have defined multiple materials called 'sameName'. Material names are case-insensitive and must be unique."));

        assertThat(another.errors().isEmpty(), is(false));
        assertThat(another.errors().on("materialName"), containsString("You have defined multiple materials called 'sameName'. Material names are case-insensitive and must be unique."));
    }

/*
                        Name
        Pipeline X    - Material1           - pipeline1
                      - Material2           - someSvn
                      - DepMaterial1        -  ""       dependant on a pipeline named "pipeline1"
Above scenario allowed
*/

    @Test
    public void shouldNotAllowAnEmptyDepMaterialWhenOtherMaterialsUseThatPipelineName() throws Exception {
        CruiseConfig config = GoConfigMother.configWithPipelines("pipeline1", "pipeline2", "pipeline3", "go");
        SvnMaterialConfig one = svn("svn://abc", "", "", false);
        one.setName(new CaseInsensitiveString("pipeline2"));
        DependencyMaterialConfig invalidOne = new DependencyMaterialConfig(new CaseInsensitiveString("pipeline2"), new CaseInsensitiveString("stage"));

        MaterialConfigs materials = new MaterialConfigs(one, invalidOne);
        ValidationContext validationContext = ConfigSaveValidationContext.forChain(config);

        materials.validate(validationContext);

        assertThat(invalidOne.errors().isEmpty(), is(false));
        assertThat(invalidOne.errors().on("materialName"), is("You have defined multiple materials called 'pipeline2'."
                + " Material names are case-insensitive and must be unique. Note that for dependency materials the default materialName is the name of the upstream pipeline. "
                + "You can override this by setting the materialName explicitly for the upstream pipeline."));
    }

    @Test
    public void shouldReturnValidWhenThereIsNoCycle() throws Exception {
        CruiseConfig cruiseConfig = new BasicCruiseConfig();
        PipelineConfig pipeline1 = goConfigMother.addPipeline(cruiseConfig, "pipeline1", "stage", "build");
        PipelineConfig pipeline2 = goConfigMother.addPipeline(cruiseConfig, "pipeline2", "stage", "build");
        goConfigMother.setDependencyOn(cruiseConfig, pipeline2, "pipeline1", "stage");

        pipeline1.materialConfigs().validate(ConfigSaveValidationContext.forChain(cruiseConfig));
        assertThat(pipeline1.materialConfigs().errors().isEmpty(), is(true));

        pipeline2.materialConfigs().validate(ConfigSaveValidationContext.forChain(cruiseConfig));
        assertThat(pipeline2.materialConfigs().errors().isEmpty(), is(true));
    }

    @Test
    public void shouldNotAllowToDependOnPipelineDefinedInConfigRepository_WhenDownstreamInFile() throws Exception {
        CruiseConfig cruiseConfig = new BasicCruiseConfig();
        PipelineConfig pipeline1 = goConfigMother.addPipeline(cruiseConfig, "pipeline1", "stage", "build");
        PipelineConfig pipeline2 = goConfigMother.addPipeline(cruiseConfig, "pipeline2", "stage", "build");
        goConfigMother.setDependencyOn(cruiseConfig, pipeline2, "pipeline1", "stage");

        pipeline1.setOrigin(new RepoConfigOrigin());
        pipeline2.setOrigin(new FileConfigOrigin());

        pipeline1.materialConfigs().validate(ConfigSaveValidationContext.forChain(cruiseConfig, new BasicPipelineConfigs(), pipeline1));
        assertThat(pipeline1.materialConfigs().errors().isEmpty(), is(true));

        pipeline2.materialConfigs().validate(ConfigSaveValidationContext.forChain(cruiseConfig, new BasicPipelineConfigs(), pipeline2));
        DependencyMaterialConfig invalidDependency = pipeline2.materialConfigs().findDependencyMaterial(new CaseInsensitiveString("pipeline1"));
        assertThat(invalidDependency.errors().isEmpty(), is(false));
        assertThat(invalidDependency.errors().on(DependencyMaterialConfig.ORIGIN), startsWith("Dependency from pipeline defined in"));
    }

    @Test
    public void shouldAllowToDependOnPipelineDefinedInConfigRepository_WhenInConfigRepository() throws Exception {
        CruiseConfig cruiseConfig = new BasicCruiseConfig();
        PipelineConfig pipeline1 = goConfigMother.addPipeline(cruiseConfig, "pipeline1", "stage", "build");
        PipelineConfig pipeline2 = goConfigMother.addPipeline(cruiseConfig, "pipeline2", "stage", "build");
        goConfigMother.setDependencyOn(cruiseConfig, pipeline2, "pipeline1", "stage");

        pipeline1.setOrigin(new RepoConfigOrigin(ConfigRepoConfig.createConfigRepoConfig(svn("http://mysvn", false), "myplugin", "id"), "123"));
        pipeline2.setOrigin(new RepoConfigOrigin(ConfigRepoConfig.createConfigRepoConfig(svn("http://othersvn", false), "myplugin", "other-id"), "2222"));

        pipeline1.materialConfigs().validate(ConfigSaveValidationContext.forChain(cruiseConfig, new BasicPipelineConfigs(), pipeline1));
        assertThat(pipeline1.materialConfigs().errors().isEmpty(), is(true));

        pipeline2.materialConfigs().validate(ConfigSaveValidationContext.forChain(cruiseConfig, new BasicPipelineConfigs(), pipeline2));
        DependencyMaterialConfig dep = pipeline2.materialConfigs().findDependencyMaterial(new CaseInsensitiveString("pipeline1"));
        assertThat(dep.errors().isEmpty(), is(true));
    }

    @Test
    public void shouldAllowToDependOnPipelineDefinedInFile_WhenInFile() throws Exception {
        CruiseConfig cruiseConfig = new BasicCruiseConfig();
        PipelineConfig pipeline1 = goConfigMother.addPipeline(cruiseConfig, "pipeline1", "stage", "build");
        PipelineConfig pipeline2 = goConfigMother.addPipeline(cruiseConfig, "pipeline2", "stage", "build");
        goConfigMother.setDependencyOn(cruiseConfig, pipeline2, "pipeline1", "stage");

        pipeline1.setOrigin(new FileConfigOrigin());
        pipeline2.setOrigin(new FileConfigOrigin());

        pipeline1.materialConfigs().validate(ConfigSaveValidationContext.forChain(cruiseConfig, new BasicPipelineConfigs(), pipeline1));
        assertThat(pipeline1.materialConfigs().errors().isEmpty(), is(true));

        pipeline2.materialConfigs().validate(ConfigSaveValidationContext.forChain(cruiseConfig, new BasicPipelineConfigs(), pipeline2));
        DependencyMaterialConfig dep = pipeline2.materialConfigs().findDependencyMaterial(new CaseInsensitiveString("pipeline1"));
        assertThat(dep.errors().isEmpty(), is(true));
    }

    @Test
    public void shouldNotAllowMultipleDependenciesForTheSamePipelines() throws Exception {
        CruiseConfig config = GoConfigMother.configWithPipelines("pipeline1", "pipeline2", "pipeline3", "go");
        DependencyMaterialConfig dependencyMaterial = new DependencyMaterialConfig(new CaseInsensitiveString("pipeline2"), new CaseInsensitiveString("stage"));
        DependencyMaterialConfig duplicateDependencyMaterial = new DependencyMaterialConfig(new CaseInsensitiveString("pipeline2"), new CaseInsensitiveString("stage"));
        MaterialConfigs materialConfigs = new MaterialConfigs(dependencyMaterial, duplicateDependencyMaterial);

        ValidationContext validationContext = ConfigSaveValidationContext.forChain(config);
        materialConfigs.validate(validationContext);

        ConfigErrors errors = duplicateDependencyMaterial.errors();
        assertThat(errors.isEmpty(), is(false));
        assertThat(errors.on("pipelineStageName"),
                is("A pipeline can depend on each upstream pipeline only once. Remove one of the occurrences of 'pipeline2' from the current pipeline dependencies."));
    }

    @Test
    public void shouldIgnorePipelineWithEmptyNameInUniquenessCheck() throws Exception {
        CruiseConfig config = GoConfigMother.configWithPipelines("pipeline1", "pipeline2", "pipeline3", "go");
        DependencyMaterialConfig one = new DependencyMaterialConfig(new CaseInsensitiveString(""), new CaseInsensitiveString("pipeline2"), new CaseInsensitiveString("stage"));
        DependencyMaterialConfig another = new DependencyMaterialConfig(new CaseInsensitiveString(""), new CaseInsensitiveString("pipeline3"), new CaseInsensitiveString("stage"));
        MaterialConfigs materials = new MaterialConfigs(one, another);
        ValidationContext validationContext = ConfigSaveValidationContext.forChain(config);
        materials.validate(validationContext);
        assertThat(one.errors().isEmpty(), is(true));
        assertThat(another.errors().isEmpty(), is(true));
    }

    @Test
    public void shouldReturnTrueWhenDependencyPipelineDoesNotExist() throws Exception {
        CruiseConfig cruiseConfig = new BasicCruiseConfig();
        PipelineConfig pipelineConfig = goConfigMother.addPipeline(cruiseConfig, "pipeline1", "stage", "build");
        goConfigMother.setDependencyOn(cruiseConfig, pipelineConfig, "pipeline2", "stage");
        pipelineConfig.materialConfigs().validate(ConfigSaveValidationContext.forChain(cruiseConfig, new BasicPipelineConfigs(), pipelineConfig));
        assertThat(pipelineConfig.materialConfigs().errors().isEmpty(), is(true));
    }

    @Test
    public void shouldFailIfAllScmMaterialsInAPipelineHaveSameFolders() throws IOException {
        HgMaterialConfig materialOne = hg("http://url1", null);
        materialOne.setConfigAttributes(Map.of(ScmMaterialConfig.FOLDER, "folder1"));
        HgMaterialConfig materialTwo = hg("http://url2", null);
        materialTwo.setConfigAttributes(Map.of(ScmMaterialConfig.FOLDER, "folder1"));
        PluggableSCMMaterialConfig materialThree = new PluggableSCMMaterialConfig(null, SCMMother.create("scm-id"), "folder1", null, false);
        CruiseConfig config = GoConfigMother.configWithPipelines("one");
        PipelineConfig pipelineOne = config.pipelineConfigByName(new CaseInsensitiveString("one"));
        pipelineOne.setMaterialConfigs(new MaterialConfigs(materialOne, materialTwo, materialThree));

        pipelineOne.materialConfigs().validate(ConfigSaveValidationContext.forChain(config));

        String conflictingDirMessage = "Invalid destination directory. Every material needs a different destination directory and the directories should not be nested.";
        assertThat(pipelineOne.materialConfigs().get(0).errors().on(ScmMaterialConfig.FOLDER), is(conflictingDirMessage));
        assertThat(pipelineOne.materialConfigs().get(1).errors().on(ScmMaterialConfig.FOLDER), is(conflictingDirMessage));
        assertThat(pipelineOne.materialConfigs().get(2).errors().on(PluggableSCMMaterialConfig.FOLDER), is(conflictingDirMessage));
    }

    @Test
    public void shouldNotFailIfAllScmMaterialsInAPipelineHaveDifferentFolders() {
        HgMaterialConfig materialOne = hg("http://url1", null);
        materialOne.setConfigAttributes(Map.of(ScmMaterialConfig.FOLDER, "folder1"));
        HgMaterialConfig materialTwo = hg("http://url2", null);
        materialTwo.setConfigAttributes(Map.of(ScmMaterialConfig.FOLDER, "folder2"));
        CruiseConfig config = GoConfigMother.configWithPipelines("one");
        PipelineConfig pipelineOne = config.pipelineConfigByName(new CaseInsensitiveString("one"));
        pipelineOne.setMaterialConfigs(new MaterialConfigs(materialOne, materialTwo));

        pipelineOne.materialConfigs().validate(ConfigSaveValidationContext.forChain(config));

        assertThat(pipelineOne.materialConfigs().get(0).errors().isEmpty(), is(true));
        assertThat(pipelineOne.materialConfigs().get(1).errors().isEmpty(), is(true));
    }

    @Test
    public void shouldReturnNullWhenMaterialNotFoundForTheGivenFingerPrint() {
        CruiseConfig cruiseConfig = new BasicCruiseConfig();
        PipelineConfig pipeline = goConfigMother.addPipeline(cruiseConfig, "pipeline1", "stage", "build");
        assertThat(pipeline.materialConfigs().getByFingerPrint("invalid"), is(nullValue()));
    }

    @Test
    public void shouldReturnNullWhenMaterialNotFoundForTheGivenMaterialFingerPrint() {
        CruiseConfig cruiseConfig = new BasicCruiseConfig();
        PipelineConfig pipeline = goConfigMother.addPipeline(cruiseConfig, "pipeline1", "stage", "build");
        assertThat(pipeline.materialConfigs().getByMaterialFingerPrint("invalid"), is(nullValue()));
    }

    @Test
    public void shouldFailIfMultipleMaterialsDoNotHaveDestinationFolderSet() {
        HgMaterialConfig materialConfigOne = hg("http://url1", null);
        materialConfigOne.setConfigAttributes(Map.of(ScmMaterialConfig.FOLDER, "folder"));
        HgMaterialConfig materialConfigTwo = hg("http://url2", null);
        PluggableSCMMaterialConfig materialConfigThree = new PluggableSCMMaterialConfig(null, SCMMother.create("scm-id"), null, null, false);
        CruiseConfig config = GoConfigMother.configWithPipelines("one");
        PipelineConfig pipelineOne = config.pipelineConfigByName(new CaseInsensitiveString("one"));
        pipelineOne.setMaterialConfigs((new MaterialConfigs(materialConfigOne, materialConfigTwo, materialConfigThree)));

        pipelineOne.materialConfigs().validate(ConfigSaveValidationContext.forChain(config));

        assertThat(pipelineOne.materialConfigs().get(0).errors().isEmpty(), is(true));
        assertThat(pipelineOne.materialConfigs().get(1).errors().on(ScmMaterialConfig.FOLDER), is("Destination directory is required when a pipeline has multiple SCM materials."));
        assertThat(pipelineOne.materialConfigs().get(2).errors().on(PluggableSCMMaterialConfig.FOLDER), is("Destination directory is required when a pipeline has multiple SCM materials."));
    }

    @Test
    public void shouldAddErrorWhenMatchingScmConfigDoesNotExist() {
        PluggableSCMMaterialConfig scmMaterialConfig = new PluggableSCMMaterialConfig(null, SCMMother.create("scm-id"), null, null, false);
        PackageMaterialConfig packageMaterialConfig = new PackageMaterialConfig(new CaseInsensitiveString("package-name"), "package-id", PackageDefinitionMother.create("package-id"));
        CruiseConfig config = GoConfigMother.configWithPipelines("one");
        PipelineConfig pipelineConfig = config.pipelineConfigByName(new CaseInsensitiveString("one"));
        MaterialConfigs materialConfigs = new MaterialConfigs(scmMaterialConfig, packageMaterialConfig);

        pipelineConfig.setMaterialConfigs(materialConfigs);
        materialConfigs.validateTree(PipelineConfigSaveValidationContext.forChain(true, "group", config));

        assertThat(pipelineConfig.materialConfigs().get(0).errors().on(scmMaterialConfig.SCM_ID), is("Could not find SCM for given scm-id: [scm-id]."));
    }

    @Test
    public void shouldAddErrorWhenMatchingPackageIDDoesNotExist() {
        PackageMaterialConfig packageMaterialConfig = new PackageMaterialConfig(new CaseInsensitiveString("package-name"), "package-id", PackageDefinitionMother.create("package-id"));
        CruiseConfig config = GoConfigMother.configWithPipelines("one");
        PipelineConfig pipelineConfig = config.pipelineConfigByName(new CaseInsensitiveString("one"));
        MaterialConfigs materialConfigs = new MaterialConfigs(packageMaterialConfig);

        pipelineConfig.setMaterialConfigs(materialConfigs);
        materialConfigs.validateTree(PipelineConfigSaveValidationContext.forChain(true, "group", config));

        assertThat(pipelineConfig.materialConfigs().get(0).errors().on(packageMaterialConfig.PACKAGE_ID), is("Could not find repository for given package id:[package-id]"));
    }


    @Test
    public void shouldNotFailIfMultipleMaterialsHaveUniqueDestinationFolderSet() {
        HgMaterialConfig materialOne = hg("http://url1", null);
        materialOne.setConfigAttributes(Map.of(ScmMaterialConfig.FOLDER, "folder"));
        HgMaterialConfig materialTwo = hg("http://url2", null);
        materialTwo.setConfigAttributes(Map.of(ScmMaterialConfig.FOLDER, "folder2"));
        CruiseConfig config = GoConfigMother.configWithPipelines("one");
        PipelineConfig pipelineOne = config.pipelineConfigByName(new CaseInsensitiveString("one"));
        pipelineOne.setMaterialConfigs(new MaterialConfigs(materialOne, materialTwo));

        pipelineOne.materialConfigs().validate(ConfigSaveValidationContext.forChain(config));

        assertThat(pipelineOne.materialConfigs().get(0).errors().isEmpty(), is(true));
        assertThat(pipelineOne.materialConfigs().get(1).errors().isEmpty(), is(true));
    }

    @Test
    public void shouldCheckSCMMaterialsHaveDestinationCorrectly() {
        HgMaterialConfig materialConfigOne = hg("http://url1", null);
        materialConfigOne.setConfigAttributes(Map.of(ScmMaterialConfig.FOLDER, "folder"));

        CruiseConfig config = GoConfigMother.configWithPipelines("one");
        PipelineConfig pipelineOne = config.pipelineConfigByName(new CaseInsensitiveString("one"));
        pipelineOne.setMaterialConfigs((new MaterialConfigs(materialConfigOne)));

        assertThat(pipelineOne.materialConfigs().scmMaterialsHaveDestination(), is(true));

        PluggableSCMMaterialConfig materialConfigTwo = new PluggableSCMMaterialConfig(null, SCMMother.create("scm-id"), null, null, false);
        pipelineOne.materialConfigs().add(materialConfigTwo);

        assertThat(pipelineOne.materialConfigs().scmMaterialsHaveDestination(), is(false));
    }

    @Test
    public void shouldShowAutoUpdateMismatchErrorTwiceWhenMaterialIsAddedToSamePipeline() throws Exception {
        HgMaterialConfig materialOne = hg("http://url1", null);
        materialOne.setConfigAttributes(Map.of(ScmMaterialConfig.FOLDER, "some-folder"));
        materialOne.setAutoUpdate(true);
        HgMaterialConfig materialTwo = hg("http://url1", null);
        materialTwo.setConfigAttributes(Map.of(ScmMaterialConfig.FOLDER, "some-folder-2"));
        materialTwo.setAutoUpdate(false);
        CruiseConfig config = GoConfigMother.configWithPipelines("one");
        PipelineConfig pipelineOne = config.pipelineConfigByName(new CaseInsensitiveString("one"));
        pipelineOne.setMaterialConfigs(new MaterialConfigs(materialOne, materialTwo));

        pipelineOne.materialConfigs().validate(ConfigSaveValidationContext.forChain(config));

        assertThat(pipelineOne.materialConfigs().get(0).errors().getAll().size(), is(1));
        assertThat(pipelineOne.materialConfigs().get(1).errors().getAll().size(), is(1));
    }

    @Test
    public void shouldNotThrowUpOnMaterialIfAutoUpdateValuesAreCorrect() throws Exception {
        HgMaterialConfig materialOne = hg("http://url1", null);
        materialOne.setAutoUpdate(true);
        HgMaterialConfig materialTwo = hg("http://url1", null);
        materialTwo.setAutoUpdate(true);
        CruiseConfig config = GoConfigMother.configWithPipelines("one", "two", "three");
        PipelineConfig pipelineOne = config.pipelineConfigByName(new CaseInsensitiveString("one"));
        pipelineOne.setMaterialConfigs(new MaterialConfigs(materialOne));
        PipelineConfig pipelineTwo = config.pipelineConfigByName(new CaseInsensitiveString("two"));
        pipelineTwo.setMaterialConfigs(new MaterialConfigs(materialTwo));

        pipelineOne.materialConfigs().validate(ConfigSaveValidationContext.forChain(config));

        assertThat(pipelineOne.materialConfigs().get(0).errors().isEmpty(), is(true));
        assertThat(pipelineTwo.materialConfigs().get(0).errors().isEmpty(), is(true));
    }

    @Test
    public void shouldReturnMaterialBasedOnPipelineUniqueFingerPrint() {
        CruiseConfig cruiseConfig = new BasicCruiseConfig();
        PipelineConfig pipeline1 = goConfigMother.addPipeline(cruiseConfig, "pipeline1", "stage", "build");
        HgMaterialConfig expectedMaterial = MaterialConfigsMother.hgMaterialConfig();
        pipeline1.addMaterialConfig(expectedMaterial);
        pipeline1.addMaterialConfig(MaterialConfigsMother.gitMaterialConfig("url"));
        pipeline1.addMaterialConfig(MaterialConfigsMother.svnMaterialConfig("url", "folder"));

        MaterialConfig actualMaterialConfig = pipeline1.materialConfigs().getByFingerPrint(expectedMaterial.getPipelineUniqueFingerprint());
        assertThat(actualMaterialConfig, is(expectedMaterial));
    }

    @Test
    public void shouldReturnMaterialBasedOnMaterialFingerPrint() {
        CruiseConfig cruiseConfig = new BasicCruiseConfig();
        PipelineConfig pipeline1 = goConfigMother.addPipeline(cruiseConfig, "pipeline1", "stage", "build");
        HgMaterialConfig expectedMaterial = MaterialConfigsMother.hgMaterialConfig();
        pipeline1.addMaterialConfig(expectedMaterial);
        pipeline1.addMaterialConfig(MaterialConfigsMother.gitMaterialConfig("url"));
        pipeline1.addMaterialConfig(MaterialConfigsMother.svnMaterialConfig("url", "folder"));

        MaterialConfig actualMaterialConfig = pipeline1.materialConfigs().getByMaterialFingerPrint(expectedMaterial.getFingerprint());
        assertThat(actualMaterialConfig, is(expectedMaterial));
    }

    @Test
    public void shouldReturnTrueWhenNoDependencyDefined() throws Exception {
        CruiseConfig cruiseConfig = new BasicCruiseConfig();
        PipelineConfig pipelineConfig = goConfigMother.addPipeline(cruiseConfig, "pipeline1", "stage", "build");
        goConfigMother.addPipeline(cruiseConfig, "pipeline2", "stage", "build");
        pipelineConfig.materialConfigs().validate(ConfigSaveValidationContext.forChain(cruiseConfig));
        assertThat(pipelineConfig.materialConfigs().errors().isEmpty(), is(true));
    }

    @Test
    public void shouldAddErrorOnMaterialIfAutoUpdateDoesNotMatchAcrossFingerPrint() throws Exception {
        HgMaterialConfig materialOne = hg("http://url1", null);
        materialOne.setAutoUpdate(false);
        HgMaterialConfig materialTwo = hg("http://url1", null);
        materialTwo.setAutoUpdate(true);
        CruiseConfig config = GoConfigMother.configWithPipelines("one", "two");
        PipelineConfig pipelineOne = config.pipelineConfigByName(new CaseInsensitiveString("one"));
        pipelineOne.setMaterialConfigs(new MaterialConfigs(materialOne));
        PipelineConfig pipelineTwo = config.pipelineConfigByName(new CaseInsensitiveString("two"));
        pipelineTwo.setMaterialConfigs(new MaterialConfigs(materialTwo));

        pipelineOne.materialConfigs().validate(ConfigSaveValidationContext.forChain(config));

        assertThat(pipelineOne.materialConfigs().get(0).errors().on(ScmMaterialConfig.AUTO_UPDATE),
                is("The material of type Mercurial (http://url1) is used elsewhere with a different value for autoUpdate (poll for changes). Those values should be the same. Pipelines:\n one (auto update disabled),\n two (auto update enabled)"));
    }

    @Test
    public void shouldAllowModifyingTheAutoUpdateFieldOfMaterials() throws Exception {
        GitMaterialConfig gitMaterial = git("https://url", "master");
        gitMaterial.setAutoUpdate(true);

        GitMaterialConfig modifiedGitMaterial = git("https://url", "master");
        modifiedGitMaterial.setAutoUpdate(false);

        MaterialConfigs configs = new MaterialConfigs();
        configs.add(gitMaterial);

        CruiseConfig config = GoConfigMother.configWithPipelines("one");
        PipelineConfig pipelineOne = config.pipelineConfigByName(new CaseInsensitiveString("one"));
        pipelineOne.setMaterialConfigs(new MaterialConfigs(modifiedGitMaterial));

        configs.validate(ConfigSaveValidationContext.forChain(config));
        assertTrue(gitMaterial.errors().isEmpty());
    }

    @Test
    public void shouldNotRunMultipleMaterialsValidationIfPipelineContainsOnlyOneMaterial() {
        CruiseConfig config = GoConfigMother.configWithPipelines("one");
        PipelineConfig pipelineOne = config.pipelineConfigByName(new CaseInsensitiveString("one"));

        SvnMaterialConfig svnMaterialConfig = MaterialConfigsMother.svnMaterialConfig();
        svnMaterialConfig.setFolder(null);
        pipelineOne.setMaterialConfigs(new MaterialConfigs(svnMaterialConfig));

        pipelineOne.materialConfigs().validate(ConfigSaveValidationContext.forChain(config));

        assertThat(svnMaterialConfig.errors().toString(), svnMaterialConfig.errors().getAll().size(), is(0));
    }

    @Test
    public void shouldSetSvnConfigAttributesForMaterial() {
        MaterialConfigs materialConfigs = new MaterialConfigs();

        Map<String, Object> svnAttrMap = new HashMap<>();
        svnAttrMap.put(SvnMaterialConfig.URL, "foo");
        svnAttrMap.put(SvnMaterialConfig.USERNAME, "bar");
        svnAttrMap.put(SvnMaterialConfig.PASSWORD, "baz");
        svnAttrMap.put(SvnMaterialConfig.CHECK_EXTERNALS, false);

        Map<String, Object> attributeMap = new HashMap<>();
        attributeMap.put(AbstractMaterialConfig.MATERIAL_TYPE, SvnMaterialConfig.TYPE);
        attributeMap.put(SvnMaterialConfig.TYPE, svnAttrMap);
        materialConfigs.setConfigAttributes(attributeMap);

        assertThat(materialConfigs.first(), is(svn("foo", "bar", "baz", false)));
    }

    @Test
    public void shouldSetTfsConfigAttributesForMaterial() {
        MaterialConfigs materialConfigs = new MaterialConfigs();

        Map<String, String> tfsAttrMap = new HashMap<>();
        tfsAttrMap.put(TfsMaterialConfig.URL, "foo");
        tfsAttrMap.put(TfsMaterialConfig.USERNAME, "bar");
        tfsAttrMap.put(TfsMaterialConfig.PASSWORD, "baz");
        tfsAttrMap.put(TfsMaterialConfig.PROJECT_PATH, "to_hell");
        tfsAttrMap.put(TfsMaterialConfig.MATERIAL_NAME, "crapy_material");
        tfsAttrMap.put(TfsMaterialConfig.DOMAIN, "CORPORATE");

        Map<String, Object> attributeMap = new HashMap<>();
        attributeMap.put(AbstractMaterialConfig.MATERIAL_TYPE, TfsMaterialConfig.TYPE);
        attributeMap.put(TfsMaterialConfig.TYPE, tfsAttrMap);
        materialConfigs.setConfigAttributes(attributeMap);

        TfsMaterialConfig tfsMaterialConfig = tfs(new GoCipher(), new UrlArgument("foo"), "bar", "CORPORATE", "baz", "to_hell");
        tfsMaterialConfig.setName(new CaseInsensitiveString("crapy_material"));
        assertThat(materialConfigs.first(), is(tfsMaterialConfig));
        assertThat(tfsMaterialConfig.getPassword(), is("baz"));
    }

    @Test
    public void shouldClearExistingAndSetHgConfigAttributesForMaterial() {
        MaterialConfigs materialConfigs = new MaterialConfigs();
        materialConfigs.add(hg("", null));
        materialConfigs.add(svn("", "", "", false));

        Map<String, String> hashMap = new HashMap<>();
        hashMap.put(HgMaterialConfig.URL, "foo");

        Map<String, Object> attributeMap = new HashMap<>();
        attributeMap.put(AbstractMaterialConfig.MATERIAL_TYPE, HgMaterialConfig.TYPE);
        attributeMap.put(HgMaterialConfig.TYPE, hashMap);
        materialConfigs.setConfigAttributes(attributeMap);

        assertThat(materialConfigs.size(), is(1));
        assertThat(materialConfigs.first(), is(hg("foo", null)));
    }

    @Test
    public void shouldSetGitConfigAttributesForMaterial() {
        MaterialConfigs materialConfigs = new MaterialConfigs();

        Map<String, String> hashMap = new HashMap<>();
        hashMap.put(GitMaterialConfig.URL, "foo");
        hashMap.put(GitMaterialConfig.BRANCH, "master");

        HashMap<String, Object> attributeMap = new HashMap<>();
        attributeMap.put(AbstractMaterialConfig.MATERIAL_TYPE, GitMaterialConfig.TYPE);
        attributeMap.put(GitMaterialConfig.TYPE, hashMap);
        materialConfigs.setConfigAttributes(attributeMap);

        assertThat(materialConfigs.size(), is(1));
        GitMaterialConfig expected = git("foo");
        expected.setConfigAttributes(Map.of(GitMaterialConfig.BRANCH, "master"));
        assertThat(materialConfigs.first(), is(expected));
    }

    @Test
    public void shouldSetP4ConfigAttributesForMaterial() {
        MaterialConfigs materialConfigs = new MaterialConfigs();

        Map<String, String> hashMap = new HashMap<>();
        hashMap.put(P4MaterialConfig.SERVER_AND_PORT, "localhost:1666");
        hashMap.put(P4MaterialConfig.USERNAME, "username");
        hashMap.put(P4MaterialConfig.PASSWORD, "password");
        hashMap.put(P4MaterialConfig.VIEW, "foo");

        Map<String, Object> attributeMap = new HashMap<>();
        attributeMap.put(AbstractMaterialConfig.MATERIAL_TYPE, P4MaterialConfig.TYPE);
        attributeMap.put(P4MaterialConfig.TYPE, hashMap);
        materialConfigs.setConfigAttributes(attributeMap);

        assertThat(materialConfigs.size(), is(1));
        P4MaterialConfig expected = p4("localhost:1666", "foo", "username");
        expected.setPassword("password");
        assertThat(materialConfigs.first(), is(expected));
    }

    @Test
    public void shouldSetDependencyMaterialConfigAttributesForMaterial() {
        MaterialConfigs materialConfigs = new MaterialConfigs();

        Map<String, String> hashMap = new HashMap<>();
        hashMap.put(DependencyMaterialConfig.PIPELINE_STAGE_NAME, "blah [foo]");

        Map<String, Object> attributeMap = new HashMap<>();
        attributeMap.put(AbstractMaterialConfig.MATERIAL_TYPE, DependencyMaterialConfig.TYPE);
        attributeMap.put(DependencyMaterialConfig.TYPE, hashMap);
        materialConfigs.setConfigAttributes(attributeMap);

        assertThat(materialConfigs.size(), is(1));
        DependencyMaterialConfig expected = new DependencyMaterialConfig(new CaseInsensitiveString("blah"), new CaseInsensitiveString("foo"));
        assertThat(materialConfigs.first(), is(expected));
    }

    @Test
    public void shouldSetPackageMaterialConfigAttributesForMaterial() {
        Map<String, String> hashMap = new HashMap<>();
        String packageId = "some-id";

        hashMap.put(PackageMaterialConfig.PACKAGE_ID, packageId);

        Map<String, Object> attributeMap = new HashMap<>();
        attributeMap.put(AbstractMaterialConfig.MATERIAL_TYPE, PackageMaterialConfig.TYPE);
        attributeMap.put(PackageMaterialConfig.TYPE, hashMap);

        MaterialConfigs materialConfigs = new MaterialConfigs();
        materialConfigs.setConfigAttributes(attributeMap);

        assertThat(materialConfigs.size(), is(1));
        assertThat(((PackageMaterialConfig) materialConfigs.first()).getPackageId(), is(packageId));
    }

    @Test
    public void shouldGetExistingOrDefaultMaterialCorrectly() {
        SvnMaterialConfig svn = svn("http://test.com", false);
        PackageMaterialConfig p1 = new PackageMaterialConfig("p1");
        PackageMaterialConfig p2 = new PackageMaterialConfig("p2");

        assertThat(new MaterialConfigs(svn, p2).getExistingOrDefaultMaterial(p1).getPackageId(), is("p2"));

        assertThat(new MaterialConfigs(svn).getExistingOrDefaultMaterial(p1).getPackageId(), is("p1"));
    }

    @Test
    public void shouldSetPluggableSCMMaterialConfigAttributesForMaterial() {
        String scmId = "scm-id";
        Map<String, String> hashMap = new HashMap<>();
        hashMap.put(PluggableSCMMaterialConfig.SCM_ID, scmId);

        Map<String, Object> attributeMap = new HashMap<>();
        attributeMap.put(AbstractMaterialConfig.MATERIAL_TYPE, PluggableSCMMaterialConfig.TYPE);
        attributeMap.put(PluggableSCMMaterialConfig.TYPE, hashMap);

        MaterialConfigs materialConfigs = new MaterialConfigs();
        materialConfigs.setConfigAttributes(attributeMap);

        assertThat(materialConfigs.size(), is(1));
        assertThat(((PluggableSCMMaterialConfig) materialConfigs.first()).getScmId(), is(scmId));
    }

    @Test
    public void shouldGetExistingOrDefaultPluggableSCMMaterialCorrectly() {
        SvnMaterialConfig svn = svn("http://test.com", false);
        PluggableSCMMaterialConfig pluggableSCMMaterialOne = new PluggableSCMMaterialConfig("scm-id-1");
        PluggableSCMMaterialConfig pluggableSCMMaterialTwo = new PluggableSCMMaterialConfig("scm-id-2");

        assertThat(new MaterialConfigs(svn, pluggableSCMMaterialTwo).getExistingOrDefaultMaterial(pluggableSCMMaterialOne).getScmId(), is("scm-id-2"));

        assertThat(new MaterialConfigs(svn).getExistingOrDefaultMaterial(pluggableSCMMaterialOne).getScmId(), is("scm-id-1"));
    }

    @Test
    public void shouldValidateTree() {
        GitMaterialConfig git = git();
        git.setName(new CaseInsensitiveString("mat-name"));
        SvnMaterialConfig svn = svn("url", true);
        svn.setName(new CaseInsensitiveString("mat-name"));
        P4MaterialConfig p4 = p4();
        TfsMaterialConfig tfs = tfs();
        HgMaterialConfig hg = hg();
        MaterialConfigs materialConfigs = new MaterialConfigs(git, svn, p4, tfs, hg);

        PipelineConfig pipelineConfig = new PipelineConfig(new CaseInsensitiveString("p1"), new MaterialConfigs(svn));
        materialConfigs.validateTree(PipelineConfigSaveValidationContext.forChain(true, "group", new BasicCruiseConfig(new BasicPipelineConfigs(pipelineConfig)), pipelineConfig));
        assertThat(git.errors().on(GitMaterialConfig.MATERIAL_NAME), contains("You have defined multiple materials called 'mat-name'"));
        assertThat(git.errors().on(GitMaterialConfig.URL), is("URL cannot be blank"));
        assertThat(svn.errors().on(SvnMaterialConfig.MATERIAL_NAME), contains("You have defined multiple materials called 'mat-name'"));
        assertThat(p4.errors().on(P4MaterialConfig.VIEW), contains("P4 view cannot be empty."));
        assertThat(tfs.errors().on(TfsMaterialConfig.URL), contains("URL cannot be blank"));
        assertThat(hg.errors().on(HgMaterialConfig.URL), is("URL cannot be blank"));
    }


    @Test
    public void shouldFailValidationInNoMaterialPresent() {
        MaterialConfigs materialConfigs = new MaterialConfigs();
        assertThat(materialConfigs.validateTree(PipelineConfigSaveValidationContext.forChain(true, "group", new PipelineConfig())), is(false));
        assertThat(materialConfigs.errors().firstError(), is("A pipeline must have at least one material"));
    }

    @Test
    public void shouldTellWhetherItHasDependencyOnSpecifiedPipeline() throws Exception {
        CruiseConfig cruiseConfig = new BasicCruiseConfig();
        PipelineConfig pipeline1 = goConfigMother.addPipeline(cruiseConfig, "pipeline1", "stage", "build");
        PipelineConfig pipeline2 = goConfigMother.addPipeline(cruiseConfig, "pipeline2", "stage", "build");
        goConfigMother.setDependencyOn(cruiseConfig, pipeline2, "pipeline1", "stage");

        assertTrue(pipeline2.materialConfigs().hasDependencyMaterial(pipeline1));
        assertFalse(pipeline1.materialConfigs().hasDependencyMaterial(pipeline2));
    }
}
