// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.IO;
using System.Linq;
using System.Text;
using System.Xml;
using Microsoft.Build.Construction;
using Microsoft.Build.Engine.UnitTests.Globbing;
using Microsoft.Build.Evaluation;
using Microsoft.Build.Execution;
using Microsoft.Build.Framework;
using Microsoft.Build.Globbing;
using Microsoft.Build.Shared;
using Shouldly;
using Xunit;
using Xunit.Abstractions;
using GlobResultList = System.Collections.Generic.List<(string, string[], System.Collections.Immutable.ImmutableHashSet<string>, System.Collections.Immutable.ImmutableHashSet<string>)>;
using InvalidProjectFileException = Microsoft.Build.Exceptions.InvalidProjectFileException;
// can't use an actual ProvenanceResult because it points to a ProjectItemElement which is hard to mock.
using ProvenanceResultTupleList = System.Collections.Generic.List<(string, Microsoft.Build.Evaluation.Operation, Microsoft.Build.Evaluation.Provenance, int)>;
using TargetDotNetFrameworkVersion = Microsoft.Build.Utilities.TargetDotNetFrameworkVersion;
using Task = System.Threading.Tasks.Task;
using ToolLocationHelper = Microsoft.Build.Utilities.ToolLocationHelper;

#nullable disable

namespace Microsoft.Build.UnitTests.OM.Definition
{
    /// <summary>
    /// Tests for Project public members
    /// </summary>
    public class Project_Tests : IDisposable
    {
        /// <summary>
        /// Number of characters in a rooted path's prefix.
        /// </summary>
        /// <remarks>
        /// The prefix is "c:\" on Windows, "/" on other OSes.
        /// </remarks>
        private readonly int RootPrefixLength = NativeMethodsShared.IsWindows ? 3 : 1;

        private ITestOutputHelper _output;

        /// <summary>
        /// Clear out the global project collection
        /// </summary>
        public Project_Tests(ITestOutputHelper output)
        {
            _output = output;
            ProjectCollection.GlobalProjectCollection.UnloadAllProjects();
        }

        /// <summary>
        /// Clear out the global project collection
        /// </summary>
        public void Dispose()
        {
            ProjectCollection.GlobalProjectCollection.UnloadAllProjects();
            ProjectCollection.GlobalProjectCollection.Count.ShouldBe(0);

            IDictionary<string, string> globalProperties = ProjectCollection.GlobalProjectCollection.GlobalProperties;
            foreach (string propertyName in globalProperties.Keys)
            {
                ProjectCollection.GlobalProjectCollection.RemoveGlobalProperty(propertyName);
            }

            ProjectCollection.GlobalProjectCollection.GlobalProperties.ShouldBeEmpty();
        }

        private const string ProjectWithItemGroup =
@"<Project ToolsVersion='msbuilddefaulttoolsversion' DefaultTargets='Build' xmlns='msbuildnamespace'>
                  <ItemGroup>
{0}
                  </ItemGroup>
                </Project>
            ";

        /// <summary>
        /// Since when the project file is saved it may be indented we want to make sure the indent characters do not affect the evaluation against empty.
        /// We test here newline, tab, and carriage return.
        /// </summary>
        [Fact]
        [Trait("Category", "serialize")]
        public void VerifyNewLinesAndTabsEvaluateToEmpty()
        {
            MockLogger mockLogger = new MockLogger();

            string projectFileContent = ObjectModelHelpers.CleanupFileContents(@"
                    <Project xmlns='msbuildnamespace'>
                       <PropertyGroup><NewLine>" + Environment.NewLine + Environment.NewLine + "</NewLine></PropertyGroup>" +
                       "<PropertyGroup><Tab>\t\t\t\t</Tab></PropertyGroup>" +
                       "<PropertyGroup><CarriageReturn>\r\r\r\r</CarriageReturn></PropertyGroup>" +
                        @"<PropertyGroup><Message1 Condition =""'$(NewLine)' == ''"">NewLineEvalAsEmpty</Message1></PropertyGroup>
                        <PropertyGroup><Message2 Condition =""'$(Tab)' == ''"">TabEvalAsEmpty</Message2></PropertyGroup>
                        <PropertyGroup><Message3 Condition =""'$(CarriageReturn)' == ''"">CarriageReturnEvalAsEmpty</Message3></PropertyGroup>

                        <Target Name=""BUild"">
                           <Message Text=""$(Message1)"" Importance=""High""/>
                          <Message Text=""$(Message2)"" Importance=""High""/>
                          <Message Text=""$(Message3)"" Importance=""High""/>
                       </Target>
                    </Project>");

            using ProjectRootElementFromString projectRootElementFromString = new(projectFileContent);
            ProjectRootElement xml = projectRootElementFromString.Project;
            Project project = new Project(xml);
            bool result = project.Build(new ILogger[] { mockLogger });
            result.ShouldBeTrue();
            mockLogger.AssertLogContains("NewLineEvalAsEmpty");
            mockLogger.AssertLogContains("TabEvalAsEmpty");
            mockLogger.AssertLogContains("CarriageReturnEvalAsEmpty");
        }

        /// <summary>
        /// Make sure if we build a project and specify no loggers that the loggers registered on the project collection is the one used.
        /// </summary>
        [Fact]
        [Trait("Category", "serialize")]
        public void LogWithLoggersOnProjectCollection()
        {
            MockLogger mockLogger = new MockLogger();

            string projectFileContent = ObjectModelHelpers.CleanupFileContents(@"
                    <Project xmlns='msbuildnamespace'>
                      <Target Name=""BUild"">
                           <Message Text=""IHaveBeenLogged"" Importance=""High""/>
                       </Target>
                    </Project>");

            using ProjectRootElementFromString projectRootElementFromString = new(projectFileContent);
            ProjectRootElement xml = projectRootElementFromString.Project;
            using ProjectCollection collection = new ProjectCollection();
            collection.RegisterLogger(mockLogger);
            Project project = new Project(xml, null, null, collection);

            bool result = project.Build();
            result.ShouldBeTrue();
            mockLogger.AssertLogContains("IHaveBeenLogged");
        }

        /// <summary>
        /// Make sure if we build a project and specify we specify a custom logger that the custom logger is used instead of the one registered on the project collection.
        /// </summary>
        [Fact]
        [Trait("Category", "serialize")]
        public void LogWithLoggersOnProjectCollectionCustomOneUsed()
        {
            MockLogger mockLogger = new MockLogger();
            MockLogger mockLogger2 = new MockLogger();

            string projectFileContent = ObjectModelHelpers.CleanupFileContents(@"
                    <Project xmlns='msbuildnamespace'>
                      <Target Name=""BUild"">
                           <Message Text=""IHaveBeenLogged"" Importance=""High""/>
                       </Target>
                    </Project>");

            using ProjectRootElementFromString projectRootElementFromString = new(projectFileContent);
            ProjectRootElement xml = projectRootElementFromString.Project;
            using ProjectCollection collection = new ProjectCollection();
            collection.RegisterLogger(mockLogger2);
            Project project = new Project(xml, null, null, collection);

            bool result = project.Build(mockLogger);
            result.ShouldBeTrue();
            mockLogger.AssertLogContains("IHaveBeenLogged");
            mockLogger2.AssertLogDoesntContain("IHaveBeenLogged");
        }

        /// <summary>
        /// Load a project from a file path
        /// </summary>
        [Fact]
        public void BasicFromFile()
        {
            string file = null;

            try
            {
                file = FileUtilities.GetTemporaryFileName();

                string content = GetSampleProjectContent();
                File.WriteAllText(file, content);

                Project project = new Project(file);

                VerifyContentOfSampleProject(project);
            }
            finally
            {
                File.Delete(file);
            }
        }

        /// <summary>
        /// Load a project from a file path that has valid XML that does not
        /// evaluate successfully; then trying again after fixing the file should succeed.
        /// </summary>
        [Fact]
        public void FailedEvaluationClearsXmlCache()
        {
            string file = Path.GetTempPath() + Path.DirectorySeparatorChar + Guid.NewGuid().ToString("N");

            try
            {
                var xml = ProjectRootElement.Create(file);
                xml.AddItem("i", "i1").Condition = "typo in ''condition''";
                xml.Save();

                Project project = null;
                try
                {
                    project = new Project(file);
                }
                catch (InvalidProjectFileException ex)
                {
                    Console.WriteLine(ex.Message);
                }

                // Verify that we don't now have invalid project XML left in the cache
                // by writing out valid project XML and trying again;
                // Don't save through the OM or the cache would get updated; do it directly
                File.WriteAllText(file, ObjectModelHelpers.CleanupFileContents(@"<Project xmlns='msbuildnamespace'/>"));

                project = new Project(file); // should not throw
            }
            finally
            {
                File.Delete(file);
            }
        }

        /// <summary>
        /// Reading from an XMLReader that has no content should throw the correct
        /// exception
        /// </summary>
        [Fact]
        public void ReadFromEmptyReader1()
        {
            Should.Throw<InvalidProjectFileException>(() =>
            {
                XmlReader reader = XmlReader.Create(new StringReader(String.Empty));
                ProjectRootElement.Create(reader);
            });
        }
        /// <summary>
        /// Reading from an XMLReader that has no content should throw the correct
        /// exception
        /// </summary>
        [Fact]
        public void ReadFromEmptyReader2()
        {
            Should.Throw<InvalidProjectFileException>(() =>
            {
                XmlReader reader = XmlReader.Create(new StringReader(String.Empty));
                Project project = new Project(reader);
            });
        }
        /// <summary>
        /// Reading from an XMLReader that has no content should throw the correct
        /// exception
        /// </summary>
        [Fact]
        public void ReadFromEmptyReader3()
        {
            Should.Throw<InvalidProjectFileException>(() =>
            {
                // Variation, we have a reader but it's already read
                XmlReader reader = XmlReader.Create(new StringReader(ProjectRootElement.Create().RawXml));

                while (reader.Read())
                {
                }

                Project project = (new ProjectCollection()).LoadProject(reader);
            });
        }

        /// <summary>
        /// Reading from an XMLReader that was closed should throw the correct
        /// exception
        /// </summary>
        [Fact]
        public void ReadFromClosedReader()
        {
            Should.Throw<InvalidProjectFileException>(() =>
            {
                XmlReader reader = XmlReader.Create(new StringReader(String.Empty));
                reader.Dispose();
                Project project = new Project(reader);
            });
        }

        /// <summary>
        /// Reading from an XMLReader that has TWO valid root elements should work
        /// if it's already read past the first one.
        /// </summary>
        [Fact]
        public void ReadFromReaderTwoDocs()
        {
            string emptyProject = ObjectModelHelpers.CleanupFileContents(@"<Project ToolsVersion=""msbuilddefaulttoolsversion"" xmlns=""msbuildnamespace""/>");
            using XmlReader reader = XmlReader.Create(new StringReader(emptyProject + emptyProject), new XmlReaderSettings { ConformanceLevel = ConformanceLevel.Fragment });

            reader.ReadToFollowing("Project");
            reader.Read();

            Project project2 = new Project(reader);

            reader.Read().ShouldBeFalse();
        }

        /// <summary>
        /// Import does not exist. Default case is an exception.
        /// </summary>
        [Fact]
        public void ImportDoesNotExistDefaultSettings()
        {
            Should.Throw<InvalidProjectFileException>(() =>
            {
                ProjectRootElement xml = ProjectRootElement.Create();
                xml.AddImport("__nonexistent__");

                Project project = new Project(xml);
            });
        }
        /// <summary>
        /// Import gives invalid uri exception
        /// </summary>
        [Fact]
        public void ImportInvalidUriFormat()
        {
            Should.Throw<InvalidProjectFileException>(() =>
            {
                ProjectRootElement xml = ProjectRootElement.Create();
                xml.AddImport(@"//MSBuildExtensionsPath32)\4.0\Microsoft.VisualStudioVersion.v11.Common.props");

                Project project = new Project(xml);
            });
        }
        /// <summary>
        /// Necessary but not sufficient for MSBuild evaluation to be thread safe.
        /// </summary>
        [Fact]
        public void ConcurrentLoadDoesNotCrash()
        {
            var tasks = new Task[500];

            for (int i = 0; i < tasks.Length; i++)
            {
                tasks[i] = Task.Factory.StartNew(delegate () { new Project(); }); // Should not throw
            }

            Task.WaitAll(tasks);
        }

        /// <summary>
        /// Import does not exist but ProjectLoadSettings.IgnoreMissingImports was set
        /// </summary>
        [Fact]
        public void ImportDoesNotExistIgnoreMissingImports()
        {
            ProjectRootElement xml = ProjectRootElement.Create();

            xml.AddProperty("p", "1");
            xml.AddImport("__nonexistent__");
            xml.AddProperty("q", "$(p)");

            using var collection = new ProjectCollection();
            Project project = new Project(xml, null, null, collection, ProjectLoadSettings.IgnoreMissingImports);

            // Make sure some evaluation did occur
            project.GetPropertyValue("q").ShouldBe("1");
        }

        /// <summary>
        /// When we try and access the ImportsIncludingDuplicates property on the project without setting
        /// the correct projectloadsetting flag, we expect an invalidoperationexception.
        /// </summary>
        [Fact]
        public void TryImportsIncludingDuplicatesExpectException()
        {
            Should.Throw<InvalidOperationException>(() =>
            {
                ProjectRootElement xml = ProjectRootElement.Create();
                Project project = new Project(xml, null, null, new ProjectCollection(), ProjectLoadSettings.IgnoreMissingImports);
                IList<ResolvedImport> imports = project.ImportsIncludingDuplicates;
                imports.ShouldBeEmpty();
            });
        }
        /// <summary>
        /// Import self ignored
        /// </summary>
        [Fact]
        public void ImportSelfIgnored()
        {
            string file = null;

            try
            {
                using ProjectCollection collection = new ProjectCollection();
                MockLogger logger = new MockLogger();
                collection.RegisterLogger(logger);

                Project project = new Project(collection);
                project.Xml.AddImport("$(MSBuildProjectFullPath)");

                file = FileUtilities.GetTemporaryFileName();
                project.Save(file);
                project.ReevaluateIfNecessary();

                logger.AssertLogContains("MSB4210"); // selfimport
            }
            finally
            {
                File.Delete(file);
            }
        }

        /// <summary>
        /// Import self indirectly ignored
        /// </summary>
        [Fact]
        public void ImportSelfIndirectIgnored()
        {
            string file = null;
            string file2 = null;

            try
            {
                using ProjectCollection collection = new ProjectCollection();
                MockLogger logger = new MockLogger();
                collection.RegisterLogger(logger);

                file = FileUtilities.GetTemporaryFileName();
                file2 = FileUtilities.GetTemporaryFileName();
                Project project = new Project(collection);
                project.Xml.AddImport(file2);
                project.Save(file);

                Project project2 = new Project(collection);
                project2.Xml.AddImport(file);
                project2.Save(file2);

                project.ReevaluateIfNecessary();

                logger.AssertLogContains("MSB4210"); // selfimport
            }
            finally
            {
                File.Delete(file);
                File.Delete(file2);
            }
        }

        /// <summary>
        /// Double import ignored
        /// </summary>
        [Fact]
        public void DoubleImportIgnored()
        {
            string file = null;
            string file2 = null;

            try
            {
                using ProjectCollection collection = new ProjectCollection();
                MockLogger logger = new MockLogger();
                collection.RegisterLogger(logger);

                file = FileUtilities.GetTemporaryFileName();
                file2 = FileUtilities.GetTemporaryFileName();
                Project project = new Project(collection);
                project.Xml.AddImport(file2);
                project.Xml.AddImport(file2);
                project.Save(file);

                Project project2 = new Project(collection);
                project2.Save(file2);

                project.ReevaluateIfNecessary();

                logger.AssertLogContains("MSB4011"); // duplicate import
            }
            finally
            {
                File.Delete(file);
                File.Delete(file2);
            }
        }

        /// <summary>
        /// Double import ignored
        /// </summary>
        [Fact]
        public void DoubleImportIndirectIgnored()
        {
            string file = null;
            string file2 = null;
            string file3 = null;

            try
            {
                using ProjectCollection collection = new ProjectCollection();
                MockLogger logger = new MockLogger();
                collection.RegisterLogger(logger);

                file = FileUtilities.GetTemporaryFileName();
                file2 = FileUtilities.GetTemporaryFileName();
                file3 = FileUtilities.GetTemporaryFileName();

                Project project = new Project(collection);
                project.Xml.AddImport(file2);
                project.Xml.AddImport(file3);
                project.Save(file);

                Project project2 = new Project(collection);
                project.Xml.AddImport(file3);
                project2.Save(file2);

                Project project3 = new Project(collection);
                project3.Save(file3);

                project.ReevaluateIfNecessary();

                logger.AssertLogContains("MSB4011"); // duplicate import
            }
            finally
            {
                File.Delete(file);
                File.Delete(file2);
                File.Delete(file3);
            }
        }

        /// <summary>
        /// Basic created from backing XML
        /// </summary>
        [Fact]
        public void BasicFromXml()
        {
            ProjectRootElement xml = GetSampleProjectRootElement();
            Project project = new Project(xml);

            VerifyContentOfSampleProject(project);
        }

        /// <summary>
        /// Test Project from an XML with an import.
        /// Also verify the Imports collection on the evaluated Project.
        /// </summary>
        [Fact]
        public void BasicFromXmlFollowImport()
        {
            string importContent = ObjectModelHelpers.CleanupFileContents(@"
                    <Project xmlns='msbuildnamespace'>
                        <PropertyGroup>
                            <p2>v3</p2>
                        </PropertyGroup>
                        <ItemGroup>
                            <i Include='i4'/>
                        </ItemGroup>
                        <Target Name='t2'>
                            <task/>
                        </Target>
                    </Project>");

            string importPath = ObjectModelHelpers.CreateFileInTempProjectDirectory("import.targets", importContent);

            string projectFileContent = ObjectModelHelpers.CleanupFileContents(@"
                    <Project xmlns='msbuildnamespace'>
                        <PropertyGroup Condition=""'$(Configuration)'=='Foo'"">
                            <p>v1</p>
                        </PropertyGroup>
                        <PropertyGroup Condition=""'$(Configuration)'!='Foo'"">
                            <p>v2</p>
                        </PropertyGroup>
                        <PropertyGroup>
                            <p2>X$(p)</p2>
                        </PropertyGroup>
                        <ItemGroup>
                            <i Condition=""'$(Configuration)'=='Foo'"" Include='i0'/>
                            <i Include='i1'/>
                            <i Include='$(p)X;i3'/>
                        </ItemGroup>
                        <Target Name='t'>
                            <task/>
                        </Target>
                        <Import Project='{0}'/>
                    </Project>");

            projectFileContent = string.Format(projectFileContent, importPath);

            using ProjectRootElementFromString projectRootElementFromString = new(projectFileContent);
            ProjectRootElement xml = projectRootElementFromString.Project;
            Project project = new Project(xml);

            project.GetPropertyValue("p2").ShouldBe("v3");

            List<ProjectItem> items = Helpers.MakeList(project.GetItems("i"));
            items.Count.ShouldBe(4);
            items[0].EvaluatedInclude.ShouldBe("i1");
            items[1].EvaluatedInclude.ShouldBe("v2X");
            items[2].EvaluatedInclude.ShouldBe("i3");
            items[3].EvaluatedInclude.ShouldBe("i4");

            IList<ResolvedImport> imports = project.Imports;
            imports.ShouldHaveSingleItem();
            object.ReferenceEquals(imports.First().ImportingElement, xml.Imports.ElementAt(0)).ShouldBeTrue();

            // We can take advantage of the fact that we will get the same ProjectRootElement from the cache if we try to
            // open it with a path; get that and then compare it to what project.Imports gave us.
            object.ReferenceEquals(imports.First().ImportedProject, ProjectRootElement.Open(importPath)).ShouldBeTrue();

            // Test the logical project iterator
            List<ProjectElement> logicalElements = new List<ProjectElement>(project.GetLogicalProject());

            logicalElements.Count.ShouldBe(18);

            ObjectModelHelpers.DeleteTempProjectDirectory();
        }

        /// <summary>
        /// Get items, transforms use correct directory base, ie., the project folder
        /// </summary>
        [Fact]
        public void TransformsUseCorrectDirectory_Basic()
        {
            string file = null;

            string projectFileContent = ObjectModelHelpers.CleanupFileContents(@"
                     <Project xmlns='msbuildnamespace'>
                         <ItemGroup>
                             <IntermediateAssembly Include='obj\i386\foo.dll'/>
                             <BuiltProjectOutputGroupKeyOutput Include=""@(IntermediateAssembly->'%(FullPath)')""/>
                         </ItemGroup>
                     </Project>");

            using ProjectRootElementFromString projectRootElementFromString = new(projectFileContent);
            ProjectRootElement xml = projectRootElementFromString.Project;

            Project project = new Project(xml);

            try
            {
                file = FileUtilities.GetTemporaryFileName();
                project.Save(file);
                project.ReevaluateIfNecessary();

                project.GetItems("BuiltProjectOutputGroupKeyOutput").First().EvaluatedInclude.ShouldBe(
                    Path.Combine(FileUtilities.TempFileDirectory, "obj", "i386", "foo.dll"));
            }
            finally
            {
                File.Delete(file);
            }
        }

        /// <summary>
        /// Get items, transforms use correct directory base, ie., the current
        /// directory at the time of load for a project that was not yet saved
        /// </summary>
        [Fact]
        public void TransformsUseCorrectDirectory_Basic_NotSaved()
        {
            string projectFileContent = ObjectModelHelpers.CleanupFileContents(@"
                    <Project xmlns='msbuildnamespace'>
                        <ItemGroup>
                            <IntermediateAssembly Include='obj\i386\foo.dll'/>
                            <BuiltProjectOutputGroupKeyOutput Include=""@(IntermediateAssembly->'%(FullPath)')""/>
                        </ItemGroup>
                    </Project>");

            using ProjectRootElementFromString projectRootElementFromString = new(projectFileContent);
            ProjectRootElement xml = projectRootElementFromString.Project;

            Project project = new Project(xml);
            ProjectInstance projectInstance = new ProjectInstance(xml);

            if (NativeMethodsShared.IsWindows)
            {
                project.GetItems("BuiltProjectOutputGroupKeyOutput").First().EvaluatedInclude.ShouldBe(Path.Combine(Directory.GetCurrentDirectory(), @"obj\i386\foo.dll"));
                projectInstance.GetItems("BuiltProjectOutputGroupKeyOutput").First().EvaluatedInclude.ShouldBe(Path.Combine(Directory.GetCurrentDirectory(), @"obj\i386\foo.dll"));
            }
            else
            {
                project.GetItems("BuiltProjectOutputGroupKeyOutput").First().EvaluatedInclude.ShouldBe(Path.Combine(Directory.GetCurrentDirectory(), @"obj/i386/foo.dll"));
                projectInstance.GetItems("BuiltProjectOutputGroupKeyOutput").First().EvaluatedInclude.ShouldBe(Path.Combine(Directory.GetCurrentDirectory(), @"obj/i386/foo.dll"));
            }
        }

        /// <summary>
        /// Directory transform uses project directory
        /// </summary>
        [Fact]
        public void TransformsUseCorrectDirectory_DirectoryTransform()
        {
            string file = null;

            string projectFileContent = ObjectModelHelpers.CleanupFileContents(@"<Project xmlns='msbuildnamespace'>
                        <ItemGroup>
                            <IntermediateAssembly Include='obj\i386\foo.dll'/>
                            <BuiltProjectOutputGroupKeyOutput Include=""@(IntermediateAssembly->'%(Directory)')""/>
                        </ItemGroup>
                    </Project>");

            using ProjectRootElementFromString projectRootElementFromString = new(projectFileContent);
            ProjectRootElement xml = projectRootElementFromString.Project;

            try
            {
                file = FileUtilities.GetTemporaryFile();
                xml.FullPath = file;

                Project project = new Project(xml);
                ProjectInstance projectInstance = new ProjectInstance(xml);

                project.GetItems("BuiltProjectOutputGroupKeyOutput").First().EvaluatedInclude.ShouldBe(Path.Combine(FileUtilities.TempFileDirectory, "obj", "i386").Substring(RootPrefixLength) + Path.DirectorySeparatorChar);
                projectInstance.GetItems("BuiltProjectOutputGroupKeyOutput").First().EvaluatedInclude.ShouldBe(Path.Combine(FileUtilities.TempFileDirectory, "obj", "i386").Substring(RootPrefixLength) + Path.DirectorySeparatorChar);
            }
            finally
            {
                File.Delete(file);
            }
        }

        /// <summary>
        /// Directory item function uses project directory
        /// </summary>
        [Fact]
        public void TransformsUseCorrectDirectory_DirectoryItemFunction()
        {
            string file = null;

            string projectFileContent = ObjectModelHelpers.CleanupFileContents(@"
                    <Project xmlns='msbuildnamespace'>
                        <ItemGroup>
                            <IntermediateAssembly Include='obj\i386\foo.dll'/>
                            <BuiltProjectOutputGroupKeyOutput Include=""@(IntermediateAssembly->Directory())""/>
                        </ItemGroup>
                    </Project>");

            using ProjectRootElementFromString projectRootElementFromString = new(projectFileContent);
            ProjectRootElement xml = projectRootElementFromString.Project;

            try
            {
                file = FileUtilities.GetTemporaryFile();
                xml.FullPath = file;

                Project project = new Project(xml);
                ProjectInstance projectInstance = new ProjectInstance(xml);

                project.GetItems("BuiltProjectOutputGroupKeyOutput").First().EvaluatedInclude.ShouldBe(Path.Combine(FileUtilities.TempFileDirectory, "obj", "i386").Substring(RootPrefixLength) + Path.DirectorySeparatorChar);
                projectInstance.GetItems("BuiltProjectOutputGroupKeyOutput").First().EvaluatedInclude.ShouldBe(Path.Combine(FileUtilities.TempFileDirectory, "obj", "i386").Substring(RootPrefixLength) + Path.DirectorySeparatorChar);
            }
            finally
            {
                File.Delete(file);
            }
        }

        /// <summary>
        /// Directory item function uses project directory
        /// </summary>
        [Fact]
        public void TransformsUseCorrectDirectory_DirectoryNameItemFunction()
        {
            string file = null;

            string projectFileContent = ObjectModelHelpers.CleanupFileContents(@"
                    <Project xmlns='msbuildnamespace'>
                        <ItemGroup>
                            <IntermediateAssembly Include='obj" + Path.DirectorySeparatorChar + "i386"
                                                                               + Path.DirectorySeparatorChar
                                                                               + @"foo.dll'/>
                            <BuiltProjectOutputGroupKeyOutput Include=""@(IntermediateAssembly->DirectoryName())""/>
                        </ItemGroup>
                    </Project>");

            using ProjectRootElementFromString projectRootElementFromString = new(projectFileContent);
            ProjectRootElement xml = projectRootElementFromString.Project;

            try
            {
                file = FileUtilities.GetTemporaryFile();
                xml.FullPath = file;

                Project project = new Project(xml);
                ProjectInstance projectInstance = new ProjectInstance(xml);

                // Should be the full path to the directory
                project.GetItems("BuiltProjectOutputGroupKeyOutput").First().EvaluatedInclude.ShouldBe(Path.Combine(FileUtilities.TempFileDirectory /* remove c:\ */, "obj", "i386"));
                projectInstance.GetItems("BuiltProjectOutputGroupKeyOutput").First().EvaluatedInclude.ShouldBe(Path.Combine(FileUtilities.TempFileDirectory /* remove c:\ */, "obj", "i386"));
            }
            finally
            {
                File.Delete(file);
            }
        }

        /// <summary>
        /// Global properties accessor
        /// </summary>
        [Fact]
        public void GetGlobalProperties()
        {
            ProjectRootElement xml = GetSampleProjectRootElement();
            var globalProperties = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
            globalProperties.Add("g1", "v1");
            globalProperties.Add("g2", "v2");
            Project project = new Project(xml, globalProperties, null);

            project.GlobalProperties["g1"].ShouldBe("v1");
            project.GlobalProperties["g2"].ShouldBe("v2");
        }

        /// <summary>
        /// Global properties are cloned when passed in:
        /// subsequent changes have no effect
        /// </summary>
        [Fact]
        public void GlobalPropertiesCloned()
        {
            ProjectRootElement xml = GetSampleProjectRootElement();
            var globalProperties = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
            globalProperties.Add("g1", "v1");
            Project project = new Project(xml, globalProperties, null);

            globalProperties.Add("g2", "v2");

            project.GlobalProperties["g1"].ShouldBe("v1");
            project.GlobalProperties.ContainsKey("g2").ShouldBeFalse();
        }

        /// <summary>
        /// Global properties accessor when no global properties
        /// </summary>
        [Fact]
        public void GetGlobalPropertiesNone()
        {
            ProjectRootElement xml = GetSampleProjectRootElement();
            Project project = new Project(xml);

            project.GlobalProperties.ShouldBeEmpty();
        }

        /// <summary>
        /// Changing global properties should make the project a candidate
        /// for reevaluation.
        /// </summary>
        [Fact]
        public void ChangeGlobalProperties()
        {
            Project project = new Project();
            ProjectPropertyElement propertyElement = project.Xml.AddProperty("p", "v0");
            propertyElement.Condition = "'$(g)'=='v1'";
            project.ReevaluateIfNecessary();
            project.GetPropertyValue("p").ShouldBe(string.Empty);

            project.SetGlobalProperty("g", "v1").ShouldBeTrue();
            project.IsDirty.ShouldBeTrue();
            project.ReevaluateIfNecessary();
            project.GetPropertyValue("p").ShouldBe("v0");
            project.GlobalProperties["g"].ShouldBe("v1");
        }

        /// <summary>
        /// Changing global property after reevaluation should not crash
        /// </summary>
        [Fact]
        public void ChangeGlobalPropertyAfterReevaluation()
        {
            Project project = new Project();
            project.SetGlobalProperty("p", "v1");
            project.ReevaluateIfNecessary();
            project.SetGlobalProperty("p", "v2");

            project.GetPropertyValue("p").ShouldBe("v2");
            project.GetProperty("p").IsGlobalProperty.ShouldBeTrue();
        }

        /// <summary>
        /// Test the SkipEvaluation functionality of ReevaluateIfNecessary
        /// </summary>
        [Fact]
        public void SkipEvaluation()
        {
            Project project = new Project();
            project.SetGlobalProperty("p", "v1");
            project.ReevaluateIfNecessary();
            project.GetPropertyValue("p").ShouldBe("v1");

            project.SkipEvaluation = true;
            ProjectPropertyElement propertyElement = project.Xml.AddProperty("p1", "v0");
            propertyElement.Condition = "'$(g)'=='v1'";
            project.SetGlobalProperty("g", "v1");
            project.ReevaluateIfNecessary();
            project.GetPropertyValue("p1").ShouldBe(string.Empty);

            project.SkipEvaluation = false;
            project.SetGlobalProperty("g", "v1");
            project.ReevaluateIfNecessary();
            project.GetPropertyValue("p1").ShouldBe("v0");
        }

        /// <summary>
        /// Setting property with same name as global property but after reevaluation should error
        /// because the property is global, not fail with null reference exception
        /// </summary>
        [Fact]
        public void ChangeGlobalPropertyAfterReevaluation2()
        {
            Should.Throw<InvalidOperationException>(() =>
            {
                Project project = new Project();
                project.SetGlobalProperty("p", "v1");
                project.ReevaluateIfNecessary();
                project.SetProperty("p", "v2");
            });
        }
        /// <summary>
        /// Setting environment property should create a real property
        /// </summary>
        [Fact]
        public void ChangeEnvironmentProperty()
        {
            Project project = new Project();
            project.SetProperty("computername", "v1");

            project.GetPropertyValue("computername").ShouldBe("v1");
            project.IsDirty.ShouldBeTrue();

            project.ReevaluateIfNecessary();

            project.GetPropertyValue("computername").ShouldBe("v1");
        }

        /// <summary>
        /// Setting a reserved property through the project should error nicely
        /// </summary>
        [Fact]
        public void SetReservedPropertyThroughProject()
        {
            Should.Throw<InvalidOperationException>(() =>
            {
                Project project = new Project();
                project.SetProperty("msbuildprojectdirectory", "v1");
            });
        }
        /// <summary>
        /// Changing global properties with some preexisting.
        /// </summary>
        [Fact]
        public void ChangeGlobalPropertiesPreexisting()
        {
            Dictionary<string, string> initial = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
            initial.Add("p0", "v0");
            initial.Add("p1", "v1");
            Project project = new Project(ProjectRootElement.Create(), initial, null);
            ProjectPropertyElement propertyElement = project.Xml.AddProperty("pp", "vv");
            propertyElement.Condition = "'$(p0)'=='v0' and '$(p1)'=='v1b'";
            project.ReevaluateIfNecessary();
            project.GetPropertyValue("pp").ShouldBe(string.Empty);

            project.SetGlobalProperty("p1", "v1b");
            project.IsDirty.ShouldBeTrue();
            project.ReevaluateIfNecessary();
            project.GetPropertyValue("pp").ShouldBe("vv");
            project.GlobalProperties["p0"].ShouldBe("v0");
            project.GlobalProperties["p1"].ShouldBe("v1b");
        }

        /// <summary>
        /// Changing global properties with some preexisting from the project collection.
        /// Should not modify those on the project collection.
        /// </summary>
#if FEATURE_INSTALLED_MSBUILD
        [Fact]
#else
        [Fact(Skip = "https://github.com/dotnet/msbuild/issues/276")]
#endif
        public void ChangeGlobalPropertiesInitiallyFromProjectCollection()
        {
            Dictionary<string, string> initial = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
            initial.Add("p0", "v0");
            initial.Add("p1", "v1");
            using ProjectCollection collection = new ProjectCollection(initial, null, ToolsetDefinitionLocations.ConfigurationFile);
            Project project = new Project(collection);
            ProjectPropertyElement propertyElement = project.Xml.AddProperty("pp", "vv");
            propertyElement.Condition = "'$(p0)'=='v0' and '$(p1)'=='v1b'";
            project.ReevaluateIfNecessary();
            project.GetPropertyValue("pp").ShouldBe(string.Empty);

            project.SetGlobalProperty("p1", "v1b");
            project.IsDirty.ShouldBeTrue();
            project.ReevaluateIfNecessary();
            project.GetPropertyValue("pp").ShouldBe("vv");
            collection.GlobalProperties["p0"].ShouldBe("v0");
            collection.GlobalProperties["p1"].ShouldBe("v1");
        }

        /// <summary>
        /// Changing global property to the same value should not dirty the project.
        /// </summary>
        [Fact]
        public void ChangeGlobalPropertiesSameValue()
        {
            Project project = new Project();
            project.SetGlobalProperty("g", "v1");
            project.IsDirty.ShouldBeTrue();
            project.ReevaluateIfNecessary();

            project.SetGlobalProperty("g", "v1").ShouldBeFalse();
            project.IsDirty.ShouldBeFalse();
        }

        /// <summary>
        /// Removing global properties should make the project a candidate
        /// for reevaluation.
        /// </summary>
        [Fact]
        public void RemoveGlobalProperties()
        {
            Project project = new Project();
            ProjectPropertyElement propertyElement = project.Xml.AddProperty("p", "v0");
            propertyElement.Condition = "'$(g)'==''";
            project.SetGlobalProperty("g", "v1");
            project.ReevaluateIfNecessary();
            project.GetPropertyValue("p").ShouldBe(string.Empty);

            bool existed = project.RemoveGlobalProperty("g");
            existed.ShouldBeTrue();
            project.IsDirty.ShouldBeTrue();
            project.ReevaluateIfNecessary();
            project.GetPropertyValue("p").ShouldBe("v0");
            project.GlobalProperties.ContainsKey("g").ShouldBeFalse();
        }

        /// <summary>
        /// Remove nonexistent global property should return false and not dirty the project.
        /// </summary>
        [Fact]
        public void RemoveNonExistentGlobalProperties()
        {
            Project project = new Project();
            bool existed = project.RemoveGlobalProperty("x");

            existed.ShouldBeFalse();
            project.IsDirty.ShouldBeFalse();
        }

        /// <summary>
        /// ToolsVersion accessor for explicitly specified
        /// </summary>
        [Fact]
        public void GetToolsVersionExplicitlySpecified()
        {
            if (ToolLocationHelper.GetPathToDotNetFramework(TargetDotNetFrameworkVersion.Version35) == null)
            {
                // "Requires 3.5 to be installed"
                return;
            }

            ProjectRootElement xml = GetSampleProjectRootElement();
            Project project = new Project(
                xml,
                new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase),
                ObjectModelHelpers.MSBuildDefaultToolsVersion);

            project.ToolsVersion.ShouldBe(ObjectModelHelpers.MSBuildDefaultToolsVersion);
        }

        /// <summary>
        /// ToolsVersion accessor when none was specified.
        /// Should not return the value on the project element.
        /// </summary>
        [Fact]
        public void GetToolsVersionNoneExplicitlySpecified()
        {
            ProjectRootElement xml = ProjectRootElement.Create();
            xml.ToolsVersion = string.Empty;
            xml.AddTarget("t");

            Project project = new Project(xml);

            project.Xml.ToolsVersion.ShouldBe(string.Empty);

            ObjectModelHelpers.DeleteTempProjectDirectory();
        }

        /// <summary>
        /// ToolsVersion defaults to 4.0
        /// </summary>
        [Fact]
        public void GetToolsVersionFromProject()
        {
            Project project = new Project();

            project.ToolsVersion.ShouldBe(ObjectModelHelpers.MSBuildDefaultToolsVersion);
        }

        /// <summary>
        /// Project.ToolsVersion should be set to ToolsVersion evaluated with,
        /// even if it is subsequently changed on the XML (without reevaluation)
        /// </summary>
        [Fact]
        public void ProjectToolsVersion20Present()
        {
            if (ToolLocationHelper.GetPathToDotNetFramework(TargetDotNetFrameworkVersion.Version20) == null)
            {
                // "Requires 2.0 to be installed"
                return;
            }

            Project project = new Project();
            project.Xml.ToolsVersion = "2.0";
            project.ReevaluateIfNecessary();

            // ... and after all that, we end up defaulting to the current ToolsVersion instead.  There's a way
            // to turn this behavior (new in Dev12) off, but it requires setting an environment variable and
            // clearing some internal state to make sure that the update environment variable is picked up, so
            // there's not a good way of doing it from these deliberately public OM only tests.
            project.ToolsVersion.ShouldBe(ObjectModelHelpers.MSBuildDefaultToolsVersion);

            project.Xml.ToolsVersion = "4.0";

            // Still defaulting to the current ToolsVersion
            project.ToolsVersion.ShouldBe(ObjectModelHelpers.MSBuildDefaultToolsVersion);
        }

        /// <summary>
        /// Project.ToolsVersion should be set to ToolsVersion evaluated with,
        /// even if it is subsequently changed on the XML (without reevaluation)
        /// </summary>
        [Fact]
        public void ProjectToolsVersion20NotPresent()
        {
            if (ToolLocationHelper.GetPathToDotNetFramework(TargetDotNetFrameworkVersion.Version20) != null)
            {
                // "Requires 2.0 to NOT be installed"
                return;
            }

            Project project = new Project();
            project.Xml.ToolsVersion = "2.0";
            project.ReevaluateIfNecessary();

            project.ToolsVersion.ShouldBe(ObjectModelHelpers.MSBuildDefaultToolsVersion);

            project.Xml.ToolsVersion = ObjectModelHelpers.MSBuildDefaultToolsVersion;

            project.ToolsVersion.ShouldBe(ObjectModelHelpers.MSBuildDefaultToolsVersion);
        }

        /// <summary>
        /// $(MSBuildToolsVersion) should be set to ToolsVersion evaluated with,
        /// even if it is subsequently changed on the XML (without reevaluation)
        /// </summary>
        [Fact]
        public void MSBuildToolsVersionProperty()
        {
            if (ToolLocationHelper.GetPathToDotNetFramework(TargetDotNetFrameworkVersion.Version20) == null)
            {
                // "Requires 2.0 to be installed"
                return;
            }

            Project project = new Project();
            project.Xml.ToolsVersion = "2.0";
            project.ReevaluateIfNecessary();

            // ... and after all that, we end up defaulting to the current ToolsVersion instead.  There's a way
            // to turn this behavior (new in Dev12) off, but it requires setting an environment variable and
            // clearing some internal state to make sure that the update environment variable is picked up, so
            // there's not a good way of doing it from these deliberately public OM only tests.
            project.GetPropertyValue("msbuildtoolsversion").ShouldBe(ObjectModelHelpers.MSBuildDefaultToolsVersion);

            project.Xml.ToolsVersion = "4.0";

            // Still current
            project.GetPropertyValue("msbuildtoolsversion").ShouldBe(ObjectModelHelpers.MSBuildDefaultToolsVersion);

            project.ReevaluateIfNecessary();

            // Still current
            project.GetPropertyValue("msbuildtoolsversion").ShouldBe(ObjectModelHelpers.MSBuildDefaultToolsVersion);
        }

        /// <summary>
        /// $(MSBuildToolsVersion) should be set to ToolsVersion evaluated with,
        /// even if it is subsequently changed on the XML (without reevaluation)
        /// </summary>
        [Fact]
        public void MSBuildToolsVersionProperty40()
        {
            Project project = new Project();

            project.GetPropertyValue("msbuildtoolsversion").ShouldBe(ObjectModelHelpers.MSBuildDefaultToolsVersion);
        }

        /// <summary>
        /// It's okay to change ToolsVersion to some apparently bogus value -- the project can be persisted
        /// that way, and maybe later it'll correspond to some known toolset. If the effective ToolsVersion was being
        /// gotten from the attribute, that'll be affected too; and thus might be bogus.
        /// </summary>
        [Fact]
        public void ChangingToolsVersionAttributeToUnrecognizedValue()
        {
            Project project = new Project();

            project.Xml.ToolsVersion = "bogus";

            project.Xml.ToolsVersion.ShouldBe("bogus");
        }

        /// <summary>
        /// Test Project's surfacing of the sub-toolset version
        /// </summary>
        [Fact]
        public void GetSubToolsetVersion()
        {
            string originalVisualStudioVersion = Environment.GetEnvironmentVariable("VisualStudioVersion");

            try
            {
                Environment.SetEnvironmentVariable("VisualStudioVersion", null);

                using var collection = new ProjectCollection();
                Project p = new Project(GetSampleProjectRootElement(), null, ObjectModelHelpers.MSBuildDefaultToolsVersion, collection);

                p.ToolsVersion.ShouldBe(ObjectModelHelpers.MSBuildDefaultToolsVersion);

                Toolset t = p.ProjectCollection.GetToolset(ObjectModelHelpers.MSBuildDefaultToolsVersion);

                p.SubToolsetVersion.ShouldBe(t.DefaultSubToolsetVersion);

                p.GetPropertyValue("VisualStudioVersion").ShouldBe(t.DefaultSubToolsetVersion ?? MSBuildConstants.CurrentVisualStudioVersion);
            }
            finally
            {
                Environment.SetEnvironmentVariable("VisualStudioVersion", originalVisualStudioVersion);
            }
        }

        /// <summary>
        /// Test Project's surfacing of the sub-toolset version when it is overridden by a value in the
        /// environment
        /// </summary>
        [Fact]
        public void GetSubToolsetVersion_FromEnvironment()
        {
            string originalVisualStudioVersion = Environment.GetEnvironmentVariable("VisualStudioVersion");

            try
            {
                Environment.SetEnvironmentVariable("VisualStudioVersion", "ABCD");

                using var collection = new ProjectCollection();
                Project p = new Project(GetSampleProjectRootElement(), null, ObjectModelHelpers.MSBuildDefaultToolsVersion, collection);

                p.ToolsVersion.ShouldBe(ObjectModelHelpers.MSBuildDefaultToolsVersion);
                p.SubToolsetVersion.ShouldBe("ABCD");
                p.GetPropertyValue("VisualStudioVersion").ShouldBe("ABCD");
            }
            finally
            {
                Environment.SetEnvironmentVariable("VisualStudioVersion", originalVisualStudioVersion);
            }
        }

        /// <summary>
        /// Test ProjectInstance's surfacing of the sub-toolset version when it is overridden by a global property
        /// </summary>
        [Fact]
        public void GetSubToolsetVersion_FromProjectGlobalProperties()
        {
            string originalVisualStudioVersion = Environment.GetEnvironmentVariable("VisualStudioVersion");

            try
            {
                Environment.SetEnvironmentVariable("VisualStudioVersion", null);

                IDictionary<string, string> globalProperties = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
                globalProperties.Add("VisualStudioVersion", "ABCDE");

                using var collection = new ProjectCollection();
                Project p = new Project(GetSampleProjectRootElement(), globalProperties, ObjectModelHelpers.MSBuildDefaultToolsVersion, collection);

                p.ToolsVersion.ShouldBe(ObjectModelHelpers.MSBuildDefaultToolsVersion);
                p.SubToolsetVersion.ShouldBe("ABCDE");
                p.GetPropertyValue("VisualStudioVersion").ShouldBe("ABCDE");
            }
            finally
            {
                Environment.SetEnvironmentVariable("VisualStudioVersion", originalVisualStudioVersion);
            }
        }

        /// <summary>
        /// Verify that if a sub-toolset version is passed to the constructor, it all other heuristic methods for
        /// getting the sub-toolset version.
        /// </summary>
        [Fact]
        public void GetSubToolsetVersion_FromConstructor()
        {
            string originalVisualStudioVersion = Environment.GetEnvironmentVariable("VisualStudioVersion");

            try
            {
                Environment.SetEnvironmentVariable("VisualStudioVersion", "ABC");

                IDictionary<string, string> globalProperties = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
                globalProperties.Add("VisualStudioVersion", "ABCD");

                IDictionary<string, string> projectCollectionGlobalProperties = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
                projectCollectionGlobalProperties.Add("VisualStudioVersion", "ABCDE");

                using var collection = new ProjectCollection(projectCollectionGlobalProperties);
                Project p = new Project(GetSampleProjectRootElement(), globalProperties, ObjectModelHelpers.MSBuildDefaultToolsVersion, "ABCDEF", collection, ProjectLoadSettings.Default);

                p.ToolsVersion.ShouldBe(ObjectModelHelpers.MSBuildDefaultToolsVersion);
                p.SubToolsetVersion.ShouldBe("ABCDEF");
                p.GetPropertyValue("VisualStudioVersion").ShouldBe("ABCDEF");
            }
            finally
            {
                Environment.SetEnvironmentVariable("VisualStudioVersion", originalVisualStudioVersion);
            }
        }

        /// <summary>
        /// Reevaluation should update the evaluation counter.
        /// </summary>
        [Fact]
        public void LastEvaluationId()
        {
            Project project = new Project();
            int last = project.LastEvaluationId;

            project.ReevaluateIfNecessary();
            last.ShouldBe(project.LastEvaluationId);
            last = project.LastEvaluationId;

            project.SetProperty("p", "v");
            project.ReevaluateIfNecessary();
            project.LastEvaluationId.ShouldNotBe(last);
        }

        /// <summary>
        /// Unload should not reset the evaluation counter.
        /// </summary>
        [Fact]
        public void LastEvaluationIdAndUnload()
        {
            string path = null;

            try
            {
                path = FileUtilities.GetTemporaryFileName();
                ProjectRootElement.Create().Save(path);

                Project project = new Project(path);
                int last = project.LastEvaluationId;

                project.ProjectCollection.UnloadAllProjects();

                project = new Project(path);
                project.LastEvaluationId.ShouldNotBe(last);
            }
            finally
            {
                File.Delete(path);
            }
        }

        /// <summary>
        /// Modifying the XML of an imported file should cause the project
        /// to be dirtied.
        /// </summary>
        [Fact]
        public void ImportedXmlModified()
        {
            string path = null;

            try
            {
                path = FileUtilities.GetTemporaryFileName();
                ProjectRootElement import = ProjectRootElement.Create(path);
                import.Save();

                Project project = new Project();
                int last = project.LastEvaluationId;

                project.Xml.AddImport(path);
                project.ReevaluateIfNecessary();
                project.LastEvaluationId.ShouldNotBe(last);
                last = project.LastEvaluationId;

                project.ReevaluateIfNecessary();
                last.ShouldBe(project.LastEvaluationId);

                import.AddProperty("p", "v");
                project.IsDirty.ShouldBeTrue();
                project.ReevaluateIfNecessary();
                project.LastEvaluationId.ShouldNotBe(last);
                last = project.LastEvaluationId;
                project.GetPropertyValue("p").ShouldBe("v");

                project.ReevaluateIfNecessary();
                last.ShouldBe(project.LastEvaluationId);
            }
            finally
            {
                File.Delete(path);
            }
        }

        /// <summary>
        /// Adding an import to an existing PRE object and re-evaluating should preserve the initial import PRE object
        /// </summary>
        [Fact]
        public void ImportingExistingPREObjectShouldPreserveTheObject()
        {
            var importProjectContents = ObjectModelHelpers.CleanupFileContents(
@"<Project xmlns=`msbuildnamespace`>

  <PropertyGroup>
    <P>p1</P>
  </PropertyGroup>

  <ItemGroup>
    <I Include=`i1`>
      <M>m1</M>
    </I>
  </ItemGroup>

</Project>");

            using (var env = TestEnvironment.Create())
            using (var projectCollection = new ProjectCollection())
            {
                var projectFiles = env.CreateTestProjectWithFiles("", new[] { "import.proj" });
                var importFile = projectFiles.CreatedFiles.First();
                using ProjectRootElementFromString projectRootElementFromString = new(
                importProjectContents,
                projectCollection,
                // preserve formatting to simulate IDE usage
                preserveFormatting: true);

                ProjectRootElement import = projectRootElementFromString.Project;

                // puts the import in the PRE cache
                import.Save(importFile);
                import.HasUnsavedChanges.ShouldBeFalse();

                Project project = new Project(projectCollection);
                project.Xml.AddImport(importFile);
                project.ReevaluateIfNecessary();
                project.Imports.First().ImportedProject.ShouldBeSameAs(import);
            }
        }

        [Fact]
        public void ReloadedImportsMarkProjectAsDirty()
        {
            var importProjectContents = ObjectModelHelpers.CleanupFileContents(
@"<Project xmlns=`msbuildnamespace`>

  <PropertyGroup>
    <P>p1</P>
  </PropertyGroup>

  <ItemGroup>
    <I Include=`i1`>
      <M>m1</M>
    </I>
  </ItemGroup>

</Project>");

            var changedImportContents = ObjectModelHelpers.CleanupFileContents(
@"<Project xmlns=`msbuildnamespace`>

  <PropertyGroup>
    <P>p2</P>
  </PropertyGroup>

  <ItemGroup>
    <I Include=`i2`>
      <M>m2</M>
    </I>
  </ItemGroup>

</Project>");

            Action<string, string, string, Project> assertContents = (p, i, m, project) =>
            {
                project.GetPropertyValue("P").ShouldBe(p);
                project.GetItems("I").ShouldHaveSingleItem();
                project.GetItems("I").First().EvaluatedInclude.ShouldBe(i);
                project.GetItems("I").First().GetMetadataValue("M").ShouldBe(m);
            };

            using (var env = TestEnvironment.Create())
            using (var projectCollection = new ProjectCollection())
            {
                var projectFiles = env.CreateTestProjectWithFiles("", new[] { "import.proj" });
                var importFile = projectFiles.CreatedFiles.First();

                using ProjectRootElementFromString projectRootElementFromString = new(
                importProjectContents,
                projectCollection,
                // preserve formatting to simulate IDE usage
                preserveFormatting: true);
                ProjectRootElement import = projectRootElementFromString.Project;

                // add to cache by saving
                import.Save(importFile);
                import.HasUnsavedChanges.ShouldBeFalse();

                var project = new Project(projectCollection);
                project.Xml.AddImport(importFile);
                project.ReevaluateIfNecessary();

                assertContents("p1", "i1", "m1", project);
                project.IsDirty.ShouldBeFalse();

                using var xmlReaderChangedContent = XmlReader.Create(new StringReader(changedImportContents));
                import.ReloadFrom(xmlReaderChangedContent);
                import.HasUnsavedChanges.ShouldBeTrue();

                project.IsDirty.ShouldBeTrue();
                assertContents("p1", "i1", "m1", project);

                project.ReevaluateIfNecessary();
                project.IsDirty.ShouldBeFalse();
                assertContents("p2", "i2", "m2", project);

                var newProject = new Project(projectCollection);
                newProject.Xml.AddImport(importFile);
                newProject.ReevaluateIfNecessary();
                assertContents("p2", "i2", "m2", newProject);

                project.Imports.First().ImportedProject.ShouldBeSameAs(import);
                newProject.Imports.First().ImportedProject.ShouldBeSameAs(import);
            }
        }

        [Fact]
        public void ReloadedProjectRootElementMarksProjectAsDirty()
        {
            var projectContents = ObjectModelHelpers.CleanupFileContents(
@"<Project xmlns=`msbuildnamespace`>

  <PropertyGroup>
    <P>p1</P>
  </PropertyGroup>

  <ItemGroup>
    <I Include=`i1`>
      <M>m1</M>
    </I>
  </ItemGroup>

</Project>");

            var changedProjectContents = ObjectModelHelpers.CleanupFileContents(
@"<Project xmlns=`msbuildnamespace`>

  <PropertyGroup>
    <P>p2</P>
  </PropertyGroup>

  <ItemGroup>
    <I Include=`i2`>
      <M>m2</M>
    </I>
  </ItemGroup>

</Project>");

            Action<string, string, string, Project> assertContents = (p, i, m, project) =>
            {
                project.GetPropertyValue("P").ShouldBe(p);
                project.GetItems("I").ShouldHaveSingleItem();
                project.GetItems("I").First().EvaluatedInclude.ShouldBe(i);
                project.GetItems("I").First().GetMetadataValue("M").ShouldBe(m);
            };

            using (var env = TestEnvironment.Create())
            using (var projectCollection = new ProjectCollection())
            using (var projectRootElementFromString = new ProjectRootElementFromString(projectContents, projectCollection, preserveFormatting: true))
            using (var xmlReaderChangedContents = XmlReader.Create(new StringReader(changedProjectContents)))
            {
                var projectFiles = env.CreateTestProjectWithFiles("", new[] { "build.proj" });
                var projectFile = projectFiles.CreatedFiles.First();


                var projectRootElement = projectRootElementFromString.Project;

                // add to cache by saving
                projectRootElement.Save(projectFile);
                projectRootElement.HasUnsavedChanges.ShouldBeFalse();

                var project = new Project(projectRootElement, new Dictionary<string, string>(), MSBuildConstants.CurrentToolsVersion, projectCollection);
                project.ReevaluateIfNecessary();

                assertContents("p1", "i1", "m1", project);
                project.IsDirty.ShouldBeFalse();

                projectRootElement.ReloadFrom(xmlReaderChangedContents);
                projectRootElement.HasUnsavedChanges.ShouldBeTrue();

                project.IsDirty.ShouldBeTrue();
                assertContents("p1", "i1", "m1", project);

                project.ReevaluateIfNecessary();
                project.IsDirty.ShouldBeFalse();
                assertContents("p2", "i2", "m2", project);
            }
        }

        /// <summary>
        /// To support certain corner cases, it is possible to explicitly mark a Project
        /// as dirty, so that reevaluate is productive.
        /// </summary>
        [Fact]
        public void ExternallyMarkDirty()
        {
            Project project = new Project();
            project.SetProperty("p", "v");
            project.ReevaluateIfNecessary();

            project.IsDirty.ShouldBeFalse();

            ProjectProperty property1 = project.GetProperty("p");

            project.MarkDirty();

            project.IsDirty.ShouldBeTrue();

            project.ReevaluateIfNecessary();

            project.IsDirty.ShouldBeFalse();

            ProjectProperty property2 = project.GetProperty("p");

            object.ReferenceEquals(property1, property2).ShouldBeFalse(); // different object indicates reevaluation occurred
        }

        /// <summary>
        /// Basic test of getting items by their include
        /// </summary>
        [Fact]
        public void ItemsByEvaluatedInclude()
        {
            Project project = new Project();
            project.Xml.AddItem("i", "i1");
            project.Xml.AddItem("i", "i1");
            project.Xml.AddItem("j", "j1");
            project.Xml.AddItem("j", "i1");

            project.ReevaluateIfNecessary();

            List<ProjectItem> items = Helpers.MakeList(project.GetItemsByEvaluatedInclude("i1"));

            items.Count.ShouldBe(3);
            foreach (ProjectItem item in items)
            {
                item.EvaluatedInclude.ShouldBe("i1");
            }
        }

        /// <summary>
        /// Basic test of getting items by their include
        /// </summary>
        [Fact]
        public void ItemsByEvaluatedInclude_EvaluatedIncludeNeedsEscaping()
        {
            Project project = new Project();
            project.Xml.AddItem("i", "i%261");
            project.Xml.AddItem("j", "i%25261");
            project.Xml.AddItem("k", "j1");
            project.Xml.AddItem("l", "i&1");

            project.ReevaluateIfNecessary();

            List<ProjectItem> items = Helpers.MakeList(project.GetItemsByEvaluatedInclude("i&1"));

            items.Count.ShouldBe(2);
            foreach (ProjectItem item in items)
            {
                item.EvaluatedInclude.ShouldBe("i&1");
                (string.Equals(item.ItemType, "i", StringComparison.OrdinalIgnoreCase)
                 || string.Equals(item.ItemType, "l", StringComparison.OrdinalIgnoreCase)).ShouldBeTrue();
            }
        }

        /// <summary>
        /// Verify none returned when none match
        /// </summary>
        [Fact]
        public void ItemsByEvaluatedIncludeNone()
        {
            Project project = new Project();
            project.Xml.AddItem("i", "i1");

            project.ReevaluateIfNecessary();

            List<ProjectItem> items = Helpers.MakeList(project.GetItemsByEvaluatedInclude("i2"));

            items.ShouldBeEmpty();
        }

        /// <summary>
        /// Tests the tracking of virtual items from the construction to instance model, with the removal of a virtual item.
        /// </summary>
        [Fact]
        public void ItemsByEvaluatedIncludeAndExpansion()
        {
            List<string> filePaths = new List<string>();
            string testFileRoot = null;
            try
            {
                int count = 0;
                testFileRoot = Path.Combine(Path.GetTempPath(), "foodir");
                Directory.CreateDirectory(testFileRoot);
                int maxFiles = 2;
                for (int i = 0; i < maxFiles; i++)
                {
                    string fileName = string.Format("foo{0}.foo", i);
                    string filePath = Path.Combine(testFileRoot, fileName);
                    File.WriteAllText(filePath, string.Empty);
                    filePaths.Add(filePath);
                }

                ProjectRootElement projectConstruction = ProjectRootElement.Create();
                projectConstruction.AddItem("foo", Path.Combine(testFileRoot, "*.foo"));

                count = Helpers.Count(projectConstruction.Items);
                count.ShouldBe(1); // "Construction Model"

                Project project = new Project(projectConstruction);

                count = Helpers.Count(project.GetItems("foo"));
                count.ShouldBe(2); // "Evaluation Model, Before Removal"

                ProjectItem itemToRemove = null;

                // Get the first item from IEnumerable Collection.
                foreach (ProjectItem item in project.Items)
                {
                    itemToRemove = item;
                    break;
                }

                project.RemoveItem(itemToRemove);
                count = Helpers.Count(project.GetItems("foo"));
                count.ShouldBe(1); // "Evaluation Model, After Removal"

                ProjectInstance projectInstance = project.CreateProjectInstance();
                count = Helpers.Count(projectInstance.Items);
                count.ShouldBe(1); // "Instance Model"

                // Ensure XML has been updated accordingly on the Evaluation model (projectInstance doesn't back onto XML)
                project.Xml.RawXml.ShouldNotContain(itemToRemove.Xml.Include);
                project.Xml.RawXml.ShouldNotContain("*.foo");
            }
            finally
            {
                foreach (string filePathToRemove in filePaths)
                {
                    File.Delete(filePathToRemove);
                }

                FileUtilities.DeleteWithoutTrailingBackslash(testFileRoot);
            }
        }

        /// <summary>
        /// Reevaluation should update items-by-evaluated-include
        /// </summary>
        [Fact]
        public void ItemsByEvaluatedIncludeReevaluation()
        {
            Project project = new Project();
            project.Xml.AddItem("i", "i1");
            project.ReevaluateIfNecessary();

            List<ProjectItem> items = Helpers.MakeList(project.GetItemsByEvaluatedInclude("i1"));
            items.ShouldHaveSingleItem();

            project.Xml.AddItem("j", "i1");
            project.ReevaluateIfNecessary();

            items = Helpers.MakeList(project.GetItemsByEvaluatedInclude("i1"));
            items.Count.ShouldBe(2);
        }

        /// <summary>
        /// Direct adds to the project (ie, not added by evaluation) should update
        /// items-by-evaluated-include
        /// </summary>
        [Fact]
        public void ItemsByEvaluatedIncludeDirectAdd()
        {
            Project project = new Project();
            project.AddItem("i", "i1");

            List<ProjectItem> items = Helpers.MakeList(project.GetItemsByEvaluatedInclude("i1"));
            items.ShouldHaveSingleItem();
        }

        /// <summary>
        /// Direct removes from the project (ie, not removed by evaluation) should update
        /// items-by-evaluated-include
        /// </summary>
        [Fact]
        public void ItemsByEvaluatedIncludeDirectRemove()
        {
            Project project = new Project();
            ProjectItem item1 = project.AddItem("i", "i1;j1")[0];
            project.RemoveItem(item1);

            List<ProjectItem> items = Helpers.MakeList(project.GetItemsByEvaluatedInclude("i1"));
            items.ShouldBeEmpty();
        }

        /// <summary>
        /// Choose, When has true condition
        /// </summary>
        [Fact]
        public void ChooseWhenTrue()
        {
            string content = ObjectModelHelpers.CleanupFileContents(@"
                    <Project xmlns='msbuildnamespace' >
                        <Choose>
                            <When Condition='true'>
                              <PropertyGroup>
                                <p>v1</p>
                              </PropertyGroup>
                              <ItemGroup>
                                <i Include='i1' />
                              </ItemGroup>
                            </When>
                        </Choose>
                    </Project>
                ");

            using ProjectFromString projectFromString = new(content);
            Project project = projectFromString.Project;

            project.GetPropertyValue("p").ShouldBe("v1");
            Helpers.MakeList(project.GetItems("i"))[0].EvaluatedInclude.ShouldBe("i1");
        }

        /// <summary>
        /// Choose, second When has true condition
        /// </summary>
        [Fact]
        public void ChooseSecondWhenTrue()
        {
            string content = ObjectModelHelpers.CleanupFileContents(@"
                    <Project xmlns='msbuildnamespace' >
                        <Choose>
                            <When Condition='false'>
                              <PropertyGroup>
                                <p>v1</p>
                              </PropertyGroup>
                              <ItemGroup>
                                <i Include='i1' />
                              </ItemGroup>
                            </When>
                            <When Condition='true'>
                              <PropertyGroup>
                                <p>v2</p>
                              </PropertyGroup>
                              <ItemGroup>
                                <i Include='i2' />
                              </ItemGroup>
                            </When>
                        </Choose>
                    </Project>
                ");

            using ProjectFromString projectFromString = new(content);
            Project project = projectFromString.Project;

            project.GetPropertyValue("p").ShouldBe("v2");
            Helpers.MakeList(project.GetItems("i"))[0].EvaluatedInclude.ShouldBe("i2");
        }

        /// <summary>
        /// Choose, when has false condition
        /// </summary>
        [Fact]
        public void ChooseOtherwise()
        {
            string content = ObjectModelHelpers.CleanupFileContents(@"
                    <Project xmlns='msbuildnamespace' >
                        <Choose>
                            <When Condition='false'>
                              <PropertyGroup>
                                <p>v1</p>
                              </PropertyGroup>
                              <ItemGroup>
                                <i Include='i1' />
                              </ItemGroup>
                            </When>
                            <Otherwise>
                              <PropertyGroup>
                                <p>v2</p>
                              </PropertyGroup>
                              <ItemGroup>
                                <i Include='i2' />
                              </ItemGroup>
                            </Otherwise>
                        </Choose>
                    </Project>
                ");

            using ProjectFromString projectFromString = new(content);
            Project project = projectFromString.Project;

            project.GetPropertyValue("p").ShouldBe("v2");
            Helpers.MakeList(project.GetItems("i"))[0].EvaluatedInclude.ShouldBe("i2");
        }

        /// <summary>
        /// Choose should be entered twice, once for properties and again for items.
        /// That means items should see properties defined below.
        /// </summary>
        [Fact]
        public void ChooseTwoPasses()
        {
            string content = ObjectModelHelpers.CleanupFileContents(@"
                    <Project xmlns='msbuildnamespace' >
                        <Choose>
                            <When Condition='true'>
                              <ItemGroup>
                                <i Include='$(p)_$(p2)' />
                              </ItemGroup>
                              <PropertyGroup>
                                <p>@(i);v1</p>
                              </PropertyGroup>
                            </When>
                        </Choose>

                      <PropertyGroup>
                        <p2>v2</p2>
                      </PropertyGroup>

                        <Choose>
                            <When Condition='false'/>
                            <Otherwise>
                              <ItemGroup>
                                <j Include='$(q)_$(q2)' />
                              </ItemGroup>
                              <PropertyGroup>
                                <q>@(j);v1</q>
                              </PropertyGroup>
                            </Otherwise>
                        </Choose>

                      <PropertyGroup>
                        <q2>v2</q2>
                      </PropertyGroup>
                    </Project>
                ");

            using ProjectFromString projectFromString = new(content);
            Project project = projectFromString.Project;

            project.GetPropertyValue("p").ShouldBe("@(i);v1");
            project.GetPropertyValue("q").ShouldBe("@(j);v1");
            project.GetItems("i").ElementAt(0).EvaluatedInclude.ShouldBe("v1_v2");
            project.GetItems("i").ShouldHaveSingleItem();
            project.GetItems("j").ElementAt(0).EvaluatedInclude.ShouldBe("v1_v2");
            project.GetItems("j").ShouldHaveSingleItem();
        }

        /// <summary>
        /// Choose conditions are only evaluated once, on the property pass
        /// </summary>
        [Fact]
        public void ChooseEvaluateConditionOnlyOnce()
        {
            string content = ObjectModelHelpers.CleanupFileContents(@"
                    <Project xmlns='msbuildnamespace' >
                        <Choose>
                            <When Condition=""'$(p)' != ''"">
                              <ItemGroup>
                                <i Include='i1' />
                              </ItemGroup>
                            </When>
                        </Choose>

                      <PropertyGroup>
                        <p>v</p>
                      </PropertyGroup>

                    </Project>
                ");

            using ProjectFromString projectFromString = new(content);
            Project project = projectFromString.Project;

            project.GetItems("i").ShouldBeEmpty();
        }

        /// <summary>
        /// Choose items can see item definitions below
        /// </summary>
        [Fact]
        public void ChooseSeesItemDefinitions()
        {
            string content = ObjectModelHelpers.CleanupFileContents(@"
                    <Project xmlns='msbuildnamespace' >
                        <Choose>
                            <When Condition='true'>
                              <ItemGroup>
                                <i Include='i1'>
                                  <m>%(m);m1</m>
                                </i>
                              </ItemGroup>
                            </When>
                        </Choose>

                      <ItemDefinitionGroup>
                        <i>
                          <m>m0</m>
                        </i>
                      </ItemDefinitionGroup>

                    </Project>
                ");

            using ProjectFromString projectFromString = new(content);
            Project project = projectFromString.Project;

            project.GetItems("i").ElementAt(0).GetMetadataValue("m").ShouldBe("m0;m1");
        }

        /// <summary>
        /// When build is disabled on the project, it shouldn't run, and should give MSB4112.
        /// </summary>
        [Fact]
        public void BuildDisabled()
        {
            Project project = new Project();
            project.Xml.AddTarget("t");
            project.IsBuildEnabled = false;
            MockLogger mockLogger = new MockLogger();
            ProjectCollection.GlobalProjectCollection.RegisterLogger(mockLogger);

            bool result = project.Build();

            result.ShouldBeFalse();

            mockLogger.Errors[0].Code.ShouldBe("MSB4112"); // "Security message about disabled targets need to have code MSB4112, because code in the VS Core project system depends on this.  See DesignTimeBuildFeedback.cpp."
        }

        /// <summary>
        /// Building a nonexistent target should log an error and return false (not throw)
        /// </summary>
        [Fact]
        [Trait("Category", "serialize")]
        public void BuildNonExistentTarget()
        {
            Project project = new Project();
            MockLogger logger = new MockLogger();
            bool result = project.Build(new string[] { "nonexistent" }, new List<ILogger>() { logger });
            result.ShouldBeFalse();
            logger.ErrorCount.ShouldBe(1);
        }

        /// <summary>
        /// When Project.Build is invoked with custom loggers, those loggers should contain the result of any evaluation warnings and errors.
        /// </summary>
        [Fact]
        [Trait("Category", "serialize")]
        public void BuildEvaluationUsesCustomLoggers()
        {
            string importProjectContent =
                ObjectModelHelpers.CleanupFileContents(@"<Project xmlns='msbuildnamespace'>
                </Project>");

            string importFileName = Microsoft.Build.Shared.FileUtilities.GetTemporaryFileName() + ".proj";
            File.WriteAllText(importFileName, importProjectContent);

            string projectContent =
                ObjectModelHelpers.CleanupFileContents(@"<Project xmlns='msbuildnamespace'>
                    <Import Project=""" + importFileName + @"""/>
                    <Import Project=""" + importFileName + @"""/>
                    <ItemGroup>
                        <Compile Include='a.cs' />
                    </ItemGroup>
                    <Target Name=""Build"" />
                </Project>");

            using ProjectFromString projectFromString = new(projectContent);
            Project project = projectFromString.Project;
            project.MarkDirty();

            MockLogger collectionLogger = new MockLogger();
            project.ProjectCollection.RegisterLogger(collectionLogger);

            MockLogger mockLogger = new MockLogger();

            bool result;

            try
            {
                result = project.Build(new ILogger[] { mockLogger });
            }
            finally
            {
                project.ProjectCollection.UnregisterAllLoggers();
            }

            result.ShouldBeTrue();

            mockLogger.WarningCount.ShouldBe(0); // "Log should not contain MSB4011 because the build logger will not receive evaluation messages."

            collectionLogger.Warnings[0].Code.ShouldBe("MSB4011"); // "Log should contain MSB4011 because the project collection logger should have been used for evaluation."
        }

        /// <summary>
        /// UsingTask expansion should throw InvalidProjectFileException
        /// if it expands to nothing.
        /// </summary>
        [Fact]
        public void UsingTaskExpansion1()
        {
            Should.Throw<InvalidProjectFileException>(() =>
            {
                ProjectRootElement xml = ProjectRootElement.Create();
                xml.AddUsingTask("x", "@(x->'%(x)')", null);
                Project project = new Project(xml);
            });
        }
        /// <summary>
        /// UsingTask expansion should throw InvalidProjectFileException
        /// if it expands to nothing.
        /// </summary>
        [Fact]
        public void UsingTaskExpansion2()
        {
            Should.Throw<InvalidProjectFileException>(() =>
            {
                ProjectRootElement xml = ProjectRootElement.Create();
                xml.AddUsingTask("@(x->'%(x)')", "y", null);
                Project project = new Project(xml);
            });
        }
        /// <summary>
        /// UsingTask expansion should throw InvalidProjectFileException
        /// if it expands to nothing.
        /// </summary>
        [Fact]
        public void UsingTaskExpansion3()
        {
            Should.Throw<InvalidProjectFileException>(() =>
            {
                ProjectRootElement xml = ProjectRootElement.Create();
                xml.AddUsingTask("x", null, "@(x->'%(x)')");
                Project project = new Project(xml);
            });
        }
        /// <summary>
        /// Saving project should make it "clean" for saving
        /// but "dirty" for reevaluation if it was to a new location
        /// </summary>
        [Fact]
        public void SavingProjectClearsDirtyBit()
        {
            string contents = ObjectModelHelpers.CleanupFileContents(@"<Project xmlns='msbuildnamespace'/>");
            using ProjectFromString projectFromString = new(contents);
            Project project = projectFromString.Project;

            project.Xml.HasUnsavedChanges.ShouldBeTrue(); // Not dirty for saving
            project.IsDirty.ShouldBeFalse(); // "1" // was evaluated on load

            string file = null;
            try
            {
                file = FileUtilities.GetTemporaryFileName();
                project.Save(file);
            }
            finally
            {
                if (file != null)
                {
                    File.Delete(file);
                }
            }

            project.Xml.HasUnsavedChanges.ShouldBeFalse(); // Not dirty for saving
            project.IsDirty.ShouldBeTrue(); // "2" // Dirty for reevaluation, because the project now has gotten a new file name
        }

        /// <summary>
        /// Remove an already removed item
        /// </summary>
        [Fact]
        public void RemoveItemTwiceEvaluationProject()
        {
            string projectOriginalContents = ObjectModelHelpers.CleanupFileContents(@"
                <Project ToolsVersion='msbuilddefaulttoolsversion' DefaultTargets='Build' xmlns='msbuildnamespace'>
                    <ItemGroup>
                        <Compile Include='a.cs' />
                    </ItemGroup>
                </Project>
                ");
            using ProjectFromString projectFromString = new(projectOriginalContents);
            Project project = projectFromString.Project;

            ProjectItem itemToRemove = Helpers.GetFirst(project.GetItems("Compile"));
            project.RemoveItem(itemToRemove);
            project.RemoveItem(itemToRemove); // should not throw

            Helpers.MakeList(project.Items).ShouldBeEmpty();
        }

        /// <summary>
        /// Remove an updated item
        /// </summary>
        [Fact]
        public void RemoveItemOutdatedByUpdate()
        {
            string projectOriginalContents = ObjectModelHelpers.CleanupFileContents(@"
                <Project ToolsVersion='msbuilddefaulttoolsversion' DefaultTargets='Build' xmlns='msbuildnamespace'>
                    <ItemGroup>
                        <Compile Include='a.cs' />
                    </ItemGroup>
                </Project>
                ");
            using ProjectFromString projectFromString = new(projectOriginalContents);
            Project project = projectFromString.Project;
            ProjectItem itemToRemove = Helpers.GetFirst(project.GetItems("Compile"));
            itemToRemove.UnevaluatedInclude = "b.cs";
            project.RemoveItem(itemToRemove); // should not throw

            Helpers.MakeList(project.Items).ShouldBeEmpty();
        }

        /// <summary>
        /// Remove several items
        /// </summary>
        [Fact]
        public void RemoveSeveralItems()
        {
            string projectOriginalContents = ObjectModelHelpers.CleanupFileContents(@"
                <Project ToolsVersion='msbuilddefaulttoolsversion' DefaultTargets='Build' xmlns='msbuildnamespace'>
                    <ItemGroup>
                        <i Include='i1' />
                        <i Include='i2' />
                    </ItemGroup>
                </Project>
                ");
            using ProjectFromString projectFromString = new(projectOriginalContents);
            Project project = projectFromString.Project;

            project.RemoveItems(project.GetItems("i"));

            project.Items.ShouldBeEmpty();
        }

        /// <summary>
        /// Remove several items
        /// </summary>
        [Fact]
        public void RemoveSeveralItemsOfVariousTypes()
        {
            string projectOriginalContents = ObjectModelHelpers.CleanupFileContents(@"
                <Project ToolsVersion='msbuilddefaulttoolsversion' DefaultTargets='Build' xmlns='msbuildnamespace'>
                    <ItemGroup>
                        <i Include='i1' />
                        <j Include='j1' />
                        <j Include='j2' />
                        <k Include='k1' />
                    </ItemGroup>
                </Project>
                ");
            using ProjectFromString projectFromString = new(projectOriginalContents);
            Project project = projectFromString.Project;


            List<ProjectItem> list = new List<ProjectItem>() { project.GetItems("i").FirstOrDefault(), project.GetItems("j").FirstOrDefault() };

            project.RemoveItems(list);

            project.Items.Count.ShouldBe(2);
        }

        /// <summary>
        /// Remove items expanding itemlist expression
        /// </summary>
        [Fact]
        public void RemoveSeveralItemsExpandExpression()
        {
            string projectOriginalContents = ObjectModelHelpers.CleanupFileContents(@"
                <Project ToolsVersion='msbuilddefaulttoolsversion' DefaultTargets='Build' xmlns='msbuildnamespace'>
                    <ItemGroup>
                        <i Include='i1;i2' />
                        <j Include='@(i);j2' />
                    </ItemGroup>
                </Project>
                ");
            using ProjectFromString projectFromString = new(projectOriginalContents);
            Project project = projectFromString.Project;

            project.RemoveItems(project.GetItems("j").Take(2));
            project.Items.Count.ShouldBe(3);

            using StringWriter writer = new EncodingStringWriter();
            project.Save(writer);

            string projectExpectedContents = ObjectModelHelpers.CleanupFileContents(@"
                <Project ToolsVersion='msbuilddefaulttoolsversion' DefaultTargets='Build' xmlns='msbuildnamespace'>
                    <ItemGroup>
                        <i Include='i1;i2' />
                        <j Include='j2' />
                    </ItemGroup>
                </Project>
                ");

            Helpers.CompareProjectXml(projectExpectedContents, writer.ToString());
        }

        /// <summary>
        /// Remove several items where removing the first one
        /// causes the second one to be detached
        /// </summary>
        [Fact]
        public void RemoveSeveralItemsFirstZombiesSecond()
        {
            string projectOriginalContents = ObjectModelHelpers.CleanupFileContents(@"
                <Project ToolsVersion='msbuilddefaulttoolsversion' DefaultTargets='Build' xmlns='msbuildnamespace'>
                    <ItemGroup>
                        <i Include='i1;i2' />
                    </ItemGroup>
                </Project>
                ");
            using ProjectFromString projectFromString = new(projectOriginalContents);
            Project project = projectFromString.Project;

            project.RemoveItems(project.GetItems("i"));

            project.Items.ShouldBeEmpty();
        }

        /// <summary>
        /// Should not get null reference
        /// </summary>
        [Fact]
        public void RemoveItemsOneNull()
        {
            Should.Throw<ArgumentNullException>(() =>
            {
                Project project = new Project();
                project.RemoveItems(new List<ProjectItem>() { null });
            });
        }
        /// <summary>
        /// Remove several items where removing the first one
        /// causes the second one to be detached
        /// </summary>
        [Fact]
        public void RemoveItemWrongProject()
        {
            Should.Throw<ArgumentException>(() =>
            {
                ProjectRootElement root1 = ProjectRootElement.Create();
                root1.AddItem("i", "i1");
                ProjectRootElement root2 = ProjectRootElement.Create();
                root2.AddItem("i", "i1");
                Project project1 = new Project(root1);
                Project project2 = new Project(root2);

                project1.RemoveItems(project2.Items);
            });
        }
        /// <summary>
        /// Remove an item that is no longer attached. For convenience,
        /// we just skip it.
        /// </summary>
        [Fact]
        public void RemoveZombiedItem()
        {
            string projectOriginalContents = ObjectModelHelpers.CleanupFileContents(@"
                <Project ToolsVersion='msbuilddefaulttoolsversion' DefaultTargets='Build' xmlns='msbuildnamespace'>
                    <ItemGroup>
                        <i Include='i1' />
                    </ItemGroup>
                </Project>
                ");
            using ProjectFromString projectFromString = new(projectOriginalContents);
            Project project = projectFromString.Project;
            ProjectItem item = project.GetItems("i").FirstOrDefault();

            project.RemoveItems(new List<ProjectItem>() { item });
            project.RemoveItems(new List<ProjectItem>() { item });

            project.Items.ShouldBeEmpty();
        }

        /// <summary>
        /// Reserved property in project constructor should just throw
        /// </summary>
        [Fact]
        public void ReservedPropertyProjectConstructor()
        {
            Should.Throw<ArgumentException>(() =>
            {
                Dictionary<string, string> globalProperties = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
                globalProperties.Add("msbuildprojectdirectory", "x");

                Project project = new Project(globalProperties, null, new ProjectCollection());
            });
        }
        /// <summary>
        /// Reserved property in project collection global properties should log an error then rethrow
        /// </summary>
        [Fact]
        public void ReservedPropertyProjectCollectionConstructor()
        {
            Should.Throw<InvalidProjectFileException>(() =>
            {
                Dictionary<string, string> globalProperties = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
                globalProperties.Add("msbuildprojectdirectory", "x");
                MockLogger logger = new MockLogger();
                List<ILogger> loggers = new List<ILogger>();
                loggers.Add(logger);

                try
                {
                    ProjectCollection collection = new ProjectCollection(globalProperties, loggers, ToolsetDefinitionLocations.None);
                }
                finally
                {
                    logger.AssertLogContains("MSB4177");
                }
            });
        }
        /// <summary>
        /// Invalid property (reserved name) in project collection global properties should log an error then rethrow
        /// </summary>
        [Fact]
        public void ReservedPropertyProjectCollectionConstructor2()
        {
            Should.Throw<InvalidProjectFileException>(() =>
            {
                Dictionary<string, string> globalProperties = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
                globalProperties.Add("Target", "x");
                MockLogger logger = new MockLogger();
                List<ILogger> loggers = new List<ILogger>();
                loggers.Add(logger);

                try
                {
                    ProjectCollection collection = new ProjectCollection(globalProperties, loggers, ToolsetDefinitionLocations.None);
                }
                finally
                {
                    logger.AssertLogContains("MSB4177");
                }
            });
        }
        /// <summary>
        /// Create tree like this
        ///
        /// \b.targets
        /// \sub\a.proj
        ///
        /// An item specified with "..\*" in b.targets should find b.targets
        /// as it was evaluated relative to the project file itself.
        /// </summary>
        [Fact]
        public void RelativePathsInItemsInTargetsFilesAreRelativeToProjectFile()
        {
            string directory = null;

            try
            {
                directory = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString());
                string subdirectory = Path.Combine(directory, "sub");
                Directory.CreateDirectory(subdirectory);

                string projectPath = Path.Combine(subdirectory, "a.proj");
                string targetsPath = Path.Combine(directory, "b.targets");

                string unevaluatedInclude = ".." + Path.DirectorySeparatorChar + "*";
                string evaluatedInclude = ".." + Path.DirectorySeparatorChar + "b.targets";

                ProjectRootElement targetsXml = ProjectRootElement.Create(targetsPath);
                targetsXml.AddItem("i", unevaluatedInclude);
                targetsXml.Save();

                ProjectRootElement projectXml = ProjectRootElement.Create(projectPath);
                projectXml.AddImport(evaluatedInclude);
                projectXml.Save();

                Project project = new Project(projectPath);

                IEnumerable<ProjectItem> items = project.GetItems("i");
                Helpers.GetFirst(items).UnevaluatedInclude.ShouldBe(unevaluatedInclude);
                Helpers.GetFirst(items).EvaluatedInclude.ShouldBe(evaluatedInclude);
            }
            finally
            {
                FileUtilities.DeleteWithoutTrailingBackslash(directory, true);
            }
        }

        /// <summary>
        /// Invalid property (space) in project collection global properties should log an error then rethrow
        /// </summary>
        [Fact]
        public void ReservedPropertyProjectCollectionConstructor3()
        {
            Should.Throw<InvalidProjectFileException>(() =>
            {
                Dictionary<string, string> globalProperties = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
                globalProperties.Add("Target", "x");
                MockLogger logger = new MockLogger();
                List<ILogger> loggers = new List<ILogger>();
                loggers.Add(logger);

                try
                {
                    ProjectCollection collection = new ProjectCollection(globalProperties, loggers, ToolsetDefinitionLocations.None);
                }
                finally
                {
                    logger.AssertLogContains("MSB4177");
                }
            });
        }
        /// <summary>
        /// Create a structure of various imports and verify that project.GetLogicalProject()
        /// walks through them correctly.
        /// </summary>
        [Fact]
        public void VariousImports()
        {
            ProjectRootElement one = ProjectRootElement.Create("c:\\1.targets");
            one.AddProperty("p", "1");
            ProjectRootElement two = ProjectRootElement.Create("c:\\2.targets");
            two.AddProperty("p", "2");

            ProjectRootElement zero = ProjectRootElement.Create("c:\\foo\\0.targets");
            zero.AddProperty("p", "0");
            zero.AddImport(one.FullPath);
            zero.AddImport(two.FullPath);
            zero.AddImport(two.FullPath); // Duplicated import: only the first one should be entered
            zero.AddImport(zero.FullPath); // Ignored self import

            ProjectRootElement three = ProjectRootElement.Create("c:\\3.targets");
            three.AddProperty("p", "3");
            one.AddImport(three.FullPath);

            ProjectRootElement four = ProjectRootElement.Create("c:\\4.targets");
            four.AddProperty("p", "4");
            one.AddImport(four.FullPath).Condition = "false"; // False condition; should not be entered

            Project project = new Project(zero);

            List<ProjectElement> logicalProject = new List<ProjectElement>(project.GetLogicalProject());

            logicalProject.Count.ShouldBe(8); // 4 properties + 4 property groups
            object.ReferenceEquals(zero, logicalProject[0].ContainingProject).ShouldBeTrue();
            object.ReferenceEquals(zero, logicalProject[1].ContainingProject).ShouldBeTrue();
            object.ReferenceEquals(one, logicalProject[2].ContainingProject).ShouldBeTrue();
            object.ReferenceEquals(one, logicalProject[3].ContainingProject).ShouldBeTrue();
            object.ReferenceEquals(three, logicalProject[4].ContainingProject).ShouldBeTrue();
            object.ReferenceEquals(three, logicalProject[5].ContainingProject).ShouldBeTrue();
            object.ReferenceEquals(two, logicalProject[6].ContainingProject).ShouldBeTrue();
            object.ReferenceEquals(two, logicalProject[7].ContainingProject).ShouldBeTrue();

            // Clear the cache
            project.ProjectCollection.UnloadAllProjects();
        }

        /// <summary>
        /// Create a structure containing a import statement such that the import statement results in more than one
        /// file being imported. Then, verify that project.GetLogicalProject() walks through them correctly.
        /// </summary>
        [Fact]
        public void LogicalProjectWithWildcardImport()
        {
            string myTempDir = Path.Combine(Path.GetTempPath() + "MyTempDir");

            try
            {
                // Create a new directory in the system temp folder.
                Directory.CreateDirectory(myTempDir);

                ProjectRootElement one = ProjectRootElement.Create(Path.Combine(myTempDir, "1.targets"));
                one.Save();
                one.AddProperty("p", "1");

                ProjectRootElement two = ProjectRootElement.Create(Path.Combine(myTempDir, "2.targets"));
                two.Save();
                two.AddProperty("p", "2");

                ProjectRootElement zero = ProjectRootElement.Create(Path.Combine(myTempDir, "0.targets"));
                zero.AddProperty("p", "0");

                // Add a single import statement that would import both one and two.
                zero.AddImport(Path.Combine(myTempDir, "*.targets"));

                Project project = new Project(zero);

                List<ProjectElement> logicalProject = new List<ProjectElement>(project.GetLogicalProject());

                logicalProject.Count.ShouldBe(6); // 3 properties + 3 property groups
                object.ReferenceEquals(zero, logicalProject[0].ContainingProject).ShouldBeTrue(); // PropertyGroup
                object.ReferenceEquals(zero, logicalProject[1].ContainingProject).ShouldBeTrue(); // p = 0
                object.ReferenceEquals(one, logicalProject[2].ContainingProject).ShouldBeTrue();  // PropertyGroup
                object.ReferenceEquals(one, logicalProject[3].ContainingProject).ShouldBeTrue();  // p = 1
                object.ReferenceEquals(two, logicalProject[4].ContainingProject).ShouldBeTrue();  // PropertyGroup
                object.ReferenceEquals(two, logicalProject[5].ContainingProject).ShouldBeTrue();  // p = 2

                // Clear the cache
                project.ProjectCollection.UnloadAllProjects();
            }
            finally
            {
                // Delete the temp directory that was created above.
                if (Directory.Exists(myTempDir))
                {
                    FileUtilities.DeleteWithoutTrailingBackslash(myTempDir, true);
                }
            }
        }

        /// <summary>
        /// Import of string that evaluates to empty should give InvalidProjectFileException
        /// </summary>
        [Fact]
        public void ImportPropertyEvaluatingToEmpty()
        {
            Should.Throw<InvalidProjectFileException>(() =>
            {
                string projectOriginalContents = ObjectModelHelpers.CleanupFileContents(@"
                <Project ToolsVersion='msbuilddefaulttoolsversion' DefaultTargets='Build' xmlns='msbuildnamespace'>
                  <Import Project='$(not_defined)'/>
                </Project>
                ");
                Project project = new Project(XmlReader.Create(new StringReader(projectOriginalContents)));
            });
        }

        [Fact]
        public void GetItemProvenanceShouldReturnNothingWhenCalledWithEmptyOrNullArgs()
        {
            var project =
                @"<Project ToolsVersion='msbuilddefaulttoolsversion' DefaultTargets='Build' xmlns='msbuildnamespace'>
                  <ItemGroup>
                    <A Include=`1;2;3`/>
                    <B Include=`1;2;3` Exclude=`1;4`/>
                  </ItemGroup>
                </Project>
                ";

            var expected = new ProvenanceResultTupleList();

            // by item value as empty string
            AssertProvenanceResult(expected, project, "");
            // by item value as null
            AssertProvenanceResult(expected, project, null);

            // by item value and type as empty string
            AssertProvenanceResult(expected, project, "", "");
            // by item value and type as null
            AssertProvenanceResult(expected, project, null, null);

            // by projectitem as null
            AssertProvenanceResult(expected, project, null, -1);
        }

        /// <summary>
        /// Import of string that evaluates to invalid path should cause InvalidProjectFileException
        /// </summary>
        [Fact]
        public void ImportPropertyEvaluatingToInvalidPath()
        {
            Should.Throw<InvalidProjectFileException>(() =>
            {
                string projectOriginalContents = ObjectModelHelpers.CleanupFileContents(@"
                <Project ToolsVersion='msbuilddefaulttoolsversion' DefaultTargets='Build' xmlns='msbuildnamespace'>
                  <PropertyGroup>
                    <p>|</p>
                  </PropertyGroup>
                  <Import Project='$(p)'/>
                </Project>
                ");
                Project project = new Project(XmlReader.Create(new StringReader(projectOriginalContents)));
            });
        }

        [Fact]
        public void GetItemProvenanceShouldReturnEmptyListOnNoMatches()
        {
            var project =
                @"<Project ToolsVersion='msbuilddefaulttoolsversion' DefaultTargets='Build' xmlns='msbuildnamespace'>
                  <ItemGroup>
                    <A Include=`1;2;3`/>
                    <B Include=`1;2;3` Exclude=`1;4`/>
                  </ItemGroup>
                </Project>
                ";

            var expected = new ProvenanceResultTupleList();

            AssertProvenanceResult(expected, project, "4");
        }

        [Fact]
        public void GetItemProvenanceOnlyStringLiteral()
        {
            var project =
                @"<Project ToolsVersion='msbuilddefaulttoolsversion' DefaultTargets='Build' xmlns='msbuildnamespace'>
                  <ItemGroup>
                    <A Include=`1;2;3`/>
                    <B Include=`1;2;3` Exclude=`1`/>
                    <C Include=`2;3` Exclude=`2`/>
                  </ItemGroup>
                </Project>
                ";

            var expected = new ProvenanceResultTupleList
            {
                ("A", Operation.Include, Provenance.StringLiteral, 1),
                ("B", Operation.Exclude, Provenance.StringLiteral, 1)
            };

            AssertProvenanceResult(expected, project, "1");
        }

        [Fact]
        public void GetItemProvenanceShouldNotReportMatchesInExcludesIfNoIncludeMatchesExist()
        {
            var project =
                @"<Project ToolsVersion='msbuilddefaulttoolsversion' DefaultTargets='Build' xmlns='msbuildnamespace'>
                  <ItemGroup>
                    <A Include=`1,2,3` Exclude=`4`/>
                  </ItemGroup>
                </Project>
                ";

            var expected = new ProvenanceResultTupleList();

            AssertProvenanceResult(expected, project, "4");
        }

        [Fact]
        public void GetItemProvenanceSimpleGlob()
        {
            var project =
            @"<Project ToolsVersion='msbuilddefaulttoolsversion' DefaultTargets='Build' xmlns='msbuildnamespace'>
                  <ItemGroup>
                    <A Include=`*`/>
                  </ItemGroup>
                </Project>
            ";

            var expected = new ProvenanceResultTupleList
            {
                ("A", Operation.Include, Provenance.Glob, 1),
            };

            AssertProvenanceResult(expected, project, "a");
            AssertProvenanceResult(expected, project, "2.foo");
            AssertProvenanceResult(new ProvenanceResultTupleList(), project, "a/2.foo");
        }

        [Fact]
        public void GetItemProvenanceOnlyGlob()
        {
            var project =
            @"<Project ToolsVersion='msbuilddefaulttoolsversion' DefaultTargets='Build' xmlns='msbuildnamespace'>
                  <ItemGroup>
                    <A Include=`*.foo`/>
                    <B Include=`1.foo;2.foo` Exclude=`*.foo`/>
                    <C Include=`2` Exclude=`*.bar`/>
                  </ItemGroup>
                </Project>
            ";

            var expected = new ProvenanceResultTupleList
            {
                ("A", Operation.Include, Provenance.Glob, 1),
                ("B", Operation.Exclude, Provenance.Glob, 1)
            };

            AssertProvenanceResult(expected, project, "2.foo");
        }

        [Fact]
        public void GetItemProvenanceGlobMatchesItselfAsGlob()
        {
            var project =
            @"<Project ToolsVersion='msbuilddefaulttoolsversion' DefaultTargets='Build' xmlns='msbuildnamespace'>
                  <ItemGroup>
                    <A Include=`ab*cd`/>
                    <B Include=`tx?yz`/>
                  </ItemGroup>
                </Project>
            ";

            var expected = new ProvenanceResultTupleList
            {
                ("A", Operation.Include, Provenance.Glob, 1),
            };

            AssertProvenanceResult(expected, project, "ab*cd");

            expected = new ProvenanceResultTupleList
            {
                ("B", Operation.Include, Provenance.Glob, 1),
            };

            AssertProvenanceResult(expected, project, "tx?yz");
        }

        [Fact]
        public void GetItemProvenanceResultsShouldBeInItemElementOrder()
        {
            var itemElements = NativeMethodsShared.GetLogicalCoreCount() * 5;
            var expected = new ProvenanceResultTupleList();

            var project =
            @"<Project ToolsVersion='msbuilddefaulttoolsversion' DefaultTargets='Build' xmlns='msbuildnamespace'>
                  <ItemGroup>
                    {0}
                  </ItemGroup>
                </Project>
            ";

            var sb = new StringBuilder();
            for (int i = 0; i < itemElements; i++)
            {
                sb.Append("<i_").Append(i).AppendLine(" Include=\"a\"/>");
                expected.Add(($"i_{i}", Operation.Include, Provenance.StringLiteral, 1));
            }

            project = string.Format(project, sb);

            AssertProvenanceResult(expected, project, "a");
        }

        [Fact]
        public void GetItemProvenanceShouldReturnTheSameResultsIfProjectIsReevaluated()
        {
            var projectContents =
            @"<Project ToolsVersion='msbuilddefaulttoolsversion' DefaultTargets='Build' xmlns='msbuildnamespace'>
                  <ItemGroup>
                    <A Include=`*.foo`/>
                    <B Include=`1.foo;2.foo` Exclude=`*.foo`/>
                    <C Include=`2` Exclude=`*.bar`/>
                  </ItemGroup>
                </Project>
            ";

            var expected = new ProvenanceResultTupleList
            {
                ("A", Operation.Include, Provenance.Glob, 1),
                ("B", Operation.Exclude, Provenance.Glob, 1)
            };

            // Create a project. The initial evaluation does not record the information needed for GetItemProvenance
            var project = ObjectModelHelpers.CreateInMemoryProject(projectContents);

            // Since GetItemProvenance does not have the required evaluator data (evaluated item elements), it internally reevaluates the project to collect it
            var provenanceResult = project.GetItemProvenance("2.foo");
            AssertProvenanceResult(expected, provenanceResult);

            // Dirty the xml to force another reevaluation.
            project.AddItem("new", "new value");
            project.ReevaluateIfNecessary();

            // Assert that provenance returns the same result and that no data duplication happened
            provenanceResult = project.GetItemProvenance("2.foo");
            AssertProvenanceResult(expected, provenanceResult);
        }

        [Fact]
        public void GetItemProvenanceShouldHandleComplexGlobExclusion()
        {
            var project =
            @"<Project ToolsVersion='msbuilddefaulttoolsversion' DefaultTargets='Build' xmlns='msbuildnamespace'>
                  <ItemGroup>
                    <A Include=`**\*.cs` Exclude=`**\bin\**`/>
                  </ItemGroup>
                </Project>
            ";

            var expected = new ProvenanceResultTupleList
            {
                ("A", Operation.Exclude, Provenance.Glob, 1)
            };

            AssertProvenanceResult(expected, project, @"bin\1.cs");
        }

        [Fact]
        public void GetItemProvenanceShouldHandleComplexGlobMismatch()
        {
            var project =
            @"<Project ToolsVersion='msbuilddefaulttoolsversion' DefaultTargets='Build' xmlns='msbuildnamespace'>
                  <ItemGroup>
                    <A Include=`**\*.cs` Exclude=`**\bin\**`/>
                  </ItemGroup>
                </Project>
            ";

            var expected = new ProvenanceResultTupleList();

            AssertProvenanceResult(expected, project, @"bin\1.foo");
        }

        [Fact]
        public void GetItemProvenanceGlobAndLiteral()
        {
            var project =
            @"<Project ToolsVersion='msbuilddefaulttoolsversion' DefaultTargets='Build' xmlns='msbuildnamespace'>
                  <ItemGroup>
                    <A Include=`*.foo;1.foo;1.foo`/>
                    <B Include=`1;2;1.foo` Exclude=`*.foo;1.foo;*.foo;1.foo;2.foo`/>
                    <C Include=`2;3` Exclude=`2`/>
                  </ItemGroup>
                </Project>
            ";

            var expected = new ProvenanceResultTupleList
            {
                ("A", Operation.Include, Provenance.Glob | Provenance.StringLiteral, 3),
                ("B", Operation.Exclude, Provenance.Glob | Provenance.StringLiteral, 4)
            };

            AssertProvenanceResult(expected, project, "1.foo");
        }

        [Fact]
        public void GetItemProvenanceByItemType()
        {
            var project =
            @"<Project ToolsVersion='msbuilddefaulttoolsversion' DefaultTargets='Build' xmlns='msbuildnamespace'>
                  <ItemGroup>
                    <A Include=`*.foo;1.foo`/>
                    <B Include=`*.foo;1.foo`/>
                    <B Include=`1;2;1.foo` Exclude=`*.foo;1.foo`/>
                    <C Include=`2;3` Exclude=`2`/>
                  </ItemGroup>
                </Project>
            ";

            var expected = new ProvenanceResultTupleList
            {
                ("B", Operation.Include, Provenance.Glob | Provenance.StringLiteral, 2),
                ("B", Operation.Exclude, Provenance.Glob | Provenance.StringLiteral, 2)
            };

            AssertProvenanceResult(expected, project, "1.foo", "B");
            AssertProvenanceResult(new ProvenanceResultTupleList(), project, "1.foo", "NotExistent");
        }

        public static IEnumerable<Object[]> GetItemProvenanceByProjectItemTestData
        {
            get
            {
                // Provenance for an item in the first element with multiple matching updates
                yield return new object[]
                {
                    @"
                    <A Include=`a;b;a`/>
                    <A Update=`a;b;c`/>
                    <A Update=`a*`/>
                    <A Update=`*a*;a`/>
                    ",
                    "a",
                    0, // first item 'a' from the include
                    new ProvenanceResultTupleList()
                    {
                        ("A", Operation.Include, Provenance.StringLiteral, 2),
                        ("A", Operation.Update, Provenance.StringLiteral, 1),
                        ("A", Operation.Update, Provenance.Glob, 1),
                        ("A", Operation.Update, Provenance.StringLiteral | Provenance.Glob, 2)
                    }
                };

                // Provenance for an item in the last element. Nothing matches
                yield return new object[]
                {
                    @"
                    <A Include=`a;b;a`/>
                    <A Update=`a`/>
                    <A Update=`b`/>
                    <A Include=`a;b`/>
                    ",
                    "a",
                    2, // item 'a' from last include
                    new ProvenanceResultTupleList()
                    {
                        ("A", Operation.Include, Provenance.StringLiteral, 1)
                    }
                };

                // Nothing matches
                yield return new object[]
                {
                    @"
                    <A Include=`a;b;c`/>
                    <A Update=`c;*ab`/>
                    <A Remove=`b`/>
                    <A Include=`a;a`/>
                    ",
                    "a",
                    0, // item 'a' from first include
                    new ProvenanceResultTupleList()
                    {
                        ("A", Operation.Include, Provenance.StringLiteral, 1)
                    }
                };

                yield return new object[]
                {
                    @"
                    <A Remove=`a`/>

                    <A Include=`a;b;c;a`/>
                    <A Update=`a`/>
                    <A Update=`b`/>
                    <B Update=`a`/>
                    <A Remove=`b`/>

                    <A Include=`a;b`/>
                    <A Update=`a;a`/>
                    <A Update=`b`/>
                    <B Update=`a`/>
                    <A Remove=`b`/>

                    <A Include=`a;a`/>
                    <A Update=`a;a*`/>
                    <A Update=`b`/>
                    <B Update=`a`/>
                    <A Remove=`b`/>
                    ",
                    "a",
                    2, // item 'a' from second include
                    new ProvenanceResultTupleList
                    {
                        ("A", Operation.Include, Provenance.StringLiteral, 1),
                        ("A", Operation.Update, Provenance.StringLiteral, 2),
                        ("A", Operation.Update, Provenance.StringLiteral | Provenance.Glob, 2)
                    }
                };
            }
        }

        [Theory]
        [MemberData(nameof(GetItemProvenanceByProjectItemTestData))]
        public void GetItemProvenanceByProjectItem(string items, string itemValue, int itemPosition, ProvenanceResultTupleList expected)
        {
            var formattedProject = string.Format(ProjectWithItemGroup, items);
            AssertProvenanceResult(expected, formattedProject, itemValue, itemPosition);
        }

        [Fact]
        public void GetItemProvenanceWhenExcludeHasIndirectReferences()
        {
            var project =
                @"<Project ToolsVersion='msbuilddefaulttoolsversion' DefaultTargets='Build' xmlns='msbuildnamespace'>
                  <ItemGroup>
                    <B Include=`1;2;3`/>
                    <A Include=`1;2;3` Exclude=`$(P);@(B)`/>
                  </ItemGroup>

                  <PropertyGroup>
                    <P>1;2;3;@(B)</P>
                  </PropertyGroup>
                </Project>
                ";

            var expected = new ProvenanceResultTupleList
            {
                ("B", Operation.Include, Provenance.StringLiteral, 1),
                ("A", Operation.Exclude, Provenance.Inconclusive | Provenance.StringLiteral, 3)
            };

            AssertProvenanceResult(expected, project, "1");
        }

        [Fact]
        public void GetItemProvenanceWhenIncludeHasIndirectReferences()
        {
            var project =
                @"<Project ToolsVersion='msbuilddefaulttoolsversion' DefaultTargets='Build' xmlns='msbuildnamespace'>
                  <ItemGroup>
                    <B Include=`1;2;3`/>
                    <A Include=`$(P);@(B)`/>
                    <C Include=`@(A)`/>
                  </ItemGroup>

                  <PropertyGroup>
                    <P>1;2;3;@(B)</P>
                  </PropertyGroup>
                </Project>
                ";

            var expected = new ProvenanceResultTupleList
            {
                ("B", Operation.Include, Provenance.StringLiteral, 1),
                ("A", Operation.Include, Provenance.Inconclusive | Provenance.StringLiteral, 3),
                ("C", Operation.Include, Provenance.Inconclusive, 3)
            };

            AssertProvenanceResult(expected, project, "1");
        }

        [Fact]
        public void GetItemProvenanceWhenIncludeHasIndirectItemReferencesAndOnlyGlobsExistDirectly()
        {
            var project =
                @"<Project ToolsVersion='msbuilddefaulttoolsversion' DefaultTargets='Build' xmlns='msbuildnamespace'>
                  <ItemGroup>
                    <B Include=`1;2;3`/>
                    <A Include=`*;$(P);@(B)`/>
                  </ItemGroup>

                  <PropertyGroup>
                    <P>@(B)</P>
                  </PropertyGroup>
                </Project>
                ";

            var expected = new ProvenanceResultTupleList
            {
                ("B", Operation.Include, Provenance.StringLiteral, 1),
                ("A", Operation.Include, Provenance.Inconclusive | Provenance.Glob, 3)
            };

            AssertProvenanceResult(expected, project, "1");
        }

        [Fact]
        // As a perf optimization, GetItemProvenance always appends Inconclusive when property references are present, even if the property does not contribute any item that matches the provenance call
        // Item references do not append Inconclusive when they do not contribute matching items.
        public void GetItemProvenanceShouldReturnInconclusiveWhenIndirectPropertyDoesNotMatch()
        {
            var project =
                @"<Project ToolsVersion='msbuilddefaulttoolsversion' DefaultTargets='Build' xmlns='msbuildnamespace'>
                  <ItemGroup>
                    <A Include=`1`/>
                    <B Include=`a;$(P)`/>
                    <C Include=`a;@(A)`/>
                  </ItemGroup>

                  <PropertyGroup>
                    <P></P>
                  </PropertyGroup>
                </Project>
                ";

            var expected = new ProvenanceResultTupleList
            {
                ("B", Operation.Include, Provenance.StringLiteral | Provenance.Inconclusive, 1),
                ("C", Operation.Include, Provenance.StringLiteral, 1)
            };

            AssertProvenanceResult(expected, project, "a");
        }

        [Fact]
        public void GetItemProvenanceShouldRespectItemConditions()
        {
            var project =
                @"<Project ToolsVersion='msbuilddefaulttoolsversion' DefaultTargets='Build' xmlns='msbuildnamespace'>
                  <ItemGroup>
                    <A Include=`1` Condition=`1 == 0`/>
                  </ItemGroup>
                </Project>
                ";

            var expected = new ProvenanceResultTupleList();

            AssertProvenanceResult(expected, project, "1");
        }

        [Fact]
        public void GetItemProvenanceShouldRespectItemGroupConditions()
        {
            var project =
                @"<Project ToolsVersion='msbuilddefaulttoolsversion' DefaultTargets='Build' xmlns='msbuildnamespace'>
                  <ItemGroup Condition=`1 == 0`>
                    <A Include=`1`/>
                  </ItemGroup>
                </Project>
                ";

            var expected = new ProvenanceResultTupleList();

            AssertProvenanceResult(expected, project, "1");
        }

        [Fact]
        public void GetItemProvenanceShouldNotLookIntoTargets()
        {
            var project =
                @"<Project ToolsVersion='msbuilddefaulttoolsversion' DefaultTargets='Build' xmlns='msbuildnamespace'>
                  <Target Name=`Build`>
                      <ItemGroup>
                        <A Include=`1`/>
                      </ItemGroup>
                  </Target>
                </Project>
                ";

            var expected = new ProvenanceResultTupleList();

            AssertProvenanceResult(expected, project, "1");
        }

        [Fact]
        public void GetItemProvenanceMatchesLiteralsWithNonCanonicPaths()
        {
            var projectContents =
                @"<Project ToolsVersion='msbuilddefaulttoolsversion' DefaultTargets='Build' xmlns='msbuildnamespace'>
                  <ItemGroup>
                    <A Include=`1.foo;.\1.foo;.\.\1.foo`/>
                    <B Include=`../../u/x/d11/d21/../d22/../../d12/2.foo`/>
                  </ItemGroup>
                </Project>
                ";

            var expected1Foo = new ProvenanceResultTupleList
            {
                ("A", Operation.Include, Provenance.StringLiteral, 3)
            };

            AssertProvenanceResult(expected1Foo, projectContents, "1.foo");
            AssertProvenanceResult(expected1Foo, projectContents, @".\1.foo");

            using (var env = TestEnvironment.Create())
            {
                var projectCollection = env.CreateProjectCollection().Collection;
                var testFiles = env.CreateTestProjectWithFiles(projectContents, Array.Empty<string>(), "u/x");
                var project = new Project(testFiles.ProjectFile, new Dictionary<string, string>(), MSBuildConstants.CurrentToolsVersion, projectCollection);

                var expected2Foo = new ProvenanceResultTupleList
                {
                    ("B", Operation.Include, Provenance.StringLiteral, 1)
                };

                AssertProvenanceResult(expected2Foo, project.GetItemProvenance(@"../x/d13/../../x/d12/d23/../2.foo"));
                AssertProvenanceResult(new ProvenanceResultTupleList(), project.GetItemProvenance(@"../x/d13/../x/d12/d23/../2.foo"));
            }
        }

        [Fact]
        public void GetItemProvenanceMatchesAbsoluteAndRelativePaths()
        {
            var projectContents =
                @"<Project ToolsVersion='msbuilddefaulttoolsversion' DefaultTargets='Build' xmlns='msbuildnamespace'>
                  <ItemGroup>
                    <A Include=`1.foo`/>
                    <B Include=`$(MSBuildProjectDirectory)\1.foo`/>
                  </ItemGroup>
                </Project>
                ";

            using (var env = TestEnvironment.Create())
            {
                var projectCollection = env.CreateProjectCollection().Collection;

                var testFiles = env.CreateTestProjectWithFiles(projectContents, Array.Empty<string>());

                var project = new Project(testFiles.ProjectFile, new Dictionary<string, string>(), MSBuildConstants.CurrentToolsVersion, projectCollection);

                var expectedProvenance = new ProvenanceResultTupleList
                {
                    ("A", Operation.Include, Provenance.StringLiteral, 1),
                    ("B", Operation.Include, Provenance.StringLiteral | Provenance.Inconclusive, 1)
                };

                AssertProvenanceResult(expectedProvenance, project.GetItemProvenance(@"1.foo"));

                var absoluteFile = Path.Combine(Path.GetDirectoryName(testFiles.ProjectFile), "1.foo");
                AssertProvenanceResult(expectedProvenance, project.GetItemProvenance(absoluteFile));
            }
        }

        [Fact]
        public void GetItemProvenanceShouldNotFailWithIllegalPathCharacters()
        {
            var project =
                @"<Project ToolsVersion='msbuilddefaulttoolsversion' DefaultTargets='Build' xmlns='msbuildnamespace'>
                  <ItemGroup>
                    <A Include=`|:/\`/>
                  </ItemGroup>
                </Project>
                ";

            var expected = new ProvenanceResultTupleList();

            AssertProvenanceResult(expected, project, @"?:/*\|");

            expected.Add(("A", Operation.Include, Provenance.StringLiteral, 1));

            AssertProvenanceResult(expected, project, @"|:/\");
        }

        [Fact]
        public void GetItemProvenanceShouldNotFailWithStringsExceedingMaxPath()
        {
            var longString = new string('a', 1000);

            var project =
                @"<Project ToolsVersion='msbuilddefaulttoolsversion' DefaultTargets='Build' xmlns='msbuildnamespace'>
                  <ItemGroup>
                    <A Include=`" + longString + @"`/>
                  </ItemGroup>
                </Project>
                ";

            var expected = new ProvenanceResultTupleList();

            AssertProvenanceResult(expected, project, longString + "a");
        }

        [Fact]
        public void GetItemProvenancePathMatchingShouldBeCaseInsensitive()
        {
            var project =
                @"<Project ToolsVersion='msbuilddefaulttoolsversion' DefaultTargets='Build' xmlns='msbuildnamespace'>
                  <ItemGroup>
                    <A Include=`a`/>
                  </ItemGroup>
                </Project>
                ";

            var expected = new ProvenanceResultTupleList
            {
                ("A", Operation.Include, Provenance.StringLiteral, 1)
            };

            AssertProvenanceResult(expected, project, FileUtilities.GetIsFileSystemCaseSensitive() ? "a" : "A");
        }


        public static IEnumerable<object[]> GetItemProvenanceShouldWorkWithEscapedCharactersTestData
        {
            get
            {
                var projectTemplate =
                @"<Project ToolsVersion='msbuilddefaulttoolsversion' DefaultTargets='Build' xmlns='msbuildnamespace'>
                  <ItemGroup>
                    <A Include=`{0}`/>
                    <A Update=`{0}`/>
                    <A Remove=`{0}`/>
                  </ItemGroup>
                </Project>";

                yield return new object[]
                {
                    // the itemspec for the include, update, and remove
                    string.Format(projectTemplate, "a;%61;*%61*"),
                    // the string argument sent to GetItemProvenance
                    "a",
                    // the expected GetItemProvenance result
                    new ProvenanceResultTupleList
                    {
                        ("A", Operation.Include, Provenance.StringLiteral | Provenance.Glob, 3),
                        ("A", Operation.Update, Provenance.StringLiteral | Provenance.Glob, 3),
                        ("A", Operation.Remove, Provenance.StringLiteral | Provenance.Glob, 3)
                    }
                };

                yield return new object[]
                {
                    string.Format(projectTemplate, "a;%61;*%61*"),
                    "%61",
                    new ProvenanceResultTupleList()
                };

                yield return new object[]
                {
                    string.Format(projectTemplate, "%61b%63"),
                    "abc",
                    new ProvenanceResultTupleList
                    {
                        ("A", Operation.Include, Provenance.StringLiteral, 1),
                        ("A", Operation.Update, Provenance.StringLiteral, 1),
                        ("A", Operation.Remove, Provenance.StringLiteral, 1)
                    }
                };

                yield return new object[]
                {
                    string.Format(projectTemplate, "%61b%63"),
                    "ab%63",
                    new ProvenanceResultTupleList()
                };

                yield return new object[]
                {
                    string.Format(projectTemplate, "a?c"),
                    "ab%63",
                    new ProvenanceResultTupleList()
                };

                yield return new object[]
                {
                    string.Format(projectTemplate, "a?c"),
                    "a%62c",
                    new ProvenanceResultTupleList()
                };

                yield return new object[]
                {
                    string.Format(projectTemplate, "a?%63"),
                    "abc",
                    new ProvenanceResultTupleList
                    {
                        ("A", Operation.Include, Provenance.Glob, 1),
                        ("A", Operation.Update, Provenance.Glob, 1),
                        ("A", Operation.Remove, Provenance.Glob, 1)
                    }
                };

                yield return new object[]
                {
                    string.Format(projectTemplate, "a?%63"),
                    "ab%63",
                    new ProvenanceResultTupleList()
                };

                yield return new object[]
                {
                    string.Format(projectTemplate, "a?%63"),
                    "a%62c",
                    new ProvenanceResultTupleList()
                };

                yield return new object[]
                {
                    string.Format(projectTemplate, "a*c"),
                    "a%62c",
                    new ProvenanceResultTupleList
                    {
                        ("A", Operation.Include, Provenance.Glob, 1),
                        ("A", Operation.Update, Provenance.Glob, 1),
                        ("A", Operation.Remove, Provenance.Glob, 1)
                    }
                };

                yield return new object[]
                {
                    string.Format(projectTemplate, "a*%63"),
                    "abcdefc",
                    new ProvenanceResultTupleList
                    {
                        ("A", Operation.Include, Provenance.Glob, 1),
                        ("A", Operation.Update, Provenance.Glob, 1),
                        ("A", Operation.Remove, Provenance.Glob, 1)
                    }
                };

                yield return new object[]
                {
                    string.Format(projectTemplate, "a*%63"),
                    "a%62%61c",
                    new ProvenanceResultTupleList
                    {
                        ("A", Operation.Include, Provenance.Glob, 1),
                        ("A", Operation.Update, Provenance.Glob, 1),
                        ("A", Operation.Remove, Provenance.Glob, 1)
                    }
                };
            }
        }
        [Theory]
        [MemberData(nameof(GetItemProvenanceShouldWorkWithEscapedCharactersTestData))]
        public void GetItemProvenanceShouldWorkWithEscapedCharacters(string project, string provenanceArgument, ProvenanceResultTupleList expectedProvenance)
        {
            AssertProvenanceResult(expectedProvenance, project, provenanceArgument);
        }

        [Fact]
        public void GetItemProvenanceShouldWorkWithUpdateElements()
        {
            var project =
                @"<Project ToolsVersion='msbuilddefaulttoolsversion' DefaultTargets='Build' xmlns='msbuildnamespace'>
                  <ItemGroup>
                    <A Include=`1.foo`/>

                    <B Update=`1.bar`/>
                    <C Update=`1.foo`/>
                    <D Update=`1.foo;*.foo`/>
                    <E Update=`$(P);@(A)`/>
                  </ItemGroup>
                  <PropertyGroup>
                    <P>*.foo;@(A)</P>
                  </PropertyGroup>
                </Project>
                ";

            var expected = new ProvenanceResultTupleList
            {
                ("A", Operation.Include, Provenance.StringLiteral, 1),
                ("C", Operation.Update, Provenance.StringLiteral, 1),
                ("D", Operation.Update, Provenance.StringLiteral | Provenance.Glob, 2),
                ("E", Operation.Update, Provenance.Glob | Provenance.Inconclusive, 3)
            };

            AssertProvenanceResult(expected, project, "1.foo");
        }

        [Fact]
        public void GetItemProvenanceShouldWorkWithRemoveElements()
        {
            var project =
                @"<Project ToolsVersion='msbuilddefaulttoolsversion' DefaultTargets='Build' xmlns='msbuildnamespace'>
                  <ItemGroup>
                    <A Include=`1.foo`/>

                    <B Remove=`1.bar`/>
                    <C Remove=`1.foo`/>
                    <D Remove=`1.foo;*.foo`/>
                    <E Remove=`$(P);@(A)`/>
                  </ItemGroup>
                  <PropertyGroup>
                    <P>*.foo;@(A)</P>
                  </PropertyGroup>
                </Project>
                ";

            var expected = new ProvenanceResultTupleList
            {
                ("A", Operation.Include, Provenance.StringLiteral, 1),
                ("C", Operation.Remove, Provenance.StringLiteral, 1),
                ("D", Operation.Remove, Provenance.StringLiteral | Provenance.Glob, 2),
                ("E", Operation.Remove, Provenance.Glob | Provenance.Inconclusive, 3)
            };

            AssertProvenanceResult(expected, project, "1.foo");
        }

        public static IEnumerable<object[]> GetItemProvenanceShouldBeSensitiveToGlobbingConeTestData => GlobbingTestData.GlobbingConesTestData;

        [Theory]
        [MemberData(nameof(GetItemProvenanceShouldBeSensitiveToGlobbingConeTestData))]
        public void GetItemProvenanceShouldBeSensitiveToGlobbingCone(string includeGlob, string getItemProvenanceArgument, string relativePathOfProjectFile, bool provenanceShouldFindAMatch)
        {
            var projectContents =
                @"<Project ToolsVersion='msbuilddefaulttoolsversion' DefaultTargets='Build' xmlns='msbuildnamespace'>
                  <ItemGroup>
                    <A Include=`{0}`/>
                  </ItemGroup>
                </Project>
                ";

            projectContents = string.Format(projectContents, includeGlob);

            using (var env = TestEnvironment.Create())
            using (var projectCollection = new ProjectCollection())
            {
                var testFiles = env.CreateTestProjectWithFiles(projectContents, Array.Empty<string>(), relativePathOfProjectFile);
                var project = new Project(testFiles.ProjectFile, new Dictionary<string, string>(), MSBuildConstants.CurrentToolsVersion, projectCollection);

                ProvenanceResultTupleList expectedProvenance = null;

                var provenanceKind = includeGlob.IndexOfAny(new[] { '*', '?' }) != -1 ? Provenance.Glob : Provenance.StringLiteral;
                expectedProvenance = provenanceShouldFindAMatch
                    ? new ProvenanceResultTupleList
                    {
                        ("A", Operation.Include, provenanceKind, 1)
                    }
                    : new ProvenanceResultTupleList();

                AssertProvenanceResult(expectedProvenance, project.GetItemProvenance(getItemProvenanceArgument));
            }
        }

        [Fact]
        public void GetAllGlobsShouldNotFindGlobsIfThereAreNoItemElements()
        {
            var project =
                @"<Project ToolsVersion='msbuilddefaulttoolsversion' DefaultTargets='Build' xmlns='msbuildnamespace'>
                  <ItemGroup>
                  </ItemGroup>
                </Project>
                ";

            var expected = new GlobResultList();

            AssertGlobResult(expected, project);
        }

        [Fact]
        public void GetAllGlobsShouldNotFindGlobsIfThereAreNone()
        {
            var project =
                @"<Project ToolsVersion='msbuilddefaulttoolsversion' DefaultTargets='Build' xmlns='msbuildnamespace'>
                  <ItemGroup>
                    <A Include=`1;2;3` Exclude=`1;*;3`/>
                    <B Include=`a;b;c` Exclude=`**`/>
                  </ItemGroup>
                </Project>
                ";

            var expected = new GlobResultList();

            AssertGlobResult(expected, project);
        }

        [Fact]
        public void GetAllGlobsShouldNotFindGlobsIfInvokedWithEmptyOrNullArguments()
        {
            var project =
                @"<Project ToolsVersion='msbuilddefaulttoolsversion' DefaultTargets='Build' xmlns='msbuildnamespace'>
                  <ItemGroup>
                    <A Include=`1;**` Exclude=`1;*;3`/>
                    <A Include=`1;2;*` Exclude=`1;*;3`/>
                    <B Include=`a;**;c` Exclude=`**`/>
                  </ItemGroup>
                </Project>
                ";

            var expected = new GlobResultList();

            AssertGlobResult(expected, project, "");
            AssertGlobResult(expected, project, null);
        }

        [Fact]
        public void GetAllGlobsShouldFindDirectlyReferencedGlobs()
        {
            var project =
                @"<Project ToolsVersion='msbuilddefaulttoolsversion' DefaultTargets='Build' xmlns='msbuildnamespace'>
                  <ItemGroup>
                    <A Include=`*.a;1;*;2;**;?a` Exclude=`1;*;3`/>
                    <B Include=`a;b;c` Exclude=`**`/>
                  </ItemGroup>
                </Project>
                ";

            var expectedIncludes = new[] { "*.a", "*", "**", "?a" };
            var expectedExcludes = new[] { "1", "*", "3" }.ToImmutableHashSet();
            var expected = new GlobResultList
            {
                ("A", expectedIncludes, expectedExcludes, ImmutableHashSet.Create<string>())
            };

            AssertGlobResult(expected, project);
        }

        [Fact]
        public void GetAllGlobsShouldFindAllExcludesAndRemoves()
        {
            var project =
                @"<Project ToolsVersion='msbuilddefaulttoolsversion' DefaultTargets='Build' xmlns='msbuildnamespace'>
                  <ItemGroup>
                    <A Include=`*` Exclude=`e*`/>
                    <A Remove=`a`/>
                    <A Remove=`b`/>
                    <A Include=`**` Exclude=`e**`/>
                    <A Remove=`c`/>
                  </ItemGroup>
                </Project>
                ";

            var expected = new GlobResultList
            {
                ("A", new []{"**"}, new [] {"e**"}.ToImmutableHashSet(), new [] {"c"}.ToImmutableHashSet()),
                ("A", new []{"*"}, new [] {"e*"}.ToImmutableHashSet(), new [] {"c", "b", "a"}.ToImmutableHashSet()),
            };

            AssertGlobResult(expected, project);
        }

        [Theory]
        // [InlineData(
        //            @"
        // <A Include=`a;b*;c*;d*;e*;f*` Exclude=`c*;d*`/>
        // <A Remove=`e*;f*`/>
        // ",
        //        new[] {"ba"},
        //        new[] {"a", "ca", "da", "ea", "fa"}
        //        )]
        //        [InlineData(
        //            @"
        // <A Include=`a;b*;c*;d*;e*;f*` Exclude=`c*;d*`/>
        // ",
        //        new[] {"ba", "ea", "fa"},
        //        new[] {"a", "ca", "da"}
        //        )]
        //        [InlineData(
        //            @"
        // <A Include=`a;b*;c*;d*;e*;f*`/>
        // ",
        //        new[] {"ba", "ca", "da", "ea", "fa"},
        //        new[] {"a"}
        //        )]
        [InlineData(
            @"
<E Include=`b`/>
<R Include=`c`/>

<A Include=`a*;b*;c*` Exclude=`@(E)`/>
<A Remove=`@(R)`/>
",
        new[] { "aa", "bb", "cc" },
        new[] { "b", "c" })]
        [InlineData(
            @"<A Include=`ab*;b|c*;de*`/>",
            new[] { "ab", "de" },
            new[] { "bc", "b|c", "b", "c" })]
        public void GetAllGlobsShouldProduceGlobThatMatches(string itemContents, string[] stringsThatShouldMatch, string[] stringsThatShouldNotMatch)
        {
            var projectTemplate =
                @"<Project ToolsVersion='msbuilddefaulttoolsversion' DefaultTargets='Build' xmlns='msbuildnamespace'>
                  <ItemGroup>
                    {0}
                  </ItemGroup>
                </Project>
                ";

            var projectContents = string.Format(projectTemplate, itemContents);

            var getAllGlobsResult = ObjectModelHelpers.CreateInMemoryProject(projectContents).GetAllGlobs();

            var uberGlob = CompositeGlob.Create(getAllGlobsResult.Select(r => r.MsBuildGlob));

            foreach (var matchingString in stringsThatShouldMatch)
            {
                uberGlob.IsMatch(matchingString).ShouldBeTrue();
            }

            foreach (var nonMatchingString in stringsThatShouldNotMatch)
            {
                uberGlob.IsMatch(nonMatchingString).ShouldBeFalse();
            }
        }

        [Fact]
        public void GetAllGlobsShouldProduceGlobsThatMatchAbsolutePaths()
        {
            var projectContents =
                @"<Project ToolsVersion='msbuilddefaulttoolsversion' DefaultTargets='Build' xmlns='msbuildnamespace'>
                  <ItemGroup>
                    <A Include=`*.cs`/>
                    <B Include=`$(MSBuildProjectDirectory)\*.cs`/>
                  </ItemGroup>
                </Project>
                ";

            using (var env = TestEnvironment.Create())
            {
                var projectCollection = env.CreateProjectCollection().Collection;

                var testFiles = env.CreateTestProjectWithFiles(projectContents, Array.Empty<string>());

                var project = new Project(testFiles.ProjectFile, new Dictionary<string, string>(), MSBuildConstants.CurrentToolsVersion, projectCollection);

                var absoluteFile = Path.Combine(Path.GetDirectoryName(testFiles.ProjectFile), "a.cs");

                foreach (var globResult in project.GetAllGlobs())
                {
                    globResult.MsBuildGlob.IsMatch("a.cs").ShouldBeTrue();
                    globResult.MsBuildGlob.IsMatch(absoluteFile).ShouldBeTrue();
                }
            }
        }

        [Fact]
        public void GetAllGlobsShouldFindGlobsByItemType()
        {
            var project =
                @"<Project ToolsVersion='msbuilddefaulttoolsversion' DefaultTargets='Build' xmlns='msbuildnamespace'>
                  <ItemGroup>
                    <A Include=`*.a;1;*;2;**;` Exclude=`1;*;3`/>
                    <B Include=`a;**;b;c;*` Exclude=`**`/>
                  </ItemGroup>
                </Project>
                ";

            var expectedIncludes = new[] { "*.a", "*", "**" };
            var expectedExcludes = new[] { "1", "*", "3" }.ToImmutableHashSet();
            var expected = new GlobResultList
            {
                ("A", expectedIncludes, expectedExcludes, ImmutableHashSet<string>.Empty)
            };

            AssertGlobResult(expected, project, "A");
            AssertGlobResult(new GlobResultList(), project, "NotExistent");
        }

        [Fact]
        public void GetAllGlobsShouldFindIndirectlyReferencedGlobsFromProperties()
        {
            var project =
                @"<Project ToolsVersion='msbuilddefaulttoolsversion' DefaultTargets='Build' xmlns='msbuildnamespace'>
                  <ItemGroup>
                    <A Include=`$(P)` Exclude=`$(P)`/>
                  </ItemGroup>
                  <PropertyGroup>
                    <P>*</P>
                  </PropertyGroup>
                </Project>
                ";

            var expected = new GlobResultList
            {
                ("A", new []{"*"}, new[] {"*"}.ToImmutableHashSet(), ImmutableHashSet<string>.Empty),
            };

            AssertGlobResult(expected, project);
        }

        [Fact]
        public void GetAllGlobsShouldNotFindIndirectlyReferencedGlobsFromItems()
        {
            var project =
                @"<Project ToolsVersion='msbuilddefaulttoolsversion' DefaultTargets='Build' xmlns='msbuildnamespace'>
                  <ItemGroup>
                    <A Include=`*`/>
                    <B Include=`@(A)`/>
                    <C Include=`**` Exclude=`@(A)`/>
                    <C Remove=`@(A)` />
                  </ItemGroup>
                </Project>
                ";

            using (var env = TestEnvironment.Create())
            using (var projectCollection = new ProjectCollection())
            {
                var testFiles = env.CreateTestProjectWithFiles(project, new[] { "a", "b" });
                var globResult = new Project(testFiles.ProjectFile, null, MSBuildConstants.CurrentToolsVersion, projectCollection).GetAllGlobs();

                var expected = new GlobResultList
                {
                    ("C", new []{"**"}, new [] {"build.proj", "a", "b"}.ToImmutableHashSet(), new [] {"build.proj", "a", "b"}.ToImmutableHashSet()),
                    ("A", new []{"*"}, ImmutableHashSet<string>.Empty, ImmutableHashSet<string>.Empty)
                };

                AssertGlobResultsEqual(expected, globResult);
            }
        }

        [Fact]
        public void ProjectInstanceShouldInitiallyHaveSameEvaluationIdAsTheProjectItCameFrom()
        {
            using (var env = TestEnvironment.Create())
            {
                var projectCollection = env.CreateProjectCollection().Collection;

                var project = new Project(null, null, projectCollection);
                var initialEvaluationId = project.LastEvaluationId;

                var projectInstance = project.CreateProjectInstance();

                initialEvaluationId.ShouldNotBe(BuildEventContext.InvalidEvaluationId);
                projectInstance.EvaluationId.ShouldBe(initialEvaluationId);

                // trigger a new evaluation which increments the evaluation ID in the Project
                project.AddItem("foo", "bar");
                project.ReevaluateIfNecessary();

                project.LastEvaluationId.ShouldNotBe(initialEvaluationId);
                projectInstance.EvaluationId.ShouldBe(initialEvaluationId);

                var newProjectInstance = project.CreateProjectInstance();
                newProjectInstance.EvaluationId.ShouldBe(project.LastEvaluationId);
            }
        }

        [Fact]
        [Trait("Category", "netcore-osx-failing")] // https://github.com/dotnet/msbuild/issues/2226
        [Trait("Category", "netcore-linux-failing")] // https://github.com/dotnet/msbuild/issues/2226
        public void ProjectImportedEventFalseCondition()
        {
            using (var env = TestEnvironment.Create(_output))
            {
                env.SetEnvironmentVariable("MSBUILDLOGIMPORTS", "1");
                ProjectRootElement pre = ProjectRootElement.Create(env.CreateFile(".proj").Path);

                using (ProjectCollection collection = new ProjectCollection())
                {
                    MockLogger logger = new MockLogger();
                    collection.RegisterLogger(logger);

                    pre.AddPropertyGroup().AddProperty("NotUsed", "");

                    var import = pre.AddImport(@"$(MSBuildExtensionsPath)\Foo");
                    import.Condition = " '$(Something)' == 'nothing' ";

                    pre.Save();
                    pre.Reload();

                    Project unused = new Project(pre, null, null, collection);

                    ProjectImportedEventArgs eventArgs = logger.AllBuildEvents.SingleOrDefault(i => i is ProjectImportedEventArgs) as ProjectImportedEventArgs;

                    eventArgs.ShouldNotBeNull();

                    eventArgs.UnexpandedProject.ShouldBe(import.Project);

                    eventArgs.ImportedProjectFile.ShouldBeNull();

                    eventArgs.ProjectFile.ShouldBe(pre.FullPath);

                    eventArgs.LineNumber.ShouldBe(6);
                    eventArgs.ColumnNumber.ShouldBe(3);

                    logger.AssertLogContains($"Project \"{import.Project}\" was not imported by \"{pre.FullPath}\" at ({eventArgs.LineNumber},{eventArgs.ColumnNumber}), due to false condition; ( \'$(Something)\' == \'nothing\' ) was evaluated as ( \'\' == \'nothing\' ).");
                }
            }
        }

        [Fact]
        [Trait("Category", "netcore-osx-failing")] // https://github.com/dotnet/msbuild/issues/2226
        [Trait("Category", "netcore-linux-failing")] // https://github.com/dotnet/msbuild/issues/2226
        public void ProjectImportedEventNoMatchingFiles()
        {
            using (var env = TestEnvironment.Create(_output))
            {
                env.SetEnvironmentVariable("MSBUILDLOGIMPORTS", "1");
                ProjectRootElement pre = ProjectRootElement.Create(env.CreateFile(".proj").Path);

                pre.AddPropertyGroup().AddProperty("NotUsed", "");
                var import = pre.AddImport(@"Foo\*");

                pre.Save();
                pre.Reload();

                using (ProjectCollection collection = new ProjectCollection())
                {
                    MockLogger logger = new MockLogger();
                    collection.RegisterLogger(logger);

                    Project unused = new Project(pre, null, null, collection);

                    ProjectImportedEventArgs eventArgs = logger.AllBuildEvents.SingleOrDefault(i => i is ProjectImportedEventArgs) as ProjectImportedEventArgs;

                    eventArgs.ShouldNotBeNull();

                    eventArgs.UnexpandedProject.ShouldBe(import.Project);

                    eventArgs.ImportedProjectFile.ShouldBeNull();

                    eventArgs.ProjectFile.ShouldBe(pre.FullPath);

                    eventArgs.LineNumber.ShouldBe(6);
                    eventArgs.ColumnNumber.ShouldBe(3);

                    logger.AssertLogContains($"Project \"{import.Project}\" was not imported by \"{pre.FullPath}\" at ({eventArgs.LineNumber},{eventArgs.ColumnNumber}), due to no matching files.");
                }
            }
        }

        [Fact]
        public void ProjectImportedEventEmptyFile()
        {
            using (var env = TestEnvironment.Create(_output))
            {
                env.SetEnvironmentVariable("MSBUILDLOGIMPORTS", "1");

                const string contents = @"<?xml version=""1.0"" encoding=""utf-8""?>
";
                var importFile = env.CreateFile(".targets");
                File.WriteAllText(importFile.Path, contents);
                ProjectRootElement pre = ProjectRootElement.Create(env.CreateFile(".proj").Path);

                pre.AddPropertyGroup().AddProperty("NotUsed", "");
                var import = pre.AddImport(importFile.Path);

                pre.Save();
                pre.Reload();

                using (ProjectCollection collection = new ProjectCollection())
                {
                    MockLogger logger = new MockLogger();
                    collection.RegisterLogger(logger);

                    Project unused = new Project(pre, null, null, collection, ProjectLoadSettings.IgnoreEmptyImports);

                    ProjectImportedEventArgs eventArgs = logger.AllBuildEvents.SingleOrDefault(i => i is ProjectImportedEventArgs) as ProjectImportedEventArgs;

                    eventArgs.ShouldNotBeNull();
                    eventArgs.ImportIgnored.ShouldBeTrue();

                    eventArgs.UnexpandedProject.ShouldBe(import.Project);

                    eventArgs.ImportedProjectFile.ShouldBe(importFile.Path);

                    eventArgs.ProjectFile.ShouldBe(pre.FullPath);

                    eventArgs.LineNumber.ShouldBe(6);
                    eventArgs.ColumnNumber.ShouldBe(3);

                    logger.AssertLogContains($"Project \"{import.Project}\" was not imported by \"{pre.FullPath}\" at ({eventArgs.LineNumber},{eventArgs.ColumnNumber}), due to the file being empty.");
                }
            }
        }

        [Fact]
        public void ProjectImportedEventInvalidFile()
        {
            using (var env = TestEnvironment.Create(_output))
            {
                env.SetEnvironmentVariable("MSBUILDLOGIMPORTS", "1");

                const string contents = @"<?xml version=""1.0"" encoding=""utf-8""?>
<Project>BROKEN</Project>
";

                var importFile = env.CreateFile(".targets");
                File.WriteAllText(importFile.Path, contents);
                ProjectRootElement pre = ProjectRootElement.Create(env.CreateFile(".proj").Path);

                pre.AddPropertyGroup().AddProperty("NotUsed", "");
                var import = pre.AddImport(importFile.Path);

                pre.Save();
                pre.Reload();

                using (ProjectCollection collection = new ProjectCollection())
                {
                    MockLogger logger = new MockLogger();
                    collection.RegisterLogger(logger);

                    Project unused = new Project(pre, null, null, collection, ProjectLoadSettings.IgnoreInvalidImports);

                    ProjectImportedEventArgs eventArgs = logger.AllBuildEvents.SingleOrDefault(i => i is ProjectImportedEventArgs) as ProjectImportedEventArgs;

                    eventArgs.ShouldNotBeNull();
                    eventArgs.ImportIgnored.ShouldBeTrue();

                    eventArgs.UnexpandedProject.ShouldBe(import.Project);

                    eventArgs.ImportedProjectFile.ShouldBe(importFile.Path);

                    eventArgs.ProjectFile.ShouldBe(pre.FullPath);

                    eventArgs.LineNumber.ShouldBe(6);
                    eventArgs.ColumnNumber.ShouldBe(3);

                    logger.AssertLogContains($"Project \"{import.Project}\" was not imported by \"{pre.FullPath}\" at ({eventArgs.LineNumber},{eventArgs.ColumnNumber}), due to the file being invalid.");
                }
            }
        }

        [Fact]
        public void ProjectImportedEventInvalidFileWhenExpressionEvaluatesToEmpty()
        {
            using (var env = TestEnvironment.Create(_output))
            {
                env.SetEnvironmentVariable("MSBUILDLOGIMPORTS", "1");

                ProjectRootElement pre = ProjectRootElement.Create(env.CreateFile(".proj").Path);

                var import = pre.AddImport("$(SomethingThatEvaluatesToEmpty)");

                pre.Save();
                pre.Reload();

                using (ProjectCollection collection = new ProjectCollection())
                {
                    MockLogger logger = new MockLogger();
                    collection.RegisterLogger(logger);

                    Project unused = new Project(pre, null, null, collection, ProjectLoadSettings.IgnoreInvalidImports);

                    ProjectImportedEventArgs eventArgs = logger.AllBuildEvents.SingleOrDefault(i => i is ProjectImportedEventArgs) as ProjectImportedEventArgs;

                    eventArgs.ShouldNotBeNull();
                    eventArgs.ImportIgnored.ShouldBeTrue();

                    eventArgs.UnexpandedProject.ShouldBe(import.Project);

                    eventArgs.ImportedProjectFile.ShouldBe(string.Empty);

                    eventArgs.ProjectFile.ShouldBe(pre.FullPath);

                    eventArgs.LineNumber.ShouldBe(3);
                    eventArgs.ColumnNumber.ShouldBe(3);

                    logger.AssertLogContains($"Project \"{import.Project}\" was not imported by \"{pre.FullPath}\" at ({eventArgs.LineNumber},{eventArgs.ColumnNumber}), due to the expression evaluating to an empty string.");
                }
            }
        }

        [Fact]
        public void ProjectImportedEventMissingFile()
        {
            using (var env = TestEnvironment.Create(_output))
            {
                env.SetEnvironmentVariable("MSBUILDLOGIMPORTS", "1");

                ProjectRootElement pre = ProjectRootElement.Create(env.CreateFile(".proj").Path);

                pre.AddPropertyGroup().AddProperty("NotUsed", "");

                string importPath = Path.Combine(pre.DirectoryPath, Guid.NewGuid().ToString());
                var import = pre.AddImport(importPath);

                pre.Save();
                pre.Reload();

                using (ProjectCollection collection = new ProjectCollection())
                {
                    MockLogger logger = new MockLogger();
                    collection.RegisterLogger(logger);

                    Project unused = new Project(pre, null, null, collection, ProjectLoadSettings.IgnoreMissingImports);

                    ProjectImportedEventArgs eventArgs = logger.AllBuildEvents.SingleOrDefault(i => i is ProjectImportedEventArgs) as ProjectImportedEventArgs;

                    eventArgs.ShouldNotBeNull();
                    eventArgs.ImportIgnored.ShouldBeTrue();

                    eventArgs.UnexpandedProject.ShouldBe(import.Project);

                    eventArgs.ImportedProjectFile.ShouldBe(importPath);

                    eventArgs.ProjectFile.ShouldBe(pre.FullPath);

                    eventArgs.LineNumber.ShouldBe(6);
                    eventArgs.ColumnNumber.ShouldBe(3);

                    logger.AssertLogContains($"Project \"{import.Project}\" was not imported by \"{pre.FullPath}\" at ({eventArgs.LineNumber},{eventArgs.ColumnNumber}), due to the file not existing.");
                }
            }
        }

        [Fact]
        public void ProjectImportedEventMissingFileNoGlobMatch()
        {
            using (var env = TestEnvironment.Create(_output))
            {
                env.SetEnvironmentVariable("MSBUILDLOGIMPORTS", "1");

                ProjectRootElement pre = ProjectRootElement.Create(env.CreateFile(".proj").Path);

                pre.AddPropertyGroup().AddProperty("NotUsed", "");

                string importGlob = Path.Combine(pre.DirectoryPath, @"__NoMatch__\**");
                var import = pre.AddImport(importGlob);

                pre.Save();
                pre.Reload();

                using (ProjectCollection collection = new ProjectCollection())
                {
                    MockLogger logger = new MockLogger();
                    collection.RegisterLogger(logger);

                    Project unused = new Project(pre, null, null, collection);

                    ProjectImportedEventArgs eventArgs = logger.AllBuildEvents.SingleOrDefault(i => i is ProjectImportedEventArgs) as ProjectImportedEventArgs;

                    eventArgs.ShouldNotBeNull();
                    eventArgs.ImportIgnored.ShouldBeFalse();

                    eventArgs.UnexpandedProject.ShouldBe(import.Project);

                    eventArgs.ImportedProjectFile.ShouldBeNull();

                    eventArgs.ProjectFile.ShouldBe(pre.FullPath);

                    eventArgs.LineNumber.ShouldBe(6);
                    eventArgs.ColumnNumber.ShouldBe(3);

                    logger.AssertLogContains($"Project \"{import.Project}\" was not imported by \"{pre.FullPath}\" at ({eventArgs.LineNumber},{eventArgs.ColumnNumber}), due to no matching files.");
                }
            }
        }

        [Fact]
        [Trait("Category", "netcore-osx-failing")] // https://github.com/dotnet/msbuild/issues/2226
        [Trait("Category", "netcore-linux-failing")] // https://github.com/dotnet/msbuild/issues/2226
        public void ProjectImportEvent()
        {
            using (var env = TestEnvironment.Create(_output))
            {
                env.SetEnvironmentVariable("MSBUILDLOGIMPORTS", "1");

                ProjectRootElement pre1 = ProjectRootElement.Create(env.CreateFile(".proj").Path);
                ProjectRootElement pre2 = ProjectRootElement.Create(env.CreateFile(".proj").Path);

                using (ProjectCollection collection = new ProjectCollection())
                {
                    MockLogger logger = new MockLogger();
                    collection.RegisterLogger(logger);

                    pre1.Save();

                    pre2.AddPropertyGroup().AddProperty("NotUsed", "");
                    var import = pre2.AddImport(pre1.FullPath);

                    pre2.Save();
                    pre2.Reload();

                    Project unused = new Project(pre2, null, null, collection);

                    ProjectImportedEventArgs eventArgs = logger.AllBuildEvents.SingleOrDefault(i => i is ProjectImportedEventArgs) as ProjectImportedEventArgs;

                    eventArgs.ShouldNotBeNull();

                    eventArgs.UnexpandedProject.ShouldBe(import.Project);

                    eventArgs.ImportedProjectFile.ShouldBe(pre1.FullPath);

                    eventArgs.ProjectFile.ShouldBe(pre2.FullPath);

                    eventArgs.ImportIgnored.ShouldBeFalse();
                    eventArgs.LineNumber.ShouldBe(6);
                    eventArgs.ColumnNumber.ShouldBe(3);

                    logger.AssertLogContains($"Importing project \"{pre1.FullPath}\" into project \"{pre2.FullPath}\" at ({eventArgs.LineNumber},{eventArgs.ColumnNumber}).");
                }
            }
        }

        [Fact]
        public void ProjectTargetNamesAreEnumerable()
        {
            // regression test for internal bug
            // https://devdiv.visualstudio.com/DevDiv/_workitems/edit/471452
            // CPS calls project.Targets.Keys to get a list of names

            ProjectRootElement projectXml = ProjectRootElement.Create();
            projectXml.AddTarget("t");

            Project project = new Project(projectXml);

            project.Targets.Keys.ShouldBe(new[] { "t" });
        }

        /// <summary>
        /// Test that the ImportedProjectNotFound error message
        /// contains the unevaluated value of the project attribute.
        /// </summary>
        [Fact]
        public void ImportNotFound()
        {
            const string PropertyName = "SomeProperty";
            const string PropertyValue = "Import";
            const string ImportValue = $"$({PropertyName})DoesNotExist";
            const string ErrorCode = "MSB4019"; // ImportedProjectNotFound

            string file = null;

            try
            {
                using ProjectCollection collection = new ProjectCollection();
                MockLogger logger = new MockLogger();
                collection.RegisterLogger(logger);

                Project project = new Project(collection);
                project.Xml.AddProperty(PropertyName, PropertyValue);
                project.Xml.AddImport(ImportValue);

                file = FileUtilities.GetTemporaryFileName();
                project.Save(file);

                Assert.Throws<InvalidProjectFileException>(() => project.ReevaluateIfNecessary());

                // Find and test the error event record.
                // logger.AssertLogContains() will find multiple strings but not within the same line
                bool hasErrorCode = false;
                foreach (var error in logger.Errors)
                {
                    if (error.Code == ErrorCode)
                    {
                        hasErrorCode = true;
                        Assert.Contains(ImportValue, error.Message);
                        break;
                    }
                }
                Assert.True(hasErrorCode);
            }
            finally
            {
                File.Delete(file);
            }
        }

        private static void AssertGlobResult(GlobResultList expected, string project)
        {
            var globs = ObjectModelHelpers.CreateInMemoryProject(project).GetAllGlobs();
            AssertGlobResultsEqual(expected, globs);
        }

        private static void AssertGlobResult(GlobResultList expected, string project, string itemType)
        {
            var globs = ObjectModelHelpers.CreateInMemoryProject(project).GetAllGlobs(itemType);
            AssertGlobResultsEqual(expected, globs);
        }

        private static void AssertGlobResultsEqual(GlobResultList expected, List<GlobResult> globs)
        {
            globs.Count.ShouldBe(expected.Count);

            for (var i = 0; i < expected.Count; i++)
            {
                globs[i].ItemElement.ItemType.ShouldBe(expected[i].Item1);
                globs[i].IncludeGlobs.ShouldBe(expected[i].Item2);
                globs[i].Excludes.ShouldBe(expected[i].Item3);
                globs[i].Removes.ShouldBe(expected[i].Item4);
            }
        }

        private static void AssertProvenanceResult(ProvenanceResultTupleList expected, string project, string itemValue)
        {
            var provenanceResult = ObjectModelHelpers.CreateInMemoryProject(project).GetItemProvenance(itemValue);
            AssertProvenanceResult(expected, provenanceResult);
        }

        private static void AssertProvenanceResult(ProvenanceResultTupleList expected, string project, string itemValue, string itemType)
        {
            var provenanceResult = ObjectModelHelpers.CreateInMemoryProject(project).GetItemProvenance(itemValue, itemType);
            AssertProvenanceResult(expected, provenanceResult);
        }

        private static void AssertProvenanceResult(ProvenanceResultTupleList expected, string project, string itemValue, int position)
        {
            Project p;
            ProjectItem item;
            GetProjectAndItemAtPosition(project, itemValue, position, out p, out item);

            var provenanceResult = p.GetItemProvenance(item);
            AssertProvenanceResult(expected, provenanceResult);
        }

        private static void GetProjectAndItemAtPosition(string project, string itemValue, int position, out Project p, out ProjectItem item)
        {
            p = ObjectModelHelpers.CreateInMemoryProject(project);

            item = null;
            if (!string.IsNullOrEmpty(itemValue))
            {
                var itemsOfValue = p.Items.Where(i => i.EvaluatedInclude.Equals(itemValue));
                item = itemsOfValue.ElementAt(position);
            }
        }

        private static void AssertProvenanceResult(ProvenanceResultTupleList expected, List<ProvenanceResult> actual)
        {
            actual.Count.ShouldBe(expected.Count);

            for (var i = 0; i < expected.Count; i++)
            {
                var expectedProvenance = expected[i];
                var actualProvenance = actual[i];

                actualProvenance.ItemElement.ItemType.ShouldBe(expectedProvenance.Item1);
                actualProvenance.Operation.ShouldBe(expectedProvenance.Item2);
                actualProvenance.Provenance.ShouldBe(expectedProvenance.Item3);
                actualProvenance.Occurrences.ShouldBe(expectedProvenance.Item4);
            }
        }

        /// <summary>
        /// Creates a simple ProjectRootElement object.
        /// (When ProjectRootElement supports editing, we need not load from a string here.)
        /// </summary>
        private ProjectRootElement GetSampleProjectRootElement()
        {
            string projectFileContent = GetSampleProjectContent();

            using ProjectRootElementFromString projectRootElementFromString = new(projectFileContent);
            ProjectRootElement xml = projectRootElementFromString.Project;

            return xml;
        }

        /// <summary>
        /// Creates a simple project content.
        /// </summary>
        private string GetSampleProjectContent()
        {
            string projectFileContent = ObjectModelHelpers.CleanupFileContents(@"
                    <Project ToolsVersion='2.0' InitialTargets='it' DefaultTargets='dt'>
                        <PropertyGroup Condition=""'$(Configuration)'=='Foo'"">
                            <p>v1</p>
                        </PropertyGroup>
                        <PropertyGroup Condition=""'$(Configuration)'!='Foo'"">
                            <p>v2</p>
                        </PropertyGroup>
                        <PropertyGroup>
                            <p2>X$(p)</p2>
                        </PropertyGroup>
                        <ItemGroup>
                            <i Condition=""'$(Configuration)'=='Foo'"" Include='i0'/>
                            <i Include='i1'/>
                            <i Include='$(p)X;i3'/>
                        </ItemGroup>
                        <Target Name='t'>
                            <task/>
                        </Target>
                    </Project>
                ");

            return projectFileContent;
        }

        /// <summary>
        /// Check the items and properties from the sample project
        /// </summary>
        private void VerifyContentOfSampleProject(Project project)
        {
            project.GetProperty("p").UnevaluatedValue.ShouldBe("v2");
            project.GetProperty("p2").EvaluatedValue.ShouldBe("Xv2");
            project.GetProperty("p2").UnevaluatedValue.ShouldBe("X$(p)");

            IList<ProjectItem> items = Helpers.MakeList(project.GetItems("i"));
            items.Count.ShouldBe(3);
            items[0].EvaluatedInclude.ShouldBe("i1");
            items[1].EvaluatedInclude.ShouldBe("v2X");
            items[1].UnevaluatedInclude.ShouldBe("$(p)X;i3");
            items[2].EvaluatedInclude.ShouldBe("i3");
        }
    }
}
