// 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.Linq;
using Microsoft.Build.Construction;
using Microsoft.Build.Evaluation;
using Microsoft.Build.Execution;
using Microsoft.Build.Framework;
using Microsoft.Build.UnitTests.Shared;
using Xunit;
using InvalidProjectFileException = Microsoft.Build.Exceptions.InvalidProjectFileException;

#nullable disable

namespace Microsoft.Build.UnitTests.OM.Instance
{
    /// <summary>
    /// Tests for ProjectItemInstance public members
    /// </summary>
    public class ProjectItemInstance_Tests : IDisposable
    {
        /// <summary>
        /// The number of built-in metadata for items.
        /// </summary>
        public const int BuiltInMetadataCount = 15;
        private Lazy<DummyMappedDrive> _mappedDrive = DummyMappedDriveUtils.GetLazyDummyMappedDrive();


        public void Dispose()
        {
            _mappedDrive.Value?.Dispose();
        }

        internal const string TargetItemWithInclude = @"
            <Project>
                <Target Name='TestTarget'>
                    <ItemGroup>
                        <i Include='{0}'/>
                    </ItemGroup>
                </Target>
            </Project>
            ";

        internal const string TargetItemWithIncludeAndExclude = @"
            <Project>
                <Target Name='TestTarget'>
                    <ItemGroup>
                        <i Include='{0}' Exclude='{1}'/>
                    </ItemGroup>
                </Target>
            </Project>
            ";

        internal const string TargetWithDefinedPropertyAndItemWithInclude = @"
            <Project>
                <PropertyGroup>
                    <{0}>{1}</{0}>
                </PropertyGroup>
                <Target Name='TestTarget'>
                    <ItemGroup>
                        <i Include='{2}' />
                    </ItemGroup>
                </Target>
            </Project>
            ";

        /// <summary>
        /// Basic ProjectItemInstance without metadata
        /// </summary>
        [Fact]
        public void AccessorsWithoutMetadata()
        {
            ProjectItemInstance item = GetItemInstance();

            Assert.Equal("i", item.ItemType);
            Assert.Equal("i1", item.EvaluatedInclude);
            Assert.False(item.Metadata.GetEnumerator().MoveNext());
        }

        /// <summary>
        /// Basic ProjectItemInstance with metadata
        /// </summary>
        [Fact]
        public void AccessorsWithMetadata()
        {
            ProjectItemInstance item = GetItemInstance();

            item.SetMetadata("m1", "v0");
            item.SetMetadata("m1", "v1");
            item.SetMetadata("m2", "v2");

            Assert.Equal("m1", item.GetMetadata("m1").Name);
            Assert.Equal("m2", item.GetMetadata("m2").Name);
            Assert.Equal("v1", item.GetMetadataValue("m1"));
            Assert.Equal("v2", item.GetMetadataValue("m2"));
        }

        /// <summary>
        /// Basic ProjectItemInstance with metadata added using ImportMetadata
        /// </summary>
        [Fact]
        public void AccessorsWithImportedMetadata()
        {
            ProjectItemInstance item = GetItemInstance();

            ((IMetadataContainer)item).ImportMetadata(new Dictionary<string, string>
            {
                { "m1", "v1" },
                { "m2", "v2" },
            });

            Assert.Equal("m1", item.GetMetadata("m1").Name);
            Assert.Equal("m2", item.GetMetadata("m2").Name);
            Assert.Equal("v1", item.GetMetadataValue("m1"));
            Assert.Equal("v2", item.GetMetadataValue("m2"));
        }

        /// <summary>
        /// ImportMetadata adds and overwrites metadata, does not delete existing metadata
        /// </summary>
        [Fact]
        public void ImportMetadataAddsAndOverwrites()
        {
            ProjectItemInstance item = GetItemInstance();

            item.SetMetadata("m1", "v1");
            item.SetMetadata("m2", "v0");

            ((IMetadataContainer)item).ImportMetadata(new Dictionary<string, string>
            {
                { "m2", "v2" },
                { "m3", "v3" },
            });

            // m1 was not deleted, m2 was overwritten, m3 was added
            Assert.Equal("v1", item.GetMetadataValue("m1"));
            Assert.Equal("v2", item.GetMetadataValue("m2"));
            Assert.Equal("v3", item.GetMetadataValue("m3"));
        }

        /// <summary>
        /// Get metadata not present
        /// </summary>
        [Fact]
        public void GetMissingMetadata()
        {
            ProjectItemInstance item = GetItemInstance();
            Assert.Null(item.GetMetadata("X"));
            Assert.Equal(String.Empty, item.GetMetadataValue("X"));
        }

        [Fact]
        public void CopyMetadataToTaskItem()
        {
            ProjectItemInstance fromItem = GetItemInstance();

            fromItem.SetMetadata("m1", "v1");
            fromItem.SetMetadata("m2", "v2");

            ITaskItem toItem = new Utilities.TaskItem();

            ((ITaskItem)fromItem).CopyMetadataTo(toItem);

            Assert.Equal("v1", toItem.GetMetadata("m1"));
            Assert.Equal("v2", toItem.GetMetadata("m2"));
        }

#if FEATURE_APPDOMAIN
        private sealed class RemoteTaskItemFactory : MarshalByRefObject
        {
            public ITaskItem CreateTaskItem() => new Utilities.TaskItem();
        }

        [Fact]
        public void CopyMetadataToRemoteTaskItem()
        {
            ProjectItemInstance fromItem = GetItemInstance();

            fromItem.SetMetadata("m1", "v1");
            fromItem.SetMetadata("m2", "v2");

            AppDomain appDomain = null;
            try
            {
                appDomain = AppDomain.CreateDomain("CopyMetadataToRemoteTaskItem", null, AppDomain.CurrentDomain.SetupInformation);
                RemoteTaskItemFactory itemFactory = (RemoteTaskItemFactory)appDomain.CreateInstanceFromAndUnwrap(typeof(RemoteTaskItemFactory).Module.FullyQualifiedName, typeof(RemoteTaskItemFactory).FullName);

                ITaskItem toItem = itemFactory.CreateTaskItem();

                ((ITaskItem)fromItem).CopyMetadataTo(toItem);

                Assert.Equal("v1", toItem.GetMetadata("m1"));
                Assert.Equal("v2", toItem.GetMetadata("m2"));
            }
            finally
            {
                AppDomain.Unload(appDomain);
            }
        }
#endif

        /// <summary>
        /// Set include
        /// </summary>
        [Fact]
        public void SetInclude()
        {
            ProjectItemInstance item = GetItemInstance();
            item.EvaluatedInclude = "i1b";
            Assert.Equal("i1b", item.EvaluatedInclude);
        }

        /// <summary>
        /// Set include to empty string
        /// </summary>
        [Fact]
        public void SetInvalidEmptyInclude()
        {
            Assert.Throws<ArgumentException>(() =>
            {
                ProjectItemInstance item = GetItemInstance();
                item.EvaluatedInclude = String.Empty;
            });
        }
        /// <summary>
        /// Set include to invalid null value
        /// </summary>
        [Fact]
        public void SetInvalidNullInclude()
        {
            Assert.Throws<ArgumentNullException>(() =>
            {
                ProjectItemInstance item = GetItemInstance();
                item.EvaluatedInclude = null;
            });
        }
        /// <summary>
        /// Create an item with a metadatum that has a null value
        /// </summary>
        [Fact]
        public void CreateItemWithNullMetadataValue()
        {
            Project project = new Project();
            ProjectInstance projectInstance = project.CreateProjectInstance();

            IDictionary<string, string> metadata = new Dictionary<string, string>();
            metadata.Add("m", null);

            ProjectItemInstance item = projectInstance.AddItem("i", "i1", metadata);
            Assert.Equal(String.Empty, item.GetMetadataValue("m"));
        }

        /// <summary>
        /// Set metadata value
        /// </summary>
        [Fact]
        public void SetMetadata()
        {
            ProjectItemInstance item = GetItemInstance();
            item.SetMetadata("m", "m1");
            Assert.Equal("m1", item.GetMetadataValue("m"));
        }

        /// <summary>
        /// Set metadata value to empty string
        /// </summary>
        [Fact]
        public void SetMetadataEmptyString()
        {
            ProjectItemInstance item = GetItemInstance();
            item.SetMetadata("m", String.Empty);
            Assert.Equal(String.Empty, item.GetMetadataValue("m"));
        }

        /// <summary>
        /// Set metadata value to null value -- this is allowed, but
        /// internally converted to the empty string.
        /// </summary>
        [Fact]
        public void SetNullMetadataValue()
        {
            ProjectItemInstance item = GetItemInstance();
            item.SetMetadata("m", null);
            Assert.Equal(String.Empty, item.GetMetadataValue("m"));
        }

        /// <summary>
        /// Set metadata with invalid empty name
        /// </summary>
        [Fact]
        public void SetInvalidNullMetadataName()
        {
            Assert.Throws<ArgumentNullException>(() =>
            {
                ProjectItemInstance item = GetItemInstance();
                item.SetMetadata(null, "m1");
            });
        }
        /// <summary>
        /// Set metadata with invalid empty name
        /// </summary>
        [Fact]
        public void SetInvalidEmptyMetadataName()
        {
            Assert.Throws<ArgumentException>(() =>
            {
                ProjectItemInstance item = GetItemInstance();
                item.SetMetadata(String.Empty, "m1");
            });
        }
        /// <summary>
        /// Cast to ITaskItem
        /// </summary>
        [Fact]
        public void CastToITaskItem()
        {
            ProjectItemInstance item = GetItemInstance();
            item.SetMetadata("m", "m1");

            ITaskItem taskItem = (ITaskItem)item;

            Assert.Equal(item.EvaluatedInclude, taskItem.ItemSpec);
            Assert.Equal(1 + BuiltInMetadataCount, taskItem.MetadataCount);
            Assert.Equal(1 + BuiltInMetadataCount, taskItem.MetadataNames.Count);
            Assert.Equal("m1", taskItem.GetMetadata("m"));
            taskItem.SetMetadata("m", "m2");
            Assert.Equal("m2", item.GetMetadataValue("m"));
        }

        /// <summary>
        /// Creates a ProjectItemInstance and casts it to ITaskItem2; makes sure that all escaped information is
        /// maintained correctly.  Also creates a new Microsoft.Build.Utilities.TaskItem from the ProjectItemInstance
        /// and verifies that none of the information is lost.
        /// </summary>
        [Fact]
        public void ITaskItem2Operations()
        {
            Project project = new Project();
            ProjectInstance projectInstance = project.CreateProjectInstance();

            ProjectItemInstance item = projectInstance.AddItem("EscapedItem", "esca%20ped%3bitem");
            item.SetMetadata("m", "m1");
            item.SetMetadata("m;", "m%3b1");
            ITaskItem2 taskItem = (ITaskItem2)item;

            Assert.Equal("esca%20ped%3bitem", taskItem.EvaluatedIncludeEscaped);
            Assert.Equal("esca ped;item", taskItem.ItemSpec);

            Assert.Equal("m;1", taskItem.GetMetadata("m;"));
            Assert.Equal("m%3b1", taskItem.GetMetadataValueEscaped("m;"));
            Assert.Equal("m1", taskItem.GetMetadataValueEscaped("m"));

            Assert.Equal("esca%20ped%3bitem", taskItem.EvaluatedIncludeEscaped);
            Assert.Equal("esca ped;item", taskItem.ItemSpec);

            ITaskItem2 taskItem2 = new Microsoft.Build.Utilities.TaskItem(taskItem);

            taskItem2.SetMetadataValueLiteral("m;", "m;2");

            Assert.Equal("m%3b2", taskItem2.GetMetadataValueEscaped("m;"));
            Assert.Equal("m;2", taskItem2.GetMetadata("m;"));

            IDictionary<string, string> taskItem2Metadata = (IDictionary<string, string>)taskItem2.CloneCustomMetadata();
            Assert.Equal(3, taskItem2Metadata.Count);

            foreach (KeyValuePair<string, string> pair in taskItem2Metadata)
            {
                if (pair.Key.Equals("m"))
                {
                    Assert.Equal("m1", pair.Value);
                }

                if (pair.Key.Equals("m;"))
                {
                    Assert.Equal("m;2", pair.Value);
                }

                if (pair.Key.Equals("OriginalItemSpec"))
                {
                    Assert.Equal("esca ped;item", pair.Value);
                }
            }

            IDictionary<string, string> taskItem2MetadataEscaped = (IDictionary<string, string>)taskItem2.CloneCustomMetadataEscaped();
            Assert.Equal(3, taskItem2MetadataEscaped.Count);

            foreach (KeyValuePair<string, string> pair in taskItem2MetadataEscaped)
            {
                if (pair.Key.Equals("m"))
                {
                    Assert.Equal("m1", pair.Value);
                }

                if (pair.Key.Equals("m;"))
                {
                    Assert.Equal("m%3b2", pair.Value);
                }

                if (pair.Key.Equals("OriginalItemSpec"))
                {
                    Assert.Equal("esca%20ped%3bitem", pair.Value);
                }
            }
        }

        /// <summary>
        /// Cast to ITaskItem
        /// </summary>
        [Fact]
        public void CastToITaskItemNoMetadata()
        {
            ProjectItemInstance item = GetItemInstance();

            ITaskItem taskItem = (ITaskItem)item;

            Assert.Equal(0 + BuiltInMetadataCount, taskItem.MetadataCount);
            Assert.Equal(0 + BuiltInMetadataCount, taskItem.MetadataNames.Count);
            Assert.Equal(String.Empty, taskItem.GetMetadata("m"));
        }

        /*
         * We must repeat all the evaluation-related tests here,
         * to exercise the path that evaluates directly to instance objects.
         * Although the Evaluator class is shared, its interactions with the two
         * different item classes could be different, and shouldn't be.
         */

        /// <summary>
        /// No metadata, simple case
        /// </summary>
        [Fact]
        public void NoMetadata()
        {
            string content = @"
                    <Project>
                        <ItemGroup>
                            <i Include='i1'/>
                        </ItemGroup>
                    </Project>
                ";

            ProjectItemInstance item = GetOneItem(content);

            Assert.Equal("i", item.ItemType);
            Assert.Equal("i1", item.EvaluatedInclude);
            Assert.False(item.Metadata.GetEnumerator().MoveNext());
            Assert.Equal(0 + BuiltInMetadataCount, Helpers.MakeList(item.MetadataNames).Count);
            Assert.Equal(0 + BuiltInMetadataCount, item.MetadataCount);
        }

        /// <summary>
        /// Read off metadata
        /// </summary>
        [Fact]
        public void ReadMetadata()
        {
            string content = @"
                    <Project>
                        <ItemGroup>
                            <i Include='i1'>
                                <m1>v1</m1>
                                <m2>v2</m2>
                            </i>
                        </ItemGroup>
                    </Project>
                ";

            ProjectItemInstance item = GetOneItem(content);

            var itemMetadata = Helpers.MakeList(item.Metadata);

            itemMetadata = itemMetadata.OrderBy(pmi => pmi.Name).ToList();

            Assert.Equal(2, itemMetadata.Count);
            Assert.Equal("m1", itemMetadata[0].Name);
            Assert.Equal("m2", itemMetadata[1].Name);
            Assert.Equal("v1", itemMetadata[0].EvaluatedValue);
            Assert.Equal("v2", itemMetadata[1].EvaluatedValue);

            Assert.Equal(itemMetadata[0], item.GetMetadata("m1"));
            Assert.Equal(itemMetadata[1], item.GetMetadata("m2"));
        }

        /// <summary>
        /// Create a new Microsoft.Build.Utilities.TaskItem from the ProjectItemInstance where the ProjectItemInstance
        /// has item definition metadata on it.
        ///
        /// Verify the Utilities task item gets the expanded metadata from the ItemDefinitionGroup.
        /// </summary>
        [Fact]
        public void InstanceItemToUtilItemIDG()
        {
            string content = @"
                    <Project>
                        <ItemDefinitionGroup>
                            <i>
                                <m0>;x86;</m0>
                                <m1>%(FileName).extension</m1>
                                <m2>;%(FileName).extension;</m2>
                                <m3>v1</m3>
                                <m4>%3bx86%3b</m4>
                            </i>
                        </ItemDefinitionGroup>
                        <ItemGroup>
                            <i Include='foo.proj'/>
                        </ItemGroup>
                    </Project>
                ";

            ProjectItemInstance item = GetOneItem(content);

            Microsoft.Build.Utilities.TaskItem taskItem = new Microsoft.Build.Utilities.TaskItem(item);

            Assert.Equal(";x86;", taskItem.GetMetadata("m0"));
            Assert.Equal("foo.extension", taskItem.GetMetadata("m1"));
            Assert.Equal(";foo.extension;", taskItem.GetMetadata("m2"));
            Assert.Equal("v1", taskItem.GetMetadata("m3"));
            Assert.Equal(";x86;", taskItem.GetMetadata("m4"));
        }

        /// <summary>
        /// Get metadata values inherited from item definitions
        /// </summary>
        [Fact]
        public void GetMetadataValuesFromDefinition()
        {
            string content = @"
                    <Project>
                        <ItemDefinitionGroup>
                            <i>
                                <m0>v0</m0>
                                <m1>v1</m1>
                            </i>
                        </ItemDefinitionGroup>
                        <ItemGroup>
                            <i Include='i1'>
                                <m1>v1b</m1>
                                <m2>v2</m2>
                            </i>
                        </ItemGroup>
                    </Project>
                ";

            ProjectItemInstance item = GetOneItem(content);
            Assert.Equal("v0", item.GetMetadataValue("m0"));
            Assert.Equal("v1b", item.GetMetadataValue("m1"));
            Assert.Equal("v2", item.GetMetadataValue("m2"));

            Assert.Equal(3, Helpers.MakeList(item.Metadata).Count);
            Assert.Equal(3 + BuiltInMetadataCount, Helpers.MakeList(item.MetadataNames).Count);
            Assert.Equal(3 + BuiltInMetadataCount, item.MetadataCount);
        }

        /// <summary>
        /// Exclude against an include with item vectors in it
        /// </summary>
        [Fact]
        public void ExcludeWithIncludeVector()
        {
            string content = @"
                    <Project>
                        <ItemGroup>
                            <i Include='a;b;c'>
                            </i>
                        </ItemGroup>

                        <ItemGroup>
                            <i Include='x;y;z;@(i);u;v;w' Exclude='b;y;v'>
                            </i>
                        </ItemGroup>
                    </Project>
                ";

            IList<ProjectItemInstance> items = GetItems(content);

            // Should contain a, b, c, x, z, a, c, u, w
            Assert.Equal(9, items.Count);
            AssertEvaluatedIncludes(items, new string[] { "a", "b", "c", "x", "z", "a", "c", "u", "w" });
        }

        /// <summary>
        /// Exclude with item vectors against an include with item vectors in it
        /// </summary>
        [Fact]
        public void ExcludeVectorWithIncludeVector()
        {
            string content = @"
                    <Project>
                        <ItemGroup>
                            <i Include='a;b;c'>
                            </i>
                            <j Include='b;y;v' />
                        </ItemGroup>

                        <ItemGroup>
                            <i Include='x;y;z;@(i);u;v;w' Exclude='x;@(j);w'>
                            </i>
                        </ItemGroup>
                    </Project>
                ";

            IList<ProjectItemInstance> items = GetItems(content);

            // Should contain a, b, c, z, a, c, u
            Assert.Equal(7, items.Count);
            AssertEvaluatedIncludes(items, new string[] { "a", "b", "c", "z", "a", "c", "u" });
        }

        /// <summary>
        /// Metadata on items can refer to metadata above
        /// </summary>
        [Fact]
        public void MetadataReferringToMetadataAbove()
        {
            string content = @"
                    <Project>
                        <ItemGroup>
                            <i Include='i1'>
                                <m1>v1</m1>
                                <m2>%(m1);v2;%(m0)</m2>
                            </i>
                        </ItemGroup>
                    </Project>
                ";

            ProjectItemInstance item = GetOneItem(content);

            var itemMetadata = Helpers.MakeList(item.Metadata);
            Assert.Equal(2, itemMetadata.Count);
            Assert.Equal("v1;v2;", item.GetMetadataValue("m2"));
        }

        /// <summary>
        /// Built-in metadata should work, too.
        /// NOTE: To work properly, this should batch. This is a temporary "patch" to make it work for now.
        /// It will only give correct results if there is exactly one item in the Include. Otherwise Batching would be needed.
        /// </summary>
        [Fact]
        public void BuiltInMetadataExpression()
        {
            string content = @"
                    <Project>
                        <ItemGroup>
                            <i Include='i1'>
                                <m>%(Identity)</m>
                            </i>
                        </ItemGroup>
                    </Project>
                ";

            ProjectItemInstance item = GetOneItem(content);

            Assert.Equal("i1", item.GetMetadataValue("m"));
        }

        /// <summary>
        /// Qualified built in metadata should work
        /// </summary>
        [Fact]
        public void BuiltInQualifiedMetadataExpression()
        {
            string content = @"
                    <Project>
                        <ItemGroup>
                            <i Include='i1'>
                                <m>%(i.Identity)</m>
                            </i>
                        </ItemGroup>
                    </Project>
                ";

            ProjectItemInstance item = GetOneItem(content);

            Assert.Equal("i1", item.GetMetadataValue("m"));
        }

        /// <summary>
        /// Mis-qualified built in metadata should not work
        /// </summary>
        [Fact]
        public void BuiltInMisqualifiedMetadataExpression()
        {
            string content = @"
                    <Project>
                        <ItemGroup>
                            <i Include='i1'>
                                <m>%(j.Identity)</m>
                            </i>
                        </ItemGroup>
                    </Project>
                ";

            ProjectItemInstance item = GetOneItem(content);

            Assert.Equal(String.Empty, item.GetMetadataValue("m"));
        }

        /// <summary>
        /// Metadata condition should work correctly with built-in metadata
        /// </summary>
        [Fact]
        public void BuiltInMetadataInMetadataCondition()
        {
            string content = @"
                    <Project>
                        <ItemGroup>
                            <i Include='i1'>
                                <m Condition=""'%(Identity)'=='i1'"">m1</m>
                                <n Condition=""'%(Identity)'=='i2'"">n1</n>
                            </i>
                        </ItemGroup>
                    </Project>
                ";

            ProjectItemInstance item = GetOneItem(content);

            Assert.Equal("m1", item.GetMetadataValue("m"));
            Assert.Equal(String.Empty, item.GetMetadataValue("n"));
        }

        /// <summary>
        /// Metadata on item condition not allowed (currently)
        /// </summary>
        [Fact]
        public void BuiltInMetadataInItemCondition()
        {
            Assert.Throws<InvalidProjectFileException>(() =>
            {
                string content = @"
                    <Project>
                        <ItemGroup>
                            <i Include='i1' Condition=""'%(Identity)'=='i1'/>
                        </ItemGroup>
                    </Project>
                ";

                GetOneItem(content);
            });
        }
        /// <summary>
        /// Two items should each get their own values for built-in metadata
        /// </summary>
        [Fact]
        public void BuiltInMetadataTwoItems()
        {
            string content = @"
                    <Project>
                        <ItemGroup>
                            <i Include='i1.cpp;" + (NativeMethodsShared.IsWindows ? @"c:\bar\i2.cpp" : "/bar/i2.cpp") + @"'>
                                <m>%(Filename).obj</m>
                            </i>
                        </ItemGroup>
                    </Project>
                ";

            IList<ProjectItemInstance> items = GetItems(content);

            Assert.Equal(@"i1.obj", items[0].GetMetadataValue("m"));
            Assert.Equal(@"i2.obj", items[1].GetMetadataValue("m"));
        }

        /// <summary>
        /// Items from another list, but with different metadata
        /// </summary>
        [Fact]
        public void DifferentMetadataItemsFromOtherList()
        {
            string content = @"
                    <Project>
                        <ItemGroup>
                            <h Include='h0'>
                                <m>m1</m>
                            </h>
                            <h Include='h1'/>

                            <i Include='@(h)'>
                                <m>%(m)</m>
                            </i>
                        </ItemGroup>
                    </Project>
                ";

            IList<ProjectItemInstance> items = GetItems(content);

            Assert.Equal(@"m1", items[0].GetMetadataValue("m"));
            Assert.Equal(String.Empty, items[1].GetMetadataValue("m"));
        }

        /// <summary>
        /// Items from another list, but with different metadata
        /// </summary>
        [Fact]
        public void DifferentBuiltInMetadataItemsFromOtherList()
        {
            string content = @"
                    <Project>
                        <ItemGroup>
                            <h Include='h0.x'/>
                            <h Include='h1.y'/>

                            <i Include='@(h)'>
                                <m>%(extension)</m>
                            </i>
                        </ItemGroup>
                    </Project>
                ";

            IList<ProjectItemInstance> items = GetItems(content);

            Assert.Equal(@".x", items[0].GetMetadataValue("m"));
            Assert.Equal(@".y", items[1].GetMetadataValue("m"));
        }

        /// <summary>
        /// Two items coming from a transform
        /// </summary>
        [Fact]
        public void BuiltInMetadataTransformInInclude()
        {
            string content = @"
                    <Project>
                        <ItemGroup>
                            <h Include='h0'/>
                            <h Include='h1'/>

                            <i Include=""@(h->'%(Identity).baz')"">
                                <m>%(Filename)%(Extension).obj</m>
                            </i>
                        </ItemGroup>
                    </Project>
                ";

            IList<ProjectItemInstance> items = GetItems(content);

            Assert.Equal(@"h0.baz.obj", items[0].GetMetadataValue("m"));
            Assert.Equal(@"h1.baz.obj", items[1].GetMetadataValue("m"));
        }

        /// <summary>
        /// Transform in the metadata value; no bare metadata involved
        /// </summary>
        [Fact]
        public void BuiltInMetadataTransformInMetadataValue()
        {
            string content = @"
                    <Project>
                        <ItemGroup>
                            <h Include='h0'/>
                            <h Include='h1'/>
                            <i Include='i0'/>
                            <i Include='i1;i2'>
                                <m>@(i);@(h->'%(Filename)')</m>
                            </i>
                        </ItemGroup>
                    </Project>
                ";

            IList<ProjectItemInstance> items = GetItems(content);

            Assert.Equal(@"i0;h0;h1", items[1].GetMetadataValue("m"));
            Assert.Equal(@"i0;h0;h1", items[2].GetMetadataValue("m"));
        }

        /// <summary>
        /// Transform in the metadata value; bare metadata involved
        /// </summary>
        [Fact]
        public void BuiltInMetadataTransformInMetadataValueBareMetadataPresent()
        {
            string content = @"
                    <Project>
                        <ItemGroup>
                            <h Include='h0'/>
                            <h Include='h1'/>
                            <i Include='i0.x'/>
                            <i Include='i1.y;i2'>
                                <m>@(i);@(h->'%(Filename)');%(Extension)</m>
                            </i>
                        </ItemGroup>
                    </Project>
                ";

            IList<ProjectItemInstance> items = GetItems(content);

            Assert.Equal(@"i0.x;h0;h1;.y", items[1].GetMetadataValue("m"));
            Assert.Equal(@"i0.x;h0;h1;", items[2].GetMetadataValue("m"));
        }

        /// <summary>
        /// Metadata on items can refer to item lists
        /// </summary>
        [Fact]
        public void MetadataValueReferringToItems()
        {
            string content = @"
                    <Project>
                        <ItemGroup>
                            <h Include='h0'/>
                            <i Include='i0'/>
                            <i Include='i1'>
                                <m1>@(h);@(i)</m1>
                            </i>
                        </ItemGroup>
                    </Project>
                ";

            IList<ProjectItemInstance> items = GetItems(content);

            Assert.Equal("h0;i0", items[1].GetMetadataValue("m1"));
        }

        /// <summary>
        /// Metadata on items' conditions can refer to item lists
        /// </summary>
        [Fact]
        public void MetadataConditionReferringToItems()
        {
            string content = @"
                    <Project>
                        <ItemGroup>
                            <h Include='h0'/>
                            <i Include='i0'/>
                            <i Include='i1'>
                                <m1 Condition=""'@(h)'=='h0' and '@(i)'=='i0'"">v1</m1>
                                <m2 Condition=""'@(h)'!='h0' or '@(i)'!='i0'"">v2</m2>
                            </i>
                        </ItemGroup>
                    </Project>
                ";

            IList<ProjectItemInstance> items = GetItems(content);

            Assert.Equal("v1", items[1].GetMetadataValue("m1"));
            Assert.Equal(String.Empty, items[1].GetMetadataValue("m2"));
        }

        /// <summary>
        /// Metadata on items' conditions can refer to other metadata
        /// </summary>
        [Fact]
        public void MetadataConditionReferringToMetadataOnSameItem()
        {
            string content = @"
                    <Project>
                        <ItemGroup>
                            <i Include='i1'>
                                <m0>0</m0>
                                <m1 Condition=""'%(m0)'=='0'"">1</m1>
                                <m2 Condition=""'%(m0)'=='3'"">2</m2>
                            </i>
                        </ItemGroup>
                    </Project>
                ";

            IList<ProjectItemInstance> items = GetItems(content);

            Assert.Equal("0", items[0].GetMetadataValue("m0"));
            Assert.Equal("1", items[0].GetMetadataValue("m1"));
            Assert.Equal(String.Empty, items[0].GetMetadataValue("m2"));
        }

        /// <summary>
        /// Fail build for drive enumerating wildcards that exist in projects on any platform.
        /// </summary>
        [Theory]
        [InlineData(
            TargetItemWithIncludeAndExclude,
            @"$(Microsoft_WindowsAzure_EngSys)\**\*",
            @"$(Microsoft_WindowsAzure_EngSys)\*.pdb;$(Microsoft_WindowsAzure_EngSys)\Microsoft.WindowsAzure.Storage.dll;$(Microsoft_WindowsAzure_EngSys)\Certificates\**\*")]

        [InlineData(
            TargetItemWithIncludeAndExclude,
            @"$(Microsoft_WindowsAzure_EngSys)\*.pdb",
            @"$(Microsoft_WindowsAzure_EngSys)\**\*")]

        [InlineData(
            TargetItemWithInclude,
            @"$(Microsoft_WindowsAzure_EngSys)\**\*")]

        [InlineData(
            TargetWithDefinedPropertyAndItemWithInclude,
            "$(Microsoft_WindowsAzure_EngSys)**",
            null,
            "Microsoft_WindowsAzure_EngSys",
            @"\")]
        public void ThrowExceptionUponBuildingProjectWithDriveEnumeration(string content, string include, string exclude = null, string property = null, string propertyValue = null)
        {
            content = (string.IsNullOrEmpty(property) && string.IsNullOrEmpty(propertyValue)) ?
                string.Format(content, include, exclude) :
                string.Format(content, property, propertyValue, include);

            Helpers.CleanContentsAndBuildTargetWithDriveEnumeratingWildcard(
                content,
                "1",
                "TestTarget",
                Helpers.ExpectedBuildResult.FailWithError);
        }

        /// <summary>
        /// Log warning for drive enumerating wildcards that exist in projects on Windows platform.
        /// </summary>
        [WindowsOnlyTheory]
        [InlineData(
            TargetItemWithIncludeAndExclude,
            @"%DRIVE%:$(Microsoft_WindowsAzure_EngSys)\**\*",
            @"$(Microsoft_WindowsAzure_EngSys)\*.pdb;$(Microsoft_WindowsAzure_EngSys)\Microsoft.WindowsAzure.Storage.dll;$(Microsoft_WindowsAzure_EngSys)\Certificates\**\*")]

        [InlineData(
            TargetWithDefinedPropertyAndItemWithInclude,
            @"$(Microsoft_WindowsAzure_EngSys)**",
            null,
            "Microsoft_WindowsAzure_EngSys",
            @"%DRIVE%:\")]

        [InlineData(
            TargetWithDefinedPropertyAndItemWithInclude,
            @"$(Microsoft_WindowsAzure_EngSys)\**\*",
            null,
            "Microsoft_WindowsAzure_EngSys",
            @"%DRIVE%:")]
        public void LogWindowsWarningUponBuildingProjectWithDriveEnumeration(string content, string include, string exclude = null, string property = null, string propertyValue = null)
        {
            include = DummyMappedDriveUtils.UpdatePathToMappedDrive(include, _mappedDrive.Value.MappedDriveLetter);
            exclude = DummyMappedDriveUtils.UpdatePathToMappedDrive(exclude, _mappedDrive.Value.MappedDriveLetter);
            propertyValue = DummyMappedDriveUtils.UpdatePathToMappedDrive(propertyValue, _mappedDrive.Value.MappedDriveLetter);
            content = (string.IsNullOrEmpty(property) && string.IsNullOrEmpty(propertyValue)) ?
                string.Format(content, include, exclude) :
                string.Format(content, property, propertyValue, include);

            Helpers.CleanContentsAndBuildTargetWithDriveEnumeratingWildcard(
                content,
                "0",
                "TestTarget",
                Helpers.ExpectedBuildResult.SucceedWithWarning);
        }

        /// <summary>
        /// Log warning for drive enumerating wildcards that exist in projects on Unix platform.
        /// </summary>
        [ActiveIssue("https://github.com/dotnet/msbuild/issues/8373")]
        [UnixOnlyTheory]
        [InlineData(
            TargetWithDefinedPropertyAndItemWithInclude,
            @"$(Microsoft_WindowsAzure_EngSys)**",
            null,
            "Microsoft_WindowsAzure_EngSys",
            @"/")]

        [InlineData(
            TargetWithDefinedPropertyAndItemWithInclude,
            @"$(Microsoft_WindowsAzure_EngSys)*/*.log",
            null,
            "Microsoft_WindowsAzure_EngSys",
            @"/*")]
        public void LogUnixWarningUponBuildingProjectWithDriveEnumeration(string content, string include, string exclude = null, string property = null, string propertyValue = null)
        {
            content = (string.IsNullOrEmpty(property) && string.IsNullOrEmpty(propertyValue)) ?
                    string.Format(content, include, exclude) :
                    string.Format(content, property, propertyValue, include);

            Helpers.CleanContentsAndBuildTargetWithDriveEnumeratingWildcard(
                    content,
                    "0",
                    "TestTarget",
                    Helpers.ExpectedBuildResult.SucceedWithWarning);
        }

        /// <summary>
        /// Tests target item evaluation resulting in no build failures.
        /// </summary>
        [WindowsOnlyTheory]
        [InlineData(
            TargetWithDefinedPropertyAndItemWithInclude,
            @"$(Microsoft_WindowsAzure_EngSys)*.cs",
            null,
            "Microsoft_WindowsAzure_EngSys",
            @"c:\*\")]

        [InlineData(
            TargetWithDefinedPropertyAndItemWithInclude,
            @"\$(Microsoft_WindowsAzure_EngSys)*\*.cs",
            null,
            "Microsoft_WindowsAzure_EngSys",
            @"c:")]

        [InlineData(
            TargetWithDefinedPropertyAndItemWithInclude,
            @":\$(Microsoft_WindowsAzure_EngSys)*\*.log",
            null,
            "Microsoft_WindowsAzure_EngSys",
            @"c")]

        [InlineData(
            TargetWithDefinedPropertyAndItemWithInclude,
            @"$(Microsoft_WindowsAzure_EngSys)*\*.log",
            null,
            "Microsoft_WindowsAzure_EngSys",
            @"\")]
        public void NoErrorsAndWarningsUponBuildingProject(string content, string include, string exclude = null, string property = null, string propertyValue = null)
        {
            content = (string.IsNullOrEmpty(property) && string.IsNullOrEmpty(propertyValue)) ?
                    string.Format(content, include, exclude) :
                    string.Format(content, property, propertyValue, include);

            Helpers.CleanContentsAndBuildTargetWithDriveEnumeratingWildcard(
                content,
                "0",
                "TestTarget",
                Helpers.ExpectedBuildResult.SucceedWithNoErrorsAndWarnings);
        }

        [Fact]
        public void UpdateShouldRespectConditions()
        {
            string content = @"
                      <Project>
                          <ItemGroup>
                              <i Include='a;b'>
                                  <m1>m1_contents</m1>
                              </i>
                              <i Update='a' Condition='1 == 1'>
                                  <m1>from_true</m1>
                              </i>
                              <i Update='b' Condition='1 == 0'>
                                  <m1>from_false_on_item</m1>
                              </i>
                              <i Update='b'>
                                  <m1 Condition='1 == 0'>from_false_on_metadata</m1>
                              </i>
                          </ItemGroup>
                      </Project>";

            var items = GetItems(content);

            var expectedInitial = new Dictionary<string, string>
            {
                {"m1", "m1_contents"}
            };

            var expectedUpdateFromTrue = new Dictionary<string, string>
            {
                {"m1", "from_true"}
            };

            AssertItemHasMetadata(expectedUpdateFromTrue, items[0]);
            AssertItemHasMetadata(expectedInitial, items[1]);
        }

        /// <summary>
        /// Gets the first item of type 'i'
        /// </summary>
        private static ProjectItemInstance GetOneItem(string content)
        {
            return GetItems(content)[0];
        }

        /// <summary>
        /// Get all items of type 'i'
        /// </summary>
        private static IList<ProjectItemInstance> GetItems(string content)
        {
            using ProjectRootElementFromString projectRootElementFromString = new(content);
            ProjectRootElement xml = projectRootElementFromString.Project;
            ProjectInstance project = new ProjectInstance(xml);

            return Helpers.MakeList(project.GetItems("i"));
        }

        /// <summary>
        /// Asserts that the list of items has the specified includes.
        /// </summary>
        private static void AssertEvaluatedIncludes(IList<ProjectItemInstance> items, string[] includes)
        {
            for (int i = 0; i < includes.Length; i++)
            {
                Assert.Equal(includes[i], items[i].EvaluatedInclude);
            }
        }

        /// <summary>
        /// Get a single item instance
        /// </summary>
        private static ProjectItemInstance GetItemInstance()
        {
            Project project = new Project();
            ProjectInstance projectInstance = project.CreateProjectInstance();
            ProjectItemInstance item = projectInstance.AddItem("i", "i1");
            return item;
        }

        private static void AssertItemHasMetadata(Dictionary<string, string> expected, ProjectItemInstance item)
        {
            Assert.Equal(expected.Keys.Count, item.DirectMetadataCount);

            foreach (var key in expected.Keys)
            {
                Assert.Equal(expected[key], item.GetMetadataValue(key));
            }
        }
    }
}
