// 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.IO;
using System.Linq;
using System.Xml;

using Microsoft.Build.Evaluation;
using Microsoft.Build.Tasks;
using Microsoft.Build.Utilities;

using Shouldly;

using Xunit;

#nullable disable

namespace Microsoft.Build.UnitTests
{
    public sealed class XmlPoke_Tests
    {
        private const string XmlNamespaceUsedByTests = "http://nsurl";

        private const string _xmlFileWithNs = @"<?xml version='1.0' encoding='utf-8'?>

<class AccessModifier='public' Name='test' xmlns:s='" + XmlNamespaceUsedByTests + @"'>
  <s:variable Type='String' Name='a'></s:variable>
  <s:variable Type='String' Name='b'></s:variable>
  <s:variable Type='String' Name='c'></s:variable>
  <method AccessModifier='public static' Name='GetVal' />
</class>";

        private const string _xmlFileNoNs = @"<?xml version='1.0' encoding='utf-8'?>

<class AccessModifier='public' Name='test'>
  <variable Type='String' Name='a'></variable>
  <variable Type='String' Name='b'></variable>
  <variable Type='String' Name='c'></variable>
  <method AccessModifier='public static' Name='GetVal' />
</class>";

        [Fact]
        public void PokeWithNamespace()
        {
            const string query = "//s:variable/@Name";

            XmlDocument xmlDocument = ExecuteXmlPoke(
                query: query,
                useNamespace: true,
                value: "Mert");

            XmlNamespaceManager ns = new XmlNamespaceManager(xmlDocument.NameTable);
            ns.AddNamespace("s", XmlNamespaceUsedByTests);

            List<XmlAttribute> nodes = xmlDocument.SelectNodes(query, ns)?.Cast<XmlAttribute>().ToList();

            nodes.ShouldNotBeNull($"There should be <variable /> elements with a Name attribute {Environment.NewLine}{xmlDocument.OuterXml}");

            nodes.Count.ShouldBe(3, $"There should be 3 <variable /> elements with a Name attribute {Environment.NewLine}{xmlDocument.OuterXml}");

            nodes.ShouldAllBe(i => i.Value.Equals("Mert"), $"All <variable /> elements should have Name=\"Mert\" {Environment.NewLine}{xmlDocument.OuterXml}");
        }

        [Fact]
        public void PokeNoNamespace()
        {
            const string query = "//variable/@Name";
            const string value = "Mert";

            XmlDocument xmlDocument = ExecuteXmlPoke(query: query, value: value);

            List<XmlAttribute> nodes = xmlDocument.SelectNodes(query)?.Cast<XmlAttribute>().ToList();

            nodes.ShouldNotBeNull($"There should be <variable /> elements with a Name attribute {Environment.NewLine}{xmlDocument.OuterXml}");

            nodes.Count.ShouldBe(3, $"There should be 3 <variable /> elements with a Name attribute {Environment.NewLine}{xmlDocument.OuterXml}");

            nodes.ShouldAllBe(i => i.Value.Equals(value), $"All <variable /> elements should have Name=\"{value}\" {Environment.NewLine}{xmlDocument.OuterXml}");
        }

        [Fact]
        public void PokeAttribute()
        {
            const string query = "//class[1]/@AccessModifier";
            const string value = "<Test>Testing</Test>";

            XmlDocument xmlDocument = ExecuteXmlPoke(query: query, value: value);

            List<XmlAttribute> nodes = xmlDocument.SelectNodes(query)?.Cast<XmlAttribute>().ToList();

            nodes.ShouldNotBeNull($"There should be <class /> elements with an AccessModifier attribute {Environment.NewLine}{xmlDocument.OuterXml}");

            nodes.Count.ShouldBe(1, $"There should be 1 <class /> element with an AccessModifier attribute {Environment.NewLine}{xmlDocument.OuterXml}");

            nodes[0].Value.ShouldBe(value);
        }

        [Fact]
        public void PokeChildren()
        {
            const string query = "//class/.";
            const string value = "<Test>Testing</Test>";

            XmlDocument xmlDocument = ExecuteXmlPoke(query: query, value: value);

            List<XmlElement> nodes = xmlDocument.SelectNodes(query)?.Cast<XmlElement>().ToList();

            nodes.ShouldNotBeNull($"There should be <class /> elements {Environment.NewLine}{xmlDocument.OuterXml}");

            nodes.Count.ShouldBe(1, $"There should be 1 <class /> element {Environment.NewLine}{xmlDocument.OuterXml}");

            var testNodes = nodes?.First().ChildNodes.Cast<XmlElement>().ToList();

            testNodes.ShouldNotBeNull($"There should be <class /> elements with one child Test element {Environment.NewLine}{xmlDocument.OuterXml}");

            testNodes.Count.ShouldBe(1, $"There should be 1 <class /> element with one child Test element {Environment.NewLine}{xmlDocument.OuterXml}");

            testNodes[0].InnerText.ShouldBe("Testing");
        }

        [Fact]
        public void PokeAttributeWithCondition()
        {
            const string original = "b";
            const string value = "x";
            const string queryTemplate = "/class/variable[@Name='{0}']/@Name";

            XmlDocument xmlDocument = ExecuteXmlPoke(query: string.Format(queryTemplate, original), value: value);

            List<XmlAttribute> nodes = xmlDocument.SelectNodes(string.Format(queryTemplate, value))?.Cast<XmlAttribute>().ToList();

            nodes.ShouldNotBeNull($"There should be <class /> element with an AccessModifier attribute {Environment.NewLine}{xmlDocument.OuterXml}");

            nodes.Count.ShouldBe(1, $"There should be 1 <class /> element with an AccessModifier attribute {Environment.NewLine}{xmlDocument.OuterXml}");

            nodes[0].Value.ShouldBe(value);
        }

        [Fact]
        public void PokeWithNoParameters()
        {
            MockLogger log = new();
            Project project = ObjectModelHelpers.CreateInMemoryProject(@"<Project><Target Name=""Test""><XmlPoke /></Target></Project>", log);

            project.Build().ShouldBeFalse();
            log.AssertLogContains("MSB4044");
        }

        [Fact]
        public void PokeWithMissingRequiredQuery()
        {
            const string projectContent = @"<Project><Target Name=""Test""><XmlPoke XmlInputPath=""nonesuch"" /></Target></Project>";

            MockLogger log = new();
            Project project = ObjectModelHelpers.CreateInMemoryProject(projectContent, log);

            project.Build().ShouldBeFalse();
            log.AssertLogContains("MSB4044");
            log.AssertLogContains("\"Query\"");
        }

        [Fact]
        public void PokeWithMissingRequiredXmlInputPath()
        {
            const string projectContent = @"<Project><Target Name=""Test""><XmlPoke Query=""nonesuch"" /></Target></Project>";

            MockLogger log = new();
            Project project = ObjectModelHelpers.CreateInMemoryProject(projectContent, log);

            project.Build().ShouldBeFalse();
            log.AssertLogContains("MSB4044");
            log.AssertLogContains("\"XmlInputPath\"");
        }

        [Fact]
        public void PokeWithRequiredParameters()
        {
            MockEngine engine = new(true);
            Prepare(_xmlFileNoNs, out string xmlInputPath);

            XmlPoke task = new()
            {
                BuildEngine = engine,
                XmlInputPath = new TaskItem(xmlInputPath),
                Query = "//variable/@Name",
            };

            task.Execute().ShouldBeTrue();
        }

        [Fact]
        // https://github.com/dotnet/msbuild/issues/5814
        public void XmlPokeWithEmptyValue()
        {
            string xmlInputPath;
            string query = "//class/variable/@Name";
            Prepare(_xmlFileNoNs, out xmlInputPath);
            string projectContents = $"""
                <Project ToolsVersion='msbuilddefaulttoolsversion'>
                <Target Name='Poke'>
                    <XmlPoke Value='' Query='{query}' XmlInputPath='{xmlInputPath}'/>
                </Target>
                </Project>
                """;

            ObjectModelHelpers.BuildProjectExpectSuccess(projectContents);

            string result = File.ReadAllText(xmlInputPath);
            XmlDocument xmlDocument = new XmlDocument();
            xmlDocument.LoadXml(result);
            List<XmlAttribute> nodes = xmlDocument.SelectNodes(query)?.Cast<XmlAttribute>().ToList();
            foreach (var node in nodes)
            {
                node.Value.ShouldBe(string.Empty);
            }
        }

        [Fact]
        public void ErrorInNamespaceDecl()
        {
            MockEngine engine = new MockEngine(true);
            string xmlInputPath;
            Prepare(_xmlFileWithNs, out xmlInputPath);

            XmlPoke p = new XmlPoke();
            p.BuildEngine = engine;
            p.XmlInputPath = new TaskItem(xmlInputPath);
            p.Query = "//s:variable/@Name";
            p.Namespaces = "<!THIS IS ERROR Namespace Prefix=\"s\" Uri=\"http://nsurl\" />";
            p.Namespaces.ShouldBe("<!THIS IS ERROR Namespace Prefix=\"s\" Uri=\"http://nsurl\" />");
            p.Value = new TaskItem("Nur");

            p.Execute().ShouldBeFalse(); // "Execution should've failed"
            engine.AssertLogContains("MSB3731");
        }

        [Fact]
        public void PokeNoNSWPrefixedQueryError()
        {
            MockEngine engine = new MockEngine(true);
            string xmlInputPath;
            Prepare(_xmlFileNoNs, out xmlInputPath);

            XmlPoke p = new XmlPoke();
            p.BuildEngine = engine;

            p.XmlInputPath = new TaskItem(xmlInputPath);
            p.Query = "//s:variable/@Name";
            p.Value = new TaskItem("Nur");
            p.Execute().ShouldBeFalse(); // "Test should've failed"
            engine.AssertLogContains("MSB3732");
        }

        [Fact]
        public void MissingNamespaceParameters()
        {
            MockEngine engine = new MockEngine(true);
            string xmlInputPath;
            Prepare(_xmlFileWithNs, out xmlInputPath);

            string[] attrs = new string[] { "Prefix=\"s\"", "Uri=\"http://nsurl\"" };
            for (int i = 0; i < Math.Pow(2, attrs.Length); i++)
            {
                string res = "";
                for (int k = 0; k < attrs.Length; k++)
                {
                    if ((i & (int)Math.Pow(2, k)) != 0)
                    {
                        res += attrs[k] + " ";
                    }
                }
                XmlPoke p = new XmlPoke();
                p.BuildEngine = engine;
                p.XmlInputPath = new TaskItem(xmlInputPath);
                p.Query = "//s:variable/@Name";
                p.Namespaces = "<Namespace " + res + " />";
                p.Value = new TaskItem("Nur");

                bool result = p.Execute();

                if (i == 3)
                {
                    result.ShouldBeTrue(); // "Only 3rd value should pass."
                }
                else
                {
                    result.ShouldBeFalse(); // "Only 3rd value should pass."
                }
            }
        }

        [Fact]
        public void PokeElement()
        {
            const string query = "//variable/.";
            const string value = "<testing the=\"element\">With<somewhat complex=\"value\" /></testing>";

            XmlDocument xmlDocument = ExecuteXmlPoke(query: query, value: value);

            List<XmlElement> nodes = xmlDocument.SelectNodes(query)?.Cast<XmlElement>().ToList();

            nodes.ShouldNotBeNull($"There should be <variable/> elements {Environment.NewLine}{xmlDocument.OuterXml}");

            nodes.Count.ShouldBe(3, $"There should be 3 <variable/> elements {Environment.NewLine}{xmlDocument.OuterXml}");

            foreach (var node in nodes)
            {
                node.InnerXml.ShouldBe(value);
            }
        }

        [Fact]
        public void PokeWithoutUsingTask()
        {
            string projectContents = @"
<Project ToolsVersion='msbuilddefaulttoolsversion'>
  <Target Name='x'>
    <XmlPoke Value='abc' Query='def' XmlInputPath='ghi.jkl' ContinueOnError='true' />
  </Target>
</Project>";

            // The task will error, but ContinueOnError means that it will just be a warning.
            MockLogger logger = ObjectModelHelpers.BuildProjectExpectSuccess(projectContents);

            // Verify that the task was indeed found.
            logger.AssertLogDoesntContain("MSB4036");
        }

        private static void Prepare(string xmlFile, out string xmlInputPath)
        {
            string dir = Path.Combine(Path.GetTempPath(), DateTime.Now.Ticks.ToString());
            Directory.CreateDirectory(dir);
            xmlInputPath = dir + Path.DirectorySeparatorChar + "doc.xml";
            File.WriteAllText(xmlInputPath, xmlFile);
        }

        /// <summary>
        /// Executes an <see cref="XmlPoke"/> task with the specified arguments.
        /// </summary>
        /// <param name="query">The query to use.</param>
        /// <param name="useNamespace"><code>true</code> to use namespaces, otherwise <code>false</code> (Default).</param>
        /// <param name="value">The value to use.</param>
        /// <returns>An <see cref="XmlDocument"/> containing the resulting XML after the XmlPoke task has executed.</returns>
        private static XmlDocument ExecuteXmlPoke(string query, bool useNamespace = false, string value = null)
        {
            MockEngine engine = new MockEngine(true);

            string xmlInputPath;
            Prepare(useNamespace ? _xmlFileWithNs : _xmlFileNoNs, out xmlInputPath);

            XmlPoke p = new XmlPoke
            {
                BuildEngine = engine,
                XmlInputPath = new TaskItem(xmlInputPath),
                Query = query,
                Namespaces = useNamespace ? $"<Namespace Prefix=\"s\" Uri=\"{XmlNamespaceUsedByTests}\" />" : null,
                Value = value == null ? null : new TaskItem(value)
            };
            Assert.True(p.Execute(), engine.Log);

            string result = File.ReadAllText(xmlInputPath);

            XmlDocument xmlDocument = new XmlDocument();

            xmlDocument.LoadXml(result);

            return xmlDocument;
        }
    }
}
